add initial sending to silent payments support
What changed, and why it matters
This commit adds the first version of 'silent payments' sending support to the Sparrow Wallet desktop app. Silent payments are a newer Bitcoin privacy technique where the recipient's address is not directly visible on the blockchain. The change touches many UI and transaction-building files to recognize silent payment addresses, build transactions for them, display them correctly, and disable RBF (Replace-By-Fee) for those transactions. It also converts PSBTs to version 0 before export and makes small cleanups. There is no direct evidence in the commit or supplied references that this fixes a known security vulnerability; it reads as a feature addition with some defensive checks.
Treat this as a feature commit rather than an urgent security patch. Reviewers should inspect the drongo submodule update for the SilentPaymentAddress and SilentPayment classes, verify that dummy addresses cannot be confused with real outputs, confirm that RBF disable logic is enforced end-to-end, and test that PSBT v0 conversion does not strip fields needed by hardware signers. Users should upgrade only after normal release testing; no immediate action is indicated by the commit alone.
Security signals we found
New cryptographic address scheme (silent payments) integrated into transaction construction and signing flow
RBF explicitly disabled for silent payment sends to prevent cross-device address recomputation failures
PSBT version downgrade to v0 on import/export to improve compatibility
DNS payment cache now distinguishes legacy addresses from silent payment addresses
Signing flow recomputes silent payment output scripts and refreshes UI via event
Validation gate prevents sending silent payments from unsupported wallet types
Evidence from the diff
The commit introduces sending support for BIP-352 silent payments. Key changes: (1) PaymentController now parses SilentPaymentAddress strings, validates wallet support (single-sig software wallets), and constructs SilentPayment payment objects using a dummy address placeholder. (2) SendController disables RBF when any payment is a SilentPayment to avoid address recomputation issues across devices. (3) AppController converts imported PSBTs to v0 before tab creation and updates DNS payment cache handling for silent payment addresses. (4) Transaction diagram/output controllers display silent payment addresses and react to TransactionOutputsChangedEvent after signing computes real output scripts. (5) EntryCell allows RBF/fee-bump on silent payment transactions only if the wallet deems it safe. (6) PSBT export paths now call getForExport() before serialization. The drongo submodule is updated but its diff is not shown. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
Sparrow Wallet desktop applicationPaymentController / SendController (send tab)AppController (PSBT import/export and DNS payment cache)TransactionDiagram / OutputController / HeadersController (transaction display and signing)EntryCell (RBF/fee bump context menu)SendToManyDialog (CSV/batch payment parsing)drongo submodule (silent payments library implementation)Inspect captured patch +438 / −148
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index 84aa006..4dbe740 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.*;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
@@ -822,10 +823,10 @@ public class AppController implements Initializable {
try(FileOutputStream outputStream = new FileOutputStream(file)) {
if(asText) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
- writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
+ writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
writer.flush();
} else {
- outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
+ outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
}
} catch(IOException e) {
log.error("Error saving PSBT", e);
@@ -848,7 +849,7 @@ public class AppController implements Initializable {
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
- String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
+ String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
ClipboardContent content = new ClipboardContent();
content.putString(data);
@@ -862,7 +863,7 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
- byte[] psbtBytes = transactionTabData.getPsbt().serialize();
+ byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
@@ -1897,6 +1898,11 @@ public class AppController implements Initializable {
}
private void addTransactionTab(String name, File file, PSBT psbt) {
+ //Convert to PSBTv0 first
+ if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
+ psbt.convertVersion(0);
+ }
+
//Add any missing previous outputs if available in open wallets
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getUtxo() == null) {
@@ -1920,13 +1926,32 @@ public class AppController implements Initializable {
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty() && psbtOutput.getScript() != null) {
Address address = psbtOutput.getScript().getToAddress();
- if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
- try {
- Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
- optDnsPayment.ifPresent(dnsPayment -> DnsPaymentCache.putDnsPayment(address, dnsPayment));
- } catch(Exception e) {
- log.debug("Error resolving DNS payment", e);
- }
+ if(address != null) {
+ Optional<SilentPaymentAddress> optSilentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
+ .map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst();
+ optSilentPaymentAddress.ifPresentOrElse(silentPaymentAddress -> {
+ if(DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
+ try {
+ Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
+ if(optDnsPayment.isPresent() && optDnsPayment.get().hasSilentPaymentAddress()) {
+ DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
+ }
+ } catch(Exception e) {
+ log.debug("Error resolving DNS payment", e);
+ }
+ }
+ }, () -> {
+ if(DnsPaymentCache.getDnsPayment(address) == null) {
+ try {
+ Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
+ if(optDnsPayment.isPresent() && optDnsPayment.get().hasAddress()) {
+ DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
+ }
+ } catch(Exception e) {
+ log.debug("Error resolving DNS payment", e);
+ }
+ }
+ });
}
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
index 2e79d8f..8c9ff60 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -835,8 +835,8 @@ public class AppServices {
}
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
- if(bitcoinURI.getPayjoinUrl() == null) {
- throw new IllegalArgumentException("Not a payjoin URI");
+ if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
+ throw new IllegalArgumentException("Not a valid payjoin URI");
}
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
index 0c925ce..7df648a 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -5,6 +5,8 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*;
+import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null);
setGraphic(null);
} else {
- if(entry instanceof TransactionEntry) {
- TransactionEntry transactionEntry = (TransactionEntry)entry;
+ if(entry instanceof TransactionEntry transactionEntry) {
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@@ -101,7 +102,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
- if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
+ if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
setGraphic(actionBox);
- } else if(entry instanceof NodeEntry) {
- NodeEntry nodeEntry = (NodeEntry)entry;
+ } else if(entry instanceof NodeEntry nodeEntry) {
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
@@ -163,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null);
setGraphic(new HBox());
}
- } else if(entry instanceof HashIndexEntry) {
- HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
+ } else if(entry instanceof HashIndexEntry hashIndexEntry) {
setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
@@ -212,13 +211,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
+ boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
- .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
+ .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
@@ -243,6 +243,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList());
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
+ boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
@@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups);
- while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
+ while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
//If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)";
}
- if(txOutput.getScript().getToAddress() != null) {
+ Address address = txOutput.getScript().getToAddress();
+ if(address != null) {
+ long value = txOutput.getValue();
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
- return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
+ boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
+ SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
+ return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
}
return null;
@@ -337,7 +342,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
- Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
+ Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
}
private static Double getMaxFeeRate() {
@@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Payment payment = new Payment(freshAddress, label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
- Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
+ Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
}
- private static boolean canRBF(BlockTransaction blockTransaction) {
- return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
+ private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
+ return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
}
private static boolean canSignMessage(WalletNode walletNode) {
@@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
}
- tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
+ tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
}
return tooltip;
@@ -544,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
+ Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
@@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
});
getItems().add(viewTransaction);
- if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
+ if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
@@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(increaseFee);
}
- if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
+ if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
index bad1016..d1cd0d7 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
@@ -6,6 +6,8 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Transaction;
+import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -30,7 +32,7 @@ import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit;
private final SpreadsheetView spreadsheetView;
- public static final AddressCellType ADDRESS = new AddressCellType();
+ public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
this.bitcoinUnit = bitcoinUnit;
@@ -92,7 +94,8 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
for(int row = 0; row < grid.getRowCount(); ++row) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
- SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
+ SendToAddress sendToAddress = SendToAddress.fromPayment(payments.get(row));
+ SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
addressCell.getStyleClass().add("fixed-width");
list.add(addressCell);
@@ -123,7 +126,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
String firstLabel = null;
for(int row = 0; row < grid.getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
- Address address = (Address)rowCells.get(0).getItem();
+ SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
Double value = (Double)rowCells.get(1).getItem();
String label = (String)rowCells.get(2).getItem();
if(firstLabel == null) {
@@ -133,12 +136,12 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
label = firstLabel;
}
- if(address != null && value != null) {
+ if(sendToAddress != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
- payments.add(new Payment(address, label, value.longValue(), false));
+ payments.add(sendToAddress.toPayment(label, value.longValue(), false));
}
}
@@ -183,9 +186,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} else {
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
}
- Address address = Address.fromString(csvReader.get(0));
String label = csvReader.get(2);
- csvPayments.add(new Payment(address, label, amount, false));
+ try {
+ SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
+ csvPayments.add(new SilentPayment(silentPaymentAddress, label, amount, false));
+ } catch(Exception e) {
+ Address address = Address.fromString(csvReader.get(0));
+ csvPayments.add(new Payment(address, label, amount, false));
+ }
} catch(NumberFormatException e) {
//ignore and continue - probably a header line
} catch(InvalidAddressException e) {
@@ -221,16 +229,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
}
- public static class AddressCellType extends SpreadsheetCellType<Address> {
- public AddressCellType() {
- this(new StringConverterWithFormat<>(new AddressStringConverter()) {
+ public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
+ public SendToAddressCellType() {
+ this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
@Override
- public String toString(Address item) {
+ public String toString(SendToAddress item) {
return toStringFormat(item, ""); //$NON-NLS-1$
}
@Override
- public Address fromString(String str) {
+ public SendToAddress fromString(String str) {
if(str == null || str.isEmpty()) { //$NON-NLS-1$
return null;
} else {
@@ -239,7 +247,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
@Override
- public String toStringFormat(Address item, String format) {
+ public String toStringFormat(SendToAddress item, String format) {
try {
if(item == null) {
return ""; //$NON-NLS-1$
@@ -253,7 +261,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
});
}
- public AddressCellType(StringConverter<Address> converter) {
+ public SendToAddressCellType(StringConverter<SendToAddress> converter) {
super(converter);
}
@@ -263,7 +271,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
- final Address value) {
+ final SendToAddress value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value);
return cell;
@@ -276,7 +284,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
@Override
public boolean match(Object value, Object... options) {
- if(value instanceof Address)
+ if(value instanceof SendToAddress)
return true;
else {
try {
@@ -289,9 +297,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
@Override
- public Address convertValue(Object value) {
- if(value instanceof Address)
- return (Address)value;
+ public SendToAddress convertValue(Object value) {
+ if(value instanceof SendToAddress)
+ return (SendToAddress)value;
else {
try {
return converter.fromString(value == null ? null : value.toString());
@@ -302,13 +310,64 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
@Override
- public String toString(Address item) {
+ public String toString(SendToAddress item) {
return converter.toString(item);
}
@Override
- public String toString(Address item, String format) {
- return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
+ public String toString(SendToAddress item, String format) {
+ return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
}
};
+
+ public static class SendToAddress {
+ private final Address address;
+ private final SilentPaymentAddress silentPaymentAddress;
+
+ public SendToAddress(Address address) {
+ this.address = address;
+ this.silentPaymentAddress = null;
+ }
+
+ public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
+ this.address = null;
+ this.silentPaymentAddress = silentPaymentAddress;
+ }
+
+ public String toString() {
+ return silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString();
+ }
+
+ public static SendToAddress fromPayment(Payment payment) {
+ return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
+ }
+
+ public Payment toPayment(String label, long value, boolean sendMax) {
+ if(silentPaymentAddress != null) {
+ return new SilentPayment(silentPaymentAddress, label, value, sendMax);
+ } else {
+ return new Payment(address, label, value, sendMax);
+ }
+ }
+ }
+
+ private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
+ private final AddressStringConverter addressStringConverter = new AddressStringConverter();
+
+ @Override
+ public SendToAddress fromString(String value) {
+ try {
+ SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
+ return new SendToAddress(silentPaymentAddress);
+ } catch(Exception e) {
+ Address address = addressStringConverter.fromString(value);
+ return address == null ? null : new SendToAddress(address);
+ }
+ }
+
+ @Override
+ public String toString(SendToAddress value) {
+ return value.toString();
+ }
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
index e19b37c..bddf074 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
@@ -8,6 +8,8 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
+import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*;
@@ -202,7 +204,7 @@ public class TransactionDiagram extends GridPane {
VBox messagePane = new VBox();
messagePane.setPrefHeight(getDiagramHeight());
- messagePane.setPadding(new Insets(0, 10, 0, 280));
+ messagePane.setPadding(new Insets(0, 10, 0, 10));
messagePane.setAlignment(Pos.CENTER);
messagePane.getChildren().add(createSpacer());
@@ -676,7 +678,8 @@ public class TransactionDiagram extends GridPane {
double width = 140.0;
long sum = walletTx.getTotal();
- List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
+ List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
+ .map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
@@ -720,16 +723,16 @@ public class TransactionDiagram extends GridPane {
for(Payment payment : displayedPayments) {
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
- Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
+ Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
- DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment.getAddress());
+ DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to "
- + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
+ + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
@@ -754,9 +757,13 @@ public class TransactionDiagram extends GridPane {
paymentBox.getChildren().addAll(region, amountLabel);
}
- Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
- PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
- outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode));
+ if(payment instanceof SilentPayment silentPayment) {
+ outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
+ } else {
+ Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
+ PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
+ outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
+ }
}
Set<Integer> seenIndexes = new HashSet<>();
@@ -820,7 +827,7 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer());
- ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode);
+ ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
outputLabelControl.setContextMenu(contextMenu);
}
@@ -995,8 +1002,11 @@ public class TransactionDiagram extends GridPane {
}
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
- List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
- TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
+ List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
+ .map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
+ TransactionOutput output = addressOutputs.stream()
+ .filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
+ .findFirst().orElseThrow();
return addressOutputs.indexOf(output);
}
@@ -1146,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
}
public String toString() {
- return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
+ return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
}
}
@@ -1155,25 +1165,27 @@ public class TransactionDiagram extends GridPane {
public Address address;
public long amount;
public PaymentCode paymentCode;
+ public SilentPaymentAddress silentPaymentAddress;
public OutputNode(Pane outputLabel, Address address, long amount) {
- this(outputLabel, address, amount, null);
+ this(outputLabel, address, amount, null, null);
}
- public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
+ public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
this.outputLabel = outputLabel;
this.address = address;
this.amount = amount;
this.paymentCode = paymentCode;
+ this.silentPaymentAddress = silentPaymentAddress;
}
}
private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Address address, long value) {
- this(address, value, null);
+ this(address, value, null, null);
}
- public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
+ public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
if(address != null) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(event -> {
@@ -1221,6 +1233,17 @@ public class TransactionDiagram extends GridPane {
});
getItems().add(copyPaymentCode);
}
+
+ if(silentPaymentAddress != null) {
+ MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
+ copySilentPaymentAddress.setOnAction(AE -> {
+ hide();
+ ClipboardContent content = new ClipboardContent();
+ content.putString(silentPaymentAddress.toString());
+ Clipboard.getSystemClipboard().setContent(content);
+ });
+ getItems().add(copySilentPaymentAddress);
+ }
}
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
index c87acb5..76432d7 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
@@ -1,7 +1,5 @@
package com.sparrowwallet.sparrow.control;
-import com.sparrowwallet.drongo.dns.DnsPayment;
-import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -208,9 +206,7 @@ public class TransactionDiagramLabel extends HBox {
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
- DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment.getAddress());
- String recipient = dnsPayment == null ? payment.getAddress().toString() : dnsPayment.toString();
- String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + recipient;
+ String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
return getOutputLabel(glyph, text);
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java
index 4816820..e67211a 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java
@@ -17,12 +17,13 @@ public class SpendUtxoEvent {
private final boolean requireAllUtxos;
private final BlockTransaction replacedTransaction;
private final PaymentCode paymentCode;
+ private final boolean allowPaymentChanges;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
- this(wallet, utxos, null, null, null, false, null);
+ this(wallet, utxos, null, null, null, false, null, true);
}
- public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
+ public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
@@ -31,6 +32,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = requireAllUtxos;
this.replacedTransaction = replacedTransaction;
this.paymentCode = null;
+ this.allowPaymentChanges = allowPaymentChanges;
}
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
@@ -42,6 +44,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = false;
this.replacedTransaction = null;
this.paymentCode = paymentCode;
+ this.allowPaymentChanges = false;
}
public Wallet getWallet() {
@@ -75,4 +78,8 @@ public class SpendUtxoEvent {
public PaymentCode getPaymentCode() {
return paymentCode;
}
+
+ public boolean allowPaymentChanges() {
+ return allowPaymentChanges;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputsChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputsChangedEvent.java
new file mode 100644
index 0000000..acda27b
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputsChangedEvent.java
@@ -0,0 +1,9 @@
+package com.sparrowwallet.sparrow.event;
+
+import com.sparrowwallet.drongo.protocol.Transaction;
+
+public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
+ public TransactionOutputsChangedEvent(Transaction transaction) {
+ super(transaction);
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java
index 141a899..76f58b1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java
@@ -624,7 +624,8 @@ public class PayNymController {
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false));
List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
- return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs);
+ return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null,
+ AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
}
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
index 979649d..5fa7e9c 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
+import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
@@ -57,7 +59,6 @@ import tornadofx.control.Fieldset;
import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form;
-import javax.swing.text.html.Option;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
@@ -640,6 +641,7 @@ public class HeadersController extends TransactionFormController implements Init
}
List<Payment> payments = new ArrayList<>();
+ List<WalletTransaction.Output> outputs = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
@@ -658,6 +660,7 @@ public class HeadersController extends TransactionFormController implements Init
changeMap.put(changeNode, txOutput.getValue());
}
}
+ outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue()));
} else {
Payment.Type paymentType = Payment.Type.DEFAULT;
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
@@ -668,24 +671,33 @@ public class HeadersController extends TransactionFormController implements Init
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
- try {
- Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
+ Address address = txOutput.getScript().getToAddress();
+ SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
+ label = receivedTxo != null ? receivedTxo.getLabel() : label;
+ if(address != null || silentPaymentAddress != null) {
+ Payment payment = (silentPaymentAddress == null ?
+ new Payment(address, label, txOutput.getValue(), false, paymentType) :
+ new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) {
- Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
+ Optional<String> optLabel = createdTx.getPayments().stream()
+ .filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(silentPaymentAddress) :
+ pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
if(optLabel.isPresent()) {
payment.setLabel(optLabel.get());
outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
}
}
payments.add(payment);
- } catch(Exception e) {
- //ignore
+ outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
+ new WalletTransaction.PaymentOutput(txOutput, payment));
+ } else {
+ outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
}
}
}
- return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
+ return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions);
} else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
@@ -695,16 +707,25 @@ public class HeadersController extends TransactionFormController implements Init
selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
List<Payment> payments = new ArrayList<>();
+ List<WalletTransaction.Output> outputs = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
- try {
- BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
- payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
- } catch(Exception e) {
- //ignore
+ Address address = txOutput.getScript().getToAddress();
+ SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
+ BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
+ String label = receivedTxo != null ? receivedTxo.getLabel() : null;
+ if(address != null || silentPaymentAddress != null) {
+ Payment payment = (silentPaymentAddress == null ?
+ new Payment(address, label, txOutput.getValue(), false) :
+ new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
+ payments.add(payment);
+ outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
+ new WalletTransaction.PaymentOutput(txOutput, payment));
+ } else {
+ outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
}
}
- return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
+ return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions);
}
}
@@ -931,7 +952,7 @@ public class HeadersController extends TransactionFormController implements Init
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning
boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
- byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos);
+ byte[] psbtBytes = headersForm.getPsbt().getForExport().serialize(true, includeNonWitnessUtxos);
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null;
@@ -1014,7 +1035,7 @@ public class HeadersController extends TransactionFormController implements Init
}
try(FileOutputStream outputStream = new FileOutputStream(file)) {
- outputStream.write(headersForm.getPsbt().serialize());
+ outputStream.write(headersForm.getPsbt().getForExport().serialize());
} catch(IOException e) {
log.error("Error saving PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
@@ -1071,7 +1092,12 @@ public class HeadersController extends TransactionFormController implements Init
private void signUnencryptedKeystores(Wallet unencryptedWallet) {
try {
- unencryptedWallet.sign(headersForm.getPsbt());
+ Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
+ List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
+ if(!silentPayments.isEmpty()) {
+ EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
+ }
+ unencryptedWallet.sign(signingNodes);
updateSignedKeystores(headersForm.getSigningWallet());
} catch(Exception e) {
log.warn("Failed to Sign", e);
@@ -1599,6 +1625,13 @@ public class HeadersController extends TransactionFormController implements Init
}
}
+ @Subscribe
+ public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
+ if(event.getTransaction().equals(headersForm.getTransaction())) {
+ headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
+ }
+ }
+
@Subscribe
public void transactionExtracted(TransactionExtractedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java
index 6ba500d..122aa01 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java
@@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
}
} else {
if(txInput.isAbsoluteTimeLocked()) {
- txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
+ txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(transaction));
}
@@ -389,7 +389,7 @@ public class InputController extends TransactionFormController implements Initia
if(rbf.selectedProperty().getValue()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
} else {
- txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
+ txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
}
if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(transaction));
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java
index b025d9b..b4cabac 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java
@@ -5,14 +5,12 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
-import com.sparrowwallet.sparrow.event.PSBTReorderedEvent;
-import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
-import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
-import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
+import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@@ -70,20 +68,7 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
});
updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
-
- value.setValue(txOutput.getValue());
- to.setVisible(false);
- try {
- Address[] addresses = txOutput.getScript().getToAddresses();
- to.setVisible(true);
- if(addresses.length == 1) {
- address.setAddress(addresses[0]);
- } else {
- address.setText("multiple addresses");
- }
- } catch(NonStandardScriptException e) {
- //ignore
- }
+ updateSends(txOutput);
spentField.managedProperty().bind(spentField.visibleProperty());
spentByField.managedProperty().bind(spentByField.visibleProperty());
@@ -98,6 +83,32 @@ public class OutputController extends TransactionFormController implements Initi
}
initializeScriptField(scriptPubKeyArea);
+ updateScriptPubKey(txOutput);
+ }
+
+ private void updateSends(TransactionOutput txOutput) {
+ value.setValue(txOutput.getValue());
+ to.setVisible(false);
+ Address toAddress = txOutput.getScript().getToAddress();
+ SilentPaymentAddress silentPaymentAddress = outputForm.getSilentPaymentAddress(txOutput);
+ if(toAddress != null) {
+ to.setVisible(true);
+ address.setAddress(toAddress);
+ } else if(silentPaymentAddress != null) {
+ to.setVisible(true);
+ address.setText(silentPaymentAddress.toAbbreviatedString());
+ } else {
+ try {
+ txOutput.getScript().getToAddresses();
+ to.setVisible(true);
+ address.setText("multiple addresses");
+ } catch(NonStandardScriptException e) {
+ //ignore
+ }
+ }
+ }
+
+ private void updateScriptPubKey(TransactionOutput txOutput) {
scriptPubKeyArea.clear();
scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
}
@@ -115,6 +126,8 @@ public class OutputController extends TransactionFormController implements Initi
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText);
+ } else if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) {
+ outputFieldset.setText(baseText + " - Silent Payment");
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
@@ -206,4 +219,12 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null);
}
}
+
+ @Subscribe
+ public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
+ if(event.getTransaction().equals(outputForm.getTransaction())) {
+ updateSends(outputForm.getTransactionOutput());
+ updateScriptPubKey(outputForm.getTransactionOutput());
+ }
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java
index ed0d3b9..fee0718 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java
@@ -89,7 +89,7 @@ public class OutputForm extends IndexedTransactionForm {
}
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
- return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().toString(),
+ return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph());
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsController.java
index 8efdc8f..24e2451 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsController.java
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
+import com.sparrowwallet.sparrow.event.TransactionOutputsChangedEvent;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@@ -60,4 +61,11 @@ public class OutputsController extends TransactionFormController implements Init
public void unitFormatChanged(UnitFormatChangedEvent event) {
total.refresh(event.getUnitFormat(), event.getBitcoinUnit());
}
+
+ @Subscribe
+ public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
+ if(event.getTransaction().equals(outputsForm.getTransaction())) {
+ updatePieData(outputsPie, outputsForm.getTransaction().getOutputs());
+ }
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java
index 7104484..59555c9 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
+import com.sparrowwallet.drongo.psbt.PSBTOutput;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
@@ -193,4 +195,16 @@ public class TransactionData {
public Wallet getWallet() {
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null);
}
+
+ protected SilentPaymentAddress getSilentPaymentAddress(TransactionOutput txOutput) {
+ if(getPsbt() != null && txOutput.getParent() != null) {
+ for(PSBTOutput psbtOutput : getPsbt().getPsbtOutputs()) {
+ if(psbtOutput.getOutput().getIndex() == txOutput.getIndex() && psbtOutput.getSilentPaymentAddress() != null) {
+ return psbtOutput.getSilentPaymentAddress();
+ }
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java
index d8cdb42..17fc23e 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java
@@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
+import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
@@ -112,6 +114,10 @@ public abstract class TransactionForm {
return txdata.getWallet();
}
+ public SilentPaymentAddress getSilentPaymentAddress(TransactionOutput output) {
+ return txdata.getSilentPaymentAddress(output);
+ }
+
public boolean isEditable() {
if(getBlockTransaction() != null) {
return false;
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
index 71a418d..cfe2b81 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager;
@@ -33,23 +34,41 @@ public abstract class TransactionFormController extends BaseController {
long totalAmt = 0;
for(int i = 0; i < outputs.size(); i++) {
TransactionOutput output = outputs.get(i);
- String name = "#" + i;
+ String name = getPieDataName(i, output);
+
+ totalAmt += output.getValue();
+ outputsPieData.add(new PieChart.Data(name, output.getValue()));
+ }
+
+ addPieData(pie, outputsPieData);
+ }
+
+ protected void updatePieData(PieChart pie, List<TransactionOutput> outputs) {
+ for(int i = 0; i < outputs.size(); i++) {
+ TransactionOutput output = outputs.get(i);
+ String name = getPieDataName(i, output);
+ pie.getData().get(i).setName(name);
+ }
+ }
+
+ private String getPieDataName(int i, TransactionOutput output) {
+ String name = "#" + i;
+ Address address = output.getScript().getToAddress();
+ SilentPaymentAddress silentPaymentAddress = getTransactionForm().getSilentPaymentAddress(output);
+ if(address != null) {
+ name = name + " " + address.getAddress();
+ } else if(silentPaymentAddress != null) {
+ name = name + " " + silentPaymentAddress.toAbbreviatedString();
+ } else {
try {
Address[] addresses = output.getScript().getToAddresses();
- if(addresses.length == 1) {
- name = name + " " + addresses[0].getAddress();
- } else {
- name = name + " [" + addresses[0].getAddress() + ",...]";
- }
+ name = name + " [" + addresses[0].getAddress() + ",...]";
} catch(NonStandardScriptException e) {
//ignore
}
-
- totalAmt += output.getValue();
- outputsPieData.add(new PieChart.Data(name, output.getValue()));
}
- addPieData(pie, outputsPieData);
+ return name;
}
protected void addCoinbasePieData(PieChart pie, long value) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
index 6e196c9..01229b8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
+import com.sparrowwallet.drongo.silentpayments.SilentPayment;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*;
@@ -143,6 +145,8 @@ public class PaymentController extends WalletFormController implements Initializ
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty<SilentPaymentAddress> silentPaymentAddressProperty = new SimpleObjectProperty<>();
+
private final ObjectProperty<DnsPayment> dnsPaymentProperty = new SimpleObjectProperty<>();
private static final Wallet payNymWallet = new Wallet() {
@@ -172,6 +176,10 @@ public class PaymentController extends WalletFormController implements Initializ
dnsPaymentProperty.set(null);
}
+ if(silentPaymentAddressProperty.get() != null && !newValue.equals(silentPaymentAddressProperty.get().getAddress())) {
+ silentPaymentAddressProperty.set(null);
+ }
+
try {
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
Platform.runLater(() -> updateFromURI(bitcoinURI));
@@ -237,6 +245,13 @@ public class PaymentController extends WalletFormController implements Initializ
}
}
+ try {
+ SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
+ setSilentPaymentAddress(silentPaymentAddress);
+ } catch(Exception e) {
+ //ignore, not a silent payment address
+ }
+
revalidateAmount();
maxButton.setDisable(!isMaxButtonEnabled());
sendController.updateTransaction();
@@ -312,6 +327,10 @@ public class PaymentController extends WalletFormController implements Initializ
revalidateAmount();
});
+ silentPaymentAddressProperty.addListener((observable, oldValue, silentPaymentAddress) -> {
+ revalidateAmount();
+ });
+
dnsPaymentProperty.addListener((observable, oldValue, dnsPayment) -> {
if(dnsPayment != null) {
MenuItem copyMenuItem = new MenuItem("Copy URI");
@@ -402,22 +421,36 @@ public class PaymentController extends WalletFormController implements Initializ
}
public void setDnsPayment(DnsPayment dnsPayment) {
- if(dnsPayment.bitcoinURI().getAddress() == null) {
- AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a Bitcoin address.");
+ if(dnsPayment.hasAddress()) {
+ DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
+ } else if(dnsPayment.hasSilentPaymentAddress()) {
+ DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
+ setSilentPaymentAddress(dnsPayment.bitcoinURI().getSilentPaymentAddress());
+ } else {
+ AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a bitcoin address.");
return;
}
- DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
dnsPaymentProperty.set(dnsPayment);
address.setText(dnsPayment.hrn());
revalidate(address, addressListener);
address.leftProperty().set(getBitcoinCharacter());
- if(label.getText().isEmpty() || label.getText().startsWith("To ₿")) {
- label.setText("To " + dnsPayment);
+ if(label.getText().isEmpty() || (label.getText().startsWith("₿") && !label.getText().contains(" "))) {
+ label.setText(dnsPayment.toString());
}
label.requestFocus();
}
+ 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."));
+ return;
+ }
+
+ silentPaymentAddressProperty.set(silentPaymentAddress);
+ label.requestFocus();
+ }
+
private void updateOpenWallets() {
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
}
@@ -489,8 +522,13 @@ public class PaymentController extends WalletFormController implements Initializ
}
private Address getRecipientAddress() throws InvalidAddressException {
+ SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
+ if(silentPaymentAddress != null) {
+ return SilentPayment.getDummyAddress();
+ }
+
DnsPayment dnsPayment = dnsPaymentProperty.get();
- if(dnsPayment != null) {
+ if(dnsPayment != null && dnsPayment.hasAddress()) {
return dnsPayment.bitcoinURI().getAddress();
}
@@ -630,7 +668,14 @@ public class PaymentController extends WalletFormController implements Initializ
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
- Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
+ Payment payment;
+ SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
+ if(silentPaymentAddress != null) {
+ payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
+ } else {
+ payment = new Payment(recipientAddress, label.getText(), value, sendAll);
+ }
+
if(address.getUserData() != null) {
payment.setType((Payment.Type)address.getUserData());
}
@@ -647,7 +692,11 @@ public class PaymentController extends WalletFormController implements Initializ
public void setPayment(Payment payment) {
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
if(payment.getAddress() != null) {
- address.setText(payment.getAddress().toString());
+ if(payment instanceof SilentPayment silentPayment) {
+ address.setText(silentPayment.getSilentPaymentAddress().getAddress());
+ } else {
+ address.setText(payment.getAddress().toString());
+ }
address.setUserData(payment.getType());
}
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
@@ -680,6 +729,7 @@ public class PaymentController extends WalletFormController implements Initializ
dustAmountProperty.set(false);
payNymProperty.set(null);
dnsPaymentProperty.set(null);
+ silentPaymentAddressProperty.set(null);
}
public void setMaxInput(ActionEvent event) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
index 0d7ddeb..21720f1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
@@ -6,12 +6,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
-import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.bip47.SecretPoint;
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.wallet.*;
import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.control.*;
@@ -615,9 +615,14 @@ public class SendController extends WalletFormController implements Initializabl
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
BlockTransaction replacedTransaction = replacedTransactionProperty.get();
+ //Disable RBF for silent payments, as we can't guarantee RBF won't be attempted on another device without knowledge to recompute the address if necessary
+ boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
+ && payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
+
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
payments, opReturnsList, excludedChangeNodes,
- feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction);
+ feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
+ currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction, allowRbf);
walletTransactionService.setOnSucceeded(event -> {
if(!walletTransactionService.isIgnoreResult()) {
walletTransactionProperty.setValue(walletTransactionService.getValue());
@@ -652,12 +657,12 @@ public class SendController extends WalletFormController implements Initializabl
walletTransactionService.start();
}
- } catch(InvalidAddressException | IllegalStateException e) {
+ } catch(IllegalStateException e) {
walletTransactionProperty.setValue(null);
}
}
- private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException {
+ private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) {
if(utxoSelectorProperty.get() != null) {
return List.of(utxoSelectorProperty.get());
}
@@ -694,13 +699,15 @@ public class SendController extends WalletFormController implements Initializabl
private final boolean groupByAddress;
private final boolean includeMempoolOutputs;
private final BlockTransaction replacedTransaction;
+ private final boolean allowRbf;
private boolean ignoreResult;
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
- Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) {
+ Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs,
+ BlockTransaction replacedTransaction, boolean allowRbf) {
this.addressNodeMap = addressNodeMap;
this.wallet = wallet;
this.utxoSelectors = utxoSelectors;
@@ -716,6 +723,7 @@ public class SendController extends WalletFormController implements Initializabl
this.groupByAddress = groupByAddress;
this.includeMempoolOutputs = includeMempoolOutputs;
this.replacedTransaction = replacedTransaction;
+ this.allowRbf = allowRbf;
}
@Override
@@ -725,7 +733,8 @@ public class SendController extends WalletFormController implements Initializabl
try {
return getWalletTransaction();
} catch(InsufficientFundsException e) {
- if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
+ if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction)
+ && utxoSelectors.size() == 1 && utxoSelectors.getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
List<TxoFilter> filters = new ArrayList<>(txoFilters);
filters.add(presetUtxoSelector.asExcludeTxoFilter());
@@ -734,7 +743,7 @@ public class SendController extends WalletFormController implements Initializabl
Collections.shuffle(outputGroups);
while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
- OutputGroup outputGroup = outputGroups.remove(0);
+ OutputGroup outputGroup = outputGroups.removeFirst();
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
presetUtxoSelector.getPresetUtxos().add(utxo);
}
@@ -748,12 +757,16 @@ public class SendController extends WalletFormController implements Initializabl
}
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
- updateMessage("Selecting UTXOs...");
- WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
- feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
- updateMessage("Deriving keys...");
- walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
- return walletTransaction;
+ try {
+ updateMessage("Selecting UTXOs...");
+ WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
+ feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
+ updateMessage("Deriving keys...");
+ walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
+ return walletTransaction;
+ } finally {
+ updateMessage("");
+ }
}
};
}
@@ -1252,7 +1265,7 @@ public class SendController extends WalletFormController implements Initializabl
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
- excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
+ excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
PSBT psbt = finalWalletTx.createPSBT();
decryptedWallet.sign(psbt);
decryptedWallet.finalise(psbt);
@@ -1511,7 +1524,7 @@ public class SendController extends WalletFormController implements Initializabl
notificationButton.setVisible(isNotificationTransaction);
notificationButton.setDefaultButton(isNotificationTransaction);
- setInputFieldsDisabled(isNotificationTransaction, false);
+ setInputFieldsDisabled(!event.allowPaymentChanges(), false);
}
}
Why this scored 34/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.