mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-05 05:46:44 +00:00
allow collaborative sends for linked paynyms, support searching for custom paynyms when initiating collaborative sends
This commit is contained in:
parent
b1940e9293
commit
b16c7345a8
6 changed files with 119 additions and 43 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit d1088fe9ee6da0e53fcfdff020cab3c16c15702b
|
Subproject commit 20f4ac96574dc7bdb811380ff948931663e19c44
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue