verify scanned or loaded transactions match the originating psbt

This commit is contained in:
Craig Raw
2026-07-16 11:55:07 +02:00
parent 9a870ba522
commit 4b8a4594b0
4 changed files with 64 additions and 28 deletions
+1 -1
Submodule drongo updated: 077d2142cc...26fa3b0977
@@ -614,6 +614,10 @@ public class AppController implements Initializable {
}
public void openTransactionFromFile(ActionEvent event) {
openTransactionFromFile(event, null);
}
private void openTransactionFromFile(ActionEvent event, PSBT contextPsbt) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@@ -628,19 +632,21 @@ public class AppController implements Initializable {
List<File> files = fileChooser.showOpenMultipleDialog(window);
if(files != null) {
for(File file : files) {
openTransactionFile(file);
openTransactionFile(file, contextPsbt);
}
}
}
private void openTransactionFile(File file) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof TransactionTabData) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
if(file.equals(transactionTabData.getFile())) {
tabs.getSelectionModel().select(tab);
return;
private void openTransactionFile(File file, PSBT contextPsbt) {
if(contextPsbt == null) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof TransactionTabData) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
if(file.equals(transactionTabData.getFile())) {
tabs.getSelectionModel().select(tab);
return;
}
}
}
}
@@ -651,9 +657,9 @@ public class AppController implements Initializable {
String name = file.getName();
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim(), contextPsbt);
} else {
addTransactionTab(name, file, bytes);
addTransactionTab(name, file, bytes, contextPsbt);
}
} catch(IOException e) {
showErrorDialog("Error opening file", e.getMessage());
@@ -675,7 +681,7 @@ public class AppController implements Initializable {
Optional<String> text = dialog.showAndWait();
if(text.isPresent() && !text.get().isEmpty()) {
try {
addTransactionTab(null, null, text.get().trim());
addTransactionTab(null, null, text.get().trim(), null);
} catch(PSBTParseException e) {
showErrorDialog("Invalid PSBT", e.getMessage());
} catch(TransactionParseException e) {
@@ -1091,7 +1097,7 @@ public class AppController implements Initializable {
verifyOpened = true;
}
} else {
openTransactionFile(file);
openTransactionFile(file, null);
}
}
}
@@ -1931,25 +1937,35 @@ public class AppController implements Initializable {
return Collections.emptyList();
}
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
private void addTransactionTab(String name, File file, String string, PSBT contextPsbt) throws ParseException, PSBTParseException, TransactionParseException {
if(Utils.isBase64(string) && !Utils.isHex(string)) {
addTransactionTab(name, file, Base64.getDecoder().decode(string));
addTransactionTab(name, file, Base64.getDecoder().decode(string), contextPsbt);
} else if(Utils.isHex(string)) {
addTransactionTab(name, file, Utils.hexToBytes(string));
addTransactionTab(name, file, Utils.hexToBytes(string), contextPsbt);
} else {
throw new ParseException("Input is not base64 or hex", 0);
}
}
private void addTransactionTab(String name, File file, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException {
private void addTransactionTab(String name, File file, byte[] bytes, PSBT contextPsbt) throws PSBTParseException, ParseException, TransactionParseException {
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);
addTransactionTab(name, file, psbt);
if(contextPsbt == null || contextPsbt.matches(psbt)) {
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);
addTransactionTab(name, file, transaction);
if(contextPsbt == null || contextPsbt.matches(transaction)) {
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());
}
@@ -3290,9 +3306,9 @@ public class AppController implements Initializable {
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
if(event.getFile() != null) {
openTransactionFile(event.getFile());
openTransactionFile(event.getFile(), event.getContextPsbt());
} else {
openTransactionFromFile(null);
openTransactionFromFile(null, event.getContextPsbt());
}
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
import javafx.stage.Window;
import java.io.File;
@@ -10,15 +11,20 @@ import java.io.File;
public class RequestTransactionOpenEvent {
private final Window window;
private final File file;
private final PSBT contextPsbt;
public RequestTransactionOpenEvent(Window window) {
this.window = window;
this.file = null;
this(window, null, null);
}
public RequestTransactionOpenEvent(Window window, File file) {
this(window, file, null);
}
public RequestTransactionOpenEvent(Window window, File file, PSBT contextPsbt) {
this.window = window;
this.file = file;
this.contextPsbt = contextPsbt;
}
public Window getWindow() {
@@ -28,4 +34,8 @@ public class RequestTransactionOpenEvent {
public File getFile() {
return file;
}
public PSBT getContextPsbt() {
return contextPsbt;
}
}
@@ -1037,9 +1037,19 @@ public class HeadersController extends TransactionFormController implements Init
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.transaction != null) {
EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction));
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.");
}
} else if(result.psbt != null) {
EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt));
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.");
}
} else if(result.seed != null) {
signFromSeed(result.seed);
} else if(result.exception != null) {
@@ -1110,7 +1120,7 @@ public class HeadersController extends TransactionFormController implements Init
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
EventManager.get().post(new RequestTransactionOpenEvent(toggleButton.getScene().getWindow()));
EventManager.get().post(new RequestTransactionOpenEvent(toggleButton.getScene().getWindow(), null, headersForm.getPsbt()));
}
public void signPSBT(ActionEvent event) {
@@ -1882,4 +1892,4 @@ public class HeadersController extends TransactionFormController implements Init
return wallet.getKeystores().stream().map(keystore -> sourceOrder.indexOf(keystore.getSource())).mapToInt(v -> v).max().orElse(0);
}
}
}
}