From 547fae891a7646821886ae9cef0ff5949c28762c Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:05:26 +0300 Subject: [PATCH] show Bitcoin address derivation path (#3177) * show Bitcoin address derivation path * Revert "show Bitcoin address derivation path" * Update cw_bitcoin/lib/bitcoin_address_record.dart [skip ci] * Update cw_bitcoin/lib/bitcoin_address_record.dart [skip ci] --------- Co-authored-by: Omar Hatem --- assets/images/info_icon.svg | 21 +++++++++ cw_bitcoin/lib/bitcoin_address_record.dart | 45 ++++++++++++++++++ lib/bitcoin/cw_bitcoin.dart | 9 ++-- lib/di.dart | 6 +++ lib/new-ui/pages/addresses_page.dart | 10 ++++ .../widgets/addresses_page/address_info.dart | 46 +++++++++++++++++++ ...let_address_edit_or_create_view_model.dart | 3 ++ .../wallet_address_list_item.dart | 2 + .../wallet_address_list_view_model.dart | 5 +- tool/configure.dart | 7 ++- 10 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 assets/images/info_icon.svg create mode 100644 lib/new-ui/widgets/addresses_page/address_info.dart diff --git a/assets/images/info_icon.svg b/assets/images/info_icon.svg new file mode 100644 index 000000000..31518cd17 --- /dev/null +++ b/assets/images/info_icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 400e325be..d6de05051 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -54,6 +54,8 @@ abstract class BaseBitcoinAddressRecord { BitcoinAddressType type; + String get derivationPath; + String toJSON(); } @@ -118,6 +120,31 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String? scriptHash; + static int _purposeForType(BitcoinAddressType type) { + if (type == P2pkhAddressType.p2pkh) return 44; + if (type == P2shAddressType.p2wpkhInP2sh) return 49; + if (type == SegwitAddresType.p2wsh) return 48; + if (type == SegwitAddresType.p2tr) return 86; + return 84; + } + + int _coinTypeForNetwork() { + if (!(network?.isMainnet ?? true)) return 1; + + switch (network) { + case BitcoinNetwork.mainnet: + return 0; + case LitecoinNetwork.mainnet: + return 2; + case BitcoinCashNetwork.mainnet: + return 145; + case DogecoinNetwork.mainnet: + return 3; + default: + return 0; + } + } + String getScriptHash(BasedUtxoNetwork network) { if (scriptHash != null) return scriptHash!; try { @@ -127,6 +154,21 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { } return scriptHash!; } + @override + String get derivationPath { + if (type == SegwitAddresType.mweb) { + return "m/1000'/$index"; + } + + final coinType = _coinTypeForNetwork(); + final purpose = _purposeForType(type); + final accountPath = isLegacyDerivation + ? electrum_path + : "m/$purpose'/$coinType'/0'"; + + final chain = isHidden ? 1 : 0; + return "$accountPath/$chain/$index"; + } @override String toJSON() => json.encode({ @@ -186,6 +228,9 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { final String? silentPaymentTweak; final String spendDerivationPath; + @override + String get derivationPath => spendDerivationPath; + @override String toJSON() => json.encode({ 'address': address, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 62db2a63e..6f5b9bfa4 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -185,7 +185,8 @@ class CWBitcoin extends Bitcoin { txCount: addr.txCount, balance: addr.balance, isChange: addr.isHidden, - isLegacyDerivation: addr.isLegacyDerivation)) + isLegacyDerivation: addr.isLegacyDerivation, + derivationPath: addr.derivationPath)) .toList(); } @@ -574,7 +575,8 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isHidden, + derivationPath: addr.derivationPath)) .toList(); } @@ -589,7 +591,8 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isHidden, + derivationPath: addr.derivationPath)) .toList(); } diff --git a/lib/di.dart b/lib/di.dart index 715c88660..7a8a8185a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -310,6 +310,7 @@ import 'buy/kryptonim/kryptonim.dart'; import 'buy/meld/meld_buy_provider.dart'; import 'dogecoin/dogecoin.dart'; import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; +import 'new-ui/widgets/addresses_page/address_info.dart'; import 'src/screens/buy/buy_sell_page.dart'; final getIt = GetIt.instance; @@ -880,6 +881,11 @@ Future setup({ walletAddressEditOrCreateViewModel: getIt.get(param1: item))); + getIt.registerFactoryParam((dynamic item, _) => + AddressInfoPopup( + walletAddressEditOrCreateViewModel: + getIt.get(param1: item))); + getIt.registerFactoryParam((dynamic item, _) => ReceiveLabelModal( walletAddressEditOrCreateViewModel: diff --git a/lib/new-ui/pages/addresses_page.dart b/lib/new-ui/pages/addresses_page.dart index a7a81d282..9016116d0 100644 --- a/lib/new-ui/pages/addresses_page.dart +++ b/lib/new-ui/pages/addresses_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/new-ui/long_press_popup.dart'; +import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart'; import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart'; import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart'; @@ -355,6 +356,15 @@ class AddressRow extends StatelessWidget { onAddressHidden(); }, color: Theme.of(context).colorScheme.error), + LongPressMenuItem( + label: 'Info', + iconPath: "assets/images/info_icon.svg", + onSelected: () async { + Navigator.of(context, rootNavigator: true).pop(); + await showPopUp( + context: context, + builder: (context) => getIt.get(param1: item)); + }), ], ), child: AnimatedContainer( diff --git a/lib/new-ui/widgets/addresses_page/address_info.dart b/lib/new-ui/widgets/addresses_page/address_info.dart new file mode 100644 index 000000000..310b3d4e4 --- /dev/null +++ b/lib/new-ui/widgets/addresses_page/address_info.dart @@ -0,0 +1,46 @@ +import 'dart:ui'; + +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:flutter/material.dart'; + +class AddressInfoPopup extends StatelessWidget { + const AddressInfoPopup({ + super.key, + required this.walletAddressEditOrCreateViewModel, + }); + + final WalletAddressEditOrCreateViewModel walletAddressEditOrCreateViewModel; + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadiusGeometry.lerp( + BorderRadius.circular(12), + BorderRadius.circular(24), + 0.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text('Index: ${walletAddressEditOrCreateViewModel.index}'), + SizedBox(height: 16), + Text( + 'Derivation Path: ${walletAddressEditOrCreateViewModel.derivationPath}'), + ], + ), + ), + ), + ]), + ); + } +} diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 269b93241..52c5954d8 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -51,6 +51,9 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { _wallet.type == WalletType.litecoin || _wallet.type == WalletType.dogecoin; + String get derivationPath => _item?.derivationPath ?? ''; + String get index => _item?.id.toString() ?? ''; + Future save() async { try { state = AddressIsSaving(); diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 2c5b0a3d9..97309173d 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -14,6 +14,7 @@ class WalletAddressListItem extends ListItem { this.isHidden = false, this.isManual = false, this.isLegacyDerivation = false, + this.derivationPath }) : super(); final int? id; @@ -26,6 +27,7 @@ class WalletAddressListItem extends ListItem { bool isHidden; bool isManual; bool isLegacyDerivation; + String? derivationPath; final bool? isOneTimeReceiveAddress; @override diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index a517cc24b..4fc003077 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -258,6 +258,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo balance: _appStore.amountParsingProxy .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)), isChange: address.isChange, + derivationPath: address.derivationPath, ); }); addressList.addAll(addressItems); @@ -275,6 +276,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)), isChange: address.isChange, isOneTimeReceiveAddress: true, + derivationPath: address.derivationPath, ); }); addressList.addAll(receivedAddressItems); @@ -291,7 +293,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo balance: _appStore.amountParsingProxy .getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)), isChange: subaddress.isChange, - isLegacyDerivation: subaddress.isLegacyDerivation); + isLegacyDerivation: subaddress.isLegacyDerivation, + derivationPath: subaddress.derivationPath); }); // don't show all 1000+ mweb addresses: diff --git a/tool/configure.dart b/tool/configure.dart index 8e8da7183..c6ee96bc6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -161,7 +161,7 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart"; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ - class ElectrumSubAddress { +class ElectrumSubAddress { ElectrumSubAddress({ required this.id, required this.name, @@ -169,13 +169,16 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart"; required this.txCount, required this.balance, required this.isChange, - this.isLegacyDerivation = false}); + this.derivationPath, + this.isLegacyDerivation = false + }); final int id; final String name; final String address; final int txCount; final int balance; final bool isChange; + final String? derivationPath; final bool isLegacyDerivation; }