From 7da2ebc760782c50efc2fb33ba6821cbe33d9543 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 26 Aug 2026 12:54:08 +0200 Subject: [PATCH] scope payjoin endpoints to the payment tab and transaction instead of the destination address --- .../sparrowwallet/sparrow/AppServices.java | 19 ++-- .../sparrow/control/TransactionDiagram.java | 20 ++-- .../sparrow/event/SendPaymentsEvent.java | 11 +++ .../transaction/HeadersController.java | 37 ++++---- .../sparrow/wallet/PaymentController.java | 27 ++++-- .../sparrow/wallet/SendController.java | 39 +++++++- .../sparrow/payjoin/PayjoinURITest.java | 95 +++++++++++++++++++ 7 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 src/test/java/com/sparrowwallet/sparrow/payjoin/PayjoinURITest.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 4cafe2b6..cb1c6227 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; 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 argUris = new ArrayList<>(); - private static final Map payjoinURIs = new HashMap<>(); + private static final Map payjoinURIs = new HashMap<>(); private final ChangeListener onlineServicesListener = new ChangeListener<>() { @Override @@ -870,19 +871,21 @@ public class AppServices { 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 @@ public class AppServices { 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()); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 3722cbdb..12c9f00e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/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 labelProperty = new SimpleObjectProperty<>(null); private final ObjectProperty optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY); + private final ObjectProperty payjoinURIProperty = new SimpleObjectProperty<>(null); private boolean expanded; private TransactionDiagram expandedDiagram; private ContextMenu contextMenu; @@ -224,6 +225,7 @@ public class TransactionDiagram extends GridPane { private void updateDerivedDiagram(TransactionDiagram diagram) { diagram.setOptimizationStrategy(getOptimizationStrategy()); + diagram.setPayjoinURI(getPayjoinURI()); diagram.walletTx = walletTx; if(diagram.isExpanded()) { @@ -342,20 +344,12 @@ public class TransactionDiagram extends GridPane { } } - 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> displayedUtxoSets) { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java index 7dda67d7..05d7dac7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java +++ b/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 @@ import java.util.List; public class SendPaymentsEvent { private final Wallet wallet; private final List payments; + private final BitcoinURI bitcoinURI; public SendPaymentsEvent(Wallet wallet, List payments) { + this(wallet, payments, null); + } + + public SendPaymentsEvent(Wallet wallet, List payments, BitcoinURI bitcoinURI) { this.wallet = wallet; this.payments = payments; + this.bitcoinURI = bitcoinURI; } public Wallet getWallet() { @@ -21,4 +28,8 @@ public class SendPaymentsEvent { public List getPayments() { return payments; } + + public BitcoinURI getBitcoinURI() { + return bitcoinURI; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index d274ee80..c049e4ed 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/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 outputIndexLabels = new TreeMap<>(); @Override @@ -459,6 +461,9 @@ public class HeadersController extends TransactionFormController implements Init updateFee(feeAmt); } + payjoinURI = getPayjoinURI(); + transactionDiagram.setPayjoinURI(payjoinURI); + headersForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> { transactionDiagram.update(walletTransaction); }); @@ -487,7 +492,6 @@ public class HeadersController extends TransactionFormController implements Init 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 @@ public class HeadersController extends TransactionFormController implements Init } 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 class HeadersController extends TransactionFormController implements Init 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 class HeadersController extends TransactionFormController implements Init } 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 class HeadersController extends TransactionFormController implements Init if(headersForm.getTransaction().equals(event.getTransaction())) { updateTxId(); updateEditable(headersForm.isEditable()); + registerPayjoinURI(); } } @@ -1847,6 +1847,7 @@ public class HeadersController extends TransactionFormController implements Init if(event.getPsbt().equals(headersForm.getPsbt())) { updateTxId(); headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions())); + registerPayjoinURI(); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index e71c7da2..3b99dd08 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -151,6 +151,8 @@ public class PaymentController extends WalletFormController implements Initializ private final ObjectProperty dnsPaymentProperty = new SimpleObjectProperty<>(); + private final ObjectProperty payjoinURIProperty = new SimpleObjectProperty<>(); + private static final Wallet payNymWallet = new Wallet() { @Override public String getFullDisplayName() { @@ -186,6 +188,10 @@ public class PaymentController extends WalletFormController implements Initializ 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 @@ public class PaymentController extends WalletFormController implements Initializ field.textProperty().addListener(listener); } + public BitcoinURI getPayjoinURI() { + return payjoinURIProperty.get(); + } + public boolean isValidPayment() { try { getPayment(); @@ -752,12 +762,6 @@ public class PaymentController extends WalletFormController implements Initializ } public void clear() { - try { - AppServices.clearPayjoinURI(getRecipientAddress()); - } catch(InvalidAddressException e) { - //ignore - } - address.setText(""); label.setText(""); @@ -773,6 +777,7 @@ public class PaymentController extends WalletFormController implements Initializ payNymProperty.set(null); dnsPaymentProperty.set(null); silentPaymentAddressProperty.set(null); + payjoinURIProperty.set(null); } public void setMaxInput(ActionEvent event) { @@ -832,12 +837,16 @@ public class PaymentController extends WalletFormController implements Initializ setRecipientValueSats(bitcoinURI.getAmount()); setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount()); } - if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) { - AppServices.addPayjoinURI(bitcoinURI); - } + setPayjoinURI(bitcoinURI); sendController.updateTransaction(); } + public void setPayjoinURI(BitcoinURI bitcoinURI) { + if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) { + payjoinURIProperty.set(bitcoinURI); + } + } + private List
getOtherAddresses() { List
otherAddresses = new ArrayList<>(); for(Tab tab : sendController.getPaymentTabs().getTabs()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 4332a30b..26204580 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.crypto.ECKey; 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 class SendController extends WalletFormController implements Initializabl setFeeRate(feeRate); } + transactionDiagram.setPayjoinURI(walletTransaction == null ? null : getPayjoinURI(walletTransaction.getPayments())); transactionDiagram.update(walletTransaction); updatePrivacyAnalysis(walletTransaction); createButton.setDisable(walletTransaction == null || isInsufficientFeeRate()); @@ -978,12 +980,35 @@ public class SendController extends WalletFormController implements Initializabl 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 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 @@ public class SendController extends WalletFormController implements Initializabl private boolean isFakeMixPossible(List 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 payments) { @@ -1170,6 +1195,10 @@ public class SendController extends WalletFormController implements Initializabl 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 class SendController extends WalletFormController implements Initializabl 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 class SendController extends WalletFormController implements Initializabl 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) { diff --git a/src/test/java/com/sparrowwallet/sparrow/payjoin/PayjoinURITest.java b/src/test/java/com/sparrowwallet/sparrow/payjoin/PayjoinURITest.java new file mode 100644 index 00000000..16b6768d --- /dev/null +++ b/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()))); + } +}