add silent payments rpc methods, capability check and notification dispatch
What changed, and why it matters
This commit adds support for a new Bitcoin privacy feature called 'silent payments' in the Sparrow Wallet. It lets the wallet ask compatible servers to scan for silent-payment transactions and report them back. The change also improves how Sparrow detects what a server can do, and it makes one error type more specific when Bitcoin Core cannot handle silent payments. There is no obvious security bug in the diff, but it does involve sending private scanning keys to an external server, which is inherent to the silent-payments protocol and is now done through a new RPC call.
Review the silent-payments key-handling path to ensure scan_priv_key_hex is zeroed or minimized in memory where feasible, confirm server.features responses cannot spoof capability flags, and verify that Cormorant's server.features pruning field cannot be abused to force a rescan. Treat as a feature commit unless additional context shows a vulnerability.
Security signals we found
New RPC methods transmit scan_priv_key_hex to external Electrum server (protocol-required but sensitive)
New server.features capability handshake parses silent_payments version list
New exception type distinguishes unsupported vs. general bitcoind failures
No input sanitization changes visible; relies on existing JsonRpcClient/RetryLogic
No CVE, advisory, or vendor security disclosure present in commit or references
Evidence from the diff
The patch implements Electrum protocol extensions for BIP352 silent payments: server.features querying, blockchain.silentpayments.subscribe/unsubscribe RPCs, ServerCapability detection of silent_payments versions, and SilentPaymentsNotificationEvent dispatch. It also splits CormorantBitcoindException into a new CormorantBitcoindUnsupportedException for unsupported operations, exposes prune height in BitcoindClient, and updates Cormorant’s server.version range to 1.4-1.6. No memory-safety, injection, or authentication flaws are visible in the diff. The main security-relevant behavior is that scan_priv_key is transmitted to the Electrum server, which is required by the protocol but increases key-exposure surface.
Changed components
BatchedElectrumServerRpcSimpleElectrumServerRpcElectrumServerServerCapabilityServerFeaturesSubscriptionServiceCormorant ElectrumServerServiceBitcoindClientInspect captured patch +301 / −24
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 0000000..71ece42
--- /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.
+ * <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;
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java
index 8f98911..6725fd4 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<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) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
index d6fc103..64846b1 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<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();
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java
index fb82af6..5c9163e 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<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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java
index fb15e24..4946e7e 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<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;
+ }
}
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 0000000..61d7555
--- /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<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 + '}';
+ }
+}
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 0000000..0585086
--- /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 0000000..be4b2f9
--- /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 9117933..97850f9 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<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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java
index b7b16a3..9870ca6 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<SilentPaymentsTx> 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 f39d736..a135397 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 0808695..70c32d7 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 0000000..d829838
--- /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 0b63c79..c89a8af 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 65e3529..7f8a615 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<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 {
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 74897ea..a0f17e7 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<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() {
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.