feat: tx history worker

This commit is contained in:
Rafael Saes 2024-11-01 17:31:26 -03:00
parent 02fabf8594
commit 4a4250a905
13 changed files with 904 additions and 681 deletions

View file

@ -24,7 +24,7 @@ abstract class BaseBitcoinAddressRecord {
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
final String address;
final bool _isHidden;
bool _isHidden;
bool get isHidden => _isHidden;
final bool _isChange;
bool get isChange => _isChange;
@ -46,7 +46,12 @@ abstract class BaseBitcoinAddressRecord {
bool get isUsed => _isUsed;
void setAsUsed() => _isUsed = true;
void setAsUsed() {
_isUsed = true;
// TODO: check is hidden flow on addr list
_isHidden = true;
}
void setNewName(String label) => _name = label;
int get hashCode => address.hashCode;
@ -119,6 +124,26 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'type': type.toString(),
'scriptHash': scriptHash,
});
@override
operator ==(Object other) {
if (identical(this, other)) return true;
return other is BitcoinAddressRecord &&
other.address == address &&
other.index == index &&
other.derivationInfo == derivationInfo &&
other.scriptHash == scriptHash &&
other.type == type;
}
@override
int get hashCode =>
address.hashCode ^
index.hashCode ^
derivationInfo.hashCode ^
scriptHash.hashCode ^
type.hashCode;
}
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {

View file

@ -11,7 +11,7 @@ import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
// import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
@ -240,6 +240,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
);
}
Future<bool> getNodeSupportsSilentPayments() async {
return true;
// As of today (august 2024), only ElectrumRS supports silent payments
// if (!(await getNodeIsElectrs())) {
// return false;
// }
// if (node == null) {
// return false;
// }
// try {
// final tweaksResponse = await electrumClient.getTweaks(height: 0);
// if (tweaksResponse != null) {
// node!.supportsSilentPayments = true;
// node!.save();
// return node!.supportsSilentPayments!;
// }
// } on RequestFailedTimeoutException catch (_) {
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
// } catch (_) {}
// node!.supportsSilentPayments = false;
// node!.save();
// return node!.supportsSilentPayments!;
}
LedgerConnection? _ledgerConnection;
BitcoinLedgerApp? _bitcoinLedgerApp;
@ -327,11 +357,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
_isolate?.then((value) => value.kill(priority: Isolate.immediate));
if (rpc!.isConnected) {
syncStatus = SyncedSyncStatus();
} else {
syncStatus = NotConnectedSyncStatus();
}
// if (rpc!.isConnected) {
// syncStatus = SyncedSyncStatus();
// } else {
// syncStatus = NotConnectedSyncStatus();
// }
}
}
@ -367,7 +397,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return;
}
await updateCoins(unspentCoins);
await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo();
}
@ -449,6 +479,20 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// }
// }
@action
Future<void> registerSilentPaymentsKey() async {
final registered = await electrumClient.tweaksRegister(
secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
labels: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex)
.toList(),
);
print("registered: $registered");
}
@action
void _updateSilentAddressRecord(BitcoinUnspent unspent) {
final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord;
@ -593,41 +637,42 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
@override
@action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
throw UnimplementedError();
// try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
await Future.wait(
BITCOIN_ADDRESS_TYPES.map(
(type) => fetchTransactionsForAddressType(historiesWithDetails, type),
),
);
// await Future.wait(
// BITCOIN_ADDRESS_TYPES.map(
// (type) => fetchTransactionsForAddressType(historiesWithDetails, type),
// ),
// );
transactionHistory.transactions.values.forEach((tx) async {
final isPendingSilentPaymentUtxo =
(tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
// transactionHistory.transactions.values.forEach((tx) async {
// final isPendingSilentPaymentUtxo =
// (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
if (isPendingSilentPaymentUtxo) {
final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
// if (isPendingSilentPaymentUtxo) {
// final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
if (info != null) {
tx.confirmations = info.confirmations;
tx.isPending = tx.confirmations == 0;
transactionHistory.addOne(tx);
await transactionHistory.save();
}
}
});
// if (info != null) {
// tx.confirmations = info.confirmations;
// tx.isPending = tx.confirmations == 0;
// transactionHistory.addOne(tx);
// await transactionHistory.save();
// }
// }
// });
return historiesWithDetails;
} catch (e) {
print("fetchTransactions $e");
return {};
}
// return historiesWithDetails;
// } catch (e) {
// print("fetchTransactions $e");
// return {};
// }
}
@override
@action
Future<void> updateTransactions() async {
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
super.updateTransactions();
transactionHistory.transactions.values.forEach((tx) {
@ -641,32 +686,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
});
}
@action
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
// @action
// Future<ElectrumBalance> fetchBalances() async {
// final balance = await super.fetchBalances();
int totalFrozen = balance.frozen;
int totalConfirmed = balance.confirmed;
// int totalFrozen = balance.frozen;
// int totalConfirmed = balance.confirmed;
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
tx.unspents!.forEach((unspent) {
if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
if (unspent.isFrozen) totalFrozen += unspent.value;
totalConfirmed += unspent.value;
}
});
}
});
// // Add values from unspent coins that are not fetched by the address list
// // i.e. scanned silent payments
// transactionHistory.transactions.values.forEach((tx) {
// if (tx.unspents != null) {
// tx.unspents!.forEach((unspent) {
// if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
// if (unspent.isFrozen) totalFrozen += unspent.value;
// totalConfirmed += unspent.value;
// }
// });
// }
// });
return ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: balance.unconfirmed,
frozen: totalFrozen,
);
}
// return ElectrumBalance(
// confirmed: totalConfirmed,
// unconfirmed: balance.unconfirmed,
// frozen: totalFrozen,
// );
// }
@override
@action
@ -713,15 +758,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
}
@override
@action
void onHeadersResponse(ElectrumHeaderResponse response) {
super.onHeadersResponse(response);
// @override
// @action
// void onHeadersResponse(ElectrumHeaderResponse response) {
// super.onHeadersResponse(response);
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
_setListeners(walletInfo.restoreHeight);
}
}
// if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
// _setListeners(walletInfo.restoreHeight);
// }
// }
@override
@action

View file

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart';
@ -14,7 +13,6 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
@ -172,10 +170,6 @@ class BitcoinWalletService extends WalletService<
@override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;

View file

@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/get_height_by_date.dart';
// import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
@ -35,13 +35,13 @@ import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
// import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:mobx/mobx.dart';
import 'package:http/http.dart' as http;
// import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart';
@ -77,8 +77,8 @@ abstract class ElectrumWalletBase
_isTransactionUpdating = false,
isEnabledAutoGenerateSubaddress = true,
// TODO: inital unspent coins
unspentCoins = ObservableSet(),
scripthashesListening = {},
unspentCoins = BitcoinUnspentCoins(),
scripthashesListening = [],
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
? {
currency: initialBalance ??
@ -107,7 +107,7 @@ abstract class ElectrumWalletBase
}
@action
void _handleWorkerResponse(dynamic message) {
Future<void> _handleWorkerResponse(dynamic message) async {
print('Main: received message: $message');
Map<String, dynamic> messageJson;
@ -146,15 +146,17 @@ abstract class ElectrumWalletBase
break;
case ElectrumRequestMethods.headersSubscribeMethod:
final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson);
onHeadersResponse(response.result);
await onHeadersResponse(response.result);
break;
case ElectrumRequestMethods.getBalanceMethod:
final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson);
onBalanceResponse(response.result);
break;
case ElectrumRequestMethods.getHistoryMethod:
final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson);
onHistoriesResponse(response.result);
break;
// case 'fetchBalances':
// final balance = ElectrumBalance.fromJSON(
// jsonDecode(workerResponse.data.toString()).toString(),
// );
// Update the balance state
// this.balance[currency] = balance!;
// break;
}
}
@ -219,8 +221,6 @@ abstract class ElectrumWalletBase
bool isEnabledAutoGenerateSubaddress;
late ElectrumClient electrumClient;
ElectrumApiProvider? electrumClient2;
BitcoinBaseElectrumRPCService? get rpc => electrumClient2?.rpc;
ApiProvider? apiProvider;
Box<UnspentCoinsInfo> unspentCoinsInfo;
@ -235,10 +235,10 @@ abstract class ElectrumWalletBase
@observable
SyncStatus syncStatus;
Set<String> get addressesSet => walletAddresses.allAddresses
List<String> get addressesSet => walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.map((addr) => addr.address)
.toSet();
.toList();
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
@ -288,14 +288,14 @@ abstract class ElectrumWalletBase
);
String _password;
ObservableSet<BitcoinUnspent> unspentCoins;
BitcoinUnspentCoins unspentCoins;
@observable
TransactionPriorities? feeRates;
int feeRate(TransactionPriority priority) => feeRates![priority];
@observable
Set<String> scripthashesListening;
List<String> scripthashesListening;
bool _chainTipListenerOn = false;
bool _isTransactionUpdating;
@ -323,16 +323,22 @@ abstract class ElectrumWalletBase
syncStatus = SynchronizingSyncStatus();
// INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero
await subscribeForHeaders();
await subscribeForUpdates();
// await updateTransactions();
// INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later.
await updateTransactions();
// await updateAllUnspents();
// await updateBalance();
// INFO: THIRD: Start loading the TX history
await updateBalance();
// await subscribeForUpdates();
// await updateFeeRates();
_updateFeeRateTimer ??=
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
// _updateFeeRateTimer ??=
// Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
syncStatus = SyncedSyncStatus();
@ -344,20 +350,6 @@ abstract class ElectrumWalletBase
}
}
@action
Future<void> registerSilentPaymentsKey() async {
final registered = await electrumClient.tweaksRegister(
secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
labels: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
.map((addr) => addr.labelIndex)
.toList(),
);
print("registered: $registered");
}
@action
void callError(FlutterErrorDetails error) {
_onError?.call(error);
@ -366,9 +358,9 @@ abstract class ElectrumWalletBase
@action
Future<void> updateFeeRates() async {
try {
feeRates = BitcoinElectrumTransactionPriorities.fromList(
await electrumClient2!.getFeeRates(),
);
// feeRates = BitcoinElectrumTransactionPriorities.fromList(
// await electrumClient2!.getFeeRates(),
// );
} catch (e, stacktrace) {
// _onError?.call(FlutterErrorDetails(
// exception: e,
@ -403,36 +395,6 @@ abstract class ElectrumWalletBase
return node!.isElectrs!;
}
Future<bool> getNodeSupportsSilentPayments() async {
return true;
// As of today (august 2024), only ElectrumRS supports silent payments
if (!(await getNodeIsElectrs())) {
return false;
}
if (node == null) {
return false;
}
try {
final tweaksResponse = await electrumClient.getTweaks(height: 0);
if (tweaksResponse != null) {
node!.supportsSilentPayments = true;
node!.save();
return node!.supportsSilentPayments!;
}
} on RequestFailedTimeoutException catch (_) {
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
} catch (_) {}
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
}
@action
@override
Future<void> connectToNode({required Node node}) async {
@ -1176,7 +1138,7 @@ abstract class ElectrumWalletBase
final path = await makePath();
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
// await transactionHistory.save();
await transactionHistory.save();
}
@override
@ -1226,28 +1188,23 @@ abstract class ElectrumWalletBase
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
// Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.forEach((addr) {
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
Set<String> scripthashes = {};
walletAddresses.allAddresses.forEach((addressRecord) {
scripthashes.add(addressRecord.scriptHash);
});
workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(),
);
await Future.wait(walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
unspentCoins.addAll(updatedUnspentCoins);
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
}
await updateCoins(unspentCoins);
// await refreshUnspentCoinsInfo();
await updateCoins(unspentCoins.toSet());
await refreshUnspentCoinsInfo();
}
@action
@ -1294,18 +1251,17 @@ abstract class ElectrumWalletBase
@action
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
List<Map<String, dynamic>> unspents = [];
List<BitcoinUnspent> updatedUnspentCoins = [];
final unspents = await electrumClient2!.request(
ElectrumScriptHashListUnspent(scriptHash: address.scriptHash),
);
unspents = await electrumClient.getListUnspent(address.scriptHash);
await Future.wait(unspents.map((unspent) async {
try {
final coin = BitcoinUnspent.fromUTXO(address, unspent);
final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isChange;
coin.confirmations = tx?.confirmations;
final coin = BitcoinUnspent.fromJSON(address, unspent);
// final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isHidden;
// coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin);
} catch (_) {}
@ -1332,6 +1288,7 @@ abstract class ElectrumWalletBase
await unspentCoinsInfo.add(newInfo);
}
// TODO: ?
Future<void> refreshUnspentCoinsInfo() async {
try {
final List<dynamic> keys = <dynamic>[];
@ -1415,7 +1372,7 @@ abstract class ElectrumWalletBase
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
allInputsAmount += outTransaction.amount.toInt();
// allInputsAmount += outTransaction.amount.toInt();
final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
@ -1565,72 +1522,15 @@ abstract class ElectrumWalletBase
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
int? time;
int? height;
final transactionHex = await electrumClient2!.request(
ElectrumGetTransactionHex(transactionHash: hash),
);
// TODO:
// if (mempoolAPIEnabled) {
if (true) {
try {
final txVerbose = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} catch (_) {}
}
final transactionHex = await electrumClient.getTransactionHex(hash: hash);
int? confirmations;
if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
final tip = currentChainTip!;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
final inputTransactionHex = await electrumClient2!.request(
ElectrumGetTransactionHex(transactionHash: vin.txId),
);
final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
@ -1643,207 +1543,62 @@ abstract class ElectrumWalletBase
);
}
Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash),
walletInfo.type,
network,
addresses: addressesSet,
height: height,
);
} catch (e, s) {
print([e, s]);
return null;
}
}
@override
@action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
if (type == WalletType.bitcoinCash) {
await Future.wait(BITCOIN_CASH_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.litecoin) {
await Future.wait(LITECOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
}
return historiesWithDetails;
} catch (e) {
print("fetchTransactions $e");
return {};
}
}
Future<void> fetchTransactionsForAddressType(
Map<String, ElectrumTransactionInfo> historiesWithDetails,
BitcoinAddressType type,
) async {
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
await Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord);
if (history.isNotEmpty) {
historiesWithDetails.addAll(history);
}
}));
}
Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
BitcoinAddressRecord addressRecord,
) async {
String txid = "";
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final history = await electrumClient2!.request(ElectrumScriptHashGetHistory(
scriptHash: addressRecord.scriptHash,
));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
addressRecord.txCount = history.length;
await Future.wait(history.map((transaction) async {
txid = transaction['tx_hash'] as String;
final height = transaction['height'] as int;
final storedTx = transactionHistory.transactions[txid];
if (storedTx != null) {
if (height > 0) {
storedTx.height = height;
// the tx's block itself is the first confirmation so add 1
if ((currentChainTip ?? 0) > 0) {
storedTx.confirmations = currentChainTip! - height + 1;
}
storedTx.isPending = storedTx.confirmations == 0;
}
historiesWithDetails[txid] = storedTx;
} else {
final tx = await fetchTransactionInfo(hash: txid, height: height);
if (tx != null) {
historiesWithDetails[txid] = tx;
// Got a new transaction fetched, add it to the transaction history
// instead of waiting all to finish, and next time it will be faster
transactionHistory.addOne(tx);
}
}
return Future.value(null);
}));
final totalAddresses = (addressRecord.isChange
? walletAddresses.changeAddresses
.where((addr) => addr.type == addressRecord.type)
.length
: walletAddresses.receiveAddresses
.where((addr) => addr.type == addressRecord.type)
.length);
final gapLimit = (addressRecord.isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
final isUsedAddressUnderGap = addressRecord.index < totalAddresses &&
(addressRecord.index >= totalAddresses - gapLimit);
if (isUsedAddressUnderGap) {
// Discover new addresses for the same address type until the gap limit is respected
await walletAddresses.discoverAddresses(
isChange: addressRecord.isChange,
gap: gapLimit,
type: addressRecord.type,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.type),
);
}
}
return historiesWithDetails;
} catch (e, stacktrace) {
_onError?.call(FlutterErrorDetails(
exception: "$txid - $e",
stack: stacktrace,
library: this.runtimeType.toString(),
));
return {};
}
throw UnimplementedError();
}
@action
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
// TODO: all
addresses ??= walletAddresses.allAddresses
.where(
(element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false,
)
.toList();
_isTransactionUpdating = true;
await fetchTransactions();
walletAddresses.updateReceiveAddresses();
_isTransactionUpdating = false;
} catch (e, stacktrace) {
print(stacktrace);
print(e);
_isTransactionUpdating = false;
}
workerSendPort!.send(
ElectrumWorkerGetHistoryRequest(
addresses: addresses,
storedTxs: transactionHistory.transactions.values.toList(),
walletType: type,
// If we still don't have currentChainTip, txs will still be fetched but shown
// with confirmations as 0 but will be auto fixed on onHeadersResponse
chainTip: currentChainTip ?? 0,
network: network,
// mempoolAPIEnabled: mempoolAPIEnabled,
// TODO:
mempoolAPIEnabled: true,
).toJson(),
);
}
@action
Future<void> subscribeForUpdates([
Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
]) async {
unsubscribedScriptHashes ??= walletAddresses.allAddresses.where(
(address) => !scripthashesListening.contains(address.scriptHash),
Future<void> subscribeForUpdates([Iterable<String>? unsubscribedScriptHashes]) async {
unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where(
(sh) => !scripthashesListening.contains(sh),
);
Map<String, String> scripthashByAddress = {};
List<String> scriptHashesList = [];
walletAddresses.allAddresses.forEach((addressRecord) {
scripthashByAddress[addressRecord.address] = addressRecord.scriptHash;
scriptHashesList.add(addressRecord.scriptHash);
});
workerSendPort!.send(
ElectrumWorkerScripthashesSubscribeRequest(scripthashByAddress: scripthashByAddress).toJson(),
ElectrumWorkerScripthashesSubscribeRequest(
scripthashByAddress: scripthashByAddress,
).toJson(),
);
scripthashesListening.addAll(scriptHashesList);
}
@action
Future<ElectrumBalance> fetchBalances() async {
var totalFrozen = 0;
var totalConfirmed = 0;
var totalUnconfirmed = 0;
unspentCoins.forEach((element) {
if (element.isFrozen) {
totalFrozen += element.value;
}
if (element.confirmations == 0) {
totalUnconfirmed += element.value;
} else {
totalConfirmed += element.value;
}
});
return ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
scripthashesListening.addAll(scripthashByAddress.values);
}
@action
Future<void> updateBalance() async {
balance[currency] = await fetchBalances();
workerSendPort!.send(
ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(),
);
}
@override
@ -1925,12 +1680,102 @@ abstract class ElectrumWalletBase
}
@action
void onHeadersResponse(ElectrumHeaderResponse response) {
Future<void> onHistoriesResponse(List<AddressHistoriesResponse> histories) async {
final firstAddress = histories.first;
final isChange = firstAddress.addressRecord.isChange;
final type = firstAddress.addressRecord.type;
final totalAddresses = histories.length;
final gapLimit = (isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
bool hasUsedAddressesUnderGap = false;
final addressesWithHistory = <BitcoinAddressRecord>[];
for (final addressHistory in histories) {
final txs = addressHistory.txs;
if (txs.isNotEmpty) {
final address = addressHistory.addressRecord;
addressesWithHistory.add(address);
hasUsedAddressesUnderGap =
address.index < totalAddresses && (address.index >= totalAddresses - gapLimit);
for (final tx in txs) {
transactionHistory.addOne(tx);
}
}
}
if (addressesWithHistory.isNotEmpty) {
walletAddresses.updateAdresses(addressesWithHistory);
}
if (hasUsedAddressesUnderGap) {
// Discover new addresses for the same address type until the gap limit is respected
final newAddresses = await walletAddresses.discoverAddresses(
isChange: isChange,
gap: gapLimit,
type: type,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(type),
);
if (newAddresses.isNotEmpty) {
// Update the transactions for the new discovered addresses
await updateTransactions(newAddresses);
}
}
}
@action
void onBalanceResponse(ElectrumBalance balanceResult) {
var totalFrozen = 0;
var totalConfirmed = balanceResult.confirmed;
var totalUnconfirmed = balanceResult.unconfirmed;
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
if (unspentCoinInfo.isFrozen) {
// TODO: verify this works well
totalFrozen += unspentCoinInfo.value;
totalConfirmed -= unspentCoinInfo.value;
totalUnconfirmed -= unspentCoinInfo.value;
}
});
balance[currency] = ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: totalFrozen,
);
}
@action
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
currentChainTip = response.height;
bool updated = false;
transactionHistory.transactions.values.forEach((tx) {
if (tx.height != null && tx.height! > 0) {
final newConfirmations = currentChainTip! - tx.height! + 1;
if (tx.confirmations != newConfirmations) {
tx.confirmations = newConfirmations;
tx.isPending = tx.confirmations == 0;
updated = true;
}
}
});
if (updated) {
await save();
}
}
@action
Future<void> subscribeForHeaders() async {
print(_chainTipListenerOn);
if (_chainTipListenerOn) return;
workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson());
@ -1970,12 +1815,17 @@ abstract class ElectrumWalletBase
@action
void syncStatusReaction(SyncStatus syncStatus) {
if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) {
final isDisconnectedStatus =
syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus;
if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) {
// Needs to re-subscribe to all scripthashes when reconnected
scripthashesListening = {};
scripthashesListening = [];
_isTransactionUpdating = false;
_chainTipListenerOn = false;
}
if (isDisconnectedStatus) {
if (_isTryingToConnect) return;
_isTryingToConnect = true;
@ -1985,10 +1835,7 @@ abstract class ElectrumWalletBase
this.syncStatus is LostConnectionSyncStatus) {
if (node == null) return;
this.electrumClient.connectToUri(
node!.uri,
useSSL: node!.useSSL ?? false,
);
connectToNode(node: this.node!);
}
_isTryingToConnect = false;
});
@ -2102,3 +1949,35 @@ class TxCreateUtxoDetails {
required this.spendsUnconfirmedTX,
});
}
class BitcoinUnspentCoins extends ObservableList<BitcoinUnspent> {
BitcoinUnspentCoins() : super();
List<UnspentCoinsInfo> forInfo(Iterable<UnspentCoinsInfo> unspentCoinsInfo) {
return unspentCoinsInfo.where((element) {
final info = this.firstWhereOrNull(
(info) =>
element.hash == info.hash &&
element.vout == info.vout &&
element.address == info.bitcoinAddressRecord.address &&
element.value == info.value,
);
return info != null;
}).toList();
}
List<BitcoinUnspent> fromInfo(Iterable<UnspentCoinsInfo> unspentCoinsInfo) {
return this.where((element) {
final info = unspentCoinsInfo.firstWhereOrNull(
(info) =>
element.hash == info.hash &&
element.vout == info.vout &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value,
);
return info != null;
}).toList();
}
}

View file

@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
int initialSilentAddressIndex = 0,
List<BitcoinAddressRecord>? initialMwebAddresses,
BitcoinAddressType? initialAddressPageType,
}) : _allAddresses = ObservableSet.of(initialAddresses ?? []),
}) : _allAddresses = ObservableList.of(initialAddresses ?? []),
addressesByReceiveType =
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
@ -89,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17;
static const gap = 20;
final ObservableSet<BitcoinAddressRecord> _allAddresses;
final ObservableList<BitcoinAddressRecord> _allAddresses;
final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses;
@ -116,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed
List<BitcoinAddressRecord> get allAddresses => _allAddresses.toList();
@computed
Set<String> get allScriptHashes =>
_allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet();
BitcoinAddressRecord getFromAddresses(String address) {
return _allAddresses.firstWhere((element) => element.address == address);
}
@ -629,6 +633,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return list;
}
@action
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
for (final address in addresses) {
_allAddresses.replaceRange(address.index, address.index + 1, [address]);
}
}
@action
void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
this._allAddresses.addAll(addresses);

View file

@ -1,136 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
// import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
class ElectrumWorker {
final SendPort sendPort;
ElectrumApiProvider? _electrumClient;
ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient})
: _electrumClient = electrumClient;
static void run(SendPort sendPort) {
final worker = ElectrumWorker._(sendPort);
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen(worker.handleMessage);
}
void _sendResponse(ElectrumWorkerResponse response) {
sendPort.send(jsonEncode(response.toJson()));
}
void _sendError(ElectrumWorkerErrorResponse response) {
sendPort.send(jsonEncode(response.toJson()));
}
void handleMessage(dynamic message) async {
print("Worker: received message: $message");
try {
Map<String, dynamic> messageJson;
if (message is String) {
messageJson = jsonDecode(message) as Map<String, dynamic>;
} else {
messageJson = message as Map<String, dynamic>;
}
final workerMethod = messageJson['method'] as String;
switch (workerMethod) {
case ElectrumWorkerMethods.connectionMethod:
await _handleConnect(ElectrumWorkerConnectRequest.fromJson(messageJson));
break;
// case 'blockchain.headers.subscribe':
// await _handleHeadersSubscribe();
// break;
// case 'blockchain.scripthash.get_balance':
// await _handleGetBalance(message);
// break;
case 'blockchain.scripthash.get_history':
// await _handleGetHistory(workerMessage);
break;
case 'blockchain.scripthash.listunspent':
// await _handleListUnspent(workerMessage);
break;
// Add other method handlers here
// default:
// _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}');
}
} catch (e, s) {
print(s);
_sendError(ElectrumWorkerErrorResponse(error: e.toString()));
}
}
Future<void> _handleConnect(ElectrumWorkerConnectRequest request) async {
_electrumClient = ElectrumApiProvider(
await ElectrumTCPService.connect(
request.uri,
onConnectionStatusChange: (status) {
_sendResponse(ElectrumWorkerConnectResponse(status: status.toString()));
},
defaultRequestTimeOut: const Duration(seconds: 5),
connectionTimeOut: const Duration(seconds: 5),
),
);
}
// Future<void> _handleHeadersSubscribe() async {
// final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
// if (listener == null) {
// _sendError('blockchain.headers.subscribe', 'Failed to subscribe');
// return;
// }
// listener((event) {
// _sendResponse('blockchain.headers.subscribe', event);
// });
// }
// Future<void> _handleGetBalance(ElectrumWorkerRequest message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scriptHash),
// );
// final balance = ElectrumBalance(
// confirmed: result['confirmed'] as int? ?? 0,
// unconfirmed: result['unconfirmed'] as int? ?? 0,
// frozen: 0,
// );
// _sendResponse(message.method, balance.toJSON());
// } catch (e, s) {
// print(s);
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleGetHistory(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.getHistory(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
// Future<void> _handleListUnspent(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.listUnspent(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
}

View file

@ -3,10 +3,16 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
// import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:http/http.dart' as http;
// TODO: ping
class ElectrumWorker {
final SendPort sendPort;
@ -58,11 +64,15 @@ class ElectrumWorker {
ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson),
);
break;
// case 'blockchain.scripthash.get_balance':
// await _handleGetBalance(message);
// break;
case 'blockchain.scripthash.get_history':
// await _handleGetHistory(workerMessage);
case ElectrumRequestMethods.getBalanceMethod:
await _handleGetBalance(
ElectrumWorkerGetBalanceRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.getHistoryMethod:
await _handleGetHistory(
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
);
break;
case 'blockchain.scripthash.listunspent':
// await _handleListUnspent(workerMessage);
@ -108,6 +118,7 @@ class ElectrumWorker {
await Future.wait(request.scripthashByAddress.entries.map((entry) async {
final address = entry.key;
final scripthash = entry.value;
final listener = await _electrumClient!.subscribe(
ElectrumScriptHashSubscribe(scriptHash: scripthash),
);
@ -129,43 +140,214 @@ class ElectrumWorker {
}));
}
// Future<void> _handleGetBalance(ElectrumWorkerRequest message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scriptHash),
// );
Future<void> _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async {
final Map<String, AddressHistoriesResponse> histories = {};
final addresses = result.addresses;
// final balance = ElectrumBalance(
// confirmed: result['confirmed'] as int? ?? 0,
// unconfirmed: result['unconfirmed'] as int? ?? 0,
await Future.wait(addresses.map((addressRecord) async {
final history = await _electrumClient!.request(ElectrumScriptHashGetHistory(
scriptHash: addressRecord.scriptHash,
));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
addressRecord.txCount = history.length;
await Future.wait(history.map((transaction) async {
final txid = transaction['tx_hash'] as String;
final height = transaction['height'] as int;
late ElectrumTransactionInfo tx;
try {
// Exception thrown on null
tx = result.storedTxs.firstWhere((tx) => tx.id == txid);
if (height > 0) {
tx.height = height;
// the tx's block itself is the first confirmation so add 1
tx.confirmations = result.chainTip - height + 1;
tx.isPending = tx.confirmations == 0;
}
} catch (_) {
tx = ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(
hash: txid,
currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled,
),
result.walletType,
result.network,
addresses: result.addresses.map((addr) => addr.address).toSet(),
height: height,
);
}
final addressHistories = histories[addressRecord.address];
if (addressHistories != null) {
addressHistories.txs.add(tx);
} else {
histories[addressRecord.address] = AddressHistoriesResponse(
addressRecord: addressRecord,
txs: [tx],
walletType: result.walletType,
);
}
return Future.value(null);
}));
}
return histories;
}));
_sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList()));
}
Future<ElectrumTransactionBundle> getTransactionExpanded({
required String hash,
required int currentChainTip,
required bool mempoolAPIEnabled,
bool getConfirmations = true,
}) async {
int? time;
int? height;
int? confirmations;
final transactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: hash),
);
if (getConfirmations) {
if (mempoolAPIEnabled) {
try {
final txVerbose = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} catch (_) {}
}
if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
final tip = currentChainTip;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
}
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
final inputTransactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: vin.txId),
);
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
return ElectrumTransactionBundle(
original,
ins: ins,
time: time,
confirmations: confirmations ?? 0,
);
}
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[];
// for (final scripthash in request.scripthashes) {
// final balanceFuture = _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
// );
// balanceFutures.add(balanceFuture);
// }
// var totalConfirmed = 0;
// var totalUnconfirmed = 0;
// final balances = await Future.wait(balanceFutures);
// for (final balance in balances) {
// final confirmed = balance['confirmed'] as int? ?? 0;
// final unconfirmed = balance['unconfirmed'] as int? ?? 0;
// totalConfirmed += confirmed;
// totalUnconfirmed += unconfirmed;
// }
// _sendResponse(ElectrumWorkerGetBalanceResponse(
// result: ElectrumBalance(
// confirmed: totalConfirmed,
// unconfirmed: totalUnconfirmed,
// frozen: 0,
// );
// _sendResponse(message.method, balance.toJSON());
// } catch (e, s) {
// print(s);
// _sendError(message.method, e.toString());
// }
// ),
// ));
// }
// Future<void> _handleGetHistory(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.getHistory(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
final balanceFutures = <Future<Map<String, dynamic>>>[];
// Future<void> _handleListUnspent(ElectrumWorkerMessage message) async {
// try {
// final scriptHash = message.params['scriptHash'] as String;
// final result = await electrumClient.listUnspent(scriptHash);
// _sendResponse(message.method, jsonEncode(result));
// } catch (e) {
// _sendError(message.method, e.toString());
// }
// }
for (final scripthash in request.scripthashes) {
final balanceFuture = _electrumClient!.request(
ElectrumGetScriptHashBalance(scriptHash: scripthash),
);
balanceFutures.add(balanceFuture);
}
var totalConfirmed = 0;
var totalUnconfirmed = 0;
final balances = await Future.wait(balanceFutures);
for (final balance in balances) {
final confirmed = balance['confirmed'] as int? ?? 0;
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
totalConfirmed += confirmed;
totalUnconfirmed += unconfirmed;
}
_sendResponse(ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: 0,
),
));
}
}

