From 439b93730af7a7e76dc7f01a3d6c7da393bd39fa Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 2 Mar 2026 14:18:14 -0600 Subject: [PATCH 1/4] fix: guard against null wallet in updateNode() for Monero-family wallets --- lib/wallets/wallet/intermediate/lib_monero_wallet.dart | 4 ++++ lib/wallets/wallet/intermediate/lib_salvium_wallet.dart | 4 ++++ lib/wallets/wallet/intermediate/lib_wownero_wallet.dart | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 6c0c49884..6725736e1 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -504,6 +504,10 @@ abstract class LibMoneroWallet @override Future updateNode() async { + if (wallet == null) { + return; + } + final node = getCurrentNode(); if (await _torNodeMismatchGuard(node)) { diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 4517f1498..6a6f57812 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -481,6 +481,10 @@ abstract class LibSalviumWallet @override Future updateNode() async { + if (wallet == null) { + return; + } + final node = getCurrentNode(); if (_torNodeMismatchGuard(node)) { diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 5ebd2191a..0585dd71b 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -506,6 +506,10 @@ abstract class LibWowneroWallet @override Future updateNode() async { + if (wallet == null) { + return; + } + final node = getCurrentNode(); if (_torNodeMismatchGuard(node)) { From 7b1f979c89cf4e2dd535af05d92ddd5128e1d9fa Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 May 2026 16:42:06 -0500 Subject: [PATCH 2/4] fix: manage Tor listener lifecycle in open()/exit() for Monero-family wallets --- .../intermediate/lib_monero_wallet.dart | 29 +++++++++++++++---- .../intermediate/lib_salvium_wallet.dart | 27 +++++++++++++---- .../intermediate/lib_wownero_wallet.dart | 27 +++++++++++++---- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 6725736e1..58764f34e 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -53,6 +53,13 @@ abstract class LibMoneroWallet int get isarTransactionVersion => 2; LibMoneroWallet(super.currency, this.compatType) { + _attachTorListeners(); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); + } + + void _attachTorListeners() { final bus = GlobalEventBus.instance; // Listen for tor status changes. @@ -83,10 +90,8 @@ abstract class LibMoneroWallet ) async { await updateNode(); }); - - // Potentially dangerous hack. See comments in _startInit() - _startInit(); } + // cw based wallet listener to handle synchronization of utxo frozen states late final StreamSubscription> _streamSub; Future _startInit() async { @@ -193,6 +198,10 @@ abstract class LibMoneroWallet @override Future open() async { + if (_torStatusListener == null || _torPreferenceListener == null) { + _attachTorListeners(); + } + bool wasNull = false; if (wallet == null) { @@ -441,7 +450,7 @@ abstract class LibMoneroWallet ); if (this.wallet != null) { - await exit(); + await _exitNative(); } this.wallet = wallet; @@ -736,13 +745,21 @@ abstract class LibMoneroWallet @override Future exit() async { Logging.instance.i("exit called on monero $walletId!"); + await _exitNative(); + await _torStatusListener?.cancel(); + await _torPreferenceListener?.cancel(); + _torStatusListener = null; + _torPreferenceListener = null; + Logging.instance.i("exit call completed monero $walletId!"); + } + + Future _exitNative() async { if (wallet != null) { csMonero.stopAutoSaving(wallet!); await csMonero.stopListeners(wallet!); await csMonero.stopSyncing(wallet!); await csMonero.save(wallet!); } - Logging.instance.i("exit call completed monero $walletId!"); } Future pathForWalletDir({ @@ -1582,7 +1599,7 @@ abstract class LibMoneroWallet ); if (this.wallet == null) { - await exit(); + await _exitNative(); } this.wallet = wallet; diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 6a6f57812..2e687975c 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -51,6 +51,13 @@ abstract class LibSalviumWallet int get isarTransactionVersion => 2; LibSalviumWallet(super.currency) { + _attachTorListeners(); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); + } + + void _attachTorListeners() { final bus = GlobalEventBus.instance; // Listen for tor status changes. @@ -81,10 +88,8 @@ abstract class LibSalviumWallet ) async { await updateNode(); }); - - // Potentially dangerous hack. See comments in _startInit() - _startInit(); } + // cw based wallet listener to handle synchronization of utxo frozen states late final StreamSubscription> _streamSub; Future _startInit() async { @@ -189,6 +194,10 @@ abstract class LibSalviumWallet @override Future open() async { + if (_torStatusListener == null || _torPreferenceListener == null) { + _attachTorListeners(); + } + bool wasNull = false; if (wallet == null) { @@ -419,7 +428,7 @@ abstract class LibSalviumWallet ); if (this.wallet != null) { - await exit(); + await _exitNative(); } this.wallet = wallet; @@ -731,6 +740,14 @@ abstract class LibSalviumWallet @override Future exit() async { Logging.instance.i("exit called on $walletId"); + await _exitNative(); + await _torStatusListener?.cancel(); + await _torPreferenceListener?.cancel(); + _torStatusListener = null; + _torPreferenceListener = null; + } + + Future _exitNative() async { if (wallet != null) { csSalvium.stopAutoSaving(wallet!); csSalvium.stopListeners(wallet!); @@ -1548,7 +1565,7 @@ abstract class LibSalviumWallet ); if (this.wallet != null) { - await exit(); + await _exitNative(); } this.wallet = wallet; diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 0585dd71b..ef0f7d9eb 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -55,6 +55,13 @@ abstract class LibWowneroWallet int get isarTransactionVersion => 2; LibWowneroWallet(super.currency, this.compatType) { + _attachTorListeners(); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); + } + + void _attachTorListeners() { final bus = GlobalEventBus.instance; // Listen for tor status changes. @@ -85,10 +92,8 @@ abstract class LibWowneroWallet ) async { await updateNode(); }); - - // Potentially dangerous hack. See comments in _startInit() - _startInit(); } + // cw based wallet listener to handle synchronization of utxo frozen states late final StreamSubscription> _streamSub; Future _startInit() async { @@ -195,6 +200,10 @@ abstract class LibWowneroWallet @override Future open() async { + if (_torStatusListener == null || _torPreferenceListener == null) { + _attachTorListeners(); + } + bool wasNull = false; if (wallet == null) { @@ -443,7 +452,7 @@ abstract class LibWowneroWallet ); if (this.wallet != null) { - await exit(); + await _exitNative(); } this.wallet = wallet; @@ -754,6 +763,14 @@ abstract class LibWowneroWallet @override Future exit() async { Logging.instance.i("exit called on $wallet!"); + await _exitNative(); + await _torStatusListener?.cancel(); + await _torPreferenceListener?.cancel(); + _torStatusListener = null; + _torPreferenceListener = null; + } + + Future _exitNative() async { if (wallet != null) { csWownero.stopAutoSaving(wallet!); csWownero.stopListeners(wallet!); @@ -1560,7 +1577,7 @@ abstract class LibWowneroWallet ); if (this.wallet == null) { - await exit(); + await _exitNative(); } this.wallet = wallet; From 61e6a24d3737e5653daeb18543552ea96573f8ee Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 May 2026 16:42:25 -0500 Subject: [PATCH 3/4] fix: serialize updateNode() with mutex for Monero-family wallets --- .../intermediate/lib_monero_wallet.dart | 96 ++++++++++--------- .../intermediate/lib_salvium_wallet.dart | 96 ++++++++++--------- .../intermediate/lib_wownero_wallet.dart | 96 ++++++++++--------- 3 files changed, 156 insertions(+), 132 deletions(-) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 58764f34e..627c54fa0 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -517,24 +517,46 @@ abstract class LibMoneroWallet return; } - final node = getCurrentNode(); + await _updateNodeMutex.protect(() async { + if (wallet == null) { + return; + } - if (await _torNodeMismatchGuard(node)) { - throw Exception("TOR – clearnet mismatch"); - } + final node = getCurrentNode(); - final host = node.host.endsWith(".onion") - ? node.host - : Uri.parse(node.host).host; - final ({InternetAddress host, int port})? proxy = - AppConfig.hasFeature(AppFeature.tor) && prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + if (await _torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } - _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); - try { - if (_requireMutex) { - await _torConnectingLock.protect(() async { + final host = node.host.endsWith(".onion") + ? node.host + : Uri.parse(node.host).host; + final ({InternetAddress host, int port})? proxy = + AppConfig.hasFeature(AppFeature.tor) && + prefs.useTor && + !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; + + _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); + try { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + await csMonero.connect( + wallet!, + daemonAddress: "$host:${node.port}", + daemonUsername: node.loginName, + daemonPassword: await node.getPassword(secureStorageInterface), + trusted: node.trusted ?? false, + useSSL: node.useSSL, + socksProxyAddress: node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", + ); + }); + } else { await csMonero.connect( wallet!, daemonAddress: "$host:${node.port}", @@ -548,37 +570,21 @@ abstract class LibMoneroWallet ? null : "${proxy.host.address}:${proxy.port}", ); - }); - } else { - await csMonero.connect( - wallet!, - daemonAddress: "$host:${node.port}", - daemonUsername: node.loginName, - daemonPassword: await node.getPassword(secureStorageInterface), - trusted: node.trusted ?? false, - useSSL: node.useSSL, - socksProxyAddress: node.forceNoTor - ? null - : proxy == null - ? null - : "${proxy.host.address}:${proxy.port}", + } + await csMonero.startSyncing(wallet!); + await csMonero.startListeners(wallet!); + csMonero.startAutoSaving(wallet!); + + _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); + } catch (e, s) { + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, ); } - await csMonero.startSyncing(wallet!); - await csMonero.startListeners(wallet!); - csMonero.startAutoSaving(wallet!); - - _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); - } catch (e, s) { - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - Logging.instance.e( - "Exception caught in $runtimeType.updateNode(): ", - error: e, - stackTrace: s, - ); - } - - return; + }); } @override @@ -1650,4 +1656,6 @@ abstract class LibMoneroWallet final Mutex _torConnectingLock = Mutex(); bool _requireMutex = false; + + final Mutex _updateNodeMutex = Mutex(); } diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 2e687975c..89096e87c 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -494,24 +494,46 @@ abstract class LibSalviumWallet return; } - final node = getCurrentNode(); + await _updateNodeMutex.protect(() async { + if (wallet == null) { + return; + } - if (_torNodeMismatchGuard(node)) { - throw Exception("TOR – clearnet mismatch"); - } + final node = getCurrentNode(); - final host = node.host.endsWith(".onion") - ? node.host - : Uri.parse(node.host).host; - final ({InternetAddress host, int port})? proxy = - AppConfig.hasFeature(AppFeature.tor) && prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } - _setSyncStatus(ConnectingSyncStatus()); - try { - if (_requireMutex) { - await _torConnectingLock.protect(() async { + final host = node.host.endsWith(".onion") + ? node.host + : Uri.parse(node.host).host; + final ({InternetAddress host, int port})? proxy = + AppConfig.hasFeature(AppFeature.tor) && + prefs.useTor && + !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; + + _setSyncStatus(ConnectingSyncStatus()); + try { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + await csSalvium.connect( + wallet!, + daemonAddress: "$host:${node.port}", + daemonUsername: node.loginName, + daemonPassword: await node.getPassword(secureStorageInterface), + trusted: node.trusted ?? false, + useSSL: node.useSSL, + socksProxyAddress: node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", + ); + }); + } else { await csSalvium.connect( wallet!, daemonAddress: "$host:${node.port}", @@ -525,37 +547,21 @@ abstract class LibSalviumWallet ? null : "${proxy.host.address}:${proxy.port}", ); - }); - } else { - await csSalvium.connect( - wallet!, - daemonAddress: "$host:${node.port}", - daemonUsername: node.loginName, - daemonPassword: await node.getPassword(secureStorageInterface), - trusted: node.trusted ?? false, - useSSL: node.useSSL, - socksProxyAddress: node.forceNoTor - ? null - : proxy == null - ? null - : "${proxy.host.address}:${proxy.port}", + } + csSalvium.startSyncing(wallet!); + csSalvium.startListeners(wallet!); + csSalvium.startAutoSaving(wallet!); + + // _setSyncStatus(ConnectedSyncStatus()); + } catch (e, s) { + // _setSyncStatus(FailedSyncStatus()); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, ); } - csSalvium.startSyncing(wallet!); - csSalvium.startListeners(wallet!); - csSalvium.startAutoSaving(wallet!); - - // _setSyncStatus(ConnectedSyncStatus()); - } catch (e, s) { - // _setSyncStatus(FailedSyncStatus()); - Logging.instance.e( - "Exception caught in $runtimeType.updateNode(): ", - error: e, - stackTrace: s, - ); - } - - return; + }); } @override @@ -1617,6 +1623,8 @@ abstract class LibSalviumWallet final Mutex _torConnectingLock = Mutex(); bool _requireMutex = false; + + final Mutex _updateNodeMutex = Mutex(); } String _libSalviumWalletPasswordKey(String walletName) => diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index ef0f7d9eb..78cf8acc3 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -519,24 +519,46 @@ abstract class LibWowneroWallet return; } - final node = getCurrentNode(); + await _updateNodeMutex.protect(() async { + if (wallet == null) { + return; + } - if (_torNodeMismatchGuard(node)) { - throw Exception("TOR – clearnet mismatch"); - } + final node = getCurrentNode(); - final host = node.host.endsWith(".onion") - ? node.host - : Uri.parse(node.host).host; - final ({InternetAddress host, int port})? proxy = - AppConfig.hasFeature(AppFeature.tor) && prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } - _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); - try { - if (_requireMutex) { - await _torConnectingLock.protect(() async { + final host = node.host.endsWith(".onion") + ? node.host + : Uri.parse(node.host).host; + final ({InternetAddress host, int port})? proxy = + AppConfig.hasFeature(AppFeature.tor) && + prefs.useTor && + !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; + + _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); + try { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + await csWownero.connect( + wallet!, + daemonAddress: "$host:${node.port}", + daemonUsername: node.loginName, + daemonPassword: await node.getPassword(secureStorageInterface), + trusted: node.trusted ?? false, + useSSL: node.useSSL, + socksProxyAddress: node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", + ); + }); + } else { await csWownero.connect( wallet!, daemonAddress: "$host:${node.port}", @@ -550,37 +572,21 @@ abstract class LibWowneroWallet ? null : "${proxy.host.address}:${proxy.port}", ); - }); - } else { - await csWownero.connect( - wallet!, - daemonAddress: "$host:${node.port}", - daemonUsername: node.loginName, - daemonPassword: await node.getPassword(secureStorageInterface), - trusted: node.trusted ?? false, - useSSL: node.useSSL, - socksProxyAddress: node.forceNoTor - ? null - : proxy == null - ? null - : "${proxy.host.address}:${proxy.port}", + } + csWownero.startSyncing(wallet!); + csWownero.startListeners(wallet!); + csWownero.startAutoSaving(wallet!); + + _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); + } catch (e, s) { + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, ); } - csWownero.startSyncing(wallet!); - csWownero.startListeners(wallet!); - csWownero.startAutoSaving(wallet!); - - _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); - } catch (e, s) { - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - Logging.instance.e( - "Exception caught in $runtimeType.updateNode(): ", - error: e, - stackTrace: s, - ); - } - - return; + }); } @override @@ -1628,4 +1634,6 @@ abstract class LibWowneroWallet final Mutex _torConnectingLock = Mutex(); bool _requireMutex = false; + + final Mutex _updateNodeMutex = Mutex(); } From 4e934836722befb1b356c995f2b4ff5dd409f4a2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 15 May 2026 10:35:50 -0500 Subject: [PATCH 4/4] refactor: attach Tor listeners lazily in open() instead of constructor --- lib/wallets/wallet/intermediate/lib_monero_wallet.dart | 2 -- lib/wallets/wallet/intermediate/lib_salvium_wallet.dart | 2 -- lib/wallets/wallet/intermediate/lib_wownero_wallet.dart | 2 -- 3 files changed, 6 deletions(-) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 627c54fa0..e7b3c2fe3 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -53,8 +53,6 @@ abstract class LibMoneroWallet int get isarTransactionVersion => 2; LibMoneroWallet(super.currency, this.compatType) { - _attachTorListeners(); - // Potentially dangerous hack. See comments in _startInit() _startInit(); } diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 89096e87c..098f5183a 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -51,8 +51,6 @@ abstract class LibSalviumWallet int get isarTransactionVersion => 2; LibSalviumWallet(super.currency) { - _attachTorListeners(); - // Potentially dangerous hack. See comments in _startInit() _startInit(); } diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 78cf8acc3..7ce6fb6cc 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -55,8 +55,6 @@ abstract class LibWowneroWallet int get isarTransactionVersion => 2; LibWowneroWallet(super.currency, this.compatType) { - _attachTorListeners(); - // Potentially dangerous hack. See comments in _startInit() _startInit(); }