Monero: keep the sync progress redrawing during wallet2's warm-up (#3575)
What changed, and why it matters
This commit fixes a UI progress-bar issue in the Monero wallet. During the initial warm-up of the underlying wallet engine, the sync progress could stop redrawing and appear frozen. The change also prevents the app from accidentally starting two simultaneous node-height checks and adds better error logging. There is no direct evidence this is a security vulnerability.
Treat as a normal bug-fix/UX improvement. No security-specific action required. If reviewing for release, verify the heartbeat does not emit excessive events and that the in-flight deduplication correctly resets on errors.
Security signals we found
Concurrency hardening: deduplicates in-flight node-height requests to avoid resource exhaustion or race conditions
Error handling added around FFI pointer access and Isolate.run
No security-relevant signals such as buffer overflows, injection, or key handling
Evidence from the diff
The patch refactors node-height refresh logic in cw_monero/lib/api/wallet.dart. It introduces a single in-flight Future (_nodeHeightInFlight) so repeated calls to getNodeHeight()/getNodeHeightSync() do not spawn duplicate Isolate.run() calls to Wallet_daemonBlockChainHeight. It adds a try/catch around the refresh with logging, and changes the sync-listener heartbeat so progress events are emitted at least every 5 ticks even when no new blocks arrive, keeping the UI progress indicator alive during wallet2 warm-up. The diff is purely a robustness/UX improvement; no cryptographic, authentication, or authorization changes are present.
Changed components
cw_monero/lib/api/wallet.dartMonero sync progress UINode height refresh logicInspect captured patch +50 / −14
diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart
index 78793321..7e094235 100644
--- a/cw_monero/lib/api/wallet.dart
+++ b/cw_monero/lib/api/wallet.dart
@@ -165,23 +165,44 @@ int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0;
int cachedNodeHeight = 0;
bool isHeightRefreshing = false;
+
+Future<int>? _nodeHeightInFlight;
+
+Future<int> _refreshNodeHeight() {
+ final inFlight = _nodeHeightInFlight;
+ if (inFlight != null) return inFlight;
+
+ try {
+ final wallet = currentWallet;
+ if (wallet == null) return Future.value(cachedNodeHeight);
+
+ final wptrAddress = wallet.ffiAddress();
+ isHeightRefreshing = true;
+ final f = Isolate.run(() async {
+ return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress));
+ }).then((h) {
+ cachedNodeHeight = h;
+ return h;
+ }).catchError((_) => cachedNodeHeight).whenComplete(() {
+ isHeightRefreshing = false;
+ _nodeHeightInFlight = null;
+ });
+ _nodeHeightInFlight = f;
+ return f;
+ } catch (e) {
+ printV("error refreshing node height: $e");
+ isHeightRefreshing = false;
+ _nodeHeightInFlight = null;
+ return Future.value(cachedNodeHeight);
+ }
+}
+
int getNodeHeightSync() {
if (isHeightRefreshing == false) {
if (cachedNodeHeight != 0 && getWlptr()?.height() == 1) {
return cachedNodeHeight;
}
- (() async {
- try {
- isHeightRefreshing = true;
- final wptrAddress = currentWallet!.ffiAddress();
- cachedNodeHeight = await Isolate.run(() async {
- return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress));
- });
- } catch (_) {
- } finally {
- isHeightRefreshing = false;
- }
- })();
+ unawaited(_refreshNodeHeight());
}
return cachedNodeHeight;
}
@@ -319,6 +340,9 @@ class SyncListener {
int _cachedBlockchainHeight;
int _lastKnownBlockHeight;
int _initialSyncHeight;
+ int _lastKnownNodeHeight = 0;
+ int _ticksSinceEmit = 0;
+ static const int _heartbeatTicks = 5;
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) {
@@ -331,6 +355,8 @@ class SyncListener {
void _start() {
_cachedBlockchainHeight = 0;
_lastKnownBlockHeight = 0;
+ _lastKnownNodeHeight = 0;
+ _ticksSinceEmit = 0;
_initialSyncHeight = 0;
_updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
if (isNewTransactionExist()) {
@@ -355,11 +381,17 @@ class SyncListener {
}
final bchHeight = max(nodeHeight, syncHeight);
// printV("syncHeight: $syncHeight, _lastKnownBlockHeight: $_lastKnownBlockHeight, bchHeight: $bchHeight");
- if (_lastKnownBlockHeight == syncHeight) {
+
+ _ticksSinceEmit++;
+ final nothingChanged = _lastKnownBlockHeight == syncHeight &&
+ _lastKnownNodeHeight == nodeHeight;
+ if (nothingChanged && _ticksSinceEmit < _heartbeatTicks) {
return;
}
+ _ticksSinceEmit = 0;
_lastKnownBlockHeight = syncHeight;
+ _lastKnownNodeHeight = nodeHeight;
final track = bchHeight - _initialSyncHeight;
final diff = track - (bchHeight - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
@@ -410,7 +442,11 @@ void startRefresh() => startRefreshSync();
Future<void> store() async => _storeSync(0);
-Future<int> getNodeHeight() async => getNodeHeightSync();
+Future<int> getNodeHeight() async {
+ final cached = getNodeHeightSync();
+ if (cached != 0) return cached;
+ return _refreshNodeHeight();
+}
void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync();
Why this scored 19/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.