fix: silent payments scanning using v10 (#2355)
* fix: silent payments scanning using v10 * chore: no http * fix: - always scan shows the toggle as disabled after restart - refresh cancels sp scanning * Update cw_bitcoin/lib/electrum_wallet.dart [skip ci] * Update cw_bitcoin/lib/electrum_wallet.dart [skip ci] * fix: get estimated date of sp tx --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
rafael_xmr committed
Jul 8, 2025 at 23:17 UTC
7d0f9323c871affd655075ffe9dfdbc739e0e475
6 files changed
+283
-43
cw_bitcoin/lib/electrum_wallet.dart
+274
-37
@@ -6,7 +6,6 @@ import 'dart:isolate';
6
import 'package:bitcoin_base/bitcoin_base.dart';
7
import 'package:cw_core/utils/proxy_wrapper.dart';
8
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
9
-import 'package:cw_core/format_amount.dart';
9
import 'package:cw_core/utils/print_verbose.dart';
10
import 'package:cw_bitcoin/bitcoin_wallet.dart';
11
import 'package:cw_bitcoin/litecoin_wallet.dart';
@@ -19,7 +18,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
18
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
19
import 'package:cw_bitcoin/bitcoin_unspent.dart';
20
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
22
-import 'package:cw_bitcoin/electrum.dart';
21
+import 'package:cw_bitcoin/electrum.dart' as electrum;
22
import 'package:cw_bitcoin/electrum_balance.dart';
23
import 'package:cw_bitcoin/electrum_derivations.dart';
24
import 'package:cw_bitcoin/electrum_transaction_history.dart';
@@ -69,10 +68,10 @@ abstract class ElectrumWalletBase
68
Uint8List? seedBytes,
69
this.passphrase,
70
List<BitcoinAddressRecord>? initialAddresses,
72
- ElectrumClient? electrumClient,
71
+ electrum.ElectrumClient? electrumClient,
72
ElectrumBalance? initialBalance,
73
CryptoCurrency? currency,
75
- this.alwaysScan,
74
+ bool? alwaysScan,
75
}) : accountHD =
76
getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
77
syncStatus = NotConnectedSyncStatus(),
@@ -82,6 +81,8 @@ abstract class ElectrumWalletBase
81
isEnabledAutoGenerateSubaddress = true,
82
unspentCoins = [],
83
_scripthashesUpdateSubject = {},
84
+ this.alwaysScan = alwaysScan,
85
+ silentPaymentsScanningActive = alwaysScan ?? false,
86
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
87
? {
88
currency: initialBalance ??
@@ -96,7 +97,7 @@ abstract class ElectrumWalletBase
97
this.isTestnet = !network.isMainnet,
98
this._mnemonic = mnemonic,
99
super(walletInfo) {
99
- this.electrumClient = electrumClient ?? ElectrumClient();
100
+ this.electrumClient = electrumClient ?? electrum.ElectrumClient();
101
this.walletInfo = walletInfo;
102
transactionHistory = ElectrumTransactionHistory(
103
walletInfo: walletInfo,
@@ -167,7 +168,7 @@ abstract class ElectrumWalletBase
168
@observable
169
bool isEnabledAutoGenerateSubaddress;
170
170
- late ElectrumClient electrumClient;
171
+ late electrum.ElectrumClient electrumClient;
172
Box<UnspentCoinsInfo> unspentCoinsInfo;
173
174
@override
@@ -333,14 +334,14 @@ abstract class ElectrumWalletBase
334
335
final receivePort = ReceivePort();
336
_isolate = Isolate.spawn(
336
- startRefresh,
337
+ _handleScanSilentPayments,
338
ScanData(
339
sendPort: receivePort.sendPort,
340
silentAddress: walletAddresses.silentAddress!,
341
network: network,
342
height: height,
343
chainTip: chainTip,
343
- electrumClient: ElectrumClient(),
344
+ electrumClient: electrum.ElectrumClient(),
345
transactionHistoryIds: transactionHistory.transactions.keys.toList(),
346
node: (await getNodeSupportsSilentPayments()) == true
347
? ScanNode(node!.uri, node!.useSSL)
@@ -439,7 +440,6 @@ abstract class ElectrumWalletBase
440
BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
441
)
442
: silentAddress.B_spend,
442
- network: network,
443
);
444
445
final addressRecord = walletAddresses.silentAddresses
@@ -463,6 +463,7 @@ abstract class ElectrumWalletBase
463
syncStatus = SyncronizingSyncStatus();
464
465
if (hasSilentPaymentsScanning) {
466
+ silentPaymentsScanningActive = alwaysScan ?? false;
467
await _setInitialHeight();
468
}
469
@@ -477,7 +478,7 @@ abstract class ElectrumWalletBase
478
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
479
480
if (alwaysScan == true) {
480
- _setListeners(walletInfo.restoreHeight);
481
+ setSilentPaymentsScanning(true);
482
} else {
483
if (syncStatus is LostConnectionSyncStatus) return;
484
syncStatus = SyncedSyncStatus();
@@ -493,9 +494,10 @@ abstract class ElectrumWalletBase
494
Future<void> updateFeeRates() async {
495
if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) {
496
try {
496
- final response = await ProxyWrapper()
497
- .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended"))
498
- .timeout(Duration(seconds: 15));
497
+ final response = await ProxyWrapper()
498
+ .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended"))
499
+ .timeout(Duration(seconds: 15));
500
+
501
final result = json.decode(response.body) as Map<String, dynamic>;
502
final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0;
503
int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0;
@@ -563,7 +565,7 @@ abstract class ElectrumWalletBase
565
node!.save();
566
return node!.supportsSilentPayments!;
567
}
566
- } on RequestFailedTimeoutException catch (_) {
568
+ } on electrum.RequestFailedTimeoutException catch (_) {
569
node!.supportsSilentPayments = false;
570
node!.save();
571
return node!.supportsSilentPayments!;
@@ -1877,17 +1879,20 @@ abstract class ElectrumWalletBase
1879
1880
if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) {
1881
try {
1880
- final blockHash = await ProxyWrapper()
1881
- .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height"))
1882
- .timeout(Duration(seconds: 15));
1882
+ final blockHash = await ProxyWrapper().get(
1883
+ clearnetUri: Uri.parse(
1884
+ "https://mempool.cakewallet.com/api/v1/block-height/$height",
1885
+ ),
1886
+ ).timeout(Duration(seconds: 15));
1887
1888
if (blockHash.statusCode == 200 &&
1889
blockHash.body.isNotEmpty &&
1890
jsonDecode(blockHash.body) != null) {
1887
- final blockResponse = await ProxyWrapper()
1888
- .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}"))
1889
- .timeout(Duration(seconds: 15));
1890
-
1891
+ final blockResponse = await ProxyWrapper().get(
1892
+ clearnetUri: Uri.parse(
1893
+ "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}",
1894
+ ),
1895
+ ).timeout(Duration(seconds: 15));
1896
if (blockResponse.statusCode == 200 &&
1897
blockResponse.body.isNotEmpty &&
1898
jsonDecode(blockResponse.body)['timestamp'] != null) {
@@ -2390,9 +2395,9 @@ abstract class ElectrumWalletBase
2395
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
2396
2397
@action
2393
- void _onConnectionStatusChange(ConnectionStatus status) {
2398
+ void _onConnectionStatusChange(electrum.ConnectionStatus status) {
2399
switch (status) {
2395
- case ConnectionStatus.connected:
2400
+ case electrum.ConnectionStatus.connected:
2401
if (syncStatus is NotConnectedSyncStatus ||
2402
syncStatus is LostConnectionSyncStatus ||
2403
syncStatus is ConnectingSyncStatus) {
@@ -2400,19 +2405,19 @@ abstract class ElectrumWalletBase
2405
}
2406
2407
break;
2403
- case ConnectionStatus.disconnected:
2408
+ case electrum.ConnectionStatus.disconnected:
2409
if (syncStatus is! NotConnectedSyncStatus &&
2410
syncStatus is! ConnectingSyncStatus &&
2411
syncStatus is! SyncronizingSyncStatus) {
2412
syncStatus = NotConnectedSyncStatus();
2413
}
2414
break;
2410
- case ConnectionStatus.failed:
2415
+ case electrum.ConnectionStatus.failed:
2416
if (syncStatus is! LostConnectionSyncStatus) {
2417
syncStatus = LostConnectionSyncStatus();
2418
}
2419
break;
2415
- case ConnectionStatus.connecting:
2420
+ case electrum.ConnectionStatus.connecting:
2421
if (syncStatus is! ConnectingSyncStatus) {
2422
syncStatus = ConnectingSyncStatus();
2423
}
@@ -2524,7 +2529,7 @@ class ScanData {
2529
final ScanNode? node;
2530
final BasedUtxoNetwork network;
2531
final int chainTip;
2527
- final ElectrumClient electrumClient;
2532
+ final electrum.ElectrumClient electrumClient;
2533
final List<String> transactionHistoryIds;
2534
final Map<String, String> labels;
2535
final List<int> labelIndexes;
@@ -2568,6 +2573,236 @@ class SyncResponse {
2573
SyncResponse(this.height, this.syncStatus);
2574
}
2575
2576
+Future<void> _handleScanSilentPayments(ScanData scanData) async {
2577
+ try {
2578
+ // if (scanData.shouldSwitchNodes) {
2579
+ var scanningClient = await ElectrumProvider.connect(
2580
+ ElectrumTCPService.connect(
2581
+ Uri.parse("tcp://electrs.cakewallet.com:50001"),
2582
+ ),
2583
+ );
2584
+ // }
2585
+
2586
+ int syncHeight = scanData.height;
2587
+ int initialSyncHeight = syncHeight;
2588
+
2589
+ final receiver = Receiver(
2590
+ scanData.silentAddress.b_scan.toHex(),
2591
+ scanData.silentAddress.B_spend.toHex(),
2592
+ scanData.network == BitcoinNetwork.testnet,
2593
+ scanData.labelIndexes,
2594
+ scanData.labelIndexes.length,
2595
+ );
2596
+
2597
+ int getCountToScanPerRequest(int syncHeight) {
2598
+ if (scanData.isSingleScan) {
2599
+ return 1;
2600
+ }
2601
+
2602
+ final amountLeft = scanData.chainTip - syncHeight + 1;
2603
+ return amountLeft;
2604
+ }
2605
+
2606
+ // Initial status UI update, send how many blocks in total to scan
2607
+ scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
2608
+
2609
+ final req = ElectrumTweaksSubscribe(
2610
+ height: syncHeight,
2611
+ count: getCountToScanPerRequest(syncHeight),
2612
+ historicalMode: false,
2613
+ );
2614
+
2615
+ var _scanningStream = await scanningClient.subscribe(req);
2616
+
2617
+ void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) {
2618
+ final response = req.onResponse(event);
2619
+
2620
+ if (response == null || _scanningStream == null) {
2621
+ return;
2622
+ }
2623
+
2624
+ // is success or error msg
2625
+ final noData = response.message != null;
2626
+
2627
+ if (noData) {
2628
+ if (scanData.isSingleScan) {
2629
+ return;
2630
+ }
2631
+
2632
+ // re-subscribe to continue receiving messages, starting from the next unscanned height
2633
+ final nextHeight = syncHeight + 1;
2634
+
2635
+ if (nextHeight <= scanData.chainTip) {
2636
+ final nextStream = scanningClient.subscribe(
2637
+ ElectrumTweaksSubscribe(
2638
+ height: nextHeight,
2639
+ count: getCountToScanPerRequest(nextHeight),
2640
+ historicalMode: false,
2641
+ ),
2642
+ );
2643
+
2644
+ if (nextStream != null) {
2645
+ nextStream.listen((event) => listenFn(event, req));
2646
+ } else {
2647
+ scanData.sendPort.send(
2648
+ SyncResponse(scanData.height, LostConnectionSyncStatus()),
2649
+ );
2650
+ }
2651
+ }
2652
+
2653
+ return;
2654
+ }
2655
+
2656
+ final tweakHeight = response.block;
2657
+
2658
+ if (initialSyncHeight < tweakHeight) initialSyncHeight = tweakHeight;
2659
+
2660
+ // Continuous status UI update, send how many blocks left to scan
2661
+ final syncingStatus = scanData.isSingleScan
2662
+ ? SyncingSyncStatus(1, 0)
2663
+ : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight);
2664
+
2665
+ scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
2666
+
2667
+ try {
2668
+ final blockTweaks = response.blockTweaks;
2669
+
2670
+ for (final txid in blockTweaks.keys) {
2671
+ final tweakData = blockTweaks[txid];
2672
+ final outputPubkeys = tweakData!.outputPubkeys;
2673
+ final tweak = tweakData.tweak;
2674
+
2675
+ try {
2676
+ final addToWallet = {};
2677
+
2678
+ // receivers.forEach((receiver) {
2679
+ // NOTE: scanOutputs, from sp_scanner package, called from rust here
2680
+ final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver);
2681
+
2682
+ if (scanResult.isEmpty) {
2683
+ continue;
2684
+ }
2685
+
2686
+ if (addToWallet[receiver.BSpend] == null) {
2687
+ addToWallet[receiver.BSpend] = scanResult;
2688
+ } else {
2689
+ addToWallet[receiver.BSpend].addAll(scanResult);
2690
+ }
2691
+ // });
2692
+
2693
+ if (addToWallet.isEmpty) {
2694
+ // no results tx, continue to next tx
2695
+ continue;
2696
+ }
2697
+
2698
+ // initial placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) on the following loop
2699
+ final txInfo = ElectrumTransactionInfo(
2700
+ WalletType.bitcoin,
2701
+ id: txid,
2702
+ height: tweakHeight,
2703
+ amount: 0,
2704
+ fee: 0,
2705
+ direction: TransactionDirection.incoming,
2706
+ isReplaced: false,
2707
+ // TODO: fetch block data and get the date from it
2708
+ date: scanData.network == BitcoinNetwork.mainnet
2709
+ ? getDateByBitcoinHeight(tweakHeight)
2710
+ : DateTime.now(),
2711
+ confirmations: scanData.chainTip - tweakHeight + 1,
2712
+ isReceivedSilentPayment: true,
2713
+ isPending: false,
2714
+ unspents: [],
2715
+ );
2716
+
2717
+ List<BitcoinUnspent> unspents = [];
2718
+
2719
+ addToWallet.forEach((BSpend, scanResultPerLabel) {
2720
+ scanResultPerLabel.forEach((label, scanOutput) {
2721
+ final labelValue = label == "None" ? null : label.toString();
2722
+
2723
+ (scanOutput as Map<String, dynamic>).forEach((outputPubkey, tweak) {
2724
+ final t_k = tweak as String;
2725
+
2726
+ final receivingOutputAddress = ECPublic.fromHex(outputPubkey)
2727
+ .toTaprootAddress(tweak: false)
2728
+ .toAddress(scanData.network);
2729
+
2730
+ final matchingOutput = outputPubkeys[outputPubkey]!;
2731
+ final amount = matchingOutput.amount;
2732
+ final pos = matchingOutput.vout;
2733
+
2734
+ // final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere(
2735
+ // (receiver) => receiver.B_spend.toHex() == BSpend.toString(),
2736
+ // );
2737
+
2738
+ // final labelIndex = labelValue != null ? scanData.labels[label] : 0;
2739
+ // final balance = ElectrumBalance();
2740
+ // balance.confirmed = amount;
2741
+
2742
+ final receivedAddressRecord = BitcoinSilentPaymentAddressRecord(
2743
+ receivingOutputAddress,
2744
+ index: 0,
2745
+ isHidden: false,
2746
+ isUsed: true,
2747
+ network: scanData.network,
2748
+ silentPaymentTweak: t_k,
2749
+ type: SegwitAddresType.p2tr,
2750
+ txCount: 1,
2751
+ balance: amount,
2752
+ );
2753
+
2754
+ final unspent = BitcoinSilentPaymentsUnspent(
2755
+ receivedAddressRecord,
2756
+ txid,
2757
+ amount,
2758
+ pos,
2759
+ silentPaymentTweak: t_k,
2760
+ silentPaymentLabel: labelValue,
2761
+ );
2762
+
2763
+ unspents.add(unspent);
2764
+ txInfo.unspents!.add(unspent);
2765
+ txInfo.amount += unspent.value;
2766
+ });
2767
+ });
2768
+ });
2769
+
2770
+ scanData.sendPort.send({txInfo.id: txInfo});
2771
+ } catch (e, stacktrace) {
2772
+ printV(stacktrace);
2773
+ printV(e.toString());
2774
+ }
2775
+ }
2776
+ } catch (e, stacktrace) {
2777
+ printV(stacktrace);
2778
+ printV(e.toString());
2779
+ }
2780
+
2781
+ syncHeight = tweakHeight;
2782
+
2783
+ if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) {
2784
+ if (tweakHeight >= scanData.chainTip)
2785
+ scanData.sendPort.send(
2786
+ SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)),
2787
+ );
2788
+
2789
+ if (scanData.isSingleScan) {
2790
+ scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
2791
+ }
2792
+
2793
+ _scanningStream?.close();
2794
+ _scanningStream = null;
2795
+ return;
2796
+ }
2797
+ }
2798
+
2799
+ _scanningStream?.listen((event) => listenFn(event, req));
2800
+ } catch (e) {
2801
+ printV("Error in _handleScanSilentPayments: $e");
2802
+ scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus()));
2803
+ }
2804
+}
2805
+
2806
Future<void> startRefresh(ScanData scanData) async {
2807
int syncHeight = scanData.height;
2808
int initialSyncHeight = syncHeight;
@@ -2580,7 +2815,7 @@ Future<void> startRefresh(ScanData scanData) async {
2815
useSSL: scanData.node?.useSSL ?? false,
2816
);
2817
2583
- int getCountPerRequest(int syncHeight) {
2818
+ int getCountToScanPerRequest(int syncHeight) {
2819
if (scanData.isSingleScan) {
2820
return 1;
2821
}
@@ -2599,7 +2834,7 @@ Future<void> startRefresh(ScanData scanData) async {
2834
);
2835
2836
// Initial status UI update, send how many blocks in total to scan
2602
- final initialCount = getCountPerRequest(syncHeight);
2837
+ final initialCount = getCountToScanPerRequest(syncHeight);
2838
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
2839
2840
tweaksSubscription = await electrumClient.tweaksSubscribe(
@@ -2610,22 +2845,24 @@ Future<void> startRefresh(ScanData scanData) async {
2845
Future<void> listenFn(t) async {
2846
final tweaks = t as Map<String, dynamic>;
2847
final msg = tweaks["message"];
2613
- // success or error msg
2848
+
2849
+ // is success or error msg
2850
final noData = msg != null;
2851
2852
if (noData) {
2853
+ if (scanData.isSingleScan) {
2854
+ return;
2855
+ }
2856
+
2857
// re-subscribe to continue receiving messages, starting from the next unscanned height
2858
final nextHeight = syncHeight + 1;
2619
- final nextCount = getCountPerRequest(nextHeight);
2620
-
2621
- if (nextCount > 0) {
2622
- tweaksSubscription?.close();
2859
2624
- final nextTweaksSubscription = electrumClient.tweaksSubscribe(
2860
+ if (nextHeight <= scanData.chainTip) {
2861
+ final nextStream = electrumClient.tweaksSubscribe(
2862
height: nextHeight,
2626
- count: nextCount,
2863
+ count: getCountToScanPerRequest(nextHeight),
2864
);
2628
- nextTweaksSubscription?.listen(listenFn);
2865
+ nextStream?.listen(listenFn);
2866
}
2867
2868
return;
cw_bitcoin/pubspec.lock
+2
-2
@@ -84,8 +84,8 @@ packages:
84
dependency: "direct overridden"
85
description:
86
path: "."
87
- ref: cake-update-v9
88
- resolved-ref: bb4318511312a454fd91bf49042e25ecc855e4ac
87
+ ref: cake-update-v10
88
+ resolved-ref: "545bcade1e8c7691553bbd811f51bc54c333b19f"
89
url: "https://github.com/cake-tech/bitcoin_base"
90
source: git
91
version: "4.7.0"
cw_bitcoin/pubspec.yaml
+1
-1
@@ -73,7 +73,7 @@ dependency_overrides:
73
bitcoin_base:
74
git:
75
url: https://github.com/cake-tech/bitcoin_base
76
- ref: cake-update-v9
76
+ ref: cake-update-v10
77
pointycastle: 3.7.4
78
ffi: 2.1.0
79
cw_bitcoin_cash/pubspec.yaml
+1
-1
@@ -42,7 +42,7 @@ dependency_overrides:
42
bitcoin_base:
43
git:
44
url: https://github.com/cake-tech/bitcoin_base
45
- ref: cake-update-v9
45
+ ref: cake-update-v10
46
47
# For information on the generic Dart part of this file, see the
48
# following page: https://dart.dev/tools/pub/pubspec
lib/view_model/dashboard/dashboard_view_model.dart
+4
-1
@@ -38,7 +38,6 @@ import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.d
38
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
39
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
40
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
41
-import 'package:cake_wallet/wownero/wownero.dart' as wow;
41
import 'package:cryptography/cryptography.dart';
42
import 'package:cw_core/balance.dart';
43
import 'package:cw_core/cake_hive.dart';
@@ -890,6 +889,10 @@ abstract class DashboardViewModelBase with Store {
889
final powNode = settingsStore.getCurrentPowNode(wallet.type);
890
await wallet.connectToPowNode(node: powNode);
891
}
892
+
893
+ if (hasSilentPayments) {
894
+ bitcoin!.setScanningActive(wallet, silentPaymentsScanningActive);
895
+ }
896
}
897
898
@action
pubspec_base.yaml
+1
-1
@@ -164,7 +164,7 @@ dependency_overrides:
164
bitcoin_base:
165
git:
166
url: https://github.com/cake-tech/bitcoin_base
167
- ref: cake-update-v9
167
+ ref: cake-update-v10
168
ffi: 2.1.0
169
ledger_flutter_plus:
170
git: