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 <omarh.ismail1@gmail.com>

Serhii committed Apr 13, 2026 at 23:05 UTC 547fae891a7646821886ae9cef0ff5949c28762c
10 files changed +148 -6
assets/images/info_icon.svg new
+21
@@ -0,0 +1,21 @@
1 +<?xml version="1.0" encoding="iso-8859-1"?>
2 +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4 +<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
5 + width="800px" height="800px" viewBox="0 0 490.318 490.318"
6 + xml:space="preserve">
7 +<g>
8 + <g>
9 + <g>
10 + <path d="M245.148,0C109.967,0,0.009,109.98,0.009,245.162c0,135.182,109.958,245.156,245.139,245.156
11 + c135.186,0,245.162-109.978,245.162-245.156C490.31,109.98,380.333,0,245.148,0z M245.148,438.415
12 + c-106.555,0-193.234-86.698-193.234-193.253c0-106.555,86.68-193.258,193.234-193.258c106.559,0,193.258,86.703,193.258,193.258
13 + C438.406,351.717,351.706,438.415,245.148,438.415z"/>
14 + <path d="M270.036,221.352h-49.771c-8.351,0-15.131,6.78-15.131,15.118v147.566c0,8.352,6.78,15.119,15.131,15.119h49.771
15 + c8.351,0,15.131-6.77,15.131-15.119V236.471C285.167,228.133,278.387,221.352,270.036,221.352z"/>
16 + <path d="M245.148,91.168c-24.48,0-44.336,19.855-44.336,44.336c0,24.484,19.855,44.34,44.336,44.34
17 + c24.485,0,44.342-19.855,44.342-44.34C289.489,111.023,269.634,91.168,245.148,91.168z"/>
18 + </g>
19 + </g>
20 +</g>
21 +</svg>
\ No newline at end of file
cw_bitcoin/lib/bitcoin_address_record.dart
+45
@@ -54,6 +54,8 @@ abstract class BaseBitcoinAddressRecord {
54
55 BitcoinAddressType type;
56
57 + String get derivationPath;
58 +
59 String toJSON();
60 }
61
@@ -118,6 +120,31 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
120
121 String? scriptHash;
122
123 + static int _purposeForType(BitcoinAddressType type) {
124 + if (type == P2pkhAddressType.p2pkh) return 44;
125 + if (type == P2shAddressType.p2wpkhInP2sh) return 49;
126 + if (type == SegwitAddresType.p2wsh) return 48;
127 + if (type == SegwitAddresType.p2tr) return 86;
128 + return 84;
129 + }
130 +
131 + int _coinTypeForNetwork() {
132 + if (!(network?.isMainnet ?? true)) return 1;
133 +
134 + switch (network) {
135 + case BitcoinNetwork.mainnet:
136 + return 0;
137 + case LitecoinNetwork.mainnet:
138 + return 2;
139 + case BitcoinCashNetwork.mainnet:
140 + return 145;
141 + case DogecoinNetwork.mainnet:
142 + return 3;
143 + default:
144 + return 0;
145 + }
146 + }
147 +
148 String getScriptHash(BasedUtxoNetwork network) {
149 if (scriptHash != null) return scriptHash!;
150 try {
@@ -127,6 +154,21 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
154 }
155 return scriptHash!;
156 }
157 + @override
158 + String get derivationPath {
159 + if (type == SegwitAddresType.mweb) {
160 + return "m/1000'/$index";
161 + }
162 +
163 + final coinType = _coinTypeForNetwork();
164 + final purpose = _purposeForType(type);
165 + final accountPath = isLegacyDerivation
166 + ? electrum_path
167 + : "m/$purpose'/$coinType'/0'";
168 +
169 + final chain = isHidden ? 1 : 0;
170 + return "$accountPath/$chain/$index";
171 + }
172
173 @override
174 String toJSON() => json.encode({
@@ -186,6 +228,9 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
228 final String? silentPaymentTweak;
229 final String spendDerivationPath;
230
231 + @override
232 + String get derivationPath => spendDerivationPath;
233 +
234 @override
235 String toJSON() => json.encode({
236 'address': address,
lib/bitcoin/cw_bitcoin.dart
+6 -3
@@ -185,7 +185,8 @@ class CWBitcoin extends Bitcoin {
185 txCount: addr.txCount,
186 balance: addr.balance,
187 isChange: addr.isHidden,
188 - isLegacyDerivation: addr.isLegacyDerivation))
188 + isLegacyDerivation: addr.isLegacyDerivation,
189 + derivationPath: addr.derivationPath))
190 .toList();
191 }
192
@@ -574,7 +575,8 @@ class CWBitcoin extends Bitcoin {
575 address: addr.address,
576 txCount: addr.txCount,
577 balance: addr.balance,
577 - isChange: addr.isHidden))
578 + isChange: addr.isHidden,
579 + derivationPath: addr.derivationPath))
580 .toList();
581 }
582
@@ -589,7 +591,8 @@ class CWBitcoin extends Bitcoin {
591 address: addr.address,
592 txCount: addr.txCount,
593 balance: addr.balance,
592 - isChange: addr.isHidden))
594 + isChange: addr.isHidden,
595 + derivationPath: addr.derivationPath))
596 .toList();
597 }
598
lib/di.dart
+6
@@ -310,6 +310,7 @@ import 'buy/kryptonim/kryptonim.dart';
310 import 'buy/meld/meld_buy_provider.dart';
311 import 'dogecoin/dogecoin.dart';
312 import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
313 +import 'new-ui/widgets/addresses_page/address_info.dart';
314 import 'src/screens/buy/buy_sell_page.dart';
315
316 final getIt = GetIt.instance;
@@ -880,6 +881,11 @@ Future<void> setup({
881 walletAddressEditOrCreateViewModel:
882 getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
883
884 + getIt.registerFactoryParam<AddressInfoPopup, dynamic, void>((dynamic item, _) =>
885 + AddressInfoPopup(
886 + walletAddressEditOrCreateViewModel:
887 + getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
888 +
889 getIt.registerFactoryParam<ReceiveLabelModal, dynamic, void>((dynamic item, _) =>
890 ReceiveLabelModal(
891 walletAddressEditOrCreateViewModel:
lib/new-ui/pages/addresses_page.dart
+10
@@ -4,6 +4,7 @@ import 'package:cake_wallet/di.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/monero/monero.dart';
6 import 'package:cake_wallet/new-ui/long_press_popup.dart';
7 +import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart';
8 import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
9 import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
10 import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart';
@@ -355,6 +356,15 @@ class AddressRow extends StatelessWidget {
356 onAddressHidden();
357 },
358 color: Theme.of(context).colorScheme.error),
359 + LongPressMenuItem(
360 + label: 'Info',
361 + iconPath: "assets/images/info_icon.svg",
362 + onSelected: () async {
363 + Navigator.of(context, rootNavigator: true).pop();
364 + await showPopUp(
365 + context: context,
366 + builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
367 + }),
368 ],
369 ),
370 child: AnimatedContainer(
lib/new-ui/widgets/addresses_page/address_info.dart new
+46
@@ -0,0 +1,46 @@
1 +import 'dart:ui';
2 +
3 +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
4 +import 'package:flutter/material.dart';
5 +
6 +class AddressInfoPopup extends StatelessWidget {
7 + const AddressInfoPopup({
8 + super.key,
9 + required this.walletAddressEditOrCreateViewModel,
10 + });
11 +
12 + final WalletAddressEditOrCreateViewModel walletAddressEditOrCreateViewModel;
13 +
14 + @override
15 + Widget build(BuildContext context) {
16 + return BackdropFilter(
17 + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
18 + child: Column(
19 + mainAxisSize: MainAxisSize.max,
20 + mainAxisAlignment: MainAxisAlignment.spaceAround,
21 + children: [
22 + Container(
23 + decoration: BoxDecoration(
24 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
25 + borderRadius: BorderRadiusGeometry.lerp(
26 + BorderRadius.circular(12),
27 + BorderRadius.circular(24),
28 + 0.5,
29 + ),
30 + ),
31 + child: Padding(
32 + padding: const EdgeInsets.all(8.0),
33 + child: Column(
34 + children: [
35 + Text('Index: ${walletAddressEditOrCreateViewModel.index}'),
36 + SizedBox(height: 16),
37 + Text(
38 + 'Derivation Path: ${walletAddressEditOrCreateViewModel.derivationPath}'),
39 + ],
40 + ),
41 + ),
42 + ),
43 + ]),
44 + );
45 + }
46 +}
lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart
+3
@@ -51,6 +51,9 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
51 _wallet.type == WalletType.litecoin ||
52 _wallet.type == WalletType.dogecoin;
53
54 + String get derivationPath => _item?.derivationPath ?? '';
55 + String get index => _item?.id.toString() ?? '';
56 +
57 Future<void> save() async {
58 try {
59 state = AddressIsSaving();
lib/view_model/wallet_address_list/wallet_address_list_item.dart
+2
@@ -14,6 +14,7 @@ class WalletAddressListItem extends ListItem {
14 this.isHidden = false,
15 this.isManual = false,
16 this.isLegacyDerivation = false,
17 + this.derivationPath
18 }) : super();
19
20 final int? id;
@@ -26,6 +27,7 @@ class WalletAddressListItem extends ListItem {
27 bool isHidden;
28 bool isManual;
29 bool isLegacyDerivation;
30 + String? derivationPath;
31 final bool? isOneTimeReceiveAddress;
32
33 @override
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+4 -1
@@ -258,6 +258,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
258 balance: _appStore.amountParsingProxy
259 .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
260 isChange: address.isChange,
261 + derivationPath: address.derivationPath,
262 );
263 });
264 addressList.addAll(addressItems);
@@ -275,6 +276,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
276 .getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
277 isChange: address.isChange,
278 isOneTimeReceiveAddress: true,
279 + derivationPath: address.derivationPath,
280 );
281 });
282 addressList.addAll(receivedAddressItems);
@@ -291,7 +293,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
293 balance: _appStore.amountParsingProxy
294 .getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
295 isChange: subaddress.isChange,
294 - isLegacyDerivation: subaddress.isLegacyDerivation);
296 + isLegacyDerivation: subaddress.isLegacyDerivation,
297 + derivationPath: subaddress.derivationPath);
298 });
299
300 // don't show all 1000+ mweb addresses:
tool/configure.dart
+5 -2
@@ -161,7 +161,7 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart";
161 const bitcoinCwPart = "part 'cw_bitcoin.dart';";
162 const bitcoinContent = """
163
164 - class ElectrumSubAddress {
164 +class ElectrumSubAddress {
165 ElectrumSubAddress({
166 required this.id,
167 required this.name,
@@ -169,13 +169,16 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart";
169 required this.txCount,
170 required this.balance,
171 required this.isChange,
172 - this.isLegacyDerivation = false});
172 + this.derivationPath,
173 + this.isLegacyDerivation = false
174 + });
175 final int id;
176 final String name;
177 final String address;
178 final int txCount;
179 final int balance;
180 final bool isChange;
181 + final String? derivationPath;
182 final bool isLegacyDerivation;
183 }
184