retrieve and show next block median fee rate in recent blocks view where available

This commit is contained in:
Craig Raw 2025-05-22 13:35:59 +02:00
parent 52470ee6d8
commit 231eb13cee
7 changed files with 95 additions and 10 deletions

View file

@ -136,6 +136,8 @@ public class AppServices {
private static Map<Integer, Double> targetBlockFeeRates; private static Map<Integer, Double> targetBlockFeeRates;
private static Double nextBlockMedianFeeRate;
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>(); private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate; private static Double minimumRelayFeeRate;
@ -748,6 +750,10 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
} }
public static Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
}
public static double getFallbackFeeRate() { public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE; return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
} }
@ -1249,11 +1255,13 @@ public class AppServices {
if(AppServices.currentBlockHeight != null) { if(AppServices.currentBlockHeight != null) {
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5); blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
} }
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
} }
@Subscribe @Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) { public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates(); targetBlockFeeRates = event.getTargetBlockFeeRates();
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
} }
@Subscribe @Subscribe

View file

@ -57,7 +57,7 @@ public class RecentBlocksView extends Pane {
} }
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) { public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription()); tooltip.setText("Fee rates from " + feeRatesSource.getDescription());
if(getCubes() != null && !getCubes().isEmpty()) { if(getCubes() != null && !getCubes().isEmpty()) {
getCubes().getFirst().setFeeRatesSource(feeRatesSource); getCubes().getFirst().setFeeRatesSource(feeRatesSource);
} }
@ -108,7 +108,7 @@ public class RecentBlocksView extends Pane {
} }
} }
public void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) { private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
if(getCubes().isEmpty()) { if(getCubes().isEmpty()) {
return; return;
} }

View file

@ -6,12 +6,18 @@ import java.util.Map;
public class BlockSummaryEvent { public class BlockSummaryEvent {
private final Map<Integer, BlockSummary> blockSummaryMap; private final Map<Integer, BlockSummary> blockSummaryMap;
private final Double nextBlockMedianFeeRate;
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap) { public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
this.blockSummaryMap = blockSummaryMap; this.blockSummaryMap = blockSummaryMap;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
} }
public Map<Integer, BlockSummary> getBlockSummaryMap() { public Map<Integer, BlockSummary> getBlockSummaryMap() {
return blockSummaryMap; return blockSummaryMap;
} }
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
} }

View file

@ -7,13 +7,23 @@ import java.util.Set;
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent { public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates; private final Map<Integer, Double> targetBlockFeeRates;
private final Double nextBlockMedianFeeRate;
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) { public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
this(targetBlockFeeRates, mempoolRateSizes, null);
}
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
super(mempoolRateSizes); super(mempoolRateSizes);
this.targetBlockFeeRates = targetBlockFeeRates; this.targetBlockFeeRates = targetBlockFeeRates;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
} }
public Map<Integer, Double> getTargetBlockFeeRates() { public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates; return targetBlockFeeRates;
} }
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
} }

View file

@ -936,6 +936,20 @@ public class ElectrumServer {
return targetBlocksFeeRatesSats; return targetBlocksFeeRatesSats;
} }
public Double getNextBlockMedianFeeRate() {
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(feeRatesSource.supportsNetwork(Network.get())) {
try {
return feeRatesSource.getNextBlockMedianFeeRate();
} catch(Exception e) {
return null;
}
}
return null;
}
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException { public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
try { try {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks); Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
@ -1460,8 +1474,9 @@ public class ElectrumServer {
if(elapsed > FEE_RATES_PERIOD) { if(elapsed > FEE_RATES_PERIOD) {
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes(); Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
feeRatesRetrievedAt = System.currentTimeMillis(); feeRatesRetrievedAt = System.currentTimeMillis();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate);
} }
} else { } else {
closeConnection(); closeConnection();
@ -1939,7 +1954,8 @@ public class ElectrumServer {
protected FeeRatesUpdatedEvent call() throws ServerException { protected FeeRatesUpdatedEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer(); ElectrumServer electrumServer = new ElectrumServer();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null); Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null, nextBlockMedianFeeRate);
} }
}; };
} }
@ -1989,7 +2005,11 @@ public class ElectrumServer {
subscribeRecent(electrumServer); subscribeRecent(electrumServer);
} }
return new BlockSummaryEvent(blockSummaryMap); Double nextBlockMedianFeeRate = null;
if(!isBlockstorm(totalBlocks)) {
nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
}
return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate);
} }
}; };
} }
@ -2054,7 +2074,7 @@ public class ElectrumServer {
}; };
} }
}; };
broadcastService.setDelay(Duration.seconds(Math.random() * 60 )); broadcastService.setDelay(Duration.seconds(Math.random() * 60 * 10));
broadcastService.setPeriod(Duration.hours(1)); broadcastService.setPeriod(Duration.hours(1));
broadcastService.setOnSucceeded(_ -> broadcastService.cancel()); broadcastService.setOnSucceeded(_ -> broadcastService.cancel());
broadcastService.setOnFailed(_ -> broadcastService.cancel()); broadcastService.setOnFailed(_ -> broadcastService.cancel());

