fetch fee rates from configurable external sources

This commit is contained in:
Craig Raw 2020-11-23 17:26:51 +02:00
parent 02c8415a7b
commit ee7b741a69
12 changed files with 215 additions and 59 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<FeeRatesUpdatedEvent> 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<FeeRatesUpdatedEvent> {
@Override
protected Task<FeeRatesUpdatedEvent> createTask() {
return new Task<>() {
protected FeeRatesUpdatedEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
}
};
}
}
}

View file

@ -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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
return Collections.emptyMap();
}
},
MEMPOOL_SPACE("mempool.space") {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = "https://mempool.space/api/v1/fees/recommended";
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
}
},
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> 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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
public String getName() {
return name;
}
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
Proxy proxy = getProxy();
Map<Integer, Double> 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;
}
}

View file

@ -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> bitcoinUnit;
@FXML
private ComboBox<FeeRateSelection> feeRateSelection;
private ComboBox<FeeRatesSource> feeRatesSource;
@FXML
private ComboBox<Currency> 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) {

View file

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

View file

@ -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() {
if(targetBlocksField.isVisible()) {
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()) {
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());
}

View file

@ -15,7 +15,7 @@
<?import com.sparrowwallet.sparrow.net.ExchangeSource?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?>
<?import com.sparrowwallet.sparrow.net.FeeRatesSource?>
<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>
@ -42,16 +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">
<Field text="Fee rates source:">
<ComboBox fx:id="feeRatesSource">
<items>
<FXCollections fx:factory="observableArrayList">
<FeeRateSelection fx:constant="BLOCK_TARGET" />
<FeeRateSelection fx:constant="MEMPOOL_SIZE" />
<FeeRatesSource fx:constant="ELECTRUM_SERVER" />
<FeeRatesSource fx:constant="MEMPOOL_SPACE" />
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
</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."/>
<HelpLabel helpText="Recommended fee rates can be sourced from your connected Electrum server, or an external source."/>
</Field>
</Fieldset>
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">

View file

@ -23,7 +23,7 @@
<?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
<?import org.controlsfx.control.SegmentedButton?>
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?>
<?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?>
<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>
@ -67,7 +67,7 @@
<Tooltip text="Determine fee via estimated number of blocks"/>
</tooltip>
<userData>
<FeeRateSelection fx:constant="BLOCK_TARGET"/>
<FeeRatesSelection fx:constant="BLOCK_TARGET"/>
</userData>
</ToggleButton>
<ToggleButton fx:id="mempoolSizeToggle" text="Mempool Size" toggleGroup="$feeSelectionToggleGroup">
@ -75,7 +75,7 @@
<Tooltip text="Determine fee via current mempool size"/>
</tooltip>
<userData>
<FeeRateSelection fx:constant="MEMPOOL_SIZE"/>
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
</userData>
</ToggleButton>
</buttons>