skip recipient parsing of the send tab pay to field while it shows the name of a selected paynym
What changed, and why it matters
This commit fixes a UI bug in Sparrow Wallet's 'Send' tab. When a user selected a PayNym (a privacy-focused contact name), the wallet was still trying to parse that display name as if it were a Bitcoin address, payment code, or web address. This could trigger unnecessary network lookups, show confusing error dialogs, or in rare cases change the intended recipient. The fix simply skips all that parsing while the field is showing a PayNym name.
Review the fix in the context of the full recipient-selection flow to ensure no other state properties (e.g., silent payment address, DNS payment) need similar guards. Consider adding regression tests for PayNym selection and verifying the pay-to field is cleared or locked when a PayNym is active.
Security signals we found
UI state confusion between display name and parseable recipient
Unintended network resolution triggered by display text
Potential recipient misparsing leading to wrong transaction destination
No input sanitization or state guard before parsing
Evidence from the diff
PaymentController’s recipient text-change listener now wraps BitcoinURI, DNS payment HRN, BIP47 payment code, silent payment address, and plain address parsing inside an if(payNymProperty.get() == null) guard. Previously these parsers ran unconditionally on every change, including when the pay-to field contained the human-readable name of a selected paynym. The patch prevents misinterpretation of the paynym display name as a URI/address/code, avoiding spurious resolution attempts, validation errors, and possible unintended recipient selection.
Changed components
src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.javaSend tab pay-to field recipient parsingPayNym selection handlingInspect captured patch +81 / −79
### src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -192,99 +192,101 @@ public void changed(ObservableValue<? extends String> observable, String oldValu
payjoinURIProperty.set(null);
}
- try {
- BitcoinURI bitcoinURI = new BitcoinURI(newValue);
- Platform.runLater(() -> updateFromURI(bitcoinURI));
- return;
- } catch(Exception e) {
- //ignore, not a URI
- }
-
- Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(newValue);
- if(optDnsPaymentHrn.isPresent()) {
- String dnsPaymentHrn = optDnsPaymentHrn.get();
- DnsPayment cachedDnsPayment = DnsPaymentCache.getDnsPayment(dnsPaymentHrn);
- if(cachedDnsPayment != null) {
- setDnsPayment(cachedDnsPayment);
+ if(payNymProperty.get() == null) {
+ try {
+ BitcoinURI bitcoinURI = new BitcoinURI(newValue);
+ Platform.runLater(() -> updateFromURI(bitcoinURI));
return;
+ } catch(Exception e) {
+ //ignore, not a URI
}
- if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
- if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
- Platform.runLater(() -> {
- ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the address?", ButtonType.NO, ButtonType.YES);
- Optional<ButtonType> optType = confirmationAlert.showAndWait();
- if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
- Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
- }
- if(optType.isPresent() && optType.get() == ButtonType.YES) {
- EventManager.get().post(new RequestConnectEvent());
- }
- });
- } else {
- Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
+ Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(newValue);
+ if(optDnsPaymentHrn.isPresent()) {
+ String dnsPaymentHrn = optDnsPaymentHrn.get();
+ DnsPayment cachedDnsPayment = DnsPaymentCache.getDnsPayment(dnsPaymentHrn);
+ if(cachedDnsPayment != null) {
+ setDnsPayment(cachedDnsPayment);
+ return;
}
- return;
- }
- DnsPaymentService dnsPaymentService = new DnsPaymentService(dnsPaymentHrn);
- dnsPaymentService.setOnSucceeded(_ -> {
- if(isCurrentHrn(dnsPaymentHrn)) {
- dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment));
- }
- });
- dnsPaymentService.setOnFailed(failEvent -> {
- Throwable exception = failEvent.getSource().getException();
- if(isCurrentHrn(dnsPaymentHrn) && exception != null && !(exception.getCause() instanceof TimeoutException)) {
- AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(exception).getMessage());
+ if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
+ if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
+ Platform.runLater(() -> {
+ ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the address?", ButtonType.NO, ButtonType.YES);
+ Optional<ButtonType> optType = confirmationAlert.showAndWait();
+ if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
+ Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
+ }
+ if(optType.isPresent() && optType.get() == ButtonType.YES) {
+ EventManager.get().post(new RequestConnectEvent());
+ }
+ });
+ } else {
+ Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
+ }
+ return;
}
- });
- dnsPaymentService.start();
- return;
- }
- if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
- try {
- PaymentCode paymentCode = new PaymentCode(newValue);
- Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
- if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
- recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
- }
+ DnsPaymentService dnsPaymentService = new DnsPaymentService(dnsPaymentHrn);
+ dnsPaymentService.setOnSucceeded(_ -> {
+ if(isCurrentHrn(dnsPaymentHrn)) {
+ dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment));
+ }
+ });
+ dnsPaymentService.setOnFailed(failEvent -> {
+ Throwable exception = failEvent.getSource().getException();
+ if(isCurrentHrn(dnsPaymentHrn) && exception != null && !(exception.getCause() instanceof TimeoutException)) {
+ AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(exception).getMessage());
+ }
+ });
+ dnsPaymentService.start();
+ return;
+ }
- if(recipientBip47Wallet != null && hasNotificationTransaction(paymentCode)) {
- PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
- Platform.runLater(() -> setPayNym(payNym));
- } else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
- ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
- Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
- if(optButton.isPresent() && optButton.get() == previewType) {
- Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
- Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
- } else {
- Platform.runLater(() -> address.setText(""));
+ if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
+ try {
+ PaymentCode paymentCode = new PaymentCode(newValue);
+ Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
+ if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
+ recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
}
+
+ if(recipientBip47Wallet != null && hasNotificationTransaction(paymentCode)) {
+ PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
+ Platform.runLater(() -> setPayNym(payNym));
+ } else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
+ ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
+ Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
+ if(optButton.isPresent() && optButton.get() == previewType) {
+ Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
+ Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
+ } else {
+ Platform.runLater(() -> address.setText(""));
+ }
+ }
+ } catch(Exception e) {
+ //ignore, not a payment code
}
- } catch(Exception e) {
- //ignore, not a payment code
}
- }
- try {
- SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
- setSilentPaymentAddress(silentPaymentAddress);
- } catch(Exception e) {
- //ignore, not a silent payment address
- }
+ try {
+ SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
+ setSilentPaymentAddress(silentPaymentAddress);
+ } catch(Exception e) {
+ //ignore, not a silent payment address
+ }
- try {
- Address toAddress = Address.fromString(newValue);
- WalletNode walletNode = sendController.getWalletNode(toAddress);
- if(walletNode != null) {
- consolidationNodeProperty.set(walletNode);
+ try {
+ Address toAddress = Address.fromString(newValue);
+ WalletNode walletNode = sendController.getWalletNode(toAddress);
+ if(walletNode != null) {
+ consolidationNodeProperty.set(walletNode);
+ }
+ label.requestFocus();
+ } catch(Exception e) {
+ //ignore, not an address
}
- label.requestFocus();
- } catch(Exception e) {
- //ignore, not an address
}
revalidateAmount();Why this scored 24/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.