mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-11-17 01:37:40 +00:00
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
This commit is contained in:
parent
6c178593a9
commit
db7f025b71
4 changed files with 113 additions and 114 deletions
|
@ -189,6 +189,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
param2: false));
|
||||
}
|
||||
|
||||
case Routes.restoreWalletTypeFromQR:
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<NewWalletTypePage>(
|
||||
param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type)));
|
||||
|
||||
case Routes.seed:
|
||||
return MaterialPageRoute<void>(
|
||||
fullscreenDialog: true,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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()}');
|
||||
}
|
||||
|
|
|
@ -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<String, WalletType> _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<int> 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<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
|
||||
String code = await presentQRScanner();
|
||||
Map<String, dynamic> 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<String, dynamic> queryParameters = {...uri.queryParameters};
|
||||
|
||||
if (queryParameters['seed'] == null) {
|
||||
queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!);
|
||||
}
|
||||
if (queryParameters['address'] == null) {
|
||||
queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> credentials) {
|
||||
static WalletRestoreMode _determineWalletRestoreMode(Map<String, dynamic> 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<void> _specifyWalletAssets(BuildContext context, String error) async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.current.error,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue