From e48cd0df413b49603fd68da59184930eedb1034b Mon Sep 17 00:00:00 2001 From: fossephate Date: Mon, 23 Sep 2024 14:26:56 -0700 Subject: [PATCH] fix mweb crash on non-fully deleted mweb cache, sync status ETA, other connection fixes --- cw_bitcoin/lib/litecoin_wallet.dart | 25 +++--- cw_bitcoin/lib/litecoin_wallet_service.dart | 18 +++- cw_core/lib/sync_status.dart | 89 ++++++++++++++++++- lib/core/sync_status_title.dart | 15 +++- .../dashboard/dashboard_view_model.dart | 1 + 5 files changed, 126 insertions(+), 22 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 533fb346b..405c1bc89 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -97,6 +97,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { late final Bip32Slip10Secp256k1 mwebHd; late final Box mwebUtxosBox; Timer? _syncTimer; + Timer? _stuckSyncTimer; Timer? _feeRatesTimer; StreamSubscription? _utxoStream; late RpcClient _stub; @@ -314,26 +315,27 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } await transactionHistory.save(); } + return; } }); // setup a watch dog to restart the sync process if it gets stuck: List lastFewProgresses = []; - Timer.periodic(const Duration(seconds: 10), (timer) async { + _stuckSyncTimer?.cancel(); + _stuckSyncTimer = Timer.periodic(const Duration(seconds: 10), (timer) async { if (syncStatus is! SyncingSyncStatus) return; - if (syncStatus.progress() > 0.98) return; + if (syncStatus.progress() > 0.98) return; // don't check if we're close to synced lastFewProgresses.add(syncStatus.progress()); - if (lastFewProgresses.length < 4) return; - // limit list size to 4: - while (lastFewProgresses.length > 4) { + if (lastFewProgresses.length < 10) return; + // limit list size to 10: + while (lastFewProgresses.length > 10) { lastFewProgresses.removeAt(0); } - // if the progress is the same over the last 40 seconds, restart the sync: + // if the progress is the same over the last 100 seconds, restart the sync: if (lastFewProgresses.every((p) => p == lastFewProgresses.first)) { print("mweb syncing is stuck, restarting..."); + syncStatus = LostConnectionSyncStatus(); await stopSync(); - startSync(); - timer.cancel(); } }); } @@ -870,10 +872,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final utxo = unspentCoins .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); - if (txInput.sequence.isEmpty) { - isHogEx = false; - } - // TODO: detect actual hog-ex inputs // print(txInput.sequence); // print(txInput.txIndex); @@ -949,6 +947,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future close() async { await super.close(); + await stopSync(); + _stuckSyncTimer?.cancel(); + _feeRatesTimer?.cancel(); _syncTimer?.cancel(); _utxoStream?.cancel(); } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 67a503de7..c659dd658 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -21,7 +21,8 @@ class LitecoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + LitecoinWalletService( + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; @@ -66,6 +67,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -103,13 +105,21 @@ class LitecoinWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); - // if there are no more litecoin wallets left, delete the neutrino db: + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd: if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) { - final appDir = await getApplicationSupportDirectory(); - File neturinoDb = File('${appDir.path}/neutrino.db'); + final appDirPath = (await getApplicationSupportDirectory()).path; + File neturinoDb = File('$appDirPath/neutrino.db'); + File blockHeaders = File('$appDirPath/block_headers.bin'); + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); if (neturinoDb.existsSync()) { neturinoDb.deleteSync(); } + if (blockHeaders.existsSync()) { + blockHeaders.deleteSync(); + } + if (regFilterHeaders.existsSync()) { + regFilterHeaders.deleteSync(); + } } } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 788309d8c..81c4a8564 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -1,6 +1,10 @@ abstract class SyncStatus { const SyncStatus(); double progress(); + + String formattedProgress() { + return "${(progress() * 100).toStringAsFixed(2)}%"; + } } class StartingScanSyncStatus extends SyncStatus { @@ -12,10 +16,12 @@ class StartingScanSyncStatus extends SyncStatus { } class SyncingSyncStatus extends SyncStatus { - SyncingSyncStatus(this.blocksLeft, this.ptc); + SyncingSyncStatus(this.blocksLeft, this.ptc) { + updateEtaHistory(blocksLeft); + } - final double ptc; - final int blocksLeft; + double ptc; + int blocksLeft; @override double progress() => ptc; @@ -32,6 +38,83 @@ class SyncingSyncStatus extends SyncStatus { // sum 1 because if at the chain tip, will say "0 blocks left" return SyncingSyncStatus(left + 1, ptc); } + + void updateEtaHistory(int blocksLeft) { + blockHistory[DateTime.now()] = blocksLeft; + + // Keep only the last 5 entries to limit memory usage + if (blockHistory.length > 5) { + var oldestKey = blockHistory.keys.reduce((a, b) => a.isBefore(b) ? a : b); + blockHistory.remove(oldestKey); + } + } + + static Map blockHistory = {}; + + DateTime? estimatedCompletionTime; + Duration? estimatedCompletionDuration; + + Duration getEtaDuration() { + DateTime now = DateTime.now(); + DateTime? completionTime = calculateETA(); + return completionTime.difference(now); + } + + String? getFormattedEta() { + // throw out any entries that are more than a minute old: + blockHistory.removeWhere( + (key, value) => key.isBefore(DateTime.now().subtract(const Duration(minutes: 1)))); + + if (blockHistory.length < 2) return null; + Duration? duration = getEtaDuration(); + + if (duration.inDays > 0) { + return null; + } + + String twoDigits(int n) => n.toString().padLeft(2, '0'); + + final hours = twoDigits(duration.inHours); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + if (hours == '00') { + return '${minutes}m${seconds}s'; + } + return '${hours}h${minutes}m${seconds}s'; + } + + // Calculate the rate of block processing (blocks per second) + double calculateRate() { + List timestamps = blockHistory.keys.toList(); + List blockCounts = blockHistory.values.toList(); + + double totalTimeMinutes = 0; + int totalBlocksProcessed = 0; + + for (int i = 0; i < blockCounts.length - 1; i++) { + int blocksProcessed = blockCounts[i + 1] - blockCounts[i]; + Duration timeDifference = timestamps[i].difference(timestamps[i + 1]); + totalTimeMinutes += timeDifference.inSeconds; + totalBlocksProcessed += blocksProcessed; + } + + if (totalTimeMinutes == 0 || totalBlocksProcessed == 0) { + return 0; + } + + return totalBlocksProcessed / totalTimeMinutes; // Blocks per second + } + + // Calculate the ETA + DateTime calculateETA() { + double rate = calculateRate(); + if (rate < 0.01) { + return DateTime.now().add(const Duration(days: 1)); + } + int remainingBlocks = this.blocksLeft; + double timeRemainingSeconds = remainingBlocks / rate; + return DateTime.now().add(Duration(seconds: timeRemainingSeconds.round())); + } } class SyncedSyncStatus extends SyncStatus { diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 4582f7b1f..53240875a 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -3,9 +3,18 @@ import 'package:cw_core/sync_status.dart'; String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is SyncingSyncStatus) { - return syncStatus.blocksLeft == 1 - ? S.current.block_remaining - : S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + + if (syncStatus.blocksLeft == 1) { + return S.current.block_remaining; + } + + String eta = syncStatus.getFormattedEta() ?? ''; + + if (eta.isEmpty) { + return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + } else { + return "${syncStatus.formattedProgress()} - $eta"; + } } if (syncStatus is SyncedTipSyncStatus) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index b7a9bbbbd..21a167e2a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -258,6 +258,7 @@ abstract class DashboardViewModelBase with Store { if (hasMweb) { mwebScanningActive = bitcoin!.getMwebEnabled(wallet); + settingsStore.mwebEnabled = mwebScanningActive; reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { if (alwaysScan) { mwebScanningActive = true;