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

View file

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

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

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

View file

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

View file

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

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.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 {

View file

@ -49,7 +49,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
}
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(500);
dialogPane.setPrefHeight(550);
existingConnection = ElectrumServer.isConnected();
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.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() {
return getTargetBlocksFeeRates().get(getTargetBlocks());
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,9 +746,17 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
feeRatesChart.update(event.getTargetBlockFeeRates());
feeRatesChart.select(getTargetBlocks());
setFeeRate(event.getTargetBlockFeeRates().get(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

View file

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

View file

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

View file

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