Skip to content

Commit

Permalink
CW-472-QR-code-restore-If-a-user-scans-a-wallet-seed-that-does-NOT-in…
Browse files Browse the repository at this point in the history
…clude (#1081)

* add restor from qr option

* minor fixes

* merge OR fixes

* add restoring nano from QR seed mode
  • Loading branch information
Serhii-Borodenko authored Oct 25, 2023
1 parent 6c17859 commit db7f025
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 114 deletions.
5 changes: 5 additions & 0 deletions lib/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 4 additions & 0 deletions lib/view_model/restore/restore_from_qr_vm.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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()}');
}
Expand Down
217 changes: 103 additions & 114 deletions lib/view_model/restore/wallet_restore_from_qr_code.dart
Original file line number Diff line number Diff line change
@@ -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');

WalletType? walletType;
String formattedUri = '';

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 seedPhrase = _extractSeedPhraseFromUrl(code, walletType);

if (code.isEmpty) {
throw Exception('Unexpected scan QR code value: value is empty');
formattedUri = seedPhrase != null
? '$walletType:?seed=$seedPhrase'
: throw Exception('Failed to determine valid seed phrase');
} else {
walletType = _extractWalletType(code);
final index = code.indexOf(':');
final query = code.substring(index + 1).replaceAll('?', '&');
formattedUri = '$walletType:?$query';
}
final formattedUri = getFormattedUri(code);

final uri = Uri.parse(formattedUri);
final queryParameters = uri.queryParameters;
credentials['type'] = getWalletTypeFromUrl(uri.scheme);

final address = getAddressFromUrl(
type: credentials['type'] as WalletType,
rawString: queryParameters.toString(),
);
if (address != null) {
credentials['address'] = address;
}
Map<String, dynamic> queryParameters = {...uri.queryParameters};

final seed =
getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType);
if (seed != null) {
credentials['seed'] = seed;
} else {
credentials['private_key'] = queryParameters['private_key'];
if (queryParameters['seed'] == null) {
queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!);
}
if (queryParameters['address'] == null) {
queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
}

credentials.addAll(queryParameters);
credentials['mode'] = getWalletRestoreMode(credentials);
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters};

credentials['mode'] = _determineWalletRestoreMode(credentials);

switch (credentials['mode']) {
case WalletRestoreMode.txids:
Expand All @@ -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;
Expand All @@ -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());
});
}

0 comments on commit db7f025

Please sign in to comment.