address table with hash indexes

This commit is contained in:
Craig Raw 2020-06-09 16:10:49 +02:00
parent 9aa8b10898
commit 70c4eeaf5e
10 changed files with 232 additions and 20 deletions

View file

@ -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(satsValue);
setText(btcValue);
} else {
tooltip.setText(btcValue); tooltip.setText(btcValue);
setText(satsValue); 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);

View file

@ -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;
}
} }

View file

@ -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);
}
}
}

View file

@ -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){

View file

@ -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;
}
}

View file

@ -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;

View file

@ -32,5 +32,5 @@ public abstract class Entry {
return children; return children;
} }
public abstract Long getAmount(); public abstract Long getValue();
} }

View file

@ -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
}
}

View file

@ -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();
}
} }

View file

@ -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;