add paynym addresses dialog

This commit is contained in:
Craig Raw 2022-03-04 14:44:22 +02:00
parent 58f20dab60
commit a10bdef484
11 changed files with 262 additions and 26 deletions

View file

@ -123,15 +123,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
HBox actionBox = new HBox(); HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions"); 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(""); Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph()); signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> { signMessageButton.setOnAction(event -> {
@ -151,7 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
} else if(entry instanceof HashIndexEntry) { } else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry; HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription()); setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry)); setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip(); Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250)); tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(hashIndexEntry.getHashIndex().toString()); tooltip.setText(hashIndexEntry.getHashIndex().toString());
@ -175,7 +178,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
actionBox.getChildren().add(spendUtxoButton); actionBox.getChildren().add(spendUtxoButton);
} }
setGraphic(actionBox); setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
} }
} }
} }
@ -287,9 +290,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
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), 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 && 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) { private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@ -502,16 +507,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
public static class AddressContextMenu extends ContextMenu { public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) { public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem receiveToAddress = new MenuItem("Receive To"); if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
receiveToAddress.setGraphic(getReceiveGlyph()); MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setOnAction(event -> { receiveToAddress.setGraphic(getReceiveGlyph());
hide(); receiveToAddress.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry)); hide();
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry))); EventManager.get().post(new ReceiveActionEvent(nodeEntry));
}); Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
getItems().add(receiveToAddress); });
getItems().add(receiveToAddress);
}
if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) { if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message"); MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph()); signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> { signVerifyMessage.setOnAction(AE -> {

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -18,7 +18,7 @@ public class PayNymDialog extends Dialog<PayNym> {
AppServices.onEscapePressed(dialogPane.getScene(), this::close); AppServices.onEscapePressed(dialogPane.getScene(), this::close);
try { 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()); dialogPane.setContent(payNymLoader.load());
PayNymController payNymController = payNymLoader.getController(); PayNymController payNymController = payNymLoader.getController();
payNymController.initializeView(walletId, selectLinkedOnly); payNymController.initializeView(walletId, selectLinkedOnly);
@ -30,7 +30,7 @@ public class PayNymDialog extends Dialog<PayNym> {
AppServices.moveToActiveWindowScreen(this); AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); 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 selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);

View file

@ -1351,7 +1351,7 @@ public class HeadersController extends TransactionFormController implements Init
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) { for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change 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(); 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())); 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 if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out

View file

@ -9,9 +9,11 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressTreeTable; import com.sparrowwallet.sparrow.control.AddressTreeTable;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.paynym.PayNymAddressesDialog;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -34,6 +36,9 @@ public class AddressesController extends WalletFormController implements Initial
@FXML @FXML
private AddressTreeTable changeTable; private AddressTreeTable changeTable;
@FXML
private Button showPayNymAddresses;
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -43,6 +48,9 @@ public class AddressesController extends WalletFormController implements Initial
public void initializeView() { public void initializeView() {
receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE)); receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE)); changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
showPayNymAddresses.managedProperty().bind(showPayNymAddresses.visibleProperty());
showPayNymAddresses.setVisible(getWalletForm().getWallet().getChildWallets().stream().anyMatch(Wallet::isBip47));
} }
@Subscribe @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) { public void exportReceiveAddresses(ActionEvent event) {
exportAddresses(KeyPurpose.RECEIVE); 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();
}
} }

View file

@ -377,6 +377,10 @@ public class WalletForm {
this.lockedProperty.set(locked); this.lockedProperty.set(locked);
} }
public List<NodeEntry> getAccountEntries() {
return accountEntries;
}
@Subscribe @Subscribe
public void walletDataChanged(WalletDataChangedEvent event) { public void walletDataChanged(WalletDataChangedEvent event) {
if(event.getWallet().equals(wallet)) { if(event.getWallet().equals(wallet)) {
@ -471,7 +475,7 @@ public class WalletForm {
} }
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { 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())); changedLabelEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, changedNode.getKeyPurpose()));
} }
} }
@ -496,7 +500,7 @@ public class WalletForm {
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) { for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) { if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { 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); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry);
} }
if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) { if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) {

View file

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

View file

@ -33,6 +33,15 @@
<Tooltip text="Export receive addresses as CSV" /> <Tooltip text="Export receive addresses as CSV" />
</tooltip> </tooltip>
</Button> </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> </HBox>
</top> </top>
<center> <center>