mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
add block summary service
This commit is contained in:
parent
c77f52f7f6
commit
3d85491e6b
9 changed files with 460 additions and 10 deletions
|
|
@ -26,6 +26,8 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
|||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
|
|
@ -43,7 +45,6 @@ import javafx.scene.Scene;
|
|||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Screen;
|
||||
|
|
@ -67,6 +68,8 @@ import java.time.ZonedDateTime;
|
|||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
|
@ -105,6 +108,8 @@ public class AppServices {
|
|||
|
||||
private TrayManager trayManager;
|
||||
|
||||
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
|
||||
|
||||
private static Image windowIcon;
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
|
|
@ -127,6 +132,8 @@ public class AppServices {
|
|||
|
||||
private static BlockHeader latestBlockHeader;
|
||||
|
||||
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
|
@ -183,6 +190,12 @@ public class AppServices {
|
|||
private AppServices(Application application, InteractionServices interactionServices) {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
|
||||
newBlockSubject.buffer(4, TimeUnit.SECONDS)
|
||||
.filter(newBlockEvents -> !newBlockEvents.isEmpty())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(this::fetchBlockSummaries, exception -> log.error("Error fetching block summaries", exception));
|
||||
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
|
|
@ -481,6 +494,19 @@ public class AppServices {
|
|||
}
|
||||
}
|
||||
|
||||
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
||||
blockSummaryService.setOnSucceeded(_ -> {
|
||||
EventManager.get().post(blockSummaryService.getValue());
|
||||
});
|
||||
blockSummaryService.setOnFailed(failedState -> {
|
||||
log.error("Error fetching block summaries", failedState.getSource().getException());
|
||||
});
|
||||
blockSummaryService.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isTorRunning() {
|
||||
return Tor.getDefault() != null;
|
||||
}
|
||||
|
|
@ -706,6 +732,10 @@ public class AppServices {
|
|||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Map<Integer, BlockSummary> getBlockSummaries() {
|
||||
return blockSummaries;
|
||||
}
|
||||
|
||||
public static Double getDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||
|
|
@ -1185,6 +1215,10 @@ public class AppServices {
|
|||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
|
||||
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
@ -1199,6 +1233,15 @@ public class AppServices {
|
|||
latestBlockHeader = event.getBlockHeader();
|
||||
String status = "Updating to new block height " + event.getHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
newBlockSubject.onNext(event);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void blockSummary(BlockSummaryEvent event) {
|
||||
blockSummaries.putAll(event.getBlockSummaryMap());
|
||||
if(AppServices.currentBlockHeight != null) {
|
||||
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
|||
65
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
65
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BlockSummary {
|
||||
private final Integer height;
|
||||
private final Date timestamp;
|
||||
private final Double medianFee;
|
||||
private final Integer transactionCount;
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp) {
|
||||
this(height, timestamp, null, null);
|
||||
}
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount) {
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.medianFee = medianFee;
|
||||
this.transactionCount = transactionCount;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Optional<Double> getMedianFee() {
|
||||
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
||||
}
|
||||
|
||||
public Optional<Integer> getTransactionCount() {
|
||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||
}
|
||||
|
||||
public String getElapsed() {
|
||||
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
||||
if(elapsed < 0) {
|
||||
return "now";
|
||||
} else if(elapsed < 60) {
|
||||
return elapsed + "s";
|
||||
} else if(elapsed < 3600) {
|
||||
return elapsed / 60 + "m";
|
||||
} else if(elapsed < 86400) {
|
||||
return elapsed / 3600 + "h";
|
||||
} else {
|
||||
return elapsed / 86400 + "d";
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getElapsed() + ":" + getMedianFee();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class BlockSummaryEvent {
|
||||
private final Map<Integer, BlockSummary> blockSummaryMap;
|
||||
|
||||
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap) {
|
||||
this.blockSummaryMap = blockSummaryMap;
|
||||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getBlockSummaryMap() {
|
||||
return blockSummaryMap;
|
||||
}
|
||||
}
|
||||
|
|
@ -151,6 +151,27 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Boolean> unsubscribeScriptHashes(Transport transport, Set<String> scriptHashes) {
|
||||
PagedBatchRequestBuilder<String, Boolean> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(String.class).returnType(Boolean.class);
|
||||
|
||||
for(String scriptHash : scriptHashes) {
|
||||
batchRequest.add(scriptHash, "blockchain.scripthash.unsubscribe", scriptHash);
|
||||
}
|
||||
|
||||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
log.warn("Failed to unsubscribe from script hashes: " + e.getErrors().keySet(), e);
|
||||
Map<String, Boolean> unsubscribedScriptHashes = scriptHashes.stream().collect(Collectors.toMap(s -> s, _ -> true));
|
||||
unsubscribedScriptHashes.keySet().removeIf(scriptHash -> e.getErrors().containsKey(scriptHash));
|
||||
return unsubscribedScriptHashes;
|
||||
} catch(Exception e) {
|
||||
log.warn("Failed to unsubscribe from script hashes: " + scriptHashes, e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.bip47.PaymentCode;
|
|||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
|
|
@ -32,11 +33,13 @@ import org.slf4j.LoggerFactory;
|
|||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ElectrumServer {
|
||||
|
|
@ -946,6 +949,71 @@ public class ElectrumServer {
|
|||
return Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getRecentBlockSummaryMap() throws ServerException {
|
||||
return getBlockSummaryMap(null, null);
|
||||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException {
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
|
||||
if(feeRatesSource.supportsNetwork(Network.get())) {
|
||||
try {
|
||||
if(blockHeader == null) {
|
||||
return feeRatesSource.getRecentBlockSummaries();
|
||||
} else {
|
||||
Map<Integer, BlockSummary> blockSummaryMap = new HashMap<>();
|
||||
BlockSummary blockSummary = feeRatesSource.getBlockSummary(Sha256Hash.twiceOf(blockHeader.bitcoinSerialize()));
|
||||
if(blockSummary != null && blockSummary.getHeight() != null) {
|
||||
blockSummaryMap.put(blockSummary.getHeight(), blockSummary);
|
||||
}
|
||||
return blockSummaryMap;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
return getServerBlockSummaryMap(height, blockHeader);
|
||||
}
|
||||
} else {
|
||||
return getServerBlockSummaryMap(height, blockHeader);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, BlockSummary> getServerBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException {
|
||||
if(blockHeader == null || height == null) {
|
||||
Integer current = AppServices.getCurrentBlockHeight();
|
||||
if(current == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Set<BlockTransactionHash> references = IntStream.range(current - 4, current + 1)
|
||||
.mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet());
|
||||
Map<Integer, BlockHeader> blockHeaders = getBlockHeaders(null, references);
|
||||
return blockHeaders.keySet().stream()
|
||||
.collect(Collectors.toMap(java.util.function.Function.identity(), v -> new BlockSummary(v, blockHeaders.get(v).getTimeAsDate())));
|
||||
} else {
|
||||
Map<Integer, BlockSummary> blockSummaryMap = new HashMap<>();
|
||||
blockSummaryMap.put(height, new BlockSummary(height, blockHeader.getTimeAsDate()));
|
||||
return blockSummaryMap;
|
||||
}
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getRecentMempoolTransactions() {
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
|
||||
if(feeRatesSource.supportsNetwork(Network.get())) {
|
||||
try {
|
||||
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
|
||||
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
|
||||
setReferences.put(recentTransactions.getFirst(), null);
|
||||
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
|
||||
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
|
||||
} catch(Exception e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public Sha256Hash broadcastTransactionPrivately(Transaction transaction) throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.isUsingProxy()) {
|
||||
|
|
@ -1809,6 +1877,85 @@ public class ElectrumServer {
|
|||
}
|
||||
}
|
||||
|
||||
public static class BlockSummaryService extends Service<BlockSummaryEvent> {
|
||||
private final List<NewBlockEvent> newBlockEvents;
|
||||
|
||||
public BlockSummaryService(List<NewBlockEvent> newBlockEvents) {
|
||||
this.newBlockEvents = newBlockEvents;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<BlockSummaryEvent> createTask() {
|
||||
return new Task<>() {
|
||||
protected BlockSummaryEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Integer, BlockSummary> blockSummaryMap = new LinkedHashMap<>();
|
||||
|
||||
int maxHeight = AppServices.getBlockSummaries().keySet().stream().mapToInt(Integer::intValue).max().orElse(0);
|
||||
int startHeight = newBlockEvents.stream().mapToInt(NewBlockEvent::getHeight).min().orElse(0);
|
||||
int endHeight = newBlockEvents.stream().mapToInt(NewBlockEvent::getHeight).max().orElse(0);
|
||||
int totalBlocks = Math.max(0, endHeight - maxHeight);
|
||||
|
||||
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
||||
if(isBlockstorm(totalBlocks)) {
|
||||
for(int height = maxHeight + 1; height < endHeight; height++) {
|
||||
blockSummaryMap.put(height, new BlockSummary(height, new Date()));
|
||||
}
|
||||
} else {
|
||||
blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap());
|
||||
}
|
||||
} else {
|
||||
for(NewBlockEvent event : newBlockEvents) {
|
||||
blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader()));
|
||||
}
|
||||
}
|
||||
|
||||
Config config = Config.get();
|
||||
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
|
||||
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
|
||||
subscribeRecent(electrumServer);
|
||||
}
|
||||
|
||||
return new BlockSummaryEvent(blockSummaryMap);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isBlockstorm(int totalBlocks) {
|
||||
return Network.get() != Network.MAINNET && totalBlocks > 2;
|
||||
}
|
||||
|
||||
private final static Set<String> subscribedRecent = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private void subscribeRecent(ElectrumServer electrumServer) {
|
||||
Set<String> unsubscribeScriptHashes = new HashSet<>(subscribedRecent);
|
||||
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
|
||||
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
||||
subscribedRecent.removeAll(unsubscribeScriptHashes);
|
||||
|
||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
|
||||
for(BlockTransaction blkTx : recentTransactions) {
|
||||
for(int i = 0; i < blkTx.getTransaction().getOutputs().size() && subscribeScriptHashes.size() < 10; i++) {
|
||||
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
|
||||
String scriptHash = getScriptHash(txOutput);
|
||||
if(!subscribedScriptHashes.containsKey(scriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!subscribeScriptHashes.isEmpty()) {
|
||||
try {
|
||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||
subscribedRecent.addAll(subscribeScriptHashes.values());
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
log.debug("Error subscribing to recent mempool transactions", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletDiscoveryService extends Service<Optional<Wallet>> {
|
||||
private final List<Wallet> wallets;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ public interface ElectrumServerRpc {
|
|||
|
||||
Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes);
|
||||
|
||||
Map<String, Boolean> unsubscribeScriptHashes(Transport transport, Set<String> scriptHashes);
|
||||
|
||||
Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights);
|
||||
|
||||
Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public enum FeeRatesSource {
|
||||
ELECTRUM_SERVER("Server", false) {
|
||||
|
|
@ -24,11 +27,34 @@ public enum FeeRatesSource {
|
|||
MEMPOOL_SPACE("mempool.space", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
|
||||
String url = getApiUrl() + "v1/fees/recommended";
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
|
||||
return requestBlockSummary(this, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, BlockSummary> getRecentBlockSummaries() throws Exception {
|
||||
String url = getApiUrl() + "v1/blocks";
|
||||
return requestBlockSummaries(this, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockTransactionHash> getRecentMempoolTransactions() throws Exception {
|
||||
String url = getApiUrl() + "mempool/recent";
|
||||
return requestRecentMempoolTransactions(this, url);
|
||||
}
|
||||
|
||||
private String getApiUrl() {
|
||||
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/" : "https://mempool.space/api/";
|
||||
if(Network.get() != Network.MAINNET && supportsNetwork(Network.get())) {
|
||||
url = url.replace("/api/", "/" + Network.get().getName() + "/api/");
|
||||
}
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -101,6 +127,18 @@ public enum FeeRatesSource {
|
|||
|
||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||
|
||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||
throw new UnsupportedOperationException(name + " does not support block summaries");
|
||||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getRecentBlockSummaries() throws Exception {
|
||||
throw new UnsupportedOperationException(name + " does not support block summaries");
|
||||
}
|
||||
|
||||
public List<BlockTransactionHash> getRecentMempoolTransactions() throws Exception {
|
||||
throw new UnsupportedOperationException(name + " does not support recent mempool transactions");
|
||||
}
|
||||
|
||||
public abstract boolean supportsNetwork(Network network);
|
||||
|
||||
public String getName() {
|
||||
|
|
@ -158,6 +196,80 @@ public enum FeeRatesSource {
|
|||
return httpClientService.requestJson(url, ThreeTierRates.class, null);
|
||||
}
|
||||
|
||||
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting block summary from " + url);
|
||||
}
|
||||
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
MempoolBlockSummary mempoolBlockSummary = feeRatesSource.requestBlockSummary(url, httpClientService);
|
||||
return mempoolBlockSummary.toBlockSummary();
|
||||
} catch (Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving block summary from " + url, e);
|
||||
} else {
|
||||
log.warn("Error retrieving block summary from " + url + " (" + e.getMessage() + ")");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected MempoolBlockSummary requestBlockSummary(String url, HttpClientService httpClientService) throws Exception {
|
||||
return httpClientService.requestJson(url, MempoolBlockSummary.class, null);
|
||||
}
|
||||
|
||||
protected static Map<Integer, BlockSummary> requestBlockSummaries(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting block summaries from " + url);
|
||||
}
|
||||
|
||||
Map<Integer, BlockSummary> blockSummaryMap = new LinkedHashMap<>();
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
MempoolBlockSummary[] blockSummaries = feeRatesSource.requestBlockSummaries(url, httpClientService);
|
||||
for(MempoolBlockSummary blockSummary : blockSummaries) {
|
||||
if(blockSummary.height != null) {
|
||||
blockSummaryMap.put(blockSummary.height, blockSummary.toBlockSummary());
|
||||
}
|
||||
}
|
||||
return blockSummaryMap;
|
||||
} catch (Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving block summaries from " + url, e);
|
||||
} else {
|
||||
log.warn("Error retrieving block summaries from " + url + " (" + e.getMessage() + ")");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected MempoolBlockSummary[] requestBlockSummaries(String url, HttpClientService httpClientService) throws Exception {
|
||||
return httpClientService.requestJson(url, MempoolBlockSummary[].class, null);
|
||||
}
|
||||
|
||||
protected List<BlockTransactionHash> requestRecentMempoolTransactions(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
MempoolRecentTransaction[] recentTransactions = feeRatesSource.requestRecentMempoolTransactions(url, httpClientService);
|
||||
return Arrays.stream(recentTransactions).sorted().map(tx -> (BlockTransactionHash)new BlockTransaction(tx.txid, 0, null, tx.fee, null)).toList();
|
||||
} catch (Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving recent mempool transactions from " + url, e);
|
||||
} else {
|
||||
log.warn("Error retrieving recent mempool from " + url + " (" + e.getMessage() + ")");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected MempoolRecentTransaction[] requestRecentMempoolTransactions(String url, HttpClientService httpClientService) throws Exception {
|
||||
return httpClientService.requestJson(url, MempoolRecentTransaction[].class, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
|
@ -172,4 +284,30 @@ public enum FeeRatesSource {
|
|||
return new ThreeTierRates(recommended_fee_099/1000, recommended_fee_090/1000, recommended_fee_050/1000, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, MempoolBlockSummaryExtras extras) {
|
||||
public Double getMedianFee() {
|
||||
return extras == null ? null : extras.medianFee();
|
||||
}
|
||||
|
||||
public BlockSummary toBlockSummary() {
|
||||
if(height == null || timestamp == null) {
|
||||
throw new IllegalStateException("Height = " + height + ", timestamp = " + timestamp + ": both must be specified");
|
||||
}
|
||||
return new BlockSummary(height, new Date(timestamp * 1000), getMedianFee(), tx_count);
|
||||
}
|
||||
}
|
||||
|
||||
private record MempoolBlockSummaryExtras(Double medianFee) {}
|
||||
|
||||
protected record MempoolRecentTransaction(Sha256Hash txid, Long fee, Long vsize) implements Comparable<MempoolRecentTransaction> {
|
||||
private Double getFeeRate() {
|
||||
return fee == null || vsize == null ? 0.0d : (double)fee / vsize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MempoolRecentTransaction o) {
|
||||
return Double.compare(o.getFeeRate(), getFeeRate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,9 +123,9 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
|||
for(String path : pathScriptHashes.keySet()) {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path));
|
||||
try {
|
||||
String scriptHash = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
String scriptHashStatus = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
|
||||
result.put(path, scriptHash);
|
||||
result.put(path, scriptHashStatus);
|
||||
} catch(Exception e) {
|
||||
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to path: " + path, e);
|
||||
|
|
@ -135,6 +135,24 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
|||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Boolean> unsubscribeScriptHashes(Transport transport, Set<String> scriptHashes) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
|
||||
Map<String, Boolean> result = new LinkedHashMap<>();
|
||||
for(String scriptHash : scriptHashes) {
|
||||
try {
|
||||
Boolean wasSubscribed = new RetryLogic<Boolean>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(Boolean.class).method("blockchain.scripthash.unsubscribe").id(idCounter.incrementAndGet()).params(scriptHash).executeNullable());
|
||||
result.put(scriptHash, wasSubscribed);
|
||||
} catch(Exception e) {
|
||||
log.warn("Failed to unsubscribe from script hash: " + scriptHash, e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ public class SubscriptionService {
|
|||
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
|
||||
List<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||
if(existingStatuses == null) {
|
||||
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||
ElectrumServer.updateSubscribedScriptHashStatus(scriptHash, status);
|
||||
log.trace("Received script hash status update for non-wallet script hash: " + scriptHash);
|
||||
} else if(status != null && existingStatuses.contains(status)) {
|
||||
log.debug("Received script hash status update, but status has not changed");
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue