diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index ab2b7f25..520e25ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -34,6 +34,7 @@ import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionData; import com.sparrowwallet.sparrow.transaction.TransactionView; +import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletForm; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; @@ -155,6 +156,9 @@ public class AppController implements Initializable { @FXML private MenuItem lockWallet; + @FXML + private MenuItem searchWallet; + @FXML private MenuItem refreshWallet; @@ -334,6 +338,7 @@ public class AppController implements Initializable { showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not()); exportWallet.setDisable(true); lockWallet.setDisable(true); + searchWallet.disableProperty().bind(exportWallet.disableProperty()); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); sendToMany.disableProperty().bind(exportWallet.disableProperty()); sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())); @@ -1353,6 +1358,19 @@ public class AppController implements Initializable { } } + public void searchWallet(ActionEvent event) { + WalletForm selectedWalletForm = getSelectedWalletForm(); + if(selectedWalletForm != null) { + SearchWalletDialog searchWalletDialog = new SearchWalletDialog(selectedWalletForm); + Optional optEntry = searchWalletDialog.showAndWait(); + if(optEntry.isPresent()) { + Entry entry = optEntry.get(); + EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet())); + Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry))); + } + } + } + public void refreshWallet(ActionEvent event) { WalletForm selectedWalletForm = getSelectedWalletForm(); if(selectedWalletForm != null) { @@ -2430,12 +2448,7 @@ public class AppController implements Initializable { } @Subscribe - public void sendAction(SendActionEvent event) { - selectTab(event.getWallet()); - } - - @Subscribe - public void recieveAction(ReceiveActionEvent event) { + public void functionAction(FunctionActionEvent event) { selectTab(event.getWallet()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 3281f75d..aac55763 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -80,6 +80,7 @@ public class EntryCell extends TreeTableCell { } HBox actionBox = new HBox(); + actionBox.getStyleClass().add("cell-actions"); Button viewTransactionButton = new Button(""); viewTransactionButton.setGraphic(getViewTransactionGlyph()); viewTransactionButton.setOnAction(event -> { @@ -118,6 +119,7 @@ public class EntryCell extends TreeTableCell { getStyleClass().add("address-cell"); HBox actionBox = new HBox(); + actionBox.getStyleClass().add("cell-actions"); Button receiveButton = new Button(""); receiveButton.setGraphic(getReceiveGlyph()); receiveButton.setOnAction(event -> { @@ -152,6 +154,7 @@ public class EntryCell extends TreeTableCell { setTooltip(tooltip); HBox actionBox = new HBox(); + actionBox.getStyleClass().add("cell-actions"); Button viewTransactionButton = new Button(""); viewTransactionButton.setGraphic(getViewTransactionGlyph()); viewTransactionButton.setOnAction(event -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java index afc31000..1b7506d5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java @@ -70,9 +70,6 @@ public class PrivateKeySweepDialog extends Dialog { dialogPane.setGraphic(imageView); } - VBox vBox = new VBox(); - vBox.setSpacing(20); - Form form = new Form(); Fieldset fieldset = new Fieldset(); fieldset.setText(""); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java new file mode 100644 index 00000000..061a71a0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java @@ -0,0 +1,218 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.wallet.*; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ListChangeListener; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import org.controlsfx.control.textfield.TextFields; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tornadofx.control.Field; +import tornadofx.control.Fieldset; +import tornadofx.control.Form; + +import java.util.ArrayList; +import java.util.List; + +public class SearchWalletDialog extends Dialog { + private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class); + + private final WalletForm walletForm; + private final TextField search; + private final CoinTreeTable results; + + public SearchWalletDialog(WalletForm walletForm) { + this.walletForm = walletForm; + + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm()); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + dialogPane.setHeaderText("Search Wallet"); + + Image image = new Image("image/sparrow-small.png", 50, 50, false, false); + if(!image.isError()) { + ImageView imageView = new ImageView(); + imageView.setSmooth(false); + imageView.setImage(image); + dialogPane.setGraphic(imageView); + } + + VBox vBox = new VBox(); + vBox.setSpacing(20); + + Form form = new Form(); + Fieldset fieldset = new Fieldset(); + fieldset.setText(""); + fieldset.setSpacing(10); + + Field searchField = new Field(); + searchField.setText("Search:"); + search = TextFields.createClearableTextField(); + search.setPromptText("Label, address, value or transaction ID"); + searchField.getInputs().add(search); + + fieldset.getChildren().addAll(searchField); + form.getChildren().add(fieldset); + + results = new CoinTreeTable(); + results.setShowRoot(false); + results.setPrefWidth(850); + results.setBitcoinUnit(walletForm.getWallet()); + results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); + results.setPlaceholder(new Label("No results")); + + TreeTableColumn typeColumn = new TreeTableColumn<>("Type"); + typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType()); + }); + results.getColumns().add(typeColumn); + + TreeTableColumn entryCol = new TreeTableColumn<>("Date / Address / Output"); + entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); + }); + entryCol.setCellFactory(p -> new SearchEntryCell()); + String address = walletForm.getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString(); + if(address != null) { + entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0)); + } + results.getColumns().add(entryCol); + + TreeTableColumn labelCol = new TreeTableColumn<>("Label"); + labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return param.getValue().getValue().labelProperty(); + }); + labelCol.setCellFactory(p -> new SearchLabelCell()); + results.getColumns().add(labelCol); + + TreeTableColumn amountCol = new TreeTableColumn<>("Value"); + amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); + }); + amountCol.setCellFactory(p -> new CoinCell()); + results.getColumns().add(amountCol); + + vBox.getChildren().addAll(form, results); + dialogPane.setContent(vBox); + + ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY); + ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + + dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType); + + Button showButton = (Button) dialogPane.lookupButton(showButtonType); + showButton.setDefaultButton(true); + showButton.setDisable(true); + + setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null); + + results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener) c -> { + showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty()); + }); + + search.textProperty().addListener((observable, oldValue, newValue) -> { + searchWallet(newValue.toLowerCase()); + }); + + setResizable(true); + } + + private void searchWallet(String searchText) { + List matchingEntries = new ArrayList<>(); + + if(!searchText.isEmpty()) { + Long searchValue = null; + + try { + searchValue = Math.abs(Long.parseLong(searchText)); + } catch(NumberFormatException e) { + //ignore + } + + WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry(); + for(Entry entry : walletTransactionsEntry.getChildren()) { + if(entry instanceof TransactionEntry transactionEntry) { + if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) || + (transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) || + (transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) { + matchingEntries.add(entry); + } + } + } + + for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { + NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose); + for(Entry entry : purposeEntry.getChildren()) { + if(entry instanceof NodeEntry nodeEntry) { + if(nodeEntry.getAddress().toString().contains(searchText) || + (nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) || + (nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) { + matchingEntries.add(entry); + } + } + } + } + + WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry(); + for(Entry entry : walletUtxosEntry.getChildren()) { + if(entry instanceof HashIndexEntry hashIndexEntry) { + if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) || + (hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) || + (hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) { + matchingEntries.add(entry); + } + } + } + } + + SearchWalletEntry rootEntry = new SearchWalletEntry(walletForm.getWallet(), matchingEntries); + RecursiveTreeItem rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren); + results.setRoot(rootItem); + } + + private static class SearchWalletEntry extends Entry { + public SearchWalletEntry(Wallet wallet, List entries) { + super(wallet, wallet.getName(), entries); + } + + @Override + public Long getValue() { + return 0L; + } + + @Override + public String getEntryType() { + return "Search Wallet Results"; + } + + @Override + public Function getWalletFunction() { + return null; + } + } + + private static class SearchEntryCell extends EntryCell { + @Override + protected void updateItem(Entry entry, boolean empty) { + super.updateItem(entry, empty); + setContextMenu(null); + } + } + + private static class SearchLabelCell extends LabelCell { + @Override + public void updateItem(String label, boolean empty) { + super.updateItem(label, empty); + setContextMenu(null); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FunctionActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FunctionActionEvent.java new file mode 100644 index 00000000..05dc9ce3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FunctionActionEvent.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.Function; + +public class FunctionActionEvent { + private final Function function; + private final Wallet wallet; + + public FunctionActionEvent(Function function, Wallet wallet) { + this.function = function; + this.wallet = wallet; + } + + public Function getFunction() { + return function; + } + + public Wallet getWallet() { + return wallet; + } + + public boolean selectFunction() { + return true; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java index e10fdce9..0aaadcc2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java @@ -1,20 +1,15 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.Function; import com.sparrowwallet.sparrow.wallet.NodeEntry; -public class ReceiveActionEvent { - private final Wallet wallet; - +public class ReceiveActionEvent extends FunctionActionEvent { public ReceiveActionEvent(NodeEntry receiveEntry) { - this.wallet = receiveEntry.getWallet(); + super(Function.RECEIVE, receiveEntry.getWallet()); } public ReceiveActionEvent(Wallet wallet) { - this.wallet = wallet; - } - - public Wallet getWallet() { - return wallet; + super(Function.RECEIVE, wallet); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SelectEntryEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SelectEntryEvent.java new file mode 100644 index 00000000..e8c5b85c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/SelectEntryEvent.java @@ -0,0 +1,20 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.Entry; + +public class SelectEntryEvent { + private final Entry entry; + + public SelectEntryEvent(Entry entry) { + this.entry = entry; + } + + public Entry getEntry() { + return entry; + } + + public Wallet getWallet() { + return entry.getWallet(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java index 486dc747..971343bc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java @@ -2,23 +2,24 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.Function; import java.util.List; -public class SendActionEvent { - private final Wallet wallet; +public class SendActionEvent extends FunctionActionEvent { private final List utxos; public SendActionEvent(Wallet wallet, List utxos) { - this.wallet = wallet; + super(Function.SEND, wallet); this.utxos = utxos; } - public Wallet getWallet() { - return wallet; - } - public List getUtxos() { return utxos; } + + @Override + public boolean selectFunction() { + return !getUtxos().isEmpty(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java index a81a5f49..12f38412 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java @@ -100,6 +100,16 @@ public class AddressesController extends WalletFormController implements Initial } } + @Subscribe + public void selectEntry(SelectEntryEvent event) { + if(event.getWallet().equals(getWalletForm().getWallet()) && event.getEntry().getWalletFunction() == Function.ADDRESSES) { + List addressTreeTables = List.of(receiveTable, changeTable); + for(AddressTreeTable addressTreeTable : addressTreeTables) { + selectEntry(addressTreeTable, event.getEntry()); + } + } + } + public void exportReceiveAddresses(ActionEvent event) { exportAddresses(KeyPurpose.RECEIVE); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java index 32d88388..ac1943d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java @@ -42,6 +42,10 @@ public abstract class Entry { public abstract Long getValue(); + public abstract String getEntryType(); + + public abstract Function getWalletFunction(); + public void updateLabel(Entry entry) { if(this.equals(entry)) { labelProperty.set(entry.getLabel()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java index 71aa74d1..c6458969 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java @@ -69,6 +69,16 @@ public class HashIndexEntry extends Entry implements Comparable return hashIndex.getValue(); } + @Override + public String getEntryType() { + return "Hash Index"; + } + + @Override + public Function getWalletFunction() { + return Function.ADDRESSES; + } + public enum Type { INPUT, OUTPUT } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java index 5cf07534..a6039050 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java @@ -63,6 +63,16 @@ public class NodeEntry extends Entry implements Comparable { return node.getUnspentValue(); } + @Override + public String getEntryType() { + return "Address"; + } + + @Override + public Function getWalletFunction() { + return Function.ADDRESSES; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java index f8de49b8..68e60a13 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java @@ -74,6 +74,16 @@ public class TransactionEntry extends Entry implements Comparable treeTableView, Entry entry) { + for(TreeItem treeEntry : treeTableView.getRoot().getChildren()) { + if(treeEntry.getValue().equals(entry)) { + treeTableView.getSelectionModel().select(treeEntry); + Platform.runLater(() -> { + treeTableView.requestFocus(); + treeTableView.scrollTo(treeTableView.getSelectionModel().getSelectedIndex()); + }); + break; + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index 51522f85..c3e24bd5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -30,6 +30,16 @@ public class WalletTransactionsEntry extends Entry { return getBalance(); } + @Override + public String getEntryType() { + return "Wallet Transactions"; + } + + @Override + public Function getWalletFunction() { + return Function.TRANSACTIONS; + } + private void calculateBalances(boolean resort) { long balance = 0L; long mempoolBalance = 0L; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java index a7339922..0ccfa5c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java @@ -20,6 +20,16 @@ public class WalletUtxosEntry extends Entry { return 0L; } + @Override + public String getEntryType() { + return "Wallet UTXOs"; + } + + @Override + public Function getWalletFunction() { + return Function.UTXOS; + } + protected void calculateDuplicates() { Map addressMap = new HashMap<>(); diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 0dfdaaa4..90c62039 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -104,6 +104,7 @@ + diff --git a/src/main/resources/com/sparrowwallet/sparrow/search.css b/src/main/resources/com/sparrowwallet/sparrow/search.css new file mode 100644 index 00000000..c6af0f5f --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/search.css @@ -0,0 +1,3 @@ +.cell-actions { + visibility: hidden; +} \ No newline at end of file