pay the silent payment address of a resolved instruction where the sending wallet can, treating the address in the body as a fallback
What changed, and why it matters
This commit changes how Sparrow Wallet chooses between a regular Bitcoin address and a newer 'silent payment' address when paying someone via a DNS name or a Bitcoin URI. Previously, the wallet preferred the regular address. Now, if the sending wallet supports silent payments, it prefers the silent payment address and only falls back to the regular address when silent payments aren't possible. This is a feature/correctness improvement rather than a fix for an active security flaw, but it reduces the chance of silently using a weaker address type when a stronger one is available.
Review the companion drongo submodule changes to confirm silent payment address parsing and `canSendSilentPayments()` behave correctly. Test that wallets unable to send silent payments still fall back cleanly to the regular address, and that the DNS payment cache key matches the final output address to avoid proof verification failures.
Security signals we found
Preferring silent payment addresses over plain addresses when both are advertised
Making address resolution wallet-capability-aware
Avoiding mismatch between cached DNS payment key and the address used in the constructed payment
Submodule bump to drongo c0333df6 (silent payment / URI parsing support)
Evidence from the diff
The patch updates Bitcoin URI and DNS payment resolution so that a silent payment address is selected when the sending wallet has canSendSilentPayments(), treating the on-chain/URI body address as a fallback. It threads the sending Wallet into SendToManyDialog and SendToAddress.toPayment() so resolution can be wallet-aware. The drongo submodule bump likely carries the underlying BitcoinURI.toPayment(Wallet) and silent-payment helpers. The change also fixes cache-key selection so DNS payment proofs are looked up under the address actually used in the PSBT output.
Changed components
Bitcoin URI parsing and payment creationDNS payment resolution (BIP353-style human-readable names)SendToManyDialogPaymentControllerdrongo submoduleInspect captured patch +33 / −26
### drongo
@@ -1 +1 @@
-Subproject commit 7facf4ca371f8df16f6da60913b45d375d5593a8
+Subproject commit c0333df629682472925279de60b6c172ab3290ac
### src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -1539,7 +1539,7 @@ private void sendToMany(List<Payment> initialPayments) {
bitcoinUnit = wallet.getAutoUnit();
}
- sendToManyDialog = new SendToManyDialog(bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
+ sendToManyDialog = new SendToManyDialog(wallet, bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
sendToManyDialog.initModality(Modality.NONE);
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
sendToManyDialog = null;
### src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -1136,7 +1136,7 @@ private static void openBitcoinUri(URI uri) {
if(wallet != null) {
final Wallet sendingWallet = wallet;
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getSpendableUtxos().keySet()), true));
- Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()), bitcoinURI)));
+ Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment(sendingWallet)), bitcoinURI)));
}
} catch(Exception e) {
showErrorDialog("Not a valid bitcoin URI", e.getMessage());
### src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
@@ -13,6 +13,7 @@
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
import com.sparrowwallet.drongo.wallet.Payment;
+import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat;
@@ -47,13 +48,15 @@
import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> {
+ private final Wallet wallet;
private final BitcoinUnit bitcoinUnit;
private final UnitFormat unitFormat;
private final UnitFormatDoubleCellType amountCellType;
private final SpreadsheetView spreadsheetView;
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
- public SendToManyDialog(BitcoinUnit bitcoinUnit, UnitFormat unitFormat, List<Payment> payments) {
+ public SendToManyDialog(Wallet wallet, BitcoinUnit bitcoinUnit, UnitFormat unitFormat, List<Payment> payments) {
+ this.wallet = wallet;
this.bitcoinUnit = bitcoinUnit;
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat);
@@ -557,26 +560,29 @@ 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) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
+ public Payment toPayment(Wallet wallet, String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
if(hrn != null) {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
if(dnsPayment == null) {
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
Optional<DnsPayment> optDnsPayment = resolver.resolve(AppServices.getProxy());
- if(optDnsPayment.isPresent()) {
- dnsPayment = optDnsPayment.get();
- if(dnsPayment.hasAddress()) {
- DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
- } else if(dnsPayment.hasSilentPaymentAddress()) {
- DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
- }
- return getPayment(optDnsPayment.get(), label, value, sendMax);
- } else {
+ if(optDnsPayment.isEmpty()) {
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
}
+
+ dnsPayment = optDnsPayment.get();
+ }
+
+ //Cached under the address this payment will be looked up by, which is how its proof chain reaches the PSBT output. A name found by
+ //hrn alone can be held under the other address, having been resolved for a wallet of the other silent payments capability
+ Payment payment = getPayment(wallet, dnsPayment, label, value, sendMax);
+ if(payment instanceof SilentPayment silentPayment) {
+ DnsPaymentCache.putDnsPayment(silentPayment.getSilentPaymentAddress(), dnsPayment);
} else {
- return getPayment(dnsPayment, label, value, sendMax);
+ DnsPaymentCache.putDnsPayment(payment.getAddress(), dnsPayment);
}
+
+ return payment;
}
if(silentPaymentAddress != null) {
@@ -586,11 +592,11 @@ public Payment toPayment(String label, long value, boolean sendMax) throws DnsPa
}
}
- private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
- if(dnsPayment.hasAddress()) {
- return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
- } else if(dnsPayment.hasSilentPaymentAddress()) {
+ private static Payment getPayment(Wallet wallet, DnsPayment dnsPayment, String label, long value, boolean sendMax) {
+ if(dnsPayment.hasSilentPaymentAddress() && (!dnsPayment.hasAddress() || wallet.canSendSilentPayments())) {
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
+ } else if(dnsPayment.hasAddress()) {
+ return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
} else {
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
}
@@ -650,7 +656,7 @@ private List<Payment> getPayments() throws DnsPaymentValidationException, IOExce
}
if(sendToAddress != null && value != null) {
- payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
+ payments.add(sendToAddress.toPayment(wallet, label, bitcoinUnit.getSatsValue(value), false));
}
}
### src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -467,11 +467,11 @@ private boolean isCurrentHrn(String hrn) {
}
public void setDnsPayment(DnsPayment dnsPayment) {
- if(dnsPayment.hasAddress()) {
- DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
- } else if(dnsPayment.hasSilentPaymentAddress()) {
+ if(dnsPayment.hasSilentPaymentAddress() && (!dnsPayment.hasAddress() || sendController.getWalletForm().getWallet().canSendSilentPayments())) {
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
setSilentPaymentAddress(dnsPayment.bitcoinURI().getSilentPaymentAddress());
+ } else if(dnsPayment.hasAddress()) {
+ DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
} else {
AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a bitcoin address.");
return;
@@ -827,10 +827,11 @@ public void scanQrAddress(ActionEvent event) {
}
private void updateFromURI(BitcoinURI bitcoinURI) {
- if(bitcoinURI.getAddress() != null) {
- address.setText(bitcoinURI.getAddress().toString());
- } else if(bitcoinURI.getSilentPaymentAddress() != null) {
+ //A URI carrying both publishes the address in its body as a fallback for a sender which cannot pay the silent payment address in its query
+ if(bitcoinURI.getSilentPaymentAddress() != null && (bitcoinURI.getAddress() == null || sendController.getWalletForm().getWallet().canSendSilentPayments())) {
address.setText(bitcoinURI.getSilentPaymentAddress().getAddress());
+ } else if(bitcoinURI.getAddress() != null) {
+ address.setText(bitcoinURI.getAddress().toString());
}
if(bitcoinURI.getLabel() != null) {
label.setText(bitcoinURI.getLabel());Why this scored 21/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.