mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
wallet transactions pane and wallet change events
This commit is contained in:
parent
d33ccfb672
commit
21f642bb5c
25 changed files with 924 additions and 332 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 81378b28b25d02dca8cdfc21a6b4fae0421d82b1
|
Subproject commit 18036268e52af8ed8cd5a289f478b302fb415255
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
199
src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
Normal file
199
src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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,22 +28,30 @@ 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
|
@Subscribe
|
||||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
List<WalletNode> receiveNodes = event.getReceiveNodes();
|
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||||
if(!receiveNodes.isEmpty()) {
|
receiveTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
|
||||||
receiveTable.updateHistory(receiveNodes);
|
changeTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<WalletNode> changeNodes = event.getChangeNodes();
|
@Subscribe
|
||||||
if(!changeNodes.isEmpty()) {
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
changeTable.updateHistory(changeNodes);
|
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||||
|
List<WalletNode> receiveNodes = event.getReceiveNodes();
|
||||||
|
if(!receiveNodes.isEmpty()) {
|
||||||
|
receiveTable.updateHistory(receiveNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WalletNode> changeNodes = event.getChangeNodes();
|
||||||
|
if(!changeNodes.isEmpty()) {
|
||||||
|
changeTable.updateHistory(changeNodes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.transactions-treetable-label {
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 1.2em;
|
||||||
|
-fx-padding: 10 0 10 0;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue