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() {