View file

@ -0,0 +1,52 @@
part of 'methods.dart';
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetBalanceRequest({required this.scripthashes});
final Set<String> scripthashes;
@override
final String method = ElectrumRequestMethods.getBalance.method;
@override
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceRequest(
scripthashes: (json['scripthashes'] as List<String>).toSet(),
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scripthashes': scripthashes.toList()};
}
}
class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetBalanceError({required String error}) : super(error: error);
@override
final String method = ElectrumRequestMethods.getBalance.method;
}
class ElectrumWorkerGetBalanceResponse
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
ElectrumWorkerGetBalanceResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getBalance.method);
@override
Map<String, int>? resultJson(result) {
return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
}
@override
factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: json['result']['confirmed'] as int,
unconfirmed: json['result']['unconfirmed'] as int,
frozen: 0,
),
error: json['error'] as String?,
);
}
}

View file

@ -0,0 +1,110 @@
part of 'methods.dart';
class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetHistoryRequest({
required this.addresses,
required this.storedTxs,
required this.walletType,
required this.chainTip,
required this.network,
required this.mempoolAPIEnabled,
});
final List<BitcoinAddressRecord> addresses;
final List<ElectrumTransactionInfo> storedTxs;
final WalletType walletType;
final int chainTip;
final BasedUtxoNetwork network;
final bool mempoolAPIEnabled;
@override
final String method = ElectrumRequestMethods.getHistory.method;
@override
factory ElectrumWorkerGetHistoryRequest.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return ElectrumWorkerGetHistoryRequest(
addresses: (json['addresses'] as List)
.map((e) => BitcoinAddressRecord.fromJSON(e as String))
.toList(),
storedTxs: (json['storedTxIds'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
chainTip: json['chainTip'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
);
}
@override
Map<String, dynamic> toJson() {
return {
'method': method,
'addresses': addresses.map((e) => e.toJSON()).toList(),
'storedTxIds': storedTxs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
'chainTip': chainTip,
'network': network.value,
'mempoolAPIEnabled': mempoolAPIEnabled,
};
}
}
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetHistoryError({required String error}) : super(error: error);
@override
final String method = ElectrumRequestMethods.getHistory.method;
}
class AddressHistoriesResponse {
final BitcoinAddressRecord addressRecord;
final List<ElectrumTransactionInfo> txs;
final WalletType walletType;
AddressHistoriesResponse(
{required this.addressRecord, required this.txs, required this.walletType});
factory AddressHistoriesResponse.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return AddressHistoriesResponse(
addressRecord: BitcoinAddressRecord.fromJSON(json['address'] as String),
txs: (json['txs'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
);
}
Map<String, dynamic> toJson() {
return {
'address': addressRecord.toJSON(),
'txs': txs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
};
}
}
class ElectrumWorkerGetHistoryResponse
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
ElectrumWorkerGetHistoryResponse({required super.result, super.error})
: super(method: ElectrumRequestMethods.getHistory.method);
@override
List<Map<String, dynamic>> resultJson(result) {
return result.map((e) => e.toJson()).toList();
}
@override
factory ElectrumWorkerGetHistoryResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetHistoryResponse(
result: (json['result'] as List)
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
.toList(),
error: json['error'] as String?,
);
}
}

View file

@ -0,0 +1,53 @@
// part of 'methods.dart';
// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
// ElectrumWorkerGetBalanceRequest({required this.scripthashes});
// final Set<String> scripthashes;
// @override
// final String method = ElectrumRequestMethods.getBalance.method;
// @override
// factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
// return ElectrumWorkerGetBalanceRequest(
// scripthashes: (json['scripthashes'] as List<String>).toSet(),
// );
// }
// @override
// Map<String, dynamic> toJson() {
// return {'method': method, 'scripthashes': scripthashes.toList()};
// }
// }
// class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
// ElectrumWorkerGetBalanceError({required String error}) : super(error: error);
// @override
// final String method = ElectrumRequestMethods.getBalance.method;
// }
// class ElectrumWorkerGetBalanceResponse
// extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
// ElectrumWorkerGetBalanceResponse({required super.result, super.error})
// : super(method: ElectrumRequestMethods.getBalance.method);
// @override
// Map<String, int>? resultJson(result) {
// return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
// }
// @override
// factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
// return ElectrumWorkerGetBalanceResponse(
// result: ElectrumBalance(
// confirmed: json['result']['confirmed'] as int,
// unconfirmed: json['result']['unconfirmed'] as int,
// frozen: 0,
// ),
// error: json['error'] as String?,
// );
// }
// }

