mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 16:43:14 +00:00
handle errors if silent payments psbt validation fails
This commit is contained in:
@@ -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<Transaction> {
|
||||
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) {
|
||||
|
||||
@@ -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<Map.Entry<TransactionInput, PSBTInput>> 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<Map.Entry<TransactionOutput, PSBTOutput>> 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<TransactionInput, PSBTInput> 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<TransactionOutput, PSBTOutput> 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<PSBT> createTask() {
|
||||
return new Task<>() {
|
||||
protected PSBT call() throws PayjoinReceiverException {
|
||||
protected PSBT call() throws PayjoinReceiverException, PSBTProofException {
|
||||
return payjoin.requestPayjoinPSBT(allowOutputSubstitution);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user