diff --git a/drongo b/drongo index 49ecc481..36847e11 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 49ecc4810e7b5301b302a7d188f2b3036ff09e9c +Subproject commit 36847e1170a089976d13426b93d03f7c19b4ae7f diff --git a/lark b/lark index 7e04adda..db408de3 160000 --- a/lark +++ b/lark @@ -1 +1 @@ -Subproject commit 7e04adda357e685f679213913fb2559c58a22dcc +Subproject commit db408de3a1f14cb840e71e9a3003b74c5c0b30ad diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index edc302ca..27d1486a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1986,39 +1986,13 @@ public class AppController implements Initializable { private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData)tab.getUserData(); - if(tabData instanceof TransactionTabData) { - TransactionTabData transactionTabData = (TransactionTabData)tabData; + if(tabData instanceof TransactionTabData transactionTabData) { + if(isExistingTransaction(transactionTabData, transaction, psbt, getTabName(tab))) { + handleTransactionMerge(transactionTabData, psbt, name, tab); + return; + } - //If an exact match bytewise of an existing tab, return that tab - if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) { - if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized()) { - if(!psbt.isFinalized()) { - //As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized - transactionTabData.getPsbt().combine(psbt); - if(name != null && !name.isEmpty()) { - ((Label)tab.getGraphic()).setText(name); - } - - EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt())); - } else { - //If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT - for(int i = 0; i < transactionTabData.getPsbt().getPsbtInputs().size(); i++) { - PSBTInput existingInput = transactionTabData.getPsbt().getPsbtInputs().get(i); - PSBTInput finalizedInput = psbt.getPsbtInputs().get(i); - existingInput.setFinalScriptSig(finalizedInput.getFinalScriptSig()); - existingInput.setFinalScriptWitness(finalizedInput.getFinalScriptWitness()); - existingInput.clearNonFinalFields(); - } - - if(name != null && !name.isEmpty()) { - ((Label)tab.getGraphic()).setText(name); - } - - EventManager.get().post(new PSBTFinalizedEvent(transactionTabData.getPsbt())); - } - } - - tabs.getSelectionModel().select(tab); + if(transactionTabData.getPsbt() != null && transactionTabData.getPsbt().possibleUnverifiableSilentPaymentsTransaction(transaction) && !openUnverifiableTransaction(getTabName(tab))) { return; } } @@ -2085,6 +2059,69 @@ public class AppController implements Initializable { } } + private boolean isExistingTransaction(TransactionTabData transactionTabData, Transaction transaction, PSBT psbt, String tabName) { + PSBT currentPsbt = transactionTabData.getPsbt(); + Transaction currentTransaction = transactionTabData.getTransaction(); + + if(currentPsbt != null && psbt != null && currentPsbt.matches(psbt)) { + return true; + } else if(currentTransaction.getTxId().equals(transaction.getTxId())) { + if(currentTransaction.getWTxId().equals(transaction.getWTxId())) { + return true; + } else if(currentPsbt == null) { + AppServices.showWarningDialog("Suspicious Transaction", + "This transaction has the same txid as the transaction in tab " + tabName + ", but contains different witnesses. It will be opened in a separate tab."); + } + } + + return false; + } + + private void handleTransactionMerge(TransactionTabData transactionTabData, PSBT psbt, String name, Tab tab) { + PSBT currentPsbt = transactionTabData.getPsbt(); + + if(currentPsbt != null && psbt != null && !currentPsbt.isFinalized()) { + if(!psbt.isFinalized()) { + //As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized + try { + currentPsbt.verifyCombinedSignatures(psbt); + currentPsbt.combine(psbt); + setTabName(tab, name); + EventManager.get().post(new PSBTCombinedEvent(currentPsbt)); + } catch(PSBTSignatureException e) { + AppServices.showErrorDialog("Invalid PSBT", e.getMessage()); + } + } else { + //If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT + currentPsbt.copyFinalizedFields(psbt); + setTabName(tab, name); + EventManager.get().post(new PSBTFinalizedEvent(currentPsbt)); + } + } + + tabs.getSelectionModel().select(tab); + } + + private boolean openUnverifiableTransaction(String tabName) { + Optional result = AppServices.showWarningDialog( + "Unverifiable Silent Payments Transaction", + "This transaction contains an unverifiable silent payments output.\n\n" + + "The tab " + tabName + " contains a similar transaction spending to a silent payments address, " + + "but this transaction does not contain enough information to determine if the recipient address is correct.\n\n" + + "Open the transaction in another tab?", ButtonType.YES, ButtonType.NO); + return result.isPresent() && result.get() == ButtonType.YES; + } + + private String getTabName(Tab tab) { + return ((Label)tab.getGraphic()).getText(); + } + + private void setTabName(Tab tab, String name){ + if(name != null && !name.isEmpty()) { + ((Label)tab.getGraphic()).setText(name); + } + } + private ContextMenu getTabContextMenu(Tab tab) { ContextMenu contextMenu = new ContextMenu(); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 618655d2..678bf29b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -6,9 +6,7 @@ import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.*; -import com.sparrowwallet.drongo.psbt.PSBT; -import com.sparrowwallet.drongo.psbt.PSBTInput; -import com.sparrowwallet.drongo.psbt.PSBTProofException; +import com.sparrowwallet.drongo.psbt.*; import com.sparrowwallet.drongo.silentpayments.SilentPayment; import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; import com.sparrowwallet.drongo.uri.BitcoinURI; @@ -1161,8 +1159,13 @@ public class HeadersController extends TransactionFormController implements Init Optional optionalSignedPsbt = dlg.showAndWait(); if(optionalSignedPsbt.isPresent()) { PSBT signedPsbt = optionalSignedPsbt.get(); - headersForm.getPsbt().combine(signedPsbt); - EventManager.get().post(new PSBTCombinedEvent(headersForm.getPsbt())); + try { + headersForm.getPsbt().verifyCombinedSignatures(signedPsbt); + headersForm.getPsbt().combine(signedPsbt); + EventManager.get().post(new PSBTCombinedEvent(headersForm.getPsbt())); + } catch(PSBTSignatureException e) { + AppServices.showErrorDialog("Invalid PSBT", e.getMessage()); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index cff12ebd..8494bdc4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -467,7 +467,7 @@ public class PaymentController extends WalletFormController implements Initializ private void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) { if(!sendController.getWalletForm().getWallet().canSendSilentPayments()) { - Platform.runLater(() -> AppServices.showErrorDialog("Silent Payments Unsupported", "This wallet does not support sending silent payments. Use a single signature software wallet.")); + Platform.runLater(() -> AppServices.showErrorDialog("Silent Payments Unsupported", "This wallet does not support sending silent payments. Use a single signature wallet.")); return; }