mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
Try to show seeds if wallet files gets corrupted (#1567)
Some checks are pending
Cache Dependencies / test (push) Waiting to run
Some checks are pending
Cache Dependencies / test (push) Waiting to run
* 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
This commit is contained in:
parent
9da9bee384
commit
5e944a8bf7
9 changed files with 175 additions and 13 deletions
|
@ -2,3 +2,18 @@
|
||||||
uri: ltc-electrum.cakewallet.com:50002
|
uri: ltc-electrum.cakewallet.com:50002
|
||||||
useSSL: true
|
useSSL: true
|
||||||
isDefault: true
|
isDefault: true
|
||||||
|
-
|
||||||
|
uri: litecoin.stackwallet.com:20063
|
||||||
|
useSSL: true
|
||||||
|
-
|
||||||
|
uri: electrum-ltc.bysh.me:50002
|
||||||
|
useSSL: true
|
||||||
|
-
|
||||||
|
uri: lightweight.fiatfaucet.com:50002
|
||||||
|
useSSL: true
|
||||||
|
-
|
||||||
|
uri: electrum.ltc.xurious.com:50002
|
||||||
|
useSSL: true
|
||||||
|
-
|
||||||
|
uri: backup.electrum-ltc.org:443
|
||||||
|
useSSL: true
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cw_core/pathForWallet.dart';
|
import 'package:cw_core/pathForWallet.dart';
|
||||||
|
import 'package:cw_core/utils/file.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_credentials.dart';
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
@ -42,4 +44,21 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||||
await File(walletDirPath).copy(backupWalletDirPath);
|
await File(walletDirPath).copy(backupWalletDirPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getSeeds(String name, String password, WalletType type) async {
|
||||||
|
try {
|
||||||
|
final path = await pathForWallet(name: name, type: type);
|
||||||
|
final jsonSource = await read(path: path, password: password);
|
||||||
|
try {
|
||||||
|
final data = json.decode(jsonSource) as Map;
|
||||||
|
return data['mnemonic'] as String? ?? '';
|
||||||
|
} catch (_) {
|
||||||
|
// if not a valid json
|
||||||
|
return jsonSource.substring(0, 200);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// if the file couldn't be opened or read
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import 'package:cw_core/transaction_priority.dart';
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_monero/api/account_list.dart';
|
|
||||||
import 'package:cw_monero/api/coins_info.dart';
|
import 'package:cw_monero/api/coins_info.dart';
|
||||||
import 'package:cw_monero/api/monero_output.dart';
|
import 'package:cw_monero/api/monero_output.dart';
|
||||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||||
|
|
|
@ -57,8 +57,11 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
|
||||||
final String spendKey;
|
final String spendKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
|
class MoneroWalletService extends WalletService<
|
||||||
MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials, MoneroNewWalletCredentials> {
|
MoneroNewWalletCredentials,
|
||||||
|
MoneroRestoreWalletFromSeedCredentials,
|
||||||
|
MoneroRestoreWalletFromKeysCredentials,
|
||||||
|
MoneroNewWalletCredentials> {
|
||||||
MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
|
MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
|
||||||
|
|
||||||
final Box<WalletInfo> walletInfoSource;
|
final Box<WalletInfo> walletInfoSource;
|
||||||
|
@ -184,10 +187,7 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
|
||||||
final waddr = openedWalletsByPath["$path/$wallet"]!.address;
|
final waddr = openedWalletsByPath["$path/$wallet"]!.address;
|
||||||
// await Isolate.run(() {
|
// await Isolate.run(() {
|
||||||
monero.WalletManager_closeWallet(
|
monero.WalletManager_closeWallet(
|
||||||
Pointer.fromAddress(wmaddr),
|
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false);
|
||||||
Pointer.fromAddress(waddr),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
// });
|
// });
|
||||||
openedWalletsByPath.remove("$path/$wallet");
|
openedWalletsByPath.remove("$path/$wallet");
|
||||||
print("wallet closed");
|
print("wallet closed");
|
||||||
|
@ -248,7 +248,8 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MoneroWallet> restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) {
|
Future<MoneroWallet> restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) {
|
||||||
throw UnimplementedError("Restoring a Monero wallet from a hardware wallet is not yet supported!");
|
throw UnimplementedError(
|
||||||
|
"Restoring a Monero wallet from a hardware wallet is not yet supported!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -350,4 +351,24 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getSeeds(String name, String password, WalletType type) async {
|
||||||
|
try {
|
||||||
|
final path = await pathForWallet(name: name, type: getType());
|
||||||
|
|
||||||
|
if (walletFilesExist(path)) {
|
||||||
|
await repairOldAndroidWallet(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
|
||||||
|
final walletInfo = walletInfoSource.values
|
||||||
|
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||||
|
final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||||
|
return wallet.seed;
|
||||||
|
} catch (_) {
|
||||||
|
// if the file couldn't be opened or read
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cake_wallet/core/generate_wallet_password.dart';
|
import 'package:cake_wallet/core/generate_wallet_password.dart';
|
||||||
import 'package:cake_wallet/core/key_service.dart';
|
import 'package:cake_wallet/core/key_service.dart';
|
||||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||||
|
import 'package:cake_wallet/reactions/on_authentication_state_change.dart';
|
||||||
import 'package:cake_wallet/utils/exception_handler.dart';
|
import 'package:cake_wallet/utils/exception_handler.dart';
|
||||||
import 'package:cw_core/cake_hive.dart';
|
import 'package:cw_core/cake_hive.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
@ -52,6 +55,12 @@ class WalletLoadingService {
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
|
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
|
||||||
|
|
||||||
|
// try fetching the seeds of the corrupted wallet to show it to the user
|
||||||
|
String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):";
|
||||||
|
try {
|
||||||
|
corruptedWalletsSeeds += await _getCorruptedWalletSeeds(name, type);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
// try opening another wallet that is not corrupted to give user access to the app
|
// try opening another wallet that is not corrupted to give user access to the app
|
||||||
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
|
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
|
||||||
|
|
||||||
|
@ -69,12 +78,23 @@ class WalletLoadingService {
|
||||||
await sharedPreferences.setInt(
|
await sharedPreferences.setInt(
|
||||||
PreferencesKey.currentWalletType, serializeToInt(wallet.type));
|
PreferencesKey.currentWalletType, serializeToInt(wallet.type));
|
||||||
|
|
||||||
|
// if found a wallet that is not corrupted, then still display the seeds of the corrupted ones
|
||||||
|
authenticatedErrorStreamController.add(corruptedWalletsSeeds);
|
||||||
|
|
||||||
return wallet;
|
return wallet;
|
||||||
|
} catch (_) {
|
||||||
|
// save seeds and show corrupted wallets' seeds to the user
|
||||||
|
try {
|
||||||
|
final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type);
|
||||||
|
if (!corruptedWalletsSeeds.contains(seeds)) {
|
||||||
|
corruptedWalletsSeeds += seeds;
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if all user's wallets are corrupted throw exception
|
// if all user's wallets are corrupted throw exception
|
||||||
throw error;
|
throw error.toString() + "\n\n" + corruptedWalletsSeeds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,4 +116,11 @@ class WalletLoadingService {
|
||||||
isPasswordUpdated = true;
|
isPasswordUpdated = true;
|
||||||
await sharedPreferences.setBool(key, isPasswordUpdated);
|
await sharedPreferences.setBool(key, isPasswordUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _getCorruptedWalletSeeds(String name, WalletType type) async {
|
||||||
|
final walletService = walletServiceFactory.call(type);
|
||||||
|
final password = await keyService.getWalletPassword(walletName: name);
|
||||||
|
|
||||||
|
return "\n\n$type ($name): ${await walletService.getSeeds(name, password, type)}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
14
lib/di.dart
14
lib/di.dart
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async' show Timer;
|
||||||
|
|
||||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||||
import 'package:cake_wallet/anonpay/anonpay_api.dart';
|
import 'package:cake_wallet/anonpay/anonpay_api.dart';
|
||||||
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
|
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
|
||||||
|
@ -487,6 +489,7 @@ Future<void> setup({
|
||||||
|
|
||||||
if (loginError != null) {
|
if (loginError != null) {
|
||||||
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
|
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
|
||||||
|
loginError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionDisposer? _reaction;
|
ReactionDisposer? _reaction;
|
||||||
|
@ -498,6 +501,17 @@ Future<void> setup({
|
||||||
linkViewModel.handleLink();
|
linkViewModel.handleLink();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
|
if (timer.tick > 30) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginError != null) {
|
||||||
|
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/utils/exception_handler.dart';
|
import 'package:cake_wallet/utils/exception_handler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -8,9 +10,16 @@ import 'package:cake_wallet/store/authentication_store.dart';
|
||||||
ReactionDisposer? _onAuthenticationStateChange;
|
ReactionDisposer? _onAuthenticationStateChange;
|
||||||
|
|
||||||
dynamic loginError;
|
dynamic loginError;
|
||||||
|
StreamController<dynamic> authenticatedErrorStreamController = StreamController<dynamic>();
|
||||||
|
|
||||||
void startAuthenticationStateChange(
|
void startAuthenticationStateChange(
|
||||||
AuthenticationStore authenticationStore, GlobalKey<NavigatorState> navigatorKey) {
|
AuthenticationStore authenticationStore, GlobalKey<NavigatorState> navigatorKey) {
|
||||||
|
authenticatedErrorStreamController.stream.listen((event) {
|
||||||
|
if (authenticationStore.state == AuthenticationState.allowed) {
|
||||||
|
ExceptionHandler.showError(event.toString(), delayInSeconds: 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_onAuthenticationStateChange ??= autorun((_) async {
|
_onAuthenticationStateChange ??= autorun((_) async {
|
||||||
final state = authenticationStore.state;
|
final state = authenticationStore.state;
|
||||||
|
|
||||||
|
@ -26,6 +35,11 @@ void startAuthenticationStateChange(
|
||||||
|
|
||||||
if (state == AuthenticationState.allowed) {
|
if (state == AuthenticationState.allowed) {
|
||||||
await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||||
|
if (!(await authenticatedErrorStreamController.stream.isEmpty)) {
|
||||||
|
ExceptionHandler.showError(
|
||||||
|
(await authenticatedErrorStreamController.stream.first).toString());
|
||||||
|
authenticatedErrorStreamController.stream.drain();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,9 @@ class MoneroAccountEditOrCreatePage extends BasePage {
|
||||||
|
|
||||||
await moneroAccountCreationViewModel.save();
|
await moneroAccountCreationViewModel.save();
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop(_textController.text);
|
Navigator.of(context).pop(_textController.text);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
text: moneroAccountCreationViewModel.isEdit
|
text: moneroAccountCreationViewModel.isEdit
|
||||||
? S.of(context).rename
|
? S.of(context).rename
|
||||||
|
|
|
@ -4,11 +4,13 @@ import 'package:cake_wallet/entities/preferences_key.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/main.dart';
|
import 'package:cake_wallet/main.dart';
|
||||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||||
|
import 'package:cake_wallet/utils/show_bar.dart';
|
||||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
import 'package:cw_core/root_dir.dart';
|
import 'package:cw_core/root_dir.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_mailer/flutter_mailer.dart';
|
import 'package:flutter_mailer/flutter_mailer.dart';
|
||||||
import 'package:cake_wallet/utils/package_info.dart';
|
import 'package:cake_wallet/utils/package_info.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -254,4 +256,53 @@ class ExceptionHandler {
|
||||||
'productName': data.productName,
|
'productName': data.productName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void showError(String error, {int? delayInSeconds}) async {
|
||||||
|
if (_hasError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hasError = true;
|
||||||
|
|
||||||
|
if (delayInSeconds != null) {
|
||||||
|
Future.delayed(Duration(seconds: delayInSeconds), () => _showCopyPopup(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) async => _showCopyPopup(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showCopyPopup(String content) async {
|
||||||
|
if (navigatorKey.currentContext != null) {
|
||||||
|
final shouldCopy = await showPopUp<bool?>(
|
||||||
|
context: navigatorKey.currentContext!,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertWithTwoActions(
|
||||||
|
isDividerExist: true,
|
||||||
|
alertTitle: S.of(context).error,
|
||||||
|
alertContent: content,
|
||||||
|
rightButtonText: S.of(context).copy,
|
||||||
|
leftButtonText: S.of(context).close,
|
||||||
|
actionRightButton: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
actionLeftButton: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldCopy == true) {
|
||||||
|
await Clipboard.setData(ClipboardData(text: content));
|
||||||
|
await showBar<void>(
|
||||||
|
navigatorKey.currentContext!,
|
||||||
|
S.of(navigatorKey.currentContext!).copied_to_clipboard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasError = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue