99 */
1010
1111import 'dart:async' ;
12+ import 'dart:math' ;
1213
1314import 'package:barcode_scan2/barcode_scan2.dart' ;
15+ import 'package:flutter/foundation.dart' ;
1416import 'package:flutter/material.dart' ;
1517import 'package:flutter/services.dart' ;
1618import 'package:flutter_riverpod/flutter_riverpod.dart' ;
1719import 'package:flutter_svg/svg.dart' ;
1820import 'package:isar/isar.dart' ;
21+ import 'package:tuple/tuple.dart' ;
1922
2023import '../../../app_config.dart' ;
2124import '../../../db/isar/main_db.dart' ;
@@ -24,6 +27,7 @@ import '../../../models/add_wallet_list_entity/sub_classes/coin_entity.dart';
2427import '../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart' ;
2528import '../../../models/isar/models/ethereum/eth_contract.dart' ;
2629import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart' ;
30+ import '../../../providers/global/secure_store_provider.dart' ;
2731import '../../../providers/providers.dart' ;
2832import '../../../themes/stack_colors.dart' ;
2933import '../../../utilities/address_utils.dart' ;
@@ -36,12 +40,16 @@ import '../../../utilities/show_loading.dart';
3640import '../../../utilities/text_styles.dart' ;
3741import '../../../utilities/util.dart' ;
3842import '../../../wallets/crypto_currency/crypto_currency.dart' ;
43+ import '../../../wallets/isar/models/wallet_info.dart' ;
44+ import '../../../wallets/wallet/impl/monero_wallet.dart' ;
3945import '../../../wallets/wallet/wallet.dart' ;
4046import '../../../widgets/background.dart' ;
4147import '../../../widgets/custom_buttons/app_bar_icon_button.dart' ;
48+ import '../../../widgets/custom_buttons/blue_text_button.dart' ;
4249import '../../../widgets/desktop/desktop_app_bar.dart' ;
4350import '../../../widgets/desktop/desktop_dialog.dart' ;
4451import '../../../widgets/desktop/desktop_scaffold.dart' ;
52+ import '../../../widgets/desktop/primary_button.dart' ;
4553import '../../../widgets/expandable.dart' ;
4654import '../../../widgets/icon_widgets/x_icon.dart' ;
4755import '../../../widgets/rounded_white_container.dart' ;
@@ -50,6 +58,8 @@ import '../../../widgets/textfield_icon_button.dart';
5058import '../../wallet_view/wallet_view.dart' ;
5159import '../add_token_view/add_custom_token_view.dart' ;
5260import '../add_token_view/sub_widgets/add_custom_token_selector.dart' ;
61+ import '../new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart' ;
62+ import '../verify_recovery_phrase_view/verify_recovery_phrase_view.dart' ;
5363import 'sub_widgets/add_wallet_text.dart' ;
5464import 'sub_widgets/expanding_sub_list_item.dart' ;
5565import 'sub_widgets/next_button.dart' ;
@@ -133,30 +143,319 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
133143 }
134144 }
135145
146+ Tuple2 <List <String >, String > randomize (
147+ List <String > mnemonic,
148+ int chosenIndex,
149+ int wordsToShow,
150+ ) {
151+ final List <String > remaining = [];
152+ final String chosenWord = mnemonic[chosenIndex];
153+
154+ for (int i = 0 ; i < mnemonic.length; i++ ) {
155+ if (chosenWord != mnemonic[i]) {
156+ remaining.add (mnemonic[i]);
157+ }
158+ }
159+
160+ final random = Random ();
161+
162+ final List <String > result = [];
163+
164+ for (int i = 0 ; i < wordsToShow - 1 ; i++ ) {
165+ final randomIndex = random.nextInt (remaining.length);
166+ result.add (remaining.removeAt (randomIndex));
167+ }
168+
169+ result.insert (random.nextInt (wordsToShow), chosenWord);
170+
171+ if (kDebugMode) {
172+ print ("Mnemonic game correct word: $chosenWord " );
173+ }
174+
175+ return Tuple2 (result, chosenWord);
176+ }
177+
136178 Future <void > scanPaperWalletQr () async {
137179 try {
138180 final qrResult = await const BarcodeScannerWrapper ().scan ();
139181
140182 final results = AddressUtils .parseWalletUri (qrResult.rawContent, logging: Logging .instance);
141183
142184 if (results != null ) {
143- if (mounted) {
144- final Wallet <CryptoCurrency >? wallet = await showLoading (
145- whileFuture: results.coin.importPaperWallet (results, ref),
146- context: context,
147- message: "Importing paper wallet..." ,
185+ if (results.coin == Monero (CryptoCurrencyNetwork .main) && results.txids != null ) {
186+ // Mnemonic for the wallet to sweep into is shown and gets confirmed
187+ // Create the new wallet info
188+ final newWallet = await Wallet .create (
189+ walletInfo: WalletInfo .createNew (
190+ coin: results.coin,
191+ name: "${results .coin .prettyName } Gift Wallet ${results .address != null ? '(${results .address !.substring (results .address !.length - 4 )})' : '' }" ,
192+ ),
193+ mainDB: ref.read (mainDBProvider),
194+ secureStorageInterface: ref.read (secureStoreProvider),
195+ nodeService: ref.read (nodeServiceChangeNotifierProvider),
196+ prefs: ref.read (prefsChangeNotifierProvider),
197+ mnemonic: null ,
198+ mnemonicPassphrase: null ,
199+ privateKey: null ,
148200 );
149- if (wallet == null ) {
150- throw Exception (
151- "Failed to import paper wallet because wallet from importPaperWallet is null: ${results .coin .prettyName }" ,
152- );
201+ await (newWallet as MoneroWallet ).init (wordCount: 16 );
202+ final mnemonic = (await newWallet.getMnemonic ()).split (" " );
203+ if (mounted) {
204+ final hasWroteDown = await showDialog <bool >(context: context, barrierDismissible: false , builder: (context) {
205+ return Dialog (
206+ insetPadding: const EdgeInsets .all (16 ), // This may seem too much, but its needed for the dialog to show the mnemonic table properly
207+ child: Container (
208+ decoration: BoxDecoration (
209+ color: Theme .of (context).extension < StackColors > ()! .background,
210+ borderRadius: BorderRadius .circular (Constants .size.circularBorderRadius),
211+ ),
212+ padding: const EdgeInsets .symmetric (horizontal: 12 , vertical: 16 ),
213+ width: double .infinity,
214+ child: Column (
215+ mainAxisSize: MainAxisSize .min,
216+ children: [
217+ Text (
218+ "Monero Gift Wallet Redeem" ,
219+ style: STextStyles .titleBold12 (context),
220+ textAlign: TextAlign .center,
221+ ),
222+ const SizedBox (height: 16 ),
223+ Text (
224+ "You are about to redeem the gift into a wallet with the following mnemonic phrase. Please write down this words. You will be asked to verify the mnemonic phrase after you have written it down." ,
225+ style: isDesktop
226+ ? STextStyles .desktopH2 (context)
227+ : STextStyles .label (context).copyWith (fontSize: 12 ),
228+ textAlign: TextAlign .center,
229+ ),
230+ const SizedBox (height: 16 ),
231+ MnemonicTable (words: mnemonic, isDesktop: isDesktop),
232+ const SizedBox (height: 16 ),
233+ PrimaryButton (
234+ label: "I have written down the mnemonic" ,
235+ onPressed: () {
236+ Navigator .of (context).pop (true );
237+ },
238+ ),
239+ ],
240+ ),
241+ ),
242+ );
243+ }) ?? false ;
244+ if (hasWroteDown) {
245+ // Verify if checked
246+ final chosenIndex = Random ().nextInt (mnemonic.length);
247+ final words = randomize (mnemonic, chosenIndex, 3 );
248+ if (mounted) {
249+ final hasVerified = await showDialog <bool >(context: context, builder: (context) {
250+ return Dialog (
251+ insetPadding: const EdgeInsets .all (16 ), // This may seem too much, but its needed for the dialog to show the mnemonic table properly
252+ child: Container (
253+ decoration: BoxDecoration (
254+ color: Theme .of (context).extension < StackColors > ()! .background,
255+ borderRadius: BorderRadius .circular (Constants .size.circularBorderRadius),
256+ ),
257+ padding: const EdgeInsets .symmetric (horizontal: 12 , vertical: 16 ),
258+ child: Column (
259+ mainAxisSize: MainAxisSize .min,
260+ children: [
261+ Text (
262+ "Monero Gift Wallet Redeem" ,
263+ style: STextStyles .titleBold12 (context),
264+ textAlign: TextAlign .center,
265+ ),
266+ const SizedBox (height: 16 ),
267+ Text (
268+ "Verify recovery phrase" ,
269+ textAlign: TextAlign .center,
270+ style:
271+ isDesktop
272+ ? STextStyles .desktopH2 (context)
273+ : STextStyles .label (context).copyWith (fontSize: 12 ),
274+ ),
275+ const SizedBox (height: 16 ),
276+ Text (
277+ isDesktop ? "Select word number" : "Tap word number " ,
278+ textAlign: TextAlign .center,
279+ style:
280+ isDesktop
281+ ? STextStyles .desktopSubtitleH1 (context)
282+ : STextStyles .pageTitleH1 (context),
283+ ),
284+ const SizedBox (height: 16 ),
285+ Container (
286+ decoration: BoxDecoration (
287+ color:
288+ Theme .of (
289+ context,
290+ ).extension < StackColors > ()! .textFieldDefaultBG,
291+ borderRadius: BorderRadius .circular (
292+ Constants .size.circularBorderRadius,
293+ ),
294+ ),
295+ child: Padding (
296+ padding: const EdgeInsets .symmetric (
297+ vertical: 8 ,
298+ horizontal: 12 ,
299+ ),
300+ child: Text (
301+ "${chosenIndex + 1 }" ,
302+ textAlign: TextAlign .center,
303+ style: STextStyles .subtitle600 (
304+ context,
305+ ).copyWith (fontSize: 32 , letterSpacing: 0.25 ),
306+ ),
307+ ),
308+ ),
309+ const SizedBox (height: 16 ),
310+ Column (
311+ children: [
312+ for (int i = 0 ; i < words.item1.length; i++ )
313+ Padding (
314+ padding: EdgeInsets .symmetric (
315+ vertical: isDesktop ? 8 : 5 ,
316+ ),
317+ child: Row (
318+ mainAxisAlignment: MainAxisAlignment .center,
319+ children: [
320+ if (isDesktop) ...[
321+ const SizedBox (width: 10 ),
322+ ],
323+ Container (
324+ decoration: BoxDecoration (
325+ color: Theme .of (context).extension < StackColors > ()! .popupBG,
326+ borderRadius: BorderRadius .circular (
327+ Constants .size.circularBorderRadius,
328+ ),
329+ ),
330+ child: MaterialButton (
331+ splashColor: Theme .of (context).extension < StackColors > ()! .highlight,
332+ padding: isDesktop
333+ ? const EdgeInsets .symmetric (
334+ vertical: 18 ,
335+ horizontal: 12 ,
336+ )
337+ : const EdgeInsets .all (12 ),
338+ materialTapTargetSize: MaterialTapTargetSize .shrinkWrap,
339+ shape: RoundedRectangleBorder (
340+ borderRadius:
341+ BorderRadius .circular (Constants .size.circularBorderRadius),
342+ ),
343+ onPressed: () {
344+ final word = words.item1[i];
345+ final wordIndex = mnemonic.indexOf (word);
346+ if (wordIndex == chosenIndex) {
347+ Navigator .of (context).pop (true );
348+ } else {
349+ Navigator .of (context).pop (false );
350+ }
351+ },
352+ child: Row (
353+ mainAxisAlignment: MainAxisAlignment .center,
354+ children: [
355+ Text (
356+ words.item1[i],
357+ textAlign: TextAlign .center,
358+ style: isDesktop
359+ ? STextStyles .desktopTextExtraSmall (context).copyWith (
360+ color: Theme .of (context)
361+ .extension < StackColors > ()!
362+ .textDark,
363+ )
364+ : STextStyles .baseXS (context).copyWith (
365+ color: Theme .of (context)
366+ .extension < StackColors > ()!
367+ .textDark,
368+ ),
369+ ),
370+ ],
371+ ),
372+ ),
373+ )
374+ ],
375+ ),
376+ ),
377+ ],
378+ )
379+ ],
380+ ),
381+ ),
382+ );
383+ }) ?? false ;
384+ if (hasVerified) {
385+ if (mounted) {
386+ final wallet = await showLoading (
387+ whileFuture: (() async {
388+ await newWallet.info.setMnemonicVerified (isar: ref
389+ .read (mainDBProvider)
390+ .isar);
391+ ref.read (pWallets).addWallet (newWallet);
392+ await newWallet.open ();
393+ await newWallet.generateNewReceivingAddress ();
394+ return results.coin.importPaperWallet (results, ref, newWallet: newWallet);
395+ })(),
396+ context: context,
397+ message: "Importing paper wallet..." ,
398+ );
399+ if (wallet == null ) {
400+ if (mounted) {
401+ ScaffoldMessenger .of (context).showSnackBar (
402+ SnackBar (
403+ content: Text (
404+ "Failed to import paper wallet for ${results .coin .prettyName }. Please try again." ,
405+ ),
406+ ),
407+ );
408+ }
409+ return ;
410+ }
411+ if (mounted) {
412+ Navigator .pop (context);
413+ await Navigator .of (context).pushNamed (
414+ WalletView .routeName,
415+ arguments: wallet.walletId,
416+ );
417+ }
418+ }
419+ } else {
420+ if (mounted) {
421+ ScaffoldMessenger .of (context).showSnackBar (
422+ const SnackBar (
423+ content: Text (
424+ "Mnemonic verification failed. Please try again." ,
425+ ),
426+ ),
427+ );
428+ }
429+ }
430+ }
431+ }
153432 }
433+ } else {
154434 if (mounted) {
155- Navigator . pop (context);
156- await Navigator . of (context). pushNamed (
157- WalletView .routeName ,
158- arguments : wallet! .walletId ,
435+ final wallet = await showLoading (
436+ whileFuture : results.coin. importPaperWallet (results, ref),
437+ context : context ,
438+ message : "Importing paper wallet..." ,
159439 );
440+ if (wallet == null ) {
441+ if (mounted) {
442+ ScaffoldMessenger .of (context).showSnackBar (
443+ SnackBar (
444+ content: Text (
445+ "Failed to import paper wallet for ${results .coin .prettyName }. Please try again." ,
446+ ),
447+ ),
448+ );
449+ }
450+ return ;
451+ }
452+ if (mounted) {
453+ Navigator .pop (context);
454+ await Navigator .of (context).pushNamed (
455+ WalletView .routeName,
456+ arguments: wallet.walletId,
457+ );
458+ }
160459 }
161460 }
162461 }
0 commit comments