wallet transactions pane and wallet change events

This commit is contained in:
Craig Raw 2020-06-21 17:15:46 +02:00
parent d33ccfb672
commit 21f642bb5c
25 changed files with 924 additions and 332 deletions

2
drongo

@ -1 +1 @@
Subproject commit 81378b28b25d02dca8cdfc21a6b4fae0421d82b1 Subproject commit 18036268e52af8ed8cd5a289f478b302fb415255

View file

@ -1,35 +1,18 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent; import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent; 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.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.NodeEntry; import com.sparrowwallet.sparrow.wallet.NodeEntry;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.text.Font; import javafx.scene.text.Font;
import javafx.util.converter.DefaultStringConverter;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
public class AddressTreeTable extends TreeTableView<Entry> { public class AddressTreeTable extends TreeTableView<Entry> {
@ -37,17 +20,14 @@ public class AddressTreeTable extends TreeTableView<Entry> {
getStyleClass().add("address-treetable"); getStyleClass().add("address-treetable");
String address = rootEntry.getAddress().toString(); String address = rootEntry.getAddress().toString();
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren); updateAll(rootEntry);
setRoot(rootItem);
rootItem.setExpanded(true);
setShowRoot(false); setShowRoot(false);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address / Outpoints"); TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address / Outpoints");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> { addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
}); });
addressCol.setCellFactory(p -> new DataCell()); addressCol.setCellFactory(p -> new EntryCell());
addressCol.setSortable(false); addressCol.setSortable(false);
getColumns().add(addressCol); getColumns().add(addressCol);
@ -96,6 +76,12 @@ public class AddressTreeTable extends TreeTableView<Entry> {
}); });
} }
public void updateAll(NodeEntry rootEntry) {
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
}
public void updateHistory(List<WalletNode> updatedNodes) { public void updateHistory(List<WalletNode> updatedNodes) {
NodeEntry rootEntry = (NodeEntry)getRoot().getValue(); NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
@ -108,244 +94,4 @@ public class AddressTreeTable extends TreeTableView<Entry> {
} }
} }
} }
private static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("spent");
if(entry != null) {
if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
}
}
}
private static class DataCell extends TreeTableCell<Entry, Entry> {
public DataCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("data-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
applyRowStyles(this, entry);
getStyleClass().remove("address-cell");
if(empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
setTooltip(tooltip);
getStyleClass().add("address-cell");
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
receiveButton.setGraphic(receiveGlyph);
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
setGraphic(viewTransactionButton);
}
}
}
}
private static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(address.toString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(outputDescriptor);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
}
}
private static class HashIndexEntryContextMenu extends ContextMenu {
public HashIndexEntryContextMenu(HashIndexEntry hashIndexEntry) {
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
}
}
private static class LabelCell extends TextFieldTreeTableCell<Entry, String> {
public LabelCell() {
super(new DefaultStringConverter());
getStyleClass().add("label-cell");
}
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
if(empty) {
setText(null);
setGraphic(null);
} else {
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
setText(label);
setContextMenu(new LabelContextMenu(label));
}
}
@Override
public void commitEdit(String label) {
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !label.equals(getItem())) {
TreeTableView<Entry> treeTable = getTreeTableView();
if(treeTable != null) {
TreeTableColumn<Entry, String> column = getTableColumn();
TreeTableColumn.CellEditEvent<Entry, String> event = new TreeTableColumn.CellEditEvent<>(
treeTable, new TreeTablePosition<>(treeTable, getIndex(), column),
TreeTableColumn.editCommitEvent(), label
);
Event.fireEvent(column, event);
}
}
super.commitEdit(label);
}
@Override
public void startEdit() {
super.startEdit();
try {
Field f = getClass().getSuperclass().getDeclaredField("textField");
f.setAccessible(true);
TextField textField = (TextField)f.get(this);
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused) {
commitEdit(getConverter().fromString(textField.getText()));
setText(getConverter().fromString(textField.getText()));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyLabel);
}
}
private static class AmountCell extends TreeTableCell<Entry, Long> {
public AmountCell() {
super();
getStyleClass().add("amount-cell");
setContentDisplay(ContentDisplay.RIGHT);
}
@Override
protected void updateItem(Long amount, boolean empty) {
super.updateItem(amount, empty);
if(empty || amount == null) {
setText(null);
setGraphic(null);
} else {
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
String satsValue = String.format(Locale.ENGLISH, "%,d", amount);
String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
}
} else {
setGraphic(null);
}
Tooltip tooltip = new Tooltip();
tooltip.setText(btcValue);
setText(satsValue);
setTooltip(tooltip);
}
}
}
} }

