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 {