CW-1157-Use-correct-derivation-paths-for-other-address-types (#2825)

* feat: use bitcoin standard derivation paths per address type * add support for Electrum derivation type in wallet * fix merge conflict * Use segwit HD for key export and simplify _hdFor * skip derivation chooser for standard scan paths * add legacy and P2SH derivation paths

Serhii committed Mar 26, 2026 at 23:46 UTC 6378e929b627bb7bc2033514f016180b4b6f346c
18 files changed +421 -135
cw_bitcoin/lib/bitcoin_address_record.dart
+25 -4
@@ -10,6 +10,7 @@ abstract class BaseBitcoinAddressRecord {
10 this.address, {
11 required this.index,
12 this.isHidden = false,
13 + this.isLegacyDerivation = false,
14 int txCount = 0,
15 int balance = 0,
16 String name = '',
@@ -26,6 +27,7 @@ abstract class BaseBitcoinAddressRecord {
27
28 final String address;
29 bool isHidden;
30 + bool isLegacyDerivation;
31 final int index;
32 int _txCount;
33 int _balance;
@@ -60,6 +62,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
62 super.address, {
63 required super.index,
64 super.isHidden = false,
65 + super.isLegacyDerivation = false,
66 super.txCount = 0,
67 super.balance = 0,
68 super.name = '',
@@ -76,21 +79,38 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
79 }
80 }
81
82 + static bool _legacyDefaultForType(BitcoinAddressType type) {
83 +
84 + // Some address types (p2wpkh, p2wsh) were historically derived from the same account/path as our new standard
85 + // but using legacy formats. For these, we default to legacy = false.
86 + if (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh) return false;
87 +
88 + // For other types (p2pkh, p2wpkh-in-p2sh, p2tr, etc.), old wallets used the non-standard
89 + return true;
90 + }
91 +
92 factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
93 final decoded = json.decode(jsonSource) as Map;
94
95 + final parsedType = decoded['type'] != null && decoded['type'] != ''
96 + ? BitcoinAddressType.values
97 + .firstWhere((type) => type.toString() == decoded['type'] as String)
98 + : SegwitAddresType.p2wpkh;
99 +
100 + final parsedIsLegacy = decoded.containsKey('isLegacyDerivation')
101 + ? (decoded['isLegacyDerivation'] as bool? ?? false)
102 + : _legacyDefaultForType(parsedType);
103 +
104 return BitcoinAddressRecord(
105 decoded['address'] as String,
106 index: decoded['index'] as int,
107 isHidden: decoded['isHidden'] as bool? ?? false,
108 + isLegacyDerivation: parsedIsLegacy,
109 isUsed: decoded['isUsed'] as bool? ?? false,
110 txCount: decoded['txCount'] as int? ?? 0,
111 name: decoded['name'] as String? ?? '',
112 balance: decoded['balance'] as int? ?? 0,
90 - type: decoded['type'] != null && decoded['type'] != ''
91 - ? BitcoinAddressType.values
92 - .firstWhere((type) => type.toString() == decoded['type'] as String)
93 - : SegwitAddresType.p2wpkh,
113 + type: parsedType,
114 scriptHash: decoded['scriptHash'] as String?,
115 network: network,
116 );
@@ -113,6 +133,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
133 'address': address,
134 'index': index,
135 'isHidden': isHidden,
136 + 'isLegacyDerivation': isLegacyDerivation,
137 'isUsed': isUsed,
138 'txCount': txCount,
139 'name': name,
cw_bitcoin/lib/bitcoin_wallet.dart
+4 -2
@@ -123,8 +123,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
123 initialChangeAddressIndex: initialChangeAddressIndex,
124 initialSilentAddresses: initialSilentAddresses,
125 initialSilentAddressIndex: initialSilentAddressIndex,
126 - mainHd: hd,
127 - sideHd: accountHD.childKey(Bip32KeyIndex(1)),
126 + mainHdByType: mainHdByType,
127 + sideHdByType: sideHdByType,
128 + legacyMainHd: mainHd,
129 + legacySideHd: sideHd,
130 network: networkParam ?? network,
131 masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
132 isHardwareWallet: walletInfo.isHardwareWallet,
cw_bitcoin/lib/bitcoin_wallet_addresses.dart
+4 -2
@@ -23,8 +23,10 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd
23 abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
24 BitcoinWalletAddressesBase(
25 WalletInfo walletInfo, {
26 - required super.mainHd,
27 - required super.sideHd,
26 + required super.mainHdByType,
27 + required super.sideHdByType,
28 + required super.legacySideHd,
29 + required super.legacyMainHd,
30 required super.network,
31 required super.isHardwareWallet,
32 required this.payjoinManager,
cw_bitcoin/lib/electrum_wallet.dart
+153 -15
@@ -115,6 +115,104 @@ abstract class ElectrumWalletBase
115 reaction((_) => syncStatus, _syncStatusReaction);
116
117 sharedPrefs.complete(SharedPreferences.getInstance());
118 +
119 + final supportedTypes = supportedAddressTypes(walletInfo.type);
120 + mainHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
121 + sideHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
122 +
123 + final isElectrumDerivation = derivationInfo.derivationType == DerivationType.electrum;
124 +
125 + final canDeriveFromSeed = _masterHD != null && currency != null;
126 +
127 + if (isElectrumDerivation) {
128 + // Electrum derivation does not follow BIP44/49/84 etc. standards
129 + for (final type in supportedTypes) {
130 + mainHdByType[type] = mainHd; // accountHD.child(0)
131 + sideHdByType[type] = sideHd; // accountHD.child(1)
132 + }
133 + } else if (canDeriveFromSeed) {
134 + final coinType = _coinTypeFor(currency);
135 +
136 + for (final type in supportedTypes) {
137 + final purpose = _purposeForType(type);
138 + final accountPath = "m/$purpose'/$coinType'/0'";
139 +
140 + mainHdByType[type] = _masterHD!.derivePath("$accountPath/0") as Bip32Slip10Secp256k1;
141 + sideHdByType[type] = _masterHD!.derivePath("$accountPath/1") as Bip32Slip10Secp256k1;
142 + }
143 + } else {
144 + // View-only wallet (xpub only)
145 + for (final type in supportedTypes) {
146 + mainHdByType[type] = mainHd;
147 + sideHdByType[type] = sideHd;
148 + }
149 + }
150 +
151 + }
152 +
153 + int _purposeForType(BitcoinAddressType type) {
154 + switch (type.value) {
155 + case 'P2PKH':
156 + return 44;
157 + case 'P2SH/P2WPKH':
158 + return 49;
159 + case 'P2WPKH':
160 + return 84;
161 + case 'P2TR':
162 + return 86;
163 + default:
164 + return 84;
165 + }
166 + }
167 +
168 + int _coinTypeFor(CryptoCurrency cur) {
169 + if (!network.isMainnet) return 1;
170 + switch (cur) {
171 + case CryptoCurrency.btc:
172 + case CryptoCurrency.tbtc:
173 + return 0;
174 + case CryptoCurrency.ltc:
175 + return 2;
176 + case CryptoCurrency.bch:
177 + return 145;
178 + case CryptoCurrency.doge:
179 + return 3;
180 + default:
181 + return 0;
182 + }
183 + }
184 +
185 + /// Returns the BIP32 account derivation path (m/purpose'/coinType'/0') for STANDARD addresses.
186 + /// For LEGACY addresses, returns the wallet's legacy derivation base (derivationInfo.derivationPath)
187 + /// which is already the account path used historically (e.g. m/0' or m/84'/0'/0').
188 + String _accountDerivationPathForRecord(BaseBitcoinAddressRecord record) {
189 +
190 + if (derivationInfo.derivationType == DerivationType.electrum) {
191 + return derivationInfo.derivationPath ?? electrum_path; // m/0'
192 + }
193 +
194 + if (record.isLegacyDerivation) {
195 + return derivationInfo.derivationPath ?? electrum_path;
196 + }
197 +
198 + final coinType = _coinTypeFor(currency);
199 + final purpose = _purposeForType(record.type);
200 + return "m/$purpose'/$coinType'/0'";
201 + }
202 +
203 + List<BitcoinAddressType> supportedAddressTypes(WalletType type) {
204 + switch (type) {
205 + case WalletType.bitcoin:
206 + return BITCOIN_ADDRESS_TYPES;
207 + case WalletType.bitcoinCash:
208 + return BITCOIN_CASH_ADDRESS_TYPES;
209 + case WalletType.dogecoin:
210 + return DOGECOIN_ADDRESS_TYPES;
211 + case WalletType.litecoin:
212 + return LITECOIN_ADDRESS_TYPES;
213 + default:
214 + return BITCOIN_ADDRESS_TYPES;
215 + }
216 }
217
218 static Bip32Slip10Secp256k1 getAccountHDWallet(
@@ -191,7 +289,10 @@ abstract class ElectrumWalletBase
289 final Bip32Slip10Secp256k1 accountHD;
290 final String? _mnemonic;
291
194 - Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
292 + late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
293 + late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
294 +
295 + Bip32Slip10Secp256k1 get mainHd => accountHD.childKey(Bip32KeyIndex(0));
296
297 Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
298
@@ -321,6 +422,9 @@ abstract class ElectrumWalletBase
422 String? wif;
423 String? privateKey;
424 String? publicKey;
425 +
426 + final hd = mainHdByType[SegwitAddresType.p2wpkh] ?? mainHd;
427 +
428 try {
429 wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
430 } catch (_) {}
@@ -330,6 +434,7 @@ abstract class ElectrumWalletBase
434 try {
435 publicKey = hd.publicKey.toHex();
436 } catch (_) {}
437 +
438 return BitcoinWalletKeys(
439 wif: wif ?? '',
440 privateKey: privateKey ?? '',
@@ -757,9 +862,7 @@ abstract class ElectrumWalletBase
862 final address = RegexUtils.addressTypeFromStr(utx.address, network);
863 ECPrivate? privkey;
864 bool? isSilentPayment = false;
760 -
761 - final hd =
762 - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
865 + final hd = _hdFor(record: utx.bitcoinAddressRecord);
866
867 if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
868 final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@@ -790,8 +893,10 @@ abstract class ElectrumWalletBase
893 pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex();
894 }
895
896 + final baseDerivationPath = _accountDerivationPathForRecord(utx.bitcoinAddressRecord);
897 +
898 final derivationPath =
794 - "${_hardenedDerivationPath(derivationInfo.derivationPath ?? electrum_path)}"
899 + "${_hardenedDerivationPath(baseDerivationPath)}"
900 "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
901 "/${utx.bitcoinAddressRecord.index}";
902 publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
@@ -1005,9 +1110,11 @@ abstract class ElectrumWalletBase
1110 isChange: true,
1111 ));
1112
1008 - // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
1113 +
1114 + // Must match the address' account root (purpose/coinType) and legacy derivation when applicable.
1115 + final changeBaseDerivationPath = _accountDerivationPathForRecord(changeAddress);
1116 final changeDerivationPath =
1010 - "${_hardenedDerivationPath(derivationInfo.derivationPath ?? "m/0'")}"
1117 + "${_hardenedDerivationPath(changeBaseDerivationPath)}"
1118 "/${changeAddress.isHidden ? "1" : "0"}"
1119 "/${changeAddress.index}";
1120 utxoDetails.publicKeys[address.pubKeyHash()] =
@@ -1897,8 +2004,11 @@ abstract class ElectrumWalletBase
2004 final addressRecord =
2005 walletAddresses.allAddresses.firstWhere((element) => element.address == address);
2006 final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
2007 +
2008 + final hd = _hdFor(record: addressRecord);
2009 +
2010 final privkey = generateECPrivate(
1901 - hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
2011 + hd: hd,
2012 index: addressRecord.index,
2013 network: network);
2014
@@ -1981,10 +2091,11 @@ abstract class ElectrumWalletBase
2091
2092 for (final utxo in unusedUtxos) {
2093 final address = RegexUtils.addressTypeFromStr(utxo.address, network);
2094 +
2095 + final hd = _hdFor(record: utxo.bitcoinAddressRecord);
2096 +
2097 final privkey = generateECPrivate(
1985 - hd: utxo.bitcoinAddressRecord.isHidden
1986 - ? walletAddresses.sideHd
1987 - : walletAddresses.mainHd,
2098 + hd: hd,
2099 index: utxo.bitcoinAddressRecord.index,
2100 network: network,
2101 );
@@ -2302,6 +2413,7 @@ abstract class ElectrumWalletBase
2413 .then((history) => history.isNotEmpty ? address.address : null);
2414 },
2415 type: type,
2416 + isLegacyDerivation: addressRecord.isLegacyDerivation,
2417 );
2418
2419 final newLength = walletAddresses.allAddresses.length;
@@ -2587,11 +2699,19 @@ abstract class ElectrumWalletBase
2699
2700 @override
2701 Future<String> signMessage(String message, {String? address = null}) async {
2590 - final index = address != null
2591 - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
2702 + final addressRecord = address != null
2703 + ? walletAddresses.allAddresses.firstWhereOrNull((addr) => addr.address == address)
2704 : null;
2593 - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
2594 - final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
2705 +
2706 + if (addressRecord != null && addressRecord.type == SegwitAddresType.p2tr) {
2707 + throw UnsupportedError("Cannot sign message with Taproot address");
2708 + }
2709 +
2710 + final hd = addressRecord != null
2711 + ? _hdFor(record: addressRecord).childKey(Bip32KeyIndex(addressRecord.index))
2712 + : mainHd;
2713 +
2714 + final priv = ECPrivate.fromHex(hd.privateKey.privKey.toHex());
2715
2716 String messagePrefix = '\x18Bitcoin Signed Message:\n';
2717 final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
@@ -2949,6 +3069,24 @@ abstract class ElectrumWalletBase
3069 syncStatus = FailedSyncStatus();
3070 }
3071 }
3072 +
3073 + Bip32Slip10Secp256k1 _hdFor({required BaseBitcoinAddressRecord record}) {
3074 + final addrType = record.type;
3075 +
3076 + if (record.isLegacyDerivation) {
3077 + if (record.isHidden) {
3078 + return walletAddresses.legacySideHd;
3079 + } else {
3080 + return walletAddresses.legacyMainHd;
3081 + }
3082 + }
3083 +
3084 + if (record.isHidden) {
3085 + return sideHdByType[addrType] ?? sideHd;
3086 + } else {
3087 + return mainHdByType[addrType] ?? mainHd;
3088 + }
3089 + }
3090 }
3091
3092 class ScanNode {
cw_bitcoin/lib/electrum_wallet_addresses.dart
+167 -85
@@ -46,8 +46,10 @@ const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
46 abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
47 ElectrumWalletAddressesBase(
48 WalletInfo walletInfo, {
49 - required this.mainHd,
50 - required this.sideHd,
49 + required this.mainHdByType,
50 + required this.sideHdByType,
51 + required this.legacyMainHd,
52 + required this.legacySideHd,
53 required this.network,
54 required this.isHardwareWallet,
55 List<BitcoinAddressRecord>? initialAddresses,
@@ -140,8 +142,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
142 // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
143 final ObservableList<BitcoinAddressRecord> mwebAddresses;
144 final BasedUtxoNetwork network;
143 - final Bip32Slip10Secp256k1 mainHd;
144 - final Bip32Slip10Secp256k1 sideHd;
145 + Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
146 + Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
147 + final Bip32Slip10Secp256k1 legacyMainHd;
148 + final Bip32Slip10Secp256k1 legacySideHd;
149 final bool isHardwareWallet;
150 final LightningWallet? lightningWallet;
151
@@ -178,14 +182,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
182 }
183
184 if (addressPageType == LightningAddressType.p2l) {
181 - return lightningAddress ??
182 - "Error: Unable to fetch your Lightning address, please check your network connection.";
185 + return lightningAddress ??
186 + "Error: Unable to fetch your Lightning address, please check your network connection.";
187 }
188
185 - final typeMatchingAddresses =
186 - _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
187 - final typeMatchingReceiveAddresses =
188 - typeMatchingAddresses.where((addr) => !addr.isUsed).toList();
189 + final typeMatchingAddressesAll = _addresses
190 + .where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr))
191 + .toList();
192 +
193 + // Prefer standard derivation addresses for the current/active address,
194 + // but keep legacy addresses present in the overall address lists.
195 + final typeMatchingAddresses = <BitcoinAddressRecord>[
196 + ...typeMatchingAddressesAll.where((a) => !a.isLegacyDerivation),
197 + ...typeMatchingAddressesAll.where((a) => a.isLegacyDerivation),
198 + ];
199 +
200 + final typeMatchingReceiveAddressesAll =
201 + typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList();
202 + final typeMatchingReceiveAddresses = <BitcoinAddressRecord>[
203 + ...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation),
204 + ...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation),
205 + ];
206
207 if (!isEnabledAutoGenerateSubaddress) {
208 if (previousAddressRecord != null && previousAddressRecord!.type == addressPageType) {
@@ -207,7 +224,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
224 if (locked != null) return locked;
225
226 final prev = previousAddressRecord;
210 - if (prev != null && prev.type == addressPageType && !prev.isUsed) {
227 + if (prev != null && prev.type == addressPageType && !prev.isUsed && !prev.isLegacyDerivation) {
228 return prev.address;
229 }
230
@@ -235,7 +252,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
252 }
253 try {
254 final addressRecord = _addresses.firstWhere(
238 - (addressRecord) => addressRecord.address == addr,
255 + (addressRecord) => addressRecord.address == addr && !addressRecord.isLegacyDerivation,
256 + orElse: () => _addresses.firstWhere((r) => r.address == addr),
257 );
258
259 lockedReceiveAddressByType.remove(addressPageType);
@@ -263,7 +281,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
281 }
282
283 @override
266 - String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType);
284 + String get primaryAddress {
285 + if (addressPageType == SilentPaymentsAddresType.p2sp) {
286 + return silentAddress?.toString() ?? '';
287 + }
288 +
289 + final mainHd = mainHdByType[addressPageType] ?? mainHdByType.values.first;
290 + return getAddress(index: 0, hd: mainHd, addressType: addressPageType);
291 + }
292
293 Map<String, int> currentReceiveAddressIndexByType;
294
@@ -302,26 +327,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
327 return acc;
328 });
329
305 - @override
306 - Future<void> init() async {
307 - if (walletInfo.type == WalletType.bitcoinCash) {
308 - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
309 - } else if (walletInfo.type == WalletType.litecoin) {
310 - await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
311 - if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
312 - await _generateInitialAddresses(type: SegwitAddresType.mweb);
313 - }
314 - } else if (walletInfo.type == WalletType.dogecoin) {
315 - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
316 - } else if (walletInfo.type == WalletType.bitcoin) {
317 - await _generateInitialAddresses();
318 - if (!isHardwareWallet) {
330 + @override
331 + Future<void> init() async {
332 + if (walletInfo.type == WalletType.bitcoinCash) {
333 + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
334 + } else if (walletInfo.type == WalletType.litecoin) {
335 + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
336 + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
337 + await _generateInitialAddresses(type: SegwitAddresType.mweb);
338 + }
339 + } else if (walletInfo.type == WalletType.dogecoin) {
340 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
320 - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
321 - await _generateInitialAddresses(type: SegwitAddresType.p2tr);
322 - await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
341 + } else if (walletInfo.type == WalletType.bitcoin) {
342 + await _generateInitialAddresses(isLegacyDerivation: true);
343 + await _generateInitialAddresses();
344 + if (!isHardwareWallet) {
345 + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh, isLegacyDerivation: true);
346 + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
347 +
348 + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
349 + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
350 +
351 + await _generateInitialAddresses(type: SegwitAddresType.p2tr, isLegacyDerivation: true);
352 + await _generateInitialAddresses(type: SegwitAddresType.p2tr);
353 +
354 + await _generateInitialAddresses(type: SegwitAddresType.p2wsh, isLegacyDerivation: true);
355 + await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
356 + }
357 }
324 - }
358
359 updateAddressesByMatch();
360 updateReceiveAddresses();
@@ -409,10 +442,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
442 final newAddressIndex = addressesByReceiveType.fold(
443 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
444
445 + final hd = _hdFor(isHidden: false, type: addressPageType, isLegacyDerivation: false);
446 final address = BitcoinAddressRecord(
413 - getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType),
447 + getAddress(index: newAddressIndex, hd: hd, addressType: addressPageType),
448 index: newAddressIndex,
449 isHidden: false,
450 + isLegacyDerivation: false,
451 name: label,
452 type: addressPageType,
453 network: network,
@@ -610,14 +645,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
645 }
646
647 addressesByReceiveType.clear();
613 - addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList());
648 + addressesByReceiveType.addAll(
649 + _addresses.where(_isAddressPageTypeMatch).toList(),
650 + );
651 }
652
653 @action
654 void updateReceiveAddresses() {
655 receiveAddresses.removeRange(0, receiveAddresses.length);
619 - final newAddresses =
620 - _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
656 + final newAddresses = _addresses.where((addressRecord) =>
657 + !addressRecord.isHidden &&
658 + !addressRecord.isUsed);
659 receiveAddresses.addAll(newAddresses);
660 }
661
@@ -633,66 +671,96 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
671 }
672
673 @action
636 - Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
637 - Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
638 - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
639 - final newAddresses = await _createNewAddresses(gap,
640 - startIndex: addressList.length, isHidden: isHidden, type: type);
674 + Future<void> discoverAddresses(
675 + List<BitcoinAddressRecord> addressList,
676 + bool isHidden,
677 + Future<String?> Function(BitcoinAddressRecord) getAddressHistory, {
678 + BitcoinAddressType type = SegwitAddresType.p2wpkh,
679 + required bool isLegacyDerivation,
680 + }) async {
681 + final newAddresses = await _createNewAddresses(
682 + gap,
683 + startIndex: addressList.length,
684 + isHidden: isHidden,
685 + isLegacyDerivation: isLegacyDerivation,
686 + type: type,
687 + );
688 +
689 addAddresses(newAddresses);
690 + addressList.addAll(newAddresses);
691
643 - final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
644 - final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
692 + final addressesWithHistory =
693 + await Future.wait(newAddresses.map(getAddressHistory));
694 + final isLastAddressUsed = addressesWithHistory.last != null;
695
696 if (isLastAddressUsed) {
647 - discoverAddresses(addressList, isHidden, getAddressHistory, type: type);
697 + await discoverAddresses(
698 + addressList,
699 + isHidden,
700 + getAddressHistory,
701 + type: type,
702 + isLegacyDerivation: isLegacyDerivation,
703 + );
704 }
705 }
706
651 - Future<void> _generateInitialAddresses(
652 - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
653 - var countOfReceiveAddresses = 0;
654 - var countOfHiddenAddresses = 0;
707 + Future<void> _generateInitialAddresses(
708 + {BitcoinAddressType type = SegwitAddresType.p2wpkh,
709 + bool isLegacyDerivation = false }) async {
710 +
711 + // Legacy derivation produces the same addresses as standard for these types.
712 + // Don't generate a legacy set to avoid duplicates.
713 + if (isLegacyDerivation && (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh)) {
714 + return;
715 + }
716
656 - _addresses.forEach((addr) {
657 - if (addr.type == type) {
658 - if (addr.isHidden) {
659 - countOfHiddenAddresses += 1;
660 - return;
717 + var countOfReceiveAddresses = 0;
718 + var countOfHiddenAddresses = 0;
719 +
720 + _addresses.forEach((addr) {
721 + if (addr.type == type && addr.isLegacyDerivation == isLegacyDerivation) {
722 + if (addr.isHidden) {
723 + countOfHiddenAddresses += 1;
724 + } else {
725 + countOfReceiveAddresses += 1;
726 + }
727 }
728 + });
729
663 - countOfReceiveAddresses += 1;
730 + if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
731 + final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
732 + final newAddresses = await _createNewAddresses(addressesCount,
733 + startIndex: countOfReceiveAddresses, isHidden: false, type: type, isLegacyDerivation: isLegacyDerivation);
734 + addAddresses(newAddresses);
735 }
665 - });
736
667 - if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
668 - final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
669 - final newAddresses = await _createNewAddresses(addressesCount,
670 - startIndex: countOfReceiveAddresses, isHidden: false, type: type);
671 - addAddresses(newAddresses);
737 + if (countOfHiddenAddresses < defaultChangeAddressesCount) {
738 + final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
739 + final newAddresses = await _createNewAddresses(addressesCount,
740 + startIndex: countOfHiddenAddresses, isHidden: true, type: type, isLegacyDerivation: isLegacyDerivation);
741 + addAddresses(newAddresses);
742 + }
743 }
744
674 - if (countOfHiddenAddresses < defaultChangeAddressesCount) {
675 - final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
676 - final newAddresses = await _createNewAddresses(addressesCount,
677 - startIndex: countOfHiddenAddresses, isHidden: true, type: type);
678 - addAddresses(newAddresses);
679 - }
680 - }
745 + Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
746 + {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type, bool isLegacyDerivation = false}) async {
747 + final list = <BitcoinAddressRecord>[];
748
682 - Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
683 - {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async {
684 - final list = <BitcoinAddressRecord>[];
749 + for (var i = startIndex; i < count + startIndex; i++) {
750
686 - for (var i = startIndex; i < count + startIndex; i++) {
687 - final address = BitcoinAddressRecord(
688 - await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
689 - index: i,
690 - isHidden: isHidden,
691 - type: type ?? addressPageType,
692 - network: network,
693 - );
694 - list.add(address);
695 - }
751 + final addrType = type ?? addressPageType;
752 + final hd = _hdFor(isHidden: isHidden, type: addrType, isLegacyDerivation: isLegacyDerivation);
753 +
754 + final address = BitcoinAddressRecord(
755 + await getAddressAsync(index: i, hd: hd, addressType: addrType),
756 + index: i,
757 + isHidden: isHidden,
758 + isLegacyDerivation: isLegacyDerivation,
759 + type: addrType,
760 + network: network,
761 + );
762 + list.add(address);
763 + }
764
765 return list;
766 }
@@ -730,6 +798,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
798 // this would add a ton of startup lag for mweb addresses since we have 1000 of them
799 return;
800 }
801 +
802 + final mainHd = _hdFor(isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
803 + final sideHd = _hdFor(isHidden: true, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
804 if (!element.isHidden &&
805 element.address !=
806 await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
@@ -757,9 +828,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
828 return _isAddressByType(addressRecord, addressPageType);
829 }
830
760 - Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
761 -
762 - bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
831 + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
832
833 bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
834 !addr.isHidden && !addr.isUsed && addr.type == type;
@@ -769,10 +838,23 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
838 final addressRecord = silentAddresses.firstWhere((addressRecord) =>
839 addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address);
840
772 - silentAddresses.remove(addressRecord);
773 - updateAddressesByMatch();
774 - }
841 + silentAddresses.remove(addressRecord);
842 + updateAddressesByMatch();
843 + }
844
845 + Bip32Slip10Secp256k1 _hdFor({
846 + required bool isHidden,
847 + required BitcoinAddressType type,
848 + required bool isLegacyDerivation,
849 + }) {
850 + if (isLegacyDerivation) return isHidden ? legacySideHd : legacyMainHd;
851 +
852 + final map = isHidden ? sideHdByType : mainHdByType;
853 + final hd = map[type];
854 + if (hd == null) throw Exception("HD not found for type $type");
855 + return hd;
856 + }
857 +
858 @action
859 Future<void> setLightningAddress(String walletName, {String newAddress = ""}) async {
860 if (lightningWallet == null) return;
cw_bitcoin/lib/litecoin_wallet.dart
+7 -5
@@ -123,8 +123,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
123 initialRegularAddressIndex: initialRegularAddressIndex,
124 initialChangeAddressIndex: initialChangeAddressIndex,
125 initialMwebAddresses: initialMwebAddresses,
126 - mainHd: hd,
127 - sideHd: accountHD.childKey(Bip32KeyIndex(1)),
126 + mainHdByType: mainHdByType,
127 + sideHdByType: sideHdByType,
128 + legacyMainHd: mainHd,
129 + legacySideHd: sideHd,
130 network: network,
131 mwebHd: mwebHd,
132 mwebEnabled: mwebEnabled,
@@ -1264,8 +1266,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1266 .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
1267 final key = generateECPrivate(
1268 hd: utxo.bitcoinAddressRecord.isHidden
1267 - ? walletAddresses.sideHd
1268 - : walletAddresses.mainHd,
1269 + ? sideHd
1270 + : mainHd,
1271 index: utxo.bitcoinAddressRecord.index,
1272 network: network);
1273 final digest = tx2.getTransactionSegwitDigit(
@@ -1438,7 +1440,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1440 final index = address != null
1441 ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
1442 : null;
1441 - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
1443 + final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
1444 final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
1445
1446 final privateKey = ECDSAPrivateKey.fromBytes(
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+5 -3
@@ -25,8 +25,10 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle
25 abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
26 LitecoinWalletAddressesBase(
27 WalletInfo walletInfo, {
28 - required super.mainHd,
29 - required super.sideHd,
28 + required super.mainHdByType,
29 + required super.sideHdByType,
30 + required super.legacyMainHd,
31 + required super.legacySideHd,
32 required super.network,
33 required super.isHardwareWallet,
34 required this.mwebHd,
@@ -147,7 +149,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
149 if (mwebAddrs.length == 0) {
150 return "";
151 }
150 - return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
152 + return hd == legacySideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
153 }
154 return generateP2WPKHAddress(hd: hd, index: index, network: network);
155 }
cw_bitcoin/lib/psbt/signer.dart
+1 -1
@@ -227,7 +227,7 @@ class UtxoWithPrivateKey extends UtxoWithAddress {
227 RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
228
229 final newHd =
230 - input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.hd;
230 + input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.mainHd;
231
232 ECPrivate privkey;
233 if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
+5 -3
@@ -56,8 +56,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
56 initialAddresses: initialAddresses,
57 initialRegularAddressIndex: initialRegularAddressIndex,
58 initialChangeAddressIndex: initialChangeAddressIndex,
59 - mainHd: hd,
60 - sideHd: accountHD.childKey(Bip32KeyIndex(1)),
59 + mainHdByType: mainHdByType,
60 + sideHdByType: sideHdByType,
61 + legacyMainHd: mainHd,
62 + legacySideHd: sideHd,
63 network: network,
64 initialAddressPageType: addressPageType,
65 isHardwareWallet: walletInfo.isHardwareWallet,
@@ -221,7 +223,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
223 ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
224 : null;
225 } catch (_) {}
224 - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
226 + final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
227 final priv = ECPrivate.fromWif(
228 WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
229 netVersion: network.wifNetVer,
cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart
+4 -2
@@ -13,8 +13,10 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin
13 abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store {
14 BitcoinCashWalletAddressesBase(
15 WalletInfo walletInfo, {
16 - required super.mainHd,
17 - required super.sideHd,
16 + required super.mainHdByType,
17 + required super.sideHdByType,
18 + required super.legacyMainHd,
19 + required super.legacySideHd,
20 required super.network,
21 required super.isHardwareWallet,
22 super.initialAddresses,
cw_dogecoin/lib/src/dogecoin_wallet.dart
+5 -3
@@ -54,8 +54,10 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
54 initialAddresses: initialAddresses,
55 initialRegularAddressIndex: initialRegularAddressIndex,
56 initialChangeAddressIndex: initialChangeAddressIndex,
57 - mainHd: hd,
58 - sideHd: accountHD.childKey(Bip32KeyIndex(1)),
57 + mainHdByType: mainHdByType,
58 + sideHdByType: sideHdByType,
59 + legacyMainHd: mainHd,
60 + legacySideHd: sideHd,
61 network: network,
62 initialAddressPageType: addressPageType,
63 isHardwareWallet: walletInfo.isHardwareWallet,
@@ -173,7 +175,7 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
175 ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
176 : null;
177 } catch (_) {}
176 - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
178 + final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
179 final priv = ECPrivate.fromWif(
180 WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
181 netVersion: network.wifNetVer,
cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart
+8 -4
@@ -8,13 +8,17 @@ import 'package:mobx/mobx.dart';
8
9 part 'dogecoin_wallet_addresses.g.dart';
10
11 -class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalletAddresses;
11 +class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase
12 + with _$DogeCoinWalletAddresses;
13
13 -abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
14 +abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses
15 + with Store {
16 DogeCoinWalletAddressesBase(
17 WalletInfo walletInfo, {
16 - required super.mainHd,
17 - required super.sideHd,
18 + required super.mainHdByType,
19 + required super.sideHdByType,
20 + required super.legacyMainHd,
21 + required super.legacySideHd,
22 required super.network,
23 required super.isHardwareWallet,
24 super.initialAddresses,
cw_wownero/devtools_options.yaml new
+3
@@ -0,0 +1,3 @@
1 +description: This file stores settings for Dart & Flutter DevTools.
2 +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3 +extensions:
lib/bitcoin/cw_bitcoin.dart
+2 -1
@@ -182,7 +182,8 @@ class CWBitcoin extends Bitcoin {
182 address: addr.address,
183 txCount: addr.txCount,
184 balance: addr.balance,
185 - isChange: addr.isHidden))
185 + isChange: addr.isHidden,
186 + isLegacyDerivation: addr.isLegacyDerivation))
187 .toList();
188 }
189
lib/src/screens/restore/wallet_restore_page.dart
+18 -1
@@ -278,14 +278,31 @@ class WalletRestorePage extends BasePage {
278
279 int derivationsWithHistory = 0;
280 int derivationWithHistoryIndex = 0;
281 + final List<String> derivationPathsWithHistory = [];
282 +
283 for (int i = 0; i < derivations.length; i++) {
284 if (derivations[i].transactionsCount > 0) {
285 derivationsWithHistory++;
286 derivationWithHistoryIndex = i;
287 + final derivationPath = derivations[i].derivationPath;
288 + if (derivationPath != null && derivationPath.isNotEmpty) {
289 + derivationPathsWithHistory.add(derivationPath);
290 + }
291 }
292 }
293
288 - if (derivationsWithHistory > 1) {
294 + final scanDerivationPaths = {
295 + "m/84'/0'/0'",
296 + "m/86'/0'/0'",
297 + "m/44'/0'/0'",
298 + "m/49'/0'/0'",
299 + };
300 +
301 + final shouldSkipChooseDerivationScreen =
302 + derivationPathsWithHistory.isNotEmpty &&
303 + derivationPathsWithHistory.every(scanDerivationPaths.contains);
304 +
305 + if (derivationsWithHistory > 1 && !shouldSkipChooseDerivationScreen) {
306 dInfo = await Navigator.of(context).pushNamed(
307 Routes.restoreWalletChooseDerivation,
308 arguments: derivations,
lib/view_model/wallet_address_list/wallet_address_list_item.dart
+2
@@ -13,6 +13,7 @@ class WalletAddressListItem extends ListItem {
13 this.isOneTimeReceiveAddress = false,
14 this.isHidden = false,
15 this.isManual = false,
16 + this.isLegacyDerivation = false,
17 }) : super();
18
19 final int? id;
@@ -24,6 +25,7 @@ class WalletAddressListItem extends ListItem {
25 final bool isChange;
26 bool isHidden;
27 bool isManual;
28 + bool isLegacyDerivation;
29 final bool? isOneTimeReceiveAddress;
30
31 @override
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+5 -3
@@ -290,7 +290,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
290 txCount: subaddress.txCount,
291 balance: _appStore.amountParsingProxy
292 .getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
293 - isChange: subaddress.isChange);
293 + isChange: subaddress.isChange,
294 + isLegacyDerivation: subaddress.isLegacyDerivation);
295 });
296
297 // don't show all 1000+ mweb addresses:
@@ -352,8 +353,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
353
354 for (var i = 0; i < addressList.length; i++) {
355 if (!(addressList[i] is WalletAddressListItem)) continue;
355 - (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses
356 - .contains((addressList[i] as WalletAddressListItem).address);
356 + final item = addressList[i] as WalletAddressListItem;
357 + item.isHidden = wallet.walletAddresses.hiddenAddresses.contains(item.address) ||
358 + (isElectrumWallet && item.isLegacyDerivation);
359 }
360
361 for (var i = 0; i < addressList.length; i++) {
tool/configure.dart
+3 -1
@@ -167,13 +167,15 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart";
167 required this.address,
168 required this.txCount,
169 required this.balance,
170 - required this.isChange});
170 + required this.isChange,
171 + this.isLegacyDerivation = false});
172 final int id;
173 final String name;
174 final String address;
175 final int txCount;
176 final int balance;
177 final bool isChange;
178 + final bool isLegacyDerivation;
179 }
180
181 abstract class Bitcoin {