show the signatures for a signed transaction opened offline or when the server cannot return every input
What changed, and why it matters
This commit fixes a UI bug in the Sparrow Bitcoin wallet. When a user opened a fully signed transaction while offline, or when the connected server could not provide every previous transaction input, the wallet failed to display the transaction's signature details and the broadcast controls. The patch makes the wallet derive and show the signature progress bar directly from the signed transaction data in those cases, instead of waiting for server data that may never arrive.
Treat as a low-severity reliability/usability fix. Review the drongo submodule bump for any related changes, but the visible diff indicates only UI/control-flow improvements. No immediate security response is required.
Security signals we found
UI state not reflecting truth of signed transaction when offline or server incomplete
Potential user confusion leading to double-signing or broadcast failure
No cryptographic validation changes; purely display/control flow
Evidence from the diff
The change refactors signature display logic in HeadersController and improves event posting in TransactionController. Previously, the signed-transaction form was only built inside blockTransactionFetched when a server response arrived. Now updateSignedTransactionForm() is called both on initialization when offline/no PSBT/no block transaction is present, and when a fetch completes. TransactionController now posts BlockTransactionFetchedEvent on the JavaFX Platform thread and, when some input transactions cannot be retrieved, posts a partial event over an empty range so the UI can still render what is known. A minor UI fix moves the CSS style class into the SignaturesProgressBar constructor.
Changed components
Sparrow Wallet desktop UIHeadersControllerTransactionControllerSignaturesProgressBardrongo subprojectInspect captured patch +58 / −44
### drongo
@@ -1 +1 @@
-Subproject commit 59f43e3aa3adeafeed2f1792054b4d2f65014434
+Subproject commit d66694369f492fc79827758987c5a1c4968f1a9d
### src/main/java/com/sparrowwallet/sparrow/control/SignaturesProgressBar.java
@@ -24,13 +24,13 @@
public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.SignatureProgressSegment> {
public SignaturesProgressBar() {
+ getStyleClass().add("signatures-progress-bar");
setOrientation(Orientation.HORIZONTAL);
setSegmentViewFactory(SignatureProgressSegmentView::new);
setInfoNodeFactory(segment -> segment.getKeystore() == null ? null : new SignatureProgressSegmentLabel(segment.getKeystore().getLabel()));
}
public void initialize(ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap, int threshold) {
- getStyleClass().add("signatures-progress-bar");
getSegments().clear();
List<Keystore> signedKeystores = new ArrayList<>(signatureKeystoreMap.values());
### src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -580,6 +580,11 @@ public SigHash fromString(String string) {
});
blockchainForm.setDynamicUpdate(this);
+
+ //Offline there is no status to fetch, so derive the signed transaction form directly here
+ if(headersForm.getPsbt() == null && headersForm.getBlockTransaction() == null && !AppServices.isConnected()) {
+ updateSignedTransactionForm();
+ }
}
private void requestOpenWallets() {
@@ -827,7 +832,7 @@ private void updateBlockchainForm(BlockTransaction blockTransaction, Integer cur
blockchainForm.setVisible(true);
updateEditable(false);
- if(Sha256Hash.ZERO_HASH.equals(blockTransaction.getBlockHash()) && blockTransaction.getHeight() == 0 && headersForm.getSigningWallet() == null) {
+ if(Sha256Hash.ZERO_HASH.equals(blockTransaction.getBlockHash()) && blockTransaction.getHeight() == 0 && headersForm.getPsbt() == null) {
//A zero block hash indicates that this blocktransaction is incomplete and the height is likely incorrect if we are not sending a tx
blockStatus.setText("Unknown");
} else if(currentHeight == null) {
@@ -1511,42 +1516,7 @@ public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || headersForm.getBlockTransaction() == null)) {
updateBlockchainForm(event.getBlockTransaction(), AppServices.getCurrentBlockHeight());
} else if(headersForm.getPsbt() == null && headersForm.getBlockTransaction() == null) {
- boolean isSigned = true;
- ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>());
- for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
- List<TransactionSignature> signatures = txInput.hasWitness() ? txInput.getWitness().getSignatures() : txInput.getScriptSig().getSignatures();
-
- if(signatures.isEmpty()) {
- isSigned = false;
- break;
- }
-
- if(signatureKeystoreMap.isEmpty()) {
- for(int i = 0; i < signatures.size(); i++) {
- signatureKeystoreMap.put(signatures.get(i), new Keystore("Keystore " + (i+1)));
- }
- }
- }
-
- if(isSigned) {
- blockchainForm.setVisible(false);
- signaturesForm.setVisible(true);
- broadcastButtonBox.setVisible(true);
- viewFinalButton.setDisable(true);
-
- if(headersForm.getSigningWallet() == null) {
- for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
- if(wallet.canSign(headersForm.getTransaction())) {
- headersForm.setSigningWallet(wallet);
- break;
- }
- }
- }
-
- if(headersForm.getSigningWallet() == null) {
- signaturesProgressBar.initialize(signatureKeystoreMap, signatureKeystoreMap.size());
- }
- }
+ updateSignedTransactionForm();
}
if(!event.getInputTransactions().isEmpty()) {
@@ -1564,6 +1534,45 @@ public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
}
}
+ private void updateSignedTransactionForm() {
+ boolean isSigned = true;
+ ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>());
+ for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
+ List<TransactionSignature> signatures = txInput.hasWitness() ? txInput.getWitness().getSignatures() : txInput.getScriptSig().getSignatures();
+
+ if(signatures.isEmpty()) {
+ isSigned = false;
+ break;
+ }
+
+ if(signatureKeystoreMap.isEmpty()) {
+ for(int i = 0; i < signatures.size(); i++) {
+ signatureKeystoreMap.put(signatures.get(i), new Keystore("Keystore " + (i+1)));
+ }
+ }
+ }
+
+ if(isSigned) {
+ blockchainForm.setVisible(false);
+ signaturesForm.setVisible(true);
+ broadcastButtonBox.setVisible(true);
+ viewFinalButton.setDisable(true);
+
+ if(headersForm.getSigningWallet() == null) {
+ for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
+ if(wallet.canSign(headersForm.getTransaction())) {
+ headersForm.setSigningWallet(wallet);
+ break;
+ }
+ }
+ }
+
+ if(headersForm.getSigningWallet() == null) {
+ signaturesProgressBar.initialize(signatureKeystoreMap, signatureKeystoreMap.size());
+ }
+ }
+ }
+
@Subscribe
public void transactionFetchFailed(TransactionFetchFailedEvent event) {
if(event.getTransaction().getTxId().equals(headersForm.getTransaction().getTxId())
### src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java
@@ -389,12 +389,13 @@ private void fetchThisAndInputBlockTransactions(int indexStart, int indexEnd) {
}
}
+ final BlockTransaction walletBlockTx = blockTx;
if(inputReferences.isEmpty() && (getPSBT() != null || blockTx != null)) {
allInputsFetchedFromWallet = true;
transactionsFetched = true;
- EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, inputTransactions, 0, getTransaction().getInputs().size()));
+ Platform.runLater(() -> EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), walletBlockTx, inputTransactions, 0, getTransaction().getInputs().size())));
} else if(!AppServices.isConnected()) {
- EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, Collections.emptyMap(), 0, getTransaction().getInputs().size()));
+ Platform.runLater(() -> EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), walletBlockTx, Collections.emptyMap(), 0, getTransaction().getInputs().size())));
} else if(AppServices.isConnected() && indexStart < getTransaction().getInputs().size()) {
Set<Sha256Hash> references = new HashSet<>();
if(getPSBT() == null) {
@@ -430,12 +431,16 @@ private void fetchThisAndInputBlockTransactions(int indexStart, int indexEnd) {
}
references.remove(getTransaction().getTxId());
- if (!references.isEmpty()) {
- log.warn("Failed to retrieve all referenced input transactions, aborting transaction fetch");
+ final BlockTransaction finalBlockTx = thisBlockTx;
+ if(!references.isEmpty()) {
+ //Post what was retrieved over an empty range, since the inputs that are missing have not been fetched
+ log.warn("Failed to retrieve all referenced input transactions");
+ Platform.runLater(() -> {
+ EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), finalBlockTx, retrievedInputTransactions, indexStart, indexStart));
+ });
return;
}
- final BlockTransaction finalBlockTx = thisBlockTx;
Platform.runLater(() -> {
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), finalBlockTx, retrievedInputTransactions, indexStart, maxIndex));
});Why this scored 17/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.