mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 20:36:44 +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;
|
||||
|
||||
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.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.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Pos;
|
||||
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.layout.Region;
|
||||
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.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AddressTreeTable extends TreeTableView<Entry> {
|
||||
|
@ -37,17 +20,14 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
|||
getStyleClass().add("address-treetable");
|
||||
|
||||
String address = rootEntry.getAddress().toString();
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
rootItem.setExpanded(true);
|
||||
updateAll(rootEntry);
|
||||
setShowRoot(false);
|
||||
|
||||
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address / Outpoints");
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new DataCell());
|
||||
addressCol.setCellFactory(p -> new EntryCell());
|
||||
addressCol.setSortable(false);
|
||||
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) {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private Integer blockHeight;
|
||||
private final Integer blockHeight;
|
||||
|
||||
public WalletBlockHeightChangedEvent(Wallet wallet, Integer blockHeight) {
|
||||
super(wallet);
|
||||
|
|
|
@ -2,6 +2,9 @@ package com.sparrowwallet.sparrow.event;
|
|||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
/**
|
||||
* The base class for all wallet events that should also trigger saving of the wallet
|
||||
*/
|
||||
public class WalletChangedEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
|||
import java.util.List;
|
||||
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 {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private final Wallet wallet;
|
||||
private final File walletFile;
|
||||
|
|
|
@ -2,11 +2,11 @@ package com.sparrowwallet.sparrow.wallet;
|
|||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
|
||||
|
@ -28,22 +28,30 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
|
||||
@Override
|
||||
public void initializeView() {
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
|
||||
receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
List<WalletNode> receiveNodes = event.getReceiveNodes();
|
||||
if(!receiveNodes.isEmpty()) {
|
||||
receiveTable.updateHistory(receiveNodes);
|
||||
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
receiveTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
changeTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletNode> changeNodes = event.getChangeNodes();
|
||||
if(!changeNodes.isEmpty()) {
|
||||
changeTable.updateHistory(changeNodes);
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
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;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
@ -9,17 +10,20 @@ import com.sparrowwallet.sparrow.event.WalletChangedEvent;
|
|||
|
||||
import java.util.Collections;
|
||||
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 BlockTransactionHashIndex hashIndex;
|
||||
private final Type type;
|
||||
private final KeyPurpose keyPurpose;
|
||||
|
||||
public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type) {
|
||||
super(hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT)) : Collections.emptyList());
|
||||
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, keyPurpose)) : Collections.emptyList());
|
||||
this.wallet = wallet;
|
||||
this.hashIndex = hashIndex;
|
||||
this.type = type;
|
||||
this.keyPurpose = keyPurpose;
|
||||
|
||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||
hashIndex.setLabel(newValue);
|
||||
|
@ -39,13 +43,18 @@ public class HashIndexEntry extends Entry {
|
|||
return type;
|
||||
}
|
||||
|
||||
public KeyPurpose getKeyPurpose() {
|
||||
return keyPurpose;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return wallet.getTransactions().get(hashIndex.getHash());
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return (type.equals(Type.INPUT) ? "Spent by " : "Received from ") +
|
||||
getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex() +
|
||||
return (type.equals(Type.INPUT) ? "Spent by input " : "Received from output ") +
|
||||
getHashIndex().getHash().toString().substring(0, 8) + "...:" +
|
||||
getHashIndex().getIndex() +
|
||||
" on " + DateLabel.getShortDateFormat(getHashIndex().getDate());
|
||||
}
|
||||
|
||||
|
@ -61,4 +70,33 @@ public class HashIndexEntry extends Entry {
|
|||
public enum Type {
|
||||
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(),
|
||||
!node.getChildren().isEmpty() ?
|
||||
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.node = node;
|
||||
|
|
|
@ -276,7 +276,6 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
try {
|
||||
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
walletForm.saveAndRefresh();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(walletForm.getWallet(), walletForm.getWalletFile()));
|
||||
} catch (IOException e) {
|
||||
AppController.showErrorDialog("Error saving wallet", e.getMessage());
|
||||
revert.setDisable(false);
|
||||
|
@ -304,7 +303,6 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
|
||||
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
|
||||
walletForm.saveAndRefresh();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(walletForm.getWallet(), walletForm.getWalletFile()));
|
||||
} catch (Exception e) {
|
||||
AppController.showErrorDialog("Error saving wallet", e.getMessage());
|
||||
revert.setDisable(false);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.sparrowwallet.sparrow.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 java.io.IOException;
|
||||
|
@ -25,10 +27,11 @@ public class SettingsWalletForm extends WalletForm {
|
|||
|
||||
@Override
|
||||
public void saveAndRefresh() throws IOException {
|
||||
//TODO: Detect trivial changes and don't clear history
|
||||
walletCopy.clearHistory();
|
||||
//TODO: Detect trivial changes and don't clear everything
|
||||
walletCopy.clearNodes();
|
||||
wallet = walletCopy.copy();
|
||||
save();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, getWalletFile()));
|
||||
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;
|
||||
protected Wallet wallet;
|
||||
|
||||
private WalletTransactionsEntry walletTransactionsEntry;
|
||||
private final List<NodeEntry> accountEntries = new ArrayList<>();
|
||||
|
||||
public WalletForm(Storage storage, Wallet currentWallet) {
|
||||
|
@ -125,6 +126,14 @@ public class WalletForm {
|
|||
return freshEntry;
|
||||
}
|
||||
|
||||
public WalletTransactionsEntry getWalletTransactionsEntry() {
|
||||
if(walletTransactionsEntry == null) {
|
||||
walletTransactionsEntry = new WalletTransactionsEntry(wallet);
|
||||
}
|
||||
|
||||
return walletTransactionsEntry;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletChanged(WalletChangedEvent event) {
|
||||
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
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
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 {
|
||||
-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