mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-11 00:47:41 +00:00
send controller initial, fee rates support
This commit is contained in:
@@ -88,10 +88,12 @@ public class AppController implements Initializable {
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
public static Integer currentBlockHeight;
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
public static boolean showTxHexProperty;
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -342,6 +344,10 @@ public class AppController implements Initializable {
|
||||
return currentBlockHeight;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public static void showErrorDialog(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
@@ -764,6 +770,7 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
String banner = event.getServerBanner();
|
||||
String status = "Connected: " + (banner == null ? "Server" : banner.split(System.lineSeparator(), 2)[0]) + " at height " + event.getBlockHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
@@ -783,6 +790,11 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void viewTransaction(ViewTransactionEvent event) {
|
||||
Tab tab = addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,11 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
}
|
||||
|
||||
public void select(Entry entry) {
|
||||
Node selectedBar = lookup(".chart-bar.selected");
|
||||
if(selectedBar != null) {
|
||||
selectedBar.getStyleClass().remove("selected");
|
||||
}
|
||||
|
||||
for(int i = 0; i < utxoSeries.getData().size(); i++) {
|
||||
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
|
||||
Node bar = lookup(".data" + i);
|
||||
@@ -71,8 +76,6 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
if(data.getExtraValue() != null && data.getExtraValue().equals(entry)) {
|
||||
bar.getStyleClass().add("selected");
|
||||
this.selectedEntry = entry;
|
||||
} else {
|
||||
bar.getStyleClass().remove("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConnectionEvent {
|
||||
public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
private final List<String> serverVersion;
|
||||
private final String serverBanner;
|
||||
private final int blockHeight;
|
||||
private final BlockHeader blockHeader;
|
||||
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader) {
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates) {
|
||||
super(targetBlockFeeRates);
|
||||
this.serverVersion = serverVersion;
|
||||
this.serverBanner = serverBanner;
|
||||
this.blockHeight = blockHeight;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,9 @@ import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
||||
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -505,6 +507,23 @@ public class ElectrumServer {
|
||||
return transactionMap;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||
BatchRequestBuilder<Integer, Double> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(Double.class);
|
||||
for(Integer targetBlock : targetBlocks) {
|
||||
batchRequest.add(targetBlock, "blockchain.estimatefee", targetBlock);
|
||||
}
|
||||
|
||||
Map<Integer, Double> targetBlocksFeeRatesBtcKb = batchRequest.execute();
|
||||
|
||||
Map<Integer, Double> targetBlocksFeeRatesSats = new TreeMap<>();
|
||||
for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) {
|
||||
targetBlocksFeeRatesSats.put(target, targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN / 1024);
|
||||
}
|
||||
|
||||
return targetBlocksFeeRatesSats;
|
||||
}
|
||||
|
||||
private String getScriptHash(Wallet wallet, WalletNode node) {
|
||||
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
||||
byte[] reversed = Utils.reverseBytes(hash);
|
||||
@@ -802,15 +821,18 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionService extends ScheduledService<ConnectionEvent> implements Thread.UncaughtExceptionHandler {
|
||||
public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler {
|
||||
private static final int FEE_RATES_PERIOD = 5 * 60 * 1000;
|
||||
|
||||
private boolean firstCall = true;
|
||||
private Thread reader;
|
||||
private Throwable lastReaderException;
|
||||
private long feeRatesRetrievedAt;
|
||||
|
||||
@Override
|
||||
protected Task<ConnectionEvent> createTask() {
|
||||
protected Task<FeeRatesUpdatedEvent> createTask() {
|
||||
return new Task<>() {
|
||||
protected ConnectionEvent call() throws ServerException {
|
||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
if(firstCall) {
|
||||
electrumServer.connect();
|
||||
@@ -826,10 +848,20 @@ public class ElectrumServer {
|
||||
BlockHeaderTip tip = electrumServer.subscribeBlockHeaders();
|
||||
String banner = electrumServer.getServerBanner();
|
||||
|
||||
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader());
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
|
||||
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader(), blockTargetFeeRates);
|
||||
} else {
|
||||
if(reader.isAlive()) {
|
||||
electrumServer.ping();
|
||||
|
||||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||
if(elapsed > FEE_RATES_PERIOD) {
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates);
|
||||
}
|
||||
} else {
|
||||
firstCall = true;
|
||||
throw new ServerException("Connection to server failed", lastReaderException);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user