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>
This commit is contained in:
rafael_xmr
2025-09-07 02:34:17 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 198a9a122b
commit 68a5821ad7
2 changed files with 326 additions and 265 deletions
+306 -245
View File
@@ -340,19 +340,21 @@ abstract class ElectrumWalletBase
}
@action
Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
Future<void> _setListeners(int height,
{int? chainTipParam, bool? doSingleScan, List<int>? rescanHeights}) async {
if (this is! BitcoinWallet) return;
if (isHardwareWallet) return;
if (seed?.isEmpty ?? true) return;
final chainTip = chainTipParam ?? await getUpdatedChainTip();
final shouldUpdateSyncStatus = rescanHeights == null || rescanHeights.isEmpty;
if (chainTip == height) {
syncStatus = SyncedSyncStatus();
return;
}
syncStatus = AttemptingScanSyncStatus();
if (shouldUpdateSyncStatus) syncStatus = AttemptingScanSyncStatus();
if (_isolate != null) {
final runningIsolate = await _isolate!;
@@ -383,6 +385,7 @@ abstract class ElectrumWalletBase
.toList(),
isSingleScan: doSingleScan ?? false,
debugLogPath: debugLogPath,
rescanHeights: rescanHeights,
),
);
@@ -406,6 +409,7 @@ abstract class ElectrumWalletBase
existingTxInfo.isReceivedSilentPayment = tx.isReceivedSilentPayment;
existingTxInfo.direction = tx.direction;
existingTxInfo.isPending = tx.isPending;
existingTxInfo.unspents = tx.unspents;
final newUnspents = tx.unspents!
.where((unspent) => !(existingTxInfo.unspents?.any((element) =>
@@ -459,9 +463,9 @@ abstract class ElectrumWalletBase
if (message.syncStatus is SyncingSyncStatus) {
var status = message.syncStatus as SyncingSyncStatus;
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
if (shouldUpdateSyncStatus) syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
} else {
syncStatus = message.syncStatus;
if (shouldUpdateSyncStatus) syncStatus = message.syncStatus;
}
await walletInfo.updateRestoreHeight(message.height);
@@ -491,6 +495,9 @@ abstract class ElectrumWalletBase
);
}
DateTime? _lastSilentPaymentsScan;
static const Duration _silentPaymentsScanDelay = Duration(minutes: 1);
@action
@override
Future<void> startSync() async {
@@ -504,6 +511,30 @@ abstract class ElectrumWalletBase
if (hasSilentPaymentsScanning) {
silentPaymentsScanningActive = alwaysScan ?? false;
await _setInitialHeight();
final now = DateTime.now();
final shouldForceRescan = _lastSilentPaymentsScan == null ||
now.difference(_lastSilentPaymentsScan!) >= _silentPaymentsScanDelay;
// Timer prevents server failure and this infinite looping and requesting
if (shouldForceRescan) {
_lastSilentPaymentsScan = now;
final rescanHeights = <int>[];
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null && tx.unspents!.isNotEmpty)
for (final unspent in tx.unspents!) {
if (unspent.silentPaymentTweak != null && tx.height != null && tx.height! > 0) {
rescanHeights.add(tx.height!);
break;
}
}
});
if (rescanHeights.isNotEmpty)
_setListeners(walletInfo.restoreHeight, rescanHeights: rescanHeights);
}
}
await subscribeForUpdates();
@@ -519,7 +550,9 @@ abstract class ElectrumWalletBase
if (alwaysScan == true) {
setSilentPaymentsScanning(true);
} else {
if (syncStatus is LostConnectionSyncStatus) return;
if (syncStatus is LostConnectionSyncStatus) {
return;
}
syncStatus = SyncedSyncStatus();
}
} catch (e, stacktrace) {
@@ -1574,12 +1607,16 @@ abstract class ElectrumWalletBase
}
unspentCoins = updatedUnspentCoins;
} else {
unspentCoins = handleFailedUtxoFetch(
failedCount: failedCount,
previousUnspentCoins: previousUnspentCoins,
updatedUnspentCoins: updatedUnspentCoins,
results: results,
);
if (updatedUnspentCoins.isEmpty) {
unspentCoins = handleFailedUtxoFetch(
failedCount: failedCount,
previousUnspentCoins: previousUnspentCoins,
updatedUnspentCoins: updatedUnspentCoins,
results: results,
);
} else {
unspentCoins = updatedUnspentCoins;
}
}
final currentWalletUnspentCoins =
@@ -2882,6 +2919,7 @@ class ScanData {
final List<int> labelIndexes;
final bool isSingleScan;
final String debugLogPath;
final List<int>? rescanHeights;
ScanData({
required this.sendPort,
@@ -2896,6 +2934,7 @@ class ScanData {
required this.labelIndexes,
required this.isSingleScan,
required this.debugLogPath,
required this.rescanHeights,
});
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
@@ -2912,6 +2951,7 @@ class ScanData {
labelIndexes: scanData.labelIndexes,
isSingleScan: scanData.isSingleScan,
debugLogPath: scanData.debugLogPath,
rescanHeights: scanData.rescanHeights,
);
}
}
@@ -2924,6 +2964,9 @@ class SyncResponse {
}
Future<void> _handleScanSilentPayments(ScanData scanData) async {
final shouldUpdateSyncStatus = scanData.rescanHeights == null || scanData.rescanHeights!.isEmpty;
final hasForcedRescanHeights = !shouldUpdateSyncStatus;
var node = Uri.parse("tcp://electrs.cakewallet.com:50001");
void log(String message, LogLevel level) {
@@ -2939,9 +2982,6 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
log("connected to ${node.toString()}", LogLevel.info);
int syncHeight = scanData.height;
int initialSyncHeight = syncHeight;
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
@@ -2955,290 +2995,311 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
LogLevel.info,
);
int getCountToScanPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
void scan(int syncHeight, bool isSingleScan) async {
int initialSyncHeight = syncHeight;
int getCountToScanPerRequest(int syncHeight) {
if (isSingleScan) {
return 1;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
// Initial status UI update, send how many blocks in total to scan
if (shouldUpdateSyncStatus)
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
// 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: hasForcedRescanHeights,
);
final req = ElectrumTweaksSubscribe(
height: syncHeight,
count: getCountToScanPerRequest(syncHeight),
historicalMode: false,
);
var _scanningStream = await scanningClient.subscribe(req);
log(
"initial request: height: $syncHeight, count: ${getCountToScanPerRequest(syncHeight)}",
LogLevel.info,
);
void endScanningSuccesfully() {
if (scanData.isSingleScan)
scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
else
scanData.sendPort.send(
SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)),
);
_scanningStream?.close();
_scanningStream = null;
var _scanningStream = await scanningClient.subscribe(req);
log(
"ended: syncHeight: $syncHeight, chainTip: ${scanData.chainTip}, isSingleScan: ${scanData.isSingleScan}",
"initial request: height: $syncHeight, count: ${getCountToScanPerRequest(syncHeight)}",
LogLevel.info,
);
}
void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) async {
final response = req.onResponse(event);
void endScanningSuccesfully() {
if (isSingleScan) {
scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
} else {
scanData.sendPort.send(
SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)),
);
}
_scanningStream?.close();
_scanningStream = null;
if (response == null || _scanningStream == null) {
log(
"ending: response = $response, stream = $_scanningStream",
LogLevel.error,
"ended: syncHeight: $syncHeight, chainTip: ${scanData.chainTip}, isSingleScan: ${isSingleScan}",
LogLevel.info,
);
return;
}
// is success or error msg
final noData = response.message != null;
void listenFn(Map<String, dynamic> event, ElectrumTweaksSubscribe req) async {
final response = req.onResponse(event);
if (noData) {
if (scanData.isSingleScan) {
log("ending: noData and isSingleScan", LogLevel.info);
endScanningSuccesfully();
if (response == null || _scanningStream == null) {
log(
"ending: response = $response, stream = $_scanningStream",
LogLevel.error,
);
return;
}
// re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1;
// is success or error msg
final noData = response.message != null;
if (nextHeight <= scanData.chainTip) {
log(
"resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
LogLevel.info,
);
if (noData) {
if (isSingleScan) {
log("ending: noData and isSingleScan", LogLevel.info);
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()),
);
endScanningSuccesfully();
return;
}
}
log(
"ending: resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
LogLevel.info,
);
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;
var blockDate = DateTime.now();
bool isDateNow = true;
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;
}
// re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1;
if (nextHeight <= scanData.chainTip) {
log(
"FOUND: addToWallet: ${addToWallet.length}, txid: $txid, tweak: $tweak, height: $tweakHeight",
"resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
LogLevel.info,
);
// Every tx in the block has the same date (the block date)
// So, if blockDate exists, reuse
if (isDateNow) {
try {
final tweakBlockHash = await ProxyWrapper()
.get(
clearnetUri: Uri.parse(
"https://mempool.cakewallet.com/api/v1/block-height/$tweakHeight",
),
)
.timeout(Duration(seconds: 15));
final blockResponse = await ProxyWrapper()
.get(
clearnetUri: Uri.parse(
"https://mempool.cakewallet.com/api/v1/block/${tweakBlockHash.body}",
),
)
.timeout(Duration(seconds: 15));
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
blockDate = DateTime.fromMillisecondsSinceEpoch(
int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()) * 1000,
);
isDateNow = false;
}
} catch (e, stacktrace) {
printV(stacktrace);
printV(e.toString());
}
}
// 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,
// TODO: fetch block data and get the date from it
date: scanData.network == BitcoinNetwork.mainnet
? (isDateNow ? getDateByBitcoinHeight(tweakHeight) : blockDate)
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
isReceivedSilentPayment: true,
isPending: false,
unspents: [],
final nextStream = scanningClient.subscribe(
ElectrumTweaksSubscribe(
height: nextHeight,
count: getCountToScanPerRequest(nextHeight),
historicalMode: hasForcedRescanHeights,
),
);
List<BitcoinUnspent> unspents = [];
if (nextStream != null) {
nextStream.listen((event) => listenFn(event, req));
} else {
if (shouldUpdateSyncStatus)
scanData.sendPort.send(
SyncResponse(scanData.height, LostConnectionSyncStatus()),
);
}
}
addToWallet.forEach((BSpend, scanResultPerLabel) {
scanResultPerLabel.forEach((label, scanOutput) {
final labelValue = label == "None" ? null : label.toString();
log(
"ending: resubscribing: nextHeight: $nextHeight, count: ${getCountToScanPerRequest(nextHeight)}",
LogLevel.info,
);
return;
}
(scanOutput as Map<String, dynamic>).forEach((outputPubkey, tweak) {
final t_k = tweak as String;
final tweakHeight = response.block;
final receivingOutputAddress = ECPublic.fromHex(outputPubkey)
.toTaprootAddress(tweak: false)
.toAddress(scanData.network);
// Continuous status UI update, send how many blocks left to scan
final syncingStatus = isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight);
final matchingOutput = outputPubkeys[outputPubkey]!;
final amount = matchingOutput.amount;
final pos = matchingOutput.vout;
if (shouldUpdateSyncStatus) scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere(
// (receiver) => receiver.B_spend.toHex() == BSpend.toString(),
// );
try {
final blockTweaks = response.blockTweaks;
// final labelIndex = labelValue != null ? scanData.labels[label] : 0;
// final balance = ElectrumBalance();
// balance.confirmed = amount;
var blockDate = DateTime.now();
bool isDateNow = true;
final receivedAddressRecord = BitcoinSilentPaymentAddressRecord(
receivingOutputAddress,
index: 0,
isHidden: false,
isUsed: true,
network: scanData.network,
silentPaymentTweak: t_k,
type: SegwitAddresType.p2tr,
txCount: 1,
balance: amount,
);
for (final txid in blockTweaks.keys) {
final tweakData = blockTweaks[txid];
final outputPubkeys = tweakData!.outputPubkeys;
final tweak = tweakData.tweak;
final unspent = BitcoinSilentPaymentsUnspent(
receivedAddressRecord,
txid,
amount,
pos,
silentPaymentTweak: t_k,
silentPaymentLabel: labelValue,
);
try {
final addToWallet = {};
unspents.add(unspent);
txInfo.unspents!.add(unspent);
txInfo.amount += unspent.value;
// 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;
}
log(
"FOUND: addToWallet: ${addToWallet.length}, txid: $txid, tweak: $tweak, height: $tweakHeight",
LogLevel.info,
);
// Every tx in the block has the same date (the block date)
// So, if blockDate exists, reuse
if (isDateNow) {
try {
final tweakBlockHash = await ProxyWrapper()
.get(
clearnetUri: Uri.parse(
"https://mempool.cakewallet.com/api/v1/block-height/$tweakHeight",
),
)
.timeout(Duration(seconds: 15));
final blockResponse = await ProxyWrapper()
.get(
clearnetUri: Uri.parse(
"https://mempool.cakewallet.com/api/v1/block/${tweakBlockHash.body}",
),
)
.timeout(Duration(seconds: 15));
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
blockDate = DateTime.fromMillisecondsSinceEpoch(
int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()) * 1000,
);
isDateNow = false;
}
} catch (e, stacktrace) {
printV(stacktrace);
printV(e.toString());
}
}
// 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,
// TODO: fetch block data and get the date from it
date: scanData.network == BitcoinNetwork.mainnet
? (isDateNow ? getDateByBitcoinHeight(tweakHeight) : blockDate)
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
isReceivedSilentPayment: true,
isPending: false,
unspents: [],
);
List<BitcoinUnspent> unspents = [];
addToWallet.forEach((BSpend, scanResultPerLabel) {
scanResultPerLabel.forEach((label, scanOutput) {
final labelValue = label == "None" ? null : label.toString();
(scanOutput as Map<String, dynamic>).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 spent = matchingOutput.spendingInput;
// 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: SegwitAddresType.p2tr,
txCount: 1,
balance: amount,
);
final unspent = BitcoinSilentPaymentsUnspent(
receivedAddressRecord,
txid,
amount,
pos,
silentPaymentTweak: t_k,
silentPaymentLabel: labelValue,
);
if (spent == null) {
unspents.add(unspent);
txInfo.unspents!.add(unspent);
}
txInfo.amount += unspent.value;
});
});
});
});
scanData.sendPort.send({txInfo.id: txInfo});
} catch (e, stacktrace) {
scanData.sendPort.send({txInfo.id: txInfo});
} catch (e, stacktrace) {
if (shouldUpdateSyncStatus)
scanData.sendPort.send(
SyncResponse(syncHeight, LostConnectionSyncStatus()),
);
log(stacktrace.toString(), LogLevel.error);
log(e.toString(), LogLevel.error);
return;
}
}
} catch (e, stacktrace) {
if (shouldUpdateSyncStatus)
scanData.sendPort.send(
SyncResponse(syncHeight, LostConnectionSyncStatus()),
);
log(stacktrace.toString(), LogLevel.error);
log(e.toString(), LogLevel.error);
return;
}
log(stacktrace.toString(), LogLevel.error);
log(e.toString(), LogLevel.error);
return;
}
} catch (e, stacktrace) {
scanData.sendPort.send(
SyncResponse(syncHeight, LostConnectionSyncStatus()),
);
log(stacktrace.toString(), LogLevel.error);
log(e.toString(), LogLevel.error);
return;
syncHeight = tweakHeight;
if ((tweakHeight >= scanData.chainTip) || isSingleScan) {
endScanningSuccesfully();
}
}
syncHeight = tweakHeight;
if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) {
endScanningSuccesfully();
}
_scanningStream?.listen((event) => listenFn(event, req));
}
_scanningStream?.listen((event) => listenFn(event, req));
if (scanData.rescanHeights != null) {
for (final height in scanData.rescanHeights!) {
log("rescanning from height: $height", LogLevel.info);
scan(height, true);
}
} else {
scan(scanData.height, scanData.isSingleScan);
}
} catch (e) {
log("Error in _handleScanSilentPayments: $e", LogLevel.error);
scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus()));
if (shouldUpdateSyncStatus)
scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus()));
}
}
+20 -20
View File
@@ -99,29 +99,29 @@ class SyncingSyncStatus extends SyncStatus {
return newDuration;
}
final currentSeconds = lastEtaDuration!.inSeconds;
final newSeconds = newDuration.inSeconds;
final diff = (newSeconds - currentSeconds).abs();
final currentMs = lastEtaDuration!.inMilliseconds;
final newMs = newDuration.inMilliseconds;
final diff = ((newMs - currentMs) / 1000).abs();
// Apply different smoothing based on the magnitude of change
if (diff > 3600) {
// If it's more than 1 hour difference, it's a large change so we move by max 30 minutes
final direction = newSeconds > currentSeconds ? 1 : -1;
final maxChange = 30 * 60;
final adjustedSeconds = currentSeconds + (direction * maxChange);
return Duration(seconds: adjustedSeconds);
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 30 * 60 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
} else if (diff > 300) {
// If it's more than 5 minutes difference, it's a medium change so we move by max 2 minutes
final direction = newSeconds > currentSeconds ? 1 : -1;
final maxChange = 2 * 60;
final adjustedSeconds = currentSeconds + (direction * maxChange);
return Duration(seconds: adjustedSeconds);
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 2 * 60 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
} else if (diff > 60) {
// If it's more than 1 minute difference, it's a small change so we move by max 30 seconds
final direction = newSeconds > currentSeconds ? 1 : -1;
final maxChange = 30;
final adjustedSeconds = currentSeconds + (direction * maxChange);
return Duration(seconds: adjustedSeconds);
// If it's more than 1 minute difference, it's a small change so we move by max 30 ms
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 30 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
}
return newDuration;
@@ -146,8 +146,8 @@ class SyncingSyncStatus extends SyncStatus {
return DateTime.now().add(const Duration(days: 2));
}
int remainingBlocks = this.blocksLeft;
double timeRemainingSeconds = remainingBlocks / rate;
return DateTime.now().add(Duration(seconds: timeRemainingSeconds.round()));
double timeRemainingMs = remainingBlocks / rate;
return DateTime.now().add(Duration(milliseconds: timeRemainingMs.round()));
}
// Enhanced block rate calculation with weighted averages
@@ -176,12 +176,12 @@ class SyncingSyncStatus extends SyncStatus {
final timeDifference = next.key.difference(current.key);
if (timeDifference.inSeconds <= 0) continue; // Skip invalid time
if (timeDifference.inMilliseconds <= 0) continue; // Skip invalid time
// Weight recent data more heavily (exponential decay)
final weight = 1.0 / (1.0 + (sortedData.length - 1 - i) * 0.1);
totalWeightedTime += timeDifference.inSeconds * weight;
totalWeightedTime += timeDifference.inMilliseconds * weight;
totalWeightedBlocks += blocksProcessed * weight;
totalWeight += weight;
}