Browse Source

send pane improvements

bwt
Craig Raw 4 years ago
parent
commit
66e558faee
  1. 2
      drongo
  2. 15
      src/main/java/com/sparrowwallet/sparrow/BitcoinUnit.java
  3. 4
      src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java
  4. 4
      src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java
  5. 269
      src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
  6. 1
      src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java
  7. 2
      src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java
  8. 180
      src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
  9. 4
      src/main/java/com/sparrowwallet/sparrow/wallet/TransactionHashIndexEntry.java
  10. 2
      src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java
  11. 22
      src/main/resources/com/sparrowwallet/sparrow/wallet/send.css

2
drongo

@ -1 +1 @@
Subproject commit ccf7de9f625c4cc73efc6948b3e699a7786da276
Subproject commit 2ff9c94c62d1f20e408f89d7a41e2e66426b8634

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

4
src/main/java/com/sparrowwallet/sparrow/control/LabelCell.java

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

4
src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java

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

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

1
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'),

2
src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java

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

180
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<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);
}
});
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, newValue) -> {
transactionDiagram.update(newValue);
create.setDisable(false);
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) {
}

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

2
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

22
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;
}
Loading…
Cancel
Save