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
This commit is contained in:
Serhii
2026-03-26 22:46:08 +01:00
committed by GitHub
parent d0851df2c6
commit 6378e929b6
18 changed files with 432 additions and 146 deletions
+25 -4
View File
@@ -10,6 +10,7 @@ abstract class BaseBitcoinAddressRecord {
this.address, {
required this.index,
this.isHidden = false,
this.isLegacyDerivation = false,
int txCount = 0,
int balance = 0,
String name = '',
@@ -26,6 +27,7 @@ abstract class BaseBitcoinAddressRecord {
final String address;
bool isHidden;
bool isLegacyDerivation;
final int index;
int _txCount;
int _balance;
@@ -60,6 +62,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
super.address, {
required super.index,
super.isHidden = false,
super.isLegacyDerivation = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
@@ -76,21 +79,38 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
}
}
static bool _legacyDefaultForType(BitcoinAddressType type) {
// Some address types (p2wpkh, p2wsh) were historically derived from the same account/path as our new standard
// but using legacy formats. For these, we default to legacy = false.
if (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh) return false;
// For other types (p2pkh, p2wpkh-in-p2sh, p2tr, etc.), old wallets used the non-standard
return true;
}
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
final parsedType = decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh;
final parsedIsLegacy = decoded.containsKey('isLegacyDerivation')
? (decoded['isLegacyDerivation'] as bool? ?? false)
: _legacyDefaultForType(parsedType);
return BitcoinAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isLegacyDerivation: parsedIsLegacy,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
type: decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh,
type: parsedType,
scriptHash: decoded['scriptHash'] as String?,
network: network,
);
@@ -113,6 +133,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'address': address,
'index': index,
'isHidden': isHidden,
'isLegacyDerivation': isLegacyDerivation,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
+4 -2
View File
@@ -123,8 +123,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
mainHdByType: mainHdByType,
sideHdByType: sideHdByType,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: networkParam ?? network,
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
+4 -2
View File
@@ -23,8 +23,10 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.mainHdByType,
required super.sideHdByType,
required super.legacySideHd,
required super.legacyMainHd,
required super.network,
required super.isHardwareWallet,
required this.payjoinManager,
+153 -15
View File
@@ -115,6 +115,104 @@ abstract class ElectrumWalletBase
reaction((_) => syncStatus, _syncStatusReaction);
sharedPrefs.complete(SharedPreferences.getInstance());
final supportedTypes = supportedAddressTypes(walletInfo.type);
mainHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
sideHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
final isElectrumDerivation = derivationInfo.derivationType == DerivationType.electrum;
final canDeriveFromSeed = _masterHD != null && currency != null;
if (isElectrumDerivation) {
// Electrum derivation does not follow BIP44/49/84 etc. standards
for (final type in supportedTypes) {
mainHdByType[type] = mainHd; // accountHD.child(0)
sideHdByType[type] = sideHd; // accountHD.child(1)
}
} else if (canDeriveFromSeed) {
final coinType = _coinTypeFor(currency);
for (final type in supportedTypes) {
final purpose = _purposeForType(type);
final accountPath = "m/$purpose'/$coinType'/0'";
mainHdByType[type] = _masterHD!.derivePath("$accountPath/0") as Bip32Slip10Secp256k1;
sideHdByType[type] = _masterHD!.derivePath("$accountPath/1") as Bip32Slip10Secp256k1;
}
} else {
// View-only wallet (xpub only)
for (final type in supportedTypes) {
mainHdByType[type] = mainHd;
sideHdByType[type] = sideHd;
}
}
}
int _purposeForType(BitcoinAddressType type) {
switch (type.value) {
case 'P2PKH':
return 44;
case 'P2SH/P2WPKH':
return 49;
case 'P2WPKH':
return 84;
case 'P2TR':
return 86;
default:
return 84;
}
}
int _coinTypeFor(CryptoCurrency cur) {
if (!network.isMainnet) return 1;
switch (cur) {
case CryptoCurrency.btc:
case CryptoCurrency.tbtc:
return 0;
case CryptoCurrency.ltc:
return 2;
case CryptoCurrency.bch:
return 145;
case CryptoCurrency.doge:
return 3;
default:
return 0;
}
}
/// Returns the BIP32 account derivation path (m/purpose'/coinType'/0') for STANDARD addresses.
/// For LEGACY addresses, returns the wallet's legacy derivation base (derivationInfo.derivationPath)
/// which is already the account path used historically (e.g. m/0' or m/84'/0'/0').
String _accountDerivationPathForRecord(BaseBitcoinAddressRecord record) {
if (derivationInfo.derivationType == DerivationType.electrum) {
return derivationInfo.derivationPath ?? electrum_path; // m/0'
}
if (record.isLegacyDerivation) {
return derivationInfo.derivationPath ?? electrum_path;
}
final coinType = _coinTypeFor(currency);
final purpose = _purposeForType(record.type);
return "m/$purpose'/$coinType'/0'";
}
List<BitcoinAddressType> supportedAddressTypes(WalletType type) {
switch (type) {
case WalletType.bitcoin:
return BITCOIN_ADDRESS_TYPES;
case WalletType.bitcoinCash:
return BITCOIN_CASH_ADDRESS_TYPES;
case WalletType.dogecoin:
return DOGECOIN_ADDRESS_TYPES;
case WalletType.litecoin:
return LITECOIN_ADDRESS_TYPES;
default:
return BITCOIN_ADDRESS_TYPES;
}
}
static Bip32Slip10Secp256k1 getAccountHDWallet(
@@ -191,7 +289,10 @@ abstract class ElectrumWalletBase
final Bip32Slip10Secp256k1 accountHD;
final String? _mnemonic;
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
Bip32Slip10Secp256k1 get mainHd => accountHD.childKey(Bip32KeyIndex(0));
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
@@ -321,6 +422,9 @@ abstract class ElectrumWalletBase
String? wif;
String? privateKey;
String? publicKey;
final hd = mainHdByType[SegwitAddresType.p2wpkh] ?? mainHd;
try {
wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
} catch (_) {}
@@ -330,6 +434,7 @@ abstract class ElectrumWalletBase
try {
publicKey = hd.publicKey.toHex();
} catch (_) {}
return BitcoinWalletKeys(
wif: wif ?? '',
privateKey: privateKey ?? '',
@@ -757,9 +862,7 @@ abstract class ElectrumWalletBase
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
final hd =
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
final hd = _hdFor(record: utx.bitcoinAddressRecord);
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@@ -790,8 +893,10 @@ abstract class ElectrumWalletBase
pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex();
}
final baseDerivationPath = _accountDerivationPathForRecord(utx.bitcoinAddressRecord);
final derivationPath =
"${_hardenedDerivationPath(derivationInfo.derivationPath ?? electrum_path)}"
"${_hardenedDerivationPath(baseDerivationPath)}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
@@ -1005,9 +1110,11 @@ abstract class ElectrumWalletBase
isChange: true,
));
// Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
// Must match the address' account root (purpose/coinType) and legacy derivation when applicable.
final changeBaseDerivationPath = _accountDerivationPathForRecord(changeAddress);
final changeDerivationPath =
"${_hardenedDerivationPath(derivationInfo.derivationPath ?? "m/0'")}"
"${_hardenedDerivationPath(changeBaseDerivationPath)}"
"/${changeAddress.isHidden ? "1" : "0"}"
"/${changeAddress.index}";
utxoDetails.publicKeys[address.pubKeyHash()] =
@@ -1897,8 +2004,11 @@ abstract class ElectrumWalletBase
final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
final hd = _hdFor(record: addressRecord);
final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
hd: hd,
index: addressRecord.index,
network: network);
@@ -1981,10 +2091,11 @@ abstract class ElectrumWalletBase
for (final utxo in unusedUtxos) {
final address = RegexUtils.addressTypeFromStr(utxo.address, network);
final hd = _hdFor(record: utxo.bitcoinAddressRecord);
final privkey = generateECPrivate(
hd: utxo.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
hd: hd,
index: utxo.bitcoinAddressRecord.index,
network: network,
);
@@ -2302,6 +2413,7 @@ abstract class ElectrumWalletBase
.then((history) => history.isNotEmpty ? address.address : null);
},
type: type,
isLegacyDerivation: addressRecord.isLegacyDerivation,
);
final newLength = walletAddresses.allAddresses.length;
@@ -2587,11 +2699,19 @@ abstract class ElectrumWalletBase
@override
Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
final addressRecord = address != null
? walletAddresses.allAddresses.firstWhereOrNull((addr) => addr.address == address)
: null;
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
if (addressRecord != null && addressRecord.type == SegwitAddresType.p2tr) {
throw UnsupportedError("Cannot sign message with Taproot address");
}
final hd = addressRecord != null
? _hdFor(record: addressRecord).childKey(Bip32KeyIndex(addressRecord.index))
: mainHd;
final priv = ECPrivate.fromHex(hd.privateKey.privKey.toHex());
String messagePrefix = '\x18Bitcoin Signed Message:\n';
final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
@@ -2949,6 +3069,24 @@ abstract class ElectrumWalletBase
syncStatus = FailedSyncStatus();
}
}
Bip32Slip10Secp256k1 _hdFor({required BaseBitcoinAddressRecord record}) {
final addrType = record.type;
if (record.isLegacyDerivation) {
if (record.isHidden) {
return walletAddresses.legacySideHd;
} else {
return walletAddresses.legacyMainHd;
}
}
if (record.isHidden) {
return sideHdByType[addrType] ?? sideHd;
} else {
return mainHdByType[addrType] ?? mainHd;
}
}
}
class ScanNode {
+178 -96
View File
@@ -46,8 +46,10 @@ const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(
WalletInfo walletInfo, {
required this.mainHd,
required this.sideHd,
required this.mainHdByType,
required this.sideHdByType,
required this.legacyMainHd,
required this.legacySideHd,
required this.network,
required this.isHardwareWallet,
List<BitcoinAddressRecord>? initialAddresses,
@@ -140,8 +142,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
// TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
final ObservableList<BitcoinAddressRecord> mwebAddresses;
final BasedUtxoNetwork network;
final Bip32Slip10Secp256k1 mainHd;
final Bip32Slip10Secp256k1 sideHd;
Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
final Bip32Slip10Secp256k1 legacyMainHd;
final Bip32Slip10Secp256k1 legacySideHd;
final bool isHardwareWallet;
final LightningWallet? lightningWallet;
@@ -178,14 +182,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
if (addressPageType == LightningAddressType.p2l) {
return lightningAddress ??
"Error: Unable to fetch your Lightning address, please check your network connection.";
return lightningAddress ??
"Error: Unable to fetch your Lightning address, please check your network connection.";
}
final typeMatchingAddresses =
_addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
final typeMatchingReceiveAddresses =
typeMatchingAddresses.where((addr) => !addr.isUsed).toList();
final typeMatchingAddressesAll = _addresses
.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr))
.toList();
// Prefer standard derivation addresses for the current/active address,
// but keep legacy addresses present in the overall address lists.
final typeMatchingAddresses = <BitcoinAddressRecord>[
...typeMatchingAddressesAll.where((a) => !a.isLegacyDerivation),
...typeMatchingAddressesAll.where((a) => a.isLegacyDerivation),
];
final typeMatchingReceiveAddressesAll =
typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList();
final typeMatchingReceiveAddresses = <BitcoinAddressRecord>[
...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation),
...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation),
];
if (!isEnabledAutoGenerateSubaddress) {
if (previousAddressRecord != null && previousAddressRecord!.type == addressPageType) {
@@ -207,7 +224,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (locked != null) return locked;
final prev = previousAddressRecord;
if (prev != null && prev.type == addressPageType && !prev.isUsed) {
if (prev != null && prev.type == addressPageType && !prev.isUsed && !prev.isLegacyDerivation) {
return prev.address;
}
@@ -235,7 +252,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
try {
final addressRecord = _addresses.firstWhere(
(addressRecord) => addressRecord.address == addr,
(addressRecord) => addressRecord.address == addr && !addressRecord.isLegacyDerivation,
orElse: () => _addresses.firstWhere((r) => r.address == addr),
);
lockedReceiveAddressByType.remove(addressPageType);
@@ -263,7 +281,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@override
String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType);
String get primaryAddress {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
return silentAddress?.toString() ?? '';
}
final mainHd = mainHdByType[addressPageType] ?? mainHdByType.values.first;
return getAddress(index: 0, hd: mainHd, addressType: addressPageType);
}
Map<String, int> currentReceiveAddressIndexByType;
@@ -302,26 +327,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return acc;
});
@override
Future<void> init() async {
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
await _generateInitialAddresses(type: SegwitAddresType.mweb);
}
} else if (walletInfo.type == WalletType.dogecoin) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses();
if (!isHardwareWallet) {
@override
Future<void> init() async {
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
} else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
await _generateInitialAddresses(type: SegwitAddresType.mweb);
}
} else if (walletInfo.type == WalletType.dogecoin) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses(isLegacyDerivation: true);
await _generateInitialAddresses();
if (!isHardwareWallet) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh, isLegacyDerivation: true);
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr, isLegacyDerivation: true);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh, isLegacyDerivation: true);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
}
}
updateAddressesByMatch();
updateReceiveAddresses();
@@ -409,10 +442,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final newAddressIndex = addressesByReceiveType.fold(
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
final hd = _hdFor(isHidden: false, type: addressPageType, isLegacyDerivation: false);
final address = BitcoinAddressRecord(
getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType),
getAddress(index: newAddressIndex, hd: hd, addressType: addressPageType),
index: newAddressIndex,
isHidden: false,
isLegacyDerivation: false,
name: label,
type: addressPageType,
network: network,
@@ -610,14 +645,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
addressesByReceiveType.clear();
addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList());
addressesByReceiveType.addAll(
_addresses.where(_isAddressPageTypeMatch).toList(),
);
}
@action
void updateReceiveAddresses() {
receiveAddresses.removeRange(0, receiveAddresses.length);
final newAddresses =
_addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
final newAddresses = _addresses.where((addressRecord) =>
!addressRecord.isHidden &&
!addressRecord.isUsed);
receiveAddresses.addAll(newAddresses);
}
@@ -633,66 +671,96 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
final newAddresses = await _createNewAddresses(gap,
startIndex: addressList.length, isHidden: isHidden, type: type);
addAddresses(newAddresses);
Future<void> discoverAddresses(
List<BitcoinAddressRecord> addressList,
bool isHidden,
Future<String?> Function(BitcoinAddressRecord) getAddressHistory, {
BitcoinAddressType type = SegwitAddresType.p2wpkh,
required bool isLegacyDerivation,
}) async {
final newAddresses = await _createNewAddresses(
gap,
startIndex: addressList.length,
isHidden: isHidden,
isLegacyDerivation: isLegacyDerivation,
type: type,
);
final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
addAddresses(newAddresses);
addressList.addAll(newAddresses);
final addressesWithHistory =
await Future.wait(newAddresses.map(getAddressHistory));
final isLastAddressUsed = addressesWithHistory.last != null;
if (isLastAddressUsed) {
discoverAddresses(addressList, isHidden, getAddressHistory, type: type);
}
}
Future<void> _generateInitialAddresses(
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
var countOfReceiveAddresses = 0;
var countOfHiddenAddresses = 0;
_addresses.forEach((addr) {
if (addr.type == type) {
if (addr.isHidden) {
countOfHiddenAddresses += 1;
return;
}
countOfReceiveAddresses += 1;
}
});
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfReceiveAddresses, isHidden: false, type: type);
addAddresses(newAddresses);
}
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfHiddenAddresses, isHidden: true, type: type);
addAddresses(newAddresses);
}
}
Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
{int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async {
final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord(
await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
index: i,
isHidden: isHidden,
type: type ?? addressPageType,
network: network,
await discoverAddresses(
addressList,
isHidden,
getAddressHistory,
type: type,
isLegacyDerivation: isLegacyDerivation,
);
list.add(address);
}
}
Future<void> _generateInitialAddresses(
{BitcoinAddressType type = SegwitAddresType.p2wpkh,
bool isLegacyDerivation = false }) async {
// Legacy derivation produces the same addresses as standard for these types.
// Don't generate a legacy set to avoid duplicates.
if (isLegacyDerivation && (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh)) {
return;
}
var countOfReceiveAddresses = 0;
var countOfHiddenAddresses = 0;
_addresses.forEach((addr) {
if (addr.type == type && addr.isLegacyDerivation == isLegacyDerivation) {
if (addr.isHidden) {
countOfHiddenAddresses += 1;
} else {
countOfReceiveAddresses += 1;
}
}
});
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfReceiveAddresses, isHidden: false, type: type, isLegacyDerivation: isLegacyDerivation);
addAddresses(newAddresses);
}
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfHiddenAddresses, isHidden: true, type: type, isLegacyDerivation: isLegacyDerivation);
addAddresses(newAddresses);
}
}
Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
{int startIndex = 0, bool isHidden = false, BitcoinAddressType? type, bool isLegacyDerivation = false}) async {
final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) {
final addrType = type ?? addressPageType;
final hd = _hdFor(isHidden: isHidden, type: addrType, isLegacyDerivation: isLegacyDerivation);
final address = BitcoinAddressRecord(
await getAddressAsync(index: i, hd: hd, addressType: addrType),
index: i,
isHidden: isHidden,
isLegacyDerivation: isLegacyDerivation,
type: addrType,
network: network,
);
list.add(address);
}
return list;
}
@@ -730,6 +798,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
return;
}
final mainHd = _hdFor(isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
final sideHd = _hdFor(isHidden: true, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
if (!element.isHidden &&
element.address !=
await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
@@ -757,9 +828,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return _isAddressByType(addressRecord, addressPageType);
}
Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
!addr.isHidden && !addr.isUsed && addr.type == type;
@@ -769,10 +838,23 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final addressRecord = silentAddresses.firstWhere((addressRecord) =>
addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address);
silentAddresses.remove(addressRecord);
updateAddressesByMatch();
}
silentAddresses.remove(addressRecord);
updateAddressesByMatch();
}
Bip32Slip10Secp256k1 _hdFor({
required bool isHidden,
required BitcoinAddressType type,
required bool isLegacyDerivation,
}) {
if (isLegacyDerivation) return isHidden ? legacySideHd : legacyMainHd;
final map = isHidden ? sideHdByType : mainHdByType;
final hd = map[type];
if (hd == null) throw Exception("HD not found for type $type");
return hd;
}
@action
Future<void> setLightningAddress(String walletName, {String newAddress = ""}) async {
if (lightningWallet == null) return;
+7 -5
View File
@@ -123,8 +123,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
initialMwebAddresses: initialMwebAddresses,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
mainHdByType: mainHdByType,
sideHdByType: sideHdByType,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
mwebHd: mwebHd,
mwebEnabled: mwebEnabled,
@@ -1264,8 +1266,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
final key = generateECPrivate(
hd: utxo.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
? sideHd
: mainHd,
index: utxo.bitcoinAddressRecord.index,
network: network);
final digest = tx2.getTransactionSegwitDigit(
@@ -1438,7 +1440,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
final privateKey = ECDSAPrivateKey.fromBytes(
@@ -25,8 +25,10 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
LitecoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.mainHdByType,
required super.sideHdByType,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
required super.isHardwareWallet,
required this.mwebHd,
@@ -147,7 +149,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
if (mwebAddrs.length == 0) {
return "";
}
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
return hd == legacySideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
}
return generateP2WPKHAddress(hd: hd, index: index, network: network);
}
+1 -1
View File
@@ -227,7 +227,7 @@ class UtxoWithPrivateKey extends UtxoWithAddress {
RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
final newHd =
input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.hd;
input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.mainHd;
ECPrivate privkey;
if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
@@ -56,8 +56,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
mainHdByType: mainHdByType,
sideHdByType: sideHdByType,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
initialAddressPageType: addressPageType,
isHardwareWallet: walletInfo.isHardwareWallet,
@@ -221,7 +223,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
} catch (_) {}
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromWif(
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
netVersion: network.wifNetVer,
@@ -13,8 +13,10 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin
abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinCashWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.mainHdByType,
required super.sideHdByType,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
required super.isHardwareWallet,
super.initialAddresses,
+5 -3
View File
@@ -54,8 +54,10 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
mainHdByType: mainHdByType,
sideHdByType: sideHdByType,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
initialAddressPageType: addressPageType,
isHardwareWallet: walletInfo.isHardwareWallet,
@@ -173,7 +175,7 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
} catch (_) {}
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final HD = index == null ? mainHd : mainHd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromWif(
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
netVersion: network.wifNetVer,
@@ -8,13 +8,17 @@ import 'package:mobx/mobx.dart';
part 'dogecoin_wallet_addresses.g.dart';
class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalletAddresses;
class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase
with _$DogeCoinWalletAddresses;
abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses
with Store {
DogeCoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.mainHdByType,
required super.sideHdByType,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
required super.isHardwareWallet,
super.initialAddresses,
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+2 -1
View File
@@ -182,7 +182,8 @@ class CWBitcoin extends Bitcoin {
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
isChange: addr.isHidden,
isLegacyDerivation: addr.isLegacyDerivation))
.toList();
}
@@ -278,14 +278,31 @@ class WalletRestorePage extends BasePage {
int derivationsWithHistory = 0;
int derivationWithHistoryIndex = 0;
final List<String> derivationPathsWithHistory = [];
for (int i = 0; i < derivations.length; i++) {
if (derivations[i].transactionsCount > 0) {
derivationsWithHistory++;
derivationWithHistoryIndex = i;
final derivationPath = derivations[i].derivationPath;
if (derivationPath != null && derivationPath.isNotEmpty) {
derivationPathsWithHistory.add(derivationPath);
}
}
}
if (derivationsWithHistory > 1) {
final scanDerivationPaths = {
"m/84'/0'/0'",
"m/86'/0'/0'",
"m/44'/0'/0'",
"m/49'/0'/0'",
};
final shouldSkipChooseDerivationScreen =
derivationPathsWithHistory.isNotEmpty &&
derivationPathsWithHistory.every(scanDerivationPaths.contains);
if (derivationsWithHistory > 1 && !shouldSkipChooseDerivationScreen) {
dInfo = await Navigator.of(context).pushNamed(
Routes.restoreWalletChooseDerivation,
arguments: derivations,
@@ -13,6 +13,7 @@ class WalletAddressListItem extends ListItem {
this.isOneTimeReceiveAddress = false,
this.isHidden = false,
this.isManual = false,
this.isLegacyDerivation = false,
}) : super();
final int? id;
@@ -24,6 +25,7 @@ class WalletAddressListItem extends ListItem {
final bool isChange;
bool isHidden;
bool isManual;
bool isLegacyDerivation;
final bool? isOneTimeReceiveAddress;
@override
@@ -290,7 +290,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
txCount: subaddress.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
isChange: subaddress.isChange);
isChange: subaddress.isChange,
isLegacyDerivation: subaddress.isLegacyDerivation);
});
// don't show all 1000+ mweb addresses:
@@ -352,8 +353,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
for (var i = 0; i < addressList.length; i++) {
if (!(addressList[i] is WalletAddressListItem)) continue;
(addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses
.contains((addressList[i] as WalletAddressListItem).address);
final item = addressList[i] as WalletAddressListItem;
item.isHidden = wallet.walletAddresses.hiddenAddresses.contains(item.address) ||
(isElectrumWallet && item.isLegacyDerivation);
}
for (var i = 0; i < addressList.length; i++) {
+3 -1
View File
@@ -167,13 +167,15 @@ import "package:breez_sdk_spark_flutter/src/rust/errors.dart";
required this.address,
required this.txCount,
required this.balance,
required this.isChange});
required this.isChange,
this.isLegacyDerivation = false});
final int id;
final String name;
final String address;
final int txCount;
final int balance;
final bool isChange;
final bool isLegacyDerivation;
}
abstract class Bitcoin {