diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 37413dc81..400e325be 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -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, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index a75de4d4d..0616f3400 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -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, diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 5d051827a..40dd15d05 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -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, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 603571d36..f3e2b7f13 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -115,6 +115,104 @@ abstract class ElectrumWalletBase reaction((_) => syncStatus, _syncStatusReaction); sharedPrefs.complete(SharedPreferences.getInstance()); + + final supportedTypes = supportedAddressTypes(walletInfo.type); + mainHdByType = {}; + sideHdByType = {}; + + 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 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 mainHdByType; + late final Map 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 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 { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 0c3343856..84ba5be6a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -46,8 +46,10 @@ const List 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? 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 mwebAddresses; final BasedUtxoNetwork network; - final Bip32Slip10Secp256k1 mainHd; - final Bip32Slip10Secp256k1 sideHd; + Map mainHdByType; + Map 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 = [ + ...typeMatchingAddressesAll.where((a) => !a.isLegacyDerivation), + ...typeMatchingAddressesAll.where((a) => a.isLegacyDerivation), + ]; + + final typeMatchingReceiveAddressesAll = + typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList(); + final typeMatchingReceiveAddresses = [ + ...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 currentReceiveAddressIndexByType; @@ -302,26 +327,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return acc; }); - @override - Future 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 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 discoverAddresses(List addressList, bool isHidden, - Future Function(BitcoinAddressRecord) getAddressHistory, - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - final newAddresses = await _createNewAddresses(gap, - startIndex: addressList.length, isHidden: isHidden, type: type); - addAddresses(newAddresses); + Future discoverAddresses( + List addressList, + bool isHidden, + Future 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 _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> _createNewAddresses(int count, - {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { - final list = []; - - 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 _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> _createNewAddresses(int count, + {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type, bool isLegacyDerivation = false}) async { + final list = []; + + 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 setLightningAddress(String walletName, {String newAddress = ""}) async { if (lightningWallet == null) return; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 7d98a52a3..3cee6c9da 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -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( diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 737bd41f4..19fed3bce 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -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); } diff --git a/cw_bitcoin/lib/psbt/signer.dart b/cw_bitcoin/lib/psbt/signer.dart index 1d0ceba8b..c46517e66 100644 --- a/cw_bitcoin/lib/psbt/signer.dart +++ b/cw_bitcoin/lib/psbt/signer.dart @@ -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) { diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 39065722b..18b9ea1c9 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -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, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 25c11b763..d45f5293f 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -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, diff --git a/cw_dogecoin/lib/src/dogecoin_wallet.dart b/cw_dogecoin/lib/src/dogecoin_wallet.dart index 964c34b85..c9db30fa7 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet.dart @@ -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, diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index ed3e56f8b..df2ede882 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -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, diff --git a/cw_wownero/devtools_options.yaml b/cw_wownero/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/cw_wownero/devtools_options.yaml @@ -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: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 5fa7a7620..cf061a3fb 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -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(); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 35c76ba3c..8ffd29885 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -278,14 +278,31 @@ class WalletRestorePage extends BasePage { int derivationsWithHistory = 0; int derivationWithHistoryIndex = 0; + final List 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, diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 725b1ddbf..2c5b0a3d9 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -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 diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 8d2d2df39..a517cc24b 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -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++) { diff --git a/tool/configure.dart b/tool/configure.dart index f54e50ff2..d95100287 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -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 {