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
}