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/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 }; 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"; } 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')); }); }); });