From 46c1d6fd8d360bfed615af28ac4f3e0f9ed37c98 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 26 Jun 2025 09:39:20 +0300 Subject: [PATCH] apply some changes from SP Fix pr --- cw_bitcoin/lib/electrum_wallet.dart | 304 ++++++++++++++++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 21 +- cw_bitcoin/lib/payjoin/manager.dart | 2 +- cw_bitcoin/lib/psbt/signer.dart | 2 +- cw_bitcoin/lib/psbt/transaction_builder.dart | 2 +- cw_bitcoin/lib/psbt/utils.dart | 2 +- 6 files changed, 286 insertions(+), 47 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 58dd166f7..e3f870c82 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,9 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_core/format_amount.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; @@ -19,7 +17,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum.dart' as electrum; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; @@ -50,6 +48,7 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; +import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -69,7 +68,7 @@ abstract class ElectrumWalletBase Uint8List? seedBytes, this.passphrase, List? initialAddresses, - ElectrumClient? electrumClient, + electrum.ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, @@ -96,7 +95,7 @@ abstract class ElectrumWalletBase this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? ElectrumClient(); + this.electrumClient = electrumClient ?? electrum.ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -167,7 +166,7 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - late ElectrumClient electrumClient; + late electrum.ElectrumClient electrumClient; Box unspentCoinsInfo; @override @@ -333,14 +332,14 @@ abstract class ElectrumWalletBase final receivePort = ReceivePort(); _isolate = Isolate.spawn( - startRefresh, + _handleScanSilentPayments, ScanData( sendPort: receivePort.sendPort, silentAddress: walletAddresses.silentAddress!, network: network, height: height, chainTip: chainTip, - electrumClient: ElectrumClient(), + electrumClient: electrum.ElectrumClient(), transactionHistoryIds: transactionHistory.transactions.keys.toList(), node: (await getNodeSupportsSilentPayments()) == true ? ScanNode(node!.uri, node!.useSSL) @@ -492,9 +491,10 @@ abstract class ElectrumWalletBase Future updateFeeRates() async { if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = await ProxyWrapper() - .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) - .timeout(Duration(seconds: 15)); + final response = await http + .get(Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 5)); + final result = json.decode(response.body) as Map; final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; @@ -562,7 +562,7 @@ abstract class ElectrumWalletBase node!.save(); return node!.supportsSilentPayments!; } - } on RequestFailedTimeoutException catch (_) { + } on electrum.RequestFailedTimeoutException catch (_) { node!.supportsSilentPayments = false; node!.save(); return node!.supportsSilentPayments!; @@ -1876,17 +1876,20 @@ abstract class ElectrumWalletBase if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { try { - final blockHash = await ProxyWrapper() - .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) - .timeout(Duration(seconds: 15)); + final blockHash = await http.get( + Uri.parse( + "https://mempool.cakewallet.com/api/v1/block-height/$height", + ), + ); if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty && jsonDecode(blockHash.body) != null) { - final blockResponse = await ProxyWrapper() - .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) - .timeout(Duration(seconds: 15)); - + final blockResponse = await http.get( + Uri.parse( + "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", + ), + ); if (blockResponse.statusCode == 200 && blockResponse.body.isNotEmpty && jsonDecode(blockResponse.body)['timestamp'] != null) { @@ -2389,9 +2392,9 @@ abstract class ElectrumWalletBase derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @action - void _onConnectionStatusChange(ConnectionStatus status) { + void _onConnectionStatusChange(electrum.ConnectionStatus status) { switch (status) { - case ConnectionStatus.connected: + case electrum.ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus || syncStatus is ConnectingSyncStatus) { @@ -2399,19 +2402,19 @@ abstract class ElectrumWalletBase } break; - case ConnectionStatus.disconnected: + case electrum.ConnectionStatus.disconnected: if (syncStatus is! NotConnectedSyncStatus && syncStatus is! ConnectingSyncStatus && syncStatus is! SyncronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; - case ConnectionStatus.failed: + case electrum.ConnectionStatus.failed: if (syncStatus is! LostConnectionSyncStatus) { syncStatus = LostConnectionSyncStatus(); } break; - case ConnectionStatus.connecting: + case electrum.ConnectionStatus.connecting: if (syncStatus is! ConnectingSyncStatus) { syncStatus = ConnectingSyncStatus(); } @@ -2523,7 +2526,7 @@ class ScanData { final ScanNode? node; final BasedUtxoNetwork network; final int chainTip; - final ElectrumClient electrumClient; + final electrum.ElectrumClient electrumClient; final List transactionHistoryIds; final Map labels; final List labelIndexes; @@ -2567,6 +2570,235 @@ class SyncResponse { SyncResponse(this.height, this.syncStatus); } +Future _handleScanSilentPayments(ScanData scanData) async { + try { + // if (scanData.shouldSwitchNodes) { + var scanningClient = await ElectrumProvider.connect( + ElectrumTCPService.connect( + Uri.parse("tcp://electrs.cakewallet.com:50001"), + ), + ); + // } + + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; + + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); + + int getCountToScanPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + + // Initial status UI update, send how many blocks in total to scan + scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); + + final req = ElectrumTweaksSubscribe( + height: syncHeight, + count: getCountToScanPerRequest(syncHeight), + historicalMode: false, + ); + + var _scanningStream = await scanningClient.subscribe(req); + + void listenFn(Map event, ElectrumTweaksSubscribe req) { + final response = req.onResponse(event); + + if (response == null || _scanningStream == null) { + return; + } + + // is success or error msg + final noData = response.message != null; + + if (noData) { + if (scanData.isSingleScan) { + return; + } + + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + + if (nextHeight <= scanData.chainTip) { + final nextStream = scanningClient.subscribe( + ElectrumTweaksSubscribe( + height: nextHeight, + count: getCountToScanPerRequest(nextHeight), + historicalMode: false, + ), + ); + + if (nextStream != null) { + nextStream.listen((event) => listenFn(event, req)); + } else { + scanData.sendPort.send( + SyncResponse(scanData.height, LostConnectionSyncStatus()), + ); + } + } + + return; + } + + final tweakHeight = response.block; + + if (initialSyncHeight < tweakHeight) initialSyncHeight = tweakHeight; + + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight); + + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + try { + final blockTweaks = response.blockTweaks; + + for (final txid in blockTweaks.keys) { + final tweakData = blockTweaks[txid]; + final outputPubkeys = tweakData!.outputPubkeys; + final tweak = tweakData.tweak; + + try { + final addToWallet = {}; + + // receivers.forEach((receiver) { + // NOTE: scanOutputs, from sp_scanner package, called from rust here + final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); + + if (scanResult.isEmpty) { + continue; + } + + if (addToWallet[receiver.BSpend] == null) { + addToWallet[receiver.BSpend] = scanResult; + } else { + addToWallet[receiver.BSpend].addAll(scanResult); + } + // }); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // initial placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) on the following loop + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isReplaced: false, + date: DateTime.fromMillisecondsSinceEpoch( + DateTime.now().millisecondsSinceEpoch * 1000, + ), + confirmations: scanData.chainTip - tweakHeight + 1, + isReceivedSilentPayment: true, + isPending: false, + unspents: [], + ); + + List unspents = []; + + addToWallet.forEach((BSpend, scanResultPerLabel) { + scanResultPerLabel.forEach((label, scanOutput) { + final labelValue = label == "None" ? null : label.toString(); + + (scanOutput as Map).forEach((outputPubkey, tweak) { + final t_k = tweak as String; + + final receivingOutputAddress = ECPublic.fromHex(outputPubkey) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + final matchingOutput = outputPubkeys[outputPubkey]!; + final amount = matchingOutput.amount; + final pos = matchingOutput.vout; + + // final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere( + // (receiver) => receiver.B_spend.toHex() == BSpend.toString(), + // ); + + // final labelIndex = labelValue != null ? scanData.labels[label] : 0; + // final balance = ElectrumBalance(); + // balance.confirmed = amount; + + final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( + receivingOutputAddress, + index: 0, + isHidden: false, + isUsed: true, + network: scanData.network, + silentPaymentTweak: t_k, + type: SegwitAddressType.p2tr, + txCount: 1, + balance: amount, + ); + + final unspent = BitcoinSilentPaymentsUnspent( + receivedAddressRecord, + txid, + amount, + pos, + silentPaymentTweak: t_k, + silentPaymentLabel: labelValue, + ); + + unspents.add(unspent); + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + }); + + scanData.sendPort.send({txInfo.id: txInfo}); + } catch (e, stacktrace) { + printV(stacktrace); + printV(e.toString()); + } + } + } catch (e, stacktrace) { + printV(stacktrace); + printV(e.toString()); + } + + syncHeight = tweakHeight; + + if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + scanData.sendPort.send( + SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)), + ); + + if (scanData.isSingleScan) { + scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + } + + _scanningStream?.close(); + _scanningStream = null; + return; + } + } + + _scanningStream?.listen((event) => listenFn(event, req)); + } catch (e) { + printV("Error in _handleScanSilentPayments: $e"); + scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus())); + } +} + Future startRefresh(ScanData scanData) async { int syncHeight = scanData.height; int initialSyncHeight = syncHeight; @@ -2579,7 +2811,7 @@ Future startRefresh(ScanData scanData) async { useSSL: scanData.node?.useSSL ?? false, ); - int getCountPerRequest(int syncHeight) { + int getCountToScanPerRequest(int syncHeight) { if (scanData.isSingleScan) { return 1; } @@ -2598,7 +2830,7 @@ Future startRefresh(ScanData scanData) async { ); // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); + final initialCount = getCountToScanPerRequest(syncHeight); scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); tweaksSubscription = await electrumClient.tweaksSubscribe( @@ -2609,22 +2841,24 @@ Future startRefresh(ScanData scanData) async { Future listenFn(t) async { final tweaks = t as Map; final msg = tweaks["message"]; - // success or error msg + + // is success or error msg final noData = msg != null; if (noData) { + if (scanData.isSingleScan) { + return; + } + // re-subscribe to continue receiving messages, starting from the next unscanned height final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - if (nextCount > 0) { - tweaksSubscription?.close(); - - final nextTweaksSubscription = electrumClient.tweaksSubscribe( + if (nextHeight <= scanData.chainTip) { + final nextStream = electrumClient.tweaksSubscribe( height: nextHeight, - count: nextCount, + count: getCountToScanPerRequest(nextHeight), ); - nextTweaksSubscription?.listen(listenFn); + nextStream?.listen(listenFn); } return; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index f8c0b9689..e2aae9e1a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,7 +3,6 @@ import 'dart:io' show Platform; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; @@ -71,9 +70,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()), - b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()), - network: network, + b_scan: ECPrivate.fromHex( + masterHd.derivePath("m/352'/1'/0'/1'/0").privateKey.toHex(), + ), + b_spend: ECPrivate.fromHex( + masterHd.derivePath("m/352'/1'/0'/0'/0").privateKey.toHex(), + ), ); if (silentAddresses.length == 0) { @@ -109,8 +111,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; @@ -144,12 +148,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } - final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); - final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); + final typeMatchingAddresses = + _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); + final typeMatchingReceiveAddresses = + typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); if (!isEnabledAutoGenerateSubaddress) { - if (previousAddressRecord != null && - previousAddressRecord!.type == addressPageType) { + if (previousAddressRecord != null && previousAddressRecord!.type == addressPageType) { return previousAddressRecord!.address; } diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index 95a523d89..5b9f9d07c 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -3,7 +3,7 @@ import 'dart:isolate'; import 'dart:math'; import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/payjoin/payjoin_persister.dart'; diff --git a/cw_bitcoin/lib/psbt/signer.dart b/cw_bitcoin/lib/psbt/signer.dart index 2dcbf8fb0..c3a6c7a64 100644 --- a/cw_bitcoin/lib/psbt/signer.dart +++ b/cw_bitcoin/lib/psbt/signer.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; diff --git a/cw_bitcoin/lib/psbt/transaction_builder.dart b/cw_bitcoin/lib/psbt/transaction_builder.dart index 2984c3f78..1a82bb42d 100644 --- a/cw_bitcoin/lib/psbt/transaction_builder.dart +++ b/cw_bitcoin/lib/psbt/transaction_builder.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:convert/convert.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:ledger_bitcoin/psbt.dart'; diff --git a/cw_bitcoin/lib/psbt/utils.dart b/cw_bitcoin/lib/psbt/utils.dart index 85d2530ae..b1414485d 100644 --- a/cw_bitcoin/lib/psbt/utils.dart +++ b/cw_bitcoin/lib/psbt/utils.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/psbt/v0_deserialize.dart';