CW-229 Improved restore options from QR code (#793)

* add restoring wallet from qr * add restore mode * add alert for exceptions * add restore from seed * add check for create wallet state * convert sweeping page into stateful * fix parsing url * restoration flow update * update restoring from key mode * update config * fix restor of BTC and LTC wallets * fix pin code issue * wallet Seed/keys uri or code fix * fix key restore credentials * update the restore workflow * update from main * PR coments fixes * update * update * PR fixes

Serhii committed Apr 21, 2023 at 21:36 UTC 1eb8d0c698e9a61a8670be876ece665b3bd3a95f
38 files changed +698 -51
lib/di.dart
+12 -1
@@ -53,6 +53,8 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
53 import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart';
54 import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
55 import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
56 +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
57 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
58 import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart';
59 import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
60 import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
@@ -320,6 +322,13 @@ Future setup(
322 type: type, language: language);
323 });
324
325 + getIt
326 + .registerFactoryParam<WalletRestorationFromQRVM, WalletType, void>((WalletType type, _) {
327 + return WalletRestorationFromQRVM(getIt.get<AppStore>(),
328 + getIt.get<WalletCreationService>(param1: type),
329 + _walletInfoSource, type);
330 + });
331 +
332 getIt.registerFactory<WalletAddressListViewModel>(() =>
333 WalletAddressListViewModel(
334 appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>(),
@@ -743,7 +752,9 @@ Future setup(
752 getIt.registerFactory(
753 () => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>()));
754
746 - getIt.registerFactory(() => RestoreOptionsPage());
755 + getIt.registerFactoryParam<RestoreOptionsPage, bool, void>((bool isNewInstall, _) =>
756 + RestoreOptionsPage(isNewInstall: isNewInstall));
757 +
758
759 getIt.registerFactory(
760 () => RestoreFromBackupViewModel(getIt.get<BackupService>()));
lib/entities/parse_address_from_domain.dart
+1 -1
@@ -33,7 +33,7 @@ class AddressResolver {
33 final addressPattern = AddressValidator.getAddressFromStringPattern(type);
34
35 if (addressPattern == null) {
36 - throw 'Unexpected token: $type for getAddressFromStringPattern';
36 + throw Exception('Unexpected token: $type for getAddressFromStringPattern');
37 }
38
39 final match = RegExp(addressPattern).firstMatch(raw);
lib/router.dart
+17 -3
@@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
10 import 'package:cake_wallet/src/screens/buy/onramper_page.dart';
11 import 'package:cake_wallet/src/screens/buy/payfura_page.dart';
12 import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
13 +import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
14 import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
15 import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
16 import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart';
@@ -40,6 +41,8 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
41 import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
42 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
43 import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
44 +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
45 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
46 import 'package:flutter/cupertino.dart';
47 import 'package:flutter/material.dart';
48 import 'package:cake_wallet/routes.dart';
@@ -158,8 +161,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
161 param2: false));
162
163 case Routes.restoreOptions:
164 + final isNewInstall = settings.arguments as bool;
165 return CupertinoPageRoute<void>(
162 - builder: (_) => getIt.get<RestoreOptionsPage>());
166 + builder: (_) => getIt.get<RestoreOptionsPage>(param1: isNewInstall));
167
168 case Routes.restoreWalletOptions:
169 final type = WalletType.monero; //settings.arguments as WalletType;
@@ -189,12 +193,18 @@ Route<dynamic> createRoute(RouteSettings settings) {
193 }));
194
195 case Routes.restoreWalletOptionsFromWelcome:
192 - return CupertinoPageRoute<void>(
196 + final isNewInstall = settings.arguments as bool;
197 + return isNewInstall ? CupertinoPageRoute<void>(
198 builder: (_) => getIt.get<SetupPinCodePage>(
199 param1: (PinCodeState<PinCodeWidget> context, dynamic _) =>
200 Navigator.pushNamed(
201 context.context, Routes.restoreWalletType)),
197 - fullscreenDialog: true);
202 + fullscreenDialog: true) : CupertinoPageRoute<void>(
203 + builder: (_) => getIt.get<NewWalletTypePage>(
204 + param1: (BuildContext context, WalletType type) =>
205 + Navigator.of(context)
206 + .pushNamed(Routes.restoreWallet, arguments: type),
207 + param2: false));
208
209 case Routes.seed:
210 return MaterialPageRoute<void>(
@@ -224,6 +234,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
234 builder: (_) => RestoreWalletFromKeysPage(
235 walletRestorationFromKeysVM: walletRestorationFromKeysVM));
236
237 + case Routes.sweepingWalletPage:
238 + return CupertinoPageRoute<void>(
239 + builder: (_) => getIt.get<SweepingWalletPage>());
240 +
241 case Routes.dashboard:
242 return CupertinoPageRoute<void>(
243 builder: (_) => getIt.get<DashboardPage>());
lib/routes.dart
+1
@@ -84,6 +84,7 @@ class Routes {
84 static const displaySettingsPage = '/display_settings_page';
85 static const otherSettingsPage = '/other_settings_page';
86 static const advancedPrivacySettings = '/advanced_privacy_settings';
87 + static const sweepingWalletPage = '/sweeping_wallet_page';
88 static const anonPayInvoicePage = '/anon_pay_invoice_page';
89 static const anonPayReceivePage = '/anon_pay_receive_page';
90 static const anonPayDetailsPage = '/anon_pay_details_page';
lib/src/screens/restore/restore_options_page.dart
+66 -12
@@ -1,3 +1,11 @@
1 +import 'package:cake_wallet/core/execution_state.dart';
2 +import 'package:cake_wallet/di.dart';
3 +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
4 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
5 +import 'package:cake_wallet/utils/language_list.dart';
6 +import 'package:cake_wallet/utils/show_pop_up.dart';
7 +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
8 +import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart';
9 import 'package:cake_wallet/utils/responsive_layout_util.dart';
10 import 'package:flutter/material.dart';
11 import 'package:cake_wallet/routes.dart';
@@ -7,15 +15,16 @@ import 'package:cake_wallet/src/screens/base_page.dart';
15 import 'package:cake_wallet/generated/i18n.dart';
16
17 class RestoreOptionsPage extends BasePage {
10 - RestoreOptionsPage();
11 -
12 - static const _aspectRatioImage = 2.086;
18 + RestoreOptionsPage({required this.isNewInstall});
19
20 @override
21 String get title => S.current.restore_restore_wallet;
22
23 +
24 + final bool isNewInstall;
25 final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png');
26 final imageBackup = Image.asset('assets/images/backup.png');
27 + final qrCode = Image.asset('assets/images/qr_code_icon.png');
28
29 @override
30 Widget body(BuildContext context) {
@@ -28,24 +37,69 @@ class RestoreOptionsPage extends BasePage {
37 child: Column(
38 children: <Widget>[
39 RestoreButton(
31 - onPressed: () =>
32 - Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome),
40 + onPressed: () => Navigator.pushNamed(
41 + context, Routes.restoreWalletOptionsFromWelcome,
42 + arguments: isNewInstall),
43 image: imageSeedKeys,
44 title: S.of(context).restore_title_from_seed_keys,
35 - description:
36 - S.of(context).restore_description_from_seed_keys),
45 + description: S.of(context).restore_description_from_seed_keys),
46 + if (isNewInstall)
47 + Padding(
48 + padding: EdgeInsets.only(top: 24),
49 + child: RestoreButton(
50 + onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
51 + image: imageBackup,
52 + title: S.of(context).restore_title_from_backup,
53 + description: S.of(context).restore_description_from_backup),
54 + ),
55 Padding(
56 padding: EdgeInsets.only(top: 24),
57 child: RestoreButton(
40 - onPressed: () =>
41 - Navigator.pushNamed(context, Routes.restoreFromBackup),
42 - image: imageBackup,
43 - title: S.of(context).restore_title_from_backup,
44 - description: S.of(context).restore_description_from_backup),
58 + onPressed: () async {
59 + bool isPinSet = false;
60 + if (isNewInstall) {
61 + await Navigator.pushNamed(context, Routes.setupPin,
62 + arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
63 + setupPinContext.close();
64 + isPinSet = true;
65 + });
66 + }
67 + if (!isNewInstall || isPinSet) {
68 + try {
69 + final restoreWallet =
70 + await WalletRestoreFromQRCode.scanQRCodeForRestoring(context);
71 +
72 + final restoreFromQRViewModel = getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type);
73 +
74 + await restoreFromQRViewModel.create(restoreWallet: restoreWallet);
75 + if (restoreFromQRViewModel.state is FailureState) {
76 + _onWalletCreateFailure(context,
77 + 'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}');
78 + }
79 + } catch (e) {
80 + _onWalletCreateFailure(context, e.toString());
81 + }
82 + }
83 + },
84 + image: qrCode,
85 + title: S.of(context).scan_qr_code,
86 + description: S.of(context).cold_or_recover_wallet),
87 )
88 ],
89 ),
90 )),
91 );
92 }
93 +
94 + void _onWalletCreateFailure(BuildContext context, String error) {
95 + showPopUp<void>(
96 + context: context,
97 + builder: (BuildContext context) {
98 + return AlertWithOneAction(
99 + alertTitle: S.current.error,
100 + alertContent: error,
101 + buttonText: S.of(context).ok,
102 + buttonAction: () => Navigator.of(context).pop());
103 + });
104 + }
105 }
lib/src/screens/restore/sweeping_wallet_page.dart new
+123
@@ -0,0 +1,123 @@
1 +import 'package:cake_wallet/themes/theme_base.dart';
2 +import 'package:flutter/material.dart';
3 +import 'package:cake_wallet/src/screens/base_page.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 +import 'package:flutter/scheduler.dart';
6 +
7 +class SweepingWalletPage extends BasePage {
8 + SweepingWalletPage();
9 +
10 + static const aspectRatioImage = 1.25;
11 + final welcomeImageLight = Image.asset('assets/images/welcome_light.png');
12 + final welcomeImageDark = Image.asset('assets/images/welcome.png');
13 +
14 +
15 +
16 + @override
17 + Widget build(BuildContext context) {
18 + return Scaffold(
19 + backgroundColor: Theme.of(context).backgroundColor,
20 + resizeToAvoidBottomInset: false,
21 + body: body(context));
22 + }
23 +
24 + @override
25 + Widget body(BuildContext context) {
26 + final welcomeImage = currentTheme.type == ThemeType.dark ? welcomeImageDark : welcomeImageLight;
27 +
28 + return SweepingWalletWidget(
29 + aspectRatioImage: aspectRatioImage,
30 + welcomeImage: welcomeImage,
31 + );
32 + }
33 +}
34 +
35 +class SweepingWalletWidget extends StatefulWidget {
36 + const SweepingWalletWidget({
37 + required this.aspectRatioImage,
38 + required this.welcomeImage,
39 + });
40 +
41 + final double aspectRatioImage;
42 + final Image welcomeImage;
43 +
44 + @override
45 + State<SweepingWalletWidget> createState() => _SweepingWalletWidgetState();
46 +}
47 +
48 +class _SweepingWalletWidgetState extends State<SweepingWalletWidget> {
49 + @override
50 + void initState() {
51 + SchedulerBinding.instance.addPostFrameCallback((_) async {
52 +
53 + });
54 + super.initState();
55 + }
56 +
57 + @override
58 + Widget build(BuildContext context) {
59 + return WillPopScope(
60 + onWillPop: () async => false,
61 + child: Container(
62 + padding: EdgeInsets.only(top: 64, bottom: 24, left: 24, right: 24),
63 + child: Column(
64 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
65 + children: <Widget>[
66 + Flexible(
67 + flex: 2,
68 + child: AspectRatio(
69 + aspectRatio: widget.aspectRatioImage,
70 + child: FittedBox(child: widget.welcomeImage, fit: BoxFit.fill))),
71 + Flexible(
72 + flex: 3,
73 + child: Column(
74 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
75 + children: <Widget>[
76 + Column(
77 + children: <Widget>[
78 + Padding(
79 + padding: EdgeInsets.only(top: 24),
80 + child: Text(
81 + S.of(context).please_wait,
82 + style: TextStyle(
83 + fontSize: 18,
84 + fontWeight: FontWeight.w500,
85 + color: Theme.of(context).accentTextTheme!.headline2!.color!,
86 + ),
87 + textAlign: TextAlign.center,
88 + ),
89 + ),
90 + Padding(
91 + padding: EdgeInsets.only(top: 5),
92 + child: Text(
93 + S.of(context).sweeping_wallet,
94 + style: TextStyle(
95 + fontSize: 36,
96 + fontWeight: FontWeight.bold,
97 + color: Theme.of(context).primaryTextTheme!.headline6!.color!,
98 + ),
99 + textAlign: TextAlign.center,
100 + ),
101 + ),
102 + Padding(
103 + padding: EdgeInsets.only(top: 5),
104 + child: Text(
105 + S.of(context).sweeping_wallet_alert,
106 + style: TextStyle(
107 + fontSize: 16,
108 + fontWeight: FontWeight.w500,
109 + color: Theme.of(context).accentTextTheme!.headline2!.color!,
110 + ),
111 + textAlign: TextAlign.center,
112 + ),
113 + ),
114 + ],
115 + ),
116 + ],
117 + ))
118 + ],
119 + )));
120 + }
121 +}
122 +
123 +
lib/src/screens/restore/wallet_restore_page.dart
+1
@@ -21,6 +21,7 @@ import 'package:cake_wallet/core/validator.dart';
21 import 'package:cake_wallet/generated/i18n.dart';
22 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
23 import 'package:cake_wallet/core/seed_validator.dart';
24 +import 'package:cake_wallet/view_model/restore/restore_mode.dart';
25
26 class WalletRestorePage extends BasePage {
27 WalletRestorePage(this.walletRestoreViewModel)
lib/src/screens/wallet_list/wallet_list_page.dart
+1 -1
@@ -178,7 +178,7 @@ class WalletListBodyState extends State<WalletListBody> {
178 Navigator.of(context).pushNamed(Routes.restoreWallet,
179 arguments: widget.walletListViewModel.currentWalletType);
180 } else {
181 - Navigator.of(context).pushNamed(Routes.restoreWalletType);
181 + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
182 }
183 },
184 image: restoreWalletImage,
lib/src/screens/welcome/welcome_page.dart
+2 -1
@@ -148,7 +148,8 @@ class WelcomePage extends BasePage {
148 padding: EdgeInsets.only(top: 10),
149 child: PrimaryImageButton(
150 onPressed: () {
151 - Navigator.pushNamed(context, Routes.restoreOptions);
151 + Navigator.pushNamed(context, Routes.restoreOptions,
152 + arguments: true);
153 },
154 image: restoreWalletImage,
155 text: S.of(context).restore_wallet,
lib/view_model/restore/restore_from_qr_vm.dart new
+106
@@ -0,0 +1,106 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 +import 'package:cake_wallet/view_model/restore/restore_mode.dart';
3 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
4 +import 'package:hive/hive.dart';
5 +import 'package:mobx/mobx.dart';
6 +import 'package:cake_wallet/monero/monero.dart';
7 +import 'package:cake_wallet/store/app_store.dart';
8 +import 'package:cw_core/wallet_base.dart';
9 +import 'package:cake_wallet/core/generate_wallet_password.dart';
10 +import 'package:cake_wallet/core/wallet_creation_service.dart';
11 +import 'package:cw_core/wallet_credentials.dart';
12 +import 'package:cw_core/wallet_type.dart';
13 +import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
14 +import 'package:cw_core/wallet_info.dart';
15 +
16 +part 'restore_from_qr_vm.g.dart';
17 +
18 +class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRestorationFromQRVM;
19 +
20 +abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store {
21 + WalletRestorationFromQRVMBase(AppStore appStore, WalletCreationService walletCreationService,
22 + Box<WalletInfo> walletInfoSource, WalletType type)
23 + : height = 0,
24 + viewKey = '',
25 + spendKey = '',
26 + wif = '',
27 + address = '',
28 + super(appStore, walletInfoSource, walletCreationService,
29 + type: type, isRecovery: true);
30 +
31 + @observable
32 + int height;
33 +
34 + @observable
35 + String viewKey;
36 +
37 + @observable
38 + String spendKey;
39 +
40 + @observable
41 + String wif;
42 +
43 + @observable
44 + String address;
45 +
46 + bool get hasRestorationHeight => type == WalletType.monero;
47 +
48 + @override
49 + WalletCredentials getCredentialsFromRestoredWallet(dynamic options, RestoredWallet restoreWallet) {
50 + final password = generateWalletPassword();
51 +
52 + switch (restoreWallet.restoreMode) {
53 + case WalletRestoreMode.keys:
54 + switch (restoreWallet.type) {
55 + case WalletType.monero:
56 + return monero!.createMoneroRestoreWalletFromKeysCredentials(
57 + name: name,
58 + password: password,
59 + language: 'English',
60 + address: restoreWallet.address ?? '',
61 + viewKey: restoreWallet.viewKey ?? '',
62 + spendKey: restoreWallet.spendKey ?? '',
63 + height: restoreWallet.height ?? 0);
64 + case WalletType.bitcoin:
65 + case WalletType.litecoin:
66 + return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials(
67 + name: name, password: password, wif: wif);
68 + default:
69 + throw Exception('Unexpected type: ${restoreWallet.type.toString()}');
70 + }
71 + case WalletRestoreMode.seed:
72 + switch (restoreWallet.type) {
73 + case WalletType.monero:
74 + return monero!.createMoneroRestoreWalletFromSeedCredentials(
75 + name: name,
76 + height: restoreWallet.height ?? 0,
77 + mnemonic: restoreWallet.mnemonicSeed ?? '',
78 + password: password);
79 + case WalletType.bitcoin:
80 + case WalletType.litecoin:
81 + return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials(
82 + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
83 + default:
84 + throw Exception('Unexpected type: ${type.toString()}');
85 + }
86 + default:
87 + throw Exception('Unexpected type: ${type.toString()}');
88 + }
89 + }
90 +
91 + @override
92 + Future<WalletBase> processFromRestoredWallet(WalletCredentials credentials, RestoredWallet restoreWallet) async {
93 + try {
94 + switch (restoreWallet.restoreMode) {
95 + case WalletRestoreMode.keys:
96 + return walletCreationService.restoreFromKeys(credentials);
97 + case WalletRestoreMode.seed:
98 + return walletCreationService.restoreFromSeed(credentials);
99 + default:
100 + throw Exception('Unexpected restore mode: ${restoreWallet.restoreMode.toString()}');
101 + }
102 + } catch (e) {
103 + throw Exception('Unexpected restore mode: ${e.toString()}');
104 + }
105 + }
106 +}
lib/view_model/restore/restore_mode.dart new
+1
@@ -0,0 +1 @@
1 +enum WalletRestoreMode { seed, keys, txids }
\ No newline at end of file
lib/view_model/restore/restore_wallet.dart new
+66
@@ -0,0 +1,66 @@
1 +import 'package:cake_wallet/view_model/restore/restore_mode.dart';
2 +import 'package:cw_core/wallet_type.dart';
3 +
4 +class RestoredWallet {
5 + RestoredWallet(
6 + {required this.restoreMode,
7 + required this.type,
8 + required this.address,
9 + this.txId,
10 + this.spendKey,
11 + this.viewKey,
12 + this.mnemonicSeed,
13 + this.txAmount,
14 + this.txDescription,
15 + this.recipientName,
16 + this.height});
17 +
18 + final WalletRestoreMode restoreMode;
19 + final WalletType type;
20 + final String? address;
21 + final String? txId;
22 + final String? spendKey;
23 + final String? viewKey;
24 + final String? mnemonicSeed;
25 + final String? txAmount;
26 + final String? txDescription;
27 + final String? recipientName;
28 + final int? height;
29 +
30 + factory RestoredWallet.fromKey(Map<String, dynamic> json) {
31 + final height = json['height'] as String?;
32 + return RestoredWallet(
33 + restoreMode: json['mode'] as WalletRestoreMode,
34 + type: json['type'] as WalletType,
35 + address: json['address'] as String?,
36 + spendKey: json['spend_key'] as String?,
37 + viewKey: json['view_key'] as String?,
38 + height: height != null ? int.parse(height) : 0,
39 + );
40 + }
41 +
42 + factory RestoredWallet.fromSeed(Map<String, dynamic> json) {
43 + final height = json['height'] as String?;
44 + final mnemonic_seed = json['mnemonic_seed'] as String?;
45 + final seed = json['seed'] as String?;
46 + return RestoredWallet(
47 + restoreMode: json['mode'] as WalletRestoreMode,
48 + type: json['type'] as WalletType,
49 + address: json['address'] as String?,
50 + mnemonicSeed: mnemonic_seed ?? seed,
51 + height: height != null ? int.parse(height) : 0,
52 + );
53 + }
54 +
55 + factory RestoredWallet.fromTxIds(Map<String, dynamic> json) {
56 + return RestoredWallet(
57 + restoreMode: json['mode'] as WalletRestoreMode,
58 + type: json['type'] as WalletType,
59 + address: json['address'] as String?,
60 + txId: json['tx_payment_id'] as String,
61 + txAmount: json['tx_amount'] as String,
62 + txDescription: json['tx_description'] as String?,
63 + recipientName: json['recipient_name'] as String?,
64 + );
65 + }
66 +}
lib/view_model/restore/wallet_restore_from_qr_code.dart new
+159
@@ -0,0 +1,159 @@
1 +import 'package:cake_wallet/core/address_validator.dart';
2 +import 'package:cake_wallet/core/seed_validator.dart';
3 +import 'package:cake_wallet/entities/mnemonic_item.dart';
4 +import 'package:cake_wallet/entities/parse_address_from_domain.dart';
5 +import 'package:cake_wallet/entities/qr_scanner.dart';
6 +import 'package:cake_wallet/view_model/restore/restore_mode.dart';
7 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
8 +import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
9 +import 'package:cw_core/wallet_type.dart';
10 +import 'package:flutter/cupertino.dart';
11 +
12 +class WalletRestoreFromQRCode {
13 + WalletRestoreFromQRCode();
14 +
15 + static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
16 + String code = await presentQRScanner();
17 + Map<String, dynamic> credentials = {};
18 +
19 + if (code.isEmpty) {
20 + throw Exception('Unexpected scan QR code value: value is empty');
21 + }
22 + final formattedUri = getFormattedUri(code);
23 + final uri = Uri.parse(formattedUri);
24 + final queryParameters = uri.queryParameters;
25 + credentials['type'] = getWalletTypeFromUrl(uri.scheme);
26 +
27 + final address = getAddressFromUrl(
28 + type: credentials['type'] as WalletType,
29 + rawString: queryParameters.toString(),
30 + );
31 + if (address != null) {
32 + credentials['address'] = address;
33 + }
34 +
35 + final seed =
36 + getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType);
37 + if (seed != null) {
38 + credentials['seed'] = seed;
39 + }
40 + credentials.addAll(queryParameters);
41 + credentials['mode'] = getWalletRestoreMode(credentials);
42 +
43 + switch (credentials['mode']) {
44 + case WalletRestoreMode.txids:
45 + return RestoredWallet.fromTxIds(credentials);
46 + case WalletRestoreMode.seed:
47 + return RestoredWallet.fromSeed(credentials);
48 + case WalletRestoreMode.keys:
49 + return RestoredWallet.fromKey(credentials);
50 + default:
51 + throw Exception('Unexpected restore mode: ${credentials['mode']}');
52 + }
53 + }
54 +
55 + static String getFormattedUri(String code) {
56 + final index = code.indexOf(':');
57 + final scheme = code.substring(0, index).replaceAll('_', '-');
58 + final query = code.substring(index + 1).replaceAll('?', '&');
59 + final formattedUri = '$scheme:?$query';
60 + return formattedUri;
61 + }
62 +
63 + static WalletType getWalletTypeFromUrl(String scheme) {
64 + switch (scheme) {
65 + case 'monero':
66 + case 'monero-wallet':
67 + return WalletType.monero;
68 + case 'bitcoin':
69 + case 'bitcoin-wallet':
70 + return WalletType.bitcoin;
71 + case 'litecoin':
72 + case 'litecoin-wallet':
73 + return WalletType.litecoin;
74 + default:
75 + throw Exception('Unexpected wallet type: ${scheme.toString()}');
76 + }
77 + }
78 +
79 + static String? getAddressFromUrl({required WalletType type, required String rawString}) {
80 + return AddressResolver.extractAddressByType(
81 + raw: rawString, type: walletTypeToCryptoCurrency(type));
82 + }
83 +
84 + static String? getSeedPhraseFromUrl(String rawString, WalletType walletType) {
85 + switch (walletType) {
86 + case WalletType.monero:
87 + RegExp regex25 = RegExp(r'\b(\S+\b\s+){24}\S+\b');
88 + RegExp regex14 = RegExp(r'\b(\S+\b\s+){13}\S+\b');
89 + RegExp regex13 = RegExp(r'\b(\S+\b\s+){12}\S+\b');
90 +
91 + if (regex25.firstMatch(rawString) == null) {
92 + if (regex14.firstMatch(rawString) == null) {
93 + if (regex13.firstMatch(rawString) == null) {
94 + return null;
95 + } else {
96 + return regex13.firstMatch(rawString)!.group(0)!;
97 + }
98 + } else {
99 + return regex14.firstMatch(rawString)!.group(0)!;
100 + }
101 + } else {
102 + return regex25.firstMatch(rawString)!.group(0)!;
103 + }
104 + case WalletType.bitcoin:
105 + case WalletType.litecoin:
106 + RegExp regex24 = RegExp(r'\b(\S+\b\s+){23}\S+\b');
107 + RegExp regex18 = RegExp(r'\b(\S+\b\s+){17}\S+\b');
108 + RegExp regex12 = RegExp(r'\b(\S+\b\s+){11}\S+\b');
109 +
110 + if (regex24.firstMatch(rawString) == null) {
111 + if (regex18.firstMatch(rawString) == null) {
112 + if (regex12.firstMatch(rawString) == null) {
113 + return null;
114 + } else {
115 + return regex12.firstMatch(rawString)!.group(0)!;
116 + }
117 + } else {
118 + return regex18.firstMatch(rawString)!.group(0)!;
119 + }
120 + } else {
121 + return regex24.firstMatch(rawString)!.group(0)!;
122 + }
123 + default:
124 + return null;
125 + }
126 + }
127 +
128 + static WalletRestoreMode getWalletRestoreMode(Map<String, dynamic> credentials) {
129 + final type = credentials['type'] as WalletType;
130 + if (credentials.containsKey('tx_payment_id')) {
131 + final txIdValue = credentials['tx_payment_id'] as String? ?? '';
132 + return txIdValue.isNotEmpty
133 + ? WalletRestoreMode.txids
134 + : throw Exception('Unexpected restore mode: tx_payment_id is invalid');
135 + }
136 +
137 + if (credentials.containsKey('seed')) {
138 + final seedValue = credentials['seed'] as String;
139 + final words = SeedValidator.getWordList(type: type, language: 'english');
140 + seedValue.split(' ').forEach((element) {
141 + if (!words.contains(element)) {
142 + throw Exception('Unexpected restore mode: mnemonic_seed is invalid');
143 + }
144 + });
145 + return WalletRestoreMode.seed;
146 + }
147 +
148 + if (credentials.containsKey('spend_key') || credentials.containsKey('view_key')) {
149 + final spendKeyValue = credentials['spend_key'] as String? ?? '';
150 + final viewKeyValue = credentials['view_key'] as String? ?? '';
151 +
152 + return spendKeyValue.isNotEmpty || viewKeyValue.isNotEmpty
153 + ? WalletRestoreMode.keys
154 + : throw Exception('Unexpected restore mode: spend_key or view_key is invalid');
155 + }
156 +
157 + throw Exception('Unexpected restore mode: restore params are invalid');
158 + }
159 +}
lib/view_model/wallet_creation_vm.dart
+15 -5
@@ -1,5 +1,5 @@
1 import 'package:cake_wallet/core/wallet_creation_service.dart';
2 -import 'package:flutter/foundation.dart';
2 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
3 import 'package:hive/hive.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:cake_wallet/core/execution_state.dart';
@@ -39,7 +39,8 @@ abstract class WalletCreationVMBase with Store {
39 bool typeExists(WalletType type)
40 => walletCreationService.typeExists(type);
41
42 - Future<void> create({dynamic options}) async {
42 + Future<void> create({dynamic options, RestoredWallet? restoreWallet}) async {
43 + final type = restoreWallet?.type ?? this.type;
44 try {
45 state = IsExecutingState();
46 if (name.isEmpty) {
@@ -49,7 +50,9 @@ abstract class WalletCreationVMBase with Store {
50 walletCreationService.checkIfExists(name);
51 final dirPath = await pathForWalletDir(name: name, type: type);
52 final path = await pathForWallet(name: name, type: type);
52 - final credentials = getCredentials(options);
53 + final credentials = restoreWallet != null
54 + ? getCredentialsFromRestoredWallet(options, restoreWallet)
55 + : getCredentials(options);
56 final walletInfo = WalletInfo.external(
57 id: WalletBase.idFor(name, type),
58 name: name,
@@ -62,7 +65,9 @@ abstract class WalletCreationVMBase with Store {
65 address: '',
66 showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven);
67 credentials.walletInfo = walletInfo;
65 - final wallet = await process(credentials);
68 + final wallet = restoreWallet != null
69 + ? await processFromRestoredWallet(credentials, restoreWallet)
70 + : await process(credentials);
71 walletInfo.address = wallet.walletAddresses.address;
72 await _walletInfoSource.add(walletInfo);
73 _appStore.changeCurrentWallet(wallet);
@@ -72,10 +77,15 @@ abstract class WalletCreationVMBase with Store {
77 state = FailureState(e.toString());
78 }
79 }
75 -
80 WalletCredentials getCredentials(dynamic options) =>
81 throw UnimplementedError();
82
83 Future<WalletBase> process(WalletCredentials credentials) =>
84 throw UnimplementedError();
85 +
86 + WalletCredentials getCredentialsFromRestoredWallet(dynamic options, RestoredWallet restoreWallet) =>
87 + throw UnimplementedError();
88 +
89 + Future<WalletBase> processFromRestoredWallet(WalletCredentials credentials, RestoredWallet restoreWallet) =>
90 + throw UnimplementedError();
91 }
lib/view_model/wallet_keys_view_model.dart
+7 -6
@@ -88,16 +88,17 @@ abstract class WalletKeysViewModelBase with Store {
88 return null;
89 }
90
91 - String get _path {
91 +
92 + String get _scheme {
93 switch (_appStore.wallet!.type) {
94 case WalletType.monero:
94 - return 'monero_wallet:';
95 + return 'monero-wallet';
96 case WalletType.bitcoin:
96 - return 'bitcoin_wallet:';
97 + return 'bitcoin-wallet';
98 case WalletType.litecoin:
98 - return 'litecoin_wallet:';
99 + return 'litecoin-wallet';
100 case WalletType.haven:
100 - return 'haven_wallet:';
101 + return 'haven-wallet';
102 default:
103 throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
104 }
@@ -124,7 +125,7 @@ abstract class WalletKeysViewModelBase with Store {
125
126 Future<Uri> get url async {
127 return Uri(
127 - path: _path,
128 + scheme: _scheme,
129 queryParameters: await _queryParams,
130 );
131 }
lib/view_model/wallet_new_vm.dart
+1
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:hive/hive.dart';
4 import 'package:mobx/mobx.dart';
lib/view_model/wallet_restoration_from_keys_vm.dart
+2 -1
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:hive/hive.dart';
4 import 'package:mobx/mobx.dart';
@@ -66,7 +67,7 @@ abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM
67 return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials(
68 name: name, password: password, wif: wif);
69 default:
69 - throw Exception('Unexpected type: ${type.toString()}');;
70 + throw Exception('Unexpected type: ${type.toString()}');
71 }
72 }
73
lib/view_model/wallet_restoration_from_seed_vm.dart
+1
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:hive/hive.dart';
4 import 'package:mobx/mobx.dart';
lib/view_model/wallet_restore_view_model.dart
+2 -1
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/core/mnemonic_length.dart';
3 +import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:hive/hive.dart';
6 import 'package:mobx/mobx.dart';
@@ -13,10 +14,10 @@ import 'package:cw_core/wallet_info.dart';
14 import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
15 import 'package:cake_wallet/monero/monero.dart';
16 import 'package:cake_wallet/haven/haven.dart';
17 +import 'package:cake_wallet/view_model/restore/restore_mode.dart';
18
19 part 'wallet_restore_view_model.g.dart';
20
19 -enum WalletRestoreMode { seed, keys }
21
22 class WalletRestoreViewModel = WalletRestoreViewModelBase
23 with _$WalletRestoreViewModel;
res/values/strings_ar.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount":"المقدار",
151 "subaddresses":"العناوين الفرعية",
152 "addresses":"عناوين",
153 - "scan_qr_code":"امسح ال QR للحصول على العنوان",
153 + "scan_qr_code_to_get_address":"امسح ال QR للحصول على العنوان",
154 "qr_fullscreen":"انقر لفتح ال QR بملء الشاشة",
155 "rename":"إعادة تسمية",
156 "choose_account":"اختر حساب",
@@ -683,6 +683,11 @@
683 "arrive_in_this_address" : "سيصل ${currency} ${tag}إلى هذا العنوان",
684 "do_not_send": "لا ترسل",
685 "error_dialog_content": "عفوًا ، لقد حصلنا على بعض الخطأ.\n\nيرجى إرسال تقرير التعطل إلى فريق الدعم لدينا لتحسين التطبيق.",
686 + "scan_qr_code": "امسح رمز QR ضوئيًا",
687 + "cold_or_recover_wallet": "أضف محفظة باردة أو استعد محفظة ورقية",
688 + "please_wait": "انتظر من فضلك",
689 + "sweeping_wallet": "كنس المحفظة",
690 + "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلاً. لا تترك هذه الشاشة وإلا فقد يتم فقد أموال سويبت",
691 "decimal_places_error": "عدد كبير جدًا من المنازل العشرية",
692 "edit_node": "تحرير العقدة",
693 "frozen_balance": "الرصيد المجمد",
res/values/strings_de.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Betrag",
151 "subaddresses" : "Unteradressen",
152 "addresses" : "Adressen",
153 - "scan_qr_code" : "Scannen Sie den QR-Code, um die Adresse zu erhalten",
153 + "scan_qr_code_to_get_address" : "Scannen Sie den QR-Code, um die Adresse zu erhalten",
154 "qr_fullscreen" : "Tippen Sie hier, um den QR-Code im Vollbildmodus zu öffnen",
155 "rename" : "Umbenennen",
156 "choose_account" : "Konto auswählen",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}wird an dieser Adresse ankommen",
686 "do_not_send": "Nicht senden",
687 "error_dialog_content": "Hoppla, wir haben einen Fehler.\n\nBitte senden Sie den Absturzbericht an unser Support-Team, um die Anwendung zu verbessern.",
688 + "scan_qr_code": "QR-Code scannen",
689 + "cold_or_recover_wallet": "Fügen Sie eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her",
690 + "please_wait": "Warten Sie mal",
691 + "sweeping_wallet": "Kehre Geldbörse",
692 + "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE SWEPT-GELDER VERLOREN GEHEN",
693 "decimal_places_error": "Zu viele Nachkommastellen",
694 "edit_node": "Knoten bearbeiten",
695 "frozen_balance": "Gefrorenes Gleichgewicht",
res/values/strings_en.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Amount",
151 "subaddresses" : "Subaddresses",
152 "addresses" : "Addresses",
153 - "scan_qr_code" : "Scan the QR code to get the address",
153 + "scan_qr_code_to_get_address" : "Scan the QR code to get the address",
154 "qr_fullscreen" : "Tap to open full screen QR code",
155 "rename" : "Rename",
156 "choose_account" : "Choose account",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}will arrive in this address",
686 "do_not_send": "Don't send",
687 "error_dialog_content": "Oops, we got some error.\n\nPlease send the crash report to our support team to make the application better.",
688 + "scan_qr_code": "Scan QR code",
689 + "cold_or_recover_wallet": "Add a cold wallet or recover a paper wallet",
690 + "please_wait": "Please wait",
691 + "sweeping_wallet": "Sweeping wallet",
692 + "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
693 "invoice_details": "Invoice details",
694 "donation_link_details": "Donation link details",
695 "anonpay_description": "Generate ${type}. The recipient can ${method} with any supported cryptocurrency, and you will receive funds in this wallet.",
res/values/strings_es.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Cantidad",
151 "subaddresses" : "Subdirecciones",
152 "addresses" : "Direcciones",
153 - "scan_qr_code" : "Escanee el código QR para obtener la dirección",
153 + "scan_qr_code_to_get_address" : "Escanee el código QR para obtener la dirección",
154 "qr_fullscreen" : "Toque para abrir el código QR en pantalla completa",
155 "rename" : "Rebautizar",
156 "choose_account" : "Elegir cuenta",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}llegará a esta dirección",
686 "do_not_send": "no enviar",
687 "error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.",
688 + "scan_qr_code": "Escanear código QR",
689 + "cold_or_recover_wallet": "Agregue una billetera fría o recupere una billetera de papel",
690 + "please_wait": "Espere por favor",
691 + "sweeping_wallet": "Billetera de barrido",
692 + "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS",
693 "decimal_places_error": "Demasiados lugares decimales",
694 "edit_node": "Editar nodo",
695 "frozen_balance": "Balance congelado",
res/values/strings_fr.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Montant",
151 "subaddresses" : "Sous-adresses",
152 "addresses" : "Adresses",
153 - "scan_qr_code" : "Scannez le QR code pour obtenir l'adresse",
153 + "scan_qr_code_to_get_address" : "Scannez le QR code pour obtenir l'adresse",
154 "qr_fullscreen" : "Appuyez pour ouvrir le QR code en mode plein écran",
155 "rename" : "Renommer",
156 "choose_account" : "Choisir le compte",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}arrivera à cette adresse",
686 "do_not_send": "Ne pas envoyer",
687 "error_dialog_content": "Oups, nous avons rencontré une erreur.\n\nMerci d'envoyer le rapport d'erreur à notre équipe d'assistance afin de nous permettre d'améliorer l'application.",
688 + "scan_qr_code": "Scannez le code QR",
689 + "cold_or_recover_wallet": "Ajoutez un cold wallet ou récupérez un paper wallet",
690 + "please_wait": "S'il vous plaît, attendez",
691 + "sweeping_wallet": "Portefeuille de balayage",
692 + "sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN OU LES FONDS BALAYÉS POURRAIENT ÊTRE PERDUS",
693 "decimal_places_error": "Trop de décimales",
694 "edit_node": "Modifier le nœud",
695 "frozen_balance": "Équilibre gelé",
res/values/strings_hi.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "रकम",
151 "subaddresses" : "उप पते",
152 "addresses" : "पतों",
153 - "scan_qr_code" : "पता प्राप्त करने के लिए QR कोड स्कैन करें",
153 + "scan_qr_code_to_get_address" : "पता प्राप्त करने के लिए QR कोड स्कैन करें",
154 "qr_fullscreen" : "फ़ुल स्क्रीन क्यूआर कोड खोलने के लिए टैप करें",
155 "rename" : "नाम बदलें",
156 "choose_account" : "खाता चुनें",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}इस पते पर पहुंचेंगे",
686 "do_not_send": "मत भेजो",
687 "error_dialog_content": "ओह, हमसे कुछ गड़बड़ी हुई है.\n\nएप्लिकेशन को बेहतर बनाने के लिए कृपया क्रैश रिपोर्ट हमारी सहायता टीम को भेजें।",
688 + "scan_qr_code": "स्कैन क्यू आर कोड",
689 + "cold_or_recover_wallet": "कोल्ड वॉलेट जोड़ें या पेपर वॉलेट पुनर्प्राप्त करें",
690 + "please_wait": "कृपया प्रतीक्षा करें",
691 + "sweeping_wallet": "स्वीपिंग वॉलेट",
692 + "sweeping_wallet_alert": "इसमें अधिक समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें या स्वैप्ट फंड खो सकते हैं",
693 "decimal_places_error": "बहुत अधिक दशमलव स्थान",
694 "edit_node": "नोड संपादित करें",
695 "frozen_balance": "जमे हुए संतुलन",
res/values/strings_hr.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Iznos",
151 "subaddresses" : "Podadrese",
152 "addresses" : "Adrese",
153 - "scan_qr_code" : "Skeniraj QR kod za dobivanje adrese",
153 + "scan_qr_code_to_get_address" : "Skeniraj QR kod za dobivanje adrese",
154 "qr_fullscreen" : "Dodirnite za otvaranje QR koda preko cijelog zaslona",
155 "rename" : "Preimenuj",
156 "choose_account" : "Odaberi račun",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}će stići na ovu adresu",
686 "do_not_send": "Ne šalji",
687 "error_dialog_content": "Ups, imamo grešku.\n\nPošaljite izvješće o padu našem timu za podršku kako bismo poboljšali aplikaciju.",
688 + "scan_qr_code": "Skenirajte QR kod",
689 + "cold_or_recover_wallet": "Dodajte hladni novčanik ili povratite papirnati novčanik",
690 + "please_wait": "Molimo pričekajte",
691 + "sweeping_wallet": "Čisti novčanik",
692 + "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON INAČE SE POBREŠENA SREDSTVA MOGU IZGUBITI",
693 "decimal_places_error": "Previše decimalnih mjesta",
694 "edit_node": "Uredi čvor",
695 "frozen_balance": "Zamrznuti saldo",
res/values/strings_it.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Ammontare",
151 "subaddresses" : "Sottoindirizzi",
152 "addresses" : "Indirizzi",
153 - "scan_qr_code" : "Scansiona il codice QR per ottenere l'indirizzo",
153 + "scan_qr_code_to_get_address" : "Scansiona il codice QR per ottenere l'indirizzo",
154 "qr_fullscreen" : "Tocca per aprire il codice QR a schermo intero",
155 "rename" : "Rinomina",
156 "choose_account" : "Scegli account",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}arriverà a questo indirizzo",
686 "do_not_send": "Non inviare",
687 "error_dialog_content": "Spiacenti, abbiamo riscontrato un errore.\n\nSi prega di inviare il rapporto sull'arresto anomalo al nostro team di supporto per migliorare l'applicazione.",
688 + "scan_qr_code": "Scansiona il codice QR",
689 + "cold_or_recover_wallet": "Aggiungi un cold wallet o recupera un paper wallet",
690 + "please_wait": "Attendere prego",
691 + "sweeping_wallet": "Portafoglio ampio",
692 + "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPAZZATI POTREBBERO ANDARE PERSI",
693 "decimal_places_error": "Troppe cifre decimali",
694 "edit_node": "Modifica nodo",
695 "frozen_balance": "Equilibrio congelato",
res/values/strings_ja.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "量",
151 "subaddresses" : "サブアドレス",
152 "addresses" : "住所",
153 - "scan_qr_code" : "QRコードをスキャンして住所を取得します",
153 + "scan_qr_code_to_get_address" : "QRコードをスキャンして住所を取得します",
154 "qr_fullscreen" : "タップして全画面QRコードを開く",
155 "rename" : "リネーム",
156 "choose_account" : "アカウントを選択",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}はこの住所に到着します",
686 "do_not_send": "送信しない",
687 "error_dialog_content": "エラーが発生しました。\n\nアプリケーションを改善するために、クラッシュ レポートをサポート チームに送信してください。",
688 + "scan_qr_code": "QRコードをスキャン",
689 + "cold_or_recover_wallet": "コールド ウォレットを追加するか、ペーパー ウォレットを復元する",
690 + "please_wait": "お待ちください",
691 + "sweeping_wallet": "スイープウォレット",
692 + "sweeping_wallet_alert": "これには時間がかかりません。この画面から離れないでください。そうしないと、スイープ ファンドが失われる可能性があります",
693 "decimal_places_error": "小数点以下の桁数が多すぎる",
694 "edit_node": "ノードを編集",
695 "frozen_balance": "冷凍残高",
res/values/strings_ko.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "양",
151 "subaddresses" : "하위 주소",
152 "addresses" : "구애",
153 - "scan_qr_code" : "QR 코드를 스캔하여 주소를 얻습니다.",
153 + "scan_qr_code_to_get_address" : "QR 코드를 스캔하여 주소를 얻습니다.",
154 "qr_fullscreen" : "전체 화면 QR 코드를 열려면 탭하세요.",
155 "rename" : "이름 바꾸기",
156 "choose_account" : "계정을 선택하십시오",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}이(가) 이 주소로 도착합니다",
686 "do_not_send": "보내지 마세요",
687 "error_dialog_content": "죄송합니다. 오류가 발생했습니다.\n\n응용 프로그램을 개선하려면 지원 팀에 충돌 보고서를 보내주십시오.",
688 + "scan_qr_code": "QR 코드 스캔",
689 + "cold_or_recover_wallet": "콜드 지갑 추가 또는 종이 지갑 복구",
690 + "please_wait": "기다리세요",
691 + "sweeping_wallet": "스위핑 지갑",
692 + "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마십시오. 그렇지 않으면 스웹트 자금이 손실될 수 있습니다.",
693 "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.",
694 "edit_node": "노드 편집",
695 "frozen_balance": "얼어붙은 균형",
res/values/strings_my.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "ပမာဏ",
151 "subaddresses" : "လိပ်စာများ",
152 "addresses" : "လိပ်စာများ",
153 - "scan_qr_code" : "လိပ်စာရယူရန် QR ကုဒ်ကို စကင်န်ဖတ်ပါ။",
153 + "scan_qr_code_to_get_address" : "လိပ်စာရယူရန် QR ကုဒ်ကို စကင်န်ဖတ်ပါ။",
154 "qr_fullscreen" : "မျက်နှာပြင်အပြည့် QR ကုဒ်ကိုဖွင့်ရန် တို့ပါ။",
155 "rename" : "အမည်ပြောင်းပါ။",
156 "choose_account" : "အကောင့်ကို ရွေးပါ။",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}ဤလိပ်စာသို့ ရောက်ရှိပါမည်။",
686 "do_not_send": "မပို့ပါနှင့်",
687 "error_dialog_content": "အိုး၊ ကျွန်ုပ်တို့တွင် အမှားအယွင်းအချို့ရှိသည်။\n\nအပလီကေးရှင်းကို ပိုမိုကောင်းမွန်စေရန်အတွက် ပျက်စီးမှုအစီရင်ခံစာကို ကျွန်ုပ်တို့၏ပံ့ပိုးကူညီရေးအဖွဲ့ထံ ပေးပို့ပါ။",
688 + "scan_qr_code": "QR ကုဒ်ကို စကင်န်ဖတ်ပါ။",
689 + "cold_or_recover_wallet": "အေးသောပိုက်ဆံအိတ်ထည့်ပါ သို့မဟုတ် စက္ကူပိုက်ဆံအိတ်ကို ပြန်ယူပါ။",
690 + "please_wait": "ကျေးဇူးပြုပြီးခဏစောင့်ပါ",
691 + "sweeping_wallet": "ိုက်ဆံအိတ် တံမြက်လှည်း",
692 + "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဤစခရင်ကို ချန်မထားပါနှင့် သို့မဟုတ် ထုတ်ယူထားသော ရန်ပုံငွေများ ဆုံးရှုံးနိုင်သည်",
693 "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။",
694 "edit_node": "Node ကို တည်းဖြတ်ပါ။",
695 "frozen_balance": "ေးခဲမှူ",
res/values/strings_nl.arb
+6 -1
@@ -151,7 +151,7 @@
151 "subaddresses" : "Subadressen",
152 "rename" : "Hernoemen",
153 "addresses" : "Adressen",
154 - "scan_qr_code" : "Scan de QR-code om het adres te krijgen",
154 + "scan_qr_code_to_get_address" : "Scan de QR-code om het adres te krijgen",
155 "qr_fullscreen" : "Tik om de QR-code op volledig scherm te openen",
156 "choose_account" : "Kies account",
157 "create_new_account" : "Creëer een nieuw account",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}komt aan op dit adres",
686 "do_not_send": "Niet sturen",
687 "error_dialog_content": "Oeps, er is een fout opgetreden.\n\nStuur het crashrapport naar ons ondersteuningsteam om de applicatie te verbeteren.",
688 + "scan_qr_code": "Scan QR-code",
689 + "cold_or_recover_wallet": "Voeg een cold wallet toe of herstel een paper wallet",
690 + "please_wait": "Even geduld aub",
691 + "sweeping_wallet": "Vegende portemonnee",
692 + "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KAN HET SWEPT-GELD VERLOREN WORDEN",
693 "decimal_places_error": "Te veel decimalen",
694 "edit_node": "Knooppunt bewerken",
695 "frozen_balance": "Bevroren saldo",
res/values/strings_pl.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Ilość",
151 "subaddresses" : "Podadresy",
152 "addresses" : "Adresy",
153 - "scan_qr_code" : "Zeskanuj kod QR, aby uzyskać adres",
153 + "scan_qr_code_to_get_address" : "Zeskanuj kod QR, aby uzyskać adres",
154 "qr_fullscreen" : "Dotknij, aby otworzyć pełnoekranowy kod QR",
155 "rename" : "Zmień nazwę",
156 "choose_account" : "Wybierz konto",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}dotrze na ten adres",
686 "do_not_send": "Nie wysyłaj",
687 "error_dialog_content": "Ups, wystąpił błąd.\n\nPrześlij raport o awarii do naszego zespołu wsparcia, aby ulepszyć aplikację.",
688 + "scan_qr_code": "Skanowania QR code",
689 + "cold_or_recover_wallet": "Dodaj zimny portfel lub odzyskaj portfel papierowy",
690 + "please_wait": "Proszę czekać",
691 + "sweeping_wallet": "Zamiatanie portfela",
692 + "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI",
693 "decimal_places_error": "Za dużo miejsc dziesiętnych",
694 "edit_node": "Edytuj węzeł",
695 "frozen_balance": "Zamrożona równowaga",
res/values/strings_pt.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Quantia",
151 "subaddresses" : "Sub-endereços",
152 "addresses" : "Endereços",
153 - "scan_qr_code" : "Digitalize o código QR para obter o endereço",
153 + "scan_qr_code_to_get_address" : "Digitalize o código QR para obter o endereço",
154 "qr_fullscreen" : "Toque para abrir o código QR em tela cheia",
155 "rename" : "Renomear",
156 "choose_account" : "Escolha uma conta",
@@ -684,6 +684,11 @@
684 "arrive_in_this_address" : "${currency} ${tag}chegará neste endereço",
685 "do_not_send": "não envie",
686 "error_dialog_content": "Ops, houve algum erro.\n\nPor favor, envie o relatório de falha para nossa equipe de suporte para melhorar o aplicativo.",
687 + "scan_qr_code": "Escanear código QR",
688 + "cold_or_recover_wallet": "Adicione uma cold wallet ou recupere uma paper wallet",
689 + "please_wait": "Por favor, aguarde",
690 + "sweeping_wallet": "Carteira varrendo",
691 + "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI",
692 "decimal_places_error": "Muitas casas decimais",
693 "edit_node": "Editar nó",
694 "frozen_balance": "Saldo Congelado",
res/values/strings_ru.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Сумма",
151 "subaddresses" : "Субадреса",
152 "addresses" : "Адреса",
153 - "scan_qr_code" : "Отсканируйте QR-код для получения адреса",
153 + "scan_qr_code_to_get_address" : "Отсканируйте QR-код для получения адреса",
154 "qr_fullscreen" : "Нажмите, чтобы открыть полноэкранный QR-код",
155 "rename" : "Переименовать",
156 "choose_account" : "Выберите аккаунт",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}придет на этот адрес",
686 "do_not_send": "Не отправлять",
687 "error_dialog_content": "Ой, у нас какая-то ошибка.\n\nПожалуйста, отправьте отчет о сбое в нашу службу поддержки, чтобы сделать приложение лучше.",
688 + "scan_qr_code": "Сканировать QR-код",
689 + "cold_or_recover_wallet": "Добавьте холодный кошелек или восстановите бумажный кошелек",
690 + "please_wait": "Пожалуйста, подождите",
691 + "sweeping_wallet": "Подметание кошелька",
692 + "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ВЫЧИСЛЕННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ",
693 "decimal_places_error": "Слишком много десятичных знаков",
694 "edit_node": "Редактировать узел",
695 "frozen_balance": "Замороженный баланс",
res/values/strings_th.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "จำนวน",
151 "subaddresses" : "ที่อยู่ย่อย",
152 "addresses" : "ที่อยู่",
153 - "scan_qr_code" : "สแกน QR code เพื่อรับที่อยู่",
153 + "scan_qr_code_to_get_address" : "สแกน QR code เพื่อรับที่อยู่",
154 "qr_fullscreen" : "แตะเพื่อเปิดหน้าจอ QR code แบบเต็มจอ",
155 "rename" : "เปลี่ยนชื่อ",
156 "choose_account" : "เลือกบัญชี",
@@ -683,6 +683,11 @@
683 "arrive_in_this_address" : "${currency} ${tag}จะมาถึงที่อยู่นี้",
684 "do_not_send": "อย่าส่ง",
685 "error_dialog_content": "อ๊ะ เราพบข้อผิดพลาดบางอย่าง\n\nโปรดส่งรายงานข้อขัดข้องไปยังทีมสนับสนุนของเราเพื่อปรับปรุงแอปพลิเคชันให้ดียิ่งขึ้น",
686 + "scan_qr_code": "สแกนรหัส QR",
687 + "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินเย็นหรือกู้คืนกระเป๋าเงินกระดาษ",
688 + "please_wait": "โปรดรอ",
689 + "sweeping_wallet": "กวาดกระเป๋าสตางค์",
690 + "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน อย่าออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดไปอาจสูญหาย",
691 "decimal_places_error": "ทศนิยมมากเกินไป",
692 "edit_node": "แก้ไขโหนด",
693 "frozen_balance": "ยอดคงเหลือแช่แข็ง",
res/values/strings_tr.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Miktar",
151 "subaddresses" : "Alt adresler",
152 "addresses" : "Adresler",
153 - "scan_qr_code" : "Adresi getirmek için QR kodunu tara",
153 + "scan_qr_code_to_get_address" : "Adresi getirmek için QR kodunu tara",
154 "qr_fullscreen" : "QR kodunu tam ekranda açmak için dokun",
155 "rename" : "Yeniden adlandır",
156 "choose_account" : "Hesabı seç",
@@ -685,6 +685,11 @@
685 "arrive_in_this_address" : "${currency} ${tag}bu adrese ulaşacak",
686 "do_not_send": "Gönderme",
687 "error_dialog_content": "Hay aksi, bir hatamız var.\n\nUygulamayı daha iyi hale getirmek için lütfen kilitlenme raporunu destek ekibimize gönderin.",
688 + "scan_qr_code": "QR kodunu tarayın",
689 + "cold_or_recover_wallet": "Soğuk bir cüzdan ekleyin veya bir kağıt cüzdanı kurtarın",
690 + "please_wait": "Lütfen bekleyin",
691 + "sweeping_wallet": "Süpürme cüzdanı",
692 + "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN BIRAKMAYIN YOKSA SÜPÜRÜLEN FONLAR KAYBOLABİLİR",
693 "decimal_places_error": "Çok fazla ondalık basamak",
694 "edit_node": "Düğümü Düzenle",
695 "frozen_balance": "Dondurulmuş Bakiye",
res/values/strings_uk.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "Сума",
151 "subaddresses" : "Субадреси",
152 "addresses" : "Адреси",
153 - "scan_qr_code" : "Скануйте QR-код для одержання адреси",
153 + "scan_qr_code_to_get_address" : "Скануйте QR-код для одержання адреси",
154 "qr_fullscreen" : "Торкніться, щоб відкрити QR-код на весь екран",
155 "rename" : "Перейменувати",
156 "choose_account" : "Оберіть акаунт",
@@ -684,6 +684,11 @@
684 "arrive_in_this_address" : "${currency} ${tag}надійде на цю адресу",
685 "do_not_send": "Не надсилайте",
686 "error_dialog_content": "На жаль, ми отримали помилку.\n\nБудь ласка, надішліть звіт про збій нашій команді підтримки, щоб покращити додаток.",
687 + "scan_qr_code": "Відскануйте QR-код",
688 + "cold_or_recover_wallet": "Додайте холодний гаманець або відновіть паперовий гаманець",
689 + "please_wait": "Будь ласка, зачекайте",
690 + "sweeping_wallet": "Підмітаня гаманця",
691 + "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЬОГО ЕКРАНУ, АБО КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ",
692 "decimal_places_error": "Забагато знаків після коми",
693 "edit_node": "Редагувати вузол",
694 "frozen_balance": "Заморожений баланс",
res/values/strings_zh.arb
+6 -1
@@ -150,7 +150,7 @@
150 "receive_amount" : "金额",
151 "subaddresses" : "子地址",
152 "addresses" : "地址",
153 - "scan_qr_code" : "扫描二维码获取地址",
153 + "scan_qr_code_to_get_address" : "扫描二维码获取地址",
154 "qr_fullscreen" : "点击打开全屏二维码",
155 "rename" : "重命名",
156 "choose_account" : "选择账户",
@@ -684,6 +684,11 @@
684 "arrive_in_this_address" : "${currency} ${tag}将到达此地址",
685 "do_not_send": "不要发送",
686 "error_dialog_content": "糟糕,我们遇到了一些错误。\n\n请将崩溃报告发送给我们的支持团队,以改进应用程序。",
687 + "scan_qr_code": "扫描二维码",
688 + "cold_or_recover_wallet": "添加冷钱包或恢复纸钱包",
689 + "please_wait": "请稍等",
690 + "sweeping_wallet": "扫一扫钱包",
691 + "sweeping_wallet_alert": "\n这应该不会花很长时间。请勿离开此屏幕,否则可能会丢失所掠取的资金",
692 "decimal_places_error": "小数位太多",
693 "edit_node": "编辑节点",
694 "frozen_balance": "冻结余额",