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> followers;
private boolean collaborativeSend;
public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> 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<ScriptType> getScriptTypes() {
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
}

View file

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

View file

@ -9,10 +9,10 @@ import java.io.IOException;
public class PayNymDialog extends Dialog<PayNym> {
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<PayNym> {
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<PayNym> {
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;
}
}

View file

@ -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<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) {
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<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(payNym -> {
counterpartyPayNymName.set(payNym.nymName());

View file

@ -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<PayNym> 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 {