diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 3be35431..75f69148 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1495,6 +1495,15 @@ public class AppController implements Initializable { selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle)); } + @Subscribe + public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { + ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService(); + feeRatesService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(feeRatesService.getValue()); + }); + feeRatesService.start(); + } + @Subscribe public void fiatCurrencySelected(FiatCurrencySelectedEvent event) { ratesService.cancel(); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java deleted file mode 100644 index 2da8fd1a..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/event/FeeRateSelectionChangedEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -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/FeeRatesSelectionChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesSelectionChangedEvent.java new file mode 100644 index 00000000..62e4e312 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesSelectionChangedEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; + +public class FeeRatesSelectionChangedEvent { + private final FeeRatesSelection feeRatesSelection; + + public FeeRatesSelectionChangedEvent(FeeRatesSelection feeRatesSelection) { + this.feeRatesSelection = feeRatesSelection; + } + + public FeeRatesSelection getFeeRateSelection() { + return feeRatesSelection; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesSourceChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesSourceChangedEvent.java new file mode 100644 index 00000000..4986b285 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesSourceChangedEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.net.FeeRatesSource; + +public class FeeRatesSourceChangedEvent { + private final FeeRatesSource feeRatesSource; + + public FeeRatesSourceChangedEvent(FeeRatesSource feeRatesSource) { + this.feeRatesSource = feeRatesSource; + } + + public FeeRatesSource getFeeRateSource() { + return feeRatesSource; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 66816877..8dc8c33b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -5,7 +5,8 @@ import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.net.ExchangeSource; -import com.sparrowwallet.sparrow.wallet.FeeRateSelection; +import com.sparrowwallet.sparrow.net.FeeRatesSource; +import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +22,8 @@ public class Config { private Mode mode; private BitcoinUnit bitcoinUnit; - private FeeRateSelection feeRateSelection; + private FeeRatesSource feeRatesSource; + private FeeRatesSelection feeRatesSelection; private Currency fiatCurrency; private ExchangeSource exchangeSource; private boolean groupByAddress = true; @@ -101,12 +103,21 @@ public class Config { flush(); } - public FeeRateSelection getFeeRateSelection() { - return feeRateSelection; + public FeeRatesSource getFeeRatesSource() { + return feeRatesSource; } - public void setFeeRateSelection(FeeRateSelection feeRateSelection) { - this.feeRateSelection = feeRateSelection; + public void setFeeRatesSource(FeeRatesSource feeRatesSource) { + this.feeRatesSource = feeRatesSource; + flush(); + } + + public FeeRatesSelection getFeeRatesSelection() { + return feeRatesSelection; + } + + public void setFeeRatesSelection(FeeRatesSelection feeRatesSelection) { + this.feeRatesSelection = feeRatesSelection; flush(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 6f4df471..565de4f4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -604,6 +604,11 @@ public class ElectrumServer { targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d); } + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); + if(feeRatesSource != null) { + targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); + } + return targetBlocksFeeRatesSats; } catch(ElectrumServerRpcException e) { throw new ServerException(e.getMessage(), e); @@ -718,7 +723,7 @@ public class ElectrumServer { } public static class ConnectionService extends ScheduledService implements Thread.UncaughtExceptionHandler { - private static final int FEE_RATES_PERIOD = 1 * 60 * 1000; + private static final int FEE_RATES_PERIOD = 30 * 1000; private final boolean subscribe; private boolean firstCall = true; @@ -1014,4 +1019,18 @@ public class ElectrumServer { }; } } + + public static class FeeRatesService extends Service { + @Override + protected Task createTask() { + return new Task<>() { + protected FeeRatesUpdatedEvent call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + Map blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE); + Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); + return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); + } + }; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java new file mode 100644 index 00000000..2ff1a74f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -0,0 +1,102 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import com.google.gson.Gson; +import com.sparrowwallet.sparrow.io.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public enum FeeRatesSource { + ELECTRUM_SERVER("Electrum Server") { + @Override + public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { + return Collections.emptyMap(); + } + }, + MEMPOOL_SPACE("mempool.space") { + @Override + public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { + String url = "https://mempool.space/api/v1/fees/recommended"; + return getThreeTierFeeRates(defaultblockTargetFeeRates, url); + } + }, + BITCOINFEES_EARN_COM("bitcoinfees.earn.com") { + @Override + public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { + String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended"; + return getThreeTierFeeRates(defaultblockTargetFeeRates, url); + } + }; + + private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class); + + private final String name; + + FeeRatesSource(String name) { + this.name = name; + } + + public abstract Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates); + + public String getName() { + return name; + } + + private static Map getThreeTierFeeRates(Map defaultblockTargetFeeRates, String url) { + Proxy proxy = getProxy(); + + Map blockTargetFeeRates = new LinkedHashMap<>(); + try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + Gson gson = new Gson(); + ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class); + for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) { + if(blockTarget < 3) { + blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee); + } else if(blockTarget < 6) { + blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee); + } else if(blockTarget <= 10 || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) { + blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee); + } else { + blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget)); + } + } + } catch (Exception e) { + log.warn("Error retrieving recommended fee rates from " + url, e); + } + + return blockTargetFeeRates; + } + + private static Proxy getProxy() { + Config config = Config.get(); + if(config.isUseProxy()) { + HostAndPort proxy = HostAndPort.fromString(config.getProxyServer()); + InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT)); + return new Proxy(Proxy.Type.SOCKS, proxyAddress); + } + + return null; + } + + @Override + public String toString() { + return name; + } + + private static class ThreeTierRates { + Double fastestFee; + Double halfHourFee; + Double hourFee; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java index 2bb6f241..697b6e31 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java @@ -3,13 +3,10 @@ package com.sparrowwallet.sparrow.preferences; 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.event.*; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.ExchangeSource; -import com.sparrowwallet.sparrow.wallet.FeeRateSelection; +import com.sparrowwallet.sparrow.net.FeeRatesSource; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; @@ -27,7 +24,7 @@ public class GeneralPreferencesController extends PreferencesDetailController { private ComboBox bitcoinUnit; @FXML - private ComboBox feeRateSelection; + private ComboBox feeRatesSource; @FXML private ComboBox fiatCurrency; @@ -68,16 +65,16 @@ public class GeneralPreferencesController extends PreferencesDetailController { EventManager.get().post(new BitcoinUnitChangedEvent(newValue)); }); - if(config.getFeeRateSelection() != null) { - feeRateSelection.setValue(config.getFeeRateSelection()); + if(config.getFeeRatesSource() != null) { + feeRatesSource.setValue(config.getFeeRatesSource()); } else { - feeRateSelection.getSelectionModel().select(0); - config.setFeeRateSelection(FeeRateSelection.BLOCK_TARGET); + feeRatesSource.getSelectionModel().select(1); + config.setFeeRatesSource(feeRatesSource.getValue()); } - feeRateSelection.valueProperty().addListener((observable, oldValue, newValue) -> { - config.setFeeRateSelection(newValue); - EventManager.get().post(new FeeRateSelectionChangedEvent(newValue)); + feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> { + config.setFeeRatesSource(newValue); + EventManager.get().post(new FeeRatesSourceChangedEvent(newValue)); }); if(config.getExchangeSource() != null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java similarity index 79% rename from src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java rename to src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java index 6d265095..9e75e9d8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRateSelection.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java @@ -1,11 +1,11 @@ package com.sparrowwallet.sparrow.wallet; -public enum FeeRateSelection { +public enum FeeRatesSelection { BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size"); private final String name; - private FeeRateSelection(String name) { + private FeeRatesSelection(String name) { this.name = name; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 7ebafbab..00b18e69 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -269,13 +269,14 @@ public class SendController extends WalletFormController implements Initializabl mempoolSizeFeeRatesChart.update(mempoolHistogram); } - FeeRateSelection feeRateSelection = Config.get().getFeeRateSelection(); - updateFeeRateSelection(feeRateSelection); - feeSelectionToggleGroup.selectToggle(feeRateSelection == FeeRateSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); + FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection(); + feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection); + updateFeeRateSelection(feeRatesSelection); + feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { - FeeRateSelection newFeeRateSelection = (FeeRateSelection)newValue.getUserData(); - Config.get().setFeeRateSelection(newFeeRateSelection); - EventManager.get().post(new FeeRateSelectionChangedEvent(newFeeRateSelection)); + FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData(); + Config.get().setFeeRatesSelection(newFeeRatesSelection); + EventManager.get().post(new FeeRatesSelectionChangedEvent(newFeeRatesSelection)); }); fee.setTextFormatter(new CoinTextFormatter()); @@ -472,8 +473,8 @@ public class SendController extends WalletFormController implements Initializabl return Collections.emptyList(); } - private void updateFeeRateSelection(FeeRateSelection feeRateSelection) { - boolean blockTargetSelection = (feeRateSelection == FeeRateSelection.BLOCK_TARGET); + private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) { + boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET); targetBlocksField.setVisible(blockTargetSelection); blockTargetFeeRatesChart.setVisible(blockTargetSelection); setDefaultFeeRate(); @@ -481,14 +482,15 @@ public class SendController extends WalletFormController implements Initializabl } private void setDefaultFeeRate() { + int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); + int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget); + Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget); 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())); + setFeeRate(defaultRate); } else { - feeRange.setValue(5.0); + feeRange.setValue(Math.log(defaultRate) / Math.log(2)); setFeeRate(getFeeRangeRate()); } } @@ -771,7 +773,7 @@ public class SendController extends WalletFormController implements Initializabl } @Subscribe - public void feeRateSelectionChanged(FeeRateSelectionChangedEvent event) { + public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) { updateFeeRateSelection(event.getFeeRateSelection()); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml index 6cbfddaa..40a8afce 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml @@ -15,7 +15,7 @@ - + @@ -42,16 +42,17 @@ - - + + - - + + + - +
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 4a583d15..29877e44 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -23,7 +23,7 @@ - +
@@ -67,7 +67,7 @@ - + @@ -75,7 +75,7 @@ - +