From fe5458928fe1889ffaf9a9a9c9f5ab1e7653ad2d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jul 2024 13:55:11 -0600 Subject: [PATCH] mobile xprivs --- .../wallet_backup_view.dart | 725 +++++++++++------- .../wallet_settings_view.dart | 9 + .../firo_rescan_recovery_error_dialog.dart | 9 +- lib/route_generator.dart | 57 ++ 4 files changed, 521 insertions(+), 279 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 70bbdd294..d2ec44ca6 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -13,7 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; @@ -25,8 +25,10 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/custom_buttons/simple_copy_button.dart'; import '../../../../widgets/detail_item.dart'; import '../../../../widgets/qr.dart'; @@ -42,6 +44,7 @@ class WalletBackupView extends ConsumerWidget { required this.mnemonic, this.frostWalletData, this.clipboardInterface = const ClipboardWrapper(), + this.xprivData, }); static const String routeName = "/walletBackup"; @@ -55,13 +58,13 @@ class WalletBackupView extends ConsumerWidget { ({String config, String keys})? prevGen, })? frostWalletData; final ClipboardInterface clipboardInterface; + final ({List xprivs, String fingerprint})? xprivData; @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); final bool frost = frostWalletData != null; - final prevGen = frostWalletData?.prevGen != null; return Background( child: Scaffold( @@ -77,296 +80,462 @@ class WalletBackupView extends ConsumerWidget { style: STextStyles.navBarTitle(context), ), actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context).extension()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, + if (xprivData != null) + Padding( + padding: const EdgeInsets.all(10), + child: CustomTextButton( + text: "xpriv(s)", + onTap: () { + Navigator.pushNamed( + context, + MobileXPrivsView.routeName, + arguments: ( + walletId: walletId, + xprivData: xprivData!, ), ); }, ), + + // child: AspectRatio( + // aspectRatio: 1, + // child: AppBarIconButton( + // color: + // Theme.of(context).extension()!.background, + // shadows: const [], + // icon: SvgPicture.asset( + // Assets.svg.copy, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension()! + // .topNavIconPrimary, + // ), + // onPressed: () async { + // await clipboardInterface + // .setData(ClipboardData(text: mnemonic.join(" "))); + // unawaited( + // showFloatingFlushBar( + // type: FlushBarType.info, + // message: "Copied to clipboard", + // iconAsset: Assets.svg.copy, + // context: context, + // ), + // ); + // }, + // ), + // ), + ), + if (!frost && xprivData == null) + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: + Theme.of(context).extension()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () async { + await clipboardInterface + .setData(ClipboardData(text: mnemonic.join(" "))); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + ), + ), ), - ), ], ), body: Padding( padding: const EdgeInsets.all(16), child: frost - ? LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "Please write down your backup data. Keep it safe and " - "never share it with anyone. " - "Your backup data is the only way you can access your " - "funds if you forget your PIN, lose your phone, etc." - "\n\n" - "${AppConfig.appName} does not keep nor is able to restore " - "your backup data. " - "Only you have access to your wallet.", - style: STextStyles.label(context), - ), - ), - const SizedBox( - height: 24, - ), - // DetailItem( - // title: "My name", - // detail: frostWalletData!.myName, - // button: Util.isDesktop - // ? IconCopyButton( - // data: frostWalletData!.myName, - // ) - // : SimpleCopyButton( - // data: frostWalletData!.myName, - // ), - // ), - // const SizedBox( - // height: 16, - // ), - DetailItem( - title: "Multisig config", - detail: frostWalletData!.config, - button: Util.isDesktop - ? IconCopyButton( - data: frostWalletData!.config, - ) - : SimpleCopyButton( - data: frostWalletData!.config, - ), - ), - const SizedBox( - height: 16, - ), - DetailItem( - title: "Keys", - detail: frostWalletData!.keys, - button: Util.isDesktop - ? IconCopyButton( - data: frostWalletData!.keys, - ) - : SimpleCopyButton( - data: frostWalletData!.keys, - ), - ), - if (prevGen) - const SizedBox( - height: 24, - ), - if (prevGen) - RoundedWhiteContainer( - child: Text( - "Previous generation info", - style: STextStyles.label(context), - ), - ), - if (prevGen) - const SizedBox( - height: 12, - ), - if (prevGen) - DetailItem( - title: "Previous multisig config", - detail: frostWalletData!.prevGen!.config, - button: Util.isDesktop - ? IconCopyButton( - data: - frostWalletData!.prevGen!.config, - ) - : SimpleCopyButton( - data: - frostWalletData!.prevGen!.config, - ), - ), - if (prevGen) - const SizedBox( - height: 16, - ), - if (prevGen) - DetailItem( - title: "Previous keys", - detail: frostWalletData!.prevGen!.keys, - button: Util.isDesktop - ? IconCopyButton( - data: frostWalletData!.prevGen!.keys, - ) - : SimpleCopyButton( - data: frostWalletData!.prevGen!.keys, - ), - ), - ], - ), - ), - ), - ); - }, + ? _FrostKeys( + frostWalletData: frostWalletData, + walletId: walletId, ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref.watch(pWalletName(walletId)), - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: - Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", - style: STextStyles.label(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, - ), - ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - final String data = - AddressUtils.encodeQRSeedData(mnemonic); - - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Recovery phrase QR code", - style: STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QR( - data: data, - size: width, - ), - ), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle( - context, - ), - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), - ), - ], + : _Mnemonic( + walletId: walletId, + mnemonic: mnemonic, ), ), ), ); } } + +class _XPrivs extends StatelessWidget { + const _XPrivs({super.key, required this.walletId, required this.xprivData}); + + final String walletId; + final ({List xprivs, String fingerprint}) xprivData; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Master fingerprint", + detail: xprivData.fingerprint, + horizontal: true, + ), + const SizedBox( + height: 16, + ), + ...xprivData.xprivs.map( + (e) => Padding( + padding: const EdgeInsets.only( + bottom: 16, + ), + child: Column( + children: [ + DetailItem( + title: e.path, + detail: e.xpriv, + ), + ], + ), + ), + ), + ], + ); + } +} + +class _Mnemonic extends ConsumerWidget { + const _Mnemonic({super.key, required this.walletId, required this.mnemonic}); + + final String walletId; + final List mnemonic; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + ref.watch(pWalletName(walletId)), + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + final String data = AddressUtils.encodeQRSeedData(mnemonic); + + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Recovery phrase QR code", + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QR( + data: data, + size: width, + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context, + ), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), + ), + ], + ); + } +} + +class _FrostKeys extends StatelessWidget { + const _FrostKeys({ + super.key, + required this.walletId, + this.frostWalletData, + }); + + static const String routeName = "/walletBackup"; + + final String walletId; + final ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData; + + @override + Widget build(BuildContext context) { + final prevGen = frostWalletData?.prevGen != null; + return LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Please write down your backup data. Keep it safe and " + "never share it with anyone. " + "Your backup data is the only way you can access your " + "funds if you forget your PIN, lose your phone, etc." + "\n\n" + "${AppConfig.appName} does not keep nor is able to restore " + "your backup data. " + "Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + const SizedBox( + height: 24, + ), + // DetailItem( + // title: "My name", + // detail: frostWalletData!.myName, + // button: Util.isDesktop + // ? IconCopyButton( + // data: frostWalletData!.myName, + // ) + // : SimpleCopyButton( + // data: frostWalletData!.myName, + // ), + // ), + // const SizedBox( + // height: 16, + // ), + DetailItem( + title: "Multisig config", + detail: frostWalletData!.config, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.config, + ) + : SimpleCopyButton( + data: frostWalletData!.config, + ), + ), + const SizedBox( + height: 16, + ), + DetailItem( + title: "Keys", + detail: frostWalletData!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.keys, + ), + ), + if (prevGen) + const SizedBox( + height: 24, + ), + if (prevGen) + RoundedWhiteContainer( + child: Text( + "Previous generation info", + style: STextStyles.label(context), + ), + ), + if (prevGen) + const SizedBox( + height: 12, + ), + if (prevGen) + DetailItem( + title: "Previous multisig config", + detail: frostWalletData!.prevGen!.config, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.prevGen!.config, + ) + : SimpleCopyButton( + data: frostWalletData!.prevGen!.config, + ), + ), + if (prevGen) + const SizedBox( + height: 16, + ), + if (prevGen) + DetailItem( + title: "Previous keys", + detail: frostWalletData!.prevGen!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.prevGen!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.prevGen!.keys, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class MobileXPrivsView extends StatelessWidget { + const MobileXPrivsView({ + super.key, + required this.walletId, + this.clipboardInterface = const ClipboardWrapper(), + required this.xprivData, + }); + + static const String routeName = "/mobileXPrivView"; + + final String walletId; + final ClipboardInterface clipboardInterface; + final ({List xprivs, String fingerprint}) xprivData; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet xpriv(s)", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: _XPrivs( + walletId: walletId, + xprivData: xprivData, + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index aca76d7f8..a9fdd52f0 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -261,6 +261,10 @@ class _WalletSettingsViewState extends ConsumerState { // TODO: [prio=med] take wallets that don't have a mnemonic into account + ({ + List xprivs, + String fingerprint + })? xprivData; List? mnemonic; ({ String myName, @@ -302,6 +306,10 @@ class _WalletSettingsViewState extends ConsumerState { await wallet.getMnemonicAsWords(); } + if (wallet is ExtendedKeysInterface) { + xprivData = await wallet.getXPrivs(); + } + if (context.mounted) { await Navigator.push( context, @@ -315,6 +323,7 @@ class _WalletSettingsViewState extends ConsumerState { mnemonic: mnemonic ?? [], frostWalletData: frostWalletData, + xprivData: xprivData, ), showBackButton: true, routeOnSuccess: diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index 3a80b0b85..6409646c6 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -11,6 +11,7 @@ import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; @@ -264,7 +265,12 @@ class _FiroRescanRecoveryErrorViewState if (wallet is MnemonicInterface) { final mnemonic = await wallet.getMnemonicAsWords(); - if (mounted) { + ({List xprivs, String fingerprint})? xprivData; + if (wallet is ExtendedKeysInterface) { + xprivData = await wallet.getXPrivs(); + } + + if (context.mounted) { await Navigator.push( context, RouteGenerator.getRoute( @@ -274,6 +280,7 @@ class _FiroRescanRecoveryErrorViewState routeOnSuccessArguments: ( walletId: widget.walletId, mnemonic: mnemonic, + xprivData: xprivData, ), showBackButton: true, routeOnSuccess: WalletBackupView.routeName, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d7b8a1563..3d07a766b 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1277,6 +1277,63 @@ class RouteGenerator { name: settings.name, ), ); + } else if (args is ({ + String walletId, + List mnemonic, + ({List xprivs, String fingerprint})? xprivData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + xprivData: args.xprivData, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } else if (args is ({ + String walletId, + List mnemonic, + ({List xprivs, String fingerprint})? xprivData, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + frostWalletData: args.frostWalletData, + xprivData: args.xprivData, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case MobileXPrivsView.routeName: + if (args is ({ + String walletId, + ({List xprivs, String fingerprint}) xprivData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MobileXPrivsView( + walletId: args.walletId, + xprivData: args.xprivData, + ), + settings: RouteSettings( + name: settings.name, + ), + ); } return _routeError("${settings.name} invalid args: ${args.toString()}");