mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
address table with hash indexes
This commit is contained in:
parent
9aa8b10898
commit
70c4eeaf5e
10 changed files with 232 additions and 20 deletions
|
@ -6,7 +6,10 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
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.TransactionViewEvent;
|
||||||
|
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;
|
||||||
|
@ -56,9 +59,9 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
labelCol.setSortable(false);
|
labelCol.setSortable(false);
|
||||||
getColumns().add(labelCol);
|
getColumns().add(labelCol);
|
||||||
|
|
||||||
TreeTableColumn<Entry, Long> amountCol = new TreeTableColumn<>("Amount");
|
TreeTableColumn<Entry, Long> amountCol = new TreeTableColumn<>("Value");
|
||||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Long> param) -> {
|
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Long> param) -> {
|
||||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getAmount());
|
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||||
});
|
});
|
||||||
amountCol.setCellFactory(p -> new AmountCell());
|
amountCol.setCellFactory(p -> new AmountCell());
|
||||||
amountCol.setSortable(false);
|
amountCol.setSortable(false);
|
||||||
|
@ -76,16 +79,36 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
private static class DataCell extends TreeTableCell<Entry, Entry> {
|
||||||
public DataCell() {
|
public DataCell() {
|
||||||
super();
|
super();
|
||||||
getStyleClass().add("address-cell");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Entry entry, boolean empty) {
|
protected void updateItem(Entry entry, boolean empty) {
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
|
||||||
|
applyRowStyles(this, entry);
|
||||||
|
getStyleClass().remove("address-cell");
|
||||||
|
|
||||||
if(empty) {
|
if(empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
@ -95,8 +118,14 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new AddressContextMenu(address));
|
setContextMenu(new AddressContextMenu(address));
|
||||||
} else {
|
getStyleClass().add("address-cell");
|
||||||
//TODO: Add transaction outpoint
|
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,6 +153,21 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
private static class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
public LabelCell() {
|
public LabelCell() {
|
||||||
super(new DefaultStringConverter());
|
super(new DefaultStringConverter());
|
||||||
|
@ -138,6 +182,8 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
|
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
|
||||||
|
|
||||||
setText(label);
|
setText(label);
|
||||||
setContextMenu(new LabelContextMenu(label));
|
setContextMenu(new LabelContextMenu(label));
|
||||||
}
|
}
|
||||||
|
@ -212,16 +258,15 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount) + " sats";
|
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
|
||||||
String btcValue = CoinLabel.BTC_FORMAT.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
|
||||||
|
String satsValue = String.format(Locale.ENGLISH, "%,d", amount);
|
||||||
|
String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
if(amount > CoinLabel.MAX_SATS_SHOWN) {
|
tooltip.setText(btcValue);
|
||||||
tooltip.setText(satsValue);
|
|
||||||
setText(btcValue);
|
setText(satsValue);
|
||||||
} else {
|
|
||||||
tooltip.setText(btcValue);
|
|
||||||
setText(satsValue);
|
|
||||||
}
|
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,6 +275,7 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
private static class ActionCell extends TreeTableCell<Entry, Entry> {
|
private static class ActionCell extends TreeTableCell<Entry, Entry> {
|
||||||
private final HBox actionBox;
|
private final HBox actionBox;
|
||||||
private final Button receiveButton;
|
private final Button receiveButton;
|
||||||
|
private final Button viewTransactionButton;
|
||||||
|
|
||||||
public ActionCell() {
|
public ActionCell() {
|
||||||
super();
|
super();
|
||||||
|
@ -248,6 +294,15 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
viewTransactionButton = new Button("");
|
||||||
|
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||||
|
searchGlyph.setFontSize(12);
|
||||||
|
viewTransactionButton.setGraphic(searchGlyph);
|
||||||
|
viewTransactionButton.setOnAction(event -> {
|
||||||
|
HashIndexEntry hashIndexEntry = (HashIndexEntry)getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
|
EventManager.get().post(new TransactionViewEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -256,9 +311,13 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
|
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
|
||||||
|
|
||||||
actionBox.getChildren().remove(0, actionBox.getChildren().size());
|
actionBox.getChildren().remove(0, actionBox.getChildren().size());
|
||||||
if(entry instanceof NodeEntry) {
|
if(entry instanceof NodeEntry) {
|
||||||
actionBox.getChildren().add(receiveButton);
|
actionBox.getChildren().add(receiveButton);
|
||||||
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
|
|
|
@ -85,4 +85,9 @@ public class CoinLabel extends CopyableLabel {
|
||||||
getItems().addAll(copySatsValue, copyBtcValue);
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DecimalFormat getBTCFormat() {
|
||||||
|
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||||
|
return BTC_FORMAT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class DateLabel extends CopyableLabel {
|
||||||
|
private static final DateFormat LAST_24_HR = new SimpleDateFormat("HH:mm");
|
||||||
|
private static final DateFormat LAST_YEAR = new SimpleDateFormat("d MMM");
|
||||||
|
private static final DateFormat ALL_TIME = new SimpleDateFormat("yyyy/MM/dd");
|
||||||
|
|
||||||
|
private Date date;
|
||||||
|
|
||||||
|
public DateLabel(Date date) {
|
||||||
|
super(getShortDateFormat(date));
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getShortDateFormat(Date date) {
|
||||||
|
Date now = new Date();
|
||||||
|
long elapsed = (now.getTime() - date.getTime()) / 1000;
|
||||||
|
|
||||||
|
if(elapsed < 24 * 60 * 60) {
|
||||||
|
return LAST_24_HR.format(date);
|
||||||
|
} else if(elapsed < 365 * 24 * 60 * 60) {
|
||||||
|
return LAST_YEAR.format(date);
|
||||||
|
} else {
|
||||||
|
return ALL_TIME.format(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ public class RecursiveTreeItem<T> extends TreeItem<T> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setExpanded(true);
|
this.setExpanded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addChildrenListener(T value){
|
private void addChildrenListener(T value){
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||||
|
|
||||||
|
public class TransactionViewEvent {
|
||||||
|
public final BlockTransaction blockTransaction;
|
||||||
|
public final HashIndexEntry hashIndexEntry;
|
||||||
|
|
||||||
|
public TransactionViewEvent(BlockTransaction blockTransaction) {
|
||||||
|
this(blockTransaction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionViewEvent(BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) {
|
||||||
|
this.blockTransaction = blockTransaction;
|
||||||
|
this.hashIndexEntry = hashIndexEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockTransaction getBlockTransaction() {
|
||||||
|
return blockTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashIndexEntry getHashIndexEntry() {
|
||||||
|
return hashIndexEntry;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
LOCK_OPEN('\uf3c1'),
|
LOCK_OPEN('\uf3c1'),
|
||||||
QUESTION_CIRCLE('\uf059'),
|
QUESTION_CIRCLE('\uf059'),
|
||||||
SD_CARD('\uf7c2'),
|
SD_CARD('\uf7c2'),
|
||||||
|
SEARCH('\uf002'),
|
||||||
WALLET('\uf555');
|
WALLET('\uf555');
|
||||||
|
|
||||||
private final char ch;
|
private final char ch;
|
||||||
|
|
|
@ -32,5 +32,5 @@ public abstract class Entry {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Long getAmount();
|
public abstract Long getValue();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
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.control.DateLabel;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletChangedEvent;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HashIndexEntry extends Entry {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final BlockTransactionHashIndex hashIndex;
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
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());
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.hashIndex = hashIndex;
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
hashIndex.setLabel(newValue);
|
||||||
|
EventManager.get().post(new WalletChangedEvent(wallet));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockTransactionHashIndex getHashIndex() {
|
||||||
|
return hashIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() +
|
||||||
|
" on " + DateLabel.getShortDateFormat(getHashIndex().getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSpent() {
|
||||||
|
return getType().equals(HashIndexEntry.Type.INPUT) || getHashIndex().getSpentBy() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getValue() {
|
||||||
|
return hashIndex.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
INPUT, OUTPUT
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,11 @@ public class NodeEntry extends Entry {
|
||||||
private final WalletNode node;
|
private final WalletNode node;
|
||||||
|
|
||||||
public NodeEntry(Wallet wallet, WalletNode node) {
|
public NodeEntry(Wallet wallet, WalletNode node) {
|
||||||
super(node.getLabel(), node.getChildren().stream().map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()));
|
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()));
|
||||||
|
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.node = node;
|
this.node = node;
|
||||||
|
|
||||||
|
@ -45,9 +49,11 @@ public class NodeEntry extends Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getAmount() {
|
public Long getValue() {
|
||||||
//TODO: Iterate through TransactionEntries to calculate amount
|
if(node.getTransactionOutputs().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return node.getUnspentValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,30 @@
|
||||||
-fx-font-family: Courier;
|
-fx-font-family: Courier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hashindex-row {
|
||||||
|
-fx-background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hashindex-row .text {
|
||||||
|
-fx-fill: #696c77;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hashindex-row.spent .text {
|
||||||
|
-fx-fill: #a0a1a7;
|
||||||
|
}
|
||||||
|
|
||||||
.label-cell .text-field {
|
.label-cell .text-field {
|
||||||
-fx-padding: 0;
|
-fx-padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
-fx-alignment: center-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell.spent .text {
|
||||||
|
-fx-strikethrough: true;
|
||||||
|
}
|
||||||
|
|
||||||
.action-cell .button {
|
.action-cell .button {
|
||||||
-fx-padding: 0;
|
-fx-padding: 0;
|
||||||
-fx-pref-height: 18;
|
-fx-pref-height: 18;
|
||||||
|
|
Loading…
Reference in a new issue