mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 20:36:44 +00:00
send pane improvements
This commit is contained in:
parent
48e741733b
commit
66e558faee
11 changed files with 435 additions and 70 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit ccf7de9f625c4cc73efc6948b3e699a7786da276
|
||||
Subproject commit 2ff9c94c62d1f20e408f89d7a41e2e66426b8634
|
|
@ -8,12 +8,20 @@ public enum BitcoinUnit {
|
|||
public long getSatsValue(double unitValue) {
|
||||
return (long)(unitValue * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public double getValue(long satsValue) {
|
||||
return (double)satsValue / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
},
|
||||
SATOSHIS("sats") {
|
||||
@Override
|
||||
public long getSatsValue(double unitValue) {
|
||||
return (long)unitValue;
|
||||
}
|
||||
|
||||
public double getValue(long satsValue) {
|
||||
return (double)satsValue;
|
||||
}
|
||||
};
|
||||
|
||||
private final String label;
|
||||
|
@ -28,6 +36,13 @@ public enum BitcoinUnit {
|
|||
|
||||
public abstract long getSatsValue(double unitValue);
|
||||
|
||||
public abstract double getValue(long satsValue);
|
||||
|
||||
public double convertFrom(double fromValue, BitcoinUnit fromUnit) {
|
||||
long satsValue = fromUnit.getSatsValue(fromValue);
|
||||
return getValue(satsValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
|
|
|
@ -33,6 +33,10 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
|||
|
||||
@Override
|
||||
public void commitEdit(String label) {
|
||||
if(label != null) {
|
||||
label = label.trim();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -38,8 +38,8 @@ public class TextFieldValidator {
|
|||
return new TextFieldValidator(integersOnlyPattern());
|
||||
}
|
||||
|
||||
public TextFormatter<Object> getFormatter() {
|
||||
return new TextFormatter<>(this::validateChange);
|
||||
public TextFormatter<String> getFormatter() {
|
||||
return new TextFormatter<>(TextFormatter.IDENTITY_STRING_CONVERTER, "", this::validateChange);
|
||||
}
|
||||
|
||||
private Change validateChange(Change c) {
|
||||
|
|
|
@ -1,25 +1,39 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 5;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
|
||||
public TransactionDiagram() {
|
||||
int columns = 5;
|
||||
double percentWidth = 100.0 / columns;
|
||||
double[] percentWidth = {20, 20, 10, 20, 30};
|
||||
|
||||
for(int i = 0; i < columns; i++) {
|
||||
ColumnConstraints columnConstraints = new ColumnConstraints();
|
||||
columnConstraints.setPercentWidth(percentWidth);
|
||||
columnConstraints.setPercentWidth(percentWidth[i]);
|
||||
getColumnConstraints().add(columnConstraints);
|
||||
}
|
||||
}
|
||||
|
@ -28,32 +42,84 @@ public class TransactionDiagram extends GridPane {
|
|||
if(walletTx == null) {
|
||||
getChildren().clear();
|
||||
} else {
|
||||
update(walletTx.getWallet(), walletTx.getSelectedUtxos(), walletTx.getRecipientAddress(), walletTx.getChangeNode(), walletTx.getFee());
|
||||
this.walletTx = walletTx;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Wallet wallet, Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, Address toAddress, WalletNode changeNode, long fee) {
|
||||
Pane inputsPane = getInputsLabels(selectedUtxos);
|
||||
public void update() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = getDisplayedUtxos();
|
||||
|
||||
Pane inputsPane = getInputsLabels(displayedUtxos);
|
||||
GridPane.setConstraints(inputsPane, 0, 0);
|
||||
|
||||
Pane inputsLinesPane = getInputsLines(displayedUtxos);
|
||||
GridPane.setConstraints(inputsLinesPane, 1, 0);
|
||||
|
||||
Pane txPane = getTransactionPane();
|
||||
GridPane.setConstraints(txPane, 2, 0);
|
||||
|
||||
Pane outputsPane = getOutputsLabels(wallet, toAddress, changeNode, fee);
|
||||
Pane outputsLinesPane = getOutputsLines();
|
||||
GridPane.setConstraints(outputsLinesPane, 3, 0);
|
||||
|
||||
Pane outputsPane = getOutputsLabels();
|
||||
GridPane.setConstraints(outputsPane, 4, 0);
|
||||
|
||||
getChildren().clear();
|
||||
getChildren().addAll(inputsPane, txPane, outputsPane);
|
||||
getChildren().addAll(inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
}
|
||||
|
||||
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> selectedUtxos) {
|
||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
||||
|
||||
if(selectedUtxos.size() > MAX_UTXOS) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
||||
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
||||
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
||||
if (utxos.size() < MAX_UTXOS) {
|
||||
utxos.put(reference, selectedUtxos.get(reference));
|
||||
} else {
|
||||
additional.add(reference);
|
||||
}
|
||||
}
|
||||
|
||||
utxos.put(new AdditionalBlockTransactionHashIndex(additional), null);
|
||||
return utxos;
|
||||
} else {
|
||||
return selectedUtxos;
|
||||
}
|
||||
}
|
||||
|
||||
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
for(BlockTransactionHashIndex input : selectedUtxos.keySet()) {
|
||||
String desc = input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "...:" + input.getIndex();
|
||||
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
|
||||
WalletNode walletNode = displayedUtxos.get(input);
|
||||
String desc = getInputDescription(input);
|
||||
Label label = new Label(desc);
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
label.getStyleClass().add("input-label");
|
||||
} else {
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
} else {
|
||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex)input;
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
label.setTooltip(tooltip);
|
||||
|
||||
inputsBox.getChildren().add(label);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
@ -61,23 +127,134 @@ public class TransactionDiagram extends GridPane {
|
|||
return inputsBox;
|
||||
}
|
||||
|
||||
private Pane getOutputsLabels(Wallet wallet, Address toAddress, WalletNode changeNode, long fee) {
|
||||
private String getInputDescription(BlockTransactionHashIndex input) {
|
||||
return input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "..:" + input.getIndex();
|
||||
}
|
||||
|
||||
private String getSatsValue(long amount) {
|
||||
return String.format(Locale.ENGLISH, "%,d", amount);
|
||||
}
|
||||
|
||||
private Pane getInputsLines(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
|
||||
Line yaxisLine = new Line();
|
||||
yaxisLine.setStartX(0);
|
||||
yaxisLine.setStartY(0);
|
||||
yaxisLine.setEndX(0);
|
||||
yaxisLine.endYProperty().bind(this.heightProperty());
|
||||
yaxisLine.getStyleClass().add("y-axis");
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
int numUtxos = displayedUtxos.size();
|
||||
for(int i = 1; i <= numUtxos; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.startYProperty().bind(getScaledProperty(this.heightProperty(), (double)i / (numUtxos + 1), 20));
|
||||
curve.endXProperty().bind(pane.widthProperty());
|
||||
curve.endYProperty().bind(getScaledProperty(this.heightProperty(), 0.5, 0));
|
||||
|
||||
curve.controlX1Property().bind(getScaledProperty(pane.widthProperty(), 0.2, 0));
|
||||
curve.controlY1Property().bind(curve.startYProperty());
|
||||
curve.controlX2Property().bind(getScaledProperty(pane.widthProperty(), 0.8, 0));
|
||||
curve.controlY2Property().bind(curve.endYProperty());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
return pane;
|
||||
}
|
||||
|
||||
private static DoubleProperty getScaledProperty(ReadOnlyDoubleProperty property, double scaleFactor, int nodeHeight) {
|
||||
SimpleDoubleProperty scaledProperty = new SimpleDoubleProperty(scale(property.doubleValue(), scaleFactor, nodeHeight));
|
||||
property.addListener((observable, oldValue, newValue) -> {
|
||||
scaledProperty.set(scale(newValue.doubleValue(), scaleFactor, nodeHeight));
|
||||
});
|
||||
|
||||
return scaledProperty;
|
||||
}
|
||||
|
||||
private static double scale(Double value, double scaleFactor, int nodeHeight) {
|
||||
double scaled = value * (1.0 - scaleFactor);
|
||||
if(nodeHeight > 0) {
|
||||
scaled += (0.5 - scaleFactor) * ( (double)nodeHeight );
|
||||
}
|
||||
|
||||
return scaled;
|
||||
}
|
||||
|
||||
private Pane getOutputsLines() {
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
|
||||
Line yaxisLine = new Line();
|
||||
yaxisLine.setStartX(0);
|
||||
yaxisLine.setStartY(0);
|
||||
yaxisLine.setEndX(0);
|
||||
yaxisLine.endYProperty().bind(this.heightProperty());
|
||||
yaxisLine.getStyleClass().add("y-axis");
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
int numOutputs = (walletTx.getChangeNode() == null ? 2 : 3);
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("output-line");
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.startYProperty().bind(getScaledProperty(this.heightProperty(), 0.5, 0));
|
||||
curve.endXProperty().bind(pane.widthProperty());
|
||||
curve.endYProperty().bind(getScaledProperty(this.heightProperty(), (double)i / (numOutputs + 1), 20));
|
||||
|
||||
curve.controlX1Property().bind(getScaledProperty(pane.widthProperty(), 0.2, 0));
|
||||
curve.controlY1Property().bind(curve.startYProperty());
|
||||
curve.controlX2Property().bind(getScaledProperty(pane.widthProperty(), 0.8, 0));
|
||||
curve.controlY2Property().bind(curve.endYProperty());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
return pane;
|
||||
}
|
||||
|
||||
private Pane getOutputsLabels() {
|
||||
VBox outputsBox = new VBox();
|
||||
outputsBox.setPadding(new Insets(0, 30, 0, 10));
|
||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
String addressDesc = toAddress.toString();
|
||||
Label addressLabel = new Label(addressDesc);
|
||||
outputsBox.getChildren().add(addressLabel);
|
||||
String recipientDesc = walletTx.getRecipientAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, getSendGlyph());
|
||||
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip("Send " + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputsBox.getChildren().add(recipientLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
String changeDesc = wallet.getAddress(changeNode).toString();
|
||||
Label changeLabel = new Label(changeDesc);
|
||||
outputsBox.getChildren().add(changeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
if(walletTx.getChangeNode() != null) {
|
||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
outputsBox.getChildren().add(changeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||
String feeDesc = "Fee";
|
||||
Label feeLabel = new Label(feeDesc);
|
||||
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||
feeLabel.setTooltip(feeTooltip);
|
||||
outputsBox.getChildren().add(feeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
|
@ -91,6 +268,8 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
|
||||
txLabel.setTooltip(tooltip);
|
||||
txPane.getChildren().add(txLabel);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
|
||||
|
@ -102,4 +281,50 @@ public class TransactionDiagram extends GridPane {
|
|||
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private Glyph getSendGlyph() {
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.getStyleClass().add("send-icon");
|
||||
sendGlyph.setFontSize(12);
|
||||
return sendGlyph;
|
||||
}
|
||||
|
||||
private Glyph getChangeGlyph() {
|
||||
Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS);
|
||||
changeGlyph.getStyleClass().add("change-icon");
|
||||
changeGlyph.setFontSize(12);
|
||||
return changeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getFeeGlyph() {
|
||||
Glyph feeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.HAND_HOLDING);
|
||||
feeGlyph.getStyleClass().add("fee-icon");
|
||||
feeGlyph.setFontSize(12);
|
||||
return feeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getWarningGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private static class AdditionalBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final List<BlockTransactionHashIndex> additionalInputs;
|
||||
|
||||
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.additionalInputs = additionalInputs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return additionalInputs.size() + " more";
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getAdditionalInputs() {
|
||||
return additionalInputs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ public class FontAwesome5 extends GlyphFont {
|
|||
EXCLAMATION_CIRCLE('\uf06a'),
|
||||
ELLIPSIS_H('\uf141'),
|
||||
EYE('\uf06e'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
KEY('\uf084'),
|
||||
LAPTOP('\uf109'),
|
||||
LOCK('\uf023'),
|
||||
|
|
|
@ -53,7 +53,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
|||
|
||||
public String getDescription() {
|
||||
return (type.equals(Type.INPUT) ? "Spent by input " : "Received from output ") +
|
||||
getHashIndex().getHash().toString().substring(0, 8) + "...:" +
|
||||
getHashIndex().getHash().toString().substring(0, 8) + "..:" +
|
||||
getHashIndex().getIndex() +
|
||||
" on " + DateLabel.getShortDateFormat(getHashIndex().getDate());
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
|
@ -27,6 +27,7 @@ import org.controlsfx.validation.Validator;
|
|||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
@ -73,10 +74,42 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private Button create;
|
||||
|
||||
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
userFeeSet.set(true);
|
||||
setTargetBlocks(getTargetBlocks());
|
||||
updateTransaction();
|
||||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<Number> targetBlocksListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
Integer target = getTargetBlocks();
|
||||
|
||||
if(targetBlocksFeeRates != null) {
|
||||
setFeeRate(targetBlocksFeeRates.get(target));
|
||||
feeRatesChart.select(target);
|
||||
} else {
|
||||
feeRate.setText("Unknown");
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip("Target confirmation within " + target + " blocks");
|
||||
targetBlocks.setTooltip(tooltip);
|
||||
|
||||
userFeeSet.set(false);
|
||||
updateTransaction();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
|
@ -90,11 +123,21 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
updateTransaction();
|
||||
});
|
||||
|
||||
amount.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_FRACTION_DIGITS, 15).getFormatter());
|
||||
amountUnit.getSelectionModel().select(0);
|
||||
amount.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_FRACTION_DIGITS, 8).getFormatter());
|
||||
amount.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
updateTransaction();
|
||||
});
|
||||
|
||||
amountUnit.getSelectionModel().select(1);
|
||||
amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Long value = getRecipientValueSats(oldValue);
|
||||
if(value != null) {
|
||||
DecimalFormat df = new DecimalFormat("#.#");
|
||||
df.setMaximumFractionDigits(8);
|
||||
amount.setText(df.format(newValue.getValue(value)));
|
||||
}
|
||||
});
|
||||
|
||||
insufficientInputsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
String amt = amount.getText();
|
||||
amount.setText(amt + " ");
|
||||
|
@ -116,24 +159,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return (double)TARGET_BLOCKS_RANGE.indexOf(Integer.valueOf(string));
|
||||
}
|
||||
});
|
||||
targetBlocks.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
Integer target = getTargetBlocks();
|
||||
|
||||
if(targetBlocksFeeRates != null) {
|
||||
setFeeRate(targetBlocksFeeRates.get(target));
|
||||
feeRatesChart.select(target);
|
||||
} else {
|
||||
feeRate.setText("Unknown");
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip("Target confirmation within " + target + " blocks");
|
||||
targetBlocks.setTooltip(tooltip);
|
||||
|
||||
//TODO: Set fee based on tx size
|
||||
});
|
||||
|
||||
feeAmountUnit.getSelectionModel().select(1);
|
||||
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
||||
|
||||
feeRatesChart.initialize();
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
|
@ -145,23 +171,60 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
setTargetBlocks(5);
|
||||
|
||||
fee.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
updateTransaction();
|
||||
fee.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_FRACTION_DIGITS, 8).getFormatter());
|
||||
fee.textProperty().addListener(feeListener);
|
||||
|
||||
feeAmountUnit.getSelectionModel().select(1);
|
||||
feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Long value = getFeeValueSats(oldValue);
|
||||
if(value != null) {
|
||||
setFee(value);
|
||||
}
|
||||
});
|
||||
|
||||
walletTransactionProperty.addListener((observable, oldValue, newValue) -> {
|
||||
transactionDiagram.update(newValue);
|
||||
create.setDisable(false);
|
||||
userFeeSet.addListener((observable, oldValue, newValue) -> {
|
||||
feeRatesChart.select(0);
|
||||
|
||||
Node thumb = getSliderThumb();
|
||||
if(thumb != null) {
|
||||
if(newValue) {
|
||||
thumb.getStyleClass().add("inactive");
|
||||
} else {
|
||||
thumb.getStyleClass().remove("inactive");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||
if(walletTransaction != null) {
|
||||
double feeRate = (double)walletTransaction.getFee() / walletTransaction.getTransaction().getVirtualSize();
|
||||
if(userFeeSet.get()) {
|
||||
setTargetBlocks(getTargetBlocks(feeRate));
|
||||
} else {
|
||||
setFee(walletTransaction.getFee());
|
||||
}
|
||||
|
||||
setFeeRate(feeRate);
|
||||
}
|
||||
|
||||
transactionDiagram.update(walletTransaction);
|
||||
create.setDisable(walletTransaction == null);
|
||||
});
|
||||
|
||||
address.setText("32YSPMaUePf511u5adEckiNq8QLec9ksXX");
|
||||
}
|
||||
|
||||
private void addValidation() {
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.registerValidator(address, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !newValue.isEmpty() && !isValidAddress())
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !newValue.isEmpty() && !isValidRecipientAddress())
|
||||
));
|
||||
validationSupport.registerValidator(amount, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", insufficientInputsProperty.get())
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", insufficientInputsProperty.get()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() == 0)
|
||||
));
|
||||
validationSupport.registerValidator(fee, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() != null && getFeeValueSats() == 0)
|
||||
));
|
||||
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
@ -169,13 +232,14 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private void updateTransaction() {
|
||||
try {
|
||||
Address recipientAddress = getAddress();
|
||||
Long recipientAmount = getAmount();
|
||||
if(recipientAmount != null) {
|
||||
Address recipientAddress = getRecipientAddress();
|
||||
Long recipientAmount = getRecipientValueSats();
|
||||
if(recipientAmount != null && recipientAmount != 0 && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate());
|
||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate(), userFeeSet.get() ? getFeeValueSats() : null);
|
||||
walletTransactionProperty.setValue(walletTransaction);
|
||||
insufficientInputsProperty.set(false);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (InvalidAddressException e) {
|
||||
|
@ -192,9 +256,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return List.of(priorityUtxoSelector);
|
||||
}
|
||||
|
||||
private boolean isValidAddress() {
|
||||
private boolean isValidRecipientAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
getRecipientAddress();
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
|
@ -202,24 +266,30 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return true;
|
||||
}
|
||||
|
||||
private Address getAddress() throws InvalidAddressException {
|
||||
private Address getRecipientAddress() throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
|
||||
private Long getAmount() {
|
||||
BitcoinUnit bitcoinUnit = amountUnit.getSelectionModel().getSelectedItem();
|
||||
private Long getRecipientValueSats() {
|
||||
return getRecipientValueSats(amountUnit.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
|
||||
private Long getRecipientValueSats(BitcoinUnit bitcoinUnit) {
|
||||
if(amount.getText() != null && !amount.getText().isEmpty()) {
|
||||
Double fieldValue = Double.parseDouble(amount.getText());
|
||||
double fieldValue = Double.parseDouble(amount.getText());
|
||||
return bitcoinUnit.getSatsValue(fieldValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long getFee() {
|
||||
BitcoinUnit bitcoinUnit = feeAmountUnit.getSelectionModel().getSelectedItem();
|
||||
private Long getFeeValueSats() {
|
||||
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
|
||||
private Long getFeeValueSats(BitcoinUnit bitcoinUnit) {
|
||||
if(fee.getText() != null && !fee.getText().isEmpty()) {
|
||||
Double fieldValue = Double.parseDouble(amount.getText());
|
||||
double fieldValue = Double.parseDouble(fee.getText());
|
||||
return bitcoinUnit.getSatsValue(fieldValue);
|
||||
}
|
||||
|
||||
|
@ -231,10 +301,26 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return TARGET_BLOCKS_RANGE.get(index);
|
||||
}
|
||||
|
||||
private Integer getTargetBlocks(double feeRate) {
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
int maxTargetBlocks = 1;
|
||||
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
|
||||
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
|
||||
Double candidate = targetBlocksFeeRates.get(targetBlocks);
|
||||
if(feeRate > candidate) {
|
||||
return targetBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
return maxTargetBlocks;
|
||||
}
|
||||
|
||||
private void setTargetBlocks(Integer target) {
|
||||
targetBlocks.valueProperty().removeListener(targetBlocksListener);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(target);
|
||||
targetBlocks.setValue(index);
|
||||
feeRatesChart.select(target);
|
||||
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
||||
}
|
||||
|
||||
private Map<Integer, Double> getTargetBlocksFeeRates() {
|
||||
|
@ -254,6 +340,18 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte");
|
||||
}
|
||||
|
||||
private void setFee(long feeValue) {
|
||||
fee.textProperty().removeListener(feeListener);
|
||||
DecimalFormat df = new DecimalFormat("#.#");
|
||||
df.setMaximumFractionDigits(8);
|
||||
fee.setText(df.format(feeAmountUnit.getValue().getValue(feeValue)));
|
||||
fee.textProperty().addListener(feeListener);
|
||||
}
|
||||
|
||||
private Node getSliderThumb() {
|
||||
return targetBlocks.lookup(".thumb");
|
||||
}
|
||||
|
||||
public void setMaxInput(ActionEvent event) {
|
||||
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ public class TransactionHashIndexEntry extends HashIndexEntry {
|
|||
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();
|
||||
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();
|
||||
return (getKeyPurpose().equals(KeyPurpose.RECEIVE) ? "Received to " : "Change to ") + getHashIndex().getHash().toString().substring(0, 8) + "..:" + getHashIndex().getIndex();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ public class UtxoEntry extends HashIndexEntry {
|
|||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex();
|
||||
return getHashIndex().getHash().toString().substring(0, 8) + "..:" + getHashIndex().getIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -28,10 +28,32 @@
|
|||
-fx-background-color: rgba(30, 136, 207, 0.6);
|
||||
}
|
||||
|
||||
.inactive {
|
||||
-fx-opacity: 0.3;
|
||||
}
|
||||
|
||||
#feeRateField .input-container {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
#transactionDiagram {
|
||||
-fx-min-height: 230px;
|
||||
}
|
||||
|
||||
#transactionDiagram .y-axis {
|
||||
-fx-stroke: transparent;
|
||||
}
|
||||
|
||||
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip {
|
||||
-fx-font-family: Courier;
|
||||
}
|
||||
|
||||
#transactionDiagram .fee-warning-icon {
|
||||
-fx-text-fill: rgb(202, 18, 67);
|
||||
}
|
||||
|
||||
#transactionDiagram .input-line, #transactionDiagram .output-line {
|
||||
-fx-fill: transparent;
|
||||
-fx-stroke: #696c77;
|
||||
-fx-stroke-width: 1px;
|
||||
}
|
Loading…
Reference in a new issue