check all open tabs when verifying a scanned or loaded transaction matches the originating psbt

This commit is contained in:
Craig Raw
2026-07-16 12:32:29 +02:00
parent 4b8a4594b0
commit 078af174af
4 changed files with 66 additions and 23 deletions
@@ -1951,20 +1951,14 @@ public class AppController implements Initializable {
if(PSBT.isPSBT(bytes)) {
//Don't verify signatures here - provided PSBT may omit UTXO data that can be found when combining with an existing PSBT
PSBT psbt = new PSBT(bytes, false);
if(contextPsbt == null || contextPsbt.matches(psbt)) {
if(verifyTransactionContext(contextPsbt, null, psbt, "loaded")) {
addTransactionTab(name, file, psbt);
} else {
AppServices.showErrorDialog("Mismatched Transaction", "The loaded transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
}
} else if(Transaction.isTransaction(bytes)) {
try {
Transaction transaction = new Transaction(bytes);
if(contextPsbt == null || contextPsbt.matches(transaction)) {
if(verifyTransactionContext(contextPsbt, transaction, null, "loaded")) {
addTransactionTab(name, file, transaction);
} else if(contextPsbt.possibleUnverifiableSilentPaymentsTransaction(transaction)) {
AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified.");
} else {
AppServices.showErrorDialog("Mismatched Transaction", "The loaded transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
}
} catch(Exception e) {
throw new TransactionParseException(e.getMessage());
@@ -2198,6 +2192,37 @@ public class AppController implements Initializable {
tabs.getSelectionModel().select(tab);
}
private boolean verifyTransactionContext(PSBT contextPsbt, Transaction transaction, PSBT psbt, String source) {
if(contextPsbt == null || matchesOpenTransactionTab(transaction, psbt)) {
return true;
}
if(psbt == null && contextPsbt.possibleUnverifiableSilentPaymentsTransaction(transaction)) {
AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified.");
} else {
AppServices.showErrorDialog("Mismatched Transaction", "The " + source + " transaction does not match the transaction in this or any other open tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
}
return false;
}
private boolean matchesOpenTransactionTab(Transaction transaction, PSBT psbt) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof TransactionTabData transactionTabData) {
if(transactionTabData.getPsbt() != null) {
if(psbt != null ? transactionTabData.getPsbt().matches(psbt) : transactionTabData.getPsbt().matches(transaction)) {
return true;
}
} else if(transactionTabData.getTransaction().calculateTxId(false).equals(psbt != null ? psbt.getTransaction().getTxId() : transaction.getTxId())) {
return true;
}
}
}
return false;
}
private boolean openUnverifiableTransaction(String tabName) {
Optional<ButtonType> result = AppServices.showWarningDialog(
"Unverifiable Silent Payments Transaction",
@@ -3236,7 +3261,7 @@ public class AppController implements Initializable {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
if(event.getBlockTransaction() != null) {
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
} else {
} else if(verifyTransactionContext(event.getContextPsbt(), event.getTransaction(), null, "scanned")) {
addTransactionTab(event.getTransaction(), event.getInitialView(), event.getInitialIndex());
}
}
@@ -3245,7 +3270,9 @@ public class AppController implements Initializable {
@Subscribe
public void viewPSBT(ViewPSBTEvent event) {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
addTransactionTab(event.getLabel(), event.getFile(), event.getPsbt());
if(verifyTransactionContext(event.getContextPsbt(), null, event.getPsbt(), "scanned")) {
addTransactionTab(event.getLabel(), event.getFile(), event.getPsbt());
}
}
}
@@ -11,18 +11,28 @@ public class ViewPSBTEvent {
private final String label;
private final File file;
private final PSBT psbt;
private final PSBT contextPsbt;
private final TransactionView initialView;
private final Integer initialIndex;
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt) {
this(window, label, file, psbt, TransactionView.HEADERS, null);
this(window, label, file, psbt, null, TransactionView.HEADERS, null);
}
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, PSBT contextPsbt) {
this(window, label, file, psbt, contextPsbt, TransactionView.HEADERS, null);
}
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, TransactionView initialView, Integer initialIndex) {
this(window, label, file, psbt, null, initialView, initialIndex);
}
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, PSBT contextPsbt, TransactionView initialView, Integer initialIndex) {
this.window = window;
this.label = label;
this.file = file;
this.psbt = psbt;
this.contextPsbt = contextPsbt;
this.initialView = initialView;
this.initialIndex = initialIndex;
}
@@ -43,6 +53,10 @@ public class ViewPSBTEvent {
return psbt;
}
public PSBT getContextPsbt() {
return contextPsbt;
}
public TransactionView getInitialView() {
return initialView;
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
@@ -9,13 +10,19 @@ import javafx.stage.Window;
public class ViewTransactionEvent {
private final Window window;
private final Transaction transaction;
private final PSBT contextPsbt;
private final BlockTransaction blockTransaction;
private final TransactionView initialView;
private final Integer initialIndex;
public ViewTransactionEvent(Window window, Transaction transaction) {
this(window, transaction, null);
}
public ViewTransactionEvent(Window window, Transaction transaction, PSBT contextPsbt) {
this.window = window;
this.transaction = transaction;
this.contextPsbt = contextPsbt;
this.blockTransaction = null;
this.initialView = TransactionView.HEADERS;
this.initialIndex = null;
@@ -32,6 +39,7 @@ public class ViewTransactionEvent {
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
this.window = window;
this.transaction = blockTransaction.getTransaction();
this.contextPsbt = null;
this.blockTransaction = blockTransaction;
this.initialView = initialView;
this.initialIndex = initialIndex;
@@ -45,6 +53,10 @@ public class ViewTransactionEvent {
return transaction;
}
public PSBT getContextPsbt() {
return contextPsbt;
}
public BlockTransaction getBlockTransaction() {
return blockTransaction;
}
@@ -1037,19 +1037,9 @@ public class HeadersController extends TransactionFormController implements Init
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.transaction != null) {
if(headersForm.getPsbt().matches(result.transaction)) {
EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction));
} else if(headersForm.getPsbt().possibleUnverifiableSilentPaymentsTransaction(result.transaction)) {
AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified.");
} else {
AppServices.showErrorDialog("Mismatched Transaction", "The scanned transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
}
EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction, headersForm.getPsbt()));
} else if(result.psbt != null) {
if(headersForm.getPsbt().matches(result.psbt)) {
EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt));
} else {
AppServices.showErrorDialog("Mismatched Transaction", "The scanned transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
}
EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt, headersForm.getPsbt()));
} else if(result.seed != null) {
signFromSeed(result.seed);
} else if(result.exception != null) {