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.event.ReceiveActionEvent;
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.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
@ -56,9 +59,9 @@ public class AddressTreeTable extends TreeTableView<Entry> {
labelCol.setSortable(false);
getColumns().add(labelCol);
TreeTableColumn<Entry, Long> amountCol = new TreeTableColumn<>("Amount");
TreeTableColumn<Entry, Long> amountCol = new TreeTableColumn<>("Value");
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.setSortable(false);
@ -76,16 +79,36 @@ public class AddressTreeTable extends TreeTableView<Entry> {
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> {
public DataCell() {
super();
getStyleClass().add("address-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);
@ -95,8 +118,14 @@ public class AddressTreeTable extends TreeTableView<Entry> {
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address));
} else {
//TODO: Add transaction outpoint
getStyleClass().add("address-cell");
} 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> {
public LabelCell() {
super(new DefaultStringConverter());
@ -138,6 +182,8 @@ public class AddressTreeTable extends TreeTableView<Entry> {
setText(null);
setGraphic(null);
} else {
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
setText(label);
setContextMenu(new LabelContextMenu(label));
}
@ -212,16 +258,15 @@ public class AddressTreeTable extends TreeTableView<Entry> {
setText(null);
setGraphic(null);
} else {
String satsValue = String.format(Locale.ENGLISH, "%,d", amount) + " sats";
String btcValue = CoinLabel.BTC_FORMAT.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
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";
Tooltip tooltip = new Tooltip();
if(amount > CoinLabel.MAX_SATS_SHOWN) {
tooltip.setText(satsValue);
setText(btcValue);
} else {
tooltip.setText(btcValue);
setText(satsValue);
}
setTooltip(tooltip);
}
}
@ -230,6 +275,7 @@ public class AddressTreeTable extends TreeTableView<Entry> {
private static class ActionCell extends TreeTableCell<Entry, Entry> {
private final HBox actionBox;
private final Button receiveButton;
private final Button viewTransactionButton;
public ActionCell() {
super();
@ -248,6 +294,15 @@ public class AddressTreeTable extends TreeTableView<Entry> {
EventManager.get().post(new ReceiveActionEvent(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
@ -256,9 +311,13 @@ public class AddressTreeTable extends TreeTableView<Entry> {
if (empty) {
setGraphic(null);
} else {
applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
actionBox.getChildren().remove(0, actionBox.getChildren().size());
if(entry instanceof NodeEntry) {
actionBox.getChildren().add(receiveButton);
} else if(entry instanceof HashIndexEntry) {
actionBox.getChildren().add(viewTransactionButton);
}
setGraphic(actionBox);

View file

@ -85,4 +85,9 @@ public class CoinLabel extends CopyableLabel {
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){

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'),
QUESTION_CIRCLE('\uf059'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
WALLET('\uf555');
private final char ch;

View file

@ -32,5 +32,5 @@ public abstract class Entry {
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;
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.node = node;
@ -45,9 +49,11 @@ public class NodeEntry extends Entry {
}
@Override
public Long getAmount() {
//TODO: Iterate through TransactionEntries to calculate amount
public Long getValue() {
if(node.getTransactionOutputs().isEmpty()) {
return null;
}
return node.getUnspentValue();
}
}

View file

@ -8,10 +8,30 @@
-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 {
-fx-padding: 0;
}
.amount-cell {
-fx-alignment: center-right;
}
.amount-cell.spent .text {
-fx-strikethrough: true;
}
.action-cell .button {
-fx-padding: 0;
-fx-pref-height: 18;