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
Serhii committed
Oct 25, 2023 at 03:19 UTC
db7f025b71643776f933d6a6816783d8f4ddfc78
4 files changed
+113
-114
lib/router.dart
+5
@@ -189,6 +189,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
189
param2: false));
190
}
191
192
+ case Routes.restoreWalletTypeFromQR:
193
+ return CupertinoPageRoute<void>(
194
+ builder: (_) => getIt.get<NewWalletTypePage>(
195
+ param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type)));
196
+
197
case Routes.seed:
198
return MaterialPageRoute<void>(
199
fullscreenDialog: true,
lib/routes.dart
+1
@@ -6,6 +6,7 @@ class Routes {
6
static const seed = '/seed';
7
static const restoreOptions = '/restore_options';
8
static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys';
9
+ static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code';
10
static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation';
11
static const dashboard = '/dashboard';
12
static const send = '/send';
lib/view_model/restore/restore_from_qr_vm.dart
+4
@@ -1,6 +1,7 @@
1
import 'package:cake_wallet/bitcoin/bitcoin.dart';
2
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
3
import 'package:cake_wallet/ethereum/ethereum.dart';
4
+import 'package:cake_wallet/nano/nano.dart';
5
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
6
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
7
import 'package:hive/hive.dart';
@@ -91,6 +92,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
92
case WalletType.ethereum:
93
return ethereum!.createEthereumRestoreWalletFromSeedCredentials(
94
name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
95
+ case WalletType.nano:
96
+ return nano!.createNanoRestoreWalletFromSeedCredentials(
97
+ name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
98
default:
99
throw Exception('Unexpected type: ${type.toString()}');
100
}
lib/view_model/restore/wallet_restore_from_qr_code.dart
+103
-114
@@ -1,44 +1,106 @@
1
import 'package:cake_wallet/core/seed_validator.dart';
2
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
3
import 'package:cake_wallet/entities/qr_scanner.dart';
4
+import 'package:cake_wallet/routes.dart';
5
+import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6
+import 'package:cake_wallet/utils/show_pop_up.dart';
7
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
8
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
9
import 'package:cw_core/wallet_type.dart';
10
import 'package:flutter/cupertino.dart';
11
+import 'package:cake_wallet/generated/i18n.dart';
12
+import 'package:collection/collection.dart';
13
14
class WalletRestoreFromQRCode {
15
WalletRestoreFromQRCode();
16
17
+ static const Map<String, WalletType> _walletTypeMap = {
18
+ 'monero': WalletType.monero,
19
+ 'monero-wallet': WalletType.monero,
20
+ 'monero_wallet': WalletType.monero,
21
+ 'bitcoin': WalletType.bitcoin,
22
+ 'bitcoin-wallet': WalletType.bitcoin,
23
+ 'bitcoin_wallet': WalletType.bitcoin,
24
+ 'litecoin': WalletType.litecoin,
25
+ 'litecoin-wallet': WalletType.litecoin,
26
+ 'litecoin_wallet': WalletType.litecoin,
27
+ 'ethereum-wallet': WalletType.ethereum,
28
+ 'nano-wallet': WalletType.nano,
29
+ 'nano_wallet': WalletType.nano,
30
+ 'bitcoincash': WalletType.bitcoinCash,
31
+ 'bitcoincash-wallet': WalletType.bitcoinCash,
32
+ 'bitcoincash_wallet': WalletType.bitcoinCash,
33
+ };
34
+
35
+ static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null;
36
+
37
+ static WalletType? _extractWalletType(String code) {
38
+ final sortedKeys = _walletTypeMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length));
39
+
40
+ final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
41
+
42
+ return _walletTypeMap[extracted];
43
+ }
44
+
45
+ static String? _extractAddressFromUrl(String rawString, WalletType type) {
46
+ return AddressResolver.extractAddressByType(
47
+ raw: rawString, type: walletTypeToCryptoCurrency(type));
48
+ }
49
+
50
+ static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) {
51
+ RegExp _getPattern(int wordCount) =>
52
+ RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)');
53
+
54
+ List<int> patternCounts = walletType == WalletType.monero ? [25, 14, 13] : [24, 18, 12];
55
+
56
+ for (final count in patternCounts) {
57
+ final pattern = _getPattern(count);
58
+ final match = pattern.firstMatch(rawString);
59
+ if (match != null) {
60
+ return match.group(1)?.trim();
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
67
String code = await presentQRScanner();
14
- Map<String, dynamic> credentials = {};
68
+ if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty');
69
+
70
+ WalletType? walletType;
71
+ String formattedUri = '';
72
+
73
+ if (!_containsAssetSpecifier(code)) {
74
+ await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually");
75
+ walletType =
76
+ await Navigator.pushNamed(context, Routes.restoreWalletTypeFromQR) as WalletType?;
77
+ if (walletType == null) throw Exception("Failed to determine wallet type.");
78
+
79
+ final seedPhrase = _extractSeedPhraseFromUrl(code, walletType);
80
16
- if (code.isEmpty) {
17
- throw Exception('Unexpected scan QR code value: value is empty');
81
+ formattedUri = seedPhrase != null
82
+ ? '$walletType:?seed=$seedPhrase'
83
+ : throw Exception('Failed to determine valid seed phrase');
84
+ } else {
85
+ walletType = _extractWalletType(code);
86
+ final index = code.indexOf(':');
87
+ final query = code.substring(index + 1).replaceAll('?', '&');
88
+ formattedUri = '$walletType:?$query';
89
}
19
- final formattedUri = getFormattedUri(code);
90
+
91
final uri = Uri.parse(formattedUri);
21
- final queryParameters = uri.queryParameters;
22
- credentials['type'] = getWalletTypeFromUrl(uri.scheme);
23
-
24
- final address = getAddressFromUrl(
25
- type: credentials['type'] as WalletType,
26
- rawString: queryParameters.toString(),
27
- );
28
- if (address != null) {
29
- credentials['address'] = address;
30
- }
92
+ Map<String, dynamic> queryParameters = {...uri.queryParameters};
93
32
- final seed =
33
- getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType);
34
- if (seed != null) {
35
- credentials['seed'] = seed;
36
- } else {
37
- credentials['private_key'] = queryParameters['private_key'];
94
+ if (queryParameters['seed'] == null) {
95
+ queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!);
96
+ }
97
+ if (queryParameters['address'] == null) {
98
+ queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
99
}
100
40
- credentials.addAll(queryParameters);
41
- credentials['mode'] = getWalletRestoreMode(credentials);
101
+ Map<String, dynamic> credentials = {'type': walletType, ...queryParameters};
102
+
103
+ credentials['mode'] = _determineWalletRestoreMode(credentials);
104
105
switch (credentials['mode']) {
106
case WalletRestoreMode.txids:
@@ -52,106 +114,21 @@ class WalletRestoreFromQRCode {
114
}
115
}
116
55
- static String getFormattedUri(String code) {
56
- final index = code.indexOf(':');
57
- if (index == -1) return throw Exception('Unexpected wallet type: $code, try to scan again');
58
- final scheme = code.substring(0, index).replaceAll('_', '-');
59
- final query = code.substring(index + 1).replaceAll('?', '&');
60
- final formattedUri = '$scheme:?$query';
61
- return formattedUri;
62
- }
63
-
64
- static WalletType getWalletTypeFromUrl(String scheme) {
65
- switch (scheme) {
66
- case 'monero':
67
- case 'monero-wallet':
68
- return WalletType.monero;
69
- case 'bitcoin':
70
- case 'bitcoin-wallet':
71
- return WalletType.bitcoin;
72
- case 'litecoin':
73
- case 'litecoin-wallet':
74
- return WalletType.litecoin;
75
- case 'bitcoincash':
76
- case 'bitcoincash-wallet':
77
- return WalletType.bitcoinCash;
78
- case 'ethereum':
79
- case 'ethereum-wallet':
80
- return WalletType.ethereum;
81
- case 'nano':
82
- case 'nano-wallet':
83
- return WalletType.nano;
84
- default:
85
- throw Exception('Unexpected wallet type: ${scheme.toString()}');
86
- }
87
- }
88
-
89
- static String? getAddressFromUrl({required WalletType type, required String rawString}) {
90
- return AddressResolver.extractAddressByType(
91
- raw: rawString, type: walletTypeToCryptoCurrency(type));
92
- }
93
-
94
- static String? getSeedPhraseFromUrl(String rawString, WalletType walletType) {
95
- switch (walletType) {
96
- case WalletType.monero:
97
- RegExp regex25 = RegExp(r'\b(\S+\b\s+){24}\S+\b');
98
- RegExp regex14 = RegExp(r'\b(\S+\b\s+){13}\S+\b');
99
- RegExp regex13 = RegExp(r'\b(\S+\b\s+){12}\S+\b');
100
-
101
- if (regex25.firstMatch(rawString) == null) {
102
- if (regex14.firstMatch(rawString) == null) {
103
- if (regex13.firstMatch(rawString) == null) {
104
- return null;
105
- } else {
106
- return regex13.firstMatch(rawString)!.group(0)!;
107
- }
108
- } else {
109
- return regex14.firstMatch(rawString)!.group(0)!;
110
- }
111
- } else {
112
- return regex25.firstMatch(rawString)!.group(0)!;
113
- }
114
- case WalletType.bitcoin:
115
- case WalletType.litecoin:
116
- case WalletType.ethereum:
117
- case WalletType.bitcoinCash:
118
- RegExp regex24 = RegExp(r'\b(\S+\b\s+){23}\S+\b');
119
- RegExp regex18 = RegExp(r'\b(\S+\b\s+){17}\S+\b');
120
- RegExp regex12 = RegExp(r'\b(\S+\b\s+){11}\S+\b');
121
-
122
- if (regex24.firstMatch(rawString) == null) {
123
- if (regex18.firstMatch(rawString) == null) {
124
- if (regex12.firstMatch(rawString) == null) {
125
- return null;
126
- } else {
127
- return regex12.firstMatch(rawString)!.group(0)!;
128
- }
129
- } else {
130
- return regex18.firstMatch(rawString)!.group(0)!;
131
- }
132
- } else {
133
- return regex24.firstMatch(rawString)!.group(0)!;
134
- }
135
- default:
136
- return null;
137
- }
138
- }
139
-
140
- static WalletRestoreMode getWalletRestoreMode(Map<String, dynamic> credentials) {
117
+ static WalletRestoreMode _determineWalletRestoreMode(Map<String, dynamic> credentials) {
118
final type = credentials['type'] as WalletType;
119
if (credentials.containsKey('tx_payment_id')) {
120
final txIdValue = credentials['tx_payment_id'] as String? ?? '';
144
- return txIdValue.isNotEmpty
145
- ? WalletRestoreMode.txids
146
- : throw Exception('Unexpected restore mode: tx_payment_id is invalid');
121
+ if (txIdValue.isNotEmpty) return WalletRestoreMode.txids;
122
+ throw Exception('Unexpected restore mode: tx_payment_id is invalid');
123
}
124
149
- if (credentials.containsKey('seed')) {
150
- final seedValue = credentials['seed'] as String;
125
+ if (credentials['seed'] != null) {
126
+ final seedValue = credentials['seed'];
127
final words = SeedValidator.getWordList(type: type, language: 'english');
128
seedValue.split(' ').forEach((element) {
129
if (!words.contains(element)) {
154
- throw Exception('Unexpected restore mode: mnemonic_seed is invalid');
130
+ throw Exception(
131
+ 'Unexpected restore mode: mnemonic_seed is invalid or does\'t match wallet type');
132
}
133
});
134
return WalletRestoreMode.seed;
@@ -177,3 +154,15 @@ class WalletRestoreFromQRCode {
154
throw Exception('Unexpected restore mode: restore params are invalid');
155
}
156
}
157
+
158
+Future<void> _specifyWalletAssets(BuildContext context, String error) async {
159
+ await showPopUp<void>(
160
+ context: context,
161
+ builder: (BuildContext context) {
162
+ return AlertWithOneAction(
163
+ alertTitle: S.current.error,
164
+ alertContent: error,
165
+ buttonText: S.of(context).ok,
166
+ buttonAction: () => Navigator.of(context).pop());
167
+ });
168
+}