View file

@ -0,0 +1,53 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.Region;
import java.util.Locale;
class AmountCell extends TreeTableCell<Entry, Long> {
public AmountCell() {
super();
getStyleClass().add("amount-cell");
setContentDisplay(ContentDisplay.RIGHT);
}
@Override
protected void updateItem(Long amount, boolean empty) {
super.updateItem(amount, empty);
if(empty || amount == null) {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
String satsValue = String.format(Locale.ENGLISH, "%,d", amount);
String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
}
} else {
setGraphic(null);
}
Tooltip tooltip = new Tooltip();
tooltip.setText(btcValue);
setText(satsValue);
setTooltip(tooltip);
}
}
}

View file

@ -0,0 +1,199 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.EventManager;
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.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
class EntryCell extends TreeTableCell<Entry, Entry> {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public EntryCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("entry-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
applyRowStyles(this, entry);
if(empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip);
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
});
setGraphic(viewTransactionButton);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
setTooltip(tooltip);
getStyleClass().add("address-cell");
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
receiveButton.setGraphic(receiveGlyph);
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
setGraphic(viewTransactionButton);
}
}
}
private static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(date);
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Integer.toString(blockTransaction.getHeight()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyDate, copyTxid, copyHeight);
}
}
private static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(address.toString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(outputDescriptor);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
}
}
private static class HashIndexEntryContextMenu extends ContextMenu {
public HashIndexEntryContextMenu(HashIndexEntry hashIndexEntry) {
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
}
}
public static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("spent");
if(entry != null) {
if(entry instanceof TransactionEntry) {
cell.getStyleClass().add("transaction-row");
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
}
}
}
}

View file

@ -0,0 +1,87 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.event.Event;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.util.converter.DefaultStringConverter;
import java.lang.reflect.Field;
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
public LabelCell() {
super(new DefaultStringConverter());
getStyleClass().add("label-cell");
}
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
if(empty) {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
setText(label);
setContextMenu(new LabelContextMenu(label));
}
}
@Override
public void commitEdit(String label) {
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !label.equals(getItem())) {
TreeTableView<Entry> treeTable = getTreeTableView();
if(treeTable != null) {
TreeTableColumn<Entry, String> column = getTableColumn();
TreeTableColumn.CellEditEvent<Entry, String> event = new TreeTableColumn.CellEditEvent<>(
treeTable, new TreeTablePosition<>(treeTable, getIndex(), column),
TreeTableColumn.editCommitEvent(), label
);
Event.fireEvent(column, event);
}
}
super.commitEdit(label);
}
@Override
public void startEdit() {
super.startEdit();
try {
Field f = getClass().getSuperclass().getDeclaredField("textField");
f.setAccessible(true);
TextField textField = (TextField)f.get(this);
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused) {
commitEdit(getConverter().fromString(textField.getText()));
setText(getConverter().fromString(textField.getText()));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyLabel);
}
}
}

View file

@ -0,0 +1,68 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.List;
public class TransactionsTreeTable extends TreeTableView<Entry> {
public void initialize(WalletTransactionsEntry rootEntry) {
getStyleClass().add("transactions-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 EntryCell());
dateCol.setSortable(true);
getColumns().add(dateCol);
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, Long> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Long> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new AmountCell());
amountCol.setSortable(true);
getColumns().add(amountCol);
TreeTableColumn<Entry, Long> balanceCol = new TreeTableColumn<>("Balance");
balanceCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Long> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue() instanceof TransactionEntry ? ((TransactionEntry)param.getValue().getValue()).getBalance() : null);
});
balanceCol.setCellFactory(p -> new AmountCell());
balanceCol.setSortable(true);
getColumns().add(balanceCol);
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
getSortOrder().add(dateCol);
}
public void updateAll(WalletTransactionsEntry rootEntry) {
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
}
public void updateHistory(List<WalletNode> updatedNodes) {
WalletTransactionsEntry rootEntry = (WalletTransactionsEntry)getRoot().getValue();
rootEntry.updateTransactions();
}
}

