add mempool size fee selection

This commit is contained in:
Craig Raw 2020-11-19 10:46:19 +02:00
parent 43bf6ab265
commit 2b55b5feb3
16 changed files with 477 additions and 47 deletions

View file

@ -25,6 +25,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.net.VersionCheckService; import com.sparrowwallet.sparrow.net.VersionCheckService;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionController;
@ -68,6 +69,9 @@ import java.io.*;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.ParseException; import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -75,7 +79,7 @@ import java.util.stream.Collectors;
public class AppController implements Initializable { public class AppController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(AppController.class); 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 ENUMERATE_HW_PERIOD = 30 * 1000;
private static final int RATES_PERIOD = 5 * 60 * 1000; private static final int RATES_PERIOD = 5 * 60 * 1000;
private static final int VERSION_CHECK_PERIOD_HOURS = 24; 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<Integer, Double> targetBlockFeeRates;
private static Map<Long, Long> feeRateHistogram; private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate; private static Double minimumRelayFeeRate;
@ -151,7 +155,7 @@ public class AppController implements Initializable {
private static List<Device> devices; private static List<Device> devices;
private static Map<Address, BitcoinURI> payjoinURIs = new HashMap<>(); private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
@ -652,8 +656,17 @@ public class AppController implements Initializable {
return targetBlockFeeRates; return targetBlockFeeRates;
} }
public static Map<Long, Long> getFeeRateHistogram() { public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
return feeRateHistogram; 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() { public static Double getMinimumRelayFeeRate() {
@ -1435,7 +1448,7 @@ public class AppController implements Initializable {
public void newConnection(ConnectionEvent event) { public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight(); currentBlockHeight = event.getBlockHeight();
targetBlockFeeRates = event.getTargetBlockFeeRates(); targetBlockFeeRates = event.getTargetBlockFeeRates();
feeRateHistogram = event.getFeeRateHistogram(); addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate(); minimumRelayFeeRate = event.getMinimumRelayFeeRate();
String banner = event.getServerBanner(); String banner = event.getServerBanner();
String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight(); String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight();
@ -1460,7 +1473,7 @@ public class AppController implements Initializable {
@Subscribe @Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) { public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates(); targetBlockFeeRates = event.getTargetBlockFeeRates();
feeRateHistogram = event.getFeeRateHistogram(); addMempoolRateSizes(event.getMempoolRateSizes());
} }
@Subscribe @Subscribe

View file

@ -9,11 +9,11 @@ import javafx.scene.chart.XYChart;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; 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 XYChart.Series<String, Number> feeRateSeries;
private Integer selectedTargetBlocks; 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); super(xAxis, yAxis);
} }

View file

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

View file

@ -1,9 +1,11 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader; import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class ConnectionEvent extends FeeRatesUpdatedEvent { public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final List<String> serverVersion; private final List<String> serverVersion;
@ -12,8 +14,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final BlockHeader blockHeader; private final BlockHeader blockHeader;
private final Double minimumRelayFeeRate; 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) { public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
super(targetBlockFeeRates, feeRateHistogram); super(targetBlockFeeRates, mempoolRateSizes);
this.serverVersion = serverVersion; this.serverVersion = serverVersion;
this.serverBanner = serverBanner; this.serverBanner = serverBanner;
this.blockHeight = blockHeight; this.blockHeight = blockHeight;

View file

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

View file

@ -1,21 +1,24 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class FeeRatesUpdatedEvent { public class FeeRatesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates; 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.targetBlockFeeRates = targetBlockFeeRates;
this.feeRateHistogram = feeRateHistogram; this.mempoolRateSizes = mempoolRateSizes;
} }
public Map<Integer, Double> getTargetBlockFeeRates() { public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates; return targetBlockFeeRates;
} }
public Map<Long, Long> getFeeRateHistogram() { public Set<MempoolRateSize> getMempoolRateSizes() {
return feeRateHistogram; return mempoolRateSizes;
} }
} }

View file

