mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
support linking and sending to payment codes without paynym.is
This commit is contained in:
parent
ef5cca26ea
commit
a765e07c10
17 changed files with 601 additions and 79 deletions
|
@ -345,7 +345,6 @@ public class AppController implements Initializable {
|
|||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
showPayNym.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !getSelectedWalletForm().getWallet().hasPaymentCode() || !newValue);
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
|
@ -1989,7 +1988,7 @@ public class AppController implements Initializable {
|
|||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
@ -2015,7 +2014,7 @@ public class AppController implements Initializable {
|
|||
if(selectedWalletForm != null) {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -710,11 +710,18 @@ public class AppServices {
|
|||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, alertType, null, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
if(graphic != null) {
|
||||
alert.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
|
|
|
@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
|||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
|
@ -242,20 +239,36 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
|
||||
}
|
||||
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), false);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating RBF payment", e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
List<byte[]> opReturns = externalOutputs.stream().map(txOutput -> {
|
||||
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
|
||||
if(scriptChunks.size() != 2 || scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
|
||||
return null;
|
||||
}
|
||||
if(scriptChunks.get(1).getData() != null) {
|
||||
return scriptChunks.get(1).getData();
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(payments.isEmpty()) {
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
|
@ -287,7 +300,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), null, blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
|
|
|
@ -39,7 +39,7 @@ public class PayNymAvatar extends StackPane {
|
|||
String cacheId = getCacheId(paymentCode, getPrefWidth());
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
setImage(paymentCodeCache.get(cacheId));
|
||||
} else {
|
||||
} else if(AppServices.isConnected()) {
|
||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
|
||||
payNymAvatarService.setOnRunning(runningEvent -> {
|
||||
getChildren().clear();
|
||||
|
@ -48,7 +48,7 @@ public class PayNymAvatar extends StackPane {
|
|||
setImage(payNymAvatarService.getValue());
|
||||
});
|
||||
payNymAvatarService.setOnFailed(failedEvent -> {
|
||||
log.error("Error", failedEvent.getSource().getException());
|
||||
log.debug("Error loading PayNym avatar", failedEvent.getSource().getException());
|
||||
});
|
||||
payNymAvatarService.start();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletLabelChangedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymController;
|
||||
|
@ -10,6 +13,8 @@ import javafx.scene.layout.BorderPane;
|
|||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class PayNymCell extends ListCell<PayNym> {
|
||||
private final PayNymController payNymController;
|
||||
|
||||
|
@ -83,6 +88,12 @@ public class PayNymCell extends ListCell<PayNym> {
|
|||
|
||||
setText(null);
|
||||
setGraphic(pane);
|
||||
|
||||
if(payNymController != null && payNym.nymId() == null) {
|
||||
setContextMenu(new PayNymCellContextMenu(payNym));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,4 +102,30 @@ public class PayNymCell extends ListCell<PayNym> {
|
|||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private class PayNymCellContextMenu extends ContextMenu {
|
||||
public PayNymCellContextMenu(PayNym payNym) {
|
||||
MenuItem rename = new MenuItem("Rename Contact...");
|
||||
rename.setOnAction(event -> {
|
||||
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(payNym.nymName(), "Contact");
|
||||
Optional<String> optLabel = walletLabelDialog.showAndWait();
|
||||
if(optLabel.isPresent()) {
|
||||
int index = getListView().getItems().indexOf(payNym);
|
||||
for(Wallet childWallet : payNymController.getMasterWallet().getChildWallets()) {
|
||||
if(childWallet.isBip47()
|
||||
&& childWallet.getKeystores().get(0).getExternalPaymentCode().equals(payNym.paymentCode())
|
||||
&& (childWallet.getLabel() == null || childWallet.getLabel().startsWith(payNym.nymName()))) {
|
||||
childWallet.setLabel(optLabel.get() + " " + childWallet.getScriptType().getName());
|
||||
EventManager.get().post(new WalletLabelChangedEvent(childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
payNymController.updateFollowing();
|
||||
getListView().getSelectionModel().select(index);
|
||||
}
|
||||
});
|
||||
|
||||
getItems().add(rename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.image.Image;
|
||||
|
@ -22,4 +23,117 @@ public class ServiceProgressDialog extends ProgressDialog {
|
|||
Image image = new Image(imagePath);
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
}
|
||||
|
||||
public static class ProxyWorker implements Worker<Boolean> {
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", State.READY);
|
||||
private final StringProperty message = new SimpleStringProperty(this, "message", "");
|
||||
private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1);
|
||||
|
||||
public void start() {
|
||||
state.set(State.SCHEDULED);
|
||||
}
|
||||
|
||||
public void end() {
|
||||
state.set(State.SUCCEEDED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getValue() {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<Boolean> valueProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getException() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<Throwable> exceptionProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getWorkDone() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty workDoneProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTotalWork() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty totalWorkProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getProgress() {
|
||||
return progress.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyBooleanProperty runningProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message.get();
|
||||
}
|
||||
|
||||
public void setMessage(String strMessage) {
|
||||
message.set(strMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty messageProperty() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,19 +10,26 @@ import javafx.scene.layout.VBox;
|
|||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
public class WalletLabelDialog extends Dialog<String> {
|
||||
private static final int MAX_LABEL_LENGTH = 25;
|
||||
|
||||
private final CustomTextField label;
|
||||
|
||||
public WalletLabelDialog(String initialName) {
|
||||
this(initialName, "Account");
|
||||
}
|
||||
|
||||
public WalletLabelDialog(String initialName, String walletType) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Account Name");
|
||||
dialogPane.setHeaderText("Enter a name for this account:");
|
||||
setTitle(walletType + " Name");
|
||||
dialogPane.setHeaderText("Enter a name for this " + walletType.toLowerCase() + ":");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(400);
|
||||
|
@ -48,17 +55,18 @@ public class WalletLabelDialog extends Dialog<String> {
|
|||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(label, Validator.combine(
|
||||
Validator.createEmptyValidator("Account name is required")
|
||||
Validator.createEmptyValidator(walletType + " name is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Label too long", newValue != null && newValue.length() > MAX_LABEL_LENGTH)
|
||||
));
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename " + walletType, ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0 || label.getText().length() > MAX_LABEL_LENGTH, label.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
label.setPromptText("Account Name");
|
||||
label.setPromptText(walletType + " Name");
|
||||
Platform.runLater(label::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : null);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
@ -15,19 +16,21 @@ public class SpendUtxoEvent {
|
|||
private final Long fee;
|
||||
private final boolean includeSpentMempoolOutputs;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, false);
|
||||
this(wallet, utxos, null, null, null, false);
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeSpentMempoolOutputs) {
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean includeSpentMempoolOutputs) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = null;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
|
@ -38,6 +41,18 @@ public class SpendUtxoEvent {
|
|||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = null;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = null;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
|
@ -67,4 +82,8 @@ public class SpendUtxoEvent {
|
|||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ package com.sparrowwallet.sparrow.paynym;
|
|||
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PayNym {
|
||||
|
@ -74,4 +76,22 @@ public class PayNym {
|
|||
|
||||
return new PayNym(paymentCode, nymId, nymName, segwit, following, followers);
|
||||
}
|
||||
|
||||
public static PayNym fromWallet(Wallet bip47Wallet) {
|
||||
if(!bip47Wallet.isBip47()) {
|
||||
throw new IllegalArgumentException("Not a BIP47 wallet");
|
||||
}
|
||||
|
||||
PaymentCode externalPaymentCode = bip47Wallet.getKeystores().get(0).getExternalPaymentCode();
|
||||
String nymName = externalPaymentCode.toAbbreviatedString();
|
||||
if(bip47Wallet.getLabel() != null) {
|
||||
String suffix = " " + bip47Wallet.getScriptType().getName();
|
||||
if(bip47Wallet.getLabel().endsWith(suffix)) {
|
||||
nymName = bip47Wallet.getLabel().substring(0, bip47Wallet.getLabel().length() - suffix.length());
|
||||
}
|
||||
}
|
||||
|
||||
boolean segwit = bip47Wallet.getScriptType() != ScriptType.P2PKH;
|
||||
return new PayNym(externalPaymentCode, null, nymName, segwit, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
|||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
|
@ -34,18 +31,19 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.wallet.PaymentController.MINIMUM_P2PKH_OUTPUT_SATS;
|
||||
|
||||
public class PayNymController {
|
||||
private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
|
||||
|
||||
public static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
|
||||
|
||||
private static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L;
|
||||
|
||||
private String walletId;
|
||||
private boolean selectLinkedOnly;
|
||||
private PayNym walletPayNym;
|
||||
|
@ -65,6 +63,9 @@ public class PayNymController {
|
|||
@FXML
|
||||
private CopyableTextField searchPayNyms;
|
||||
|
||||
@FXML
|
||||
private Button searchPayNymsScan;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator findPayNym;
|
||||
|
||||
|
@ -83,6 +84,8 @@ public class PayNymController {
|
|||
|
||||
private final Map<Sha256Hash, PayNym> notificationTransactions = new HashMap<>();
|
||||
|
||||
private final BooleanProperty closeProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public void initializeView(String walletId, boolean selectLinkedOnly) {
|
||||
this.walletId = walletId;
|
||||
this.selectLinkedOnly = selectLinkedOnly;
|
||||
|
@ -90,6 +93,7 @@ public class PayNymController {
|
|||
payNymName.managedProperty().bind(payNymName.visibleProperty());
|
||||
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
|
||||
payNymRetrieve.visibleProperty().bind(payNymName.visibleProperty().not());
|
||||
payNymRetrieve.setDisable(!AppServices.isConnected());
|
||||
|
||||
retrievePayNymProgress.managedProperty().bind(retrievePayNymProgress.visibleProperty());
|
||||
retrievePayNymProgress.maxHeightProperty().bind(payNymName.heightProperty());
|
||||
|
@ -132,6 +136,8 @@ public class PayNymController {
|
|||
|
||||
return change;
|
||||
};
|
||||
searchPayNymsScan.disableProperty().bind(searchPayNyms.disableProperty());
|
||||
searchPayNyms.setDisable(true);
|
||||
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
|
||||
searchPayNyms.addEventFilter(KeyEvent.ANY, event -> {
|
||||
if(event.getCode() == KeyCode.ENTER) {
|
||||
|
@ -158,10 +164,11 @@ public class PayNymController {
|
|||
followersList.setSelectionModel(new NoSelectionModel<>());
|
||||
followersList.setFocusTraversable(false);
|
||||
|
||||
if(Config.get().isUsePayNym() && masterWallet.hasPaymentCode()) {
|
||||
if(Config.get().isUsePayNym() && AppServices.isConnected() && masterWallet.hasPaymentCode()) {
|
||||
refresh();
|
||||
} else {
|
||||
payNymName.setVisible(false);
|
||||
updateFollowing();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,12 +181,13 @@ public class PayNymController {
|
|||
AppServices.getPayNymService().getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
|
||||
retrievePayNymProgress.setVisible(false);
|
||||
walletPayNym = payNym;
|
||||
searchPayNyms.setDisable(false);
|
||||
payNymName.setText(payNym.nymName());
|
||||
paymentCode.setPaymentCode(payNym.paymentCode());
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
followingList.setUserData(null);
|
||||
followingList.setPlaceholder(new Label("No contacts"));
|
||||
followingList.setItems(FXCollections.observableList(payNym.following()));
|
||||
updateFollowing();
|
||||
followersList.setPlaceholder(new Label("No followers"));
|
||||
followersList.setItems(FXCollections.observableList(payNym.followers()));
|
||||
Platform.runLater(() -> addWalletIfNotificationTransactionPresent(payNym.following()));
|
||||
|
@ -187,6 +195,7 @@ public class PayNymController {
|
|||
retrievePayNymProgress.setVisible(false);
|
||||
if(error.getMessage().endsWith("404")) {
|
||||
payNymName.setVisible(false);
|
||||
updateFollowing();
|
||||
} else {
|
||||
log.error("Error retrieving PayNym", error);
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
|
@ -194,6 +203,7 @@ public class PayNymController {
|
|||
refresh();
|
||||
} else {
|
||||
payNymName.setVisible(false);
|
||||
updateFollowing();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -202,7 +212,7 @@ public class PayNymController {
|
|||
private void resetFollowing() {
|
||||
if(followingList.getUserData() != null) {
|
||||
followingList.setUserData(null);
|
||||
followingList.setItems(FXCollections.observableList(walletPayNym.following()));
|
||||
updateFollowing();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,6 +310,33 @@ public class PayNymController {
|
|||
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
|
||||
}
|
||||
|
||||
public void updateFollowing() {
|
||||
List<PayNym> followingPayNyms = new ArrayList<>();
|
||||
if(walletPayNym != null) {
|
||||
followingPayNyms.addAll(walletPayNym.following());
|
||||
}
|
||||
|
||||
Map<PaymentCode, PayNym> followingPayNymMap = followingPayNyms.stream().collect(Collectors.toMap(PayNym::paymentCode, Function.identity()));
|
||||
followingPayNyms.addAll(getExistingWalletPayNyms(followingPayNymMap));
|
||||
followingList.setItems(FXCollections.observableList(followingPayNyms));
|
||||
}
|
||||
|
||||
private List<PayNym> getExistingWalletPayNyms(Map<PaymentCode, PayNym> followingPayNymMap) {
|
||||
Map<PaymentCode, PayNym> existingPayNyms = new LinkedHashMap<>();
|
||||
List<Wallet> childWallets = new ArrayList<>(getMasterWallet().getChildWallets());
|
||||
childWallets.sort(Comparator.comparingInt(o -> -o.getScriptType().ordinal()));
|
||||
for(Wallet childWallet : childWallets) {
|
||||
if(childWallet.isBip47()) {
|
||||
PaymentCode externalPaymentCode = childWallet.getKeystores().get(0).getExternalPaymentCode();
|
||||
if(!existingPayNyms.containsKey(externalPaymentCode) && !followingPayNymMap.containsKey(externalPaymentCode)) {
|
||||
existingPayNyms.put(externalPaymentCode, PayNym.fromWallet(childWallet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(existingPayNyms.values());
|
||||
}
|
||||
|
||||
private void addWalletIfNotificationTransactionPresent(List<PayNym> following) {
|
||||
Map<BlockTransaction, PayNym> unlinkedPayNyms = new HashMap<>();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
|
||||
|
@ -397,11 +434,20 @@ public class PayNymController {
|
|||
}
|
||||
|
||||
public void linkPayNym(PayNym payNym) {
|
||||
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" +
|
||||
"It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
"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);
|
||||
} else if(optButtonType.isPresent() && optButtonType.get() == previewType) {
|
||||
PaymentCode paymentCode = payNym.paymentCode();
|
||||
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet())));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, List.of(payment), List.of(new byte[80]), paymentCode)));
|
||||
closeProperty.set(true);
|
||||
} else {
|
||||
followingList.refresh();
|
||||
}
|
||||
|
@ -544,7 +590,7 @@ public class PayNymController {
|
|||
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false);
|
||||
}
|
||||
|
||||
private Wallet getMasterWallet() {
|
||||
public Wallet getMasterWallet() {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
}
|
||||
|
@ -561,6 +607,10 @@ public class PayNymController {
|
|||
return payNymProperty;
|
||||
}
|
||||
|
||||
public BooleanProperty closeProperty() {
|
||||
return closeProperty;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||
|
|
|
@ -48,6 +48,12 @@ public class PayNymDialog extends Dialog<PayNym> {
|
|||
dialogPane.getButtonTypes().add(doneButtonType);
|
||||
}
|
||||
|
||||
payNymController.closeProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(payNymController);
|
||||
});
|
||||
|
|
|
@ -18,10 +18,8 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
|
@ -41,7 +39,7 @@ import javafx.event.ActionEvent;
|
|||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
|
@ -59,6 +57,8 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
|||
public class PaymentController extends WalletFormController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentController.class);
|
||||
|
||||
public static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L;
|
||||
|
||||
private SendController sendController;
|
||||
|
||||
private ValidationSupport validationSupport;
|
||||
|
@ -115,7 +115,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
Long recipientValueSats = getRecipientValueSats();
|
||||
if(recipientValueSats != null) {
|
||||
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), recipientValueSats);
|
||||
dustAmountProperty.set(recipientValueSats <= getRecipientDustThreshold());
|
||||
dustAmountProperty.set(recipientValueSats < getRecipientDustThreshold());
|
||||
emptyAmountProperty.set(false);
|
||||
} else {
|
||||
fiatAmount.setText("");
|
||||
|
@ -134,7 +134,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
private static final Wallet payNymWallet = new Wallet() {
|
||||
@Override
|
||||
public String getFullDisplayName() {
|
||||
return "PayNym...";
|
||||
return "PayNym or Payment code...";
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -150,17 +150,6 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
|
||||
@Override
|
||||
public void initializeView() {
|
||||
openWallets.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Wallet wallet) {
|
||||
return wallet == null ? "" : wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
updateOpenWallets();
|
||||
openWallets.prefWidthProperty().bind(address.widthProperty());
|
||||
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
@ -168,12 +157,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet());
|
||||
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true, selectLinkedOnly);
|
||||
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||
if(optPayNym.isPresent()) {
|
||||
PayNym payNym = optPayNym.get();
|
||||
payNymProperty.set(payNym);
|
||||
address.setText(payNym.nymName());
|
||||
label.requestFocus();
|
||||
}
|
||||
optPayNym.ifPresent(this::setPayNym);
|
||||
} else if(newValue != null) {
|
||||
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Address freshAddress = freshNode.getAddress();
|
||||
|
@ -181,6 +165,19 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
label.requestFocus();
|
||||
}
|
||||
});
|
||||
openWallets.setCellFactory(c -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Wallet wallet, boolean empty) {
|
||||
super.updateItem(wallet, empty);
|
||||
if(empty || wallet == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : ""));
|
||||
setGraphic(wallet == payNymWallet ? getPayNymGlyph() : null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
payNymProperty.addListener((observable, oldValue, payNym) -> {
|
||||
updateMixOnlyStatus(payNym);
|
||||
|
@ -188,6 +185,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
address.leftProperty().set(null);
|
||||
|
||||
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
|
||||
payNymProperty.set(null);
|
||||
}
|
||||
|
@ -200,6 +199,32 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
//ignore, not a URI
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
revalidateAmount();
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.updateTransaction();
|
||||
|
@ -253,6 +278,13 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
addValidation(validationSupport);
|
||||
}
|
||||
|
||||
public void setPayNym(PayNym payNym) {
|
||||
payNymProperty.set(payNym);
|
||||
address.setText(payNym.nymName());
|
||||
address.leftProperty().set(getPayNymGlyph());
|
||||
label.requestFocus();
|
||||
}
|
||||
|
||||
public void updateMixOnlyStatus() {
|
||||
updateMixOnlyStatus(payNymProperty.get());
|
||||
}
|
||||
|
@ -296,7 +328,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
));
|
||||
validationSupport.registerValidator(amount, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", getRecipientValueSats() != null && sendController.isInsufficientInputs()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() <= getRecipientDustThreshold())
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() < getRecipientDustThreshold())
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -394,7 +426,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
public void revalidateAmount() {
|
||||
revalidate(amount, amountListener);
|
||||
Long recipientValueSats = getRecipientValueSats();
|
||||
dustAmountProperty.set(recipientValueSats != null && recipientValueSats <= getRecipientDustThreshold());
|
||||
dustAmountProperty.set(recipientValueSats != null && recipientValueSats < getRecipientDustThreshold());
|
||||
emptyAmountProperty.set(recipientValueSats == null);
|
||||
}
|
||||
|
||||
|
@ -426,7 +458,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
Address recipientAddress = getRecipientAddress();
|
||||
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
|
||||
|
||||
if(!label.getText().isEmpty() && value != null && value > getRecipientDustThreshold()) {
|
||||
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
|
||||
Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
|
||||
if(address.getUserData() != null) {
|
||||
payment.setType((Payment.Type)address.getUserData());
|
||||
|
@ -509,6 +541,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.uri != null) {
|
||||
updateFromURI(result.uri);
|
||||
} else if(result.payload != null) {
|
||||
address.setText(result.payload);
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
|
@ -556,6 +590,14 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
amountUnit.setDisable(disable);
|
||||
scanQrButton.setDisable(disable);
|
||||
addPaymentButton.setDisable(disable);
|
||||
maxButton.setDisable(disable);
|
||||
}
|
||||
|
||||
public static Glyph getPayNymGlyph() {
|
||||
Glyph payNymGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ROBOT);
|
||||
payNymGlyph.getStyleClass().add("paynym-icon");
|
||||
payNymGlyph.setFontSize(12);
|
||||
return payNymGlyph;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
@ -4,10 +4,16 @@ import com.google.common.eventbus.Subscribe;
|
|||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutPoint;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
|
@ -19,6 +25,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
|||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
|
@ -26,6 +33,7 @@ import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
|||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
@ -143,6 +151,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private Button premixButton;
|
||||
|
||||
@FXML
|
||||
private Button notificationButton;
|
||||
|
||||
private StackPane tabHeader;
|
||||
|
||||
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
||||
|
@ -153,6 +164,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
||||
|
@ -218,8 +231,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> premixButtonOnlineListener = (observable, oldValue, newValue) -> {
|
||||
private final ChangeListener<Boolean> broadcastButtonsOnlineListener = (observable, oldValue, newValue) -> {
|
||||
premixButton.setDisable(!newValue);
|
||||
notificationButton.setDisable(walletTransactionProperty.get() == null || isInsufficientFeeRate() || !newValue);
|
||||
};
|
||||
|
||||
private ValidationSupport validationSupport;
|
||||
|
@ -397,6 +411,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
transactionDiagram.update(walletTransaction);
|
||||
updatePrivacyAnalysis(walletTransaction);
|
||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymMixOnlyPayment(walletTransaction.getPayments()));
|
||||
notificationButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || !AppServices.isConnected());
|
||||
});
|
||||
|
||||
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
||||
|
@ -430,9 +445,11 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
createButton.managedProperty().bind(createButton.visibleProperty());
|
||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
||||
notificationButton.managedProperty().bind(notificationButton.visibleProperty());
|
||||
createButton.visibleProperty().bind(Bindings.and(premixButton.visibleProperty().not(), notificationButton.visibleProperty().not()));
|
||||
premixButton.setVisible(false);
|
||||
AppServices.onlineProperty().addListener(new WeakChangeListener<>(premixButtonOnlineListener));
|
||||
notificationButton.setVisible(false);
|
||||
AppServices.onlineProperty().addListener(new WeakChangeListener<>(broadcastButtonsOnlineListener));
|
||||
}
|
||||
|
||||
private void initializeTabHeader(int count) {
|
||||
|
@ -582,6 +599,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
if(currentWalletTransactionService.isRunning()) {
|
||||
transactionDiagram.update("Selecting UTXOs...");
|
||||
createButton.setDisable(true);
|
||||
notificationButton.setDisable(true);
|
||||
}
|
||||
});
|
||||
final Timeline timeline = new Timeline(delay);
|
||||
|
@ -1047,13 +1065,17 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
|
||||
setInputFieldsDisabled(false);
|
||||
setInputFieldsDisabled(false, false);
|
||||
|
||||
efficiencyToggle.setDisable(false);
|
||||
privacyToggle.setDisable(false);
|
||||
|
||||
premixButton.setVisible(false);
|
||||
notificationButton.setVisible(false);
|
||||
createButton.setDefaultButton(true);
|
||||
|
||||
whirlpoolProperty.set(null);
|
||||
paymentCodeProperty.set(null);
|
||||
}
|
||||
|
||||
public UtxoSelector getUtxoSelector() {
|
||||
|
@ -1181,18 +1203,182 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
tx0BroadcastService.start();
|
||||
}
|
||||
|
||||
private void setInputFieldsDisabled(boolean disable) {
|
||||
public void broadcastNotification(ActionEvent event) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
if(wallet.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
broadcastNotification(decryptedWallet);
|
||||
decryptedWallet.clearPrivate();
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
broadcastNotification(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastNotification(Wallet decryptedWallet) {
|
||||
try {
|
||||
PaymentCode paymentCode = decryptedWallet.getPaymentCode();
|
||||
PaymentCode externalPaymentCode = paymentCodeProperty.get();
|
||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||
Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0);
|
||||
ECKey input0Key = keystore.getKey(input0Node);
|
||||
TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint();
|
||||
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
|
||||
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||
byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
||||
|
||||
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true));
|
||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||
double feeRate = getUserFeeRate();
|
||||
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
||||
|
||||
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getUtxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
PSBT psbt = finalWalletTx.createPSBT();
|
||||
decryptedWallet.sign(psbt);
|
||||
decryptedWallet.finalise(psbt);
|
||||
Transaction transaction = psbt.extractTransaction();
|
||||
|
||||
ServiceProgressDialog.ProxyWorker proxyWorker = new ServiceProgressDialog.ProxyWorker();
|
||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction);
|
||||
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
||||
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
||||
transactionMempoolService.setDelay(Duration.seconds(2));
|
||||
transactionMempoolService.setPeriod(Duration.seconds(5));
|
||||
transactionMempoolService.setRestartOnFailure(false);
|
||||
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
|
||||
Set<String> scriptHashes = transactionMempoolService.getValue();
|
||||
if(!scriptHashes.isEmpty()) {
|
||||
transactionMempoolService.cancel();
|
||||
clear(null);
|
||||
if(Config.get().isUsePayNym()) {
|
||||
proxyWorker.setMessage("Finding PayNym...");
|
||||
AppServices.getPayNymService().getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
|
||||
proxyWorker.end();
|
||||
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, payNym);
|
||||
}, error -> {
|
||||
proxyWorker.end();
|
||||
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, null);
|
||||
});
|
||||
} else {
|
||||
proxyWorker.end();
|
||||
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, null);
|
||||
}
|
||||
}
|
||||
|
||||
if(transactionMempoolService.getIterationCount() > 5 && transactionMempoolService.isRunning()) {
|
||||
transactionMempoolService.cancel();
|
||||
proxyWorker.end();
|
||||
log.error("Timeout searching for broadcasted notification transaction");
|
||||
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
|
||||
}
|
||||
});
|
||||
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
||||
transactionMempoolService.cancel();
|
||||
proxyWorker.end();
|
||||
log.error("Error searching for broadcasted notification transaction", mempoolWorkerStateEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
|
||||
});
|
||||
proxyWorker.setMessage("Receiving notification transaction...");
|
||||
transactionMempoolService.start();
|
||||
});
|
||||
broadcastTransactionService.setOnFailed(failedEvent -> {
|
||||
proxyWorker.end();
|
||||
log.error("Error broadcasting notification transaction", failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error broadcasting notification transaction", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
ServiceProgressDialog progressDialog = new ServiceProgressDialog("Broadcast", "Broadcast Notification Transaction", "/image/paynym.png", proxyWorker);
|
||||
AppServices.moveToActiveWindowScreen(progressDialog);
|
||||
proxyWorker.setMessage("Broadcasting notification transaction...");
|
||||
proxyWorker.start();
|
||||
broadcastTransactionService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating notification transaction", e);
|
||||
AppServices.showErrorDialog("Error creating notification transaction", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void addChildWallets(Wallet wallet, PaymentCode externalPaymentCode, Transaction transaction, PayNym payNym) {
|
||||
List<Wallet> addedWallets = addChildWallets(externalPaymentCode, payNym);
|
||||
Wallet masterWallet = getWalletForm().getMasterWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, masterWallet, addedWallets));
|
||||
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(transaction.getTxId());
|
||||
if(blockTransaction != null && blockTransaction.getLabel() == null) {
|
||||
blockTransaction.setLabel("Link " + (payNym == null ? externalPaymentCode.toAbbreviatedString() : payNym.nymName()));
|
||||
TransactionEntry transactionEntry = new TransactionEntry(wallet, blockTransaction, Collections.emptyMap(), Collections.emptyMap());
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, List.of(transactionEntry)));
|
||||
}
|
||||
|
||||
if(paymentTabs.getTabs().size() > 0 && !addedWallets.isEmpty()) {
|
||||
Wallet addedWallet = addedWallets.stream().filter(w -> w.getScriptType() == ScriptType.P2WPKH).findFirst().orElse(addedWallets.iterator().next());
|
||||
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(0).getUserData();
|
||||
controller.setPayNym(payNym == null ? PayNym.fromWallet(addedWallet) : payNym);
|
||||
}
|
||||
|
||||
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
successGlyph.getStyleClass().add("success");
|
||||
successGlyph.setFontSize(50);
|
||||
|
||||
AppServices.showAlertDialog("Notification Successful", "The notification transaction was successfully sent for payment code " +
|
||||
externalPaymentCode.toAbbreviatedString() + (payNym == null ? "" : " (" + payNym.nymName() + ")") +
|
||||
".\n\nYou can send to it by entering the payment code, or selecting `PayNym or Payment code` in the Pay to dropdown.", Alert.AlertType.INFORMATION, successGlyph, ButtonType.OK);
|
||||
}
|
||||
|
||||
public List<Wallet> addChildWallets(PaymentCode externalPaymentCode, PayNym payNym) {
|
||||
List<Wallet> addedWallets = new ArrayList<>();
|
||||
Wallet masterWallet = getWalletForm().getMasterWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||
List<ScriptType> scriptTypes = PayNym.getSegwitScriptTypes();
|
||||
for(ScriptType childScriptType : scriptTypes) {
|
||||
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType);
|
||||
addedWallet.setLabel((payNym == null ? externalPaymentCode.toAbbreviatedString() : payNym.nymName()) + " " + childScriptType.getName());
|
||||
if(!storage.isPersisted(addedWallet)) {
|
||||
try {
|
||||
storage.saveWallet(addedWallet);
|
||||
} catch(Exception e) {
|
||||
log.error("Error saving wallet", e);
|
||||
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
addedWallets.add(addedWallet);
|
||||
}
|
||||
|
||||
return addedWallets;
|
||||
}
|
||||
|
||||
private void setInputFieldsDisabled(boolean disablePayments, boolean disableFeeSelection) {
|
||||
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
||||
Tab tab = paymentTabs.getTabs().get(i);
|
||||
tab.setClosable(!disable);
|
||||
tab.setClosable(!disablePayments);
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.setInputFieldsDisabled(disable);
|
||||
|
||||
feeRange.setDisable(disable);
|
||||
targetBlocks.setDisable(disable);
|
||||
fee.setDisable(disable);
|
||||
feeAmountUnit.setDisable(disable);
|
||||
controller.setInputFieldsDisabled(disablePayments);
|
||||
}
|
||||
|
||||
feeRange.setDisable(disableFeeSelection);
|
||||
targetBlocks.setDisable(disableFeeSelection);
|
||||
fee.setDisable(disableFeeSelection);
|
||||
feeAmountUnit.setDisable(disableFeeSelection);
|
||||
|
||||
transactionDiagram.requestFocus();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
@ -1262,11 +1448,15 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void spendUtxos(SpendUtxoEvent event) {
|
||||
if(!event.getUtxos().isEmpty() && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(whirlpoolProperty.get() != null || paymentCodeProperty.get() != null) {
|
||||
clear(null);
|
||||
}
|
||||
|
||||
if(event.getPayments() != null) {
|
||||
clear(null);
|
||||
setPayments(event.getPayments());
|
||||
} else if(paymentTabs.getTabs().size() == 1) {
|
||||
} else if(paymentTabs.getTabs().size() == 1 && event.getUtxos() != null) {
|
||||
Payment payment = new Payment(null, null, event.getUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
|
||||
setPayments(List.of(payment));
|
||||
}
|
||||
|
@ -1282,16 +1472,25 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
includeSpentMempoolOutputsProperty.set(event.isIncludeSpentMempoolOutputs());
|
||||
|
||||
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
||||
if(event.getUtxos() != null) {
|
||||
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
||||
}
|
||||
|
||||
utxoFilterProperty.set(null);
|
||||
whirlpoolProperty.set(event.getPool());
|
||||
paymentCodeProperty.set(event.getPaymentCode());
|
||||
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||
|
||||
boolean isWhirlpoolPremix = (event.getPool() != null);
|
||||
setInputFieldsDisabled(isWhirlpoolPremix);
|
||||
premixButton.setVisible(isWhirlpoolPremix);
|
||||
premixButton.setDefaultButton(isWhirlpoolPremix);
|
||||
|
||||
boolean isNotificationTransaction = (event.getPaymentCode() != null);
|
||||
notificationButton.setVisible(isNotificationTransaction);
|
||||
notificationButton.setDefaultButton(isNotificationTransaction);
|
||||
|
||||
setInputFieldsDisabled(isWhirlpoolPremix || isNotificationTransaction, isWhirlpoolPremix);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<Label text="Find Contact:" styleClass="field-label" />
|
||||
<HBox spacing="10">
|
||||
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/>
|
||||
<Button onAction="#scanQR">
|
||||
<Button fx:id="searchPayNymsScan" onAction="#scanQR">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
||||
</graphic>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#address .paynym-icon {
|
||||
-fx-padding: 0 0 0 5;
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
<ComboBox fx:id="openWallets" />
|
||||
<ComboBoxTextField fx:id="address" styleClass="address-text-field" comboProperty="$openWallets">
|
||||
<tooltip>
|
||||
<Tooltip text="Address or bitcoin: URI"/>
|
||||
<Tooltip text="Address, payment code or bitcoin: URI"/>
|
||||
</tooltip>
|
||||
</ComboBoxTextField>
|
||||
</StackPane>
|
||||
|
|
|
@ -186,6 +186,11 @@
|
|||
<HBox AnchorPane.rightAnchor="10">
|
||||
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
|
||||
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
|
||||
<Button fx:id="notificationButton" text="Broadcast Notification" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastNotification">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="SATELLITE_DISH" />
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button fx:id="premixButton" text="Broadcast Premix Transaction" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastPremix">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="RANDOM" />
|
||||
|
|
Loading…
Reference in a new issue