if input transactions are not all fetched correctly or had a failure,… (#3353)
What changed, and why it matters
This commit changes how Cake Wallet handles Bitcoin transactions when it cannot fetch all of their 'input' transaction data from the Electrum server. Previously, such transactions were skipped entirely and not shown in the wallet. Now they are still parsed and displayed, but with a warning ('Transaction has missing data') and a null fee. The change also makes the list of input transactions nullable and adds null checks in fee-calculation and replace-by-fee code paths, throwing errors if input data is missing there. This is primarily a robustness/usability fix, but it touches transaction parsing and fee logic, which are security-sensitive areas.
Review the null-handling paths to ensure no arithmetic or UI path can be tricked by a crafted transaction with missing inputs. Verify that the new hasMissingInputTx flag cannot be spoofed by a malicious Electrum server response. Consider adding tests for transactions with missing inputs and for the corrected getTransactionHex hash parameter.
Security signals we found
Transaction parsing now tolerates missing input data, which could affect balance/amount/fee calculations if bounds checks are insufficient.
New bounds check added: input.txIndex >= inputTransaction.outputs.length prevents an out-of-range access when an input transaction is present but malformed/short.
Fee is set to null when inputs are missing, preventing a potentially incorrect fee computed from partial input amounts.
Replace-by-fee and fee-calculation paths now throw on missing inputs rather than silently using null.
A real bug fix: input transaction hex fetch previously used the wrong txid (original hash instead of vin.txId), which could cause input lookup failures.
No explicit security disclosure, CVE, or researcher attribution in commit or supplied references.
Evidence from the diff
The patch modifies cw_bitcoin’s Electrum transaction handling so that missing or failed input fetches no longer cause a transaction to be dropped. ElectrumTransactionBundle.ins becomes List
Changed components
cw_bitcoin/lib/electrum_transaction_info.dartcw_bitcoin/lib/electrum_wallet.dartlib/view_model/dashboard/transaction_list_item.dartInspect captured patch +46 / −29
diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart
index 9e18d0e6..a58829e0 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<BtcTransaction> ins;
+ final List<BtcTransaction?> ins;
final int? time;
final int confirmations;
}
@@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
List<String> inputAddresses = [];
List<String> 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 095d1a45..21b67146 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 = <BtcTransaction>[];
+ final ins = <BtcTransaction?>[];
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(BtcTransaction.fromRaw(inputTransactionHex));
+ ins.add(inputTransactionHex.isEmpty ? null : BtcTransaction.fromRaw(inputTransactionHex));
+ } catch (_) {
+ ins.add(null);
+ }
}
return ElectrumTransactionBundle(
@@ -3204,22 +3214,11 @@ abstract class ElectrumWalletBase
}
}
- final ins = <BtcTransaction>[];
final inputTxids = inputTxidsByHash[txid] ?? const <String>[];
- 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 = <BtcTransaction?>[
+ 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 0a38ff2d..40524631 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";
}
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.