Try to show seeds if wallet files gets corrupted (#1567)

* add litecoin nodes minor ui fix * Try to open the wallet or fetch the seeds and show them to the user * make sure the seeds are only displayed after authentication

Omar Hatem committed Aug 6, 2024 at 17:59 UTC 5e944a8bf7069f0b083f177c44d1b4e5eb1f265e
9 files changed +175 -13
assets/litecoin_electrum_server_list.yml
+16 -1
@@ -1,4 +1,19 @@
1 -
2 uri: ltc-electrum.cakewallet.com:50002
3 useSSL: true
4 - isDefault: true
\ No newline at end of file
4 + isDefault: true
5 +-
6 + uri: litecoin.stackwallet.com:20063
7 + useSSL: true
8 +-
9 + uri: electrum-ltc.bysh.me:50002
10 + useSSL: true
11 +-
12 + uri: lightweight.fiatfaucet.com:50002
13 + useSSL: true
14 +-
15 + uri: electrum.ltc.xurious.com:50002
16 + useSSL: true
17 +-
18 + uri: backup.electrum-ltc.org:443
19 + useSSL: true
cw_core/lib/wallet_service.dart
+19
@@ -1,6 +1,8 @@
1 +import 'dart:convert';
2 import 'dart:io';
3
4 import 'package:cw_core/pathForWallet.dart';
5 +import 'package:cw_core/utils/file.dart';
6 import 'package:cw_core/wallet_base.dart';
7 import 'package:cw_core/wallet_credentials.dart';
8 import 'package:cw_core/wallet_type.dart';
@@ -42,4 +44,21 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
44 await File(walletDirPath).copy(backupWalletDirPath);
45 }
46 }
47 +
48 + Future<String> getSeeds(String name, String password, WalletType type) async {
49 + try {
50 + final path = await pathForWallet(name: name, type: type);
51 + final jsonSource = await read(path: path, password: password);
52 + try {
53 + final data = json.decode(jsonSource) as Map;
54 + return data['mnemonic'] as String? ?? '';
55 + } catch (_) {
56 + // if not a valid json
57 + return jsonSource.substring(0, 200);
58 + }
59 + } catch (_) {
60 + // if the file couldn't be opened or read
61 + return '';
62 + }
63 + }
64 }
cw_monero/lib/monero_wallet.dart
-1
@@ -19,7 +19,6 @@ import 'package:cw_core/transaction_priority.dart';
19 import 'package:cw_core/unspent_coins_info.dart';
20 import 'package:cw_core/wallet_base.dart';
21 import 'package:cw_core/wallet_info.dart';
22 -import 'package:cw_monero/api/account_list.dart';
22 import 'package:cw_monero/api/coins_info.dart';
23 import 'package:cw_monero/api/monero_output.dart';
24 import 'package:cw_monero/api/structs/pending_transaction.dart';
cw_monero/lib/monero_wallet_service.dart
+29 -8
@@ -57,8 +57,11 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
57 final String spendKey;
58 }
59
60 -class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
61 - MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials, MoneroNewWalletCredentials> {
60 +class MoneroWalletService extends WalletService<
61 + MoneroNewWalletCredentials,
62 + MoneroRestoreWalletFromSeedCredentials,
63 + MoneroRestoreWalletFromKeysCredentials,
64 + MoneroNewWalletCredentials> {
65 MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
66
67 final Box<WalletInfo> walletInfoSource;
@@ -183,11 +186,8 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
186 final wmaddr = wmPtr.address;
187 final waddr = openedWalletsByPath["$path/$wallet"]!.address;
188 // await Isolate.run(() {
186 - monero.WalletManager_closeWallet(
187 - Pointer.fromAddress(wmaddr),
188 - Pointer.fromAddress(waddr),
189 - false
190 - );
189 + monero.WalletManager_closeWallet(
190 + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false);
191 // });
192 openedWalletsByPath.remove("$path/$wallet");
193 print("wallet closed");
@@ -248,7 +248,8 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
248
249 @override
250 Future<MoneroWallet> restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) {
251 - throw UnimplementedError("Restoring a Monero wallet from a hardware wallet is not yet supported!");
251 + throw UnimplementedError(
252 + "Restoring a Monero wallet from a hardware wallet is not yet supported!");
253 }
254
255 @override
@@ -350,4 +351,24 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
351 print(e.toString());
352 }
353 }
354 +
355 + @override
356 + Future<String> getSeeds(String name, String password, WalletType type) async {
357 + try {
358 + final path = await pathForWallet(name: name, type: getType());
359 +
360 + if (walletFilesExist(path)) {
361 + await repairOldAndroidWallet(name);
362 + }
363 +
364 + await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
365 + final walletInfo = walletInfoSource.values
366 + .firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
367 + final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
368 + return wallet.seed;
369 + } catch (_) {
370 + // if the file couldn't be opened or read
371 + return '';
372 + }
373 + }
374 }
lib/core/wallet_loading_service.dart
+29 -2
@@ -1,6 +1,9 @@
1 +import 'dart:async';
2 +
3 import 'package:cake_wallet/core/generate_wallet_password.dart';
4 import 'package:cake_wallet/core/key_service.dart';
5 import 'package:cake_wallet/entities/preferences_key.dart';
6 +import 'package:cake_wallet/reactions/on_authentication_state_change.dart';
7 import 'package:cake_wallet/utils/exception_handler.dart';
8 import 'package:cw_core/cake_hive.dart';
9 import 'package:cw_core/wallet_base.dart';
@@ -52,6 +55,12 @@ class WalletLoadingService {
55 } catch (error, stack) {
56 ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
57
58 + // try fetching the seeds of the corrupted wallet to show it to the user
59 + String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):";
60 + try {
61 + corruptedWalletsSeeds += await _getCorruptedWalletSeeds(name, type);
62 + } catch (_) {}
63 +
64 // try opening another wallet that is not corrupted to give user access to the app
65 final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
66
@@ -69,12 +78,23 @@ class WalletLoadingService {
78 await sharedPreferences.setInt(
79 PreferencesKey.currentWalletType, serializeToInt(wallet.type));
80
81 + // if found a wallet that is not corrupted, then still display the seeds of the corrupted ones
82 + authenticatedErrorStreamController.add(corruptedWalletsSeeds);
83 +
84 return wallet;
73 - } catch (_) {}
85 + } catch (_) {
86 + // save seeds and show corrupted wallets' seeds to the user
87 + try {
88 + final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type);
89 + if (!corruptedWalletsSeeds.contains(seeds)) {
90 + corruptedWalletsSeeds += seeds;
91 + }
92 + } catch (_) {}
93 + }
94 }
95
96 // if all user's wallets are corrupted throw exception
77 - throw error;
97 + throw error.toString() + "\n\n" + corruptedWalletsSeeds;
98 }
99 }
100
@@ -96,4 +116,11 @@ class WalletLoadingService {
116 isPasswordUpdated = true;
117 await sharedPreferences.setBool(key, isPasswordUpdated);
118 }
119 +
120 + Future<String> _getCorruptedWalletSeeds(String name, WalletType type) async {
121 + final walletService = walletServiceFactory.call(type);
122 + final password = await keyService.getWalletPassword(walletName: name);
123 +
124 + return "\n\n$type ($name): ${await walletService.getSeeds(name, password, type)}";
125 + }
126 }
lib/di.dart
+14
@@ -1,3 +1,5 @@
1 +import 'dart:async' show Timer;
2 +
3 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 import 'package:cake_wallet/anonpay/anonpay_api.dart';
5 import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
@@ -487,6 +489,7 @@ Future<void> setup({
489
490 if (loginError != null) {
491 authPageState.changeProcessText('ERROR: ${loginError.toString()}');
492 + loginError = null;
493 }
494
495 ReactionDisposer? _reaction;
@@ -498,6 +501,17 @@ Future<void> setup({
501 linkViewModel.handleLink();
502 }
503 });
504 +
505 + Timer.periodic(Duration(seconds: 1), (timer) {
506 + if (timer.tick > 30) {
507 + timer.cancel();
508 + }
509 +
510 + if (loginError != null) {
511 + authPageState.changeProcessText('ERROR: ${loginError.toString()}');
512 + timer.cancel();
513 + }
514 + });
515 }
516 });
517 });
lib/reactions/on_authentication_state_change.dart
+14
@@ -1,3 +1,5 @@
1 +import 'dart:async';
2 +
3 import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/utils/exception_handler.dart';
5 import 'package:flutter/widgets.dart';
@@ -8,9 +10,16 @@ import 'package:cake_wallet/store/authentication_store.dart';
10 ReactionDisposer? _onAuthenticationStateChange;
11
12 dynamic loginError;
13 +StreamController<dynamic> authenticatedErrorStreamController = StreamController<dynamic>();
14
15 void startAuthenticationStateChange(
16 AuthenticationStore authenticationStore, GlobalKey<NavigatorState> navigatorKey) {
17 + authenticatedErrorStreamController.stream.listen((event) {
18 + if (authenticationStore.state == AuthenticationState.allowed) {
19 + ExceptionHandler.showError(event.toString(), delayInSeconds: 3);
20 + }
21 + });
22 +
23 _onAuthenticationStateChange ??= autorun((_) async {
24 final state = authenticationStore.state;
25
@@ -26,6 +35,11 @@ void startAuthenticationStateChange(
35
36 if (state == AuthenticationState.allowed) {
37 await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
38 + if (!(await authenticatedErrorStreamController.stream.isEmpty)) {
39 + ExceptionHandler.showError(
40 + (await authenticatedErrorStreamController.stream.first).toString());
41 + authenticatedErrorStreamController.stream.drain();
42 + }
43 return;
44 }
45 });
lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart
+3 -1
@@ -51,7 +51,9 @@ class MoneroAccountEditOrCreatePage extends BasePage {
51
52 await moneroAccountCreationViewModel.save();
53
54 - Navigator.of(context).pop(_textController.text);
54 + if (context.mounted) {
55 + Navigator.of(context).pop(_textController.text);
56 + }
57 },
58 text: moneroAccountCreationViewModel.isEdit
59 ? S.of(context).rename
lib/utils/exception_handler.dart
+51
@@ -4,11 +4,13 @@ import 'package:cake_wallet/entities/preferences_key.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/main.dart';
6 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
7 +import 'package:cake_wallet/utils/show_bar.dart';
8 import 'package:cake_wallet/utils/show_pop_up.dart';
9 import 'package:cw_core/root_dir.dart';
10 import 'package:device_info_plus/device_info_plus.dart';
11 import 'package:flutter/foundation.dart';
12 import 'package:flutter/material.dart';
13 +import 'package:flutter/services.dart';
14 import 'package:flutter_mailer/flutter_mailer.dart';
15 import 'package:cake_wallet/utils/package_info.dart';
16 import 'package:shared_preferences/shared_preferences.dart';
@@ -254,4 +256,53 @@ class ExceptionHandler {
256 'productName': data.productName,
257 };
258 }
259 +
260 + static void showError(String error, {int? delayInSeconds}) async {
261 + if (_hasError) {
262 + return;
263 + }
264 + _hasError = true;
265 +
266 + if (delayInSeconds != null) {
267 + Future.delayed(Duration(seconds: delayInSeconds), () => _showCopyPopup(error));
268 + return;
269 + }
270 +
271 + WidgetsBinding.instance.addPostFrameCallback(
272 + (_) async => _showCopyPopup(error),
273 + );
274 + }
275 +
276 + static Future<void> _showCopyPopup(String content) async {
277 + if (navigatorKey.currentContext != null) {
278 + final shouldCopy = await showPopUp<bool?>(
279 + context: navigatorKey.currentContext!,
280 + builder: (context) {
281 + return AlertWithTwoActions(
282 + isDividerExist: true,
283 + alertTitle: S.of(context).error,
284 + alertContent: content,
285 + rightButtonText: S.of(context).copy,
286 + leftButtonText: S.of(context).close,
287 + actionRightButton: () {
288 + Navigator.of(context).pop(true);
289 + },
290 + actionLeftButton: () {
291 + Navigator.of(context).pop();
292 + },
293 + );
294 + },
295 + );
296 +
297 + if (shouldCopy == true) {
298 + await Clipboard.setData(ClipboardData(text: content));
299 + await showBar<void>(
300 + navigatorKey.currentContext!,
301 + S.of(navigatorKey.currentContext!).copied_to_clipboard,
302 + );
303 + }
304 + }
305 +
306 + _hasError = false;
307 + }
308 }