View file

@ -2,8 +2,12 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
/**
* This event is posted if the wallet block height has changed.
* Note that it is not posted if the wallet history has also changed - this event is used mainly to ensure the new block height is saved
*/
public class WalletBlockHeightChangedEvent extends WalletChangedEvent { public class WalletBlockHeightChangedEvent extends WalletChangedEvent {
private Integer blockHeight; private final Integer blockHeight;
public WalletBlockHeightChangedEvent(Wallet wallet, Integer blockHeight) { public WalletBlockHeightChangedEvent(Wallet wallet, Integer blockHeight) {
super(wallet); super(wallet);

View file

@ -2,6 +2,9 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
/**
* The base class for all wallet events that should also trigger saving of the wallet
*/
public class WalletChangedEvent { public class WalletChangedEvent {
private final Wallet wallet; private final Wallet wallet;

View file

@ -7,6 +7,10 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* This is posted by WalletForm once the history of the wallet has been refreshed, and new transactions detected
* Extends WalletChangedEvent so also saves the wallet.
*/
public class WalletHistoryChangedEvent extends WalletChangedEvent { public class WalletHistoryChangedEvent extends WalletChangedEvent {
private final List<WalletNode> historyChangedNodes; private final List<WalletNode> historyChangedNodes;

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
/**
* This event is posted by WalletForm once it has received a WalletSettingsChangedEvent and cleared it's entry caches
* It does not extend WalletChangedEvent for the same reason WalletSettingsChangedEvent does not - it does not want to trigger a wallet save.
*/
public class WalletNodesChangedEvent {
private final Wallet wallet;
public WalletNodesChangedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}

View file

@ -4,6 +4,12 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.File; import java.io.File;
/**
* This event is posted when a wallet's settings are changed (keystores, policy, script type).
* This event marks a fundamental change that is used to update application level UI, clear node entry caches and similar. It should only be subscribed to by application-level classes.
* It does not extend WalletChangedEvent since that is used to save the wallet, and the wallet is saved directly in SettingsController before this event is posted.
* Note that all wallet detail controllers that share a WalletForm, that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
*/
public class WalletSettingsChangedEvent { public class WalletSettingsChangedEvent {
private final Wallet wallet; private final Wallet wallet;
private final File walletFile; private final File walletFile;

View file

@ -2,11 +2,11 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
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.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -28,14 +28,21 @@ public class AddressesController extends WalletFormController implements Initial
@Override @Override
public void initializeView() { public void initializeView() {
Wallet wallet = walletForm.getWallet();
receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE)); receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE)); changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
} }
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
receiveTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
changeTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
}
}
@Subscribe @Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) { public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
List<WalletNode> receiveNodes = event.getReceiveNodes(); List<WalletNode> receiveNodes = event.getReceiveNodes();
if(!receiveNodes.isEmpty()) { if(!receiveNodes.isEmpty()) {
receiveTable.updateHistory(receiveNodes); receiveTable.updateHistory(receiveNodes);
@ -46,4 +53,5 @@ public class AddressesController extends WalletFormController implements Initial
changeTable.updateHistory(changeNodes); changeTable.updateHistory(changeNodes);
} }
} }
}
} }

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
@ -9,17 +10,20 @@ import com.sparrowwallet.sparrow.event.WalletChangedEvent;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class HashIndexEntry extends Entry { public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry> {
private final Wallet wallet; private final Wallet wallet;
private final BlockTransactionHashIndex hashIndex; private final BlockTransactionHashIndex hashIndex;
private final Type type; private final Type type;
private final KeyPurpose keyPurpose;
public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type) { public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, KeyPurpose keyPurpose) {
super(hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT)) : Collections.emptyList()); super(hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT, keyPurpose)) : Collections.emptyList());
this.wallet = wallet; this.wallet = wallet;
this.hashIndex = hashIndex; this.hashIndex = hashIndex;
this.type = type; this.type = type;
this.keyPurpose = keyPurpose;
labelProperty().addListener((observable, oldValue, newValue) -> { labelProperty().addListener((observable, oldValue, newValue) -> {
hashIndex.setLabel(newValue); hashIndex.setLabel(newValue);
@ -39,13 +43,18 @@ public class HashIndexEntry extends Entry {
return type; return type;
} }
public KeyPurpose getKeyPurpose() {
return keyPurpose;
}
public BlockTransaction getBlockTransaction() { public BlockTransaction getBlockTransaction() {
return wallet.getTransactions().get(hashIndex.getHash()); return wallet.getTransactions().get(hashIndex.getHash());
} }
public String getDescription() { public String getDescription() {
return (type.equals(Type.INPUT) ? "Spent by " : "Received from ") + return (type.equals(Type.INPUT) ? "Spent by input " : "Received from output ") +
getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex() + getHashIndex().getHash().toString().substring(0, 8) + "...:" +
getHashIndex().getIndex() +
" on " + DateLabel.getShortDateFormat(getHashIndex().getDate()); " on " + DateLabel.getShortDateFormat(getHashIndex().getDate());
} }
@ -61,4 +70,33 @@ public class HashIndexEntry extends Entry {
public enum Type { public enum Type {
INPUT, OUTPUT INPUT, OUTPUT
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashIndexEntry that = (HashIndexEntry) o;
return wallet.equals(that.wallet) &&
hashIndex.equals(that.hashIndex) &&
type == that.type &&
keyPurpose == that.keyPurpose;
}
@Override
public int hashCode() {
return Objects.hash(wallet, hashIndex, type, keyPurpose);
}
@Override
public int compareTo(HashIndexEntry o) {
if(!getType().equals(o.getType())) {
return o.getType().ordinal() - getType().ordinal();
}
if(getHashIndex().getHeight() != o.getHashIndex().getHeight()) {
return o.getHashIndex().getHeight() - getHashIndex().getHeight();
}
return (int)o.getHashIndex().getIndex() - (int)getHashIndex().getIndex();
}
} }

View file

@ -17,7 +17,7 @@ public class NodeEntry extends Entry {
super(node.getLabel(), super(node.getLabel(),
!node.getChildren().isEmpty() ? !node.getChildren().isEmpty() ?
node.getChildren().stream().map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()) : node.getChildren().stream().map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()) :
node.getTransactionOutputs().stream().map(txo -> new HashIndexEntry(wallet, txo, HashIndexEntry.Type.OUTPUT)).collect(Collectors.toList())); node.getTransactionOutputs().stream().map(txo -> new HashIndexEntry(wallet, txo, HashIndexEntry.Type.OUTPUT, node.getKeyPurpose())).collect(Collectors.toList()));
this.wallet = wallet; this.wallet = wallet;
this.node = node; this.node = node;

View file

@ -276,7 +276,6 @@ public class SettingsController extends WalletFormController implements Initiali
try { try {
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY); walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
walletForm.saveAndRefresh(); walletForm.saveAndRefresh();
EventManager.get().post(new WalletSettingsChangedEvent(walletForm.getWallet(), walletForm.getWalletFile()));
} catch (IOException e) { } catch (IOException e) {
AppController.showErrorDialog("Error saving wallet", e.getMessage()); AppController.showErrorDialog("Error saving wallet", e.getMessage());
revert.setDisable(false); revert.setDisable(false);
@ -304,7 +303,6 @@ public class SettingsController extends WalletFormController implements Initiali
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
walletForm.saveAndRefresh(); walletForm.saveAndRefresh();
EventManager.get().post(new WalletSettingsChangedEvent(walletForm.getWallet(), walletForm.getWalletFile()));
} catch (Exception e) { } catch (Exception e) {
AppController.showErrorDialog("Error saving wallet", e.getMessage()); AppController.showErrorDialog("Error saving wallet", e.getMessage());
revert.setDisable(false); revert.setDisable(false);

View file

@ -1,6 +1,8 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import java.io.IOException; import java.io.IOException;
@ -25,10 +27,11 @@ public class SettingsWalletForm extends WalletForm {
@Override @Override
public void saveAndRefresh() throws IOException { public void saveAndRefresh() throws IOException {
//TODO: Detect trivial changes and don't clear history //TODO: Detect trivial changes and don't clear everything
walletCopy.clearHistory(); walletCopy.clearNodes();
wallet = walletCopy.copy(); wallet = walletCopy.copy();
save(); save();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, getWalletFile()));
refreshHistory(wallet.getStoredBlockHeight()); refreshHistory(wallet.getStoredBlockHeight());
} }
} }

