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)); 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 @Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) { public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
ratesService.cancel(); 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.Mode;
import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.net.ExchangeSource; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -21,7 +22,8 @@ public class Config {
private Mode mode; private Mode mode;
private BitcoinUnit bitcoinUnit; private BitcoinUnit bitcoinUnit;
private FeeRateSelection feeRateSelection; private FeeRatesSource feeRatesSource;
private FeeRatesSelection feeRatesSelection;
private Currency fiatCurrency; private Currency fiatCurrency;
private ExchangeSource exchangeSource; private ExchangeSource exchangeSource;
private boolean groupByAddress = true; private boolean groupByAddress = true;
@ -101,12 +103,21 @@ public class Config {
flush(); flush();
} }
public FeeRateSelection getFeeRateSelection() { public FeeRatesSource getFeeRatesSource() {
return feeRateSelection; return feeRatesSource;
} }
public void setFeeRateSelection(FeeRateSelection feeRateSelection) { public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
this.feeRateSelection = feeRateSelection; this.feeRatesSource = feeRatesSource;
flush();
}
public FeeRatesSelection getFeeRatesSelection() {
return feeRatesSelection;
}
public void setFeeRatesSelection(FeeRatesSelection feeRatesSelection) {
this.feeRatesSelection = feeRatesSelection;
flush(); flush();
} }

View file

@ -604,6 +604,11 @@ public class ElectrumServer {
targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d); targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d);
} }
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
if(feeRatesSource != null) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
}
return targetBlocksFeeRatesSats; return targetBlocksFeeRatesSats;
} catch(ElectrumServerRpcException e) { } catch(ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e); throw new ServerException(e.getMessage(), e);
@ -718,7 +723,7 @@ public class ElectrumServer {
} }
public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler { public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler {
private static final int FEE_RATES_PERIOD = 1 * 60 * 1000; private static final int FEE_RATES_PERIOD = 30 * 1000;
private final boolean subscribe; private final boolean subscribe;
private boolean firstCall = true; 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.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.*;
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.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource; 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.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -27,7 +24,7 @@ public class GeneralPreferencesController extends PreferencesDetailController {
private ComboBox<BitcoinUnit> bitcoinUnit; private ComboBox<BitcoinUnit> bitcoinUnit;
@FXML @FXML
private ComboBox<FeeRateSelection> feeRateSelection; private ComboBox<FeeRatesSource> feeRatesSource;
@FXML @FXML
private ComboBox<Currency> fiatCurrency; private ComboBox<Currency> fiatCurrency;
@ -68,16 +65,16 @@ public class GeneralPreferencesController extends PreferencesDetailController {
EventManager.get().post(new BitcoinUnitChangedEvent(newValue)); EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
}); });
if(config.getFeeRateSelection() != null) { if(config.getFeeRatesSource() != null) {
feeRateSelection.setValue(config.getFeeRateSelection()); feeRatesSource.setValue(config.getFeeRatesSource());
} else { } else {
feeRateSelection.getSelectionModel().select(0); feeRatesSource.getSelectionModel().select(1);
config.setFeeRateSelection(FeeRateSelection.BLOCK_TARGET); config.setFeeRatesSource(feeRatesSource.getValue());
} }
feeRateSelection.valueProperty().addListener((observable, oldValue, newValue) -> { feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> {
config.setFeeRateSelection(newValue); config.setFeeRatesSource(newValue);
EventManager.get().post(new FeeRateSelectionChangedEvent(newValue)); EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
}); });
if(config.getExchangeSource() != null) { if(config.getExchangeSource() != null) {

View file

@ -1,11 +1,11 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
public enum FeeRateSelection { public enum FeeRatesSelection {
BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size"); BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size");
private final String name; private final String name;
private FeeRateSelection(String name) { private FeeRatesSelection(String name) {
this.name = name; this.name = name;
} }

View file

@ -269,13 +269,14 @@ public class SendController extends WalletFormController implements Initializabl
mempoolSizeFeeRatesChart.update(mempoolHistogram); mempoolSizeFeeRatesChart.update(mempoolHistogram);
} }
FeeRateSelection feeRateSelection = Config.get().getFeeRateSelection(); FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
updateFeeRateSelection(feeRateSelection); feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
feeSelectionToggleGroup.selectToggle(feeRateSelection == FeeRateSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); updateFeeRateSelection(feeRatesSelection);
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
FeeRateSelection newFeeRateSelection = (FeeRateSelection)newValue.getUserData(); FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
Config.get().setFeeRateSelection(newFeeRateSelection); Config.get().setFeeRatesSelection(newFeeRatesSelection);
EventManager.get().post(new FeeRateSelectionChangedEvent(newFeeRateSelection)); EventManager.get().post(new FeeRatesSelectionChangedEvent(newFeeRatesSelection));
}); });
fee.setTextFormatter(new CoinTextFormatter()); fee.setTextFormatter(new CoinTextFormatter());
@ -472,8 +473,8 @@ public class SendController extends WalletFormController implements Initializabl
return Collections.emptyList(); return Collections.emptyList();
} }
private void updateFeeRateSelection(FeeRateSelection feeRateSelection) { private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
boolean blockTargetSelection = (feeRateSelection == FeeRateSelection.BLOCK_TARGET); boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
targetBlocksField.setVisible(blockTargetSelection); targetBlocksField.setVisible(blockTargetSelection);
blockTargetFeeRatesChart.setVisible(blockTargetSelection); blockTargetFeeRatesChart.setVisible(blockTargetSelection);
setDefaultFeeRate(); setDefaultFeeRate();
@ -481,14 +482,15 @@ public class SendController extends WalletFormController implements Initializabl
} }
private void setDefaultFeeRate() { 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()) { 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); targetBlocks.setValue(index);
blockTargetFeeRatesChart.select(defaultTarget); blockTargetFeeRatesChart.select(defaultTarget);
setFeeRate(getTargetBlocksFeeRates().get(getTargetBlocks())); setFeeRate(defaultRate);
} else { } else {
feeRange.setValue(5.0); feeRange.setValue(Math.log(defaultRate) / Math.log(2));
setFeeRate(getFeeRangeRate()); setFeeRate(getFeeRangeRate());
} }
} }
@ -771,7 +773,7 @@ public class SendController extends WalletFormController implements Initializabl
} }
@Subscribe @Subscribe
public void feeRateSelectionChanged(FeeRateSelectionChangedEvent event) { public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
updateFeeRateSelection(event.getFeeRateSelection()); updateFeeRateSelection(event.getFeeRateSelection());
} }

View file

@ -15,7 +15,7 @@
<?import com.sparrowwallet.sparrow.net.ExchangeSource?> <?import com.sparrowwallet.sparrow.net.ExchangeSource?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?> <?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?> <?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?> <?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"> <GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.GeneralPreferencesController">
<padding> <padding>
@ -42,16 +42,17 @@
</ComboBox> </ComboBox>
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/> <HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
</Field> </Field>
<Field text="Fee rate selection:"> <Field text="Fee rates source:">
<ComboBox fx:id="feeRateSelection"> <ComboBox fx:id="feeRatesSource">
<items> <items>
<FXCollections fx:factory="observableArrayList"> <FXCollections fx:factory="observableArrayList">
<FeeRateSelection fx:constant="BLOCK_TARGET" /> <FeeRatesSource fx:constant="ELECTRUM_SERVER" />
<FeeRateSelection fx:constant="MEMPOOL_SIZE" /> <FeeRatesSource fx:constant="MEMPOOL_SPACE" />
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
</FXCollections> </FXCollections>
</items> </items>
</ComboBox> </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> </Field>
</Fieldset> </Fieldset>
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet"> <Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">

View file

@ -23,7 +23,7 @@
<?import org.controlsfx.glyphfont.Glyph?> <?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?> <?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
<?import org.controlsfx.control.SegmentedButton?> <?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"> <BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
<center> <center>
@ -67,7 +67,7 @@
<Tooltip text="Determine fee via estimated number of blocks"/> <Tooltip text="Determine fee via estimated number of blocks"/>
</tooltip> </tooltip>
<userData> <userData>
<FeeRateSelection fx:constant="BLOCK_TARGET"/> <FeeRatesSelection fx:constant="BLOCK_TARGET"/>
</userData> </userData>
</ToggleButton> </ToggleButton>
<ToggleButton fx:id="mempoolSizeToggle" text="Mempool Size" toggleGroup="$feeSelectionToggleGroup"> <ToggleButton fx:id="mempoolSizeToggle" text="Mempool Size" toggleGroup="$feeSelectionToggleGroup">
@ -75,7 +75,7 @@
<Tooltip text="Determine fee via current mempool size"/> <Tooltip text="Determine fee via current mempool size"/>
</tooltip> </tooltip>
<userData> <userData>
<FeeRateSelection fx:constant="MEMPOOL_SIZE"/> <FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
</userData> </userData>
</ToggleButton> </ToggleButton>
</buttons> </buttons>