scope payjoin endpoints to the payment tab and transaction instead of the destination address
What changed, and why it matters
This commit changes how Sparrow Wallet remembers Payjoin payment instructions. Previously, the app stored Payjoin details keyed only by the destination Bitcoin address. That meant if you later made an unrelated payment to the same address, the app might still treat it as a Payjoin and contact the original Payjoin server. Now, Payjoin details are tied to the specific transaction (using its ID) and the payment tab, so they don't leak across separate payments to the same address. The included tests explicitly check that a later payment to the same address no longer retrieves the old Payjoin URI.
Reviewers should verify that `calculateTxId(false)` is stable across PSBT lifecycle stages (creation, export, signing, finalization) and that the URI is cleared in all cancellation/close paths, not only on broadcast. Users should upgrade to a release containing this commit if they use Payjoin.
Security signals we found
State isolation bug: Payjoin metadata previously shared per address across unrelated transactions
Fix maps Payjoin URI to transaction ID (PSBT txid) instead of destination address
Payjoin URI is now cleared after broadcast, reducing window for reuse
New regression test confirms same-address later transaction does not retrieve old Payjoin URI
No explicit CVE, vendor advisory, or researcher attribution in commit or supplied references
Evidence from the diff
The patch refactors Payjoin URI storage from Map<Address, BitcoinURI> to Map<Sha256Hash, BitcoinURI> keyed by PSBT.getTransaction().calculateTxId(false). It threads the BitcoinURI through SendPaymentsEvent and TransactionDiagram, scopes lookups to the active payment tab’s PaymentController, and clears the stored URI after broadcast. A new test, laterTransactionToTheSameAddressDoesNotRetrievePayjoinURI, demonstrates the bug class: address-scoped storage caused Payjoin metadata to persist across distinct transactions sharing an output address.
Changed components
AppServices.java - Payjoin URI storage/retrieval/clearingSendPaymentsEvent.java - event payload carrying BitcoinURIPaymentController.java - per-tab Payjoin URI propertySendController.java - Payjoin detection scoped to payment tabs and PSBT registrationTransactionDiagram.java - Payjoin URI property propagationHeadersController.java - Payjoin URI lookup and registration by PSBTPayjoinURITest.java - regression testsInspect captured patch +196 / −50
### src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -19,6 +19,7 @@
import com.sparrowwallet.sparrow.net.Auth47;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
@@ -151,7 +152,7 @@ public class AppServices {
private static final List<URI> argUris = new ArrayList<>();
- private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
+ private static final Map<Sha256Hash, BitcoinURI> payjoinURIs = new HashMap<>();
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
@Override
@@ -870,19 +871,21 @@ public static List<Device> getDevices() {
return devices == null ? new ArrayList<>() : devices;
}
- public static BitcoinURI getPayjoinURI(Address address) {
- return payjoinURIs.get(address);
+ public static BitcoinURI getPayjoinURI(PSBT psbt) {
+ return psbt == null ? null : payjoinURIs.get(psbt.getTransaction().calculateTxId(false));
}
- public static void addPayjoinURI(BitcoinURI bitcoinURI) {
+ public static void addPayjoinURI(PSBT psbt, BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
throw new IllegalArgumentException("Not a valid payjoin URI");
}
- payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
+ payjoinURIs.put(psbt.getTransaction().calculateTxId(false), bitcoinURI);
}
- public static void clearPayjoinURI(Address address) {
- payjoinURIs.remove(address);
+ public static void clearPayjoinURI(PSBT psbt) {
+ if(psbt != null) {
+ payjoinURIs.remove(psbt.getTransaction().calculateTxId(false));
+ }
}
public static void clearTransactionHistoryCache(Wallet wallet) {
@@ -1123,7 +1126,7 @@ private static void openBitcoinUri(URI uri) {
if(wallet != null) {
final Wallet sendingWallet = wallet;
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getSpendableUtxos().keySet()), true));
- Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
+ Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()), bitcoinURI)));
}
} catch(Exception e) {
showErrorDialog("Not a valid bitcoin URI", e.getMessage());
### src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
@@ -77,6 +77,7 @@ public class TransactionDiagram extends GridPane {
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
private final ObjectProperty<TransactionDiagramLabel> labelProperty = new SimpleObjectProperty<>(null);
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
+ private final ObjectProperty<BitcoinURI> payjoinURIProperty = new SimpleObjectProperty<>(null);
private boolean expanded;
private TransactionDiagram expandedDiagram;
private ContextMenu contextMenu;
@@ -224,6 +225,7 @@ public void clear() {
private void updateDerivedDiagram(TransactionDiagram diagram) {
diagram.setOptimizationStrategy(getOptimizationStrategy());
+ diagram.setPayjoinURI(getPayjoinURI());
diagram.walletTx = walletTx;
if(diagram.isExpanded()) {
@@ -342,20 +344,12 @@ private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos(Map<BlockTr
}
}
- private BitcoinURI getPayjoinURI() {
- for(Payment payment : walletTx.getPayments()) {
- try {
- Address address = payment.getAddress();
- BitcoinURI bitcoinURI = AppServices.getPayjoinURI(address);
- if(bitcoinURI != null) {
- return bitcoinURI;
- }
- } catch(Exception e) {
- //ignore
- }
- }
+ public BitcoinURI getPayjoinURI() {
+ return payjoinURIProperty.get();
+ }
- return null;
+ public void setPayjoinURI(BitcoinURI payjoinURI) {
+ this.payjoinURIProperty.set(payjoinURI);
}
private Pane getInputsType(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
### src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
+import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -8,10 +9,16 @@
public class SendPaymentsEvent {
private final Wallet wallet;
private final List<Payment> payments;
+ private final BitcoinURI bitcoinURI;
public SendPaymentsEvent(Wallet wallet, List<Payment> payments) {
+ this(wallet, payments, null);
+ }
+
+ public SendPaymentsEvent(Wallet wallet, List<Payment> payments, BitcoinURI bitcoinURI) {
this.wallet = wallet;
this.payments = payments;
+ this.bitcoinURI = bitcoinURI;
}
public Wallet getWallet() {
@@ -21,4 +28,8 @@ public Wallet getWallet() {
public List<Payment> getPayments() {
return payments;
}
+
+ public BitcoinURI getBitcoinURI() {
+ return bitcoinURI;
+ }
}
### src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -251,6 +251,8 @@ public class HeadersController extends TransactionFormController implements Init
private ElectrumServer.TransactionMempoolService transactionMempoolService;
+ private BitcoinURI payjoinURI;
+
private final Map<Integer, String> outputIndexLabels = new TreeMap<>();
@Override
@@ -459,6 +461,9 @@ public LocalDate fromString(String value) {
updateFee(feeAmt);
}
+ payjoinURI = getPayjoinURI();
+ transactionDiagram.setPayjoinURI(payjoinURI);
+
headersForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> {
transactionDiagram.update(walletTransaction);
});
@@ -487,7 +492,6 @@ public LocalDate fromString(String value) {
saveFinalButton.visibleProperty().bind(broadcastButton.visibleProperty().not());
broadcastButton.visibleProperty().bind(AppServices.onlineProperty());
- BitcoinURI payjoinURI = getPayjoinURI();
boolean isPayjoinOriginalTx = payjoinURI != null && headersForm.getPsbt() != null && headersForm.getPsbt().getPsbtInputs().stream().noneMatch(PSBTInput::isFinalized);
payjoinButton.managedProperty().bind(payjoinButton.visibleProperty());
payjoinButton.visibleProperty().set(isPayjoinOriginalTx);
@@ -906,21 +910,13 @@ private void initializeSignButton(Wallet signingWallet) {
}
private BitcoinURI getPayjoinURI() {
- if(headersForm.getPsbt() != null) {
- for(TransactionOutput txOutput : headersForm.getPsbt().getTransaction().getOutputs()) {
- try {
- Address address = txOutput.getScript().getToAddresses()[0];
- BitcoinURI bitcoinURI = AppServices.getPayjoinURI(address);
- if(bitcoinURI != null) {
- return bitcoinURI;
- }
- } catch(Exception e) {
- //ignore
- }
- }
- }
+ return AppServices.getPayjoinURI(headersForm.getPsbt());
+ }
- return null;
+ private void registerPayjoinURI() {
+ if(payjoinURI != null) {
+ AppServices.addPayjoinURI(headersForm.getPsbt(), payjoinURI);
+ }
}
private static class BlockHeightContextMenu extends ContextMenu {
@@ -1283,6 +1279,9 @@ public void broadcastTransaction(ActionEvent event) {
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction(), fee.getValue());
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
+ AppServices.clearPayjoinURI(headersForm.getPsbt());
+ payjoinButton.setVisible(false);
+
//Although we wait for WalletNodeHistoryChangedEvent to indicate tx is in mempool, start a scheduled service to check the script hashes should notifications fail
if(headersForm.getSigningWallet() != null) {
if(transactionMempoolService != null) {
@@ -1438,12 +1437,12 @@ public void saveFinalTransaction(ActionEvent event) {
}
public void getPayjoinTransaction(ActionEvent event) {
- BitcoinURI payjoinURI = getPayjoinURI();
- if(payjoinURI == null) {
+ BitcoinURI currentPayjoinURI = getPayjoinURI();
+ if(currentPayjoinURI == null) {
throw new IllegalStateException("No valid Payjoin URI");
}
- Payjoin payjoin = new Payjoin(payjoinURI, headersForm.getSigningWallet(), headersForm.getPsbt());
+ Payjoin payjoin = new Payjoin(currentPayjoinURI, headersForm.getSigningWallet(), headersForm.getPsbt());
Payjoin.RequestPayjoinPSBTService requestPayjoinPSBTService = new Payjoin.RequestPayjoinPSBTService(payjoin, true);
requestPayjoinPSBTService.setOnSucceeded(successEvent -> {
PSBT proposalPsbt = requestPayjoinPSBTService.getValue();
@@ -1502,6 +1501,7 @@ public void transactionChanged(TransactionChangedEvent event) {
if(headersForm.getTransaction().equals(event.getTransaction())) {
updateTxId();
updateEditable(headersForm.isEditable());
+ registerPayjoinURI();
}
}
@@ -1847,6 +1847,7 @@ public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
updateTxId();
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
+ registerPayjoinURI();
}
}
### src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -151,6 +151,8 @@ public void changed(ObservableValue<? extends String> observable, String oldValu
private final ObjectProperty<DnsPayment> dnsPaymentProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty<BitcoinURI> payjoinURIProperty = new SimpleObjectProperty<>();
+
private static final Wallet payNymWallet = new Wallet() {
@Override
public String getFullDisplayName() {
@@ -186,6 +188,10 @@ public void changed(ObservableValue<? extends String> observable, String oldValu
silentPaymentAddressProperty.set(null);
}
+ if(payjoinURIProperty.get() != null && !newValue.equals(payjoinURIProperty.get().getAddress().toString())) {
+ payjoinURIProperty.set(null);
+ }
+
try {
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
Platform.runLater(() -> updateFromURI(bitcoinURI));
@@ -685,6 +691,10 @@ private void revalidate(TextField field, ChangeListener<String> listener) {
field.textProperty().addListener(listener);
}
+ public BitcoinURI getPayjoinURI() {
+ return payjoinURIProperty.get();
+ }
+
public boolean isValidPayment() {
try {
getPayment();
@@ -752,12 +762,6 @@ public void setPayment(Payment payment) {
}
public void clear() {
- try {
- AppServices.clearPayjoinURI(getRecipientAddress());
- } catch(InvalidAddressException e) {
- //ignore
- }
-
address.setText("");
label.setText("");
@@ -773,6 +777,7 @@ public void clear() {
payNymProperty.set(null);
dnsPaymentProperty.set(null);
silentPaymentAddressProperty.set(null);
+ payjoinURIProperty.set(null);
}
public void setMaxInput(ActionEvent event) {
@@ -832,10 +837,14 @@ private void updateFromURI(BitcoinURI bitcoinURI) {
setRecipientValueSats(bitcoinURI.getAmount());
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount());
}
+ setPayjoinURI(bitcoinURI);
+ sendController.updateTransaction();
+ }
+
+ public void setPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) {
- AppServices.addPayjoinURI(bitcoinURI);
+ payjoinURIProperty.set(bitcoinURI);
}
- sendController.updateTransaction();
}
private List<Address> getOtherAddresses() {
### src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
@@ -12,6 +12,7 @@
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.control.*;
@@ -414,6 +415,7 @@ public Double fromString(String string) {
setFeeRate(feeRate);
}
+ transactionDiagram.setPayjoinURI(walletTransaction == null ? null : getPayjoinURI(walletTransaction.getPayments()));
transactionDiagram.update(walletTransaction);
updatePrivacyAnalysis(walletTransaction);
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
@@ -978,12 +980,35 @@ private void setFeeRatePriority(Double feeRateAmt) {
private boolean isPayjoinTx() {
if(walletTransactionProperty.get() != null) {
- return walletTransactionProperty.get().getPayments().stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
+ return getPayjoinURI(walletTransactionProperty.get().getPayments()) != null;
}
return false;
}
+ private BitcoinURI getPayjoinURI(List<Payment> payments) {
+ for(Payment payment : payments) {
+ BitcoinURI payjoinURI = getPayjoinURI(payment.getAddress());
+ if(payjoinURI != null) {
+ return payjoinURI;
+ }
+ }
+
+ return null;
+ }
+
+ private BitcoinURI getPayjoinURI(Address address) {
+ for(Tab tab : paymentTabs.getTabs()) {
+ PaymentController controller = (PaymentController)tab.getUserData();
+ BitcoinURI payjoinURI = controller.getPayjoinURI();
+ if(payjoinURI != null && payjoinURI.getAddress().equals(address)) {
+ return payjoinURI;
+ }
+ }
+
+ return null;
+ }
+
private Node getSliderThumb() {
return targetBlocks.lookup(".thumb");
}
@@ -1011,7 +1036,7 @@ private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilte
private boolean isFakeMixPossible(List<Payment> payments) {
return utxoSelectorProperty.get() == null && payments.size() == 1
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getNode(KeyPurpose.RECEIVE).getAddress().getScriptType())
- && AppServices.getPayjoinURI(payments.get(0).getAddress()) == null;
+ && getPayjoinURI(payments.get(0).getAddress()) == null;
}
private void updateOptimizationButtons(List<Payment> payments) {
@@ -1170,6 +1195,10 @@ public void createTransaction(ActionEvent event) {
addWalletTransactionNodes();
walletForm.setCreatedWalletTransaction(walletTransaction);
PSBT psbt = walletTransaction.createPSBT();
+ BitcoinURI payjoinURI = getPayjoinURI(walletTransaction.getPayments());
+ if(payjoinURI != null) {
+ AppServices.addPayjoinURI(psbt, payjoinURI);
+ }
EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransaction.getPayments().get(0).getLabel(), null, psbt));
}
@@ -1511,6 +1540,10 @@ public void sendPayments(SendPaymentsEvent event) {
clear(null);
Platform.runLater(() -> {
setPayments(event.getPayments());
+ if(event.getBitcoinURI() != null) {
+ PaymentController controller = (PaymentController)paymentTabs.getTabs().get(0).getUserData();
+ controller.setPayjoinURI(event.getBitcoinURI());
+ }
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
});
}
@@ -1661,7 +1694,7 @@ public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty());
- boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
+ boolean payjoinPresent = getPayjoinURI(userPayments) != null;
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
if(fakeMixPresent) {
### src/test/java/com/sparrowwallet/sparrow/payjoin/PayjoinURITest.java
@@ -0,0 +1,95 @@
+package com.sparrowwallet.sparrow.payjoin;
+
+import com.sparrowwallet.drongo.crypto.ECKey;
+import com.sparrowwallet.drongo.protocol.Script;
+import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.protocol.Sha256Hash;
+import com.sparrowwallet.drongo.protocol.Transaction;
+import com.sparrowwallet.drongo.protocol.TransactionOutput;
+import com.sparrowwallet.drongo.protocol.TransactionWitness;
+import com.sparrowwallet.drongo.psbt.PSBT;
+import com.sparrowwallet.drongo.psbt.PSBTInput;
+import com.sparrowwallet.drongo.uri.BitcoinURI;
+import com.sparrowwallet.sparrow.AppServices;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.List;
+
+public class PayjoinURITest {
+ private static final ECKey SENDER_KEY = ECKey.fromPrivate(BigInteger.valueOf(1001));
+ private static final ECKey CHANGE_KEY = ECKey.fromPrivate(BigInteger.valueOf(1002));
+ private static final ECKey PAYMENT_KEY = ECKey.fromPrivate(BigInteger.valueOf(1003));
+
+ private static final Sha256Hash SENDER_UTXO_HASH = Sha256Hash.wrap("1111111111111111111111111111111111111111111111111111111111111111");
+ private static final Sha256Hash OTHER_UTXO_HASH = Sha256Hash.wrap("3333333333333333333333333333333333333333333333333333333333333333");
+
+ private static final long SENDER_UTXO_VALUE = 200000L;
+ private static final long PAYMENT_VALUE = 100000L;
+ private static final long CHANGE_VALUE = 90000L;
+
+ @AfterEach
+ public void clearPayjoinURIs() {
+ AppServices.clearPayjoinURI(getOriginalPSBT(SENDER_UTXO_HASH));
+ AppServices.clearPayjoinURI(getOriginalPSBT(OTHER_UTXO_HASH));
+ }
+
+ @Test
+ public void signedTransactionReturnedForBroadcastRetrievesPayjoinURI() throws Exception {
+ PSBT original = getOriginalPSBT(SENDER_UTXO_HASH);
+ AppServices.addPayjoinURI(original, getPayjoinURI());
+
+ //The signing device is sent the exported PSBT, and returns it signed
+ PSBT exported = PSBT.fromString(original.getForExport().toBase64String());
+ finalise(exported.getPsbtInputs().get(0), exported.getTransaction());
+ PSBT signed = PSBT.fromString(exported.toBase64String());
+
+ Assertions.assertNotNull(AppServices.getPayjoinURI(signed));
+ }
+
+ @Test
+ public void laterTransactionToTheSameAddressDoesNotRetrievePayjoinURI() throws Exception {
+ PSBT original = getOriginalPSBT(SENDER_UTXO_HASH);
+ AppServices.addPayjoinURI(original, getPayjoinURI());
+
+ //A later payment to the same address spending a different utxo
+ PSBT later = getOriginalPSBT(OTHER_UTXO_HASH);
+
+ Assertions.assertNull(AppServices.getPayjoinURI(later));
+ }
+
+ @Test
+ public void clearedPayjoinURIIsNotRetrieved() throws Exception {
+ PSBT original = getOriginalPSBT(SENDER_UTXO_HASH);
+ AppServices.addPayjoinURI(original, getPayjoinURI());
+ Assertions.assertNotNull(AppServices.getPayjoinURI(original));
+
+ AppServices.clearPayjoinURI(original);
+ Assertions.assertNull(AppServices.getPayjoinURI(original));
+ }
+
+ private BitcoinURI getPayjoinURI() throws Exception {
+ return new BitcoinURI("bitcoin:" + ScriptType.P2WPKH.getAddress(PAYMENT_KEY.getPubKeyHash()) + "?pj=https://payjoin.example.com/pj");
+ }
+
+ private PSBT getOriginalPSBT(Sha256Hash utxoHash) {
+ Transaction transaction = new Transaction();
+ transaction.setVersion(2);
+ transaction.addInput(utxoHash, 0, new Script(new byte[0]));
+ transaction.addOutput(PAYMENT_VALUE, ScriptType.P2WPKH.getOutputScript(PAYMENT_KEY.getPubKeyHash()));
+ transaction.addOutput(CHANGE_VALUE, ScriptType.P2WPKH.getOutputScript(CHANGE_KEY.getPubKeyHash()));
+
+ //Sparrow creates PSBTv2, which is exported as PSBTv0 where no silent payments are present
+ PSBT psbt = new PSBT(transaction);
+ psbt.getPsbtInputs().get(0).setWitnessUtxo(new TransactionOutput(null, SENDER_UTXO_VALUE, ScriptType.P2WPKH.getOutputScript(SENDER_KEY.getPubKeyHash())));
+
+ return psbt;
+ }
+
+ private void finalise(PSBTInput psbtInput, Transaction transaction) {
+ psbtInput.setFinalScriptSig(new Script(new byte[0]));
+ psbtInput.setFinalScriptWitness(new TransactionWitness(transaction, List.of(new byte[71], SENDER_KEY.getPubKey())));
+ }
+}Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.