From cae27b3835374879284a98b33c4c864cb6954f75 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jul 2024 10:32:20 -0600 Subject: [PATCH] display xprivs along side mnemonic on backup screen --- .../unlock_wallet_keys_desktop.dart | 2 + .../wallet_keys_desktop_popup.dart | 304 +++++++++++++----- lib/route_generator.dart | 2 + lib/wallets/wallet/impl/bitcoin_wallet.dart | 2 + .../wallet/impl/bitcoincash_wallet.dart | 2 + lib/wallets/wallet/impl/dash_wallet.dart | 3 +- lib/wallets/wallet/impl/dogecoin_wallet.dart | 4 +- lib/wallets/wallet/impl/ecash_wallet.dart | 2 + lib/wallets/wallet/impl/firo_wallet.dart | 2 + lib/wallets/wallet/impl/litecoin_wallet.dart | 2 + lib/wallets/wallet/impl/particl_wallet.dart | 6 +- lib/wallets/wallet/impl/peercoin_wallet.dart | 7 +- .../extended_keys_interface.dart | 79 +++++ 13 files changed, 326 insertions(+), 91 deletions(-) create mode 100644 lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 24dbe3150..267a4e6b6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -105,6 +105,7 @@ class _UnlockWalletKeysDesktopState WalletKeysDesktopPopup.routeName, arguments: ( mnemonic: words ?? [], + walletId: widget.walletId, frostData: frostData, ), ); @@ -344,6 +345,7 @@ class _UnlockWalletKeysDesktopState WalletKeysDesktopPopup.routeName, arguments: ( mnemonic: words ?? [], + walletId: widget.walletId, frostData: frostData, ), ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart index df9252fe5..aabba6c58 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -12,38 +12,46 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart'; +import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; +import '../../../../widgets/custom_tab_view.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/detail_item.dart'; +import '../../../../widgets/loading_indicator.dart'; import '../../../../widgets/rounded_white_container.dart'; import 'qr_code_desktop_popup_content.dart'; -class WalletKeysDesktopPopup extends StatelessWidget { +class WalletKeysDesktopPopup extends ConsumerWidget { const WalletKeysDesktopPopup({ super.key, required this.words, + required this.walletId, this.frostData, this.clipboardInterface = const ClipboardWrapper(), }); final List words; + final String walletId; final ({String keys, String config})? frostData; final ClipboardInterface clipboardInterface; static const String routeName = "walletKeysDesktopPopup"; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return DesktopDialog( maxWidth: 614, maxHeight: double.infinity, @@ -168,94 +176,26 @@ class WalletKeysDesktopPopup extends StatelessWidget { ), ], ) - : Column( - children: [ - Text( - "Recovery phrase", - style: STextStyles.desktopTextMedium(context), - ), - const SizedBox( - height: 8, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, + : (ref.watch(pWallets).getWallet(walletId) + is ExtendedKeysInterface) + ? CustomTabView( + titles: const ["Mnemonic", "XPriv(s)"], + children: [ + Padding( + padding: const EdgeInsets.only(top: 16), + child: _Mnemonic( + words: words, + ), ), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, + _MasterSeedPrivateKey( + words: words, + walletId: walletId, ), - ), + ], + ) + : _Mnemonic( + words: words, ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: MnemonicTable( - words: words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Show QR code", - onPressed: () { - // TODO: address utils - final String value = - AddressUtils.encodeQRSeedData(words); - Navigator.of(context).pushNamed( - QRCodeDesktopPopupContent.routeName, - arguments: value, - ); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Copy", - onPressed: () async { - await clipboardInterface.setData( - ClipboardData(text: words.join(" ")), - ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ), - ); - } - }, - ), - ), - ], - ), - ), - ], - ), const SizedBox( height: 32, ), @@ -264,3 +204,193 @@ class WalletKeysDesktopPopup extends StatelessWidget { ); } } + +class _Mnemonic extends StatelessWidget { + const _Mnemonic({ + super.key, + required this.words, + this.clipboardInterface = const ClipboardWrapper(), + }); + + final List words; + final ClipboardInterface clipboardInterface; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and " + "save it to keep your funds secure. You will also be asked to" + " verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: + Theme.of(context).extension()!.buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + // TODO: address utils + final String value = AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + }, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _MasterSeedPrivateKey extends ConsumerStatefulWidget { + const _MasterSeedPrivateKey({ + super.key, + required this.words, + required this.walletId, + this.clipboardInterface = const ClipboardWrapper(), + }); + + final List words; + final String walletId; + final ClipboardInterface clipboardInterface; + + @override + ConsumerState<_MasterSeedPrivateKey> createState() => + _MasterSeedPrivateKeyState(); +} + +class _MasterSeedPrivateKeyState extends ConsumerState<_MasterSeedPrivateKey> { + final controller = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox( + height: 12, + ), + FutureBuilder( + future: (ref.read(pWallets).getWallet(widget.walletId) + as ExtendedKeysInterface) + .getXPrivs(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Master fingerprint", + detail: snapshot.data!.fingerprint, + ), + ...snapshot.data!.xprivs.map( + (e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Derivation", + detail: e.path, + ), + DetailItem( + title: "xpriv", + detail: e.xpriv, + ), + ], + ), + ), + ), + ], + ); + } else { + return const LoadingIndicator( + width: 100, + height: 100, + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index be011c90f..9e70573bb 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2293,11 +2293,13 @@ class RouteGenerator { case WalletKeysDesktopPopup.routeName: if (args is ({ List mnemonic, + String walletId, ({String keys, String config})? frostData })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, + walletId: args.walletId, frostData: args.frostData, ), RouteSettings( diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index cbe49e180..5c7f3ed75 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -8,12 +8,14 @@ import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/cpfp_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; import '../wallet_mixin_interfaces/paynym_interface.dart'; import '../wallet_mixin_interfaces/rbf_interface.dart'; class BitcoinWallet extends Bip39HDWallet with ElectrumXInterface, + ExtendedKeysInterface, CoinControlInterface, PaynymInterface, RbfInterface, diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index b00f650f2..3c8fb58bc 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -19,11 +19,13 @@ import '../wallet_mixin_interfaces/bcash_interface.dart'; import '../wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class BitcoincashWallet extends Bip39HDWallet with ElectrumXInterface, + ExtendedKeysInterface, BCashInterface, CoinControlInterface, CashFusionInterface { diff --git a/lib/wallets/wallet/impl/dash_wallet.dart b/lib/wallets/wallet/impl/dash_wallet.dart index 8138b684b..bf1e91b9a 100644 --- a/lib/wallets/wallet/impl/dash_wallet.dart +++ b/lib/wallets/wallet/impl/dash_wallet.dart @@ -12,9 +12,10 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class DashWallet extends Bip39HDWallet - with ElectrumXInterface, CoinControlInterface { + with ElectrumXInterface, ExtendedKeysInterface, CoinControlInterface { DashWallet(CryptoCurrencyNetwork network) : super(Dash(network) as T); @override diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index 2c2dcd309..8d2d2f029 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -13,9 +13,11 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class DogecoinWallet - extends Bip39HDWallet with ElectrumXInterface, CoinControlInterface { + extends Bip39HDWallet + with ElectrumXInterface, ExtendedKeysInterface, CoinControlInterface { DogecoinWallet(CryptoCurrencyNetwork network) : super(Dogecoin(network) as T); @override diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index 53c0f895b..968ea72cc 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -19,10 +19,12 @@ import '../wallet_mixin_interfaces/bcash_interface.dart'; import '../wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class EcashWallet extends Bip39HDWallet with ElectrumXInterface, + ExtendedKeysInterface, BCashInterface, CoinControlInterface, CashFusionInterface { diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 61c9346f7..33ae3fb24 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -22,6 +22,7 @@ import '../../models/tx_data.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; import '../wallet_mixin_interfaces/lelantus_interface.dart'; import '../wallet_mixin_interfaces/spark_interface.dart'; @@ -30,6 +31,7 @@ const sparkStartBlock = 819300; // (approx 18 Jan 2024) class FiroWallet extends Bip39HDWallet with ElectrumXInterface, + ExtendedKeysInterface, LelantusInterface, SparkInterface, CoinControlInterface { diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index 63c9d7420..d301d0608 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -15,6 +15,7 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; import '../wallet_mixin_interfaces/ordinals_interface.dart'; import '../wallet_mixin_interfaces/rbf_interface.dart'; @@ -22,6 +23,7 @@ class LitecoinWallet extends Bip39HDWallet with ElectrumXInterface, + ExtendedKeysInterface, CoinControlInterface, RbfInterface, OrdinalsInterface { diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 0ce72b393..567700e6f 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -19,10 +19,14 @@ import '../../models/tx_data.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class ParticlWallet extends Bip39HDWallet - with ElectrumXInterface, CoinControlInterface { + with + ElectrumXInterface, + ExtendedKeysInterface, + CoinControlInterface { @override int get isarTransactionVersion => 2; diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 2567e841d..e1d993584 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -1,4 +1,5 @@ import 'package:isar/isar.dart'; + import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -11,10 +12,14 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; class PeercoinWallet extends Bip39HDWallet - with ElectrumXInterface, CoinControlInterface { + with + ElectrumXInterface, + ExtendedKeysInterface, + CoinControlInterface { @override int get isarTransactionVersion => 2; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart new file mode 100644 index 000000000..5101b6a45 --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart @@ -0,0 +1,79 @@ +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import 'electrumx_interface.dart'; + +typedef XPub = ({String path, String xpub}); +typedef XPriv = ({String path, String xpriv}); + +mixin ExtendedKeysInterface + on ElectrumXInterface { + Future<({List xpubs, String fingerprint})> getXPubs() async { + final paths = cryptoCurrency.supportedDerivationPathTypes.map( + (e) => ( + path: e, + addressType: e.getAddressType(), + ), + ); + + final master = await getRootHDNode(); + final fingerprint = master.fingerprint.toRadixString(16); + + final futures = paths.map((e) async { + String path = cryptoCurrency.constructDerivePath( + derivePathType: e.path, + chain: 0, + index: 0, + ); + // trim chain and address index + path = path.substring(0, path.lastIndexOf("'") + 1); + final node = master.derivePath(path); + + return ( + path: path, + xpub: node.hdPublicKey.encode( + cryptoCurrency.networkParams.pubHDPrefix, + // 0x04b24746, + ), + ); + }); + + return ( + fingerprint: fingerprint, + xpubs: await Future.wait(futures), + ); + } + + Future<({List xprivs, String fingerprint})> getXPrivs() async { + final paths = cryptoCurrency.supportedDerivationPathTypes.map( + (e) => ( + path: e, + addressType: e.getAddressType(), + ), + ); + + final master = await getRootHDNode(); + final fingerprint = master.fingerprint.toRadixString(16); + + final futures = paths.map((e) async { + String path = cryptoCurrency.constructDerivePath( + derivePathType: e.path, + chain: 0, + index: 0, + ); + // trim chain and address index + path = path.substring(0, path.lastIndexOf("'") + 1); + final node = master.derivePath(path); + + return ( + path: path, + xpriv: node.encode( + cryptoCurrency.networkParams.privHDPrefix, + ), + ); + }); + + return ( + fingerprint: fingerprint, + xprivs: await Future.wait(futures), + ); + } +}