diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index 588541ed..63e92a3d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -4,7 +4,6 @@ import com.github.arteam.simplejsonrpc.client.JsonRpcClient; import com.github.arteam.simplejsonrpc.client.Transport; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; -import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; @@ -191,6 +190,24 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { } } + @Override + @SuppressWarnings("unchecked") + public Map getBlockStats(Transport transport, Set blockHeights) { + PagedBatchRequestBuilder batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(BlockStats.class); + + for(Integer height : blockHeights) { + batchRequest.add(height, "blockchain.block.stats", height); + } + + try { + return batchRequest.execute(); + } catch(JsonRpcBatchException e) { + return (Map)e.getSuccesses(); + } catch(Exception e) { + throw new ElectrumServerRpcException("Failed to retrieve block stats for block heights: " + blockHeights, e); + } + } + @Override @SuppressWarnings("unchecked") public Map getTransactions(Transport transport, Wallet wallet, Set txids) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java b/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java new file mode 100644 index 00000000..a5d2d02f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java @@ -0,0 +1,14 @@ +package com.sparrowwallet.sparrow.net; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.sparrowwallet.sparrow.BlockSummary; + +import java.util.Date; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record BlockStats(int height, String blockhash, double[] feerate_percentiles, int total_weight, int txs, long time) { + public BlockSummary toBlockSummary() { + Double medianFee = feerate_percentiles != null && feerate_percentiles.length > 0 ? feerate_percentiles[feerate_percentiles.length / 2] : null; + return new BlockSummary(height, new Date(time * 1000), medianFee, txs, total_weight); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index b9b8e871..a71b113d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -82,6 +82,8 @@ public class ElectrumServer { private static Server coreElectrumServer; + private static ServerCapability serverCapability; + private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*"); private static synchronized CloseableTransport getTransport() throws ServerException { @@ -981,6 +983,21 @@ public class ElectrumServer { } public Map getBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException { + if(serverCapability.supportsBlockStats()) { + if(height == null) { + Integer current = AppServices.getCurrentBlockHeight(); + if(current == null) { + return Collections.emptyMap(); + } + Set heights = IntStream.range(current - 1, current + 1).boxed().collect(Collectors.toSet()); + Map blockStats = electrumServerRpc.getBlockStats(getTransport(), heights); + return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary())); + } else { + Map blockStats = electrumServerRpc.getBlockStats(getTransport(), Set.of(height)); + return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary())); + } + } + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); @@ -1010,7 +1027,7 @@ public class ElectrumServer { if(current == null) { return Collections.emptyMap(); } - Set references = IntStream.range(current - 4, current + 1) + Set references = IntStream.range(current - 1, current + 1) .mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet()); Map blockHeaders = getBlockHeaders(null, references); return blockHeaders.keySet().stream() @@ -1219,7 +1236,7 @@ public class ElectrumServer { } if(server.startsWith("cormorant")) { - return new ServerCapability(true); + return new ServerCapability(true, false, true); } if(server.startsWith("electrs/")) { @@ -1405,7 +1422,7 @@ public class ElectrumServer { firstCall = false; //If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching. - ServerCapability serverCapability = getServerCapability(serverVersion); + serverCapability = getServerCapability(serverVersion); if(serverCapability.supportsBatching()) { log.debug("Upgrading to batched JSON-RPC"); electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks()); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java index 92e2be52..fb82af61 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java @@ -26,6 +26,8 @@ public interface ElectrumServerRpc { Map getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights); + Map getBlockStats(Transport transport, Set blockHeights); + Map getTransactions(Transport transport, Wallet wallet, Set txids); Map getVerboseTransactions(Transport transport, Set txids, String scriptHash); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java index cc1b8d81..ff1a5900 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java @@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHash; -class ScriptHashTx { +public class ScriptHashTx { public static final ScriptHashTx ERROR_TX = new ScriptHashTx() { @Override public BlockTransactionHash getBlockchainTransactionHash() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java index 6bb33c6b..98c7099b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java @@ -5,15 +5,29 @@ import com.sparrowwallet.sparrow.AppServices; public class ServerCapability { private final boolean supportsBatching; private final int maxTargetBlocks; + private final boolean supportsRecentMempool; + private final boolean supportsBlockStats; public ServerCapability(boolean supportsBatching) { - this.supportsBatching = supportsBatching; - this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast(); + this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast()); } public ServerCapability(boolean supportsBatching, int maxTargetBlocks) { this.supportsBatching = supportsBatching; this.maxTargetBlocks = maxTargetBlocks; + this.supportsRecentMempool = false; + this.supportsBlockStats = false; + } + + public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) { + this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats); + } + + public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) { + this.supportsBatching = supportsBatching; + this.maxTargetBlocks = maxTargetBlocks; + this.supportsRecentMempool = supportsRecentMempool; + this.supportsBlockStats = supportsBlockStats; } public boolean supportsBatching() { @@ -23,4 +37,12 @@ public class ServerCapability { public int getMaxTargetBlocks() { return maxTargetBlocks; } + + public boolean supportsRecentMempool() { + return supportsRecentMempool; + } + + public boolean supportsBlockStats() { + return supportsBlockStats; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index d3947a48..7a16e03b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -177,6 +177,29 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { return result; } + @Override + public Map getBlockStats(Transport transport, Set blockHeights) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(Integer blockHeight : blockHeights) { + try { + BlockStats blockStats = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> + client.createRequest().returnAs(BlockStats.class).method("blockchain.block.stats").id(idCounter.incrementAndGet()).params(blockHeight).execute()); + result.put(blockHeight, blockStats); + } catch(ServerException e) { + //If there is an error with the server connection, don't keep trying - this may take too long given many blocks + throw new ElectrumServerRpcException("Failed to retrieve block stats for block height: " + blockHeight, e); + } catch(JsonRpcException e) { + log.warn("Failed to retrieve block stats for block height: " + blockHeight + (e.getErrorMessage() != null ? " (" + e.getErrorMessage().getMessage() + ")" : "")); + } catch(Exception e) { + log.warn("Failed to retrieve block stats for block height: " + blockHeight + " (" + e.getMessage() + ")"); + } + } + + return result; + } + @Override public Map getTransactions(Transport transport, Wallet wallet, Set txids) { JsonRpcClient client = new JsonRpcClient(transport); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java index 55121033..bedfc12d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java @@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppServices; import java.util.Date; @JsonIgnoreProperties(ignoreUnknown = true) -class VerboseTransaction { +public class VerboseTransaction { public String blockhash; public long blocktime; public int confirmations; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java index 008d3c14..398f15e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java @@ -7,6 +7,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.sparrow.net.BlockStats; import java.util.List; import java.util.Map; @@ -48,6 +49,9 @@ public interface BitcoindClientService { @JsonRpcMethod("getblockheader") VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash); + @JsonRpcMethod("getblockstats") + BlockStats getBlockStats(@JsonRpcParam("blockhash") int hash_or_height); + @JsonRpcMethod("getrawtransaction") Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java index d9659dc1..9a3e71cf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java @@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.SparrowWallet; import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent; import com.sparrowwallet.drongo.Version; +import com.sparrowwallet.sparrow.net.BlockStats; import com.sparrowwallet.sparrow.net.cormorant.Cormorant; import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*; import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry; @@ -157,6 +158,17 @@ public class ElectrumServerService { } } + @JsonRpcMethod("blockchain.block.stats") + public BlockStats getBlockStats(@JsonRpcParam("height") int height) throws BitcoindIOException, BlockNotFoundException { + try { + return bitcoindClient.getBitcoindService().getBlockStats(height); + } catch(JsonRpcException e) { + throw new BlockNotFoundException(e.getErrorMessage()); + } catch(IllegalStateException e) { + throw new BitcoindIOException(e); + } + } + @JsonRpcMethod("blockchain.transaction.get") @SuppressWarnings("unchecked") public Object getTransaction(@JsonRpcParam("tx_hash") String tx_hash, @JsonRpcParam("verbose") @JsonRpcOptional boolean verbose) throws BitcoindIOException, TransactionNotFoundException {