add silent payments rpc methods, capability check and notification dispatch

This commit is contained in:
Craig Raw 2026-04-29 13:28:44 +02:00
parent 69aef9c228
commit abe1f9c9e5
17 changed files with 301 additions and 24 deletions

2
drongo

@ -1 +1 @@
Subproject commit ee2f3391361bd40aa2b9eb756e9848db5087a2e8
Subproject commit 9c53c6d6d087e3bc833fbc90a718fa56bc1cf508

View File

@ -0,0 +1,40 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.net.SilentPaymentsSubscription;
import com.sparrowwallet.sparrow.net.SilentPaymentsTx;
import java.util.List;
/**
* Posted when a blockchain.silentpayments.subscribe notification is received from an Electrum server.
* Carries the subscription payload, the progress (with 1.0 marking the historical-scan-complete
* moment per BIP352), and the list of silent-payment transactions in this batch.
* <p>
* The event holds no wallet reference; consumers (typically a WalletForm) match
* {@link SilentPaymentsSubscription#address} against their wallet's silent-payment address and
* ignore the event otherwise. This mirrors how WalletNodeHistoryChangedEvent is consumed and avoids
* pinning closed wallets through static event-router state.
*/
public class SilentPaymentsNotificationEvent {
private final SilentPaymentsSubscription subscription;
private final double progress;
private final List<SilentPaymentsTx> history;
public SilentPaymentsNotificationEvent(SilentPaymentsSubscription subscription, double progress, List<SilentPaymentsTx> history) {
this.subscription = subscription;
this.progress = progress;
this.history = history;
}
public SilentPaymentsSubscription getSubscription() {
return subscription;
}
public double getProgress() {
return progress;
}
public List<SilentPaymentsTx> getHistory() {
return history;
}
}

View File

@ -64,6 +64,17 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
}
@Override
public ServerFeatures getServerFeatures(Transport transport) {
try {
JsonRpcClient client = new JsonRpcClient(transport);
return new RetryLogic<ServerFeatures>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() ->
client.createRequest().returnAs(ServerFeatures.class).method("server.features").id(idCounter.incrementAndGet()).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Error getting server features", e);
}
}
@Override
public BlockHeaderTip subscribeBlockHeaders(Transport transport) {
try {
@ -171,6 +182,28 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
}
@Override
public String subscribeSilentPayments(Transport transport, Wallet wallet, String scanPrivKeyHex, String spendPubKeyHex, Object start, int[] labels) {
JsonRpcClient client = new JsonRpcClient(transport);
try {
return new RetryLogic<String>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.silentpayments.subscribe").id(idCounter.incrementAndGet()).params(scanPrivKeyHex, spendPubKeyHex, start, labels).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to subscribe to silent payments for wallet " + wallet.getName(), e);
}
}
@Override
public String unsubscribeSilentPayments(Transport transport, String scanPrivKeyHex, String spendPubKeyHex) {
JsonRpcClient client = new JsonRpcClient(transport);
try {
return new RetryLogic<String>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.silentpayments.unsubscribe").id(idCounter.incrementAndGet()).params(scanPrivKeyHex, spendPubKeyHex).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to unsubscribe silent payments", e);
}
}
@Override
@SuppressWarnings("unchecked")
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {

View File

@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindException;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindUnsupportedException;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.application.Platform;
@ -173,6 +174,10 @@ public class ElectrumServer {
return electrumServerRpc.getServerVersion(getTransport(), "Sparrow", SUPPORTED_VERSIONS);
}
public ServerFeatures getServerFeatures() throws ServerException {
return electrumServerRpc.getServerFeatures(getTransport());
}
public String getServerBanner() throws ServerException {
return electrumServerRpc.getServerBanner(getTransport());
}
@ -1234,6 +1239,12 @@ public class ElectrumServer {
return subscribedScriptHashes;
}
public static void requireSilentPaymentsSupport() {
if(serverCapability == null || !serverCapability.supportsSilentPayments()) {
throw new ElectrumServerRpcException("This server does not support silent payments");
}
}
public static String getSubscribedScriptHashStatus(String scriptHash) {
List<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
if(existingStatuses != null && !existingStatuses.isEmpty()) {
@ -1256,15 +1267,15 @@ public class ElectrumServer {
if(!serverVersion.isEmpty()) {
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
if(server.contains("electrumx")) {
return new ServerCapability(true, true);
return new ServerCapability(true, true, true);
}
if(server.startsWith("frigate")) {
return new ServerCapability(true, true);
return new ServerCapability(true, true, true);
}
if(server.startsWith("cormorant")) {
return new ServerCapability(true, false, true, false);
return new ServerCapability(true, false, true, false, true);
}
if(server.startsWith("electrs/")) {
@ -1276,7 +1287,7 @@ public class ElectrumServer {
try {
Version version = new Version(electrsVersion);
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
return new ServerCapability(true, true);
return new ServerCapability(true, true, true);
}
} catch(Exception e) {
//ignore
@ -1292,7 +1303,7 @@ public class ElectrumServer {
try {
Version version = new Version(fulcrumVersion);
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
return new ServerCapability(true, true);
return new ServerCapability(true, true, true);
}
} catch(Exception e) {
//ignore
@ -1311,7 +1322,7 @@ public class ElectrumServer {
Version version = new Version(mempoolElectrsVersion);
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
return new ServerCapability(true, 25, false);
return new ServerCapability(true, 25, false, true);
}
} catch(Exception e) {
//ignore
@ -1319,11 +1330,11 @@ public class ElectrumServer {
}
if(server.startsWith("electrumpersonalserver")) {
return new ServerCapability(false, false);
return new ServerCapability(false, false, false);
}
}
return new ServerCapability(false, true);
return new ServerCapability(false, true, true);
}
public static class ServerVersionService extends Service<List<String>> {
@ -1386,6 +1397,8 @@ public class ElectrumServer {
ElectrumServer.cormorant = new Cormorant(subscribe);
ElectrumServer.coreElectrumServer = cormorant.start();
}
} catch(CormorantBitcoindUnsupportedException e) {
throw new ServerException(e.getMessage());
} catch(CormorantBitcoindException e) {
ElectrumServer.cormorant = null;
log.debug("Cannot start cormorant: " + e.getMessage() + ". Starting BWT...");
@ -1459,13 +1472,21 @@ public class ElectrumServer {
List<String> serverVersion = electrumServer.getServerVersion();
firstCall = false;
//If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching.
serverCapability = getServerCapability(serverVersion);
if(serverCapability.supportsBatching()) {
log.debug("Upgrading to batched JSON-RPC");
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());
}
if(serverCapability.supportsServerFeatures()) {
try {
ServerFeatures features = electrumServer.getServerFeatures();
serverCapability.withServerFeatures(features);
} catch(ElectrumServerRpcException e) {
log.debug("Call to server.features failed for " + serverVersion, e);
}
}
BlockHeaderTip tip;
if(subscribe) {
tip = electrumServer.subscribeBlockHeaders();

View File

@ -12,6 +12,8 @@ public interface ElectrumServerRpc {
List<String> getServerVersion(Transport transport, String clientName, String[] supportedVersions);
ServerFeatures getServerFeatures(Transport transport);
String getServerBanner(Transport transport);
BlockHeaderTip subscribeBlockHeaders(Transport transport);
@ -24,6 +26,10 @@ public interface ElectrumServerRpc {
Map<String, Boolean> unsubscribeScriptHashes(Transport transport, Set<String> scriptHashes);
String subscribeSilentPayments(Transport transport, Wallet wallet, String scanPrivKeyHex, String spendPubKeyHex, Object start, int[] labels);
String unsubscribeSilentPayments(Transport transport, String scanPrivKeyHex, String spendPubKeyHex);
Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights);
Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights);

View File

@ -2,35 +2,42 @@ package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.sparrow.AppServices;
import java.util.Collections;
import java.util.List;
public class ServerCapability {
private final boolean supportsBatching;
private final int maxTargetBlocks;
private final boolean supportsRecentMempool;
private final boolean supportsBlockStats;
private final boolean supportsUnsubscribe;
private final boolean supportsServerFeatures;
private List<Integer> supportedSilentPaymentsVersions = Collections.emptyList();
public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe) {
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe);
public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe, boolean supportsServerFeatures) {
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe, supportsServerFeatures);
}
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe) {
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe, boolean supportsServerFeatures) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = false;
this.supportsBlockStats = false;
this.supportsUnsubscribe = supportsUnsubscribe;
this.supportsServerFeatures = supportsServerFeatures;
}
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe);
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe, boolean supportsServerFeatures) {
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe, supportsServerFeatures);
}
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe, boolean supportsServerFeatures) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = supportsRecentMempool;
this.supportsBlockStats = supportsBlockStats;
this.supportsUnsubscribe = supportsUnsubscribe;
this.supportsServerFeatures = supportsServerFeatures;
}
public boolean supportsBatching() {
@ -52,4 +59,23 @@ public class ServerCapability {
public boolean supportsUnsubscribe() {
return supportsUnsubscribe;
}
public boolean supportsServerFeatures() {
return supportsServerFeatures;
}
public List<Integer> getSupportedSilentPaymentsVersions() {
return supportedSilentPaymentsVersions;
}
public boolean supportsSilentPayments() {
return supportedSilentPaymentsVersions.contains(0);
}
public ServerCapability withServerFeatures(ServerFeatures features) {
if(features != null && features.silent_payments != null) {
this.supportedSilentPaymentsVersions = List.copyOf(features.silent_payments);
}
return this;
}
}

View File

@ -0,0 +1,30 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ServerFeatures {
//hosts is typed as Map<String, Object> rather than Map<String, HostInfo> because the wire shape
//varies in practice: most servers return {host: {tcp_port: N, ssl_port: N}} per the Electrum spec,
//but some (electrs) return {host: N} (a bare port number) or omit fields. Sparrow doesn't read this
//(it's reserved for inbound deserialization compatibility); cormorant writes the spec-conformant
//nested-map shape via a manually-constructed Map.
public Map<String, Object> hosts;
public String genesis_hash;
public String hash_function;
public String server_version;
public String protocol_min;
public String protocol_max;
public Integer pruning;
public List<Integer> silent_payments;
public ServerFeatures() {}
@Override
public String toString() {
return "ServerFeatures{server_version='" + server_version + "', protocol_min='" + protocol_min + "', protocol_max='" + protocol_max + "', silent_payments=" + silent_payments + '}';
}
}

View File

@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.net;
import java.util.Arrays;
public class SilentPaymentsSubscription {
public String address;
public Integer[] labels;
public int start_height;
public SilentPaymentsSubscription() {}
public SilentPaymentsSubscription(String address, Integer[] labels, int start_height) {
this.address = address;
this.labels = labels;
this.start_height = start_height;
}
@Override
public String toString() {
return "SilentPaymentsSubscription{address='" + address + "', labels=" + Arrays.toString(labels) + ", start_height=" + start_height + '}';
}
}

View File

@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.net;
public class SilentPaymentsTx {
public int height;
public String tx_hash;
public String tweak_key;
public SilentPaymentsTx() {}
public SilentPaymentsTx(int height, String tx_hash, String tweak_key) {
this.height = height;
this.tx_hash = tx_hash;
this.tweak_key = tweak_key;
}
@Override
public String toString() {
return "SilentPaymentsTx{height=" + height + ", tx_hash='" + tx_hash + "', tweak_key='" + tweak_key + "'}";
}
}

View File

@ -76,6 +76,17 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
}
}
@Override
public ServerFeatures getServerFeatures(Transport transport) {
try {
JsonRpcClient client = new JsonRpcClient(transport);
return new RetryLogic<ServerFeatures>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
client.createRequest().returnAs(ServerFeatures.class).method("server.features").id(idCounter.incrementAndGet()).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Error getting server features", e);
}
}
@Override
public BlockHeaderTip subscribeBlockHeaders(Transport transport) {
try {
@ -170,6 +181,28 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
return result;
}
@Override
public String subscribeSilentPayments(Transport transport, Wallet wallet, String scanPrivKeyHex, String spendPubKeyHex, Object start, int[] labels) {
JsonRpcClient client = new JsonRpcClient(transport);
try {
return new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.silentpayments.subscribe").id(idCounter.incrementAndGet()).params(scanPrivKeyHex, spendPubKeyHex, start, labels).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to subscribe to silent payments for wallet " + wallet.getName(), e);
}
}
@Override
public String unsubscribeSilentPayments(Transport transport, String scanPrivKeyHex, String spendPubKeyHex) {
JsonRpcClient client = new JsonRpcClient(transport);
try {
return new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.silentpayments.unsubscribe").id(idCounter.incrementAndGet()).params(scanPrivKeyHex, spendPubKeyHex).execute());
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to unsubscribe silent payments", e);
}
}
@Override
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);

View File

@ -7,6 +7,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
import com.google.common.collect.Iterables;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.NewBlockEvent;
import com.sparrowwallet.sparrow.event.SilentPaymentsNotificationEvent;
import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent;
import javafx.application.Platform;
import org.slf4j.Logger;
@ -40,4 +41,9 @@ public class SubscriptionService {
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash, status)));
}
@JsonRpcMethod("blockchain.silentpayments.subscribe")
public void silentPaymentsUpdate(@JsonRpcParam("subscription") final SilentPaymentsSubscription subscription, @JsonRpcParam("progress") final double progress, @JsonRpcParam("history") final List<SilentPaymentsTx> history) {
Platform.runLater(() -> EventManager.get().post(new SilentPaymentsNotificationEvent(subscription, progress, history)));
}
}

View File

@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.net.Protocol;
import com.sparrowwallet.sparrow.net.ServerException;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindException;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindUnsupportedException;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.ImportFailedException;
import com.sparrowwallet.sparrow.net.cormorant.electrum.ElectrumServerRunnable;
import org.slf4j.Logger;
@ -37,7 +38,7 @@ public class Cormorant {
public Server start() throws CormorantBitcoindException {
if(useWallets && AppServices.get().getOpenWallets().keySet().stream().anyMatch(wallet -> wallet.getPolicyType() == PolicyType.SINGLE_SP)) {
throw new CormorantBitcoindException("Scanning silent payment wallets is not currently supported with Bitcoin Core");
throw new CormorantBitcoindUnsupportedException("Scanning silent payment wallets is not currently supported with Bitcoin Core");
}
bitcoindClient = new BitcoindClient(useWallets);

View File

@ -76,6 +76,7 @@ public class BitcoindClient {
private final boolean useWallets;
private boolean pruned;
private Integer pruneHeight;
private boolean legacyWalletExists;
private final Lock syncingLock = new ReentrantLock();
@ -119,6 +120,7 @@ public class BitcoindClient {
BlockchainInfo blockchainInfo = getBitcoindService().getBlockchainInfo();
pruned = blockchainInfo.pruned();
pruneHeight = blockchainInfo.pruneheight();
VerboseBlockHeader blockHeader = getBitcoindService().getBlockHeader(blockchainInfo.bestblockhash());
tip = blockHeader.getBlockHeader();
timer.schedule(new PollTask(), 5000, 5000);
@ -645,6 +647,14 @@ public class BitcoindClient {
return useWallets;
}
public boolean isPruned() {
return pruned;
}
public Integer getPruneHeight() {
return pruneHeight;
}
public Store getStore() {
return store;
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.net.cormorant.bitcoind;
public class CormorantBitcoindUnsupportedException extends CormorantBitcoindException {
public CormorantBitcoindUnsupportedException(String message) {
super(message);
}
}

View File

@ -49,7 +49,7 @@ public class ElectrumServerRunnable implements Runnable {
}
throw new RuntimeException("Error accepting client connection", e);
}
RequestHandler requestHandler = new RequestHandler(clientSocket, bitcoindClient);
RequestHandler requestHandler = new RequestHandler(clientSocket, bitcoindClient, getPort());
this.threadPool.execute(requestHandler);
}

View File

@ -11,6 +11,8 @@ 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.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerFeatures;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry;
@ -22,27 +24,30 @@ import java.util.*;
@JsonRpcService
public class ElectrumServerService {
private static final Logger log = LoggerFactory.getLogger(ElectrumServerService.class);
private static final Version VERSION = new Version("1.4");
private static final Version MIN_VERSION = new Version("1.4");
private static final Version MAX_VERSION = new Version("1.6");
private static final long VSIZE_BIN_WIDTH = 50000;
private static final double DEFAULT_FEE_RATE = 0.00001d;
private final BitcoindClient bitcoindClient;
private final RequestHandler requestHandler;
private final int electrumPort;
public ElectrumServerService(BitcoindClient bitcoindClient, RequestHandler requestHandler) {
public ElectrumServerService(BitcoindClient bitcoindClient, RequestHandler requestHandler, int electrumPort) {
this.bitcoindClient = bitcoindClient;
this.requestHandler = requestHandler;
this.electrumPort = electrumPort;
}
@JsonRpcMethod("server.version")
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String[] protocolVersion) throws UnsupportedVersionException {
String version = protocolVersion.length > 1 ? protocolVersion[1] : protocolVersion[0];
Version clientVersion = new Version(version);
if(clientVersion.compareTo(VERSION) < 0) {
if(clientVersion.compareTo(MIN_VERSION) < 0) {
throw new UnsupportedVersionException(version);
}
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, MIN_VERSION.get());
}
@JsonRpcMethod("server.banner")
@ -50,6 +55,23 @@ public class ElectrumServerService {
return Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION + "\n" + bitcoindClient.getNetworkInfo().subversion() + (bitcoindClient.getNetworkInfo().networkactive() ? "" : " (disconnected)");
}
@JsonRpcMethod("server.features")
public ServerFeatures getServerFeatures() throws BitcoindIOException {
try {
ServerFeatures features = new ServerFeatures();
features.hosts = Map.of(ElectrumServer.CORE_ELECTRUM_HOST, Map.of("tcp_port", electrumPort));
features.genesis_hash = bitcoindClient.getBitcoindService().getBlockHash(0);
features.hash_function = "sha256";
features.server_version = Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION;
features.protocol_min = MIN_VERSION.get();
features.protocol_max = MAX_VERSION.get();
features.pruning = bitcoindClient.isPruned() ? bitcoindClient.getPruneHeight() : null;
return features;
} catch(IllegalStateException e) {
throw new BitcoindIOException(e);
}
}
@JsonRpcMethod("blockchain.estimatefee")
public Double estimateFee(@JsonRpcParam("number") int blocks) throws BitcoindIOException {
try {

View File

@ -23,9 +23,9 @@ public class RequestHandler implements Runnable {
private boolean headersSubscribed;
private final Set<String> scriptHashesSubscribed = new HashSet<>();
public RequestHandler(Socket clientSocket, BitcoindClient bitcoindClient) {
public RequestHandler(Socket clientSocket, BitcoindClient bitcoindClient, int electrumPort) {
this.clientSocket = clientSocket;
this.electrumServerService = new ElectrumServerService(bitcoindClient, this);
this.electrumServerService = new ElectrumServerService(bitcoindClient, this, electrumPort);
}
public void run() {