What changed, and why it matters
This commit fixes how the wallet's sync progress bar estimates and displays remaining time. It changes when the app shows 'blocks remaining' versus a percentage, smooths the ETA calculation, and makes the connection-status check correctly show 'disconnected' if the network drops while syncing. There is no direct security vulnerability being patched; it is a user-experience and reliability improvement for sync feedback.
Treat as a routine UX/reliability fix. Reviewers may optionally verify that blockHistory is always cleared on wallet switch and that the new connectivity check behaves correctly on all supported platforms, but no security response is required.
Security signals we found
Connection-state handling tightened: disconnected status now overrides syncing/connecting states, reducing risk of misleading 'syncing' UI when the node is unreachable.
Connectivity API usage updated from equality to .contains(ConnectivityResult.none), which may fix false-positive offline detection on newer connectivity_plus versions.
ETA history no longer auto-expires by age; it is explicitly cleared on wallet change and sync completion/failure, which reduces stale-data risk but relies on correct lifecycle calls.
Evidence from the diff
The patch refactors sync-status presentation and ETA logic. Key changes: (1) ElectrumWallet now sets NotConnectedSyncStatus on any disconnect, overriding prior syncing/connecting states. (2) SyncingSyncStatus tracks a global sync start time and shows blocks remaining for the first 15 seconds before switching to percentage; it also removes the 2-minute data-age cleanup and 30-entry cap on blockHistory, instead clearing history on wallet change and sync completion/failure. (3) check_connection.dart now treats connectivityResult as a List/Set and checks .contains(ConnectivityResult.none) rather than equality, matching connectivity_plus API changes. (4) Reactions reset ETA state on Connected/Synced/Failed transitions and clear block history on wallet change and sync success. No cryptographic, authentication, or asset-handling code is modified.
Changed components
cw_bitcoin/lib/electrum_wallet.dartcw_core/lib/sync_status.dartlib/core/sync_status_title.dartlib/reactions/check_connection.dartlib/reactions/on_current_wallet_change.dartlib/reactions/on_wallet_sync_status_change.dartInspect captured patch +45 / −21
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 816833cc..7a0fecd3 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -2657,9 +2657,8 @@ abstract class ElectrumWalletBase
break;
case electrum.ConnectionStatus.disconnected:
- if (syncStatus is! NotConnectedSyncStatus &&
- syncStatus is! ConnectingSyncStatus &&
- syncStatus is! SyncronizingSyncStatus) {
+ // Always show disconnected status when connection is lost, regardless of current sync state
+ if (syncStatus is! NotConnectedSyncStatus) {
syncStatus = NotConnectedSyncStatus();
}
break;
diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart
index 0126ea7f..e85db9a0 100644
--- a/cw_core/lib/sync_status.dart
+++ b/cw_core/lib/sync_status.dart
@@ -21,6 +21,7 @@ class StartingScanSyncStatus extends SyncStatus {
class SyncingSyncStatus extends SyncStatus {
SyncingSyncStatus(this.blocksLeft, this.ptc) {
updateEtaHistory(blocksLeft);
+ _globalSyncStartTime ??= DateTime.now();
}
double ptc;
@@ -32,6 +33,19 @@ class SyncingSyncStatus extends SyncStatus {
@override
String toString() => '$blocksLeft';
+ /// Returns true if we should show blocks remaining instead of percentage
+ /// Shows blocks remaining for the first 15 seconds of syncing
+ bool shouldShowBlocksRemaining() {
+ if (_globalSyncStartTime == null) return true;
+ final elapsed = DateTime.now().difference(_globalSyncStartTime!);
+ return elapsed.inSeconds < 15;
+ }
+
+ /// Reset the global sync start time (call when sync completes or fails)
+ static void resetSyncStartTime() {
+ _globalSyncStartTime = null;
+ }
+
factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
final track = chainTip - initialSyncHeight;
final diff = track - (chainTip - syncHeight);
@@ -45,21 +59,14 @@ class SyncingSyncStatus extends SyncStatus {
static void updateEtaHistory(int blocksLeft) {
blockHistory[DateTime.now()] = blocksLeft;
-
- // keep only the last 30 entries (gives us better statistical accuracy)
- while (blockHistory.length > 30) {
- blockHistory.remove(blockHistory.keys.first);
- }
}
static Map<DateTime, int> blockHistory = {};
static Duration? lastEtaDuration;
static const int _minDataPoints = 3;
- static const int _maxDataAgeMinutes = 2;
+ static DateTime? _globalSyncStartTime;
String? getFormattedEtaWithPlaceholder() {
- _cleanOldEntries();
-
// If we have enough data, show actual ETA
if (blockHistory.length >= _minDataPoints) {
final eta = getFormattedEta();
@@ -70,11 +77,6 @@ class SyncingSyncStatus extends SyncStatus {
return '--:--';
}
- void _cleanOldEntries() {
- final cutoffTime = DateTime.now().subtract(Duration(minutes: _maxDataAgeMinutes));
- blockHistory.removeWhere((key, value) => key.isBefore(cutoffTime));
- }
-
String? getFormattedEta() {
Duration? duration = getEtaDuration();
diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart
index 44628ccd..1cfc6613 100644
--- a/lib/core/sync_status_title.dart
+++ b/lib/core/sync_status_title.dart
@@ -4,6 +4,20 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDisplayMode) {
if (syncStatus is SyncingSyncStatus) {
+ // Show blocks remaining for the first 3 seconds, then switch to percentage
+ if (syncStatus.shouldShowBlocksRemaining()) {
+ if (syncStatus.blocksLeft == 1) {
+ return S.current.block_remaining;
+ }
+ return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
+ }
+
+ // After 3 seconds, show percentage-based display
+ // Don't show ETA for very few blocks (less than 100) to avoid inconsistency
+ if (syncStatus.blocksLeft < 100) {
+ return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
+ }
+
if (syncStatus.blocksLeft == 1) {
return S.current.block_remaining;
}
diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart
index 4591bb9a..e7e62c87 100644
--- a/lib/reactions/check_connection.dart
+++ b/lib/reactions/check_connection.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/utils/tor.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -8,8 +7,6 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:get_it/get_it.dart';
Timer? _checkConnectionTimer;
@@ -29,7 +26,7 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore
try {
final connectivityResult = await (Connectivity().checkConnectivity());
- if (connectivityResult == ConnectivityResult.none) {
+ if (connectivityResult.contains(ConnectivityResult.none)) {
wallet.syncStatus = FailedSyncStatus();
return;
}
diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart
index d6c872c0..4b862d18 100644
--- a/lib/reactions/on_current_wallet_change.dart
+++ b/lib/reactions/on_current_wallet_change.dart
@@ -11,6 +11,7 @@ import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/tor.dart';
import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_info.dart';
@@ -92,6 +93,7 @@ void startCurrentWalletChangeReaction(
}
await wallet.connectToNode(node: node);
+ SyncingSyncStatus.blockHistory.clear();
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
final powNode = settingsStore.getCurrentPowNode(wallet.type);
await wallet.connectToPowNode(node: powNode);
diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart
index bbf4f44f..6d31cb22 100644
--- a/lib/reactions/on_wallet_sync_status_change.dart
+++ b/lib/reactions/on_wallet_sync_status_change.dart
@@ -19,13 +19,23 @@ void startWalletSyncStatusChangeReaction(
_onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async {
try {
if (status is ConnectedSyncStatus) {
+ SyncingSyncStatus.resetSyncStartTime();
await wallet.startSync();
}
+
if (status is SyncingSyncStatus || status is ProcessingSyncStatus) {
await WakelockPlus.enable();
}
- if (status is SyncedSyncStatus || status is FailedSyncStatus) {
+
+ if (status is SyncedSyncStatus) {
+ await WakelockPlus.disable();
+ SyncingSyncStatus.resetSyncStartTime();
+ SyncingSyncStatus.blockHistory.clear();
+ }
+
+ if (status is FailedSyncStatus) {
await WakelockPlus.disable();
+ SyncingSyncStatus.resetSyncStartTime();
}
if (status is SyncedSyncStatus &&
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.