What changed, and why it matters
This commit fixes several wallet-loading bugs in a Monero wallet app. The most important change corrects how restored wallets are created: previously the app told the backend that every restored wallet was 'new', which made it skip scanning old transactions, so users could see a zero balance after restoring. The patch also prevents the app from opening the same wallet file twice, stops repeated connection attempts from racing each other, and makes the app detect when a wallet exists in the opposite mode (full node vs light wallet server). These are reliability and correctness fixes rather than remote attack vectors, but the 'new wallet' flag bug could cause real funds to appear missing.
Review and merge after testing restore flows for both LWS and full-node modes, verify that switching between modes no longer leaves orphaned wallet files, and confirm the monero_c dependency bump does not introduce unrelated breaking changes. Consider adding automated tests for restore height handling and mode-switch file detection.
Security signals we found
Corrected restore flag that caused restored wallets to skip blockchain scan, potentially showing zero balance
Prevented duplicate wallet opens that could leave two sync loops writing the same cache
Serialized in-flight daemon connections to avoid overlapping Wallet_init calls on the same native wallet
Added wallet-file existence check for the correct mode before onboarding, reducing risk of overwriting an existing wallet
Dependency bump in pubspec.lock for monero_c FFI wrapper
Evidence from the diff
The diff refactors startup in lib/main.dart so wallet loading and service startup run once instead of on every Consumer2 rebuild, avoiding duplicate wallet opens and sync loops. In lib/models/wallet_model.dart it adds connection-state guards (_connectionLoaded, _connectInFlight, _connectFailures with backoff), deduplicates connectToDaemon attempts, fixes restoreFromMnemonic to pass newWallet=false for restored seeds, deletes conflicting wallet files when wallet2 reports ‘file already exists’, sets the correct refresh height for polyseed node restores, and adds _adoptModeWithExistingWallet to recover from LWS/node mode mismatches. lib/screens/create_wallet.dart routes restore differently for node vs LWS mode. pubspec.lock bumps the monero_c dependency to a newer ref.
Changed components
lib/main.dartlib/models/wallet_model.dartlib/screens/create_wallet.dartpubspec.lock (monero_c git dependency)Inspect captured patch +366 / −111
diff --git a/lib/main.dart b/lib/main.dart
index d9007b1..4e70cb5 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -128,94 +128,75 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (context) => FiatRateModel()),
ChangeNotifierProvider(create: (context) => ContactModel()),
],
- child: Consumer2<LanguageModel, ThemeModel>(
- builder: (context, languageProvider, themeProvider, child) {
- final wallet = Provider.of<WalletModel>(context, listen: false);
- final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
-
- return FutureBuilder(
- // We need to check for wallet existence to determine the correct initial route,
- // but we'll do this quickly without loading the wallet to avoid startup delay.
- future: Future.wait([
- SharedPreferences.getInstance(),
- loadExistingWalletIfExists(wallet),
- ]),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done && snapshot.data != null) {
- TorSettingsService.sharedInstance.loadSettings();
- TorService.sharedInstance.start();
+ child: const _AppRoot(),
+ );
+ }
+}
+
+class _AppRoot extends StatefulWidget {
+ const _AppRoot();
+
+ @override
+ State<_AppRoot> createState() => _AppRootState();
+}
- final sharedPreferences = snapshot.data![0] as SharedPreferences;
- final walletExists = snapshot.data![1] as bool;
+class _AppRootState extends State<_AppRoot> {
+ // Started once, on the first build. Building it inside the builder would
+ // re-run it on every theme/language change, opening the wallet again — a
+ // second wallet on the same file, with its own sync loop, while the first is
+ // left running.
+ Future<List<Object>>? _startup;
+ // Services that must fire once the startup work is done, not on every build.
+ var _startedServices = false;
- final theme = sharedPreferences.getString(SharedPreferencesKeys.theme) ?? 'system';
+ Future<List<Object>> _runStartup() {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+
+ // We need to check for wallet existence to determine the correct initial route,
+ // but we'll do this quickly without loading the wallet to avoid startup delay.
+ return Future.wait([SharedPreferences.getInstance(), loadExistingWalletIfExists(wallet)]);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ _startup ??= _runStartup();
- final appLockEnabled =
- sharedPreferences.getBool(SharedPreferencesKeys.appLockEnabled) ?? false;
+ return Consumer2<LanguageModel, ThemeModel>(
+ builder: (context, languageProvider, themeProvider, child) {
+ final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
- final initialRoute = walletExists
- ? appLockEnabled || isDesktop
- ? '/unlock'
- : '/wallet_home'
- : '/welcome';
+ return FutureBuilder(
+ future: _startup,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done && snapshot.data != null) {
+ final sharedPreferences = snapshot.data![0] as SharedPreferences;
+ final walletExists = snapshot.data![1] as bool;
+
+ final theme = sharedPreferences.getString(SharedPreferencesKeys.theme) ?? 'system';
+
+ final appLockEnabled =
+ sharedPreferences.getBool(SharedPreferencesKeys.appLockEnabled) ?? false;
+
+ final initialRoute = walletExists
+ ? appLockEnabled || isDesktop
+ ? '/unlock'
+ : '/wallet_home'
+ : '/welcome';
+
+ if (!_startedServices) {
+ _startedServices = true;
+ TorSettingsService.sharedInstance.loadSettings();
+ TorService.sharedInstance.start();
if (walletExists) {
fiatRate.startService();
}
-
- return MaterialApp(
- title: 'Skylight Monero Wallet',
- localizationsDelegates: AppLocalizations.localizationsDelegates,
- supportedLocales: AppLocalizations.supportedLocales,
- theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
- darkTheme: ThemeData(
- colorScheme: ColorScheme.fromSeed(
- seedColor: Colors.blue,
- brightness: Brightness.dark,
- ),
- ),
- themeMode: theme == 'dark'
- ? ThemeMode.dark
- : theme == 'light'
- ? ThemeMode.light
- : ThemeMode.system,
- initialRoute: initialRoute,
- locale: Locale.fromSubtags(languageCode: languageProvider.language),
- routes: {
- '/welcome': (context) => WelcomeScreen(),
- '/tor_info': (context) => TorInfoScreen(),
- '/tor_settings': (context) => TorSettingsScreen(),
- '/connection_setup': (context) => ConnectionSetupScreen(),
- '/fiat_api_setup': (context) => FiatApiSetupScreen(),
- '/create_wallet_password': (context) => CreateWalletPasswordScreen(),
- '/create_wallet': (context) => CreateWalletScreen(),
- '/generate_seed': (context) => GenerateSeedScreen(),
- '/lws_details': (context) => LwsDetailsScreen(),
- '/restore_warning': (context) => RestoreWarningScreen(),
- '/restore_wallet': (context) => RestoreWalletScreen(),
- '/unlock': (context) => UnlockScreen(),
- '/wallet_home': (context) => WalletHomeScreen(),
- '/settings': (context) => SettingsScreen(),
- '/lws_keys': (context) => LwsKeysScreen(),
- '/secret_keys': (context) => SecretKeysScreen(),
- '/send': (context) => SendScreen(),
- '/confirm_send': (context) => ConfirmSendScreen(),
- '/scan_qr': (context) => ScanQrScreen(),
- '/receive': (context) => ReceiveScreen(),
- '/address_book': (context) => AddressBookScreen(),
- '/terms_of_service': (context) => TermsOfService(),
- '/privacy_policy': (context) => PrivacyPolicy(),
- },
- );
- }
-
- if (snapshot.data == null) {
- log(LogLevel.error, 'Future builder snapshot data is null.');
- log(LogLevel.error, snapshot.error.toString());
}
return MaterialApp(
title: 'Skylight Monero Wallet',
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
@@ -223,13 +204,60 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark,
),
),
- themeMode: ThemeMode.system,
- builder: (context, child) => Scaffold(),
+ themeMode: theme == 'dark'
+ ? ThemeMode.dark
+ : theme == 'light'
+ ? ThemeMode.light
+ : ThemeMode.system,
+ initialRoute: initialRoute,
+ locale: Locale.fromSubtags(languageCode: languageProvider.language),
+ routes: {
+ '/welcome': (context) => WelcomeScreen(),
+ '/tor_info': (context) => TorInfoScreen(),
+ '/tor_settings': (context) => TorSettingsScreen(),
+ '/connection_setup': (context) => ConnectionSetupScreen(),
+ '/fiat_api_setup': (context) => FiatApiSetupScreen(),
+ '/create_wallet_password': (context) => CreateWalletPasswordScreen(),
+ '/create_wallet': (context) => CreateWalletScreen(),
+ '/generate_seed': (context) => GenerateSeedScreen(),
+ '/lws_details': (context) => LwsDetailsScreen(),
+ '/restore_warning': (context) => RestoreWarningScreen(),
+ '/restore_wallet': (context) => RestoreWalletScreen(),
+ '/unlock': (context) => UnlockScreen(),
+ '/wallet_home': (context) => WalletHomeScreen(),
+ '/settings': (context) => SettingsScreen(),
+ '/lws_keys': (context) => LwsKeysScreen(),
+ '/secret_keys': (context) => SecretKeysScreen(),
+ '/send': (context) => SendScreen(),
+ '/confirm_send': (context) => ConfirmSendScreen(),
+ '/scan_qr': (context) => ScanQrScreen(),
+ '/receive': (context) => ReceiveScreen(),
+ '/address_book': (context) => AddressBookScreen(),
+ '/terms_of_service': (context) => TermsOfService(),
+ '/privacy_policy': (context) => PrivacyPolicy(),
+ },
);
- },
- );
- },
- ),
+ }
+
+ if (snapshot.hasError) {
+ log(LogLevel.error, 'Startup failed: ${snapshot.error}');
+ }
+
+ return MaterialApp(
+ title: 'Skylight Monero Wallet',
+ theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
+ darkTheme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.blue,
+ brightness: Brightness.dark,
+ ),
+ ),
+ themeMode: ThemeMode.system,
+ builder: (context, child) => Scaffold(),
+ );
+ },
+ );
+ },
);
}
}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index b00750b..edb63aa 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -148,10 +148,22 @@ class WalletModel with ChangeNotifier {
late bool _connectionUseTor;
late bool _connectionUseSsl;
String _connectionType = 'lws';
+ // False until a connection is in memory (loaded from prefs or set by the
+ // settings form). Which wallet file exists/opens depends on it, so anything
+ // that resolves a wallet path has to wait for it.
+ bool _connectionLoaded = false;
int? _daemonTargetHeight;
DateTime? _lastDaemonHeightFetch;
+ // Reconnect policy. Seconds to wait before the next attempt, indexed by how
+ // many attempts have failed in a row: the first retry is nearly immediate,
+ // then it backs off to the refresh cycle's cadence.
+ static const _reconnectBackoffSeconds = [1, 2, 5, 10, 20];
+ Future<void>? _connectInFlight;
+ DateTime? _lastConnectAttempt;
+ int _connectFailures = 0;
+
final _sessionStartedAt = DateTime.now().secondsSinceEpoch;
var _hasAttemptedConnection = false;
var _isConnected = false;
@@ -231,8 +243,13 @@ class WalletModel with ChangeNotifier {
/// `mywallet` path; the node gets a `_node` suffix so toggling modes doesn't
/// force a rescan.
Future<String> resolveWalletPath() async {
+ return walletPathForType(_connectionType);
+ }
+
+ /// Path of the wallet file a given mode ('lws' | 'node') would use.
+ Future<String> walletPathForType(String connectionType) async {
final basePath = await getWalletPath();
- return isNodeMode ? '${basePath}_node' : basePath;
+ return connectionType == 'node' ? '${basePath}_node' : basePath;
}
void _startTimers() {
@@ -258,11 +275,50 @@ class WalletModel with ChangeNotifier {
notifyListeners();
}
+ // Not awaited: a connect can take seconds over Tor and must not hold up the
+ // sync poll below. Re-entry from the next tick is guarded by the in-flight
+ // attempt inside connectToDaemon.
+ unawaited(_retryConnectIfDue());
+
// Node sync state flips off the native background thread; poll it here so
// "blocks remaining" advances between the slower refresh cycles.
await pollSyncStatus();
}
+ /// Retries a connection that isn't up, on a short backoff.
+ ///
+ /// Without this the only reconnect is the 20s refresh cycle, so a connect
+ /// that fails on launch — Tor not ready yet, node unreachable for a moment —
+ /// leaves the wallet doing nothing for up to 20 seconds behind a sync
+ /// spinner. `load()` also abandons its refresh + stats when its connect
+ /// throws, so those are picked up here once a retry gets through.
+ Future<void> _retryConnectIfDue() async {
+ if (_w2Wallet == null || _isConnected || _connectInFlight != null) return;
+ // Nothing to connect to yet.
+ if (!_connectionLoaded || _connectionAddress.isEmpty) return;
+
+ final lastAttempt = _lastConnectAttempt;
+
+ if (lastAttempt != null) {
+ final backoffIndex = min(_connectFailures, _reconnectBackoffSeconds.length - 1);
+ final wait = Duration(seconds: _reconnectBackoffSeconds[backoffIndex]);
+ if (DateTime.now().difference(lastAttempt) < wait) return;
+ }
+
+ try {
+ await connectToDaemon();
+ if (!_isConnected) return;
+
+ await refresh();
+ // Same deferral as the refresh cycle: in node mode the stat reads wait
+ // until the background scan has caught up.
+ if (isNodeMode && !_isSynced) return;
+ await loadAllStats();
+ } catch (e) {
+ log(LogLevel.warn, 'Reconnect attempt failed: $e');
+ }
+ }
+
/// Node-only: reads sync flag + heights off the background scan thread. When
/// it just caught up, pulls fresh balances/tx immediately.
Future<void> pollSyncStatus() async {
@@ -426,6 +482,12 @@ class WalletModel with ChangeNotifier {
);
}
+ /// Loads the persisted connection unless one is already in memory.
+ Future<void> _ensureConnectionLoaded() async {
+ if (_connectionLoaded) return;
+ await loadPersistedConnection();
+ }
+
Future<void> persistTxHistoryCount() async {
if (_txHistory.isEmpty) {
return;
@@ -471,6 +533,7 @@ class WalletModel with ChangeNotifier {
_connectionUseTor = useTor;
_connectionUseSsl = useSsl;
_connectionType = connectionType;
+ _connectionLoaded = true;
notifyListeners();
}
@@ -478,9 +541,32 @@ class WalletModel with ChangeNotifier {
_desktopWalletPassword = password;
}
+ /// Connects the open wallet to the configured server. Callers that arrive
+ /// while an attempt is in flight join it instead of starting a second one:
+ /// two overlapping `Wallet_init` calls race on the same native wallet, and
+ /// over Tor they burn a second circuit for nothing.
Future<void> connectToDaemon() async {
+ final inFlight = _connectInFlight;
+ if (inFlight != null) return inFlight;
+
+ final attempt = _runConnectAttempt();
+ _connectInFlight = attempt;
+
+ try {
+ await attempt;
+ } finally {
+ if (identical(_connectInFlight, attempt)) _connectInFlight = null;
+ // A connect can fail without throwing — Wallet_connectToDaemon reports
+ // failure by logging — so the backoff counts outcomes, not exceptions.
+ _connectFailures = _isConnected ? 0 : _connectFailures + 1;
+ }
+ }
+
+ Future<void> _runConnectAttempt() async {
if (_w2Wallet == null) throw Exception("w2wallet is null");
+ _lastConnectAttempt = DateTime.now();
+
// The open wallet is bound to the factory of the mode it was opened in
// (LWS vs node). Connecting before a rebuild would call Wallet_init with a
// mismatched lightWallet flag and abort.
@@ -516,6 +602,9 @@ class WalletModel with ChangeNotifier {
);
_hasAttemptedConnection = true;
+ // Read the outcome now rather than leaving it to the 1s poll — that's up to
+ // a second of sync spinner after a connection is already up.
+ _isConnected = await getIsConnected();
notifyListeners();
}
@@ -913,7 +1002,7 @@ class WalletModel with ChangeNotifier {
final restoreHeight = getHeightByDate(date: DateTime.now());
log(LogLevel.info, 'Using blockchain height: $restoreHeight');
- await restoreFromMnemonic(polyseed, restoreHeight);
+ await restoreFromMnemonic(polyseed, restoreHeight, isNewWallet: true);
await SharedPreferencesService.set<int>(
SharedPreferencesKeys.walletRestoreHeight,
restoreHeight,
@@ -993,6 +1082,7 @@ class WalletModel with ChangeNotifier {
required int restoreHeight,
required String password,
bool isDummy = false,
+ bool newWallet = true,
}) async {
if (!isDummy && password == '') {
throw Exception('Password should not be empty.');
@@ -1001,6 +1091,10 @@ class WalletModel with ChangeNotifier {
final wmFfiAddr = (await _walletManager()).ffiAddress();
final walletPath = await resolveWalletPath();
+ // `newWallet` is what tells the backend this seed has history to scan. With
+ // it set, both backends ignore the restore height: wallet2 starts the scan
+ // at the current chain tip and LWSF never asks the server to rescan, so a
+ // restored wallet comes up empty.
final walletFfiAddr = await Isolate.run(() {
// ignore: deprecated_member_use
return monero.WalletManager_createWalletFromPolyseed(
@@ -1010,7 +1104,7 @@ class WalletModel with ChangeNotifier {
restoreHeight: restoreHeight,
path: isDummy ? '' : walletPath,
password: password,
- newWallet: true,
+ newWallet: newWallet,
kdfRounds: 1,
).address;
});
@@ -1020,33 +1114,82 @@ class WalletModel with ChangeNotifier {
return MoneroWallet(Pointer<Void>.fromAddress(walletFfiAddr));
}
- Future<void> restoreFromMnemonic(
- String mnemonic,
- int restoreHeight, [
- String passphrase = '',
- ]) async {
- final walletPassword = _desktopWalletPassword ?? genWalletPassword();
- MoneroWallet? wallet;
+ /// Removes the wallet files for the current mode (cache + `.keys` +
+ /// `.address.txt`). The other mode's files are left alone.
+ Future<void> _deleteWalletFilesForCurrentMode() async {
+ final path = await resolveWalletPath();
- if (Polyseed.isValidSeed(mnemonic)) {
- wallet = await _getWalletFromPolyseed(
+ for (final p in [path, '$path.keys', '$path.address.txt']) {
+ final file = File(p);
+ if (await file.exists()) {
+ log(LogLevel.warn, 'Removing existing wallet file before restore: $p');
+ await file.delete();
+ }
+ }
+ }
+
+ /// Builds the wallet for [mnemonic] with the factory its seed format needs.
+ /// A BIP39 mnemonic is converted to the equivalent legacy word list first.
+ Future<MoneroWallet> _buildWalletFromMnemonic({
+ required String mnemonic,
+ required int restoreHeight,
+ required String password,
+ required bool isPolyseed,
+ required bool isNewWallet,
+ }) async {
+ if (isPolyseed) {
+ return _getWalletFromPolyseed(
mnemonic: mnemonic,
restoreHeight: restoreHeight,
- password: walletPassword,
+ password: password,
+ newWallet: isNewWallet,
);
- } else if (bip39.validateMnemonic(mnemonic)) {
- final legacyMnemonic = getLegacySeedFromBip39(mnemonic);
+ }
- wallet = await _getWalletFromLegacySeed(
- mnemonic: legacyMnemonic,
- restoreHeight: restoreHeight,
- password: walletPassword,
- );
- } else {
- wallet = await _getWalletFromLegacySeed(
+ return _getWalletFromLegacySeed(
+ mnemonic: bip39.validateMnemonic(mnemonic) ? getLegacySeedFromBip39(mnemonic) : mnemonic,
+ restoreHeight: restoreHeight,
+ password: password,
+ );
+ }
+
+ /// Restores (or, with [isNewWallet], creates) a wallet from [mnemonic].
+ /// [isNewWallet] must only be set for a seed generated right now: it tells
+ /// the backend there is no history behind the seed, which skips the rescan
+ /// from [restoreHeight].
+ Future<void> restoreFromMnemonic(
+ String mnemonic,
+ int restoreHeight, {
+ String passphrase = '',
+ bool isNewWallet = false,
+ }) async {
+ final walletPassword = _desktopWalletPassword ?? genWalletPassword();
+ final isPolyseed = Polyseed.isValidSeed(mnemonic);
+
+ var wallet = await _buildWalletFromMnemonic(
+ mnemonic: mnemonic,
+ restoreHeight: restoreHeight,
+ password: walletPassword,
+ isPolyseed: isPolyseed,
+ isNewWallet: isNewWallet,
+ );
+
+ // wallet2 refuses to recover onto an existing wallet file, which surfaces
+ // as an unexplained restore failure the user can't get out of. Reaching
+ // this means the seed itself was accepted (it's decoded before the file is
+ // touched), so the mode's derived files can be cleared and the restore
+ // retried. Not done when creating a wallet: there'd be no seed in hand to
+ // recover a clobbered file from.
+ if (!isNewWallet && wallet.errorString().contains('file already exists')) {
+ log(LogLevel.warn, 'Restore hit an existing wallet file: ${wallet.errorString()}');
+ await _deleteWalletFilesForCurrentMode();
+
+ wallet = await _buildWalletFromMnemonic(
mnemonic: mnemonic,
restoreHeight: restoreHeight,
password: walletPassword,
+ isPolyseed: isPolyseed,
+ isNewWallet: isNewWallet,
);
}
@@ -1065,6 +1208,24 @@ class WalletModel with ChangeNotifier {
_w2TxHistory = _w2Wallet!.history();
_loadedType = _desiredManagerType;
+ if (!isNewWallet && restoreHeight > 0) {
+ // wallet2's polyseed factory derives the scan start from the seed's
+ // birthday and drops the height handed to it, so apply it here. The other
+ // factories already took it: LWSF's polyseed path honours it once
+ // newWallet is false, and both recoveryWallet implementations set it.
+ if (isPolyseed && isNodeMode) {
+ log(LogLevel.info, 'Setting refresh from block height: $restoreHeight');
+ wallet.setRefreshFromBlockHeight(refresh_from_block_height: restoreHeight);
+ }
+
+ // Fallback for getRestoreHeight(), which needs a height to rebuild the
+ // other mode's wallet file from the seed on an LWS↔node switch.
+ await SharedPreferencesService.set<int>(
+ SharedPreferencesKeys.walletRestoreHeight,
+ restoreHeight,
+ );
+ }
+
if (Platform.isAndroid || Platform.isIOS) {
await storeMobileWalletPassword(walletPassword);
}
@@ -1074,6 +1235,13 @@ class WalletModel with ChangeNotifier {
}
Future<void> openExisting({String? desktopWalletPassword}) async {
+ // Opening the same file twice leaves two wallets (and two sync loops)
+ // running against it; the first is never closed and both write the cache.
+ if (_w2Wallet != null && _loadedType == _desiredManagerType) {
+ log(LogLevel.warn, 'Wallet is already open for "$_loadedType"; skipping re-open.');
+ return;
+ }
+
final wm = await _walletManager();
final path = await resolveWalletPath();
@@ -1213,11 +1381,20 @@ class WalletModel with ChangeNotifier {
}
Future<bool> hasExistingWallet() async {
+ // Each mode keeps its own file in a format only its own manager recognizes
+ // (LWSF's `mywallet` vs wallet2's `mywallet_node`), so the persisted
+ // connection has to be known before looking one up — checking the default
+ // LWS path makes a node wallet look like a fresh install and drops the user
+ // back into onboarding.
+ await _ensureConnectionLoaded();
+
+ final path = await resolveWalletPath();
+
log(LogLevel.info, 'Calling WalletManager_walletExists with parameters:');
- log(LogLevel.info, ' path: ${await getWalletPath()}');
+ log(LogLevel.info, ' path: $path');
final wm = await _walletManager();
- final exists = wm.walletExists(await getWalletPath());
+ final exists = wm.walletExists(path);
log(LogLevel.info, 'WalletManager_walletExists result: $exists');
@@ -1227,7 +1404,43 @@ class WalletModel with ChangeNotifier {
log(LogLevel.error, 'WalletManager_walletExists error: $errorString');
}
- return exists;
+ if (exists) {
+ return true;
+ }
+
+ return _adoptModeWithExistingWallet();
+ }
+
+ /// Recovers from a connection/wallet-file mismatch: when the persisted mode
+ /// has no wallet file but the other mode does (e.g. an LWS↔node switch that
+ /// was interrupted before the new file was written), switch to the mode we
+ /// actually have a wallet for instead of reporting "no wallet" and sending
+ /// the user through onboarding on top of an existing wallet.
+ Future<bool> _adoptModeWithExistingWallet() async {
+ final otherType = isNodeMode ? 'lws' : 'node';
+ final otherPath = await walletPathForType(otherType);
+
+ // wallet2 (node) writes `<path>` plus `<path>.keys`; LWSF writes a single
+ // `<path>` file.
+ final otherExists = await File(otherPath).exists() || await File('$otherPath.keys').exists();
+
+ if (!otherExists) {
+ return false;
+ }
+
+ log(
+ LogLevel.warn,
+ 'No "$_connectionType" wallet file found but a "$otherType" one exists; '
+ 'switching the connection type to match.',
+ );
+
+ _connectionType = otherType;
+ // Persisted, not just in-memory: callers reload the connection from prefs
+ // right after this check.
+ await SharedPreferencesService.set<String>(SharedPreferencesKeys.connectionType, otherType);
+ notifyListeners();
+
+ return true;
}
Future<bool> getIsConnected() async {
diff --git a/lib/screens/create_wallet.dart b/lib/screens/create_wallet.dart
index 55d0d58..1b61484 100644
--- a/lib/screens/create_wallet.dart
+++ b/lib/screens/create_wallet.dart
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/wallet_model.dart';
class CreateWalletScreenArgs {
String toastMessage;
@@ -34,6 +37,18 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
});
}
+ /// The warning is about the LWS server learning your whole transaction
+ /// history from the view key. A full node scans locally and is told nothing,
+ /// so restoring against one goes straight to the seed form.
+ void _restoreExisting() {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+
+ Navigator.pushNamed(
+ context,
+ wallet.isNodeMode ? '/restore_wallet' : '/restore_warning',
+ );
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
@@ -62,8 +77,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
- onPressed: () =>
- Navigator.pushNamed(context, '/restore_warning'),
+ onPressed: _restoreExisting,
child: Text(i18n.createWalletRestoreExistingButton),
),
FilledButton(
diff --git a/pubspec.lock b/pubspec.lock
index 53591af..7dc49eb 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -665,8 +665,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
- ref: "8c9af02665e0c0c8e2e8dc7022679b2743842ed4"
- resolved-ref: "8c9af02665e0c0c8e2e8dc7022679b2743842ed4"
+ ref: c74f8dfcc07f56720c0239d5829995c8e20033c2
+ resolved-ref: c74f8dfcc07f56720c0239d5829995c8e20033c2
url: "https://github.com/magicgrants/monero_c"
source: git
version: "0.0.0"
Why this scored 59/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.