diff --git a/drongo b/drongo index ccf7de9f..2ff9c94c 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit ccf7de9f625c4cc73efc6948b3e699a7786da276 +Subproject commit 2ff9c94c62d1f20e408f89d7a41e2e66426b8634 diff --git a/src/main/java/com/sparrowwallet/sparrow/BitcoinUnit.java b/src/main/java/com/sparrowwallet/sparrow/BitcoinUnit.java index 6cf0d01e..56319cca 100644 --- a/src/main/java/com/sparrowwallet/sparrow/BitcoinUnit.java +++ b/src/main/java/com/sparrowwallet/sparrow/BitcoinUnit.java @@ -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; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java b/src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java index ce7ad824..d243da10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java @@ -33,6 +33,10 @@ class LabelCell extends TextFieldTreeTableCell { @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 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java b/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java index b4a42681..2da19c1b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java @@ -38,8 +38,8 @@ public class TextFieldValidator { return new TextFieldValidator(integersOnlyPattern()); } - public TextFormatter getFormatter() { - return new TextFormatter<>(this::validateChange); + public TextFormatter getFormatter() { + return new TextFormatter<>(TextFormatter.IDENTITY_STRING_CONVERTER, "", this::validateChange); } private Change validateChange(Change c) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 7a315009..d341b29a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -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 selectedUtxos, Address toAddress, WalletNode changeNode, long fee) { - Pane inputsPane = getInputsLabels(selectedUtxos); + public void update() { + Map 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 selectedUtxos) { + private Map getDisplayedUtxos() { + Map selectedUtxos = walletTx.getSelectedUtxos(); + + if(selectedUtxos.size() > MAX_UTXOS) { + Map utxos = new LinkedHashMap<>(); + List 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 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 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 additionalInputs; + + public AdditionalBlockTransactionHashIndex(List additionalInputs) { + super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0); + this.additionalInputs = additionalInputs; + } + + @Override + public String getLabel() { + return additionalInputs.size() + " more"; + } + + public List getAdditionalInputs() { + return additionalInputs; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 8080aaa3..fea5d907 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -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'), diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java index 230c3e38..8aed3db5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java @@ -53,7 +53,7 @@ public class HashIndexEntry extends Entry implements Comparable 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()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 56f7b2c0..204f8219 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -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 walletTransactionProperty = new SimpleObjectProperty<>(null); private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false); + private final ChangeListener feeListener = new ChangeListener<>() { + @Override + public void changed(ObservableValue observable, String oldValue, String newValue) { + userFeeSet.set(true); + setTargetBlocks(getTargetBlocks()); + updateTransaction(); + } + }; + + private final ChangeListener targetBlocksListener = new ChangeListener<>() { + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + Map 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 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 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 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 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) { } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionHashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionHashIndexEntry.java index 3ef6bc67..bcce85e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionHashIndexEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionHashIndexEntry.java @@ -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(); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java index 4b050088..e0fa8eea 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java @@ -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 diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css index 5fe202c9..50a06e84 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css @@ -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; } \ No newline at end of file