From 2b55b5feb37aec677b834a0ffa8735f77ef03581 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 19 Nov 2020 10:46:19 +0200 Subject: [PATCH] add mempool size fee selection --- .../sparrowwallet/sparrow/AppController.java | 27 ++- ...art.java => BlockTargetFeeRatesChart.java} | 4 +- .../control/MempoolSizeFeeRatesChart.java | 169 ++++++++++++++++++ .../sparrow/event/ConnectionEvent.java | 6 +- .../event/FeeRateSelectionChangedEvent.java | 15 ++ .../sparrow/event/FeeRatesUpdatedEvent.java | 13 +- .../com/sparrowwallet/sparrow/io/Config.java | 11 ++ .../sparrow/net/ElectrumServer.java | 20 ++- .../sparrow/net/MempoolRateSize.java | 43 +++++ .../GeneralPreferencesController.java | 17 ++ .../preferences/PreferencesDialog.java | 2 +- .../sparrow/wallet/FeeRateSelection.java | 20 +++ .../sparrow/wallet/SendController.java | 126 +++++++++++-- .../sparrow/preferences/general.fxml | 12 ++ .../com/sparrowwallet/sparrow/wallet/send.css | 19 +- .../sparrowwallet/sparrow/wallet/send.fxml | 20 ++- 16 files changed, 477 insertions(+), 47 deletions(-) rename src/main/java/com/sparrowwallet/sparrow/control/{FeeRatesChart.java => BlockTargetFeeRatesChart.java} (90%) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index ed415ad5..1e619a5a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -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 targetBlockFeeRates; - private static Map feeRateHistogram; + private static final Map> mempoolHistogram = new TreeMap<>(); private static Double minimumRelayFeeRate; @@ -151,7 +155,7 @@ public class AppController implements Initializable { private static List devices; - private static Map payjoinURIs = new HashMap<>(); + private static final Map 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 getFeeRateHistogram() { - return feeRateHistogram; + public static Map> getMempoolHistogram() { + return mempoolHistogram; + } + + private void addMempoolRateSizes(Set 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 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FeeRatesChart.java b/src/main/java/com/sparrowwallet/sparrow/control/BlockTargetFeeRatesChart.java similarity index 90% rename from src/main/java/com/sparrowwallet/sparrow/control/FeeRatesChart.java rename to src/main/java/com/sparrowwallet/sparrow/control/BlockTargetFeeRatesChart.java index 540318ac..f632d20b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FeeRatesChart.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/BlockTargetFeeRatesChart.java @@ -9,11 +9,11 @@ import javafx.scene.chart.XYChart; import java.util.Iterator; import java.util.Map; -public class FeeRatesChart extends LineChart { +public class BlockTargetFeeRatesChart extends LineChart { private XYChart.Series feeRateSeries; private Integer selectedTargetBlocks; - public FeeRatesChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis) { + public BlockTargetFeeRatesChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis) { super(xAxis, yAxis); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java new file mode 100644 index 00000000..0285bc25 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java @@ -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 { + private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm"); + + private Tooltip tooltip; + + public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis 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> mempoolRateSizes) { + getData().clear(); + if(tooltip.isShowing()) { + tooltip.hide(); + } + + Map> periodRateSizes = getPeriodRateSizes(mempoolRateSizes); + List 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() { + @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 series = new XYChart.Series<>(); + series.setName(feeRate + "+ vB"); + + for(Date date : periodRateSizes.keySet()) { + Set 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> getPeriodRateSizes(Map> 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 getCategories(Map> mempoolHistogram) { + List 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> 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 series = seriesList.get(i); + for(XYChart.Data 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); + } + } + } + } + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java index 67552634..616d1436 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java @@ -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 serverVersion; @@ -12,8 +14,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent { private final BlockHeader blockHeader; private final Double minimumRelayFeeRate; - public ConnectionEvent(List serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map targetBlockFeeRates, Map feeRateHistogram, Double minimumRelayFeeRate) { - super(targetBlockFeeRates, feeRateHistogram); + public ConnectionEvent(List serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map targetBlockFeeRates, Set mempoolRateSizes, Double minimumRelayFeeRate) { + super(targetBlockFeeRates, mempoolRateSizes); this.serverVersion = serverVersion; this.serverBanner = serverBanner; this.blockHeight = blockHeight; diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java new file mode 100644 index 00000000..2da8fd1a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java index ce5aed83..75db7224 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java @@ -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 targetBlockFeeRates; - private final Map feeRateHistogram; + private final Set mempoolRateSizes; - public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Map feeRateHistogram) { + public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes) { this.targetBlockFeeRates = targetBlockFeeRates; - this.feeRateHistogram = feeRateHistogram; + this.mempoolRateSizes = mempoolRateSizes; } public Map getTargetBlockFeeRates() { return targetBlockFeeRates; } - public Map getFeeRateHistogram() { - return feeRateHistogram; + public Set getMempoolRateSizes() { + return mempoolRateSizes; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 9a69cd06..9314060b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -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; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 312b5cf7..6f4df471 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -610,8 +610,14 @@ public class ElectrumServer { } } - public Map getFeeRateHistogram() throws ServerException { - return electrumServerRpc.getFeeRateHistogram(getTransport()); + public Set getMempoolRateSizes() throws ServerException { + Map feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport()); + Set 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 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 blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE); - Map feeRateHistogram = electrumServer.getFeeRateHistogram(); + Set 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 blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE); - Map feeRateHistogram = electrumServer.getFeeRateHistogram(); + Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); feeRatesRetrievedAt = System.currentTimeMillis(); - return new FeeRatesUpdatedEvent(blockTargetFeeRates, feeRateHistogram); + return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); } } else { resetConnection(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java b/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java new file mode 100644 index 00000000..67c6d908 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java @@ -0,0 +1,43 @@ +package com.sparrowwallet.sparrow.net; + +import java.util.Objects; + +public class MempoolRateSize implements Comparable { + 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); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java index 795fd8e6..011e960f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java @@ -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; + @FXML + private ComboBox feeRateSelection; + @FXML private ComboBox 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 { diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java index cff80b7a..2d5325c3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java @@ -49,7 +49,7 @@ public class PreferencesDialog extends Dialog { } dialogPane.setPrefWidth(650); - dialogPane.setPrefHeight(500); + dialogPane.setPrefHeight(550); existingConnection = ElectrumServer.isConnected(); setOnCloseRequest(event -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java new file mode 100644 index 00000000..6d265095 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index ed67cea4..8c239d4f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -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 TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50, 100, 500); + public static final List 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 feeRangeListener = new ChangeListener<>() { + @Override + public void changed(ObservableValue 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() { + @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 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> 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> 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 diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml index 88dda0c2..972696cc 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml @@ -15,6 +15,7 @@ + @@ -41,6 +42,17 @@ + + + + + + + + + + +
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css index 7bcc67b6..ce43389c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css @@ -39,7 +39,7 @@ -fx-max-width: 76px; } -#feeRatesChart { +.feeRatesChart { -fx-max-width: 335px; -fx-max-height: 130px; } @@ -107,4 +107,19 @@ #transactionDiagram .utxo-label:hover .button .label .text { -fx-fill: -fx-text-base-color; -} \ No newline at end of file +} + +.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; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 6b76c6c3..d7814790 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -14,13 +14,14 @@ - + +
@@ -49,9 +50,12 @@
- + + + + @@ -71,14 +75,22 @@
- + - + + + + + + + + +