@ -4,6 +4,7 @@ import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -19,6 +20,7 @@ public class Config {
private Mode mode; private Mode mode;
private BitcoinUnit bitcoinUnit; private BitcoinUnit bitcoinUnit;
private FeeRateSelection feeRateSelection;
private Currency fiatCurrency; private Currency fiatCurrency;
private ExchangeSource exchangeSource; private ExchangeSource exchangeSource;
private boolean groupByAddress = true; private boolean groupByAddress = true;
@ -98,6 +100,15 @@ public class Config {
flush(); flush();
} }
public FeeRateSelection getFeeRateSelection() {
return feeRateSelection;
}
public void setFeeRateSelection(FeeRateSelection feeRateSelection) {
this.feeRateSelection = feeRateSelection;
flush();
}
public Currency getFiatCurrency() { public Currency getFiatCurrency() {
return fiatCurrency; return fiatCurrency;
} }

View file

@ -610,8 +610,14 @@ public class ElectrumServer {
} }
} }
public Map<Long, Long> getFeeRateHistogram() throws ServerException { public Set<MempoolRateSize> getMempoolRateSizes() throws ServerException {
return electrumServerRpc.getFeeRateHistogram(getTransport()); 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 { public Double getMinimumRelayFee() throws ServerException {
@ -712,7 +718,7 @@ public class ElectrumServer {
} }
public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler { 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 final boolean subscribe;
private boolean firstCall = true; private boolean firstCall = true;
@ -764,7 +770,7 @@ public class ElectrumServer {
String banner = electrumServer.getServerBanner(); String banner = electrumServer.getServerBanner();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE); Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
Map<Long, Long> feeRateHistogram = electrumServer.getFeeRateHistogram(); Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
feeRatesRetrievedAt = System.currentTimeMillis(); feeRatesRetrievedAt = System.currentTimeMillis();
Double minimumRelayFeeRate = electrumServer.getMinimumRelayFee(); Double minimumRelayFeeRate = electrumServer.getMinimumRelayFee();
@ -772,7 +778,7 @@ public class ElectrumServer {
blockTargetFeeRates.computeIfPresent(blockTarget, (blocks, feeRate) -> feeRate < minimumRelayFeeRate ? minimumRelayFeeRate : feeRate); 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 { } else {
if(reader.isAlive()) { if(reader.isAlive()) {
electrumServer.ping(); electrumServer.ping();
@ -780,9 +786,9 @@ public class ElectrumServer {
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt; long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
if(elapsed > FEE_RATES_PERIOD) { if(elapsed > FEE_RATES_PERIOD) {
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE); Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
Map<Long, Long> feeRateHistogram = electrumServer.getFeeRateHistogram(); Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
feeRatesRetrievedAt = System.currentTimeMillis(); feeRatesRetrievedAt = System.currentTimeMillis();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, feeRateHistogram); return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
} }
} else { } else {
resetConnection(); resetConnection();

View file

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

View file

@ -4,10 +4,12 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.FeeRateSelectionChangedEvent;
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent; import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent; import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ExchangeSource; import com.sparrowwallet.sparrow.io.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -24,6 +26,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
@FXML @FXML
private ComboBox<BitcoinUnit> bitcoinUnit; private ComboBox<BitcoinUnit> bitcoinUnit;
@FXML
private ComboBox<FeeRateSelection> feeRateSelection;
@FXML @FXML
private ComboBox<Currency> fiatCurrency; private ComboBox<Currency> fiatCurrency;
@ -63,6 +68,18 @@ public class GeneralPreferencesController extends PreferencesDetailController {
EventManager.get().post(new BitcoinUnitChangedEvent(newValue)); 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) { if(config.getExchangeSource() != null) {
exchangeSource.setValue(config.getExchangeSource()); exchangeSource.setValue(config.getExchangeSource());
} else { } else {

View file

@ -49,7 +49,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
} }
dialogPane.setPrefWidth(650); dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(500); dialogPane.setPrefHeight(550);
existingConnection = ElectrumServer.isConnected(); existingConnection = ElectrumServer.isConnected();
setOnCloseRequest(event -> { setOnCloseRequest(event -> {

View file

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

View file

@ -2,11 +2,8 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppController; 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.Config;
import com.sparrowwallet.sparrow.io.ExchangeSource; import com.sparrowwallet.sparrow.io.ExchangeSource;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
@ -36,6 +34,7 @@ import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@ -49,15 +48,25 @@ public class SendController extends WalletFormController implements Initializabl
private static final Logger log = LoggerFactory.getLogger(SendController.class); 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<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; public static final double FALLBACK_FEE_RATE = 20000d / 1000;
@FXML @FXML
private TabPane paymentTabs; private TabPane paymentTabs;
@FXML
private Field targetBlocksField;
@FXML @FXML
private Slider targetBlocks; private Slider targetBlocks;
@FXML
private Field feeRangeField;
@FXML
private Slider feeRange;
@FXML @FXML
private CopyableLabel feeRate; private CopyableLabel feeRate;
@ -71,7 +80,10 @@ public class SendController extends WalletFormController implements Initializabl
private FiatLabel fiatFeeAmount; private FiatLabel fiatFeeAmount;
@FXML @FXML
private FeeRatesChart feeRatesChart; private BlockTargetFeeRatesChart blockTargetFeeRatesChart;
@FXML
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
@FXML @FXML
private TransactionDiagram transactionDiagram; private TransactionDiagram transactionDiagram;
@ -121,7 +133,7 @@ public class SendController extends WalletFormController implements Initializabl
if(targetBlocksFeeRates != null) { if(targetBlocksFeeRates != null) {
setFeeRate(targetBlocksFeeRates.get(target)); setFeeRate(targetBlocksFeeRates.get(target));
feeRatesChart.select(target); blockTargetFeeRatesChart.select(target);
} else { } else {
feeRate.setText("Unknown"); 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; private ValidationSupport validationSupport;
@Override @Override
@ -180,6 +205,7 @@ public class SendController extends WalletFormController implements Initializabl
revalidate(fee, feeListener); revalidate(fee, feeListener);
}); });
targetBlocksField.managedProperty().bind(targetBlocksField.visibleProperty());
targetBlocks.setMin(0); targetBlocks.setMin(0);
targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1); targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1);
targetBlocks.setMajorTickUnit(1); targetBlocks.setMajorTickUnit(1);
@ -198,18 +224,43 @@ public class SendController extends WalletFormController implements Initializabl
}); });
targetBlocks.valueProperty().addListener(targetBlocksListener); 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(); Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
if(targetBlocksFeeRates != null) { if(targetBlocksFeeRates != null) {
feeRatesChart.update(targetBlocksFeeRates); blockTargetFeeRatesChart.update(targetBlocksFeeRates);
} else { } else {
feeRate.setText("Unknown"); feeRate.setText("Unknown");
} }
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget); mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not());
targetBlocks.setValue(index); mempoolSizeFeeRatesChart.initialize();
feeRatesChart.select(defaultTarget); Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
if(mempoolHistogram != null) {
mempoolSizeFeeRatesChart.update(mempoolHistogram);
}
updateFeeRateSelection(Config.get().getFeeRateSelection());
fee.setTextFormatter(new CoinTextFormatter()); fee.setTextFormatter(new CoinTextFormatter());
fee.textProperty().addListener(feeListener); fee.textProperty().addListener(feeListener);
@ -224,7 +275,7 @@ public class SendController extends WalletFormController implements Initializabl
}); });
userFeeSet.addListener((observable, oldValue, newValue) -> { userFeeSet.addListener((observable, oldValue, newValue) -> {
feeRatesChart.select(0); blockTargetFeeRatesChart.select(0);
Node thumb = getSliderThumb(); Node thumb = getSliderThumb();
if(thumb != null) { if(thumb != null) {
@ -405,6 +456,27 @@ public class SendController extends WalletFormController implements Initializabl
return Collections.emptyList(); 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() { private Long getFeeValueSats() {
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem()); return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
} }
@ -450,7 +522,7 @@ public class SendController extends WalletFormController implements Initializabl
targetBlocks.valueProperty().removeListener(targetBlocksListener); targetBlocks.valueProperty().removeListener(targetBlocksListener);
int index = TARGET_BLOCKS_RANGE.indexOf(target); int index = TARGET_BLOCKS_RANGE.indexOf(target);
targetBlocks.setValue(index); targetBlocks.setValue(index);
feeRatesChart.select(target); blockTargetFeeRatesChart.select(target);
targetBlocks.valueProperty().addListener(targetBlocksListener); targetBlocks.valueProperty().addListener(targetBlocksListener);
} }
@ -465,8 +537,16 @@ public class SendController extends WalletFormController implements Initializabl
return retrievedFeeRates; return retrievedFeeRates;
} }
private Double getFeeRangeRate() {
return Math.pow(2.0, feeRange.getValue());
}
public Double getFeeRate() { public Double getFeeRate() {
if(targetBlocksField.isVisible()) {
return getTargetBlocksFeeRates().get(getTargetBlocks()); return getTargetBlocksFeeRates().get(getTargetBlocks());
} else {
return getFeeRangeRate();
}
} }
private Double getMinimumFeeRate() { private Double getMinimumFeeRate() {
@ -475,6 +555,10 @@ public class SendController extends WalletFormController implements Initializabl
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
} }
private Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
return AppController.getMempoolHistogram();
}
public boolean isInsufficientFeeRate() { public boolean isInsufficientFeeRate() {
return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate(); return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate();
} }
@ -662,10 +746,18 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void feeRatesUpdated(FeeRatesUpdatedEvent event) { public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
feeRatesChart.update(event.getTargetBlockFeeRates()); blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates());
feeRatesChart.select(getTargetBlocks()); blockTargetFeeRatesChart.select(getTargetBlocks());
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
if(targetBlocksField.isVisible()) {
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks())); setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
} }
}
@Subscribe
public void feeRateSelectionChanged(FeeRateSelectionChangedEvent event) {
updateFeeRateSelection(event.getFeeRateSelection());
}
@Subscribe @Subscribe
public void spendUtxos(SpendUtxoEvent event) { public void spendUtxos(SpendUtxoEvent event) {

View file

@ -15,6 +15,7 @@
<?import com.sparrowwallet.sparrow.io.ExchangeSource?> <?import com.sparrowwallet.sparrow.io.ExchangeSource?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?> <?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?> <?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"> <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> <padding>
@ -41,6 +42,17 @@
</ComboBox> </ComboBox>
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/> <HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
</Field> </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>
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet"> <Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">
<Field text="Currency:"> <Field text="Currency:">

View file

@ -39,7 +39,7 @@
-fx-max-width: 76px; -fx-max-width: 76px;
} }
#feeRatesChart { .feeRatesChart {
-fx-max-width: 335px; -fx-max-width: 335px;
-fx-max-height: 130px; -fx-max-height: 130px;
} }
@ -108,3 +108,18 @@
#transactionDiagram .utxo-label:hover .button .label .text { #transactionDiagram .utxo-label:hover .button .label .text {
-fx-fill: -fx-text-base-color; -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; }

