Cw 1127 sp utxos not updated saved correctly (#2472)
* fix: ETA for sp scan * fix: missing SP unspent when re-open * fix: address comments * feat: check sp unspents for spendingTx * feat: resolve comments * fix: always starts scanning * minor [skip ci] * fix stuck at 1 block remaining --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
rafael_xmr committed
Sep 6, 2025 at 20:34 UTC
68a5821ad7c9f1eed101238b598a86714517fb00
2 files changed
+321
-260
cw_bitcoin/lib/electrum_wallet.dart
+301
-240
@@ -340,19 +340,21 @@ abstract class ElectrumWalletBase
340
}
341
342
@action
343
- Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
343
+ Future<void> _setListeners(int height,
344
+ {int? chainTipParam, bool? doSingleScan, List<int>? rescanHeights}) async {
345
if (this is! BitcoinWallet) return;
346
if (isHardwareWallet) return;
347
if (seed?.isEmpty ?? true) return;
348
349
final chainTip = chainTipParam ?? await getUpdatedChainTip();
350
+ final shouldUpdateSyncStatus = rescanHeights == null || rescanHeights.isEmpty;
351
352
if (chainTip == height) {
353
syncStatus = SyncedSyncStatus();
354
return;
355
}
356
355
- syncStatus = AttemptingScanSyncStatus();
357
+ if (shouldUpdateSyncStatus) syncStatus = AttemptingScanSyncStatus();
358
359
if (_isolate != null) {
360
final runningIsolate = await _isolate!;
@@ -383,6 +385,7 @@ abstract class ElectrumWalletBase
385
.toList(),
386
isSingleScan: doSingleScan ?? false,
387
debugLogPath: debugLogPath,
388
+ rescanHeights: rescanHeights,
389
),
390
);
391
@@ -406,6 +409,7 @@ abstract class ElectrumWalletBase
409
existingTxInfo.isReceivedSilentPayment = tx.isReceivedSilentPayment;
410
existingTxInfo.direction = tx.direction;
411
existingTxInfo.isPending = tx.isPending;
412
+ existingTxInfo.unspents = tx.unspents;
413
414
final newUnspents = tx.unspents!
415
.where((unspent) => !(existingTxInfo.unspents?.any((element) =>
@@ -459,9 +463,9 @@ abstract class ElectrumWalletBase
463
464
if (message.syncStatus is SyncingSyncStatus) {
465
var status = message.syncStatus as SyncingSyncStatus;
462
- syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
466
+ if (shouldUpdateSyncStatus) syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
467
} else {
464
- syncStatus = message.syncStatus;
468
+ if (shouldUpdateSyncStatus) syncStatus = message.syncStatus;
469
}
470
471
await walletInfo.updateRestoreHeight(message.height);
@@ -491,6 +495,9 @@ abstract class ElectrumWalletBase
495
);
496
}
497
498
+ DateTime? _lastSilentPaymentsScan;
499
+ static const Duration _silentPaymentsScanDelay = Duration(minutes: 1);
500
+
501
@action
502
@override
503
Future<void> startSync() async {
@@ -504,6 +511,30 @@ abstract class ElectrumWalletBase
511
if (hasSilentPaymentsScanning) {
512
silentPaymentsScanningActive = alwaysScan ?? false;
513
await _setInitialHeight();
514
+
515
+ final now = DateTime.now();
516
+ final shouldForceRescan = _lastSilentPaymentsScan == null ||
517
+ now.difference(_lastSilentPaymentsScan!) >= _silentPaymentsScanDelay;
518
+
519
+ // Timer prevents server failure and this infinite looping and requesting
520
+ if (shouldForceRescan) {
521
+ _lastSilentPaymentsScan = now;
522
+
523
+ final rescanHeights = <int>[];
524
+
525
+ transactionHistory.transactions.values.forEach((tx) {
526
+ if (tx.unspents != null && tx.unspents!.isNotEmpty)
527
+ for (final unspent in tx.unspents!) {
528
+ if (unspent.silentPaymentTweak != null && tx.height != null && tx.height! > 0) {
529
+ rescanHeights.add(tx.height!);
530
+ break;
531
+ }
532
+ }
533
+ });
534
+
535
+ if (rescanHeights.isNotEmpty)
536
+ _setListeners(walletInfo.restoreHeight, rescanHeights: rescanHeights);
537
+ }
538
}
539
540
await subscribeForUpdates();
@@ -519,7 +550,9 @@ abstract class ElectrumWalletBase
550
if (alwaysScan == true) {
551
setSilentPaymentsScanning(true);
552
} else {
522
- if (syncStatus is LostConnectionSyncStatus) return;
553
+ if (syncStatus is LostConnectionSyncStatus) {
554
+ return;
555
+ }
556
syncStatus = SyncedSyncStatus();
557
}
558
} catch (e, stacktrace) {
@@ -1574,12 +1607,16 @@ abstract class ElectrumWalletBase
1607
}
1608
unspentCoins = updatedUnspentCoins;
1609
} else {
1577
- unspentCoins = handleFailedUtxoFetch(
1578
- failedCount: failedCount,
1579
- previousUnspentCoins: previousUnspentCoins,
1580
- updatedUnspentCoins: updatedUnspentCoins,
1581
- results: results,
1582
- );
1610
+ if (updatedUnspentCoins.isEmpty) {
1611
+ unspentCoins = handleFailedUtxoFetch(
1612
+ failedCount: failedCount,
1613
+ previousUnspentCoins: previousUnspentCoins,
1614
+ updatedUnspentCoins: updatedUnspentCoins,
1615
+ results: results,
1616
+ );
1617
+ } else {
1618
+ unspentCoins = updatedUnspentCoins;
1619
+ }
1620
}
1621
1622
final currentWalletUnspentCoins =
@@ -2882,6 +2919,7 @@ class ScanData {
2919
final List<int> labelIndexes;
2920
final bool isSingleScan;
2921
final String debugLogPath;
2922
+ final List<int>? rescanHeights;
2923
2924
ScanData({
2925
required this.sendPort,
@@ -2896,6 +2934,7 @@ class ScanData {
2934
required this.labelIndexes,
2935
required this.isSingleScan,
2936
required this.debugLogPath,
2937
+ required this.rescanHeights,
2938
});
2939
2940
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
@@ -2912,6 +2951,7 @@ class ScanData {
2951
labelIndexes: scanData.labelIndexes,
2952
isSingleScan: scanData.isSingleScan,
2953
debugLogPath: scanData.debugLogPath,
2954
+ rescanHeights: scanData.rescanHeights,
2955
);
2956
}
2957
}
@@ -2924,6 +2964,9 @@ class SyncResponse {
2964
}
2965
2966
Future<void> _handleScanSilentPayments(ScanData scanData) async {
2967
+ final shouldUpdateSyncStatus = scanData.rescanHeights == null || scanData.rescanHeights!.isEmpty;
2968
+ final hasForcedRescanHeights = !shouldUpdateSyncStatus;
2969
+
2970
var node = Uri.parse("tcp://electrs.cakewallet.com:50001");
2971
2972
void log(String message, LogLevel level) {
@@ -2939,9 +2982,6 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
2982
2983
log("connected to ${node.toString()}", LogLevel.info);
2984
2942
- int syncHeight = scanData.height;
2943
- int initialSyncHeight = syncHeight;
2944
-
2985
final receiver = Receiver(
2986
scanData.silentAddress.b_scan.toHex(),
2987
scanData.silentAddress.B_spend.toHex(),
@@ -2955,290 +2995,311 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
2995
LogLevel.info,
2996
);
2997
2958
- int getCountToScanPerRequest(int syncHeight) {
2959
- if (scanData.isSingleScan) {
2960
- return 1;
2961
- }
2998
+ void scan(int syncHeight, bool isSingleScan) async {
2999
+ int initialSyncHeight = syncHeight;
3000
2963
- final amountLeft = scanData.chainTip - syncHeight + 1;
2964
- return amountLeft;
2965
- }
2966
-
2967
- // Initial status UI update, send how many blocks in total to scan
2968
- scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
2969
-
2970
- final req = ElectrumTweaksSubscribe(
2971
- height: syncHeight,
2972
- count: getCountToScanPerRequest(syncHeight),
2973
- historicalMode: false,
2974
- );
3001
+ int getCountToScanPerRequest(int syncHeight) {
3002
+ if (isSingleScan) {
3003
+ return 1;
3004
+ }
3005
2976
- var _scanningStream = await scanningClient.subscribe(req);
3006
+ final amountLeft = scanData.chainTip - syncHeight + 1;
3007
+ return amountLeft;
3008
+ }
3009
2978
- log(
2979
- "initial request: height: $syncHeight, count: ${getCountToScanPerRequest(syncHeight)}",
2980
- LogLevel.info,
2981
- );
3010
+ // Initial status UI update, send how many blocks in total to scan
3011
+ if (shouldUpdateSyncStatus)
3012
+ scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
3013
2983
- void endScanningSuccesfully() {
2984
- if (scanData.isSingleScan)
2985
- scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
2986
- else
2987
- scanData.sendPort.send(
2988
- SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)),
2989
- );
3014
+ final req = ElectrumTweaksSubscribe(
3015
+ height: syncHeight,
3016
+ count: getCountToScanPerRequest(syncHeight),
3017
+ historicalMode: hasForcedRescanHeights,
3018
+ );
3019
2991
- _scanningStream?.close();
2992
- _scanningStream = null;
3020
+ var _scanningStream = await scanningClient.subscribe(req);
3021
3022
log(
2995
- "ended: syncHeight: $syncHeight, chainTip: ${scanData.chainTip}, isSingleScan: ${scanData.isSingleScan}",
3023
+ "initial request: height: $syncHeight, count: ${getCountToScanPerRequest(syncHeight)}",
3024
LogLevel.info,
3025
);
2998
- }
3026
3000
- void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) async {
3001
- final response = req.onResponse(event);
3027
+ void endScanningSuccesfully() {
3028
+ if (isSingleScan) {
3029
+ scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
3030
+ } else {
3031
+ scanData.sendPort.send(
3032
+ SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)),
3033
+ );
3034
+ }
3035
+
3036
+ _scanningStream?.close();
3037
+ _scanningStream = null;
3038
3003
- if (response == null || _scanningStream == null) {
3039
log(
3005
- "ending: response = $response, stream = $_scanningStream",
3006
- LogLevel.error,
3040
+ "ended: syncHeight: $syncHeight, chainTip: ${scanData.chainTip}, isSingleScan: ${isSingleScan}",
3041
+ LogLevel.info,
3042
);
3008
- return;
3043
}
3044
3011
- // is success or error msg
3012
- final noData = response.message != null;
3045
+ void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) async {
3046
+ final response = req.onResponse(event);
3047
3014
- if (noData) {
3015
- if (scanData.isSingleScan) {
3016
- log("ending: noData and isSingleScan", LogLevel.info);
3017
-
3018
- endScanningSuccesfully();
3048
+ if (response == null || _scanningStream == null) {
3049
+ log(
3050
+ "ending: response = $response, stream = $_scanningStream",
3051
+ LogLevel.error,
3052
+ );
3053
return;
3054
}
3055
3022
- // re-subscribe to continue receiving messages, starting from the next unscanned height
3023
- final nextHeight = syncHeight + 1;
3056
+ // is success or error msg
3057
+ final noData = response.message != null;
3058
3025
- if (nextHeight <= scanData.chainTip) {
3026
- log(
3027
- "resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
3028
- LogLevel.info,
3029
- );
3059
+ if (noData) {
3060
+ if (isSingleScan) {
3061
+ log("ending: noData and isSingleScan", LogLevel.info);
3062
3031
- final nextStream = scanningClient.subscribe(
3032
- ElectrumTweaksSubscribe(
3033
- height: nextHeight,
3034
- count: getCountToScanPerRequest(nextHeight),
3035
- historicalMode: false,
3036
- ),
3037
- );
3063
+ endScanningSuccesfully();
3064
+ return;
3065
+ }
3066
3039
- if (nextStream != null) {
3040
- nextStream.listen((event) => listenFn(event, req));
3041
- } else {
3042
- scanData.sendPort.send(
3043
- SyncResponse(scanData.height, LostConnectionSyncStatus()),
3067
+ // re-subscribe to continue receiving messages, starting from the next unscanned height
3068
+ final nextHeight = syncHeight + 1;
3069
+
3070
+ if (nextHeight <= scanData.chainTip) {
3071
+ log(
3072
+ "resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
3073
+ LogLevel.info,
3074
);
3045
- }
3046
- }
3075
3048
- log(
3049
- "ending: resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
3050
- LogLevel.info,
3051
- );
3052
- return;
3053
- }
3076
+ final nextStream = scanningClient.subscribe(
3077
+ ElectrumTweaksSubscribe(
3078
+ height: nextHeight,
3079
+ count: getCountToScanPerRequest(nextHeight),
3080
+ historicalMode: hasForcedRescanHeights,
3081
+ ),
3082
+ );
3083
3055
- final tweakHeight = response.block;
3084
+ if (nextStream != null) {
3085
+ nextStream.listen((event) => listenFn(event, req));
3086
+ } else {
3087
+ if (shouldUpdateSyncStatus)
3088
+ scanData.sendPort.send(
3089
+ SyncResponse(scanData.height, LostConnectionSyncStatus()),
3090
+ );
3091
+ }
3092
+ }
3093
3057
- if (initialSyncHeight < tweakHeight) initialSyncHeight = tweakHeight;
3094
+ log(
3095
+ "ending: resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
3096
+ LogLevel.info,
3097
+ );
3098
+ return;
3099
+ }
3100
3059
- // Continuous status UI update, send how many blocks left to scan
3060
- final syncingStatus = scanData.isSingleScan
3061
- ? SyncingSyncStatus(1, 0)
3062
- : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight);
3101
+ final tweakHeight = response.block;
3102
3064
- scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
3103
+ // Continuous status UI update, send how many blocks left to scan
3104
+ final syncingStatus = isSingleScan
3105
+ ? SyncingSyncStatus(1, 0)
3106
+ : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight);
3107
3066
- try {
3067
- final blockTweaks = response.blockTweaks;
3108
+ if (shouldUpdateSyncStatus) scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
3109
3069
- var blockDate = DateTime.now();
3070
- bool isDateNow = true;
3110
+ try {
3111
+ final blockTweaks = response.blockTweaks;
3112
3072
- for (final txid in blockTweaks.keys) {
3073
- final tweakData = blockTweaks[txid];
3074
- final outputPubkeys = tweakData!.outputPubkeys;
3075
- final tweak = tweakData.tweak;
3113
+ var blockDate = DateTime.now();
3114
+ bool isDateNow = true;
3115
3077
- try {
3078
- final addToWallet = {};
3116
+ for (final txid in blockTweaks.keys) {
3117
+ final tweakData = blockTweaks[txid];
3118
+ final outputPubkeys = tweakData!.outputPubkeys;
3119
+ final tweak = tweakData.tweak;
3120
3080
- // receivers.forEach((receiver) {
3081
- // NOTE: scanOutputs, from sp_scanner package, called from rust here
3082
- final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver);
3121
+ try {
3122
+ final addToWallet = {};
3123
3084
- if (scanResult.isEmpty) {
3085
- continue;
3086
- }
3124
+ // receivers.forEach((receiver) {
3125
+ // NOTE: scanOutputs, from sp_scanner package, called from rust here
3126
+ final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver);
3127
3088
- if (addToWallet[receiver.BSpend] == null) {
3089
- addToWallet[receiver.BSpend] = scanResult;
3090
- } else {
3091
- addToWallet[receiver.BSpend].addAll(scanResult);
3092
- }
3093
- // });
3128
+ if (scanResult.isEmpty) {
3129
+ continue;
3130
+ }
3131
3095
- if (addToWallet.isEmpty) {
3096
- // no results tx, continue to next tx
3097
- continue;
3098
- }
3132
+ if (addToWallet[receiver.BSpend] == null) {
3133
+ addToWallet[receiver.BSpend] = scanResult;
3134
+ } else {
3135
+ addToWallet[receiver.BSpend].addAll(scanResult);
3136
+ }
3137
+ // });
3138
3100
- log(
3101
- "FOUND: addToWallet: ${addToWallet.length}, txid: $txid, tweak: $tweak, height: $tweakHeight",
3102
- LogLevel.info,
3103
- );
3139
+ if (addToWallet.isEmpty) {
3140
+ // no results tx, continue to next tx
3141
+ continue;
3142
+ }
3143
3105
- // Every tx in the block has the same date (the block date)
3106
- // So, if blockDate exists, reuse
3107
- if (isDateNow) {
3108
- try {
3109
- final tweakBlockHash = await ProxyWrapper()
3110
- .get(
3111
- clearnetUri: Uri.parse(
3112
- "https://mempool.cakewallet.com/api/v1/block-height/$tweakHeight",
3113
- ),
3114
- )
3115
- .timeout(Duration(seconds: 15));
3116
- final blockResponse = await ProxyWrapper()
3117
- .get(
3118
- clearnetUri: Uri.parse(
3119
- "https://mempool.cakewallet.com/api/v1/block/${tweakBlockHash.body}",
3120
- ),
3121
- )
3122
- .timeout(Duration(seconds: 15));
3123
-
3124
- if (blockResponse.statusCode == 200 &&
3125
- blockResponse.body.isNotEmpty &&
3126
- jsonDecode(blockResponse.body)['timestamp'] != null) {
3127
- blockDate = DateTime.fromMillisecondsSinceEpoch(
3128
- int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()) * 1000,
3129
- );
3130
- isDateNow = false;
3144
+ log(
3145
+ "FOUND: addToWallet: ${addToWallet.length}, txid: $txid, tweak: $tweak, height: $tweakHeight",
3146
+ LogLevel.info,
3147
+ );
3148
+
3149
+ // Every tx in the block has the same date (the block date)
3150
+ // So, if blockDate exists, reuse
3151
+ if (isDateNow) {
3152
+ try {
3153
+ final tweakBlockHash = await ProxyWrapper()
3154
+ .get(
3155
+ clearnetUri: Uri.parse(
3156
+ "https://mempool.cakewallet.com/api/v1/block-height/$tweakHeight",
3157
+ ),
3158
+ )
3159
+ .timeout(Duration(seconds: 15));
3160
+ final blockResponse = await ProxyWrapper()
3161
+ .get(
3162
+ clearnetUri: Uri.parse(
3163
+ "https://mempool.cakewallet.com/api/v1/block/${tweakBlockHash.body}",
3164
+ ),
3165
+ )
3166
+ .timeout(Duration(seconds: 15));
3167
+
3168
+ if (blockResponse.statusCode == 200 &&
3169
+ blockResponse.body.isNotEmpty &&
3170
+ jsonDecode(blockResponse.body)['timestamp'] != null) {
3171
+ blockDate = DateTime.fromMillisecondsSinceEpoch(
3172
+ int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()) * 1000,
3173
+ );
3174
+ isDateNow = false;
3175
+ }
3176
+ } catch (e, stacktrace) {
3177
+ printV(stacktrace);
3178
+ printV(e.toString());
3179
}
3132
- } catch (e, stacktrace) {
3133
- printV(stacktrace);
3134
- printV(e.toString());
3180
}
3136
- }
3181
3138
- // initial placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) on the following loop
3139
- final txInfo = ElectrumTransactionInfo(
3140
- WalletType.bitcoin,
3141
- id: txid,
3142
- height: tweakHeight,
3143
- amount: 0,
3144
- fee: 0,
3145
- direction: TransactionDirection.incoming,
3146
- isReplaced: false,
3147
- // TODO: fetch block data and get the date from it
3148
- date: scanData.network == BitcoinNetwork.mainnet
3149
- ? (isDateNow ? getDateByBitcoinHeight(tweakHeight) : blockDate)
3150
- : DateTime.now(),
3151
- confirmations: scanData.chainTip - tweakHeight + 1,
3152
- isReceivedSilentPayment: true,
3153
- isPending: false,
3154
- unspents: [],
3155
- );
3182
+ // initial placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) on the following loop
3183
+ final txInfo = ElectrumTransactionInfo(
3184
+ WalletType.bitcoin,
3185
+ id: txid,
3186
+ height: tweakHeight,
3187
+ amount: 0,
3188
+ fee: 0,
3189
+ direction: TransactionDirection.incoming,
3190
+ isReplaced: false,
3191
+ // TODO: fetch block data and get the date from it
3192
+ date: scanData.network == BitcoinNetwork.mainnet
3193
+ ? (isDateNow ? getDateByBitcoinHeight(tweakHeight) : blockDate)
3194
+ : DateTime.now(),
3195
+ confirmations: scanData.chainTip - tweakHeight + 1,
3196
+ isReceivedSilentPayment: true,
3197
+ isPending: false,
3198
+ unspents: [],
3199
+ );
3200
3157
- List<BitcoinUnspent> unspents = [];
3158
-
3159
- addToWallet.forEach((BSpend, scanResultPerLabel) {
3160
- scanResultPerLabel.forEach((label, scanOutput) {
3161
- final labelValue = label == "None" ? null : label.toString();
3162
-
3163
- (scanOutput as Map<String, dynamic>).forEach((outputPubkey, tweak) {
3164
- final t_k = tweak as String;
3165
-
3166
- final receivingOutputAddress = ECPublic.fromHex(outputPubkey)
3167
- .toTaprootAddress(tweak: false)
3168
- .toAddress(scanData.network);
3169
-
3170
- final matchingOutput = outputPubkeys[outputPubkey]!;
3171
- final amount = matchingOutput.amount;
3172
- final pos = matchingOutput.vout;
3173
-
3174
- // final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere(
3175
- // (receiver) => receiver.B_spend.toHex() == BSpend.toString(),
3176
- // );
3177
-
3178
- // final labelIndex = labelValue != null ? scanData.labels[label] : 0;
3179
- // final balance = ElectrumBalance();
3180
- // balance.confirmed = amount;
3181
-
3182
- final receivedAddressRecord = BitcoinSilentPaymentAddressRecord(
3183
- receivingOutputAddress,
3184
- index: 0,
3185
- isHidden: false,
3186
- isUsed: true,
3187
- network: scanData.network,
3188
- silentPaymentTweak: t_k,
3189
- type: SegwitAddresType.p2tr,
3190
- txCount: 1,
3191
- balance: amount,
3192
- );
3193
-
3194
- final unspent = BitcoinSilentPaymentsUnspent(
3195
- receivedAddressRecord,
3196
- txid,
3197
- amount,
3198
- pos,
3199
- silentPaymentTweak: t_k,
3200
- silentPaymentLabel: labelValue,
3201
- );
3202
-
3203
- unspents.add(unspent);
3204
- txInfo.unspents!.add(unspent);
3205
- txInfo.amount += unspent.value;
3201
+ List<BitcoinUnspent> unspents = [];
3202
+
3203
+ addToWallet.forEach((BSpend, scanResultPerLabel) {
3204
+ scanResultPerLabel.forEach((label, scanOutput) {
3205
+ final labelValue = label == "None" ? null : label.toString();
3206
+
3207
+ (scanOutput as Map<String, dynamic>).forEach((outputPubkey, tweak) {
3208
+ final t_k = tweak as String;
3209
+
3210
+ final receivingOutputAddress = ECPublic.fromHex(outputPubkey)
3211
+ .toTaprootAddress(tweak: false)
3212
+ .toAddress(scanData.network);
3213
+
3214
+ final matchingOutput = outputPubkeys[outputPubkey]!;
3215
+ final amount = matchingOutput.amount;
3216
+ final pos = matchingOutput.vout;
3217
+ final spent = matchingOutput.spendingInput;
3218
+
3219
+ // final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere(
3220
+ // (receiver) => receiver.B_spend.toHex() == BSpend.toString(),
3221
+ // );
3222
+
3223
+ // final labelIndex = labelValue != null ? scanData.labels[label] : 0;
3224
+ // final balance = ElectrumBalance();
3225
+ // balance.confirmed = amount;
3226
+
3227
+ final receivedAddressRecord = BitcoinSilentPaymentAddressRecord(
3228
+ receivingOutputAddress,
3229
+ index: 0,
3230
+ isHidden: false,
3231
+ isUsed: true,
3232
+ network: scanData.network,
3233
+ silentPaymentTweak: t_k,
3234
+ type: SegwitAddresType.p2tr,
3235
+ txCount: 1,
3236
+ balance: amount,
3237
+ );
3238
+
3239
+ final unspent = BitcoinSilentPaymentsUnspent(
3240
+ receivedAddressRecord,
3241
+ txid,
3242
+ amount,
3243
+ pos,
3244
+ silentPaymentTweak: t_k,
3245
+ silentPaymentLabel: labelValue,
3246
+ );
3247
+
3248
+ if (spent == null) {
3249
+ unspents.add(unspent);
3250
+ txInfo.unspents!.add(unspent);
3251
+ }
3252
+
3253
+ txInfo.amount += unspent.value;
3254
+ });
3255
});
3256
});
3208
- });
3257
3210
- scanData.sendPort.send({txInfo.id: txInfo});
3211
- } catch (e, stacktrace) {
3258
+ scanData.sendPort.send({txInfo.id: txInfo});
3259
+ } catch (e, stacktrace) {
3260
+ if (shouldUpdateSyncStatus)
3261
+ scanData.sendPort.send(
3262
+ SyncResponse(syncHeight, LostConnectionSyncStatus()),
3263
+ );
3264
+
3265
+ log(stacktrace.toString(), LogLevel.error);
3266
+ log(e.toString(), LogLevel.error);
3267
+ return;
3268
+ }
3269
+ }
3270
+ } catch (e, stacktrace) {
3271
+ if (shouldUpdateSyncStatus)
3272
scanData.sendPort.send(
3273
SyncResponse(syncHeight, LostConnectionSyncStatus()),
3274
);
3275
3216
- log(stacktrace.toString(), LogLevel.error);
3217
- log(e.toString(), LogLevel.error);
3218
- return;
3219
- }
3276
+ log(stacktrace.toString(), LogLevel.error);
3277
+ log(e.toString(), LogLevel.error);
3278
+ return;
3279
}
3221
- } catch (e, stacktrace) {
3222
- scanData.sendPort.send(
3223
- SyncResponse(syncHeight, LostConnectionSyncStatus()),
3224
- );
3280
3226
- log(stacktrace.toString(), LogLevel.error);
3227
- log(e.toString(), LogLevel.error);
3228
- return;
3281
+ syncHeight = tweakHeight;
3282
+
3283
+ if ((tweakHeight >= scanData.chainTip) || isSingleScan) {
3284
+ endScanningSuccesfully();
3285
+ }
3286
}
3287
3231
- syncHeight = tweakHeight;
3288
+ _scanningStream?.listen((event) => listenFn(event, req));
3289
+ }
3290
3233
- if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) {
3234
- endScanningSuccesfully();
3291
+ if (scanData.rescanHeights != null) {
3292
+ for (final height in scanData.rescanHeights!) {
3293
+ log("rescanning from height: $height", LogLevel.info);
3294
+ scan(height, true);
3295
}
3296
+ } else {
3297
+ scan(scanData.height, scanData.isSingleScan);
3298
}
3237
-
3238
- _scanningStream?.listen((event) => listenFn(event, req));
3299
} catch (e) {
3300
log("Error in _handleScanSilentPayments: $e", LogLevel.error);
3241
- scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus()));
3301
+ if (shouldUpdateSyncStatus)
3302
+ scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus()));
3303
}
3304
}
3305
cw_core/lib/sync_status.dart
+20
-20
@@ -99,29 +99,29 @@ class SyncingSyncStatus extends SyncStatus {
99
return newDuration;
100
}
101
102
- final currentSeconds = lastEtaDuration!.inSeconds;
103
- final newSeconds = newDuration.inSeconds;
104
- final diff = (newSeconds - currentSeconds).abs();
102
+ final currentMs = lastEtaDuration!.inMilliseconds;
103
+ final newMs = newDuration.inMilliseconds;
104
+ final diff = ((newMs - currentMs) / 1000).abs();
105
106
// Apply different smoothing based on the magnitude of change
107
if (diff > 3600) {
108
// If it's more than 1 hour difference, it's a large change so we move by max 30 minutes
109
- final direction = newSeconds > currentSeconds ? 1 : -1;
110
- final maxChange = 30 * 60;
111
- final adjustedSeconds = currentSeconds + (direction * maxChange);
112
- return Duration(seconds: adjustedSeconds);
109
+ final direction = newMs > currentMs ? 1 : -1;
110
+ final maxChange = 30 * 60 * 1000;
111
+ final adjustedMs = currentMs + (direction * maxChange);
112
+ return Duration(milliseconds: adjustedMs);
113
} else if (diff > 300) {
114
// If it's more than 5 minutes difference, it's a medium change so we move by max 2 minutes
115
- final direction = newSeconds > currentSeconds ? 1 : -1;
116
- final maxChange = 2 * 60;
117
- final adjustedSeconds = currentSeconds + (direction * maxChange);
118
- return Duration(seconds: adjustedSeconds);
115
+ final direction = newMs > currentMs ? 1 : -1;
116
+ final maxChange = 2 * 60 * 1000;
117
+ final adjustedMs = currentMs + (direction * maxChange);
118
+ return Duration(milliseconds: adjustedMs);
119
} else if (diff > 60) {
120
- // If it's more than 1 minute difference, it's a small change so we move by max 30 seconds
121
- final direction = newSeconds > currentSeconds ? 1 : -1;
122
- final maxChange = 30;
123
- final adjustedSeconds = currentSeconds + (direction * maxChange);
124
- return Duration(seconds: adjustedSeconds);
120
+ // If it's more than 1 minute difference, it's a small change so we move by max 30 ms
121
+ final direction = newMs > currentMs ? 1 : -1;
122
+ final maxChange = 30 * 1000;
123
+ final adjustedMs = currentMs + (direction * maxChange);
124
+ return Duration(milliseconds: adjustedMs);
125
}
126
127
return newDuration;
@@ -146,8 +146,8 @@ class SyncingSyncStatus extends SyncStatus {
146
return DateTime.now().add(const Duration(days: 2));
147
}
148
int remainingBlocks = this.blocksLeft;
149
- double timeRemainingSeconds = remainingBlocks / rate;
150
- return DateTime.now().add(Duration(seconds: timeRemainingSeconds.round()));
149
+ double timeRemainingMs = remainingBlocks / rate;
150
+ return DateTime.now().add(Duration(milliseconds: timeRemainingMs.round()));
151
}
152
153
// Enhanced block rate calculation with weighted averages
@@ -176,12 +176,12 @@ class SyncingSyncStatus extends SyncStatus {
176
177
final timeDifference = next.key.difference(current.key);
178
179
- if (timeDifference.inSeconds <= 0) continue; // Skip invalid time
179
+ if (timeDifference.inMilliseconds <= 0) continue; // Skip invalid time
180
181
// Weight recent data more heavily (exponential decay)
182
final weight = 1.0 / (1.0 + (sortedData.length - 1 - i) * 0.1);
183
184
- totalWeightedTime += timeDifference.inSeconds * weight;
184
+ totalWeightedTime += timeDifference.inMilliseconds * weight;
185
totalWeightedBlocks += blocksProcessed * weight;
186
totalWeight += weight;
187
}