diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 72ca4b23e..c90e7d65d 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -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 { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ec2384a08..3ad83b54f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -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 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 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> fetchTransactions() async { - try { - final Map historiesWithDetails = {}; + throw UnimplementedError(); + // try { + // final Map 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 updateTransactions() async { + Future updateTransactions([List? addresses]) async { super.updateTransactions(); transactionHistory.transactions.values.forEach((tx) { @@ -641,32 +686,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }); } - @action - Future fetchBalances() async { - final balance = await super.fetchBalances(); + // @action + // Future 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 diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index a38452329..941c25265 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -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 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; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e95416b63..a7745c205 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -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.of(currency != null ? { currency: initialBalance ?? @@ -107,7 +107,7 @@ abstract class ElectrumWalletBase } @action - void _handleWorkerResponse(dynamic message) { + Future _handleWorkerResponse(dynamic message) async { print('Main: received message: $message'); Map 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; @@ -235,10 +235,10 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses + List get addressesSet => walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .map((addr) => addr.address) - .toSet(); + .toList(); List get scriptHashes => walletAddresses.addressesByReceiveType .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) @@ -288,14 +288,14 @@ abstract class ElectrumWalletBase ); String _password; - ObservableSet unspentCoins; + BitcoinUnspentCoins unspentCoins; @observable TransactionPriorities? feeRates; int feeRate(TransactionPriority priority) => feeRates![priority]; @observable - Set scripthashesListening; + List 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 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 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 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 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 updateAllUnspents() async { List 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 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> fetchUnspent(BitcoinAddressRecord address) async { + List> unspents = []; List 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 refreshUnspentCoinsInfo() async { try { final List keys = []; @@ -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 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 = []; 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 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> fetchTransactions() async { - try { - final Map 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 fetchTransactionsForAddressType( - Map 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> _fetchAddressHistory( - BitcoinAddressRecord addressRecord, - ) async { - String txid = ""; - - try { - final Map 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 updateTransactions() async { - try { - if (_isTransactionUpdating) { - return; - } + Future updateTransactions([List? 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 subscribeForUpdates([ - Iterable? unsubscribedScriptHashes, - ]) async { - unsubscribedScriptHashes ??= walletAddresses.allAddresses.where( - (address) => !scripthashesListening.contains(address.scriptHash), + Future subscribeForUpdates([Iterable? unsubscribedScriptHashes]) async { + unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where( + (sh) => !scripthashesListening.contains(sh), ); Map scripthashByAddress = {}; - List 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 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 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 onHistoriesResponse(List 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 = []; + + 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 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 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 { + BitcoinUnspentCoins() : super(); + + List forInfo(Iterable 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 fromInfo(Iterable 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(); + } +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 81ed23d28..468947c15 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { int initialSilentAddressIndex = 0, List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, - }) : _allAddresses = ObservableSet.of(initialAddresses ?? []), + }) : _allAddresses = ObservableList.of(initialAddresses ?? []), addressesByReceiveType = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of( @@ -89,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; - final ObservableSet _allAddresses; + final ObservableList _allAddresses; final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -116,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed List get allAddresses => _allAddresses.toList(); + @computed + Set 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 addresses) { + for (final address in addresses) { + _allAddresses.replaceRange(address.index, address.index + 1, [address]); + } + } + @action void addAddresses(Iterable addresses) { this._allAddresses.addAll(addresses); diff --git a/cw_bitcoin/lib/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker.dart deleted file mode 100644 index c28fe91ab..000000000 --- a/cw_bitcoin/lib/electrum_worker.dart +++ /dev/null @@ -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 messageJson; - if (message is String) { - messageJson = jsonDecode(message) as Map; - } else { - messageJson = message as Map; - } - 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 _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 _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 _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 _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 _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()); - // } - // } -} diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 26385bff0..8b372bd3f 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -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 _handleGetBalance(ElectrumWorkerRequest message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await _electrumClient!.request( - // ElectrumGetScriptHashBalance(scriptHash: scriptHash), - // ); + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async { + final Map 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 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 = []; + + 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 _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { + // final balanceFutures = >>[]; + + // 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 _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 _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { + final balanceFutures = >>[]; - // Future _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, + ), + )); + } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart new file mode 100644 index 000000000..fc79967e1 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -0,0 +1,52 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + + final Set scripthashes; + + @override + final String method = ElectrumRequestMethods.getBalance.method; + + @override + factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { + return ElectrumWorkerGetBalanceRequest( + scripthashes: (json['scripthashes'] as List).toSet(), + ); + } + + @override + Map 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?> { + ElectrumWorkerGetBalanceResponse({required super.result, super.error}) + : super(method: ElectrumRequestMethods.getBalance.method); + + @override + Map? resultJson(result) { + return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; + } + + @override + factory ElectrumWorkerGetBalanceResponse.fromJson(Map json) { + return ElectrumWorkerGetBalanceResponse( + result: ElectrumBalance( + confirmed: json['result']['confirmed'] as int, + unconfirmed: json['result']['unconfirmed'] as int, + frozen: 0, + ), + error: json['error'] as String?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart new file mode 100644 index 000000000..584f4b6d1 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -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 addresses; + final List 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 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, walletType)) + .toList(), + walletType: walletType, + chainTip: json['chainTip'] as int, + network: BasedUtxoNetwork.fromName(json['network'] as String), + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + ); + } + + @override + Map 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 txs; + final WalletType walletType; + + AddressHistoriesResponse( + {required this.addressRecord, required this.txs, required this.walletType}); + + factory AddressHistoriesResponse.fromJson(Map 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, walletType)) + .toList(), + walletType: walletType, + ); + } + + Map toJson() { + return { + 'address': addressRecord.toJSON(), + 'txs': txs.map((e) => e.toJson()).toList(), + 'walletType': walletType.index, + }; + } +} + +class ElectrumWorkerGetHistoryResponse + extends ElectrumWorkerResponse, List>> { + ElectrumWorkerGetHistoryResponse({required super.result, super.error}) + : super(method: ElectrumRequestMethods.getHistory.method); + + @override + List> resultJson(result) { + return result.map((e) => e.toJson()).toList(); + } + + @override + factory ElectrumWorkerGetHistoryResponse.fromJson(Map json) { + return ElectrumWorkerGetHistoryResponse( + result: (json['result'] as List) + .map((e) => AddressHistoriesResponse.fromJson(e as Map)) + .toList(), + error: json['error'] as String?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart new file mode 100644 index 000000000..c3a626a0b --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart @@ -0,0 +1,53 @@ +// part of 'methods.dart'; + +// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { +// ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + +// final Set scripthashes; + +// @override +// final String method = ElectrumRequestMethods.getBalance.method; + +// @override +// factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { +// return ElectrumWorkerGetBalanceRequest( +// scripthashes: (json['scripthashes'] as List).toSet(), +// ); +// } + +// @override +// Map 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?> { +// ElectrumWorkerGetBalanceResponse({required super.result, super.error}) +// : super(method: ElectrumRequestMethods.getBalance.method); + +// @override +// Map? resultJson(result) { +// return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; +// } + +// @override +// factory ElectrumWorkerGetBalanceResponse.fromJson(Map json) { +// return ElectrumWorkerGetBalanceResponse( +// result: ElectrumBalance( +// confirmed: json['result']['confirmed'] as int, +// unconfirmed: json['result']['unconfirmed'] as int, +// frozen: 0, +// ), +// error: json['error'] as String?, +// ); +// } +// } + diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 32247c2f2..31b82bf9e 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -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'; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 4ad64e0da..716ec0ca5 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -774,113 +774,114 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future> fetchTransactions() async { - try { - final Map historiesWithDetails = {}; + throw UnimplementedError(); + // try { + // final Map 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 subscribeForUpdates([ - Iterable? unsubscribedScriptHashes, - ]) async { - final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => - !scripthashesListening.contains(address.scriptHash) && - address.type != SegwitAddresType.mweb, - ); + // @override + // @action + // Future subscribeForUpdates([ + // Iterable? 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 fetchBalances() async { - final balance = await super.fetchBalances(); + // @override + // Future 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) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 70d293041..f6bea4483 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -603,7 +603,7 @@ class CWBitcoin extends Bitcoin { @override Future 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 getNodeIsElectrsSPEnabled(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return bitcoinWallet.getNodeSupportsSilentPayments(); }