View file

@ -1,6 +1,13 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
part 'connection.dart';
part 'headers_subscribe.dart';
part 'scripthashes_subscribe.dart';
part 'get_balance.dart';
part 'get_history.dart';

View file

@ -774,113 +774,114 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
@action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
throw UnimplementedError();
// try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
await Future.wait(LITECOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
// await Future.wait(LITECOIN_ADDRESS_TYPES
// .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
return historiesWithDetails;
} catch (e) {
print("fetchTransactions $e");
return {};
}
// return historiesWithDetails;
// } catch (e) {
// print("fetchTransactions $e");
// return {};
// }
}
@override
@action
Future<void> subscribeForUpdates([
Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
]) async {
final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
(address) =>
!scripthashesListening.contains(address.scriptHash) &&
address.type != SegwitAddresType.mweb,
);
// @override
// @action
// Future<void> subscribeForUpdates([
// Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
// ]) async {
// final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
// (address) =>
// !scripthashesListening.contains(address.scriptHash) &&
// address.type != SegwitAddresType.mweb,
// );
return super.subscribeForUpdates(unsubscribedScriptHashes);
}
// return super.subscribeForUpdates(unsubscribedScriptHashes);
// }
@override
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
// @override
// Future<ElectrumBalance> fetchBalances() async {
// final balance = await super.fetchBalances();
if (!mwebEnabled) {
return balance;
}
// if (!mwebEnabled) {
// return balance;
// }
// update unspent balances:
await updateUnspent();
// // update unspent balances:
// await updateUnspent();
int confirmed = balance.confirmed;
int unconfirmed = balance.unconfirmed;
int confirmedMweb = 0;
int unconfirmedMweb = 0;
try {
mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) {
confirmedMweb += utxo.value.toInt();
} else {
unconfirmedMweb += utxo.value.toInt();
}
});
if (unconfirmedMweb > 0) {
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
}
} catch (_) {}
// int confirmed = balance.confirmed;
// int unconfirmed = balance.unconfirmed;
// int confirmedMweb = 0;
// int unconfirmedMweb = 0;
// try {
// mwebUtxosBox.values.forEach((utxo) {
// if (utxo.height > 0) {
// confirmedMweb += utxo.value.toInt();
// } else {
// unconfirmedMweb += utxo.value.toInt();
// }
// });
// if (unconfirmedMweb > 0) {
// unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
// }
// } catch (_) {}
for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0;
addressRecord.txCount = 0;
}
// for (var addressRecord in walletAddresses.allAddresses) {
// addressRecord.balance = 0;
// addressRecord.txCount = 0;
// }
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
// unspentCoins.forEach((coin) {
// final coinInfoList = unspentCoinsInfo.values.where(
// (element) =>
// element.walletId.contains(id) &&
// element.hash.contains(coin.hash) &&
// element.vout == coin.vout,
// );
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
// if (coinInfoList.isNotEmpty) {
// final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
super.addCoinInfo(coin);
}
});
// coin.isFrozen = coinInfo.isFrozen;
// coin.isSending = coinInfo.isSending;
// coin.note = coinInfo.note;
// if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
// coin.bitcoinAddressRecord.balance += coinInfo.value;
// } else {
// super.addCoinInfo(coin);
// }
// });
// update the txCount for each address using the tx history, since we can't rely on mwebd
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
for (final tx in transactionHistory.transactions.values) {
// if (tx.isPending) continue;
if (tx.inputAddresses == null || tx.outputAddresses == null) {
continue;
}
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
for (final address in txAddresses) {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
if (addressRecord == null) {
continue;
}
addressRecord.txCount++;
}
}
// // update the txCount for each address using the tx history, since we can't rely on mwebd
// // to have an accurate count, we should just keep it in sync with what we know from the tx history:
// for (final tx in transactionHistory.transactions.values) {
// // if (tx.isPending) continue;
// if (tx.inputAddresses == null || tx.outputAddresses == null) {
// continue;
// }
// final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
// for (final address in txAddresses) {
// final addressRecord = walletAddresses.allAddresses
// .firstWhereOrNull((addressRecord) => addressRecord.address == address);
// if (addressRecord == null) {
// continue;
// }
// addressRecord.txCount++;
// }
// }
return ElectrumBalance(
confirmed: confirmed,
unconfirmed: unconfirmed,
frozen: balance.frozen,
secondConfirmed: confirmedMweb,
secondUnconfirmed: unconfirmedMweb,
);
}
// return ElectrumBalance(
// confirmed: confirmed,
// unconfirmed: unconfirmed,
// frozen: balance.frozen,
// secondConfirmed: confirmedMweb,
// secondUnconfirmed: unconfirmedMweb,
// );
// }
@override
int feeRate(TransactionPriority priority) {

View file

@ -603,7 +603,7 @@ class CWBitcoin extends Bitcoin {
@override
Future<void> registerSilentPaymentsKey(Object wallet, bool active) async {
final bitcoinWallet = wallet as ElectrumWallet;
final bitcoinWallet = wallet as BitcoinWallet;
return await bitcoinWallet.registerSilentPaymentsKey();
}
@ -634,7 +634,7 @@ class CWBitcoin extends Bitcoin {
@override
Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
final bitcoinWallet = wallet as BitcoinWallet;
return bitcoinWallet.getNodeSupportsSilentPayments();
}