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 }