View file

@ -0,0 +1,122 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletChangedEvent;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
public class TransactionEntry extends Entry implements Comparable<TransactionEntry> {
private static final int BLOCKS_TO_CONFIRM = 6;
private final Wallet wallet;
private final BlockTransaction blockTransaction;
private WalletTransactionsEntry parent;
public TransactionEntry(Wallet wallet, BlockTransaction blockTransaction, Map<BlockTransactionHashIndex, KeyPurpose> inputs, Map<BlockTransactionHashIndex, KeyPurpose> outputs) {
super(blockTransaction.getLabel(), createChildEntries(wallet, inputs, outputs));
this.wallet = wallet;
this.blockTransaction = blockTransaction;
labelProperty().addListener((observable, oldValue, newValue) -> {
blockTransaction.setLabel(newValue);
EventManager.get().post(new WalletChangedEvent(wallet));
});
}
public Wallet getWallet() {
return wallet;
}
void setParent(WalletTransactionsEntry walletTransactionsEntry) {
this.parent = walletTransactionsEntry;
}
public BlockTransaction getBlockTransaction() {
return blockTransaction;
}
@Override
public Long getValue() {
long value = 0L;
for(Entry entry : getChildren()) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.INPUT)) {
value -= hashIndexEntry.getValue();
} else {
value += hashIndexEntry.getValue();
}
}
return value;
}
public Long getBalance() {
return parent.getBalance(this);
}
public boolean isConfirming() {
return getConfirmations() < BLOCKS_TO_CONFIRM;
}
public int getConfirmations() {
if(blockTransaction.getHeight() == 0) {
return 0;
}
return wallet.getStoredBlockHeight() - blockTransaction.getHeight() + 1;
}
private static List<Entry> createChildEntries(Wallet wallet, Map<BlockTransactionHashIndex, KeyPurpose> incoming, Map<BlockTransactionHashIndex, KeyPurpose> outgoing) {
List<Entry> incomingOutputEntries = incoming.entrySet().stream().map(input -> new TransactionHashIndexEntry(wallet, input.getKey(), HashIndexEntry.Type.OUTPUT, input.getValue())).collect(Collectors.toList());
List<Entry> outgoingInputEntries = outgoing.entrySet().stream().map(output -> new TransactionHashIndexEntry(wallet, output.getKey(), HashIndexEntry.Type.INPUT, output.getValue())).collect(Collectors.toList());
List<Entry> childEntries = new ArrayList<>();
childEntries.addAll(incomingOutputEntries);
childEntries.addAll(outgoingInputEntries);
childEntries.sort((o1, o2) -> {
TransactionHashIndexEntry entry1 = (TransactionHashIndexEntry) o1;
TransactionHashIndexEntry entry2 = (TransactionHashIndexEntry) o2;
if (!entry1.getHashIndex().getHash().equals(entry2.getHashIndex().getHash())) {
return entry1.getHashIndex().getHash().compareTo(entry2.getHashIndex().getHash());
}
if (!entry1.getType().equals(entry2.getType())) {
return entry1.getType().ordinal() - entry2.getType().ordinal();
}
return (int) entry1.getHashIndex().getIndex() - (int) entry2.getHashIndex().getIndex();
});
return childEntries;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TransactionEntry that = (TransactionEntry) o;
return wallet.equals(that.wallet) &&
blockTransaction.equals(that.blockTransaction) &&
parent.equals(that.parent);
}
@Override
public int hashCode() {
return Objects.hash(wallet, blockTransaction, parent);
}
@Override
public int compareTo(@NotNull TransactionEntry other) {
return blockTransaction.compareTo(other.blockTransaction);
}
}

