What changed, and why it matters
This commit is a routine development progress patch for a Monero light wallet app. It restructures how the wallet connects to the network, moves some work onto background threads, fixes a bug where a fiat-exchange-rate timer was not being stored/cancelled correctly, and adds a wait-until-Tor-is-ready helper. There is no clear security vulnerability in the diff, but the changes touch sensitive areas (network/Tor setup, wallet refresh, FFI pointer handling) and the patch is incomplete in places (debug print statements left in, a TODO comment, hardcoded wallet password still present).
Treat this as a normal development commit, not a security fix or advisory. Reviewers should verify that the new Isolate.run FFI calls preserve object lifetime safety, that waitUntilConnected() has a timeout/failure path, that the fiat-rate timer is properly disposed on app pause/destroy, and that leftover debug prints/TODOs are cleaned up before release. No immediate security response is indicated by the diff alone.
Security signals we found
Refactored Tor/network initialization and wallet daemon connection logic
Fixed timer lifecycle bug in fiat rate fetching (previously uncancelable timer)
Added waitUntilConnected() polling helper for Tor readiness
Moved FFI wallet operations into Isolate.run
Removed stale cached FFI pointer fields in WalletModel
Debug print statements and TODO left in wallet_home.dart indicating work-in-progress
Hardcoded wallet password 'pass' remains in wallet creation helpers
Evidence from the diff
The diff refactors WalletModel to remove cached FFI addresses and fetch them on demand, wraps several monero FFI calls in Isolate.run, extracts wallet creation helpers, and reorders refresh/connect calls. It also fixes a Dart Timer lifecycle bug in FiatRateModel (rateFetchTimer was never assigned, so it could not be cancelled) and adds TorService.waitUntilConnected() with a 50 ms polling loop. WalletHome gets new UI state logic for Tor connecting/connected/disconnected. Several debug prints and a TODO remain. The hardcoded wallet password ‘pass’ is unchanged. No explicit security bug is introduced, but the broad, unfinished nature of the change warrants caution.
Changed components
lib/main.dartlib/models/fiat_rate_model.dartlib/models/wallet_model.dartlib/periodic_tasks.dartlib/screens/wallet_home.dartlib/services/tor_service.dartInspect captured patch +180 / −134
diff --git a/lib/main.dart b/lib/main.dart
index c93a752..caf9885 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -77,6 +77,16 @@ class MyApp extends StatelessWidget {
final initialRoute = walletExists ? '/wallet_home' : '/welcome';
TorService.sharedInstance.start();
+ if (walletExists) {
+ (() async {
+ await wallet.refresh();
+ await wallet.loadAllStats();
+ wallet.notifyListenersFromOutside();
+ await wallet.connectToDaemon();
+ wallet.notifyListenersFromOutside();
+ })();
+ }
+
return MaterialApp(
title: 'Monero Light Wallet',
localizationsDelegates:
diff --git a/lib/models/fiat_rate_model.dart b/lib/models/fiat_rate_model.dart
index d6745b1..6ac9b07 100644
--- a/lib/models/fiat_rate_model.dart
+++ b/lib/models/fiat_rate_model.dart
@@ -10,7 +10,7 @@ class FiatRateModel with ChangeNotifier {
double? _rate;
bool _hasFailed = false;
String _fiatCode = 'USD';
- Timer? rateFetchTimer;
+ Timer? _rateFetchTimer;
double? get rate => _rate;
bool get hasFailed => _hasFailed;
@@ -24,19 +24,19 @@ class FiatRateModel with ChangeNotifier {
void _startTorStatusCheckTimer() {
Timer.periodic(Duration(seconds: 1), (timer) {
if (TorService.sharedInstance.status == TorConnectionStatus.connected &&
- rateFetchTimer == null) {
+ _rateFetchTimer == null) {
_startRateFetchTimer();
}
});
}
void _startRateFetchTimer() {
- Timer.periodic(Duration(minutes: 1), (timer) {
+ _rateFetchTimer = Timer.periodic(Duration(minutes: 1), (timer) async {
if (TorService.sharedInstance.status == TorConnectionStatus.connected) {
- _fetch();
+ await _fetch();
} else {
timer.cancel();
- rateFetchTimer = null;
+ _rateFetchTimer = null;
}
});
}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index a027bdc..92bf764 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -1,5 +1,7 @@
// ignore_for_file: implementation_imports
+// import 'dart:async';
+// import 'dart:async' show Timer;
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
@@ -126,13 +128,9 @@ class WalletModel with ChangeNotifier {
.walletManagerFactory()
.getLWSFWalletManager();
- int? _w2WalletManagerFfiAddr;
-
Wallet2Wallet? _w2Wallet;
- int? _w2WalletFfiAddress;
Wallet2TransactionHistory? _w2TxHistory;
- int? _w2TxHistoryFfiAddress;
late String _connectionAddress;
late String _connectionProxyPort;
@@ -153,13 +151,16 @@ class WalletModel with ChangeNotifier {
double? get unlockedBalance => _unlockedBalance;
double? get totalBalance => _totalBalance;
List<TxDetails>? get txHistory => _txHistory;
+ bool get usingTor => _connectionUseTor;
WalletModel() {
- _w2WalletManagerFfiAddr = _w2WalletManager.ffiAddress();
-
_startTimers();
}
+ void notifyListenersFromOutside() {
+ notifyListeners();
+ }
+
void _startTimers() {
log(LogLevel.info, "Starting timers");
@@ -181,13 +182,6 @@ class WalletModel with ChangeNotifier {
if (isConnected != _isConnected) {
_isConnected = isConnected;
-
- if (isConnected) {
- refresh();
- await _loadTxHistory();
- await store();
- }
-
notifyListeners();
}
}
@@ -197,6 +191,12 @@ class WalletModel with ChangeNotifier {
return;
}
+ await refresh();
+ await loadAllStats();
+ notifyListeners();
+ }
+
+ Future<void> loadAllStats() async {
await Future.wait([
loadIsSynced(),
loadSyncedHeight(),
@@ -204,8 +204,6 @@ class WalletModel with ChangeNotifier {
loadTotalBalance(),
_loadTxHistory(),
]);
-
- notifyListeners();
}
Future<void> _loadTxHistory() async {
@@ -216,15 +214,6 @@ class WalletModel with ChangeNotifier {
}
}
- // Future<int> _getTxHistoryCount() async {
- // return Isolate.run(
- // // ignore: deprecated_member_use
- // () => monero.TransactionHistory_count(
- // Pointer<Void>.fromAddress(_w2TxHistoryFfiAddress),
- // ),
- // );
- // }
-
Future<void> persistCurrentConnection() async {
await SharedPreferencesService.set(
SharedPreferencesKeys.connectionAddress,
@@ -314,27 +303,65 @@ class WalletModel with ChangeNotifier {
String? torProxyPort;
if (_connectionUseTor) {
+ await TorService.sharedInstance.waitUntilConnected();
torProxyPort = TorService.sharedInstance.getProxyInfo().port.toString();
}
final proxyPort = torProxyPort ?? _connectionProxyPort;
- _w2Wallet!.init(
- daemonAddress: _connectionAddress,
- proxyAddress: proxyPort != '' ? '127.0.0.1:$proxyPort' : '',
+ await _connectToDaemon(
+ address: _connectionAddress,
+ proxyPort: proxyPort,
useSsl: _connectionUseSsl,
- lightWallet: true,
);
- _w2Wallet!.connectToDaemon();
+ notifyListeners();
}
- void refresh() {
- if (_w2Wallet == null) throw Exception("w2wallet is null");
+ Future<void> _connectToDaemon({
+ required String address,
+ String? proxyPort,
+ bool useSsl = false,
+ }) async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
+
+ await Isolate.run(
+ // ignore: deprecated_member_use
+ () => monero.Wallet_init(
+ Pointer.fromAddress(walletFfiAddr),
+ daemonAddress: address,
+ proxyAddress: proxyPort != '' ? '127.0.0.1:$proxyPort' : '',
+ useSsl: useSsl,
+ lightWallet: true,
+ ),
+ );
+
+ await Isolate.run(
+ // ignore: deprecated_member_use
+ () => monero.Wallet_connectToDaemon(Pointer.fromAddress(walletFfiAddr)),
+ );
+ }
- _w2Wallet!.startRefresh();
- _w2Wallet!.refresh();
- _w2TxHistory!.refresh();
+ Future<void> refresh() async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
+ final historyFfiAddr = _w2TxHistory!.ffiAddress();
+
+ await Future.wait([
+ Isolate.run(
+ // ignore: deprecated_member_use
+ () => monero.Wallet_startRefresh(Pointer.fromAddress(walletFfiAddr)),
+ ),
+ Isolate.run(
+ // ignore: deprecated_member_use
+ () => monero.Wallet_refresh(Pointer.fromAddress(walletFfiAddr)),
+ ),
+ Isolate.run(
+ // ignore: deprecated_member_use
+ () => monero.TransactionHistory_refresh(
+ Pointer.fromAddress(historyFfiAddr),
+ ),
+ ),
+ ]);
}
Future<String> create() async {
@@ -342,7 +369,7 @@ class WalletModel with ChangeNotifier {
final polyseed = await Isolate.run(() => monero.Wallet_createPolyseed());
final currentHeight = await getCurrentBlockchainHeight();
await restoreFromMnemonic(polyseed, currentHeight);
- refresh();
+ await refresh();
await connectToDaemon();
store();
@@ -360,106 +387,85 @@ class WalletModel with ChangeNotifier {
});
}
+ Future<MoneroWallet> _getWalletFromLegacySeed(
+ String mnemonic,
+ int restoreHeight, {
+ bool isDummy = false,
+ }) async {
+ final wmFfiAddr = _w2WalletManager.ffiAddress();
+ final walletPath = await getWalletPath();
+
+ final walletFfiAddr = await Isolate.run(() {
+ // ignore: deprecated_member_use
+ return monero.WalletManager_recoveryWallet(
+ Pointer.fromAddress(wmFfiAddr),
+ mnemonic: mnemonic,
+ seedOffset: '',
+ restoreHeight: restoreHeight,
+ password: 'pass',
+ path: isDummy ? '' : walletPath,
+ ).address;
+ });
+
+ return MoneroWallet(Pointer<Void>.fromAddress(walletFfiAddr));
+ }
+
+ Future<MoneroWallet> _getWalletFromPolyseed(
+ String mnemonic,
+ int restoreHeight, {
+ bool isDummy = false,
+ }) async {
+ final wmFfiAddr = _w2WalletManager.ffiAddress();
+ final walletPath = await getWalletPath();
+
+ final walletFfiAddr = await Isolate.run(() {
+ // ignore: deprecated_member_use
+ return monero.WalletManager_createWalletFromPolyseed(
+ Pointer.fromAddress(wmFfiAddr),
+ mnemonic: mnemonic,
+ seedOffset: '',
+ restoreHeight: restoreHeight,
+ path: isDummy ? '' : walletPath,
+ password: 'pass',
+ newWallet: true,
+ kdfRounds: 1,
+ ).address;
+ });
+
+ return MoneroWallet(Pointer<Void>.fromAddress(walletFfiAddr));
+ }
+
Future<void> restoreFromMnemonic(
String mnemonic,
int restoreHeight, [
String passphrase = '',
]) async {
- final wmFfiAddr = _w2WalletManager.ffiAddress();
-
- final legacyWalletPtr = Pointer<Void>.fromAddress(
- await Isolate.run(() {
- // ignore: deprecated_member_use
- return monero.WalletManager_recoveryWallet(
- Pointer.fromAddress(wmFfiAddr),
- mnemonic: mnemonic,
- password: 'pass',
- path: '',
- restoreHeight: restoreHeight,
- seedOffset: passphrase,
- ).address;
- }),
+ final legacyWallet = await _getWalletFromLegacySeed(
+ mnemonic,
+ restoreHeight,
+ isDummy: true,
);
- final legacyWallet = MoneroWallet(legacyWalletPtr);
-
- final polyseedWalletPtr = Pointer<Void>.fromAddress(
- await Isolate.run(() {
- // ignore: deprecated_member_use
- return monero.WalletManager_createWalletFromPolyseed(
- Pointer.fromAddress(wmFfiAddr),
- path: '',
- password: 'pass',
- mnemonic: mnemonic,
- seedOffset: '',
- newWallet: true,
- restoreHeight: restoreHeight,
- kdfRounds: 1,
- ).address;
- }),
+ final polyseedWallet = await _getWalletFromPolyseed(
+ mnemonic,
+ restoreHeight,
+ isDummy: true,
);
- final polyseedWallet = MoneroWallet(polyseedWalletPtr);
-
final legacyError = legacyWallet.errorString();
final polyseedError = polyseedWallet.errorString();
- final walletPath = await getWalletPath();
-
if (!legacyError.contains('word list failed verification')) {
- final walletPtr = Pointer<Void>.fromAddress(
- await Isolate.run(() {
- // ignore: deprecated_member_use
- return monero.WalletManager_recoveryWallet(
- Pointer.fromAddress(wmFfiAddr),
- mnemonic: mnemonic,
- password: 'pass',
- path: walletPath,
- restoreHeight: restoreHeight,
- seedOffset: passphrase,
- ).address;
- }),
- );
-
- _w2Wallet = MoneroWallet(walletPtr);
+ _w2Wallet = await _getWalletFromLegacySeed(mnemonic, restoreHeight);
} else if (polyseedError != 'Failed polyseed decode') {
- final walletPtr = Pointer<Void>.fromAddress(
- await Isolate.run(() {
- // ignore: deprecated_member_use
- return monero.WalletManager_createWalletFromPolyseed(
- Pointer.fromAddress(wmFfiAddr),
- path: walletPath,
- password: 'pass',
- mnemonic: mnemonic,
- seedOffset: passphrase,
- newWallet: true,
- restoreHeight: restoreHeight,
- kdfRounds: 1,
- ).address;
- }),
- );
-
- _w2Wallet = MoneroWallet(walletPtr);
+ _w2Wallet = await _getWalletFromPolyseed(mnemonic, restoreHeight);
}
if (_w2Wallet == null) {
throw Exception("Something went wrong when generating seed");
}
- _w2WalletFfiAddress = _w2Wallet!.ffiAddress();
-
- _w2TxHistory = MoneroTransactionHistory(
- Pointer<Void>.fromAddress(
- await Isolate.run(
- // ignore: deprecated_member_use
- () => monero.Wallet_history(
- Pointer<Void>.fromAddress(_w2WalletFfiAddress!),
- ).address,
- ),
- ),
- );
-
- _w2TxHistoryFfiAddress = _w2TxHistory!.ffiAddress();
+ _w2TxHistory = _w2Wallet!.history();
store();
notifyListeners();
@@ -522,28 +528,30 @@ class WalletModel with ChangeNotifier {
}
Future<void> loadIsSynced() async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
_isSynced = await Isolate.run(
- // ignore: deprecated_member_use
- () => monero.Wallet_synchronized(
- Pointer<Void>.fromAddress(_w2WalletFfiAddress!),
- ),
+ () =>
+ // ignore: deprecated_member_use
+ monero.Wallet_synchronized(Pointer<Void>.fromAddress(walletFfiAddr)),
);
}
Future<void> loadSyncedHeight() async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
_syncedHeight = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_blockChainHeight(
- Pointer<Void>.fromAddress(_w2WalletFfiAddress!),
+ Pointer<Void>.fromAddress(walletFfiAddr),
),
);
}
Future<void> loadTotalBalance() async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
final amount = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_balance(
- Pointer<Void>.fromAddress(_w2WalletFfiAddress!),
+ Pointer<Void>.fromAddress(walletFfiAddr),
accountIndex: 0,
),
);
@@ -552,10 +560,11 @@ class WalletModel with ChangeNotifier {
}
Future<void> loadUnlockedBalance() async {
+ final walletFfiAddr = _w2Wallet!.ffiAddress();
final amount = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_unlockedBalance(
- Pointer<Void>.fromAddress(_w2WalletFfiAddress!),
+ Pointer<Void>.fromAddress(walletFfiAddr),
accountIndex: 0,
),
);
@@ -661,7 +670,7 @@ class WalletModel with ChangeNotifier {
await addPendingTx(txDetails);
store();
- refresh();
+ await refresh();
}
String resolveOpenAlias(String address) {
diff --git a/lib/periodic_tasks.dart b/lib/periodic_tasks.dart
index 545e073..f421605 100644
--- a/lib/periodic_tasks.dart
+++ b/lib/periodic_tasks.dart
@@ -20,8 +20,8 @@ void callbackDispatcher() {
await wallet.openExisting();
await wallet.loadPersistedConnection();
- wallet.connectToDaemon();
- wallet.refresh();
+ await wallet.refresh();
+ await wallet.connectToDaemon();
if (wallet.isConnected) {
return false;
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index 0706aff..96606d2 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -95,6 +95,11 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
(wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
final fiatSymbol = fiatRate.fiatCode == 'EUR' ? '€' : '\$';
+ print(wallet.isConnected);
+ print(wallet.isSynced);
+ print(wallet.syncedHeight);
+ print('----');
+
return Scaffold(
bottomNavigationBar: WalletNavigationBar(selectedIndex: 0),
body: SafeArea(
@@ -131,9 +136,13 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
),
),
- if (wallet.isConnected &&
- (!wallet.isSynced ||
- wallet.syncedHeight == 0))
+ // TODO: make dis better
+ if ((wallet.usingTor &&
+ TorService.sharedInstance.status ==
+ TorConnectionStatus.connecting) ||
+ wallet.isConnected &&
+ (!wallet.isSynced ||
+ wallet.syncedHeight == 0))
SizedBox(
width: 22,
height: 22,
@@ -146,7 +155,11 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
),
),
- if (!wallet.isConnected)
+ if (wallet.usingTor
+ ? TorService.sharedInstance.status !=
+ TorConnectionStatus.connecting &&
+ !wallet.isConnected
+ : !wallet.isConnected)
SizedBox(
width: 26,
height: 26,
diff --git a/lib/services/tor_service.dart b/lib/services/tor_service.dart
index ff72778..4ff350d 100644
--- a/lib/services/tor_service.dart
+++ b/lib/services/tor_service.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -76,4 +77,17 @@ class TorService {
return;
}
+
+ Future<void> waitUntilConnected() async {
+ final completer = Completer<void>();
+
+ Timer.periodic(Duration(milliseconds: 50), (timer) {
+ if (status == TorConnectionStatus.connected) {
+ timer.cancel();
+ completer.complete();
+ }
+ });
+
+ return completer.future;
+ }
}
Why this scored 31/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.