From 680cf22a98b12dfadb770531a4c1b3e64ec1e4ff Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:36:46 +0100 Subject: [PATCH 1/4] feat: Add Tether Gold (XAUT0) token support to Solana wallet and add migration for existing wallets (#3136) --- assets/images/xau_sol.png | Bin 0 -> 6389 bytes cw_solana/lib/default_spl_tokens.dart | 9 ++++ lib/entities/default_settings_migration.dart | 40 ++++++++++++++++++ lib/main.dart | 2 +- .../assets_history/asset_details_modal.dart | 2 +- .../coins_page/assets_history/asset_tile.dart | 2 +- 6 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 assets/images/xau_sol.png diff --git a/assets/images/xau_sol.png b/assets/images/xau_sol.png new file mode 100644 index 0000000000000000000000000000000000000000..955d600b42a24f7c2bf70d921eb0e1276de8b2ed GIT binary patch literal 6389 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 defaultSettingsMigration( enabled: false, ); break; + case 63: + await _addXaut0TokenToExistingSolanaWallets(); + break; default: break; } @@ -1489,3 +1493,39 @@ Future _addXautTokenToExistingEthereumWallets() async { printV('Error in XAUT migration: $e'); } } +Future _addXaut0TokenToExistingSolanaWallets() async { + try { + final xaut0Token = SPLToken( + name: "Tether Gold", + symbol: "XAUT0", + mintAddress: "AymATz4TCL9sWNEEV9Kvyz45CHVhDZ6kUgjTJPzLpU9P", + decimal: 6, + mint: 'xaut0', + enabled: false, + iconPath: "assets/images/xau_sol.png", + ); + + final allWallets = await WalletInfo.getAll(); + + final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList(); + + for (final walletInfo in solanaWallets) { + final sanitizedName = walletInfo.name.replaceAll(' ', '_'); + final boxName = '${sanitizedName}_${SPLToken.boxName}'; + + Box tokenBox; + if (CakeHive.isBoxOpen(boxName)) { + tokenBox = CakeHive.box(boxName); + } else { + tokenBox = await CakeHive.openBox(boxName); + } + + final xaut0Address = xaut0Token.mintAddress; + if (!tokenBox.containsKey(xaut0Address)) { + await tokenBox.put(xaut0Address, xaut0Token); + } + } + } catch (e) { + printV('Error in XAUT0 migration: $e'); + } +} diff --git a/lib/main.dart b/lib/main.dart index 56f0d02e2..3d4493a36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -324,7 +324,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 62, + initialMigrationVersion: 63, ); } diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart index e555ae564..8bdf7571c 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart @@ -74,7 +74,7 @@ class AssetDetailsModal extends StatelessWidget { child: Stack( children: [ if (iconPath.isNotEmpty) - Image.asset(iconPath, width: 75, height: 75) + CakeImageWidget(imageUrl: iconPath, width: 75, height: 75) else Container( width: 75, diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart index 380588e15..9eb4d7539 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -92,7 +92,7 @@ class AssetTile extends StatelessWidget { child: Stack( children: [ if((iconPath).isNotEmpty) - Image.asset(iconPath) + CakeImageWidget(imageUrl: iconPath) else Container( width: 45, From d0851df2c68a9dcccd4934536475b07818681653 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Thu, 26 Mar 2026 22:45:06 +0100 Subject: [PATCH 2/4] fix uri generation for certain wallet types (#3126) --- .../exchange/exchange_trade_view_model.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index e85d493b2..60f6e2335 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -482,6 +482,8 @@ abstract class ExchangeTradeViewModelBase with Store { final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type; + printV(uriWalletType); + switch (uriWalletType) { case WalletType.bitcoin: return BitcoinURI(address: inputAddress, amount: amount); @@ -510,7 +512,19 @@ abstract class ExchangeTradeViewModelBase with Store { return MoneroURI( address: inputAddress, amount: amount); - default: + case WalletType.litecoin: + return LitecoinURI(amount: amount, address: inputAddress); + case WalletType.nano: + return NanoURI(amount: amount, address: inputAddress); + case WalletType.zano: + return ZanoURI(amount: amount, address: inputAddress); + case WalletType.decred: + return DecredURI(amount: amount, address: inputAddress); + case WalletType.zcash: + return ZcashURI(amount: amount, address: inputAddress); + case WalletType.banano: + case WalletType.none: + case WalletType.haven: return null; } } From 6378e929b627bb7bc2033514f016180b4b6f346c Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:46:08 +0200 Subject: [PATCH 3/4] 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 --- cw_bitcoin/lib/bitcoin_address_record.dart | 29 +- cw_bitcoin/lib/bitcoin_wallet.dart | 6 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 6 +- cw_bitcoin/lib/electrum_wallet.dart | 168 ++++++++++- cw_bitcoin/lib/electrum_wallet_addresses.dart | 274 ++++++++++++------ cw_bitcoin/lib/litecoin_wallet.dart | 12 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 8 +- cw_bitcoin/lib/psbt/signer.dart | 2 +- .../lib/src/bitcoin_cash_wallet.dart | 8 +- .../src/bitcoin_cash_wallet_addresses.dart | 6 +- cw_dogecoin/lib/src/dogecoin_wallet.dart | 8 +- .../lib/src/dogecoin_wallet_addresses.dart | 12 +- cw_wownero/devtools_options.yaml | 3 + lib/bitcoin/cw_bitcoin.dart | 3 +- .../screens/restore/wallet_restore_page.dart | 19 +- .../wallet_address_list_item.dart | 2 + .../wallet_address_list_view_model.dart | 8 +- tool/configure.dart | 4 +- 18 files changed, 432 insertions(+), 146 deletions(-) create mode 100644 cw_wownero/devtools_options.yaml 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 { From ebfc339117a9666c27442199b62b93619bc2c823 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:35:06 +0100 Subject: [PATCH 4/4] fix: Handle fiat amount edgecase that makes crypto amount decimal length greater than multiplier length (#3135) * fix: Handle fiat amount edgecase that makes crypto amount decimal length greater than multiplier length * fix: Switch currency decimal for crypto conversion --- cw_core/lib/parse_fixed.dart | 3 +-- lib/view_model/exchange/exchange_view_model.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index 1323b6c76..695bad60f 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -45,8 +45,7 @@ BigInt parseFixed(String value, int decimals) { var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0"); if (fraction.length > multiplier.length - 1) { - throw FormatException( - "fractional component exceeds decimals, underflow, parseFixed"); + throw FormatException("fractional component exceeds decimals, underflow, parseFixed"); } final wholeValue = BigInt.parse(whole); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index defdd8316..df83af02a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -763,7 +763,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (price == null || price == 0.0) return; final crypto = _enteredAmount / price; - final depositAmountTmp = crypto.toString().withMaxDecimals(receiveCurrency.decimals); + final depositAmountTmp = crypto.toString().withMaxDecimals(depositCurrency.decimals); if (_depositAmount != depositAmountTmp) { changeDepositAmount(amount: depositAmountTmp, isCanonical: true); }