From b16c7345a8c3aaeff949ba478d2108a8a847fd35 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 30 Mar 2022 18:26:41 +0200 Subject: [PATCH] allow collaborative sends for linked paynyms, support searching for custom paynyms when initiating collaborative sends --- drongo | 2 +- .../sparrowwallet/sparrow/paynym/PayNym.java | 10 +++ .../sparrow/paynym/PayNymController.java | 2 +- .../sparrow/paynym/PayNymDialog.java | 47 +++++++++++-- .../sparrow/soroban/InitiatorController.java | 68 ++++++++++++------- .../sparrow/wallet/PaymentController.java | 33 +++++---- 6 files changed, 119 insertions(+), 43 deletions(-) diff --git a/drongo b/drongo index d1088fe9..20f4ac96 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit d1088fe9ee6da0e53fcfdff020cab3c16c15702b +Subproject commit 20f4ac96574dc7bdb811380ff948931663e19c44 diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java index cefd5089..cf34a8c9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java @@ -20,6 +20,8 @@ public class PayNym { private final List following; private final List followers; + private boolean collaborativeSend; + public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List following, List followers) { this.paymentCode = paymentCode; this.nymId = nymId; @@ -53,6 +55,14 @@ public class PayNym { return followers; } + public boolean isCollaborativeSend() { + return collaborativeSend; + } + + public void setCollaborativeSend(boolean collaborativeSend) { + this.collaborativeSend = collaborativeSend; + } + public List getScriptTypes() { return segwit ? getSegwitScriptTypes() : getV1ScriptTypes(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 4cfb5b99..2a888cb0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -453,7 +453,7 @@ public class PayNymController { ButtonType previewType = new ButtonType("Preview", ButtonBar.ButtonData.LEFT); ButtonType sendType = new ButtonType("Send", ButtonBar.ButtonData.YES); Optional optButtonType = AppServices.showAlertDialog("Link PayNym?", - "Linking to this contact will allow you to send to it non-collaboratively through unique private addresses you can generate independently.\n\n" + + "Linking to this contact will allow you to send to it directly (non-collaboratively) through unique private addresses you can generate independently.\n\n" + "It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link through a notification transaction, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, previewType, ButtonType.CANCEL, sendType); if(optButtonType.isPresent() && optButtonType.get() == sendType) { broadcastNotificationTransaction(payNym); diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java index 70cd94de..96d55bf6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java @@ -9,10 +9,10 @@ import java.io.IOException; public class PayNymDialog extends Dialog { public PayNymDialog(String walletId) { - this(walletId, false, false); + this(walletId, Operation.SHOW, false); } - public PayNymDialog(String walletId, boolean selectPayNym, boolean selectLinkedOnly) { + public PayNymDialog(String walletId, Operation operation, boolean selectLinkedOnly) { final DialogPane dialogPane = getDialogPane(); AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.onEscapePressed(dialogPane.getScene(), this::close); @@ -32,11 +32,34 @@ public class PayNymDialog extends Dialog { dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm()); + final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send Directly", ButtonBar.ButtonData.APPLY); + final ButtonType sendCollaborativelyButtonType = new javafx.scene.control.ButtonType("Send Collaboratively", ButtonBar.ButtonData.OK_DONE); final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); - if(selectPayNym) { + if(operation == Operation.SEND) { + if(selectLinkedOnly) { + dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType); + } else { + dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, sendCollaborativelyButtonType, cancelButtonType); + Button sendCollaborativelyButton = (Button)dialogPane.lookupButton(sendCollaborativelyButtonType); + sendCollaborativelyButton.setDisable(true); + sendCollaborativelyButton.setDefaultButton(false); + payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> { + sendCollaborativelyButton.setDisable(payNym == null); + sendCollaborativelyButton.setDefaultButton(payNym != null && !payNymController.isLinked(payNym)); + }); + } + + Button sendDirectlyButton = (Button)dialogPane.lookupButton(sendDirectlyButtonType); + sendDirectlyButton.setDisable(true); + sendDirectlyButton.setDefaultButton(true); + payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> { + sendDirectlyButton.setDisable(payNym == null || !payNymController.isLinked(payNym)); + sendDirectlyButton.setDefaultButton(!sendDirectlyButton.isDisable()); + }); + } else if(operation == Operation.SELECT) { dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType); Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); selectButton.setDisable(true); @@ -58,9 +81,25 @@ public class PayNymDialog extends Dialog { EventManager.get().unregister(payNymController); }); - setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null); + setResultConverter(dialogButton -> { + if(dialogButton == sendCollaborativelyButtonType) { + PayNym payNym = payNymController.getPayNym(); + payNym.setCollaborativeSend(true); + return payNym; + } else if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) { + PayNym payNym = payNymController.getPayNym(); + payNym.setCollaborativeSend(false); + return payNym; + } + + return null; + }); } catch(IOException e) { throw new RuntimeException(e); } } + + public enum Operation { + SHOW, SELECT, SEND; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index 0111abae..7a68a874 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -39,10 +39,13 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.VBox; import javafx.util.Duration; import javafx.util.StringConverter; @@ -148,6 +151,22 @@ public class InitiatorController extends SorobanController { private boolean closed; + private final ChangeListener counterpartyListener = (observable, oldValue, newValue) -> { + if(newValue != null) { + if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { + //Assumed valid payment code + } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { + if(!newValue.equals(counterpartyPayNymName.get())) { + searchPayNyms(newValue); + } + } else if(!newValue.equals(counterpartyPayNymName.get())) { + counterpartyPayNymName.set(null); + counterpartyPaymentCode.set(null); + payNymAvatar.clearPaymentCode(); + } + } + }; + public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) { this.walletId = walletId; this.wallet = wallet; @@ -247,29 +266,11 @@ public class InitiatorController extends SorobanController { return change; }; counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter)); - - counterparty.textProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null) { - if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { - //Assumed valid payment code - } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { - if(!newValue.equals(counterpartyPayNymName.get())) { - payNymLoading.setVisible(true); - AppServices.getPayNymService().getPayNym(newValue).subscribe(payNym -> { - payNymLoading.setVisible(false); - counterpartyPayNymName.set(payNym.nymName()); - counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); - payNymAvatar.setPaymentCode(payNym.paymentCode()); - }, error -> { - payNymLoading.setVisible(false); - //ignore, probably doesn't exist but will try again on meeting request - }); - } - } else { - counterpartyPayNymName.set(null); - counterpartyPaymentCode.set(null); - payNymAvatar.clearPaymentCode(); - } + counterparty.textProperty().addListener(counterpartyListener); + counterparty.addEventFilter(KeyEvent.ANY, event -> { + if(counterparty.isEditable() && event.getCode() == KeyCode.ENTER) { + searchPayNyms(counterparty.getText()); + event.consume(); } }); @@ -308,6 +309,25 @@ public class InitiatorController extends SorobanController { }); } + private void searchPayNyms(String identifier) { + payNymLoading.setVisible(true); + AppServices.getPayNymService().getPayNym(identifier).subscribe(payNym -> { + payNymLoading.setVisible(false); + counterpartyPayNymName.set(payNym.nymName()); + counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + counterparty.textProperty().removeListener(counterpartyListener); + int caret = counterparty.getCaretPosition(); + counterparty.setText(""); + counterparty.setText(payNym.nymName()); + counterparty.positionCaret(caret); + counterparty.textProperty().addListener(counterpartyListener); + }, error -> { + payNymLoading.setVisible(false); + //ignore, probably doesn't exist but will try again on meeting request + }); + } + private void setPayNymFollowers() { Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> { @@ -617,7 +637,7 @@ public class InitiatorController extends SorobanController { } public void findPayNym(ActionEvent event) { - PayNymDialog payNymDialog = new PayNymDialog(walletId, true, false); + PayNymDialog payNymDialog = new PayNymDialog(walletId, PayNymDialog.Operation.SELECT, false); Optional optPayNym = payNymDialog.showAndWait(); optPayNym.ifPresent(payNym -> { counterpartyPayNymName.set(payNym.nymName()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 680bdab7..eb7b7a9e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -155,7 +155,7 @@ public class PaymentController extends WalletFormController implements Initializ openWallets.valueProperty().addListener((observable, oldValue, newValue) -> { if(newValue == payNymWallet) { boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet()); - PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true, selectLinkedOnly); + PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly); Optional optPayNym = payNymDialog.showAndWait(); optPayNym.ifPresent(this::setPayNym); } else if(newValue != null) { @@ -279,10 +279,14 @@ public class PaymentController extends WalletFormController implements Initializ } public void setPayNym(PayNym payNym) { + PayNym existingPayNym = payNymProperty.get(); payNymProperty.set(payNym); address.setText(payNym.nymName()); address.leftProperty().set(getPayNymGlyph()); label.requestFocus(); + if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName()) && payNym.isCollaborativeSend() != existingPayNym.isCollaborativeSend()) { + sendController.updateTransaction(); + } } public void updateMixOnlyStatus() { @@ -350,25 +354,28 @@ public class PaymentController extends WalletFormController implements Initializ } private Address getRecipientAddress() throws InvalidAddressException { - if(payNymProperty.get() == null) { + PayNym payNym = payNymProperty.get(); + if(payNym == null) { return Address.fromString(address.getText()); } - try { - Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get()); - if(recipientBip47Wallet != null) { - WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND); - ECKey pubKey = sendNode.getPubKey(); - Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey); - if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) { - return address; + if(!payNym.isCollaborativeSend()) { + try { + Wallet recipientBip47Wallet = getWalletForPayNym(payNym); + if(recipientBip47Wallet != null) { + WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND); + ECKey pubKey = sendNode.getPubKey(); + Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey); + if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) { + return address; + } } + } catch(InvalidPaymentCodeException e) { + log.error("Error creating payment code from PayNym", e); } - } catch(InvalidPaymentCodeException e) { - log.error("Error creating payment code from PayNym", e); } - return new PayNymAddress(payNymProperty.get()); + return new PayNymAddress(payNym); } private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {