wallet tx confirmation indicator and event refactor

This commit is contained in:
Craig Raw 2020-06-23 14:08:14 +02:00
parent 21f642bb5c
commit a8f16c15e0
14 changed files with 340 additions and 50 deletions

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
@ -14,7 +15,6 @@ class AmountCell extends TreeTableCell<Entry, Long> {
public AmountCell() {
super();
getStyleClass().add("amount-cell");
setContentDisplay(ContentDisplay.RIGHT);
}
@Override
@ -25,16 +25,37 @@ class AmountCell extends TreeTableCell<Entry, Long> {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
String satsValue = String.format(Locale.ENGLISH, "%,d", amount);
String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
if(entry instanceof HashIndexEntry) {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
Tooltip tooltip = new Tooltip();
tooltip.setText(btcValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
setTooltip(tooltip);
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
Tooltip newTooltip = new Tooltip();
newTooltip.setText(btcValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
setTooltip(newTooltip);
});
if(transactionEntry.isConfirming()) {
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
arc.confirmationsProperty().bind(transactionEntry.confirmationsProperty());
setGraphic(arc);
setContentDisplay(ContentDisplay.LEFT);
} else {
setGraphic(null);
}
} else if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
setContentDisplay(ContentDisplay.RIGHT);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
@ -43,11 +64,14 @@ class AmountCell extends TreeTableCell<Entry, Long> {
setGraphic(null);
}
if(getTooltip() == null) {
Tooltip tooltip = new Tooltip();
tooltip.setText(btcValue);
setText(satsValue);
setTooltip(tooltip);
}
setText(satsValue);
}
}
}

View file

@ -0,0 +1,137 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.animation.*;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.util.Duration;
public class ConfirmationProgressIndicator extends StackPane {
private final Group confirmationGroup;
private final Arc arc;
private final Line downTickLine;
private final Line upTickLine;
public ConfirmationProgressIndicator(int confirmations) {
Circle circle = new Circle(7, 7, 7);
circle.setFill(Color.TRANSPARENT);
circle.getStyleClass().add("confirmation-progress-circle");
arc = new Arc(7, 7, 7, 7, 90, getDegrees(confirmations));
arc.setType(ArcType.ROUND);
arc.getStyleClass().add("confirmation-progress-arc");
downTickLine = new Line(4, 8, 4, 8);
downTickLine.setStrokeWidth(1.0);
downTickLine.setOpacity(0);
downTickLine.getStyleClass().add("confirmation-progress-tick");
upTickLine = new Line(6, 10, 6, 10);
upTickLine.setStrokeWidth(1.0);
upTickLine.setOpacity(0);
upTickLine.getStyleClass().add("confirmation-progress-tick");
confirmationGroup = new Group(circle, arc, downTickLine, upTickLine);
getStyleClass().add("confirmation-progress");
setAlignment(Pos.CENTER);
getChildren().addAll(confirmationGroup);
confirmationsProperty().set(confirmations);
confirmationsProperty().addListener((observable, oldValue, newValue) -> {
if(!oldValue.equals(newValue)) {
SequentialTransition sequence = new SequentialTransition();
Timeline arcLengthTimeline = new Timeline();
KeyValue arcLengthValue = new KeyValue(arc.lengthProperty(), getDegrees(newValue.intValue()));
KeyFrame arcLengthFrame = new KeyFrame(Duration.millis(1000), arcLengthValue);
arcLengthTimeline.getKeyFrames().add(arcLengthFrame);
sequence.getChildren().add(arcLengthTimeline);
if(newValue.intValue() == TransactionEntry.BLOCKS_TO_CONFIRM) {
Timeline arcRadiusTimeline = new Timeline();
KeyValue arcRadiusXValue = new KeyValue(arc.radiusXProperty(), 0.0);
KeyValue arcRadiusYValue = new KeyValue(arc.radiusYProperty(), 0.0);
KeyFrame arcRadiusFrame = new KeyFrame(Duration.millis(500), arcRadiusXValue, arcRadiusYValue);
arcRadiusTimeline.getKeyFrames().add(arcRadiusFrame);
sequence.getChildren().add(arcRadiusTimeline);
FadeTransition downTickFadeIn = new FadeTransition(Duration.millis(50), downTickLine);
downTickFadeIn.setFromValue(0);
downTickFadeIn.setToValue(1);
sequence.getChildren().add(downTickFadeIn);
Timeline downTickLineTimeline = new Timeline();
KeyValue downTickLineX = new KeyValue(downTickLine.endXProperty(), 6);
KeyValue downTickLineY = new KeyValue(downTickLine.endYProperty(), 10);
KeyFrame downTickLineFrame = new KeyFrame(Duration.millis(125), downTickLineX, downTickLineY);
downTickLineTimeline.getKeyFrames().add(downTickLineFrame);
sequence.getChildren().add(downTickLineTimeline);
FadeTransition upTickFadeIn = new FadeTransition(Duration.millis(50), upTickLine);
upTickFadeIn.setFromValue(0);
upTickFadeIn.setToValue(1);
sequence.getChildren().add(upTickFadeIn);
Timeline upTickLineTimeline = new Timeline();
KeyValue upTickLineX = new KeyValue(upTickLine.endXProperty(), 10);
KeyValue upTickLineY = new KeyValue(upTickLine.endYProperty(), 4);
KeyFrame upTickLineFrame = new KeyFrame(Duration.millis(250), upTickLineX, upTickLineY);
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
sequence.getChildren().add(upTickLineTimeline);
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(1), confirmationGroup);
groupFadeOut.setFromValue(1);
groupFadeOut.setToValue(0);
sequence.getChildren().add(groupFadeOut);
}
sequence.play();
}
});
}
private static double getDegrees(int confirmations) {
int requiredConfirmations = TransactionEntry.BLOCKS_TO_CONFIRM;
return ((double)Math.min(confirmations, requiredConfirmations)/ requiredConfirmations) * -360d;
}
/**
* Defines the number of confirmations
*/
private IntegerProperty confirmations;
public final void setConfirmations(int value) {
if(confirmations != null || value != 0) {
confirmationsProperty().set(value);
}
}
public final int getConfirmations() {
return confirmations == null ? 0 : confirmations.get();
}
public final IntegerProperty confirmationsProperty() {
if(confirmations == null) {
confirmations = new IntegerPropertyBase(0) {
@Override
public Object getBean() {
return ConfirmationProgressIndicator.this;
}
@Override
public String getName() {
return "confirmations";
}
};
}
return confirmations;
}
}

View file

@ -2,17 +2,16 @@ 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 com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
@ -185,6 +184,15 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
if(entry != null) {
if(entry instanceof TransactionEntry) {
cell.getStyleClass().add("transaction-row");
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
if(!transactionEntry.isConfirming()) {
cell.getStyleClass().remove("confirming");
}
});
}
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof HashIndexEntry) {

View file

@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
public class WalletEntryLabelChangedEvent extends WalletChangedEvent {
private final Entry entry;
public WalletEntryLabelChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
}

View file

@ -11,11 +11,11 @@ import java.util.stream.Collectors;
* This is posted by WalletForm once the history of the wallet has been refreshed, and new transactions detected
* Extends WalletChangedEvent so also saves the wallet.
*/
public class WalletHistoryChangedEvent extends WalletChangedEvent {
public class WalletHistoryChangedEvent extends WalletBlockHeightChangedEvent {
private final List<WalletNode> historyChangedNodes;
public WalletHistoryChangedEvent(Wallet wallet, List<WalletNode> historyChangedNodes) {
super(wallet);
public WalletHistoryChangedEvent(Wallet wallet, Integer blockHeight, List<WalletNode> historyChangedNodes) {
super(wallet, blockHeight);
this.historyChangedNodes = historyChangedNodes;
}

View file

@ -4,16 +4,10 @@ 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.
* The controllers in the wallet package listen to this event to update their views should a wallet's settings change
*/
public class WalletNodesChangedEvent {
private final Wallet wallet;
public class WalletNodesChangedEvent extends WalletChangedEvent {
public WalletNodesChangedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
super(wallet);
}
}

View file

@ -7,22 +7,18 @@ 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.
* Note that WalletForm does not listen to this event to save the wallet, since the wallet is foreground saved directly in SettingsController before this event is posted.
* This is because any failure in saving the wallet must be immediately reported to the user.
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
*/
public class WalletSettingsChangedEvent {
private final Wallet wallet;
public class WalletSettingsChangedEvent extends WalletChangedEvent {
private final File walletFile;
public WalletSettingsChangedEvent(Wallet wallet, File walletFile) {
this.wallet = wallet;
super(wallet);
this.walletFile = walletFile;
}
public Wallet getWallet() {
return wallet;
}
public File getWalletFile() {
return walletFile;
}

View file

@ -6,7 +6,7 @@ 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 com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import java.util.Collections;
import java.util.List;
@ -27,7 +27,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
labelProperty().addListener((observable, oldValue, newValue) -> {
hashIndex.setLabel(newValue);
EventManager.get().post(new WalletChangedEvent(wallet));
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
});
}

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import java.util.stream.Collectors;
@ -24,7 +24,7 @@ public class NodeEntry extends Entry {
labelProperty().addListener((observable, oldValue, newValue) -> {
node.setLabel(newValue);
EventManager.get().post(new WalletChangedEvent(wallet));
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
});
}

View file

@ -1,20 +1,22 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
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 com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import java.util.*;
import java.util.stream.Collectors;
public class TransactionEntry extends Entry implements Comparable<TransactionEntry> {
private static final int BLOCKS_TO_CONFIRM = 6;
public static final int BLOCKS_TO_CONFIRM = 6;
public static final int BLOCKS_TO_FULLY_CONFIRM = 100;
private final Wallet wallet;
private final BlockTransaction blockTransaction;
@ -27,8 +29,13 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
labelProperty().addListener((observable, oldValue, newValue) -> {
blockTransaction.setLabel(newValue);
EventManager.get().post(new WalletChangedEvent(wallet));
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
});
setConfirmations(calculateConfirmations());
if(isFullyConfirming()) {
EventManager.get().register(this);
}
}
public Wallet getWallet() {
@ -66,7 +73,11 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
return getConfirmations() < BLOCKS_TO_CONFIRM;
}
public int getConfirmations() {
public boolean isFullyConfirming() {
return getConfirmations() < BLOCKS_TO_FULLY_CONFIRM;
}
public int calculateConfirmations() {
if(blockTransaction.getHeight() == 0) {
return 0;
}
@ -74,6 +85,17 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
return wallet.getStoredBlockHeight() - blockTransaction.getHeight() + 1;
}
public String getConfirmationsDescription() {
int confirmations = getConfirmations();
if(confirmations == 0) {
return "Unconfirmed in mempool";
} else if(confirmations < BLOCKS_TO_FULLY_CONFIRM) {
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
} else {
return BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
}
}
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());
@ -116,7 +138,51 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
}
@Override
public int compareTo(@NotNull TransactionEntry other) {
public int compareTo(TransactionEntry other) {
return blockTransaction.compareTo(other.blockTransaction);
}
/**
* Defines the number of confirmations
*/
private IntegerProperty confirmations;
public final void setConfirmations(int value) {
if(confirmations != null || value != 0) {
confirmationsProperty().set(value);
}
}
public final int getConfirmations() {
return confirmations == null ? 0 : confirmations.get();
}
public final IntegerProperty confirmationsProperty() {
if(confirmations == null) {
confirmations = new IntegerPropertyBase(0) {
@Override
public Object getBean() {
return TransactionEntry.this;
}
@Override
public String getName() {
return "confirmations";
}
};
}
return confirmations;
}
@Subscribe
public void blockHeightChanged(WalletBlockHeightChangedEvent event) {
if(getWallet().equals(event.getWallet())) {
setConfirmations(calculateConfirmations());
if(!isFullyConfirming()) {
EventManager.get().unregister(this);
}
}
}
}

View file

@ -3,10 +3,12 @@ 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.WalletBlockHeightChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.input.MouseEvent;
import java.net.URL;
import java.util.ResourceBundle;
@ -39,4 +41,12 @@ public class TransactionsController extends WalletFormController implements Init
transactionsTable.updateHistory(event.getHistoryChangedNodes());
}
}
//TODO: Remove
public void advanceBlock(MouseEvent event) {
Integer currentBlock = getWalletForm().getWallet().getStoredBlockHeight();
getWalletForm().getWallet().setStoredBlockHeight(currentBlock+1);
System.out.println("Advancing from " + currentBlock + " to " + getWalletForm().getWallet().getStoredBlockHeight());
EventManager.get().post(new WalletBlockHeightChangedEvent(getWalletForm().getWallet(), currentBlock+1));
}
}

View file

@ -75,7 +75,7 @@ public class WalletForm {
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren()));
if(!historyChangedNodes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, historyChangedNodes)));
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, blockHeight, historyChangedNodes)));
} else if(blockHeight != null && !blockHeight.equals(previousWallet.getStoredBlockHeight())) {
Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(wallet, blockHeight)));
}
@ -135,7 +135,16 @@ public class WalletForm {
}
@Subscribe
public void walletChanged(WalletChangedEvent event) {
public void walletLabelChanged(WalletEntryLabelChangedEvent event) {
backgroundSaveWallet(event);
}
@Subscribe
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
backgroundSaveWallet(event);
}
private void backgroundSaveWallet(WalletChangedEvent event) {
if(event.getWallet().equals(wallet)) {
try {
save();

View file

@ -13,7 +13,7 @@
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
</padding>
<top>
<Label styleClass="transactions-treetable-label" text="Transactions:"/>
<Label styleClass="transactions-treetable-label" text="Transactions:" /> <!-- onMouseClicked="#advanceBlock" -->
</top>
<center>
<TransactionsTreeTable fx:id="transactionsTable" />

View file

@ -47,7 +47,11 @@
-fx-text-fill: #a0a1a7;
}
.tree-table-row-cell:selected .hashindex-row {
.transaction-row.confirming {
-fx-text-fill: #696c77;
}
.tree-table-row-cell:selected .hashindex-row, .tree-table-row-cell:selected .transaction-row {
-fx-text-fill: white;
}
@ -63,6 +67,27 @@
-fx-strikethrough: true;
}
.amount-cell .confirmation-progress {
-fx-pref-width: 14;
-fx-padding: 0 8 0 0;
}
.confirmation-progress-circle, .confirmation-progress-tick {
-fx-stroke: -fx-text-base-color;
}
.tree-table-row-cell:selected .confirmation-progress-circle, .tree-table-row-cell:selected .confirmation-progress-tick {
-fx-stroke: white;
}
.confirmation-progress-arc {
-fx-fill: -fx-text-base-color;
}
.tree-table-row-cell:selected .confirmation-progress-arc {
-fx-fill: white;
}
.entry-cell .button {
-fx-padding: 0;
-fx-pref-height: 18;
@ -78,3 +103,7 @@
.entry-cell:hover .button .label .text {
-fx-fill: -fx-text-base-color;
}
.tree-table-row-cell:selected .entry-cell:hover .button .label .text {
-fx-fill: white;
}