Skip to content

Commit 2a6349c

Browse files
committed
feat: gift mnemonic confirmation
1 parent e4d259f commit 2a6349c

File tree

4 files changed

+442
-141
lines changed

4 files changed

+442
-141
lines changed

lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart

Lines changed: 312 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
*/
1010

1111
import 'dart:async';
12+
import 'dart:math';
1213

1314
import 'package:barcode_scan2/barcode_scan2.dart';
15+
import 'package:flutter/foundation.dart';
1416
import 'package:flutter/material.dart';
1517
import 'package:flutter/services.dart';
1618
import 'package:flutter_riverpod/flutter_riverpod.dart';
1719
import 'package:flutter_svg/svg.dart';
1820
import 'package:isar/isar.dart';
21+
import 'package:tuple/tuple.dart';
1922

2023
import '../../../app_config.dart';
2124
import '../../../db/isar/main_db.dart';
@@ -24,6 +27,7 @@ import '../../../models/add_wallet_list_entity/sub_classes/coin_entity.dart';
2427
import '../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
2528
import '../../../models/isar/models/ethereum/eth_contract.dart';
2629
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
30+
import '../../../providers/global/secure_store_provider.dart';
2731
import '../../../providers/providers.dart';
2832
import '../../../themes/stack_colors.dart';
2933
import '../../../utilities/address_utils.dart';
@@ -36,12 +40,16 @@ import '../../../utilities/show_loading.dart';
3640
import '../../../utilities/text_styles.dart';
3741
import '../../../utilities/util.dart';
3842
import '../../../wallets/crypto_currency/crypto_currency.dart';
43+
import '../../../wallets/isar/models/wallet_info.dart';
44+
import '../../../wallets/wallet/impl/monero_wallet.dart';
3945
import '../../../wallets/wallet/wallet.dart';
4046
import '../../../widgets/background.dart';
4147
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
48+
import '../../../widgets/custom_buttons/blue_text_button.dart';
4249
import '../../../widgets/desktop/desktop_app_bar.dart';
4350
import '../../../widgets/desktop/desktop_dialog.dart';
4451
import '../../../widgets/desktop/desktop_scaffold.dart';
52+
import '../../../widgets/desktop/primary_button.dart';
4553
import '../../../widgets/expandable.dart';
4654
import '../../../widgets/icon_widgets/x_icon.dart';
4755
import '../../../widgets/rounded_white_container.dart';
@@ -50,6 +58,8 @@ import '../../../widgets/textfield_icon_button.dart';
5058
import '../../wallet_view/wallet_view.dart';
5159
import '../add_token_view/add_custom_token_view.dart';
5260
import '../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';
5363
import 'sub_widgets/add_wallet_text.dart';
5464
import 'sub_widgets/expanding_sub_list_item.dart';
5565
import '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

Comments
 (0)