diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index e5adc238..53df02ac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -136,6 +136,8 @@ public class AppServices { private static Map targetBlockFeeRates; + private static Double nextBlockMedianFeeRate; + private static final TreeMap> mempoolHistogram = new TreeMap<>(); private static Double minimumRelayFeeRate; @@ -748,6 +750,10 @@ public class AppServices { return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); } + public static Double getNextBlockMedianFeeRate() { + return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate; + } + public static double getFallbackFeeRate() { return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE; } @@ -1249,11 +1255,13 @@ public class AppServices { if(AppServices.currentBlockHeight != null) { blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5); } + nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate(); } @Subscribe public void feesUpdated(FeeRatesUpdatedEvent event) { targetBlockFeeRates = event.getTargetBlockFeeRates(); + nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate(); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index 653c2e46..bb0df858 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -57,7 +57,7 @@ public class RecentBlocksView extends Pane { } 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()) { getCubes().getFirst().setFeeRatesSource(feeRatesSource); } @@ -108,7 +108,7 @@ public class RecentBlocksView extends Pane { } } - public void addNewBlock(List latestBlocks, Double currentFeeRate) { + private void addNewBlock(List latestBlocks, Double currentFeeRate) { if(getCubes().isEmpty()) { return; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java index 4cd74e14..973560b1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java @@ -6,12 +6,18 @@ import java.util.Map; public class BlockSummaryEvent { private final Map blockSummaryMap; + private final Double nextBlockMedianFeeRate; - public BlockSummaryEvent(Map blockSummaryMap) { + public BlockSummaryEvent(Map blockSummaryMap, Double nextBlockMedianFeeRate) { this.blockSummaryMap = blockSummaryMap; + this.nextBlockMedianFeeRate = nextBlockMedianFeeRate; } public Map getBlockSummaryMap() { return blockSummaryMap; } + + public Double getNextBlockMedianFeeRate() { + return nextBlockMedianFeeRate; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java index 82a99958..660d882c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java @@ -7,13 +7,23 @@ import java.util.Set; public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent { private final Map targetBlockFeeRates; + private final Double nextBlockMedianFeeRate; public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes) { + this(targetBlockFeeRates, mempoolRateSizes, null); + } + + public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes, Double nextBlockMedianFeeRate) { super(mempoolRateSizes); this.targetBlockFeeRates = targetBlockFeeRates; + this.nextBlockMedianFeeRate = nextBlockMedianFeeRate; } public Map getTargetBlockFeeRates() { return targetBlockFeeRates; } + + public Double getNextBlockMedianFeeRate() { + return nextBlockMedianFeeRate; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 88972ada..0c1c3f5f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -936,6 +936,20 @@ public class ElectrumServer { 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 getDefaultFeeEstimates(List targetBlocks) throws ServerException { try { Map targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks); @@ -1460,8 +1474,9 @@ public class ElectrumServer { if(elapsed > FEE_RATES_PERIOD) { Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); + Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate(); feeRatesRetrievedAt = System.currentTimeMillis(); - return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); + return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate); } } else { closeConnection(); @@ -1939,7 +1954,8 @@ public class ElectrumServer { protected FeeRatesUpdatedEvent call() throws ServerException { ElectrumServer electrumServer = new ElectrumServer(); Map 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); } - 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.setOnSucceeded(_ -> broadcastService.cancel()); broadcastService.setOnFailed(_ -> broadcastService.cancel()); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index 5b99f782..4eebee6e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -34,6 +34,12 @@ public enum FeeRatesSource { return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url); } + @Override + public Double getNextBlockMedianFeeRate() throws Exception { + String url = getApiUrl() + "v1/fees/mempool-blocks"; + return requestNextBlockMedianFeeRate(this, url); + } + @Override public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception { String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes()); @@ -130,6 +136,10 @@ public enum FeeRatesSource { public abstract Map getBlockTargetFeeRates(Map 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 { throw new UnsupportedOperationException(name + " does not support block summaries"); } @@ -199,6 +209,30 @@ public enum FeeRatesSource { 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 { if(log.isInfoEnabled()) { 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) { public Double getMedianFee() { return extras == null ? null : extras.medianFee(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 720c6b04..97271657 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS)); List blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList(); if(!blockSummaries.isEmpty()) { - recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate()); + recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate()); } feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> { @@ -1411,7 +1411,12 @@ public class SendController extends WalletFormController implements Initializabl setFeeRatePriority(getFeeRangeRate()); } feeRange.updateTrackHighlight(); - recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates()); + + if(event.getNextBlockMedianFeeRate() != null) { + recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate()); + } else { + recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates()); + } if(updateDefaultFeeRate) { if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) { @@ -1435,7 +1440,7 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe 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