fix mweb crash on non-fully deleted mweb cache, sync status ETA, other connection fixes

This commit is contained in:
fossephate 2024-09-23 14:26:56 -07:00
parent 8d7bad37f4
commit e48cd0df41
5 changed files with 126 additions and 22 deletions

View file

@ -97,6 +97,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
late final Bip32Slip10Secp256k1 mwebHd; late final Bip32Slip10Secp256k1 mwebHd;
late final Box<MwebUtxo> mwebUtxosBox; late final Box<MwebUtxo> mwebUtxosBox;
Timer? _syncTimer; Timer? _syncTimer;
Timer? _stuckSyncTimer;
Timer? _feeRatesTimer; Timer? _feeRatesTimer;
StreamSubscription<Utxo>? _utxoStream; StreamSubscription<Utxo>? _utxoStream;
late RpcClient _stub; late RpcClient _stub;
@ -314,26 +315,27 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
} }
await transactionHistory.save(); await transactionHistory.save();
} }
return;
} }
}); });
// setup a watch dog to restart the sync process if it gets stuck: // setup a watch dog to restart the sync process if it gets stuck:
List<double> lastFewProgresses = []; List<double> 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 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()); lastFewProgresses.add(syncStatus.progress());
if (lastFewProgresses.length < 4) return; if (lastFewProgresses.length < 10) return;
// limit list size to 4: // limit list size to 10:
while (lastFewProgresses.length > 4) { while (lastFewProgresses.length > 10) {
lastFewProgresses.removeAt(0); 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)) { if (lastFewProgresses.every((p) => p == lastFewProgresses.first)) {
print("mweb syncing is stuck, restarting..."); print("mweb syncing is stuck, restarting...");
syncStatus = LostConnectionSyncStatus();
await stopSync(); await stopSync();
startSync();
timer.cancel();
} }
}); });
} }
@ -870,10 +872,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final utxo = unspentCoins final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex);
if (txInput.sequence.isEmpty) {
isHogEx = false;
}
// TODO: detect actual hog-ex inputs // TODO: detect actual hog-ex inputs
// print(txInput.sequence); // print(txInput.sequence);
// print(txInput.txIndex); // print(txInput.txIndex);
@ -949,6 +947,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override @override
Future<void> close() async { Future<void> close() async {
await super.close(); await super.close();
await stopSync();
_stuckSyncTimer?.cancel();
_feeRatesTimer?.cancel();
_syncTimer?.cancel(); _syncTimer?.cancel();
_utxoStream?.cancel(); _utxoStream?.cancel();
} }

View file

@ -21,7 +21,8 @@ class LitecoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromWIFCredentials,
BitcoinNewWalletCredentials> { BitcoinNewWalletCredentials> {
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); LitecoinWalletService(
this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
@ -66,6 +67,7 @@ class LitecoinWalletService extends WalletService<
@override @override
Future<LitecoinWallet> openWallet(String name, String password) async { Future<LitecoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
@ -103,13 +105,21 @@ class LitecoinWalletService extends WalletService<
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key); 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) { if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) {
final appDir = await getApplicationSupportDirectory(); final appDirPath = (await getApplicationSupportDirectory()).path;
File neturinoDb = File('${appDir.path}/neutrino.db'); File neturinoDb = File('$appDirPath/neutrino.db');
File blockHeaders = File('$appDirPath/block_headers.bin');
File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin');
if (neturinoDb.existsSync()) { if (neturinoDb.existsSync()) {
neturinoDb.deleteSync(); neturinoDb.deleteSync();
} }
if (blockHeaders.existsSync()) {
blockHeaders.deleteSync();
}
if (regFilterHeaders.existsSync()) {
regFilterHeaders.deleteSync();
}
} }
} }

View file

@ -1,6 +1,10 @@
abstract class SyncStatus { abstract class SyncStatus {
const SyncStatus(); const SyncStatus();
double progress(); double progress();
String formattedProgress() {
return "${(progress() * 100).toStringAsFixed(2)}%";
}
} }
class StartingScanSyncStatus extends SyncStatus { class StartingScanSyncStatus extends SyncStatus {
@ -12,10 +16,12 @@ class StartingScanSyncStatus extends SyncStatus {
} }
class SyncingSyncStatus extends SyncStatus { class SyncingSyncStatus extends SyncStatus {
SyncingSyncStatus(this.blocksLeft, this.ptc); SyncingSyncStatus(this.blocksLeft, this.ptc) {
updateEtaHistory(blocksLeft);
}
final double ptc; double ptc;
final int blocksLeft; int blocksLeft;
@override @override
double progress() => ptc; double progress() => ptc;
@ -32,6 +38,83 @@ class SyncingSyncStatus extends SyncStatus {
// sum 1 because if at the chain tip, will say "0 blocks left" // sum 1 because if at the chain tip, will say "0 blocks left"
return SyncingSyncStatus(left + 1, ptc); 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<DateTime, int> 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<DateTime> timestamps = blockHistory.keys.toList();
List<int> 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 { class SyncedSyncStatus extends SyncStatus {

View file

@ -3,9 +3,18 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus) { String syncStatusTitle(SyncStatus syncStatus) {
if (syncStatus is SyncingSyncStatus) { if (syncStatus is SyncingSyncStatus) {
return syncStatus.blocksLeft == 1
? S.current.block_remaining if (syncStatus.blocksLeft == 1) {
: S.current.Blocks_remaining('${syncStatus.blocksLeft}'); 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) { if (syncStatus is SyncedTipSyncStatus) {

View file

@ -258,6 +258,7 @@ abstract class DashboardViewModelBase with Store {
if (hasMweb) { if (hasMweb) {
mwebScanningActive = bitcoin!.getMwebEnabled(wallet); mwebScanningActive = bitcoin!.getMwebEnabled(wallet);
settingsStore.mwebEnabled = mwebScanningActive;
reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) { reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) {
if (alwaysScan) { if (alwaysScan) {
mwebScanningActive = true; mwebScanningActive = true;