From 910a5cd9ef18f96f19fc1bbc034913d55f7564db Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 8 Jul 2026 23:48:32 +0300 Subject: [PATCH 1/3] =?UTF-8?q?if=20input=20transactions=20are=20not=20all?= =?UTF-8?q?=20fetched=20correctly=20or=20had=20a=20failure,=E2=80=A6=20(#3?= =?UTF-8?q?353)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * if input transactions are not all fetched correctly or had a failure, still parse the transaction correctly instead of skipping it * handle nullable tx fee * show warning for BTC txs with missing inputs --------- Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com> --- cw_bitcoin/lib/electrum_transaction_info.dart | 23 +++++++--- cw_bitcoin/lib/electrum_wallet.dart | 46 +++++++++---------- .../dashboard/transaction_list_item.dart | 6 +++ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 9e18d0e60..a58829e00 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -18,7 +18,7 @@ class ElectrumTransactionBundle { {required this.ins, required this.confirmations, this.time}); final BtcTransaction originalTransaction; - final List ins; + final List ins; final int? time; final int confirmations; } @@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo { List inputAddresses = []; List outputAddresses = []; + var hasMissingInputTx = false; for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; - final inputTransaction = bundle.ins[i]; + final inputTransaction = i < bundle.ins.length ? bundle.ins[i] : null; + if (inputTransaction == null || input.txIndex >= inputTransaction.outputs.length) { + hasMissingInputTx = true; + continue; + } final outTransaction = inputTransaction.outputs[input.txIndex]; inputAmount += outTransaction.amount.toInt(); if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { @@ -184,9 +189,14 @@ class ElectrumTransactionInfo extends TransactionInfo { final b = tx.outputs.first.scriptPubKey.toBytes(); return b.length == 34 && b[0] == 88 && b[1] == 32; }; - final isHogEx = isHogExTx(bundle.originalTransaction) && isHogExTx(bundle.ins.first); + final firstInput = bundle.ins.isNotEmpty ? bundle.ins.first : null; + final isHogEx = firstInput != null && + isHogExTx(bundle.originalTransaction) && + isHogExTx(firstInput); - final fee = inputAmount - totalOutAmount; + final fee = hasMissingInputTx ? null : inputAmount - totalOutAmount; + final walletCurrency = walletTypeToCryptoCurrency(type); + final feeMoney = fee != null ? Money.fromInt(fee, walletCurrency) : null; return ElectrumTransactionInfo(type, id: bundle.originalTransaction.txId(), height: height, @@ -194,11 +204,12 @@ class ElectrumTransactionInfo extends TransactionInfo { isReplaced: false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, - fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)), + fee: feeMoney, direction: direction, - amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)), + amount: Money.fromInt(amount, walletCurrency), date: date, isHogEx: isHogEx, + additionalInfo: {'hasMissingInputTx': hasMissingInputTx}, confirmations: bundle.confirmations); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 095d1a45e..21b671466 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2118,6 +2118,9 @@ abstract class ElectrumWalletBase for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; + if (inputTransaction == null) { + throw Exception("Missing input transaction for fee calculation"); + } final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; allInputsAmount += outTransaction.amount.toInt(); @@ -2146,6 +2149,9 @@ abstract class ElectrumWalletBase for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; + if (inputTransaction == null) { + throw Exception("Missing input transaction for replace-by-fee"); + } final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; final address = addressFromOutputScript(outTransaction.scriptPubKey, network); @@ -2434,20 +2440,24 @@ abstract class ElectrumWalletBase } final original = BtcTransaction.fromRaw(transactionHex); - final ins = []; + final ins = []; for (final vin in original.inputs) { - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId); + try { + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId); - final String inputTransactionHex; + final String inputTransactionHex; - if (verboseTransaction.isEmpty) { - inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); - } else { - inputTransactionHex = verboseTransaction['hex'] as String; + if (verboseTransaction.isEmpty) { + inputTransactionHex = await electrumClient.getTransactionHex(hash: vin.txId); + } else { + inputTransactionHex = verboseTransaction['hex'] as String; + } + + ins.add(inputTransactionHex.isEmpty ? null : BtcTransaction.fromRaw(inputTransactionHex)); + } catch (_) { + ins.add(null); } - - ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } return ElectrumTransactionBundle( @@ -3204,22 +3214,11 @@ abstract class ElectrumWalletBase } } - final ins = []; final inputTxids = inputTxidsByHash[txid] ?? const []; - bool allInputsPresent = true; - for (final inputTxid in inputTxids) { - final inTx = parsedInputTxById[inputTxid]; - if (inTx == null) { - allInputsPresent = false; - break; - } - ins.add(inTx); - } - - if (!allInputsPresent || ins.length != original.inputs.length) { - continue; - } + final ins = [ + for (final inputTxid in inputTxids) parsedInputTxById[inputTxid], + ]; bundles[txid] = ElectrumTransactionBundle( original, @@ -3796,6 +3795,7 @@ abstract class ElectrumWalletBase for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; + if (inputTransaction == null) continue; final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; final address = addressFromOutputScript(outTransaction.scriptPubKey, network); diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 0a38ff2d2..405246319 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -55,6 +55,12 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedTitle { + + if (balanceViewModel.wallet.type == WalletType.bitcoin && + transaction.additionalInfo['hasMissingInputTx'] == true) { + return 'Transaction has missing data'; + } + if (transaction.additionalInfo['autoShield'] == true) { return "Autoshield"; } From da9f0d62ee188c8694a903a79b22dcbcdc4cfeeb Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:12:24 +0100 Subject: [PATCH 2/3] fix failing test based on resolver refactor (#3384) --- .../parse_address_from_domain_test.dart | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/test/entities/parse_address_from_domain_test.dart b/test/entities/parse_address_from_domain_test.dart index b89852c33..0ae9b8d62 100644 --- a/test/entities/parse_address_from_domain_test.dart +++ b/test/entities/parse_address_from_domain_test.dart @@ -1,4 +1,5 @@ -import 'package:cake_wallet/core/address_resolver/parse_address_from_domain.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; +import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,7 +9,7 @@ void main() { // late MockWalletBase mockWallet; // late MockSettingsStore mockSettingsStore; // late MockBuildContext mockContext; - // late AddressResolver addressResolver; + // late AddressResolver addressResolverUtils; // // setUp(() { // mockYatService = MockYatService(); @@ -19,7 +20,7 @@ void main() { // when(mockWallet.type).thenReturn(WalletType.bitcoin); // when(mockWallet.currency).thenReturn(CryptoCurrency.btc); // - // addressResolver = AddressResolver( + // addressResolverUtils = AddressResolverUtils( // yatService: mockYatService, // wallet: mockWallet, // settingsStore: mockSettingsStore, @@ -30,7 +31,7 @@ void main() { test('extracts Bitcoin address correctly', () { final raw = 'My Bitcoin address is bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq please use it'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.btc, ); @@ -40,7 +41,7 @@ void main() { test('extracts Ethereum address correctly', () { final raw = 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.eth, ); @@ -50,7 +51,7 @@ void main() { test('extracts Monero address correctly', () { final raw = 'XMR: 85s6zfxGAkdCN21h566R8EFDSfThxCrFiEkhw3JEtaXN2DDfahABLXTjRj385Ro7om5saGWJG7iuE6EyW5MYcoz93DLvNqh'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.xmr, ); @@ -61,7 +62,7 @@ void main() { test('extracts Bitcoin Cash address correctly', () { final raw = 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.bch, ); @@ -72,7 +73,7 @@ void main() { test('extracts Nano address correctly', () { final raw = 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.nano, ); @@ -83,13 +84,13 @@ void main() { test('returns null for unsupported currency', () { final raw = 'Some text without an address'; expect( - () => AddressResolver.extractAddressByType( + () => AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.btc, ), returnsNormally); - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.btc, ); @@ -99,7 +100,7 @@ void main() { test('extracts monero address from URI', () { final raw = 'monero_wallet:467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz?spend_key=029c559cd7669f14e91fd835144916009f8697ab5ac5c7f7c06e1ff869c17b0b&view_key=afaf646edbff3d3bcee8efd3383ffe5d20c947040f74e1110b70ca0fbb0ef90d'; - final result = AddressResolver.extractAddressByType( + final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.xmr, requireSurroundingWhitespaces: false); @@ -118,15 +119,15 @@ sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucw #LTC MWEB ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc '''; - final resultXmr = AddressResolver.extractAddressByType( + final resultXmr = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.xmr); expect(resultXmr, '89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc'); - final resultBtc = AddressResolver.extractAddressByType( + final resultBtc = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.btc); expect(resultBtc, 'sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45'); - final resultLtc = AddressResolver.extractAddressByType( + final resultLtc = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.ltc); expect(resultLtc, 'ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc'); @@ -135,7 +136,7 @@ ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88 // test('throws exception for unexpected token', () { // // Create a custom crypto currency that won't have a pattern // final customCurrency = CryptoCurrency('CUSTOM', 'Custom'); - // expect(() => AddressResolver.extractAddressByType( + // expect(() => AddressResolverUtils.extractAddressByType( // raw: 'Some text', // type: customCurrency, // ), throwsException); @@ -144,25 +145,25 @@ ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88 // // group('isEmailFormat', () { // test('returns true for valid email format', () { - // expect(addressResolver.isEmailFormat('user@example.com'), isTrue); - // expect(addressResolver.isEmailFormat('name.surname@domain.co.uk'), isTrue); - // expect(addressResolver.isEmailFormat('user123@subdomain.example.org'), isTrue); + // expect(addressResolverUtils.isEmailFormat('user@example.com'), isTrue); + // expect(addressResolverUtils.isEmailFormat('name.surname@domain.co.uk'), isTrue); + // expect(addressResolverUtils.isEmailFormat('user123@subdomain.example.org'), isTrue); // }); // // test('returns false for invalid email format', () { - // expect(addressResolver.isEmailFormat('user@'), isFalse); - // expect(addressResolver.isEmailFormat('@domain.com'), isFalse); - // expect(addressResolver.isEmailFormat('user@domain'), isFalse); - // expect(addressResolver.isEmailFormat('user.domain.com'), isFalse); - // expect(addressResolver.isEmailFormat('user@domain@com'), isFalse); - // expect(addressResolver.isEmailFormat('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), isFalse); + // expect(addressResolverUtils.isEmailFormat('user@'), isFalse); + // expect(addressResolverUtils.isEmailFormat('@domain.com'), isFalse); + // expect(addressResolverUtils.isEmailFormat('user@domain'), isFalse); + // expect(addressResolverUtils.isEmailFormat('user.domain.com'), isFalse); + // expect(addressResolverUtils.isEmailFormat('user@domain@com'), isFalse); + // expect(addressResolverUtils.isEmailFormat('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), isFalse); // }); // }); // // group('resolve', () { // test('returns ParsedAddress with original text when no resolution is possible', () async { // final text = 'bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'; - // final result = await addressResolver.resolve(mockContext, text, CryptoCurrency.btc); + // final result = await addressResolverUtils.resolve(mockContext, text, CryptoCurrency.btc); // // expect(result, isA()); // expect(result.addresses, [text]); @@ -176,11 +177,11 @@ ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88 group('unstoppableDomains', () { test('contains expected TLDs', () { - expect(AddressResolver.unstoppableDomains, contains('crypto')); - expect(AddressResolver.unstoppableDomains, contains('eth')); - expect(AddressResolver.unstoppableDomains, contains('bitcoin')); - expect(AddressResolver.unstoppableDomains, contains('x')); - expect(AddressResolver.unstoppableDomains, contains('wallet')); + expect(UnstoppableAddressProvider.unstoppableDomains, contains('crypto')); + expect(UnstoppableAddressProvider.unstoppableDomains, contains('eth')); + expect(UnstoppableAddressProvider.unstoppableDomains, contains('bitcoin')); + expect(UnstoppableAddressProvider.unstoppableDomains, contains('x')); + expect(UnstoppableAddressProvider.unstoppableDomains, contains('wallet')); }); }); }); From c9e0ac85d4c3e78428d650c2dc73f32aee02c3a7 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 9 Jul 2026 15:55:28 +0200 Subject: [PATCH 3/3] feat(exchange): add TRX and USDT-on-Tron to Chainflip (#3341) Chainflip added Tron support with asset ids trx.tron and usdt.tron. Enable both for swaps in the Chainflip provider. Tron needs explicit handling because Chainflip's network slug is 'tron', while Cake uses the 'TRX' tag (and null for native TRX), so the generic title.tag normalization can't produce the right ids: - _supported: add trx, usdttrc20 - _normalizeCurrency: map trx -> trx.tron, usdttrc20 -> usdt.tron - _normalizeNetworkName: TRON -> Tron (status lookup in findTradeById) - _toCurrency: reverse-map trx.tron/usdt.tron for trade resolution Co-authored-by: Claude Opus 4.8 (1M context) --- lib/exchange/provider/chainflip_exchange_provider.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart index e82fa6d4a..99b2830cd 100644 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -29,6 +29,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { CryptoCurrency.arbEth, CryptoCurrency.usdcArb, CryptoCurrency.usdtArb, + CryptoCurrency.trx, + CryptoCurrency.usdttrc20, ]; static const _baseURL = 'chainflip-broker.io'; @@ -315,6 +317,11 @@ class ChainflipExchangeProvider extends ExchangeProvider { } String _normalizeCurrency(CryptoCurrency currency) { + // Chainflip uses 'tron' as the network slug, but Cake uses tag 'TRX' (and null + // for native TRX), so these can't go through the generic title.tag logic below. + if (currency == CryptoCurrency.trx) return 'trx.tron'; + if (currency == CryptoCurrency.usdttrc20) return 'usdt.tron'; + final tag = currency.tag?.toLowerCase(); final title = currency.title.toLowerCase(); @@ -330,6 +337,7 @@ class ChainflipExchangeProvider extends ExchangeProvider { 'ETHEREUM' => 'Ethereum', 'ARBITRUM' => 'Arbitrum', 'SOLANA' => 'Solana', + 'TRON' => 'Tron', _ => name }; @@ -350,6 +358,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { 'eth.arb' => CryptoCurrency.arbEth, 'usdc.arb' => CryptoCurrency.usdcArb, 'usdt.arb' => CryptoCurrency.usdtArb, + 'trx.tron' => CryptoCurrency.trx, + 'usdt.tron' => CryptoCurrency.usdttrc20, _ => null };