Minor refactoring and subaddress check result caching
What changed, and why it matters
This commit is a routine cleanup of a Monero wallet app. It mainly moves repeated startup steps into a single helper function, caches whether the server supports subaddresses in local app storage, and improves loading indicators on the connection setup screen. There is no obvious security bug introduced, but the change is not purely cosmetic because it alters when and how the app checks server capabilities and stores that result.
Review as normal code maintenance. Specifically verify that cached serverSupportsSubaddresses cannot be used to bypass a fresh capability check when the server or connection settings change, and that the rethrow in checkSubaddressSupport() does not leave the wallet in an unhandled error state. No immediate security patch is indicated.
Security signals we found
New SharedPreferences key serverSupportsSubaddresses caches a server capability check result
checkSubaddressSupport() now rethrows the last exception after exhausting retries instead of swallowing it
Connection setup now awaits persistCurrentConnection() and checks mounted before navigation
Wallet restore defers wallet.load() and stops showing loading spinner before calling it
Indentation-only whitespace changes in localization files
Evidence from the diff
The patch refactors wallet initialization by introducing WalletModel.load(), which calls loadPersistedSubaddressSupport(), refresh(), loadAllStats(), connectToDaemon(), and checkSubaddressSupport(). Subaddress support is now read from and written to SharedPreferences under a new key, and it is cleared on wallet reset. The restore flow now defers wallet.load() until after validation and sets loading state slightly differently. Connection setup now awaits persistCurrentConnection() and guards navigation with mounted. The only functional security-relevant change is the caching of server capability state and the rethrow of exceptions in checkSubaddressSupport() after retries are exhausted.
Changed components
lib/models/wallet_model.dartlib/services/shared_preferences_service.dartlib/screens/connection_setup.dartlib/screens/generate_seed.dartlib/screens/restore_wallet.dartlib/main.dartInspect captured patch +89 / −43
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index daffbf7..27810b7 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -24,7 +24,7 @@
"connectionSetupUseSslLabel": "Use SSL",
"connectionSetupTestConnectionButton": "Test Connection",
"connectionSetupContinueButton": "Continue",
- "createWalletTitle": "Create Wallet",
+ "createWalletTitle": "Create Wallet",
"createWalletDescription": "Do you already have a Monero wallet seed, or do you need to make a new one?",
"createWalletRestoreExistingButton": "Restore Existing",
"createWalletCreateNewButton": "Create New",
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 0f4fc56..cfae72b 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -24,7 +24,7 @@
"connectionSetupUseSslLabel": "Usar SSL",
"connectionSetupTestConnectionButton": "Testar Conexão",
"connectionSetupContinueButton": "Continuar",
- "createWalletTitle": "Criar Carteira",
+ "createWalletTitle": "Criar Carteira",
"createWalletDescription": "Você já possui uma semente de carteira Monero ou precisa criar uma nova?",
"createWalletRestoreExistingButton": "Restaurar Existente",
"createWalletCreateNewButton": "Criar Nova",
diff --git a/lib/main.dart b/lib/main.dart
index 977dbb1..f6980be 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -102,12 +102,7 @@ class MyApp extends StatelessWidget {
: '/welcome';
if (walletExists) {
- (() async {
- await wallet.refresh();
- await wallet.loadAllStats();
- await wallet.connectToDaemon();
- await wallet.checkSubaddressSupport();
- })();
+ wallet.load();
}
return MaterialApp(
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 0c927e8..c1f2786 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -207,6 +207,18 @@ class WalletModel with ChangeNotifier {
}
}
+ Future<void> load() async {
+ if (_w2Wallet == null) {
+ return;
+ }
+
+ loadPersistedSubaddressSupport();
+ await refresh();
+ await loadAllStats();
+ await connectToDaemon();
+ await checkSubaddressSupport();
+ }
+
Future<void> loadAllStats() async {
if (_w2Wallet == null) {
log(
@@ -402,6 +414,12 @@ class WalletModel with ChangeNotifier {
}
}
+ Future<void> loadPersistedSubaddressSupport() async {
+ _serverSupportsSubaddresses = await SharedPreferencesService.get<bool>(
+ SharedPreferencesKeys.serverSupportsSubaddresses,
+ );
+ }
+
Future<void> checkSubaddressSupport() async {
final protocol = _connectionUseSsl ? 'https' : 'http';
final url = Uri.parse('$protocol://$_connectionAddress/upsert_subaddrs');
@@ -466,12 +484,19 @@ class WalletModel with ChangeNotifier {
'Failed to check subaddress support after ${i + 1} attempts.',
);
log(LogLevel.warn, 'Error: $e');
+
+ rethrow;
}
}
}
_serverSupportsSubaddresses = httpStatus == 200;
+ await SharedPreferencesService.set<bool>(
+ SharedPreferencesKeys.serverSupportsSubaddresses,
+ _serverSupportsSubaddresses!,
+ );
+
log(
LogLevel.info,
'Subaddress support check result: $_serverSupportsSubaddresses (status: $httpStatus)',
@@ -757,6 +782,9 @@ class WalletModel with ChangeNotifier {
await SharedPreferencesService.remove(
SharedPreferencesKeys.pendingOutgoingTxs,
);
+ await SharedPreferencesService.remove(
+ SharedPreferencesKeys.serverSupportsSubaddresses,
+ );
await SharedPreferencesService.remove(SharedPreferencesKeys.contacts);
}
diff --git a/lib/screens/connection_setup.dart b/lib/screens/connection_setup.dart
index 2cdb98a..d61e1cd 100644
--- a/lib/screens/connection_setup.dart
+++ b/lib/screens/connection_setup.dart
@@ -21,11 +21,13 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
final TextEditingController _addressController = TextEditingController();
final TextEditingController _customProxyPortController =
TextEditingController();
+
bool _useTor = false;
bool _useSsl = false;
bool _hasTested = false;
- bool _isLoading = false;
+ bool _connectionTestIsLoading = false;
bool _connectionSuccess = false;
+ bool _connectionSaveIsLoading = false;
@override
void initState() {
@@ -137,7 +139,7 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
setState(() {
_hasTested = true;
- _isLoading = true;
+ _connectionTestIsLoading = true;
});
final url = '$proto://$daemonAddress/get_address_info';
@@ -179,12 +181,16 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
});
} finally {
setState(() {
- _isLoading = false;
+ _connectionTestIsLoading = false;
});
}
}
- void _saveConnection() {
+ Future<void> _saveConnection() async {
+ setState(() {
+ _connectionSaveIsLoading = true;
+ });
+
final daemonAddress = cleanAddress(_addressController.text);
final proxyAddress = _customProxyPortController.text;
@@ -197,13 +203,21 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
useSsl: _useSsl,
);
- wallet.persistCurrentConnection();
- Navigator.pushNamed(context, '/create_wallet');
+ await wallet.persistCurrentConnection();
+
+ setState(() {
+ _connectionSaveIsLoading = false;
+ });
+
+ if (mounted) {
+ Navigator.pushNamed(context, '/create_wallet');
+ }
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
body: Center(
@@ -239,7 +253,7 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
- suffixIcon: _hasTested && !_isLoading
+ suffixIcon: _hasTested && !_connectionTestIsLoading
? Icon(
_connectionSuccess
? Icons.check
@@ -286,34 +300,39 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
- TextButton(
+ TextButton.icon(
+ label: Text(i18n.connectionSetupTestConnectionButton),
onPressed: () => _testConnection(),
- child: Stack(
- alignment: Alignment.center,
- children: [
- if (!_isLoading)
- AnimatedOpacity(
- opacity: _isLoading ? 0.0 : 1.0,
- duration: Duration(milliseconds: 300),
- child: Text(
- i18n.connectionSetupTestConnectionButton,
- ),
- ),
- if (_isLoading)
- SizedBox(
+ icon: !_connectionTestIsLoading
+ ? Icon(Icons.network_check)
+ : SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
- ],
- ),
),
- if (_connectionSuccess && _hasTested && !_isLoading)
- FilledButton(
+ if (_connectionSuccess &&
+ _hasTested &&
+ !_connectionTestIsLoading)
+ FilledButton.icon(
onPressed: _saveConnection,
- child: Text(i18n.connectionSetupContinueButton),
+ icon: !_connectionSaveIsLoading
+ ? Icon(Icons.arrow_outward_rounded)
+ : SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: isDarkTheme
+ ? Theme.of(
+ context,
+ ).colorScheme.onPrimary
+ : Colors.white,
+ ),
+ ),
+ label: Text(i18n.connectionSetupContinueButton),
),
],
),
diff --git a/lib/screens/generate_seed.dart b/lib/screens/generate_seed.dart
index 21b7d55..00b8ef7 100644
--- a/lib/screens/generate_seed.dart
+++ b/lib/screens/generate_seed.dart
@@ -31,6 +31,8 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> {
try {
final seed = await wallet.create();
+ await wallet.load();
+
setState(() {
_seed = seed.split(' ');
});
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index 8538d47..3591874 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -25,7 +25,6 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
final i18n = AppLocalizations.of(context)!;
setState(() {
- _isLoading = true;
_mnemonicError = null;
_restoreHeightError = null;
});
@@ -49,16 +48,12 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
final mnemonic = _mnemonicController.text;
final restoreHeight = int.parse(_restoreHeightController.text);
+ setState(() {
+ _isLoading = true;
+ });
+
try {
await wallet.restoreFromMnemonic(mnemonic, restoreHeight);
- await wallet.refresh();
- await wallet.loadAllStats();
- await wallet.connectToDaemon();
- wallet.checkSubaddressSupport();
-
- setState(() {
- _isLoading = false;
- });
} on Exception catch (error) {
final errorMsg = error.toString().replaceFirst('Exception: ', '');
@@ -100,6 +95,12 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
return;
}
+ setState(() {
+ _isLoading = false;
+ });
+
+ wallet.load();
+
if (mounted) {
Navigator.pushNamedAndRemoveUntil(
context,
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index e3a0f28..68f950e 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -12,6 +12,7 @@ class SharedPreferencesKeys {
static const String connectionProxyPort = 'connectionProxyPort';
static const String connectionUseTor = 'connectionUseTor';
static const String connectionUseSsl = 'connectionUseSsl';
+ static const String serverSupportsSubaddresses = 'serverSupportsSubaddresses';
static const String walletRestoreHeight = 'walletRestoreHeight';
static const String txHistoryCount = 'txHistoryCount';
static const String pendingOutgoingTxs = 'pendingOutgoingTxs';
Why this scored 14/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.