mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add mempool size fee selection
This commit is contained in:
parent
43bf6ab265
commit
2b55b5feb3
16 changed files with 477 additions and 47 deletions
|
@ -25,6 +25,7 @@ import com.sparrowwallet.sparrow.control.*;
|
|||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.net.VersionCheckService;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
|
@ -68,6 +69,9 @@ import java.io.*;
|
|||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -75,7 +79,7 @@ import java.util.stream.Collectors;
|
|||
public class AppController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||
|
||||
private static final int SERVER_PING_PERIOD = 8 * 60 * 1000;
|
||||
private static final int SERVER_PING_PERIOD = 1 * 60 * 1000;
|
||||
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
|
||||
private static final int RATES_PERIOD = 5 * 60 * 1000;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
|
@ -143,7 +147,7 @@ public class AppController implements Initializable {
|
|||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static Map<Long, Long> feeRateHistogram;
|
||||
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
|
@ -151,7 +155,7 @@ public class AppController implements Initializable {
|
|||
|
||||
private static List<Device> devices;
|
||||
|
||||
private static Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
|
||||
private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
@ -652,8 +656,17 @@ public class AppController implements Initializable {
|
|||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public static Map<Long, Long> getFeeRateHistogram() {
|
||||
return feeRateHistogram;
|
||||
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
return mempoolHistogram;
|
||||
}
|
||||
|
||||
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
|
||||
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||
if(mempoolHistogram.isEmpty()) {
|
||||
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
}
|
||||
|
||||
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
|
@ -1435,7 +1448,7 @@ public class AppController implements Initializable {
|
|||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
feeRateHistogram = event.getFeeRateHistogram();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
String banner = event.getServerBanner();
|
||||
String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight();
|
||||
|
@ -1460,7 +1473,7 @@ public class AppController implements Initializable {
|
|||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
feeRateHistogram = event.getFeeRateHistogram();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
@ -9,11 +9,11 @@ import javafx.scene.chart.XYChart;
|
|||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public class FeeRatesChart extends LineChart<String, Number> {
|
||||
public class BlockTargetFeeRatesChart 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) {
|
||||
public BlockTargetFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
setCreateSymbols(false);
|
||||
setCursor(Cursor.CROSSHAIR);
|
||||
setVerticalGridLinesVisible(false);
|
||||
tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.ZERO);
|
||||
tooltip.setHideDelay(Duration.ZERO);
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
Platform.runLater(() -> {
|
||||
Node node = this.lookup(".plot-content");
|
||||
Tooltip.install(node, tooltip);
|
||||
});
|
||||
}
|
||||
|
||||
public void update(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||
getData().clear();
|
||||
if(tooltip.isShowing()) {
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
Map<Date, Set<MempoolRateSize>> periodRateSizes = getPeriodRateSizes(mempoolRateSizes);
|
||||
List<String> categories = getCategories(periodRateSizes);
|
||||
|
||||
CategoryAxis categoryAxis = (CategoryAxis)getXAxis();
|
||||
if(categoryAxis.getCategories() == null) {
|
||||
categoryAxis.setCategories(FXCollections.observableArrayList(categories));
|
||||
} else {
|
||||
categoryAxis.getCategories().retainAll(categories);
|
||||
categories.removeAll(categoryAxis.getCategories());
|
||||
categoryAxis.getCategories().addAll(categories);
|
||||
}
|
||||
|
||||
categoryAxis.setGapStartAndEnd(false);
|
||||
categoryAxis.setOnMouseMoved(mouseEvent -> {
|
||||
String category = categoryAxis.getValueForDisplay(mouseEvent.getX());
|
||||
if(category != null) {
|
||||
tooltip.setGraphic(new ChartTooltip(category, getData()));
|
||||
}
|
||||
});
|
||||
|
||||
NumberAxis numberAxis = (NumberAxis)getYAxis();
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
||||
long totalVSize = 0;
|
||||
for(MempoolRateSize rateSize : rateSizes) {
|
||||
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
|
||||
totalVSize += rateSize.getVSize();
|
||||
}
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<>(dateFormatter.format(date), totalVSize));
|
||||
}
|
||||
|
||||
previousFeeRate = feeRate;
|
||||
getData().add(series);
|
||||
}
|
||||
|
||||
if(categories.iterator().hasNext()) {
|
||||
tooltip.setGraphic(new ChartTooltip(categories.iterator().next(), getData()));
|
||||
numberAxis.setTickLabelsVisible(true);
|
||||
numberAxis.setOpacity(1);
|
||||
} else {
|
||||
numberAxis.setTickLabelsVisible(false);
|
||||
numberAxis.setOpacity(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||
if(mempoolRateSizes.size() == 1) {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
|
||||
LocalDateTime period = LocalDateTime.now().minusHours(6);
|
||||
return mempoolRateSizes.entrySet().stream().filter(entry -> {
|
||||
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
return dateTime.isAfter(period);
|
||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate dates"); },
|
||||
TreeMap::new));
|
||||
}
|
||||
|
||||
private List<String> getCategories(Map<Date, Set<MempoolRateSize>> mempoolHistogram) {
|
||||
List<String> categories = new ArrayList<>();
|
||||
for(Date date : mempoolHistogram.keySet()) {
|
||||
categories.add(dateFormatter.format(date));
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static class ChartTooltip extends VBox {
|
||||
public ChartTooltip(String category, List<Series<String, Number>> seriesList) {
|
||||
Label title = new Label("At " + category);
|
||||
HBox titleBox = new HBox(title);
|
||||
title.getStyleClass().add("tooltip-title");
|
||||
getChildren().add(titleBox);
|
||||
|
||||
for(int i = seriesList.size() - 1; i >= 0; i--) {
|
||||
Series<String, Number> series = seriesList.get(i);
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
if(data.getXValue().equals(category)) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
if(mvb >= 0.01) {
|
||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
circle.getStyleClass().add("tooltip-series" + i);
|
||||
label.setGraphic(circle);
|
||||
getChildren().add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
private final List<String> serverVersion;
|
||||
|
@ -12,8 +14,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
|||
private final BlockHeader blockHeader;
|
||||
private final Double minimumRelayFeeRate;
|
||||
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Map<Long, Long> feeRateHistogram, Double minimumRelayFeeRate) {
|
||||
super(targetBlockFeeRates, feeRateHistogram);
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||
super(targetBlockFeeRates, mempoolRateSizes);
|
||||
this.serverVersion = serverVersion;
|
||||
this.serverBanner = serverBanner;
|
||||
this.blockHeight = blockHeight;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
|
||||
public class FeeRateSelectionChangedEvent {
|
||||
private final FeeRateSelection feeRateSelection;
|
||||
|
||||
public FeeRateSelectionChangedEvent(FeeRateSelection feeRateSelection) {
|
||||
this.feeRateSelection = feeRateSelection;
|
||||
}
|
||||
|
||||
public FeeRateSelection getFeeRateSelection() {
|
||||
return feeRateSelection;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,24 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FeeRatesUpdatedEvent {
|
||||
private final Map<Integer, Double> targetBlockFeeRates;
|
||||
private final Map<Long, Long> feeRateHistogram;
|
||||
private final Set<MempoolRateSize> mempoolRateSizes;
|
||||
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Map<Long, Long> feeRateHistogram) {
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||
this.feeRateHistogram = feeRateHistogram;
|
||||
this.mempoolRateSizes = mempoolRateSizes;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public Map<Long, Long> getFeeRateHistogram() {
|
||||
return feeRateHistogram;
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.google.gson.*;
|
|||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -19,6 +20,7 @@ public class Config {
|
|||
|
||||
private Mode mode;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private FeeRateSelection feeRateSelection;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean groupByAddress = true;
|
||||
|
@ -98,6 +100,15 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public FeeRateSelection getFeeRateSelection() {
|
||||
return feeRateSelection;
|
||||
}
|
||||
|
||||
public void setFeeRateSelection(FeeRateSelection feeRateSelection) {
|
||||
this.feeRateSelection = feeRateSelection;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Currency getFiatCurrency() {
|
||||
return fiatCurrency;
|
||||
}
|
||||
|
|
|
@ -610,8 +610,14 @@ public class ElectrumServer {
|
|||
}
|
||||
}
|
||||
|
||||
public Map<Long, Long> getFeeRateHistogram() throws ServerException {
|
||||
return electrumServerRpc.getFeeRateHistogram(getTransport());
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() throws ServerException {
|
||||
Map<Long, Long> feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport());
|
||||
Set<MempoolRateSize> mempoolRateSizes = new TreeSet<>();
|
||||
for(Long fee : feeRateHistogram.keySet()) {
|
||||
mempoolRateSizes.add(new MempoolRateSize(fee, feeRateHistogram.get(fee)));
|
||||
}
|
||||
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
|
||||
public Double getMinimumRelayFee() throws ServerException {
|
||||
|
@ -712,7 +718,7 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler {
|
||||
private static final int FEE_RATES_PERIOD = 10 * 60 * 1000;
|
||||
private static final int FEE_RATES_PERIOD = 1 * 60 * 1000;
|
||||
|
||||
private final boolean subscribe;
|
||||
private boolean firstCall = true;
|
||||
|
@ -764,7 +770,7 @@ public class ElectrumServer {
|
|||
String banner = electrumServer.getServerBanner();
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
||||
Map<Long, Long> feeRateHistogram = electrumServer.getFeeRateHistogram();
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
|
||||
Double minimumRelayFeeRate = electrumServer.getMinimumRelayFee();
|
||||
|
@ -772,7 +778,7 @@ public class ElectrumServer {
|
|||
blockTargetFeeRates.computeIfPresent(blockTarget, (blocks, feeRate) -> feeRate < minimumRelayFeeRate ? minimumRelayFeeRate : feeRate);
|
||||
}
|
||||
|
||||
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader(), blockTargetFeeRates, feeRateHistogram, minimumRelayFeeRate);
|
||||
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader(), blockTargetFeeRates, mempoolRateSizes, minimumRelayFeeRate);
|
||||
} else {
|
||||
if(reader.isAlive()) {
|
||||
electrumServer.ping();
|
||||
|
@ -780,9 +786,9 @@ public class ElectrumServer {
|
|||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||
if(elapsed > FEE_RATES_PERIOD) {
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
||||
Map<Long, Long> feeRateHistogram = electrumServer.getFeeRateHistogram();
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, feeRateHistogram);
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
}
|
||||
} else {
|
||||
resetConnection();
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
||||
private final long fee;
|
||||
private final long vSize;
|
||||
|
||||
public MempoolRateSize(long fee, long vSize) {
|
||||
this.fee = fee;
|
||||
this.vSize = vSize;
|
||||
}
|
||||
|
||||
public long getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
public long getVSize() {
|
||||
return vSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MempoolRateSize that = (MempoolRateSize) o;
|
||||
return fee == that.fee;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fee);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MempoolRateSize other) {
|
||||
return Long.compare(fee, other.fee);
|
||||
}
|
||||
}
|
|
@ -4,10 +4,12 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
|||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.FeeRateSelectionChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||
import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
|
@ -24,6 +26,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
|||
@FXML
|
||||
private ComboBox<BitcoinUnit> bitcoinUnit;
|
||||
|
||||
@FXML
|
||||
private ComboBox<FeeRateSelection> feeRateSelection;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Currency> fiatCurrency;
|
||||
|
||||
|
@ -63,6 +68,18 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
|||
EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
|
||||
});
|
||||
|
||||
if(config.getFeeRateSelection() != null) {
|
||||
feeRateSelection.setValue(config.getFeeRateSelection());
|
||||
} else {
|
||||
feeRateSelection.getSelectionModel().select(0);
|
||||
config.setFeeRateSelection(FeeRateSelection.BLOCK_TARGET);
|
||||
}
|
||||
|
||||
feeRateSelection.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setFeeRateSelection(newValue);
|
||||
EventManager.get().post(new FeeRateSelectionChangedEvent(newValue));
|
||||
});
|
||||
|
||||
if(config.getExchangeSource() != null) {
|
||||
exchangeSource.setValue(config.getExchangeSource());
|
||||
} else {
|
||||
|
|
|
@ -49,7 +49,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
|
|||
}
|
||||
|
||||
dialogPane.setPrefWidth(650);
|
||||
dialogPane.setPrefHeight(500);
|
||||
dialogPane.setPrefHeight(550);
|
||||
|
||||
existingConnection = ElectrumServer.isConnected();
|
||||
setOnCloseRequest(event -> {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
public enum FeeRateSelection {
|
||||
BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size");
|
||||
|
||||
private final String name;
|
||||
|
||||
private FeeRateSelection(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -2,11 +2,8 @@ package com.sparrowwallet.sparrow.wallet;
|
|||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
|
@ -17,6 +14,7 @@ import com.sparrowwallet.sparrow.event.*;
|
|||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
|
@ -36,6 +34,7 @@ import org.controlsfx.validation.Validator;
|
|||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
@ -49,15 +48,25 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private static final Logger log = LoggerFactory.getLogger(SendController.class);
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50, 100, 500);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
|
||||
@FXML
|
||||
private TabPane paymentTabs;
|
||||
|
||||
@FXML
|
||||
private Field targetBlocksField;
|
||||
|
||||
@FXML
|
||||
private Slider targetBlocks;
|
||||
|
||||
@FXML
|
||||
private Field feeRangeField;
|
||||
|
||||
@FXML
|
||||
private Slider feeRange;
|
||||
|
||||
@FXML
|
||||
private CopyableLabel feeRate;
|
||||
|
||||
|
@ -71,7 +80,10 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private FiatLabel fiatFeeAmount;
|
||||
|
||||
@FXML
|
||||
private FeeRatesChart feeRatesChart;
|
||||
private BlockTargetFeeRatesChart blockTargetFeeRatesChart;
|
||||
|
||||
@FXML
|
||||
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
@ -121,7 +133,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
if(targetBlocksFeeRates != null) {
|
||||
setFeeRate(targetBlocksFeeRates.get(target));
|
||||
feeRatesChart.select(target);
|
||||
blockTargetFeeRatesChart.select(target);
|
||||
} else {
|
||||
feeRate.setText("Unknown");
|
||||
}
|
||||
|
@ -138,6 +150,19 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<Number> feeRangeListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
||||
setFeeRate(getFeeRangeRate());
|
||||
userFeeSet.set(false);
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
}
|
||||
updateTransaction();
|
||||
}
|
||||
};
|
||||
|
||||
private ValidationSupport validationSupport;
|
||||
|
||||
@Override
|
||||
|
@ -180,6 +205,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
revalidate(fee, feeListener);
|
||||
});
|
||||
|
||||
targetBlocksField.managedProperty().bind(targetBlocksField.visibleProperty());
|
||||
targetBlocks.setMin(0);
|
||||
targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1);
|
||||
targetBlocks.setMajorTickUnit(1);
|
||||
|
@ -198,18 +224,43 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
});
|
||||
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
||||
|
||||
feeRatesChart.initialize();
|
||||
feeRangeField.managedProperty().bind(feeRangeField.visibleProperty());
|
||||
feeRangeField.visibleProperty().bind(targetBlocksField.visibleProperty().not());
|
||||
feeRange.setMin(0);
|
||||
feeRange.setMax(FEE_RATES_RANGE.size() - 1);
|
||||
feeRange.setMajorTickUnit(1);
|
||||
feeRange.setMinorTickCount(0);
|
||||
feeRange.setLabelFormatter(new StringConverter<Double>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
return Long.toString(FEE_RATES_RANGE.get(object.intValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
feeRange.valueProperty().addListener(feeRangeListener);
|
||||
|
||||
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
||||
blockTargetFeeRatesChart.initialize();
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
if(targetBlocksFeeRates != null) {
|
||||
feeRatesChart.update(targetBlocksFeeRates);
|
||||
blockTargetFeeRatesChart.update(targetBlocksFeeRates);
|
||||
} else {
|
||||
feeRate.setText("Unknown");
|
||||
}
|
||||
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||
targetBlocks.setValue(index);
|
||||
feeRatesChart.select(defaultTarget);
|
||||
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
||||
mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not());
|
||||
mempoolSizeFeeRatesChart.initialize();
|
||||
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
||||
if(mempoolHistogram != null) {
|
||||
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
||||
}
|
||||
|
||||
updateFeeRateSelection(Config.get().getFeeRateSelection());
|
||||
|
||||
fee.setTextFormatter(new CoinTextFormatter());
|
||||
fee.textProperty().addListener(feeListener);
|
||||
|
@ -224,7 +275,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
});
|
||||
|
||||
userFeeSet.addListener((observable, oldValue, newValue) -> {
|
||||
feeRatesChart.select(0);
|
||||
blockTargetFeeRatesChart.select(0);
|
||||
|
||||
Node thumb = getSliderThumb();
|
||||
if(thumb != null) {
|
||||
|
@ -405,6 +456,27 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void updateFeeRateSelection(FeeRateSelection feeRateSelection) {
|
||||
boolean blockTargetSelection = (feeRateSelection == FeeRateSelection.BLOCK_TARGET);
|
||||
targetBlocksField.setVisible(blockTargetSelection);
|
||||
blockTargetFeeRatesChart.setVisible(blockTargetSelection);
|
||||
setDefaultFeeRate();
|
||||
updateTransaction();
|
||||
}
|
||||
|
||||
private void setDefaultFeeRate() {
|
||||
if(targetBlocksField.isVisible()) {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||
targetBlocks.setValue(index);
|
||||
blockTargetFeeRatesChart.select(defaultTarget);
|
||||
setFeeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
|
||||
} else {
|
||||
feeRange.setValue(5.0);
|
||||
setFeeRate(getFeeRangeRate());
|
||||
}
|
||||
}
|
||||
|
||||
private Long getFeeValueSats() {
|
||||
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
|
@ -450,7 +522,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
targetBlocks.valueProperty().removeListener(targetBlocksListener);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(target);
|
||||
targetBlocks.setValue(index);
|
||||
feeRatesChart.select(target);
|
||||
blockTargetFeeRatesChart.select(target);
|
||||
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
||||
}
|
||||
|
||||
|
@ -465,8 +537,16 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return retrievedFeeRates;
|
||||
}
|
||||
|
||||
private Double getFeeRangeRate() {
|
||||
return Math.pow(2.0, feeRange.getValue());
|
||||
}
|
||||
|
||||
public Double getFeeRate() {
|
||||
if(targetBlocksField.isVisible()) {
|
||||
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
||||
} else {
|
||||
return getFeeRangeRate();
|
||||
}
|
||||
}
|
||||
|
||||
private Double getMinimumFeeRate() {
|
||||
|
@ -475,6 +555,10 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
private Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
return AppController.getMempoolHistogram();
|
||||
}
|
||||
|
||||
public boolean isInsufficientFeeRate() {
|
||||
return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate();
|
||||
}
|
||||
|
@ -662,10 +746,18 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
|
||||
feeRatesChart.update(event.getTargetBlockFeeRates());
|
||||
feeRatesChart.select(getTargetBlocks());
|
||||
blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates());
|
||||
blockTargetFeeRatesChart.select(getTargetBlocks());
|
||||
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
||||
if(targetBlocksField.isVisible()) {
|
||||
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSelectionChanged(FeeRateSelectionChangedEvent event) {
|
||||
updateFeeRateSelection(event.getFeeRateSelection());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void spendUtxos(SpendUtxoEvent event) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<?import com.sparrowwallet.sparrow.io.ExchangeSource?>
|
||||
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
|
||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?>
|
||||
|
||||
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.GeneralPreferencesController">
|
||||
<padding>
|
||||
|
@ -41,6 +42,17 @@
|
|||
</ComboBox>
|
||||
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
|
||||
</Field>
|
||||
<Field text="Fee rate selection:">
|
||||
<ComboBox fx:id="feeRateSelection">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<FeeRateSelection fx:constant="BLOCK_TARGET" />
|
||||
<FeeRateSelection fx:constant="MEMPOOL_SIZE" />
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<HelpLabel helpText="Fee rate selection can be done either by estimating the number of blocks until a transaction is mined, or examining the current size of the mempool."/>
|
||||
</Field>
|
||||
</Fieldset>
|
||||
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">
|
||||
<Field text="Currency:">
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
-fx-max-width: 76px;
|
||||
}
|
||||
|
||||
#feeRatesChart {
|
||||
.feeRatesChart {
|
||||
-fx-max-width: 335px;
|
||||
-fx-max-height: 130px;
|
||||
}
|
||||
|
@ -108,3 +108,18 @@
|
|||
#transactionDiagram .utxo-label:hover .button .label .text {
|
||||
-fx-fill: -fx-text-base-color;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
-fx-alignment: center;
|
||||
-fx-font-size: 12px;
|
||||
-fx-padding: 0 0 5 0;
|
||||
}
|
||||
|
||||
.tooltip-series0 { -fx-text-fill: CHART_COLOR_1; }
|
||||
.tooltip-series1 { -fx-text-fill: CHART_COLOR_2; }
|
||||
.tooltip-series2 { -fx-text-fill: CHART_COLOR_3; }
|
||||
.tooltip-series3 { -fx-text-fill: CHART_COLOR_4; }
|
||||
.tooltip-series4 { -fx-text-fill: CHART_COLOR_5; }
|
||||
.tooltip-series5 { -fx-text-fill: CHART_COLOR_6; }
|
||||
.tooltip-series6 { -fx-text-fill: CHART_COLOR_7; }
|
||||
.tooltip-series7 { -fx-text-fill: CHART_COLOR_8; }
|
|
@ -14,13 +14,14 @@
|
|||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import com.sparrowwallet.sparrow.control.FeeRatesChart?>
|
||||
<?import com.sparrowwallet.sparrow.control.BlockTargetFeeRatesChart?>
|
||||
<?import javafx.scene.chart.CategoryAxis?>
|
||||
<?import javafx.scene.chart.NumberAxis?>
|
||||
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
|
||||
<?import com.sparrowwallet.drongo.BitcoinUnit?>
|
||||
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
|
||||
|
||||
<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>
|
||||
|
@ -49,9 +50,12 @@
|
|||
</BorderPane>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Fee">
|
||||
<Field text="Block target">
|
||||
<Field fx:id="targetBlocksField" text="Block target">
|
||||
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
|
||||
</Field>
|
||||
<Field fx:id="feeRangeField" text="Range:">
|
||||
<Slider fx:id="feeRange" snapToTicks="false" showTickLabels="true" showTickMarks="true" />
|
||||
</Field>
|
||||
<Field fx:id="feeRateField" text="Rate:">
|
||||
<CopyableLabel fx:id="feeRate" />
|
||||
</Field>
|
||||
|
@ -71,14 +75,22 @@
|
|||
</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">
|
||||
<BlockTargetFeeRatesChart fx:id="blockTargetFeeRatesChart" styleClass="feeRatesChart" legendVisible="false" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="20" animated="false">
|
||||
<xAxis>
|
||||
<CategoryAxis side="BOTTOM" />
|
||||
</xAxis>
|
||||
<yAxis>
|
||||
<NumberAxis side="LEFT" />
|
||||
</yAxis>
|
||||
</FeeRatesChart>
|
||||
</BlockTargetFeeRatesChart>
|
||||
<MempoolSizeFeeRatesChart fx:id="mempoolSizeFeeRatesChart" styleClass="feeRatesChart" legendVisible="false" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="20" animated="false">
|
||||
<xAxis>
|
||||
<CategoryAxis side="BOTTOM" />
|
||||
</xAxis>
|
||||
<yAxis>
|
||||
<NumberAxis side="LEFT" />
|
||||
</yAxis>
|
||||
</MempoolSizeFeeRatesChart>
|
||||
</AnchorPane>
|
||||
</GridPane>
|
||||
<AnchorPane>
|
||||
|
|
Loading…
Reference in a new issue