allow collaborative sends for linked paynyms, support searching for custom paynyms when initiating collaborative sends

This commit is contained in:
Craig Raw 2022-03-30 18:26:41 +02:00
parent b1940e9293
commit b16c7345a8
6 changed files with 119 additions and 43 deletions

2
drongo

@ -1 +1 @@
Subproject commit d1088fe9ee6da0e53fcfdff020cab3c16c15702b Subproject commit 20f4ac96574dc7bdb811380ff948931663e19c44

View file

@ -20,6 +20,8 @@ public class PayNym {
private final List<PayNym> following; private final List<PayNym> following;
private final List<PayNym> followers; private final List<PayNym> followers;
private boolean collaborativeSend;
public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) { public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.nymId = nymId; this.nymId = nymId;
@ -53,6 +55,14 @@ public class PayNym {
return followers; return followers;
} }
public boolean isCollaborativeSend() {
return collaborativeSend;
}
public void setCollaborativeSend(boolean collaborativeSend) {
this.collaborativeSend = collaborativeSend;
}
public List<ScriptType> getScriptTypes() { public List<ScriptType> getScriptTypes() {
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes(); return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
} }

View file

@ -453,7 +453,7 @@ public class PayNymController {
ButtonType previewType = new ButtonType("Preview", ButtonBar.ButtonData.LEFT); ButtonType previewType = new ButtonType("Preview", ButtonBar.ButtonData.LEFT);
ButtonType sendType = new ButtonType("Send", ButtonBar.ButtonData.YES); ButtonType sendType = new ButtonType("Send", ButtonBar.ButtonData.YES);
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link PayNym?", Optional<ButtonType> 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); "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) { if(optButtonType.isPresent() && optButtonType.get() == sendType) {
broadcastNotificationTransaction(payNym); broadcastNotificationTransaction(payNym);

View file

@ -9,10 +9,10 @@ import java.io.IOException;
public class PayNymDialog extends Dialog<PayNym> { public class PayNymDialog extends Dialog<PayNym> {
public PayNymDialog(String walletId) { 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(); final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close); AppServices.onEscapePressed(dialogPane.getScene(), this::close);
@ -32,11 +32,34 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.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 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 cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); 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); dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType);
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); Button selectButton = (Button)dialogPane.lookupButton(selectButtonType);
selectButton.setDisable(true); selectButton.setDisable(true);
@ -58,9 +81,25 @@ public class PayNymDialog extends Dialog<PayNym> {
EventManager.get().unregister(payNymController); 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) { } catch(IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public enum Operation {
SHOW, SELECT, SEND;
}
} }

View file

@ -39,10 +39,13 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
@ -148,6 +151,22 @@ public class InitiatorController extends SorobanController {
private boolean closed; private boolean closed;
private final ChangeListener<String> 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) { public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
this.walletId = walletId; this.walletId = walletId;
this.wallet = wallet; this.wallet = wallet;
@ -247,29 +266,11 @@ public class InitiatorController extends SorobanController {
return change; return change;
}; };
counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter)); counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
counterparty.textProperty().addListener(counterpartyListener);
counterparty.textProperty().addListener((observable, oldValue, newValue) -> { counterparty.addEventFilter(KeyEvent.ANY, event -> {
if(newValue != null) { if(counterparty.isEditable() && event.getCode() == KeyCode.ENTER) {
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { searchPayNyms(counterparty.getText());
//Assumed valid payment code event.consume();
} 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();
}
} }
}); });
@ -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() { private void setPayNymFollowers() {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> { 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) { public void findPayNym(ActionEvent event) {
PayNymDialog payNymDialog = new PayNymDialog(walletId, true, false); PayNymDialog payNymDialog = new PayNymDialog(walletId, PayNymDialog.Operation.SELECT, false);
Optional<PayNym> optPayNym = payNymDialog.showAndWait(); Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(payNym -> { optPayNym.ifPresent(payNym -> {
counterpartyPayNymName.set(payNym.nymName()); counterpartyPayNymName.set(payNym.nymName());

View file

@ -155,7 +155,7 @@ public class PaymentController extends WalletFormController implements Initializ
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> { openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == payNymWallet) { if(newValue == payNymWallet) {
boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet()); 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<PayNym> optPayNym = payNymDialog.showAndWait(); Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(this::setPayNym); optPayNym.ifPresent(this::setPayNym);
} else if(newValue != null) { } else if(newValue != null) {
@ -279,10 +279,14 @@ public class PaymentController extends WalletFormController implements Initializ
} }
public void setPayNym(PayNym payNym) { public void setPayNym(PayNym payNym) {
PayNym existingPayNym = payNymProperty.get();
payNymProperty.set(payNym); payNymProperty.set(payNym);
address.setText(payNym.nymName()); address.setText(payNym.nymName());
address.leftProperty().set(getPayNymGlyph()); address.leftProperty().set(getPayNymGlyph());
label.requestFocus(); label.requestFocus();
if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName()) && payNym.isCollaborativeSend() != existingPayNym.isCollaborativeSend()) {
sendController.updateTransaction();
}
} }
public void updateMixOnlyStatus() { public void updateMixOnlyStatus() {
@ -350,25 +354,28 @@ public class PaymentController extends WalletFormController implements Initializ
} }
private Address getRecipientAddress() throws InvalidAddressException { private Address getRecipientAddress() throws InvalidAddressException {
if(payNymProperty.get() == null) { PayNym payNym = payNymProperty.get();
if(payNym == null) {
return Address.fromString(address.getText()); return Address.fromString(address.getText());
} }
try { if(!payNym.isCollaborativeSend()) {
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get()); try {
if(recipientBip47Wallet != null) { Wallet recipientBip47Wallet = getWalletForPayNym(payNym);
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND); if(recipientBip47Wallet != null) {
ECKey pubKey = sendNode.getPubKey(); WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey); ECKey pubKey = sendNode.getPubKey();
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) { Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
return address; 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 { private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {