diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 103a5729d..eb102bfe7 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -22,6 +22,7 @@ import '../../../utilities/address_utils.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/address_private_key.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -30,6 +31,7 @@ import '../../../widgets/custom_buttons/simple_copy_button.dart'; import '../../../widgets/custom_buttons/simple_edit_button.dart'; import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/detail_item.dart'; import '../../../widgets/qr.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/transaction_card.dart'; @@ -298,9 +300,9 @@ class _AddressDetailsViewState extends ConsumerState { const SizedBox( height: 16, ), - _Item( + DetailItem( title: "Address", - data: address.value, + detail: address.value, button: isDesktop ? IconCopyButton( data: address.value, @@ -312,9 +314,9 @@ class _AddressDetailsViewState extends ConsumerState { const _Div( height: 12, ), - _Item( + DetailItem( title: "Label", - data: label!.value, + detail: label!.value, button: SimpleEditButton( editValue: label!.value, editLabel: 'label', @@ -338,9 +340,9 @@ class _AddressDetailsViewState extends ConsumerState { height: 12, ), if (address.derivationPath != null) - _Item( + DetailItem( title: "Derivation path", - data: address.derivationPath!.value, + detail: address.derivationPath!.value, button: Container(), ), if (address.type == AddressType.spark) @@ -348,27 +350,34 @@ class _AddressDetailsViewState extends ConsumerState { height: 12, ), if (address.type == AddressType.spark) - _Item( + DetailItem( title: "Diversifier", - data: address.derivationIndex.toString(), + detail: address.derivationIndex.toString(), button: Container(), ), const _Div( height: 12, ), - _Item( + DetailItem( title: "Type", - data: address.type.readableName, + detail: address.type.readableName, button: Container(), ), const _Div( height: 12, ), - _Item( + DetailItem( title: "Sub type", - data: address.subType.prettyName, + detail: address.subType.prettyName, button: Container(), ), + const _Div( + height: 12, + ), + AddressPrivateKey( + walletId: widget.walletId, + address: address, + ), if (!isDesktop) const SizedBox( height: 20, @@ -631,64 +640,3 @@ class _Tags extends StatelessWidget { ); } } - -class _Item extends StatelessWidget { - const _Item({ - super.key, - required this.title, - required this.data, - required this.button, - }); - - final String title; - final String data; - final Widget button; - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => RoundedWhiteContainer( - child: child, - ), - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), - button, - ], - ), - const SizedBox( - height: 5, - ), - data.isNotEmpty - ? SelectableText( - data, - style: STextStyles.w500_14(context), - ) - : Text( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 6d87b7ace..3d603fc03 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bip39/bip39.dart' as bip39; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; @@ -6,6 +8,7 @@ import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/extensions/extensions.dart'; import '../../crypto_currency/intermediate/bip39_hd_currency.dart'; import '../wallet_mixin_interfaces/multi_address_interface.dart'; import 'bip39_wallet.dart'; @@ -28,6 +31,22 @@ abstract class Bip39HDWallet extends Bip39Wallet return coinlib.HDPrivateKey.fromSeed(seed); } + Future getPrivateKeyWIF(Address address) async { + final keys = + (await getRootHDNode()).derivePath(address.derivationPath!.value); + + final List data = [ + cryptoCurrency.networkParams.wifPrefix, + ...keys.privateKey.data, + if (keys.privateKey.compressed) 1, + ]; + final checksum = + coinlib.sha256DoubleHash(Uint8List.fromList(data)).sublist(0, 4); + data.addAll(checksum); + + return Uint8List.fromList(data).toBase58Encoded; + } + Future
generateNextReceivingAddress({ required DerivePathType derivePathType, }) async { diff --git a/lib/widgets/address_private_key.dart b/lib/widgets/address_private_key.dart new file mode 100644 index 000000000..8bcbd404d --- /dev/null +++ b/lib/widgets/address_private_key.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../models/isar/models/isar_models.dart'; +import '../providers/global/wallets_provider.dart'; +import '../utilities/show_loading.dart'; +import '../utilities/text_styles.dart'; +import '../utilities/util.dart'; +import '../wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'custom_buttons/blue_text_button.dart'; +import 'detail_item.dart'; + +class AddressPrivateKey extends ConsumerStatefulWidget { + /// The [walletId] MUST be the id of a [Bip39HDWallet]! + const AddressPrivateKey({ + super.key, + required this.walletId, + required this.address, + }); + + final String walletId; + final Address address; + + @override + ConsumerState createState() => _AddressPrivateKeyState(); +} + +class _AddressPrivateKeyState extends ConsumerState { + String? _private; + + bool _lock = false; + + Future _loadPrivKey() async { + // sanity check that should never actually fail in practice. + // Big problems if it actually does though so we check and crash if it fails. + assert(widget.walletId == widget.address.walletId); + + if (_lock) { + return; + } + _lock = true; + + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as Bip39HDWallet; + + _private = await showLoading( + whileFuture: wallet.getPrivateKeyWIF(widget.address), + context: context, + message: "Loading...", + delay: const Duration(milliseconds: 800), + rootNavigator: Util.isDesktop, + ); + + if (context.mounted) { + setState(() {}); + } else { + _private == null; + } + } finally { + _lock = false; + } + } + + @override + Widget build(BuildContext context) { + return DetailItemBase( + button: CustomTextButton( + text: "Show", + onTap: _loadPrivKey, + enabled: _private == null, + ), + title: Text( + "Private key", + style: STextStyles.itemSubtitle(context), + ), + detail: SelectableText( + _private ?? "*" * 52, // 52 is approx length + style: STextStyles.w500_14(context), + ), + ); + } +} diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart index b3b1552a2..c619c4192 100644 --- a/lib/widgets/detail_item.dart +++ b/lib/widgets/detail_item.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../themes/stack_colors.dart'; import '../utilities/text_styles.dart'; import '../utilities/util.dart'; @@ -27,15 +28,61 @@ class DetailItem extends StatelessWidget { @override Widget build(BuildContext context) { - final TextStyle detailStyle; + TextStyle detailStyle = STextStyles.w500_14(context); + String _detail = detail; if (overrideDetailTextColor != null) { detailStyle = STextStyles.w500_14(context).copyWith( color: overrideDetailTextColor, ); - } else { - detailStyle = STextStyles.w500_14(context); } + if (detail.isEmpty && showEmptyDetail) { + _detail = "$title will appear here"; + detailStyle = detailStyle.copyWith( + color: Theme.of(context).extension()!.textSubtitle3, + ); + } + + return DetailItemBase( + horizontal: horizontal, + title: disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + detail: disableSelectableText + ? Text( + _detail, + style: detailStyle, + ) + : SelectableText( + _detail, + style: detailStyle, + ), + ); + } +} + +class DetailItemBase extends StatelessWidget { + const DetailItemBase({ + super.key, + required this.title, + required this.detail, + this.button, + this.horizontal = false, + }); + + final Widget title; + final Widget detail; + final Widget? button; + final bool horizontal; + + @override + Widget build(BuildContext context) { return ConditionalParent( condition: !Util.isDesktop, builder: (child) => RoundedWhiteContainer( @@ -51,24 +98,8 @@ class DetailItem extends StatelessWidget { ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - disableSelectableText - ? Text( - title, - style: STextStyles.itemSubtitle(context), - ) - : SelectableText( - title, - style: STextStyles.itemSubtitle(context), - ), - disableSelectableText - ? Text( - detail, - style: detailStyle, - ) - : SelectableText( - detail, - style: detailStyle, - ), + title, + detail, ], ) : Column( @@ -77,48 +108,14 @@ class DetailItem extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - disableSelectableText - ? Text( - title, - style: STextStyles.itemSubtitle(context), - ) - : SelectableText( - title, - style: STextStyles.itemSubtitle(context), - ), + title, button ?? Container(), ], ), const SizedBox( height: 5, ), - detail.isEmpty && showEmptyDetail - ? disableSelectableText - ? Text( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), - ) - : SelectableText( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), - ) - : disableSelectableText - ? Text( - detail, - style: detailStyle, - ) - : SelectableText( - detail, - style: detailStyle, - ), + detail, ], ), ),