mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c87ecd4ec | ||
|
|
5324e5fcc2 | ||
|
|
c02da607e7 | ||
|
|
281fad5970 | ||
|
|
95d8201bd9 | ||
|
|
04cb27f85e | ||
|
|
a765e07c10 | ||
|
|
ef5cca26ea | ||
|
|
d86517606b | ||
|
|
9dcf3b7eea | ||
|
|
689f4abfde | ||
|
|
5357b55ef4 | ||
|
|
a10bdef484 | ||
|
|
58f20dab60 | ||
|
|
1c7abc1b24 | ||
|
|
02e0fd1357 | ||
|
|
7b3ff2a6d3 |
+3
-4
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.6.0'
|
||||
def sparrowVersion = '1.6.2'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -127,7 +127,7 @@ test {
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError", "-Djava.net.preferIPv6Addresses=system",
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
@@ -173,8 +173,7 @@ jlink {
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["-Djava.net.preferIPv6Addresses=system",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
|
||||
+1
-1
Submodule drongo updated: 0734757a17...fe57ad3e7a
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.0</string>
|
||||
<string>1.6.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.6.0";
|
||||
public static final String APP_VERSION = "1.6.2";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -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;
|
||||
@@ -123,15 +120,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
}
|
||||
|
||||
if(canSignMessage(nodeEntry.getNode())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
@@ -151,7 +151,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||
@@ -175,7 +175,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
actionBox.getChildren().add(spendUtxoButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,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() {
|
||||
@@ -284,12 +300,14 @@ 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(Wallet wallet) {
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
|
||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
@@ -502,16 +520,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
|
||||
@@ -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,16 +13,20 @@ 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;
|
||||
private final boolean contact;
|
||||
|
||||
public PayNymCell(PayNymController payNymController) {
|
||||
public PayNymCell(PayNymController payNymController, boolean contact) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
getStyleClass().add("paynym-cell");
|
||||
setPrefHeight(50);
|
||||
this.payNymController = payNymController;
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,7 +54,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
labelBox.getChildren().add(label);
|
||||
pane.setLeft(labelBox);
|
||||
|
||||
if(getListView().getUserData() == Boolean.TRUE) {
|
||||
if(getListView().getUserData() == Boolean.TRUE || (!contact && payNymController != null && payNymController.isFollowing(payNym) == Boolean.FALSE)) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
Button button = new Button("Add Contact");
|
||||
@@ -57,7 +64,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
button.setDisable(true);
|
||||
payNymController.followPayNym(payNym.paymentCode());
|
||||
});
|
||||
} else if(payNymController != null) {
|
||||
} else if(contact && payNymController != null) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
pane.setRight(hBox);
|
||||
@@ -83,6 +90,12 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
|
||||
setText(null);
|
||||
setGraphic(pane);
|
||||
|
||||
if(contact && payNymController != null && payNymController.isLinked(payNym)) {
|
||||
setContextMenu(new PayNymCellContextMenu(payNym));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +104,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class ProgressTimer extends ProgressIndicator {
|
||||
}
|
||||
|
||||
public void start(EventHandler<ActionEvent> onFinished) {
|
||||
getStyleClass().remove("warn");
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,13 +82,14 @@ public class TransactionDiagram extends GridPane {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
vBox.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
vBox.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
vBox.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setPadding(new Insets(20, 40, 20, 50));
|
||||
|
||||
expandedDiagram = new TransactionDiagram();
|
||||
|
||||
@@ -165,8 +165,9 @@ public class TransactionHexArea extends CodeArea {
|
||||
for(int j = 0; j < witness.getPushes().size(); j++) {
|
||||
byte[] push = witness.getPushes().get(j);
|
||||
VarInt witnessLen = new VarInt(push.length);
|
||||
boolean isSignature = isSignature(push);
|
||||
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data" + (isSignature ? "-signature" : "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +205,19 @@ public class TransactionHexArea extends CodeArea {
|
||||
return "other";
|
||||
}
|
||||
|
||||
private boolean isSignature(byte[] data) {
|
||||
if(data.length >= 64) {
|
||||
try {
|
||||
TransactionSignature.decodeFromBitcoin(data, false);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
//ignore, not a signature
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String describeTransactionPart(Collection<String> styles) {
|
||||
String style = "";
|
||||
Integer index = null;
|
||||
@@ -238,7 +252,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
case "output-pubkeyscript" -> "Output #" + index + " scriptPubKey";
|
||||
case "witness-count" -> "Input #" + index + " witness count";
|
||||
case "witness-length" -> "Input #" + index + " witness #" + witnessIndex + " length";
|
||||
case "witness-data" -> "Input #" + index + " witness #" + witnessIndex + " data";
|
||||
case "witness-data", "witness-data-signature" -> "Input #" + index + " witness #" + witnessIndex + " data";
|
||||
case "locktime" -> "Locktime";
|
||||
default -> "";
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
private synchronized void createUpdateExecutor(Wallet masterWallet) {
|
||||
if(updateExecutor == null) {
|
||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(false).priority(Thread.NORM_PRIORITY).build();
|
||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
|
||||
updateExecutor = Executors.newSingleThreadExecutor(factory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1303,6 +1303,11 @@ public class ElectrumServer {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<WalletNode> nodes = (filterToNodes == null ? null : filterToNodes.stream().filter(node -> node.getWallet().equals(wallet)).collect(Collectors.toSet()));
|
||||
if(filterToNodes != null && nodes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean initial = (walletSynchronizeLocks.putIfAbsent(wallet, new Object()) == null);
|
||||
synchronized(walletSynchronizeLocks.get(wallet)) {
|
||||
if(initial) {
|
||||
@@ -1311,7 +1316,6 @@ public class ElectrumServer {
|
||||
|
||||
if(isConnected()) {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Set<WalletNode> nodes = (filterToNodes == null ? null : filterToNodes.stream().filter(node -> node.getWallet().equals(wallet)).collect(Collectors.toSet()));
|
||||
|
||||
Map<String, String> previousScriptHashes = getCalculatedScriptHashes(wallet);
|
||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes));
|
||||
@@ -1713,12 +1717,8 @@ public class ElectrumServer {
|
||||
PayNym payNym = Config.get().isUsePayNym() ? getPayNym(paymentCode) : null;
|
||||
List<ScriptType> scriptTypes = payNym == null || wallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
|
||||
for(ScriptType childScriptType : scriptTypes) {
|
||||
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx);
|
||||
if(payNym != null) {
|
||||
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
|
||||
} else {
|
||||
addedWallet.setLabel(paymentCode.toAbbreviatedString() + " " + childScriptType.getName());
|
||||
}
|
||||
String label = (payNym == null ? paymentCode.toAbbreviatedString() : payNym.nymName()) + " " + childScriptType.getName();
|
||||
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx, label);
|
||||
//Check this is a valid payment code, will throw IllegalArgumentException if not
|
||||
try {
|
||||
WalletNode receiveNode = new WalletNode(addedWallet, KeyPurpose.RECEIVE, 0);
|
||||
|
||||
@@ -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,31 @@ 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();
|
||||
String walletNymName = getNymName(bip47Wallet);
|
||||
if(walletNymName != null) {
|
||||
nymName = walletNymName;
|
||||
}
|
||||
|
||||
boolean segwit = bip47Wallet.getScriptType() != ScriptType.P2PKH;
|
||||
return new PayNym(externalPaymentCode, null, nymName, segwit, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public static String getNymName(Wallet bip47Wallet) {
|
||||
if(bip47Wallet.getLabel() != null) {
|
||||
String suffix = " " + bip47Wallet.getScriptType().getName();
|
||||
if(bip47Wallet.getLabel().endsWith(suffix)) {
|
||||
return bip47Wallet.getLabel().substring(0, bip47Wallet.getLabel().length() - suffix.length());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PayNymAddressesController {
|
||||
|
||||
@FXML
|
||||
private ComboBox<WalletForm> payNymWalletForms;
|
||||
|
||||
@FXML
|
||||
private AddressTreeTable receiveTable;
|
||||
|
||||
@FXML
|
||||
private AddressTreeTable sendTable;
|
||||
|
||||
public void initializeView(WalletForm walletForm) {
|
||||
payNymWalletForms.setItems(FXCollections.observableList(walletForm.getNestedWalletForms().stream().filter(nested -> nested.getWallet().isBip47()).collect(Collectors.toList())));
|
||||
payNymWalletForms.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WalletForm nested) {
|
||||
return nested == null ? "" : nested.getWallet().getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletForm fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Optional<WalletForm> optInitial = walletForm.getNestedWalletForms().stream().filter(nested -> nested.getWallet().isBip47() && nested.getWallet().getScriptType() == ScriptType.P2WPKH).findFirst();
|
||||
if(optInitial.isPresent()) {
|
||||
optInitial.get().getAccountEntries().clear();
|
||||
receiveTable.initialize(optInitial.get().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
sendTable.initialize(optInitial.get().getNodeEntry(KeyPurpose.SEND));
|
||||
payNymWalletForms.getSelectionModel().select(optInitial.get());
|
||||
}
|
||||
|
||||
payNymWalletForms.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, selected) -> {
|
||||
selected.getAccountEntries().clear();
|
||||
receiveTable.updateAll(selected.getNodeEntry(KeyPurpose.RECEIVE));
|
||||
sendTable.updateAll(selected.getNodeEntry(KeyPurpose.SEND));
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(event.getWallet().equals(payNymWalletForms.getValue().getWallet())) {
|
||||
List<WalletNode> receiveNodes = event.getReceiveNodes();
|
||||
if(!receiveNodes.isEmpty()) {
|
||||
receiveTable.updateHistory(receiveNodes);
|
||||
}
|
||||
|
||||
List<WalletNode> sendNodes = event.getChangeNodes();
|
||||
if(!sendNodes.isEmpty()) {
|
||||
sendTable.updateHistory(sendNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet().equals(payNymWalletForms.getValue().getWallet())) {
|
||||
for(Entry entry : event.getEntries()) {
|
||||
receiveTable.updateLabel(entry);
|
||||
sendTable.updateLabel(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PayNymAddressesDialog extends Dialog<Boolean> {
|
||||
public PayNymAddressesDialog(WalletForm walletForm) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynymaddresses.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymAddressesController controller = payNymLoader.getController();
|
||||
controller.initializeView(walletForm);
|
||||
|
||||
EventManager.get().register(controller);
|
||||
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().add(doneButtonType);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(controller);
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == doneButtonType ? Boolean.TRUE : Boolean.FALSE);
|
||||
|
||||
dialogPane.setPrefWidth(800);
|
||||
dialogPane.setPrefHeight(600);
|
||||
|
||||
setResizable(true);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,17 +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;
|
||||
public static final String INVALID_PAYMENT_CODE_LABEL = "Invalid Payment Code";
|
||||
|
||||
private String walletId;
|
||||
private boolean selectLinkedOnly;
|
||||
@@ -65,6 +64,9 @@ public class PayNymController {
|
||||
@FXML
|
||||
private CopyableTextField searchPayNyms;
|
||||
|
||||
@FXML
|
||||
private Button searchPayNymsScan;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator findPayNym;
|
||||
|
||||
@@ -83,6 +85,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 +94,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 +137,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) {
|
||||
@@ -144,7 +151,7 @@ public class PayNymController {
|
||||
findPayNym.setVisible(false);
|
||||
|
||||
followingList.setCellFactory(param -> {
|
||||
return new PayNymCell(this);
|
||||
return new PayNymCell(this, true);
|
||||
});
|
||||
|
||||
followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> {
|
||||
@@ -152,16 +159,17 @@ public class PayNymController {
|
||||
});
|
||||
|
||||
followersList.setCellFactory(param -> {
|
||||
return new PayNymCell(null);
|
||||
return new PayNymCell(this, false);
|
||||
});
|
||||
|
||||
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 +182,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 +196,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 +204,7 @@ public class PayNymController {
|
||||
refresh();
|
||||
} else {
|
||||
payNymName.setVisible(false);
|
||||
updateFollowing();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -202,7 +213,7 @@ public class PayNymController {
|
||||
private void resetFollowing() {
|
||||
if(followingList.getUserData() != null) {
|
||||
followingList.setUserData(null);
|
||||
followingList.setItems(FXCollections.observableList(walletPayNym.following()));
|
||||
updateFollowing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,35 +282,74 @@ public class PayNymController {
|
||||
public void followPayNym(PaymentCode contact) {
|
||||
PayNymService payNymService = AppServices.getPayNymService();
|
||||
Wallet masterWallet = getMasterWallet();
|
||||
retrievePayNymProgress.setVisible(true);
|
||||
payNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
|
||||
String signature = payNymService.getSignature(masterWallet, authToken);
|
||||
payNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
|
||||
refresh();
|
||||
}, error -> {
|
||||
retrievePayNymProgress.setVisible(false);
|
||||
log.error("Could not follow payment code", error);
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
followPayNym(contact);
|
||||
} else {
|
||||
followingList.refresh();
|
||||
followersList.refresh();
|
||||
}
|
||||
});
|
||||
}, error -> {
|
||||
retrievePayNymProgress.setVisible(false);
|
||||
log.error("Could not follow payment code", error);
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
followPayNym(contact);
|
||||
} else {
|
||||
followingList.refresh();
|
||||
followersList.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Boolean isFollowing(PayNym payNym) {
|
||||
if(followingList.getItems() != null) {
|
||||
return followingList.getItems().stream().anyMatch(following -> payNym.paymentCode().equals(following.paymentCode()));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isLinked(PayNym payNym) {
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
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> payNymMap = followingPayNyms.stream().collect(Collectors.toMap(PayNym::paymentCode, Function.identity(), (u, v) -> u, LinkedHashMap::new));
|
||||
followingList.setItems(FXCollections.observableList(getExistingWalletPayNyms(payNymMap)));
|
||||
}
|
||||
|
||||
private List<PayNym> getExistingWalletPayNyms(Map<PaymentCode, PayNym> payNymMap) {
|
||||
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();
|
||||
String walletNymName = PayNym.getNymName(childWallet);
|
||||
if(payNymMap.get(externalPaymentCode) == null || (walletNymName != null && !walletNymName.equals(payNymMap.get(externalPaymentCode).nymName()))) {
|
||||
payNymMap.put(externalPaymentCode, PayNym.fromWallet(childWallet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(payNymMap.values());
|
||||
}
|
||||
|
||||
private void addWalletIfNotificationTransactionPresent(List<PayNym> following) {
|
||||
Map<BlockTransaction, PayNym> unlinkedPayNyms = new HashMap<>();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
|
||||
@@ -307,7 +357,7 @@ public class PayNymController {
|
||||
if(!isLinked(payNym)) {
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
|
||||
if(!unlinkedNotification.isEmpty()) {
|
||||
if(!unlinkedNotification.isEmpty() && !INVALID_PAYMENT_CODE_LABEL.equals(unlinkedNotification.keySet().iterator().next().getLabel())) {
|
||||
unlinkedNotifications.putAll(unlinkedNotification);
|
||||
unlinkedPayNyms.put(unlinkedNotification.keySet().iterator().next(), payNym);
|
||||
}
|
||||
@@ -360,6 +410,9 @@ public class PayNymController {
|
||||
byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction());
|
||||
if(Arrays.equals(opReturnData, blindedPaymentCode)) {
|
||||
addedWallets.addAll(addChildWallets(payNym, externalPaymentCode));
|
||||
} else {
|
||||
blockTransaction.setLabel(INVALID_PAYMENT_CODE_LABEL);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(input0Node.getWallet(), new TransactionEntry(input0Node.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error adding linked contact from notification transaction", e);
|
||||
@@ -380,8 +433,8 @@ public class PayNymController {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||
List<ScriptType> scriptTypes = masterWallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
|
||||
for(ScriptType childScriptType : scriptTypes) {
|
||||
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType);
|
||||
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
|
||||
String label = payNym.nymName() + " " + childScriptType.getName();
|
||||
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType, label);
|
||||
if(!storage.isPersisted(addedWallet)) {
|
||||
try {
|
||||
storage.saveWallet(addedWallet);
|
||||
@@ -397,11 +450,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 +606,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 +623,10 @@ public class PayNymController {
|
||||
return payNymProperty;
|
||||
}
|
||||
|
||||
public BooleanProperty closeProperty() {
|
||||
return closeProperty;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml"));
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynym.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymController payNymController = payNymLoader.getController();
|
||||
payNymController.initializeView(walletId, selectLinkedOnly);
|
||||
@@ -30,7 +30,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm());
|
||||
|
||||
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);
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ public class CounterpartyController extends SorobanController {
|
||||
private PayNymAvatar mixPartnerAvatar;
|
||||
|
||||
@FXML
|
||||
private Label meetingFail;
|
||||
private Hyperlink meetingFail;
|
||||
|
||||
@FXML
|
||||
private VBox mixDetails;
|
||||
@@ -187,6 +187,15 @@ public class CounterpartyController extends SorobanController {
|
||||
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.visibleProperty().bind(mixingPartner.visibleProperty().not());
|
||||
meetingFail.setOnAction(event -> {
|
||||
step2Desc.setText("Ask your mix partner to initiate the Soroban communication.");
|
||||
mixingPartner.setVisible(true);
|
||||
startCounterpartyMeetingReceive();
|
||||
step2Timer.start(e -> {
|
||||
step2Desc.setText("Mix declined due to timeout.");
|
||||
meetingReceived.set(Boolean.FALSE);
|
||||
});
|
||||
});
|
||||
|
||||
mixDetails.managedProperty().bind(mixDetails.visibleProperty());
|
||||
mixDetails.setVisible(false);
|
||||
|
||||
@@ -1351,7 +1351,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
|
||||
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
|
||||
String label = outputIndexLabels.containsKey((int)output.getIndex()) ? outputIndexLabels.get((int)output.getIndex()) : headersForm.getName();
|
||||
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? (walletNode.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
|
||||
}
|
||||
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
|
||||
|
||||
@@ -9,9 +9,11 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddressesDialog;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
@@ -34,6 +36,9 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
@FXML
|
||||
private AddressTreeTable changeTable;
|
||||
|
||||
@FXML
|
||||
private Button showPayNymAddresses;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -43,6 +48,9 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
public void initializeView() {
|
||||
receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||
|
||||
showPayNymAddresses.managedProperty().bind(showPayNymAddresses.visibleProperty());
|
||||
showPayNymAddresses.setVisible(getWalletForm().getWallet().getChildWallets().stream().anyMatch(Wallet::isBip47));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -110,6 +118,13 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
showPayNymAddresses.setVisible(getWalletForm().getWallet().getChildWallets().stream().anyMatch(Wallet::isBip47));
|
||||
}
|
||||
}
|
||||
|
||||
public void exportReceiveAddresses(ActionEvent event) {
|
||||
exportAddresses(KeyPurpose.RECEIVE);
|
||||
}
|
||||
@@ -151,4 +166,9 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNymAddresses(ActionEvent event) {
|
||||
PayNymAddressesDialog payNymAddressesDialog = new PayNymAddressesDialog(getWalletForm());
|
||||
payNymAddressesDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
));
|
||||
}
|
||||
|
||||
@@ -328,7 +360,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
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))) {
|
||||
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
String label = (payNym == null ? externalPaymentCode.toAbbreviatedString() : payNym.nymName()) + " " + childScriptType.getName();
|
||||
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType, label);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -259,12 +259,12 @@ public class WalletForm {
|
||||
}
|
||||
|
||||
private List<WalletNode> getHistoryChangedNodes(Set<WalletNode> previousNodes, Set<WalletNode> currentNodes) {
|
||||
Map<WalletNode, WalletNode> previousNodeMap = new HashMap<>(previousNodes.size());
|
||||
previousNodes.forEach(walletNode -> previousNodeMap.put(walletNode, walletNode));
|
||||
Map<String, WalletNode> previousNodeMap = new HashMap<>(previousNodes.size());
|
||||
previousNodes.forEach(walletNode -> previousNodeMap.put(walletNode.getDerivationPath(), walletNode));
|
||||
|
||||
List<WalletNode> changedNodes = new ArrayList<>();
|
||||
for(WalletNode currentNode : currentNodes) {
|
||||
WalletNode previousNode = previousNodeMap.get(currentNode);
|
||||
WalletNode previousNode = previousNodeMap.get(currentNode.getDerivationPath());
|
||||
if(previousNode != null) {
|
||||
if(!currentNode.getTransactionOutputs().equals(previousNode.getTransactionOutputs())) {
|
||||
changedNodes.add(currentNode);
|
||||
@@ -286,7 +286,7 @@ public class WalletForm {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<WalletNode> allNodes = new LinkedHashSet<>();
|
||||
Set<WalletNode> allNodes = new LinkedHashSet<>(walletNodes);
|
||||
for(WalletNode walletNode : walletNodes) {
|
||||
for(Set<WalletNode> nodes : walletTransactionNodes) {
|
||||
if(nodes.contains(walletNode)) {
|
||||
@@ -377,6 +377,10 @@ public class WalletForm {
|
||||
this.lockedProperty.set(locked);
|
||||
}
|
||||
|
||||
public List<NodeEntry> getAccountEntries() {
|
||||
return accountEntries;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDataChanged(WalletDataChangedEvent event) {
|
||||
if(event.getWallet().equals(wallet)) {
|
||||
@@ -471,7 +475,7 @@ public class WalletForm {
|
||||
}
|
||||
|
||||
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? (changedNode.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, changedNode.getKeyPurpose()));
|
||||
}
|
||||
}
|
||||
@@ -496,7 +500,7 @@ public class WalletForm {
|
||||
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
|
||||
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
|
||||
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? (event.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry);
|
||||
}
|
||||
if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) {
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
-fx-text-fill: #e06c75;
|
||||
}
|
||||
|
||||
.root .failure.hyperlink:visited {
|
||||
-fx-text-fill: #e06c75;
|
||||
}
|
||||
|
||||
.root .titled-description-pane > .title {
|
||||
-fx-background-color: derive(-fx-base, 10%);
|
||||
-fx-padding: 0;
|
||||
|
||||
@@ -114,6 +114,10 @@
|
||||
-fx-underline: false;
|
||||
}
|
||||
|
||||
.failure.hyperlink:visited {
|
||||
-fx-text-fill: rgb(202, 18, 67);
|
||||
}
|
||||
|
||||
.hyperlink:hover:visited {
|
||||
-fx-underline: true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.control.AddressTreeTable?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
|
||||
<?import tornadofx.control.Fieldset?>
|
||||
<?import tornadofx.control.Form?>
|
||||
<?import tornadofx.control.Field?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<StackPane stylesheets="@../wallet/addresses.css, @../wallet/wallet.css, @paynym.css, @../general.css" styleClass="paynym-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.paynym.PayNymAddressesController">
|
||||
<VBox>
|
||||
<HBox styleClass="title-area">
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label text="PayNym Addresses" styleClass="title-label" />
|
||||
</HBox>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<ImageView AnchorPane.rightAnchor="0">
|
||||
<Image url="/image/paynym.png" requestedWidth="50" requestedHeight="50" smooth="false" />
|
||||
</ImageView>
|
||||
</HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets left="25" right="25" bottom="25" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="15">
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="" styleClass="header">
|
||||
<Field text="PayNym:">
|
||||
<ComboBox fx:id="payNymWalletForms" />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Receive Addresses"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="receiveTable" maxHeight="160" styleClass="bip47" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="2">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Send Addresses"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="sendTable" maxHeight="160" styleClass="bip47" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</StackPane>
|
||||
@@ -107,11 +107,11 @@
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="Mix partner:" styleClass="field-label" />
|
||||
<Label fx:id="mixingPartner" text="Waiting for mix partner..." styleClass="field-control" />
|
||||
<Label fx:id="meetingFail" text="Failed to find mix partner." styleClass="failure" graphicTextGap="5">
|
||||
<Hyperlink fx:id="meetingFail" text="Failed to find mix partner. Try again?" styleClass="failure" graphicTextGap="5">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
||||
</graphic>
|
||||
</Label>
|
||||
</Hyperlink>
|
||||
</HBox>
|
||||
<VBox fx:id="mixDetails" spacing="10">
|
||||
<HBox styleClass="field-box">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
.witness-count { -fx-fill: color-0 }
|
||||
.witness-length { -fx-fill: color-6 }
|
||||
.witness-data { -fx-fill: color-6 }
|
||||
.witness-data-signature { -fx-fill: color-3 }
|
||||
|
||||
.locktime { -fx-fill: color-7 }
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
.witness-count { -fx-fill: color-0 }
|
||||
.witness-length { -fx-fill: color-7 }
|
||||
.witness-data { -fx-fill: color-6 }
|
||||
.witness-data-signature { -fx-fill: color-3 }
|
||||
|
||||
.locktime { -fx-fill: color-grey }
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
.witness-count { -fx-fill: color-0 }
|
||||
.witness-length { -fx-fill: color-7 }
|
||||
.witness-data { -fx-fill: color-6 }
|
||||
.witness-data-signature { -fx-fill: color-3 }
|
||||
|
||||
.locktime { -fx-fill: color-grey }
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
.witness-count { -fx-fill: color-grey }
|
||||
.witness-length { -fx-fill: color-grey }
|
||||
.witness-data { -fx-fill: color-grey }
|
||||
.witness-data-signature { -fx-fill: color-grey }
|
||||
|
||||
.locktime { -fx-fill: color-grey }
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
.witness-count { -fx-fill: color-grey }
|
||||
.witness-length { -fx-fill: color-grey }
|
||||
.witness-data { -fx-fill: color-grey }
|
||||
.witness-data-signature { -fx-fill: color-grey }
|
||||
|
||||
.locktime { -fx-fill: color-grey }
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@
|
||||
<Tooltip text="Export receive addresses as CSV" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Region HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="showPayNymAddresses" onAction="#showPayNymAddresses" text="PayNyms">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ROBOT" fontSize="12" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Show PayNym addresses" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user