View file

@ -34,6 +34,12 @@ public enum FeeRatesSource {
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url); return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
} }
@Override
public Double getNextBlockMedianFeeRate() throws Exception {
String url = getApiUrl() + "v1/fees/mempool-blocks";
return requestNextBlockMedianFeeRate(this, url);
}
@Override @Override
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception { public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes()); String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
@ -130,6 +136,10 @@ public enum FeeRatesSource {
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates); public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
public Double getNextBlockMedianFeeRate() throws Exception {
throw new UnsupportedOperationException(name + " does not support retrieving the next block median fee rate");
}
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception { public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
throw new UnsupportedOperationException(name + " does not support block summaries"); throw new UnsupportedOperationException(name + " does not support block summaries");
} }
@ -199,6 +209,30 @@ public enum FeeRatesSource {
return httpClientService.requestJson(url, ThreeTierRates.class, null); return httpClientService.requestJson(url, ThreeTierRates.class, null);
} }
protected static Double requestNextBlockMedianFeeRate(FeeRatesSource feeRatesSource, String url) throws Exception {
if(log.isInfoEnabled()) {
log.info("Requesting next block median fee rate from " + url);
}
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
MempoolBlock[] mempoolBlocks = feeRatesSource.requestMempoolBlocks(url, httpClientService);
return mempoolBlocks.length > 0 ? mempoolBlocks[0].medianFee : null;
} catch (Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving next block median fee rate from " + url, e);
} else {
log.warn("Error retrieving next block median fee rate from " + url + " (" + e.getMessage() + ")");
}
throw e;
}
}
protected MempoolBlock[] requestMempoolBlocks(String url, HttpClientService httpClientService) throws Exception {
return httpClientService.requestJson(url, MempoolBlock[].class, null);
}
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception { protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
if(log.isInfoEnabled()) { if(log.isInfoEnabled()) {
log.info("Requesting block summary from " + url); log.info("Requesting block summary from " + url);
@ -309,6 +343,8 @@ public enum FeeRatesSource {
} }
} }
protected record MempoolBlock(Integer nTx, Double medianFee) {}
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) { protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
public Double getMedianFee() { public Double getMedianFee() {
return extras == null ? null : extras.medianFee(); return extras == null ? null : extras.medianFee();

View file

@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS)); recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList(); List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
if(!blockSummaries.isEmpty()) { if(!blockSummaries.isEmpty()) {
recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate()); recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate());
} }
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> { feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
@ -1411,7 +1411,12 @@ public class SendController extends WalletFormController implements Initializabl
setFeeRatePriority(getFeeRangeRate()); setFeeRatePriority(getFeeRangeRate());
} }
feeRange.updateTrackHighlight(); feeRange.updateTrackHighlight();
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
if(event.getNextBlockMedianFeeRate() != null) {
recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate());
} else {
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
}
if(updateDefaultFeeRate) { if(updateDefaultFeeRate) {
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) { if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
@ -1435,7 +1440,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void blockSummary(BlockSummaryEvent event) { public void blockSummary(BlockSummaryEvent event) {
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate())); Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getNextBlockMedianFeeRate()));
} }
@Subscribe @Subscribe