if input transactions are not all fetched correctly or had a failure,… (#3353)
* 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>
Omar Hatem committed
Jul 8, 2026 at 23:48 UTC
910a5cd9ef18f96f19fc1bbc034913d55f7564db
3 files changed
+46
-29
cw_bitcoin/lib/electrum_transaction_info.dart
+17
-6
@@ -18,7 +18,7 @@ class ElectrumTransactionBundle {
18
{required this.ins, required this.confirmations, this.time});
19
20
final BtcTransaction originalTransaction;
21
- final List<BtcTransaction> ins;
21
+ final List<BtcTransaction?> ins;
22
final int? time;
23
final int confirmations;
24
}
@@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
127
List<String> inputAddresses = [];
128
List<String> outputAddresses = [];
129
130
+ var hasMissingInputTx = false;
131
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
132
final input = bundle.originalTransaction.inputs[i];
132
- final inputTransaction = bundle.ins[i];
133
+ final inputTransaction = i < bundle.ins.length ? bundle.ins[i] : null;
134
+ if (inputTransaction == null || input.txIndex >= inputTransaction.outputs.length) {
135
+ hasMissingInputTx = true;
136
+ continue;
137
+ }
138
final outTransaction = inputTransaction.outputs[input.txIndex];
139
inputAmount += outTransaction.amount.toInt();
140
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
@@ -184,9 +189,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
189
final b = tx.outputs.first.scriptPubKey.toBytes();
190
return b.length == 34 && b[0] == 88 && b[1] == 32;
191
};
187
- final isHogEx = isHogExTx(bundle.originalTransaction) && isHogExTx(bundle.ins.first);
192
+ final firstInput = bundle.ins.isNotEmpty ? bundle.ins.first : null;
193
+ final isHogEx = firstInput != null &&
194
+ isHogExTx(bundle.originalTransaction) &&
195
+ isHogExTx(firstInput);
196
189
- final fee = inputAmount - totalOutAmount;
197
+ final fee = hasMissingInputTx ? null : inputAmount - totalOutAmount;
198
+ final walletCurrency = walletTypeToCryptoCurrency(type);
199
+ final feeMoney = fee != null ? Money.fromInt(fee, walletCurrency) : null;
200
return ElectrumTransactionInfo(type,
201
id: bundle.originalTransaction.txId(),
202
height: height,
@@ -194,11 +204,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
204
isReplaced: false,
205
inputAddresses: inputAddresses,
206
outputAddresses: outputAddresses,
197
- fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
207
+ fee: feeMoney,
208
direction: direction,
199
- amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
209
+ amount: Money.fromInt(amount, walletCurrency),
210
date: date,
211
isHogEx: isHogEx,
212
+ additionalInfo: {'hasMissingInputTx': hasMissingInputTx},
213
confirmations: bundle.confirmations);
214
}
215
cw_bitcoin/lib/electrum_wallet.dart
+23
-23
@@ -2118,6 +2118,9 @@ abstract class ElectrumWalletBase
2118
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
2119
final input = bundle.originalTransaction.inputs[i];
2120
final inputTransaction = bundle.ins[i];
2121
+ if (inputTransaction == null) {
2122
+ throw Exception("Missing input transaction for fee calculation");
2123
+ }
2124
final vout = input.txIndex;
2125
final outTransaction = inputTransaction.outputs[vout];
2126
allInputsAmount += outTransaction.amount.toInt();
@@ -2146,6 +2149,9 @@ abstract class ElectrumWalletBase
2149
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
2150
final input = bundle.originalTransaction.inputs[i];
2151
final inputTransaction = bundle.ins[i];
2152
+ if (inputTransaction == null) {
2153
+ throw Exception("Missing input transaction for replace-by-fee");
2154
+ }
2155
final vout = input.txIndex;
2156
final outTransaction = inputTransaction.outputs[vout];
2157
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
@@ -2434,20 +2440,24 @@ abstract class ElectrumWalletBase
2440
}
2441
2442
final original = BtcTransaction.fromRaw(transactionHex);
2437
- final ins = <BtcTransaction>[];
2443
+ final ins = <BtcTransaction?>[];
2444
2445
for (final vin in original.inputs) {
2440
- final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
2446
+ try {
2447
+ final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
2448
2442
- final String inputTransactionHex;
2449
+ final String inputTransactionHex;
2450
2444
- if (verboseTransaction.isEmpty) {
2445
- inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
2446
- } else {
2447
- inputTransactionHex = verboseTransaction['hex'] as String;
2448
- }
2451
+ if (verboseTransaction.isEmpty) {
2452
+ inputTransactionHex = await electrumClient.getTransactionHex(hash: vin.txId);
2453
+ } else {
2454
+ inputTransactionHex = verboseTransaction['hex'] as String;
2455
+ }
2456
2450
- ins.add(BtcTransaction.fromRaw(inputTransactionHex));
2457
+ ins.add(inputTransactionHex.isEmpty ? null : BtcTransaction.fromRaw(inputTransactionHex));
2458
+ } catch (_) {
2459
+ ins.add(null);
2460
+ }
2461
}
2462
2463
return ElectrumTransactionBundle(
@@ -3204,22 +3214,11 @@ abstract class ElectrumWalletBase
3214
}
3215
}
3216
3207
- final ins = <BtcTransaction>[];
3217
final inputTxids = inputTxidsByHash[txid] ?? const <String>[];
3218
3210
- bool allInputsPresent = true;
3211
- for (final inputTxid in inputTxids) {
3212
- final inTx = parsedInputTxById[inputTxid];
3213
- if (inTx == null) {
3214
- allInputsPresent = false;
3215
- break;
3216
- }
3217
- ins.add(inTx);
3218
- }
3219
-
3220
- if (!allInputsPresent || ins.length != original.inputs.length) {
3221
- continue;
3222
- }
3219
+ final ins = <BtcTransaction?>[
3220
+ for (final inputTxid in inputTxids) parsedInputTxById[inputTxid],
3221
+ ];
3222
3223
bundles[txid] = ElectrumTransactionBundle(
3224
original,
@@ -3796,6 +3795,7 @@ abstract class ElectrumWalletBase
3795
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
3796
final input = bundle.originalTransaction.inputs[i];
3797
final inputTransaction = bundle.ins[i];
3798
+ if (inputTransaction == null) continue;
3799
final vout = input.txIndex;
3800
final outTransaction = inputTransaction.outputs[vout];
3801
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
lib/view_model/dashboard/transaction_list_item.dart
+6
@@ -55,6 +55,12 @@ class TransactionListItem extends ActionListItem with Keyable {
55
}
56
57
String get formattedTitle {
58
+
59
+ if (balanceViewModel.wallet.type == WalletType.bitcoin &&
60
+ transaction.additionalInfo['hasMissingInputTx'] == true) {
61
+ return 'Transaction has missing data';
62
+ }
63
+
64
if (transaction.additionalInfo['autoShield'] == true) {
65
return "Autoshield";
66
}