add wallet utxo pane

This commit is contained in:
Craig Raw 2020-06-26 12:07:19 +02:00
parent 183d0ded2f
commit 7444a87d89
19 changed files with 420 additions and 29 deletions

View file

@ -0,0 +1,41 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
public class AddressCell extends TreeTableCell<Entry, Entry> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
EntryCell.applyRowStyles(this, entry);
getStyleClass().add("address-cell");
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
Tooltip tooltip = new Tooltip();
tooltip.setText(utxoEntry.getNode().getDerivationPath());
setTooltip(tooltip);
}
setGraphic(null);
}
}
}

View file

@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
@ -51,6 +52,8 @@ class AmountCell extends TreeTableCell<Entry, Number> {
} else {
setGraphic(null);
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);

View file

@ -0,0 +1,68 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateCell extends TreeTableCell<Entry, Entry> {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public DateCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("date-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
EntryCell.applyRowStyles(this, entry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
Tooltip tooltip = new Tooltip();
tooltip.setText(Integer.toString(utxoEntry.getHashIndex().getHeight()));
setTooltip(tooltip);
}
setGraphic(null);
}
}
private static class DateContextMenu extends ContextMenu {
public DateContextMenu(String date, BlockTransactionHashIndex reference) {
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(date);
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyHeight.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Integer.toString(reference.getHeight()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyDate, copyHeight);
}
}
}

View file

@ -8,10 +8,7 @@ import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
@ -118,7 +115,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
});
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyTxid.setOnAction(AE -> {
copyHeight.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Integer.toString(blockTransaction.getHeight()));
@ -129,7 +126,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
}
private static class AddressContextMenu extends ContextMenu {
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
@ -195,6 +192,8 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry) {
cell.getStyleClass().add("utxo-row");
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;

View file

@ -70,7 +70,7 @@ public class TransactionsTreeTable extends TreeTableView<Entry> {
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Recalculate from scratch and update according - any changes may affect the balance of other transactions
//Recalculate from scratch and update accordingly - any changes may affect the balance of other transactions
WalletTransactionsEntry rootEntry = (WalletTransactionsEntry)getRoot().getValue();
rootEntry.updateTransactions();
sort();

View file

@ -0,0 +1,95 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.List;
public class UtxosTreeTable extends TreeTableView<Entry> {
public void initialize(WalletUtxosEntry rootEntry) {
getStyleClass().add("utxos-treetable");
updateAll(rootEntry);
setShowRoot(false);
TreeTableColumn<Entry, Entry> dateCol = new TreeTableColumn<>("Date");
dateCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
dateCol.setCellFactory(p -> new DateCell());
dateCol.setSortable(true);
getColumns().add(dateCol);
TreeTableColumn<Entry, Entry> outputCol = new TreeTableColumn<>("Output");
outputCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
outputCol.setCellFactory(p -> new EntryCell());
outputCol.setSortable(true);
outputCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getDescription().compareTo(entry2.getDescription());
});
getColumns().add(outputCol);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
});
getColumns().add(addressCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new LabelCell());
labelCol.setSortable(true);
getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new AmountCell());
amountCol.setSortable(true);
getColumns().add(amountCol);
setTreeColumn(amountCol);
setPlaceholder(new Label("No unspent outputs"));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
getSortOrder().add(dateCol);
}
public void updateAll(WalletUtxosEntry rootEntry) {
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
if(getColumns().size() > 0 && getSortOrder().isEmpty()) {
TreeTableColumn<Entry, ?> dateCol = getColumns().get(0);
getSortOrder().add(dateCol);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Recalculate from scratch and update accordingly
WalletUtxosEntry rootEntry = (WalletUtxosEntry)getRoot().getValue();
rootEntry.updateUtxos();
sort();
}
}

View file

@ -17,6 +17,7 @@ public class FontAwesome5 extends GlyphFont {
public static enum Glyph implements INamedCharacter {
CHECK_CIRCLE('\uf058'),
CIRCLE('\uf111'),
COINS('\uf51e'),
EXCLAMATION_CIRCLE('\uf06a'),
ELLIPSIS_H('\uf141'),
EYE('\uf06e'),

View file

@ -1,5 +1,5 @@
package com.sparrowwallet.sparrow.wallet;
public enum Function {
TRANSACTIONS, SEND, RECEIVE, ADDRESSES, POLICIES, SETTINGS;
TRANSACTIONS, SEND, RECEIVE, ADDRESSES, UTXOS, SETTINGS;
}

View file

@ -29,14 +29,6 @@ public class TransactionHashIndexEntry extends HashIndexEntry {
}
}
public BlockTransaction getSpentByTransaction() {
if(getHashIndex().getSpentBy() != null) {
return getWallet().getTransactions().get(getHashIndex().getSpentBy().getHash());
}
return null;
}
@Override
public boolean isSpent() {
return false;

View file

@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class UtxoEntry extends HashIndexEntry {
private final WalletNode node;
public UtxoEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, WalletNode node) {
super(wallet, hashIndex, type, node.getKeyPurpose());
this.node = node;
}
@Override
public ObservableList<Entry> getChildren() {
return FXCollections.emptyObservableList();
}
@Override
public String getDescription() {
return getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex();
}
@Override
public boolean isSpent() {
return false;
}
public Address getAddress() {
return getWallet().getAddress(node);
}
public WalletNode getNode() {
return node;
}
public String getOutputDescriptor() {
return getWallet().getOutputDescriptor(node);
}
}

View file

@ -0,0 +1,42 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UtxosTreeTable;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class UtxosController extends WalletFormController implements Initializable {
@FXML
private UtxosTreeTable utxosTable;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
}
@Override
public void initializeView() {
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
}
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
utxosTable.updateAll(getWalletForm().getWalletUtxosEntry());
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
utxosTable.updateHistory(event.getHistoryChangedNodes());
}
}
}

View file

@ -20,6 +20,7 @@ public class WalletForm {
protected Wallet wallet;
private WalletTransactionsEntry walletTransactionsEntry;
private WalletUtxosEntry walletUtxosEntry;
private final List<NodeEntry> accountEntries = new ArrayList<>();
public WalletForm(Storage storage, Wallet currentWallet) {
@ -136,6 +137,14 @@ public class WalletForm {
return walletTransactionsEntry;
}
public WalletUtxosEntry getWalletUtxosEntry() {
if(walletUtxosEntry == null) {
walletUtxosEntry = new WalletUtxosEntry(wallet);
}
return walletUtxosEntry;
}
@Subscribe
public void walletLabelChanged(WalletEntryLabelChangedEvent event) {
if(event.getWallet().equals(wallet)) {
@ -164,6 +173,7 @@ public class WalletForm {
if(event.getWalletFile().equals(storage.getWalletFile())) {
wallet = event.getWallet();
walletTransactionsEntry = null;
walletUtxosEntry = null;
accountEntries.clear();
EventManager.get().post(new WalletNodesChangedEvent(wallet));
refreshHistory(AppController.getCurrentBlockHeight());

View file

@ -0,0 +1,57 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import java.util.*;
import java.util.stream.Collectors;
public class WalletUtxosEntry extends Entry {
private final Wallet wallet;
public WalletUtxosEntry(Wallet wallet) {
super(wallet.getName(), getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
@Override
public Long getValue() {
return 0L;
}
public void updateUtxos() {
List<Entry> current = getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
List<Entry> previous = new ArrayList<>(getChildren());
List<Entry> entriesAdded = new ArrayList<>(current);
entriesAdded.removeAll(previous);
getChildren().addAll(entriesAdded);
List<Entry> entriesRemoved = new ArrayList<>(previous);
entriesRemoved.removeAll(current);
getChildren().removeAll(entriesRemoved);
}
private static Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(Wallet wallet) {
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
getWalletUtxos(wallet, walletUtxos, wallet.getNode(KeyPurpose.RECEIVE));
getWalletUtxos(wallet, walletUtxos, wallet.getNode(KeyPurpose.CHANGE));
return walletUtxos;
}
private static void getWalletUtxos(Wallet wallet, Map<BlockTransactionHashIndex, WalletNode> walletUtxos, WalletNode purposeNode) {
for(WalletNode addressNode : purposeNode.getChildren()) {
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs()) {
walletUtxos.put(utxo, addressNode);
}
}
}
}

View file

@ -1 +1,5 @@
.addresses-treetable-label {
-fx-font-weight: bold;
-fx-font-size: 1.2em;
-fx-padding: 10 0 10 0;
}

View file

@ -21,7 +21,7 @@
</rowConstraints>
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
<top>
<Label styleClass="address-treetable-label" text="Receiving Addresses:"/>
<Label styleClass="addresses-treetable-label" text="Receiving Addresses:"/>
</top>
<center>
<AddressTreeTable fx:id="receiveTable" />
@ -29,7 +29,7 @@
</BorderPane>
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="1">
<top>
<Label styleClass="address-treetable-label" text="Change Addresses:"/>
<Label styleClass="addresses-treetable-label" text="Change Addresses:"/>
</top>
<center>
<AddressTreeTable fx:id="changeTable" />

View file

@ -0,0 +1,5 @@
.utxos-treetable-label {
-fx-font-weight: bold;
-fx-font-size: 1.2em;
-fx-padding: 10 0 10 0;
}

View file

@ -0,0 +1,36 @@
<?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 javafx.scene.chart.BarChart?>
<?import com.sparrowwallet.sparrow.control.UtxosTreeTable?>
<GridPane hgap="10.0" vgap="10.0" stylesheets="@utxos.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.UtxosController">
<padding>
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints percentHeight="50" />
<RowConstraints percentHeight="50" />
</rowConstraints>
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
<top>
<Label styleClass="utxos-treetable-label" text="Unspent Transaction Outputs:"/>
</top>
<center>
<UtxosTreeTable fx:id="utxosTable" />
</center>
</BorderPane>
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="1">
<center>
<!-- <BarChart fx:id="changeTable" /> -->
</center>
</BorderPane>
</GridPane>

View file

@ -29,13 +29,7 @@
-fx-background-color: -fx-background;
}
.address-treetable-label {
-fx-font-weight: bold;
-fx-font-size: 1.2em;
-fx-padding: 10 0 10 0;
}
.address-cell {
.address-cell, .utxo-row.entry-cell {
-fx-font-family: Courier;
}

View file

@ -46,12 +46,12 @@
<Function fx:constant="ADDRESSES"/>
</userData>
</ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Policies" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">
<ToggleButton VBox.vgrow="ALWAYS" text="UTXOs" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">
<graphic>
<Glyph fontFamily="FontAwesome" icon="FILE_TEXT" fontSize="20" />
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="COINS" fontSize="20" />
</graphic>
<userData>
<Function fx:constant="POLICIES"/>
<Function fx:constant="UTXOS"/>
</userData>
</ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Settings" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">