13 changed files with 462 additions and 17 deletions
@ -1 +1 @@ |
|||||
Subproject commit 24cde9d073da636fbc2150b7abbd50b48342e040 |
Subproject commit c4dd1cb9dd40a7a16829a00f45acbd55f63d9895 |
@ -0,0 +1,35 @@ |
|||||
|
package com.sparrowwallet.sparrow; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.protocol.Transaction; |
||||
|
|
||||
|
public enum BitcoinUnit { |
||||
|
BTC("BTC") { |
||||
|
@Override |
||||
|
public long getSatsValue(double unitValue) { |
||||
|
return (long)(unitValue * Transaction.SATOSHIS_PER_BITCOIN); |
||||
|
} |
||||
|
}, |
||||
|
SATOSHIS("sats") { |
||||
|
@Override |
||||
|
public long getSatsValue(double unitValue) { |
||||
|
return (long)unitValue; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private final String label; |
||||
|
|
||||
|
BitcoinUnit(String label) { |
||||
|
this.label = label; |
||||
|
} |
||||
|
|
||||
|
public String getLabel() { |
||||
|
return label; |
||||
|
} |
||||
|
|
||||
|
public abstract long getSatsValue(double unitValue); |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return label; |
||||
|
} |
||||
|
} |
@ -0,0 +1,54 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import javafx.beans.NamedArg; |
||||
|
import javafx.scene.Node; |
||||
|
import javafx.scene.chart.Axis; |
||||
|
import javafx.scene.chart.LineChart; |
||||
|
import javafx.scene.chart.XYChart; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class FeeRatesChart extends LineChart<String, Number> { |
||||
|
private XYChart.Series<String, Number> feeRateSeries; |
||||
|
private Integer selectedTargetBlocks; |
||||
|
|
||||
|
public FeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) { |
||||
|
super(xAxis, yAxis); |
||||
|
} |
||||
|
|
||||
|
public void initialize() { |
||||
|
feeRateSeries = new XYChart.Series<>(); |
||||
|
getData().add(feeRateSeries); |
||||
|
} |
||||
|
|
||||
|
public void update(Map<Integer, Double> targetBlocksFeeRates) { |
||||
|
feeRateSeries.getData().clear(); |
||||
|
|
||||
|
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) { |
||||
|
XYChart.Data<String, Number> data = new XYChart.Data<>(Integer.toString(targetBlocks), targetBlocksFeeRates.get(targetBlocks)); |
||||
|
feeRateSeries.getData().add(data); |
||||
|
} |
||||
|
|
||||
|
if(selectedTargetBlocks != null) { |
||||
|
select(selectedTargetBlocks); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void select(Integer targetBlocks) { |
||||
|
Node selectedSymbol = lookup(".chart-line-symbol.selected"); |
||||
|
if(selectedSymbol != null) { |
||||
|
selectedSymbol.getStyleClass().remove("selected"); |
||||
|
} |
||||
|
|
||||
|
for(int i = 0; i < feeRateSeries.getData().size(); i++) { |
||||
|
XYChart.Data<String, Number> data = feeRateSeries.getData().get(i); |
||||
|
Node symbol = lookup(".chart-line-symbol.data" + i); |
||||
|
if(symbol != null) { |
||||
|
if(data.getXValue().equals(targetBlocks.toString())) { |
||||
|
symbol.getStyleClass().add("selected"); |
||||
|
selectedTargetBlocks = targetBlocks; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package com.sparrowwallet.sparrow.event; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class FeeRatesUpdatedEvent { |
||||
|
private final Map<Integer, Double> targetBlockFeeRates; |
||||
|
|
||||
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates) { |
||||
|
this.targetBlockFeeRates = targetBlockFeeRates; |
||||
|
} |
||||
|
|
||||
|
public Map<Integer, Double> getTargetBlockFeeRates() { |
||||
|
return targetBlockFeeRates; |
||||
|
} |
||||
|
} |
@ -0,0 +1,164 @@ |
|||||
|
package com.sparrowwallet.sparrow.wallet; |
||||
|
|
||||
|
import com.google.common.eventbus.Subscribe; |
||||
|
import com.sparrowwallet.drongo.address.Address; |
||||
|
import com.sparrowwallet.drongo.address.InvalidAddressException; |
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
import com.sparrowwallet.sparrow.AppController; |
||||
|
import com.sparrowwallet.sparrow.BitcoinUnit; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.control.CopyableLabel; |
||||
|
import com.sparrowwallet.sparrow.control.CopyableTextField; |
||||
|
import com.sparrowwallet.sparrow.control.FeeRatesChart; |
||||
|
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; |
||||
|
import javafx.beans.value.ChangeListener; |
||||
|
import javafx.beans.value.ObservableValue; |
||||
|
import javafx.event.ActionEvent; |
||||
|
import javafx.fxml.FXML; |
||||
|
import javafx.fxml.Initializable; |
||||
|
import javafx.scene.control.*; |
||||
|
import javafx.util.StringConverter; |
||||
|
import org.controlsfx.validation.ValidationResult; |
||||
|
import org.controlsfx.validation.ValidationSupport; |
||||
|
import org.controlsfx.validation.Validator; |
||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration; |
||||
|
|
||||
|
import java.net.URL; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.ResourceBundle; |
||||
|
|
||||
|
public class SendController extends WalletFormController implements Initializable { |
||||
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50); |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableTextField address; |
||||
|
|
||||
|
@FXML |
||||
|
private TextField label; |
||||
|
|
||||
|
@FXML |
||||
|
private TextField amount; |
||||
|
|
||||
|
@FXML |
||||
|
private ComboBox<BitcoinUnit> amountUnit; |
||||
|
|
||||
|
@FXML |
||||
|
private Slider targetBlocks; |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableLabel feeRate; |
||||
|
|
||||
|
@FXML |
||||
|
private TextField fee; |
||||
|
|
||||
|
@FXML |
||||
|
private ComboBox<BitcoinUnit> feeAmountUnit; |
||||
|
|
||||
|
@FXML |
||||
|
private FeeRatesChart feeRatesChart; |
||||
|
|
||||
|
@Override |
||||
|
public void initialize(URL location, ResourceBundle resources) { |
||||
|
EventManager.get().register(this); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void initializeView() { |
||||
|
ValidationSupport validationSupport = new ValidationSupport(); |
||||
|
validationSupport.registerValidator(address, Validator.combine( |
||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !isValidAddress()) |
||||
|
)); |
||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); |
||||
|
|
||||
|
amountUnit.getSelectionModel().select(0); |
||||
|
|
||||
|
targetBlocks.setMin(0); |
||||
|
targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1); |
||||
|
targetBlocks.setMajorTickUnit(1); |
||||
|
targetBlocks.setMinorTickCount(0); |
||||
|
targetBlocks.setLabelFormatter(new StringConverter<Double>() { |
||||
|
@Override |
||||
|
public String toString(Double object) { |
||||
|
return Integer.toString(TARGET_BLOCKS_RANGE.get(object.intValue())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Double fromString(String string) { |
||||
|
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); |
||||
|
|
||||
|
feeRatesChart.initialize(); |
||||
|
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates(); |
||||
|
if(targetBlocksFeeRates != null) { |
||||
|
feeRatesChart.update(targetBlocksFeeRates); |
||||
|
} else { |
||||
|
feeRate.setText("Unknown"); |
||||
|
} |
||||
|
|
||||
|
setTargetBlocks(5); |
||||
|
} |
||||
|
|
||||
|
private boolean isValidAddress() { |
||||
|
try { |
||||
|
getAddress(); |
||||
|
} catch (InvalidAddressException e) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private Address getAddress() throws InvalidAddressException { |
||||
|
return Address.fromString(address.getText()); |
||||
|
} |
||||
|
|
||||
|
private Integer getTargetBlocks() { |
||||
|
int index = (int)targetBlocks.getValue(); |
||||
|
return TARGET_BLOCKS_RANGE.get(index); |
||||
|
} |
||||
|
|
||||
|
private void setTargetBlocks(Integer target) { |
||||
|
int index = TARGET_BLOCKS_RANGE.indexOf(target); |
||||
|
targetBlocks.setValue(index); |
||||
|
feeRatesChart.select(target); |
||||
|
} |
||||
|
|
||||
|
private Map<Integer, Double> getTargetBlocksFeeRates() { |
||||
|
return AppController.getTargetBlockFeeRates(); |
||||
|
} |
||||
|
|
||||
|
private void setFeeRate(Double feeRateAmt) { |
||||
|
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); |
||||
|
} |
||||
|
|
||||
|
public void setMaxInput(ActionEvent event) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Subscribe |
||||
|
public void feeRatesUpdated(FeeRatesUpdatedEvent event) { |
||||
|
feeRatesChart.update(event.getTargetBlockFeeRates()); |
||||
|
feeRatesChart.select(getTargetBlocks()); |
||||
|
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks())); |
||||
|
} |
||||
|
} |
@ -0,0 +1,33 @@ |
|||||
|
.form .fieldset:horizontal .field { |
||||
|
-fx-pref-height: 40px; |
||||
|
} |
||||
|
|
||||
|
.send-form .form .fieldset:horizontal .label-container { |
||||
|
-fx-pref-width: 90px; |
||||
|
} |
||||
|
|
||||
|
.amount-field { |
||||
|
-fx-max-width: 150px; |
||||
|
} |
||||
|
|
||||
|
#feeRatesChart { |
||||
|
-fx-max-width: 350px; |
||||
|
-fx-max-height: 130px; |
||||
|
} |
||||
|
|
||||
|
.default-color0.chart-series-line { |
||||
|
-fx-stroke: rgba(105, 108, 119, 0.6); |
||||
|
-fx-stroke-width: 1px; |
||||
|
} |
||||
|
|
||||
|
.chart-line-symbol { |
||||
|
-fx-background-color: rgba(30, 136, 207, 0); |
||||
|
} |
||||
|
|
||||
|
.chart-line-symbol.selected { |
||||
|
-fx-background-color: rgba(30, 136, 207, 0.6); |
||||
|
} |
||||
|
|
||||
|
#feeRateField .input-container { |
||||
|
-fx-alignment: center-left; |
||||
|
} |
@ -0,0 +1,92 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
|
||||
|
<?import java.lang.*?> |
||||
|
<?import java.util.*?> |
||||
|
<?import javafx.scene.*?> |
||||
|
<?import javafx.scene.control.*?> |
||||
|
<?import javafx.scene.layout.*?> |
||||
|
|
||||
|
<?import tornadofx.control.Fieldset?> |
||||
|
<?import tornadofx.control.Form?> |
||||
|
<?import tornadofx.control.Field?> |
||||
|
<?import com.sparrowwallet.sparrow.control.CopyableTextField?> |
||||
|
|
||||
|
<?import javafx.geometry.Insets?> |
||||
|
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> |
||||
|
<?import javafx.collections.FXCollections?> |
||||
|
<?import com.sparrowwallet.drongo.policy.PolicyType?> |
||||
|
<?import com.sparrowwallet.sparrow.BitcoinUnit?> |
||||
|
<?import com.sparrowwallet.sparrow.control.FeeRatesChart?> |
||||
|
<?import javafx.scene.chart.CategoryAxis?> |
||||
|
<?import javafx.scene.chart.NumberAxis?> |
||||
|
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController"> |
||||
|
<center> |
||||
|
<GridPane styleClass="send-form" hgap="10.0" vgap="10.0"> |
||||
|
<padding> |
||||
|
<Insets left="25.0" right="25.0" top="25.0" /> |
||||
|
</padding> |
||||
|
<columnConstraints> |
||||
|
<ColumnConstraints percentWidth="50" /> |
||||
|
<ColumnConstraints percentWidth="30" /> |
||||
|
<ColumnConstraints percentWidth="20" /> |
||||
|
</columnConstraints> |
||||
|
<rowConstraints> |
||||
|
<RowConstraints /> |
||||
|
</rowConstraints> |
||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2"> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Send"> |
||||
|
<Field text="Pay to:"> |
||||
|
<CopyableTextField fx:id="address" styleClass="address-text-field"/> |
||||
|
</Field> |
||||
|
<Field text="Label:"> |
||||
|
<TextField fx:id="label" /> |
||||
|
</Field> |
||||
|
<Field text="Amount:"> |
||||
|
<TextField fx:id="amount" styleClass="amount-field" /> |
||||
|
<ComboBox fx:id="amountUnit"> |
||||
|
<items> |
||||
|
<FXCollections fx:factory="observableArrayList"> |
||||
|
<BitcoinUnit fx:constant="BTC" /> |
||||
|
<BitcoinUnit fx:constant="SATOSHIS" /> |
||||
|
</FXCollections> |
||||
|
</items> |
||||
|
</ComboBox> |
||||
|
<Region style="-fx-pref-width: 20" /> |
||||
|
<Button fx:id="maxButton" text="Max" onAction="#setMaxInput" /> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
</Form> |
||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="1"> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Fee"> |
||||
|
<Field text="Block Target:"> |
||||
|
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" /> |
||||
|
</Field> |
||||
|
<Field fx:id="feeRateField" text="Rate:"> |
||||
|
<CopyableLabel fx:id="feeRate" /> |
||||
|
</Field> |
||||
|
<Field text="Fee:"> |
||||
|
<TextField fx:id="fee" styleClass="amount-field"/> |
||||
|
<ComboBox fx:id="feeAmountUnit"> |
||||
|
<items> |
||||
|
<FXCollections fx:factory="observableArrayList"> |
||||
|
<BitcoinUnit fx:constant="BTC" /> |
||||
|
<BitcoinUnit fx:constant="SATOSHIS" /> |
||||
|
</FXCollections> |
||||
|
</items> |
||||
|
</ComboBox> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
</Form> |
||||
|
<AnchorPane GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2"> |
||||
|
<FeeRatesChart fx:id="feeRatesChart" legendVisible="false" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="20" animated="false"> |
||||
|
<xAxis> |
||||
|
<CategoryAxis side="BOTTOM" /> |
||||
|
</xAxis> |
||||
|
<yAxis> |
||||
|
<NumberAxis side="LEFT" /> |
||||
|
</yAxis> |
||||
|
</FeeRatesChart> |
||||
|
</AnchorPane> |
||||
|
</GridPane> |
||||
|
</center> |
||||
|
</BorderPane> |
Loading…
Reference in new issue