From db7f025b71643776f933d6a6816783d8f4ddfc78 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 25 Oct 2023 03:19:59 +0300 Subject: [PATCH] CW-472-QR-code-restore-If-a-user-scans-a-wallet-seed-that-does-NOT-include (#1081) * add restor from qr option * minor fixes * merge OR fixes * add restoring nano from QR seed mode --- lib/router.dart | 5 + lib/routes.dart | 1 + .../restore/restore_from_qr_vm.dart | 4 + .../restore/wallet_restore_from_qr_code.dart | 217 +++++++++--------- 4 files changed, 113 insertions(+), 114 deletions(-) diff --git a/lib/router.dart b/lib/router.dart index f9a42ffc6..26a37c900 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -189,6 +189,11 @@ Route createRoute(RouteSettings settings) { param2: false)); } + case Routes.restoreWalletTypeFromQR: + return CupertinoPageRoute( + builder: (_) => getIt.get( + param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type))); + case Routes.seed: return MaterialPageRoute( fullscreenDialog: true, diff --git a/lib/routes.dart b/lib/routes.dart index 2ccc2c765..ec7ad8ae8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -6,6 +6,7 @@ class Routes { static const seed = '/seed'; static const restoreOptions = '/restore_options'; static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; + static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code'; static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation'; static const dashboard = '/dashboard'; static const send = '/send'; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 2bc595469..4ffc81cef 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; @@ -91,6 +92,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.nano: + return nano!.createNanoRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index bc100f9fe..077675d1f 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,44 +1,106 @@ import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:collection/collection.dart'; class WalletRestoreFromQRCode { WalletRestoreFromQRCode(); + static const Map _walletTypeMap = { + 'monero': WalletType.monero, + 'monero-wallet': WalletType.monero, + 'monero_wallet': WalletType.monero, + 'bitcoin': WalletType.bitcoin, + 'bitcoin-wallet': WalletType.bitcoin, + 'bitcoin_wallet': WalletType.bitcoin, + 'litecoin': WalletType.litecoin, + 'litecoin-wallet': WalletType.litecoin, + 'litecoin_wallet': WalletType.litecoin, + 'ethereum-wallet': WalletType.ethereum, + 'nano-wallet': WalletType.nano, + 'nano_wallet': WalletType.nano, + 'bitcoincash': WalletType.bitcoinCash, + 'bitcoincash-wallet': WalletType.bitcoinCash, + 'bitcoincash_wallet': WalletType.bitcoinCash, + }; + + static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; + + static WalletType? _extractWalletType(String code) { + final sortedKeys = _walletTypeMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length)); + + final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + + return _walletTypeMap[extracted]; + } + + static String? _extractAddressFromUrl(String rawString, WalletType type) { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } + + static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) { + RegExp _getPattern(int wordCount) => + RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)'); + + List patternCounts = walletType == WalletType.monero ? [25, 14, 13] : [24, 18, 12]; + + for (final count in patternCounts) { + final pattern = _getPattern(count); + final match = pattern.firstMatch(rawString); + if (match != null) { + return match.group(1)?.trim(); + } + } + return null; + } + static Future scanQRCodeForRestoring(BuildContext context) async { String code = await presentQRScanner(); - Map credentials = {}; + if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); - if (code.isEmpty) { - throw Exception('Unexpected scan QR code value: value is empty'); - } - final formattedUri = getFormattedUri(code); - final uri = Uri.parse(formattedUri); - final queryParameters = uri.queryParameters; - credentials['type'] = getWalletTypeFromUrl(uri.scheme); + WalletType? walletType; + String formattedUri = ''; - final address = getAddressFromUrl( - type: credentials['type'] as WalletType, - rawString: queryParameters.toString(), - ); - if (address != null) { - credentials['address'] = address; - } + if (!_containsAssetSpecifier(code)) { + await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually"); + walletType = + await Navigator.pushNamed(context, Routes.restoreWalletTypeFromQR) as WalletType?; + if (walletType == null) throw Exception("Failed to determine wallet type."); - final seed = - getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType); - if (seed != null) { - credentials['seed'] = seed; + final seedPhrase = _extractSeedPhraseFromUrl(code, walletType); + + formattedUri = seedPhrase != null + ? '$walletType:?seed=$seedPhrase' + : throw Exception('Failed to determine valid seed phrase'); } else { - credentials['private_key'] = queryParameters['private_key']; + walletType = _extractWalletType(code); + final index = code.indexOf(':'); + final query = code.substring(index + 1).replaceAll('?', '&'); + formattedUri = '$walletType:?$query'; } - credentials.addAll(queryParameters); - credentials['mode'] = getWalletRestoreMode(credentials); + final uri = Uri.parse(formattedUri); + Map queryParameters = {...uri.queryParameters}; + + if (queryParameters['seed'] == null) { + queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!); + } + if (queryParameters['address'] == null) { + queryParameters['address'] = _extractAddressFromUrl(code, walletType!); + } + + Map credentials = {'type': walletType, ...queryParameters}; + + credentials['mode'] = _determineWalletRestoreMode(credentials); switch (credentials['mode']) { case WalletRestoreMode.txids: @@ -52,106 +114,21 @@ class WalletRestoreFromQRCode { } } - static String getFormattedUri(String code) { - final index = code.indexOf(':'); - if (index == -1) return throw Exception('Unexpected wallet type: $code, try to scan again'); - final scheme = code.substring(0, index).replaceAll('_', '-'); - final query = code.substring(index + 1).replaceAll('?', '&'); - final formattedUri = '$scheme:?$query'; - return formattedUri; - } - - static WalletType getWalletTypeFromUrl(String scheme) { - switch (scheme) { - case 'monero': - case 'monero-wallet': - return WalletType.monero; - case 'bitcoin': - case 'bitcoin-wallet': - return WalletType.bitcoin; - case 'litecoin': - case 'litecoin-wallet': - return WalletType.litecoin; - case 'bitcoincash': - case 'bitcoincash-wallet': - return WalletType.bitcoinCash; - case 'ethereum': - case 'ethereum-wallet': - return WalletType.ethereum; - case 'nano': - case 'nano-wallet': - return WalletType.nano; - default: - throw Exception('Unexpected wallet type: ${scheme.toString()}'); - } - } - - static String? getAddressFromUrl({required WalletType type, required String rawString}) { - return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); - } - - static String? getSeedPhraseFromUrl(String rawString, WalletType walletType) { - switch (walletType) { - case WalletType.monero: - RegExp regex25 = RegExp(r'\b(\S+\b\s+){24}\S+\b'); - RegExp regex14 = RegExp(r'\b(\S+\b\s+){13}\S+\b'); - RegExp regex13 = RegExp(r'\b(\S+\b\s+){12}\S+\b'); - - if (regex25.firstMatch(rawString) == null) { - if (regex14.firstMatch(rawString) == null) { - if (regex13.firstMatch(rawString) == null) { - return null; - } else { - return regex13.firstMatch(rawString)!.group(0)!; - } - } else { - return regex14.firstMatch(rawString)!.group(0)!; - } - } else { - return regex25.firstMatch(rawString)!.group(0)!; - } - case WalletType.bitcoin: - case WalletType.litecoin: - case WalletType.ethereum: - case WalletType.bitcoinCash: - RegExp regex24 = RegExp(r'\b(\S+\b\s+){23}\S+\b'); - RegExp regex18 = RegExp(r'\b(\S+\b\s+){17}\S+\b'); - RegExp regex12 = RegExp(r'\b(\S+\b\s+){11}\S+\b'); - - if (regex24.firstMatch(rawString) == null) { - if (regex18.firstMatch(rawString) == null) { - if (regex12.firstMatch(rawString) == null) { - return null; - } else { - return regex12.firstMatch(rawString)!.group(0)!; - } - } else { - return regex18.firstMatch(rawString)!.group(0)!; - } - } else { - return regex24.firstMatch(rawString)!.group(0)!; - } - default: - return null; - } - } - - static WalletRestoreMode getWalletRestoreMode(Map credentials) { + static WalletRestoreMode _determineWalletRestoreMode(Map credentials) { final type = credentials['type'] as WalletType; if (credentials.containsKey('tx_payment_id')) { final txIdValue = credentials['tx_payment_id'] as String? ?? ''; - return txIdValue.isNotEmpty - ? WalletRestoreMode.txids - : throw Exception('Unexpected restore mode: tx_payment_id is invalid'); + if (txIdValue.isNotEmpty) return WalletRestoreMode.txids; + throw Exception('Unexpected restore mode: tx_payment_id is invalid'); } - if (credentials.containsKey('seed')) { - final seedValue = credentials['seed'] as String; + if (credentials['seed'] != null) { + final seedValue = credentials['seed']; final words = SeedValidator.getWordList(type: type, language: 'english'); seedValue.split(' ').forEach((element) { if (!words.contains(element)) { - throw Exception('Unexpected restore mode: mnemonic_seed is invalid'); + throw Exception( + 'Unexpected restore mode: mnemonic_seed is invalid or does\'t match wallet type'); } }); return WalletRestoreMode.seed; @@ -177,3 +154,15 @@ class WalletRestoreFromQRCode { throw Exception('Unexpected restore mode: restore params are invalid'); } } + +Future _specifyWalletAssets(BuildContext context, String error) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); +}