View file

@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class TransactionHashIndexEntry extends HashIndexEntry {
public TransactionHashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, KeyPurpose keyPurpose) {
super(wallet, hashIndex, type, keyPurpose);
}
@Override
public ObservableList<Entry> getChildren() {
return FXCollections.emptyObservableList();
}
@Override
public String getDescription() {
if(getType().equals(Type.INPUT)) {
TransactionInput txInput = getBlockTransaction().getTransaction().getInputs().get((int)getHashIndex().getIndex());
return "Spent " + txInput.getOutpoint().getHash().toString().substring(0, 8) + "...:" + txInput.getOutpoint().getIndex();
} else {
return (getKeyPurpose().equals(KeyPurpose.RECEIVE) ? "Received to " : "Change to ") + getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex();
}
}
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,42 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.TransactionsTreeTable;
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 TransactionsController extends WalletFormController implements Initializable {
@FXML
private TransactionsTreeTable transactionsTable;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
}
@Override
public void initializeView() {
transactionsTable.initialize(getWalletForm().getWalletTransactionsEntry());
}
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
transactionsTable.updateAll(getWalletForm().getWalletTransactionsEntry());
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
transactionsTable.updateHistory(event.getHistoryChangedNodes());
}
}
}

View file

@ -19,6 +19,7 @@ public class WalletForm {
private final Storage storage; private final Storage storage;
protected Wallet wallet; protected Wallet wallet;
private WalletTransactionsEntry walletTransactionsEntry;
private final List<NodeEntry> accountEntries = new ArrayList<>(); private final List<NodeEntry> accountEntries = new ArrayList<>();
public WalletForm(Storage storage, Wallet currentWallet) { public WalletForm(Storage storage, Wallet currentWallet) {
@ -125,6 +126,14 @@ public class WalletForm {
return freshEntry; return freshEntry;
} }
public WalletTransactionsEntry getWalletTransactionsEntry() {
if(walletTransactionsEntry == null) {
walletTransactionsEntry = new WalletTransactionsEntry(wallet);
}
return walletTransactionsEntry;
}
@Subscribe @Subscribe
public void walletChanged(WalletChangedEvent event) { public void walletChanged(WalletChangedEvent event) {
if(event.getWallet().equals(wallet)) { if(event.getWallet().equals(wallet)) {
@ -137,6 +146,14 @@ public class WalletForm {
} }
} }
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
walletTransactionsEntry = null;
accountEntries.clear();
EventManager.get().post(new WalletNodesChangedEvent(wallet));
}
@Subscribe @Subscribe
public void newBlock(NewBlockEvent event) { public void newBlock(NewBlockEvent event) {
refreshHistory(event.getHeight()); refreshHistory(event.getHeight());

View file

@ -0,0 +1,103 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
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 WalletTransactionsEntry extends Entry {
private final Wallet wallet;
public WalletTransactionsEntry(Wallet wallet) {
super(wallet.getName(), getWalletTransactions(wallet).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList()));
this.wallet = wallet;
getChildren().forEach(entry -> ((TransactionEntry)entry).setParent(this));
}
@Override
public Long getValue() {
return getBalance(null);
}
protected Long getBalance(TransactionEntry transactionEntry) {
long balance = 0L;
for(Entry entry : getChildren()) {
balance += entry.getValue();
if(entry == transactionEntry) {
return balance;
}
}
return balance;
}
public void updateTransactions() {
List<Entry> current = getWalletTransactions(wallet).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList());
List<Entry> previous = new ArrayList<>(getChildren());
for(Entry currentEntry : current) {
int index = previous.indexOf(currentEntry);
if (index > -1) {
getChildren().set(index, currentEntry);
} else {
getChildren().add(currentEntry);
}
}
getChildren().sort(Comparator.comparing(TransactionEntry.class::cast));
}
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet) {
Map<BlockTransaction, WalletTransaction> walletTransactionMap = new TreeMap<>();
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(KeyPurpose.RECEIVE));
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(KeyPurpose.CHANGE));
return walletTransactionMap.values();
}
private static void getWalletTransactions(Wallet wallet, Map<BlockTransaction, WalletTransaction> walletTransactionMap, WalletNode purposeNode) {
KeyPurpose keyPurpose = purposeNode.getKeyPurpose();
for(WalletNode addressNode : purposeNode.getChildren()) {
for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) {
BlockTransaction inputTx = wallet.getTransactions().get(hashIndex.getHash());
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
if(inputWalletTx == null) {
inputWalletTx = new WalletTransaction(wallet, inputTx);
walletTransactionMap.put(inputTx, inputWalletTx);
}
inputWalletTx.incoming.put(hashIndex, keyPurpose);
if(hashIndex.getSpentBy() != null) {
BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash());
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
if(outputWalletTx == null) {
outputWalletTx = new WalletTransaction(wallet, outputTx);
walletTransactionMap.put(outputTx, outputWalletTx);
}
outputWalletTx.outgoing.put(hashIndex.getSpentBy(), keyPurpose);
}
}
}
}
private static class WalletTransaction {
private final Wallet wallet;
private final BlockTransaction blockTransaction;
private final Map<BlockTransactionHashIndex, KeyPurpose> incoming = new TreeMap<>();
private final Map<BlockTransactionHashIndex, KeyPurpose> outgoing = new TreeMap<>();
public WalletTransaction(Wallet wallet, BlockTransaction blockTransaction) {
this.wallet = wallet;
this.blockTransaction = blockTransaction;
}
public TransactionEntry getTransactionEntry() {
return new TransactionEntry(wallet, blockTransaction, incoming, outgoing);
}
}
}

View file

@ -1,49 +1 @@
.address-treetable-label {
-fx-font-weight: bold;
-fx-font-size: 1.2em;
-fx-padding: 10 0 10 0;
}
.address-cell {
-fx-font-family: Courier;
}
.hashindex-row {
-fx-text-fill: #696c77;
}
.hashindex-row.spent {
-fx-text-fill: #a0a1a7;
}
.tree-table-row-cell:selected .hashindex-row {
-fx-text-fill: white;
}
.label-cell .text-field {
-fx-padding: 0;
}
.amount-cell {
-fx-alignment: center-right;
}
.amount-cell.spent .text {
-fx-strikethrough: true;
}
.data-cell .button {
-fx-padding: 0;
-fx-pref-height: 18;
-fx-pref-width: 18;
-fx-border-width: 0;
-fx-background-color: -fx-background;
}
.data-cell .button .label .text {
-fx-fill: -fx-background;
}
.data-cell:hover .button .label .text {
-fx-fill: -fx-text-base-color;
}

View file

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

View file

@ -0,0 +1,21 @@
<?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 com.sparrowwallet.sparrow.control.TransactionsTreeTable?>
<?import javafx.geometry.Insets?>
<BorderPane stylesheets="@transactions.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.TransactionsController">
<padding>
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
</padding>
<top>
<Label styleClass="transactions-treetable-label" text="Transactions:"/>
</top>
<center>
<TransactionsTreeTable fx:id="transactionsTable" />
</center>
</BorderPane>

View file

@ -28,3 +28,53 @@
.wallet-pane { .wallet-pane {
-fx-background-color: -fx-background; -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 {
-fx-font-family: Courier;
}
.hashindex-row {
-fx-text-fill: #696c77;
}
.hashindex-row.spent {
-fx-text-fill: #a0a1a7;
}
.tree-table-row-cell:selected .hashindex-row {
-fx-text-fill: white;
}
.label-cell .text-field {
-fx-padding: 0;
}
.amount-cell {
-fx-alignment: center-right;
}
.amount-cell.spent .text {
-fx-strikethrough: true;
}
.entry-cell .button {
-fx-padding: 0;
-fx-pref-height: 18;
-fx-pref-width: 18;
-fx-border-width: 0;
-fx-background-color: -fx-background;
}
.entry-cell .button .label .text {
-fx-fill: -fx-background;
}
.entry-cell:hover .button .label .text {
-fx-fill: -fx-text-base-color;
}