diff --git a/drongo b/drongo index 6aca78ca..49ecc481 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 6aca78cade1ce6d2eb2455b5cfbf1fb873e003dc +Subproject commit 49ecc4810e7b5301b302a7d188f2b3036ff09e9c diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java index fb4392d4..ff713f3d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java @@ -13,6 +13,7 @@ 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.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.AppServices; @@ -28,8 +29,6 @@ import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; @@ -461,7 +460,11 @@ public class PrivateKeySweepDialog extends Dialog { psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness()); } - setResult(psbt.extractTransaction()); + try { + setResult(psbt.extractTransaction()); + } catch(PSBTProofException e) { + AppServices.showErrorDialog("Invalid Silent Payments Transaction", e.getMessage()); + } } public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) { diff --git a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java index 0c2e9bf5..a1e752e3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java +++ b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java @@ -6,10 +6,7 @@ import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.protocol.TransactionOutput; -import com.sparrowwallet.drongo.psbt.PSBT; -import com.sparrowwallet.drongo.psbt.PSBTInput; -import com.sparrowwallet.drongo.psbt.PSBTOutput; -import com.sparrowwallet.drongo.psbt.PSBTParseException; +import com.sparrowwallet.drongo.psbt.*; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; @@ -38,7 +35,7 @@ public class Payjoin { public Payjoin(BitcoinURI payjoinURI, Wallet wallet, PSBT psbt) { this.payjoinURI = payjoinURI; this.wallet = wallet; - this.psbt = psbt; + this.psbt = psbt.getForExport(); if(payjoinURI.getAddress() == null) { throw new IllegalArgumentException("Payjoin URI must have an address"); @@ -55,7 +52,7 @@ public class Payjoin { } } - public PSBT requestPayjoinPSBT(boolean allowOutputSubstitution) throws PayjoinReceiverException { + public PSBT requestPayjoinPSBT(boolean allowOutputSubstitution) throws PayjoinReceiverException, PSBTProofException { if(!payjoinURI.isPayjoinOutputSubstitutionAllowed()) { allowOutputSubstitution = false; } @@ -66,15 +63,17 @@ public class Payjoin { throw new PayjoinReceiverException("No payjoin URL provided"); } + long additionalFeeContribution = getAdditionalFeeContribution(); + try { - String base64Psbt = psbt.getForExport().getPublicCopy().toBase64String(); + String base64Psbt = psbt.getPublicCopy().toBase64String(); String appendQuery = "v=1&minfeerate=" + AppServices.getMinimumRelayFeeRate(); int changeOutputIndex = getChangeOutputIndex(); long maxAdditionalFeeContribution = 0; if(changeOutputIndex > -1) { appendQuery += "&additionalfeeoutputindex=" + changeOutputIndex; - maxAdditionalFeeContribution = getAdditionalFeeContribution(); + maxAdditionalFeeContribution = additionalFeeContribution; appendQuery += "&maxadditionalfeecontribution=" + maxAdditionalFeeContribution; } @@ -117,15 +116,16 @@ public class Payjoin { } } - private void checkProposal(PSBT original, PSBT proposal, int changeOutputIndex, long maxAdditionalFeeContribution, boolean allowOutputSubstitution) throws PayjoinReceiverException { + private void checkProposal(PSBT original, PSBT proposal, int changeOutputIndex, long maxAdditionalFeeContribution, boolean allowOutputSubstitution) throws PayjoinReceiverException, PSBTProofException { + Transaction originalTx = original.getTransaction(); Queue> originalInputs = new ArrayDeque<>(); for(int i = 0; i < original.getPsbtInputs().size(); i++) { - originalInputs.add(Map.entry(original.getTransaction().getInputs().get(i), original.getPsbtInputs().get(i))); + originalInputs.add(Map.entry(originalTx.getInputs().get(i), original.getPsbtInputs().get(i))); } Queue> originalOutputs = new ArrayDeque<>(); for(int i = 0; i < original.getPsbtOutputs().size(); i++) { - originalOutputs.add(Map.entry(original.getTransaction().getOutputs().get(i), original.getPsbtOutputs().get(i))); + originalOutputs.add(Map.entry(originalTx.getOutputs().get(i), original.getPsbtOutputs().get(i))); } // Checking that the PSBT of the receiver is clean @@ -133,7 +133,6 @@ public class Payjoin { throw new PayjoinReceiverException("Global xpubs should not be included in the receiver's PSBT"); } - Transaction originalTx = original.getTransaction(); Transaction proposalTx = proposal.getTransaction(); // Verify that the transaction version, and nLockTime are unchanged. if(proposalTx.getVersion() != originalTx.getVersion()) { @@ -154,7 +153,7 @@ public class Payjoin { } TransactionInput proposedTxIn = proposedPSBTInput.getInput(); - boolean isOriginalInput = originalInputs.size() > 0 && originalInputs.peek().getKey().getOutpoint().equals(proposedTxIn.getOutpoint()); + boolean isOriginalInput = !originalInputs.isEmpty() && originalInputs.peek().getKey().getOutpoint().equals(proposedTxIn.getOutpoint()); if(isOriginalInput) { Map.Entry originalInput = originalInputs.remove(); TransactionInput originalTxIn = originalInput.getKey(); @@ -223,11 +222,11 @@ public class Payjoin { } TransactionOutput proposedTxOut = proposalTx.getOutputs().get(i); - boolean isOriginalOutput = originalOutputs.size() > 0 && originalOutputs.peek().getKey().getScript().equals(proposedTxOut.getScript()); + boolean isOriginalOutput = !originalOutputs.isEmpty() && originalOutputs.peek().getKey().getScript().equals(proposedTxOut.getScript()); if(isOriginalOutput) { Map.Entry originalOutput = originalOutputs.remove(); if(originalOutput.getKey() == changeOutput) { - var actualContribution = changeOutput.getValue() - proposedTxOut.getValue(); + var actualContribution = originalOutput.getKey().getValue() - proposedTxOut.getValue(); // The amount that was subtracted from the output's value is less than or equal to maxadditionalfeecontribution if(actualContribution > maxAdditionalFeeContribution) { throw new PayjoinReceiverException("The actual contribution is more than maxadditionalfeecontribution"); @@ -245,7 +244,7 @@ public class Payjoin { // That's the payment output, the receiver may have changed it. } else { if(originalOutput.getKey().getValue() > proposedTxOut.getValue()) { - throw new PayjoinReceiverException("The receiver decreased the value of one of the outputs"); + throw new PayjoinReceiverException("The receiver decreased the value of one of the outputs from " + originalOutput.getKey().getValue() + " sats to " + proposedTxOut.getValue() + " sats"); } } @@ -282,17 +281,17 @@ public class Payjoin { return -1; } - private long getAdditionalFeeContribution() { + private long getAdditionalFeeContribution() throws PSBTProofException { return getSingleInputFee(); } - private long getSingleInputFee() { + private long getSingleInputFee() throws PSBTProofException { Transaction transaction = psbt.extractTransaction(); double feeRate = psbt.getFee().doubleValue() / transaction.getVirtualSize(); int vSize = 68; - if(transaction.getInputs().size() > 0) { - TransactionInput input = transaction.getInputs().get(0); + if(!transaction.getInputs().isEmpty()) { + TransactionInput input = transaction.getInputs().getFirst(); vSize = input.getLength() * Transaction.WITNESS_SCALE_FACTOR; vSize += input.getWitness() != null ? input.getWitness().getLength() : 0; vSize = (int)Math.ceil((double)vSize / Transaction.WITNESS_SCALE_FACTOR); @@ -338,7 +337,7 @@ public class Payjoin { @Override protected Task createTask() { return new Task<>() { - protected PSBT call() throws PayjoinReceiverException { + protected PSBT call() throws PayjoinReceiverException, PSBTProofException { return payjoin.requestPayjoinPSBT(allowOutputSubstitution); } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index e41d12af..618655d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -8,6 +8,7 @@ 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.silentpayments.SilentPayment; import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; import com.sparrowwallet.drongo.uri.BitcoinURI; @@ -1187,17 +1188,31 @@ public class HeadersController extends TransactionFormController implements Init } public void extractTransaction(ActionEvent event) { + extractTransaction(); + } + + public boolean extractTransaction() { viewFinalButton.setDisable(true); - Transaction finalTx = headersForm.getPsbt().extractTransaction(); - headersForm.setFinalTransaction(finalTx); - EventManager.get().post(new TransactionExtractedEvent(headersForm.getPsbt(), finalTx)); + try { + Transaction finalTx = headersForm.getPsbt().extractTransaction(); + headersForm.setFinalTransaction(finalTx); + EventManager.get().post(new TransactionExtractedEvent(headersForm.getPsbt(), finalTx)); + return true; + } catch(PSBTProofException e) { + AppServices.showErrorDialog("Invalid Silent Payments Transaction", e.getMessage()); + viewFinalButton.setDisable(false); + return false; + } } public void broadcastTransaction(ActionEvent event) { broadcastButton.setDisable(true); if(headersForm.getPsbt() != null) { - extractTransaction(event); + if(!extractTransaction()) { + broadcastButton.setDisable(false); + return; + } } if(fee.getValue() > 0) { @@ -1363,10 +1378,12 @@ public class HeadersController extends TransactionFormController implements Init File file = fileChooser.showSaveDialog(window); if(file != null) { try { + Transaction finalTx = headersForm.getPsbt().extractTransaction(); try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) { - Transaction finalTx = headersForm.getPsbt().extractTransaction(); writer.print(Utils.bytesToHex(finalTx.bitcoinSerialize())); } + } catch(PSBTProofException e) { + AppServices.showErrorDialog("Invalid Silent Payments Transaction", e.getMessage()); } catch(IOException e) { log.error("Error saving transaction", e); AppServices.showErrorDialog("Error saving transaction", "Cannot write to " + file.getAbsolutePath()); @@ -1387,7 +1404,8 @@ public class HeadersController extends TransactionFormController implements Init EventManager.get().post(new ViewPSBTEvent(payjoinButton.getScene().getWindow(), headersForm.getName() + " Payjoin", null, proposalPsbt)); }); requestPayjoinPSBTService.setOnFailed(failedEvent -> { - AppServices.showErrorDialog("Error Requesting Payjoin Transaction", failedEvent.getSource().getException().getMessage()); + Throwable exception = failedEvent.getSource().getException(); + AppServices.showErrorDialog(exception instanceof PSBTProofException ? "Invalid Silent Payments Transaction" : "Error Requesting Payjoin Transaction", exception.getMessage()); }); requestPayjoinPSBTService.start(); }