treat an input transaction the server cannot return as unfetched in the fee, spend, outpoint and diagram views
What changed, and why it matters
This commit fixes a bug in the Sparrow Wallet desktop app where, if the Bitcoin server could not return a transaction referenced by a wallet input, the app would crash or show incorrect data. The change makes the app treat such transactions as 'unfetched' and display 'Unknown' or the raw transaction hash instead of throwing exceptions or dereferencing missing data. This is a defensive UI/data-handling fix rather than a cryptographic vulnerability, but it prevents user-facing errors and possible misleading fee/value displays.
No urgent security action required. This is a robustness/UI fix. Users should upgrade to a release containing this commit to avoid crashes or misleading transaction displays when the server cannot return an input transaction. Developers should audit other sites that dereference BlockTransaction.getTransaction() without null checks.
Security signals we found
Null-pointer-dereference crash avoided in transaction fee calculation
Missing input transaction data no longer causes IllegalStateException or incorrect UI output
Fee and total value labels now display 'Unknown' instead of potentially wrong computed values
UI consistency fix across TransactionDiagram, HeadersController, InputController, InputForm, InputsController
Evidence from the diff
The patch adds null checks on BlockTransaction.getTransaction() across five UI controllers. Previously, code assumed that a non-null BlockTransaction in the inputTransactions map always contained a populated Transaction object. When the server returns a BlockTransaction wrapper with a null transaction body, the old code would NPE or compute incorrect fees/outputs. The fix treats blockTransaction != null && blockTransaction.getTransaction() == null as unfetched, returning null/’Unknown’/showing the raw outpoint hash, and avoids dereferencing the missing transaction. It also refactors InputForm.getAddress() to use getReferencedTransactionOutput() for consistency.
Changed components
com.sparrowwallet.sparrow.control.TransactionDiagramcom.sparrowwallet.sparrow.transaction.HeadersControllercom.sparrowwallet.sparrow.transaction.InputControllercom.sparrowwallet.sparrow.transaction.InputFormcom.sparrowwallet.sparrow.transaction.InputsControllerInspect captured patch +29 / −8
### src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
@@ -504,8 +504,8 @@ private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> di
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
tooltip.setText("");
} else {
- if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
- BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
+ BlockTransaction blockTransaction = walletTx.getInputTransactions() == null ? null : walletTx.getInputTransactions().get(input.getHash());
+ if(blockTransaction != null && blockTransaction.getTransaction() != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) input.getIndex());
Address fromAddress = txOutput.getScript().getToAddress();
inputValue = txOutput.getValue();
### src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -655,6 +655,11 @@ private Long calculateFee(Map<Sha256Hash, BlockTransaction> inputTransactions) {
inputTx = headersForm.getInputTransactions().get(input.getOutpoint().getHash());
}
+ if(inputTx != null && inputTx.getTransaction() == null) {
+ fee.setText("Unknown");
+ return null;
+ }
+
if(inputTx == null) {
if(headersForm.allInputsFetched()) {
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
@@ -818,7 +823,7 @@ private WalletTransaction getWalletTransaction(Map<Sha256Hash, BlockTransaction>
private BlockTransactionHashIndex getBlockTransactionInput(Map<Sha256Hash, BlockTransaction> inputTransactions, TransactionInput txInput) {
if(inputTransactions != null) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
- if(blockTransaction != null) {
+ if(blockTransaction != null && blockTransaction.getTransaction() != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) txInput.getOutpoint().getIndex());
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
}
### src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java
@@ -188,10 +188,18 @@ private void initializeInputFields(TransactionInput txInput, PSBTInput psbtInput
}
private void updateOutpoint(Map<Sha256Hash, BlockTransaction> inputTransactions) {
+ TransactionInput txInput = inputForm.getTransactionInput();
+ BlockTransaction fetchedTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
+ if(fetchedTransaction != null && fetchedTransaction.getTransaction() == null) {
+ outpoint.setVisible(true);
+ linkedOutpoint.setVisible(false);
+ outpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex());
+ return;
+ }
+
outpoint.setVisible(false);
linkedOutpoint.setVisible(true);
- TransactionInput txInput = inputForm.getTransactionInput();
linkedOutpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex());
linkedOutpoint.setOnAction(event -> {
BlockTransaction linkedTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
@@ -204,6 +212,10 @@ private void updateSpends(Map<Sha256Hash, BlockTransaction> inputTransactions) {
TransactionInput txInput = inputForm.getTransactionInput();
if(!txInput.isCoinBase()) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
+ if(blockTransaction != null && blockTransaction.getTransaction() == null) {
+ return;
+ }
+
if(blockTransaction == null) {
if(inputForm.getIndex() < inputForm.getMaxInputFetched()) {
throw new IllegalStateException("Could not retrieve block transaction for input #" + inputForm.getIndex());
### src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java
@@ -61,10 +61,9 @@ public boolean isWalletTxo() {
@Override
public Address getAddress() {
TransactionInput txInput = getTransactionInput();
- if(txInput != null && !txInput.isCoinBase() && getInputTransactions() != null) {
- BlockTransaction blockTransaction = getInputTransactions().get(txInput.getOutpoint().getHash());
- if(blockTransaction != null) {
- TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
+ if(txInput != null && !txInput.isCoinBase()) {
+ TransactionOutput output = getReferencedTransactionOutput();
+ if(output != null) {
return output.getScript().getToAddress();
}
}
### src/main/java/com/sparrowwallet/sparrow/transaction/InputsController.java
@@ -133,6 +133,11 @@ private void updateBlockTransactionInputs(Map<Sha256Hash, BlockTransaction> inpu
inputTx = inputsForm.getInputTransactions().get(input.getOutpoint().getHash());
}
+ if(inputTx != null && inputTx.getTransaction() == null) {
+ total.setText("Unknown");
+ return;
+ }
+
if(inputTx == null) {
if(inputsForm.allInputsFetched()) {
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());Why this scored 36/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.