View file

@ -14,13 +14,14 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> <?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<?import javafx.collections.FXCollections?> <?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.CategoryAxis?>
<?import javafx.scene.chart.NumberAxis?> <?import javafx.scene.chart.NumberAxis?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?> <?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<?import com.sparrowwallet.drongo.BitcoinUnit?> <?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.control.FiatLabel?> <?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import org.controlsfx.glyphfont.Glyph?> <?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"> <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> <center>
@ -49,9 +50,12 @@
</BorderPane> </BorderPane>
<Form GridPane.columnIndex="0" GridPane.rowIndex="1"> <Form GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Fee"> <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" /> <Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
</Field> </Field>
<Field fx:id="feeRangeField" text="Range:">
<Slider fx:id="feeRange" snapToTicks="false" showTickLabels="true" showTickMarks="true" />
</Field>
<Field fx:id="feeRateField" text="Rate:"> <Field fx:id="feeRateField" text="Rate:">
<CopyableLabel fx:id="feeRate" /> <CopyableLabel fx:id="feeRate" />
</Field> </Field>
@ -71,14 +75,22 @@
</Fieldset> </Fieldset>
</Form> </Form>
<AnchorPane GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2"> <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> <xAxis>
<CategoryAxis side="BOTTOM" /> <CategoryAxis side="BOTTOM" />
</xAxis> </xAxis>
<yAxis> <yAxis>
<NumberAxis side="LEFT" /> <NumberAxis side="LEFT" />
</yAxis> </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> </AnchorPane>
</GridPane> </GridPane>
<AnchorPane> <AnchorPane>