From abe1f9c9e56d6d91086dffed743951e332dc4e57 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 29 Apr 2026 13:28:44 +0200 Subject: [PATCH] add silent payments rpc methods, capability check and notification dispatch --- drongo | 2 +- .../SilentPaymentsNotificationEvent.java | 40 +++++++++++++++++++ .../sparrow/net/BatchedElectrumServerRpc.java | 33 +++++++++++++++ .../sparrow/net/ElectrumServer.java | 39 +++++++++++++----- .../sparrow/net/ElectrumServerRpc.java | 6 +++ .../sparrow/net/ServerCapability.java | 38 +++++++++++++++--- .../sparrow/net/ServerFeatures.java | 30 ++++++++++++++ .../net/SilentPaymentsSubscription.java | 22 ++++++++++ .../sparrow/net/SilentPaymentsTx.java | 20 ++++++++++ .../sparrow/net/SimpleElectrumServerRpc.java | 33 +++++++++++++++ .../sparrow/net/SubscriptionService.java | 6 +++ .../sparrow/net/cormorant/Cormorant.java | 3 +- .../cormorant/bitcoind/BitcoindClient.java | 10 +++++ ...CormorantBitcoindUnsupportedException.java | 7 ++++ .../electrum/ElectrumServerRunnable.java | 2 +- .../electrum/ElectrumServerService.java | 30 ++++++++++++-- .../cormorant/electrum/RequestHandler.java | 4 +- 17 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/SilentPaymentsNotificationEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/ServerFeatures.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsSubscription.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsTx.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/CormorantBitcoindUnsupportedException.java diff --git a/drongo b/drongo index ee2f3391..9c53c6d6 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit ee2f3391361bd40aa2b9eb756e9848db5087a2e8 +Subproject commit 9c53c6d6d087e3bc833fbc90a718fa56bc1cf508 diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SilentPaymentsNotificationEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SilentPaymentsNotificationEvent.java new file mode 100644 index 00000000..71ece42e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/SilentPaymentsNotificationEvent.java @@ -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. + *

+ * 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 history; + + public SilentPaymentsNotificationEvent(SilentPaymentsSubscription subscription, double progress, List history) { + this.subscription = subscription; + this.progress = progress; + this.history = history; + } + + public SilentPaymentsSubscription getSubscription() { + return subscription; + } + + public double getProgress() { + return progress; + } + + public List getHistory() { + return history; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index 8f98911a..6725fd4b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -64,6 +64,17 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { } } + @Override + public ServerFeatures getServerFeatures(Transport transport) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + return new RetryLogic(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(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(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 getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index d6fc103b..64846b15 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -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 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> { @@ -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 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(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java index fb82af61..5c9163ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java @@ -12,6 +12,8 @@ public interface ElectrumServerRpc { List 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 unsubscribeScriptHashes(Transport transport, Set scriptHashes); + String subscribeSilentPayments(Transport transport, Wallet wallet, String scanPrivKeyHex, String spendPubKeyHex, Object start, int[] labels); + + String unsubscribeSilentPayments(Transport transport, String scanPrivKeyHex, String spendPubKeyHex); + Map getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights); Map getBlockStats(Transport transport, Set blockHeights); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java index fb15e240..4946e7ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java @@ -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 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 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; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerFeatures.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerFeatures.java new file mode 100644 index 00000000..61d7555b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerFeatures.java @@ -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 rather than Map 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 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 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 + '}'; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsSubscription.java b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsSubscription.java new file mode 100644 index 00000000..0585086b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsSubscription.java @@ -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 + '}'; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsTx.java b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsTx.java new file mode 100644 index 00000000..be4b2f96 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsTx.java @@ -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 + "'}"; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index 91179334..97850f98 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -76,6 +76,17 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { } } + @Override + public ServerFeatures getServerFeatures(Transport transport) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + return new RetryLogic(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(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(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 getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights) { JsonRpcClient client = new JsonRpcClient(transport); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java index b7b16a33..9870ca6e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java @@ -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 history) { + Platform.runLater(() -> EventManager.get().post(new SilentPaymentsNotificationEvent(subscription, progress, history))); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java index f39d7368..a135397b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java @@ -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); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java index 08086952..70c32d71 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java @@ -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; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/CormorantBitcoindUnsupportedException.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/CormorantBitcoindUnsupportedException.java new file mode 100644 index 00000000..d829838c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/CormorantBitcoindUnsupportedException.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.sparrow.net.cormorant.bitcoind; + +public class CormorantBitcoindUnsupportedException extends CormorantBitcoindException { + public CormorantBitcoindUnsupportedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerRunnable.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerRunnable.java index 0b63c790..c89a8afb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerRunnable.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerRunnable.java @@ -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); } 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 65e3529d..7f8a6159 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 @@ -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 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 { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/RequestHandler.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/RequestHandler.java index 74897ea4..a0f17e73 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/RequestHandler.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/RequestHandler.java @@ -23,9 +23,9 @@ public class RequestHandler implements Runnable { private boolean headersSubscribed; private final Set 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() {