From fa7d09c5a602cf3e4096ed6b83589a12f5858adf Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 10 Aug 2020 18:58:47 +0200 Subject: [PATCH] add non batched electrum server rpc --- .../sparrow/control/WelcomeDialog.java | 8 +- .../com/sparrowwallet/sparrow/io/Config.java | 2 +- .../sparrow/net/BatchedElectrumServerRpc.java | 8 +- .../sparrow/net/ElectrumServer.java | 34 ++- .../sparrow/net/SimpleElectrumServerRpc.java | 208 ++++++++++++++++++ .../ServerPreferencesController.java | 4 + .../sparrow/preferences/server.fxml | 2 +- 7 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java index 3e9014d6..6cb9fe5b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java @@ -14,11 +14,9 @@ import org.controlsfx.control.ToggleSwitch; public class WelcomeDialog extends Dialog { private static final String[] ELECTRUM_SERVERS = new String[]{ - "ElectrumX", "https://github.com/spesmilo/electrumx", + "ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx", "electrs", "https://github.com/romanz/electrs", - "esplora-electrs", "https://github.com/Blockstream/electrs", - "Electrum Personal Server", "https://github.com/chris-belcher/electrum-personal-server", - "Bitcoin Wallet Tracker", "https://github.com/shesek/bwt"}; + "esplora-electrs", "https://github.com/Blockstream/electrs"}; private final HostServices hostServices; @@ -31,7 +29,7 @@ public class WelcomeDialog extends Dialog { dialogPane.setHeaderText("Welcome to Sparrow!"); dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); dialogPane.setPrefWidth(600); - dialogPane.setPrefHeight(500); + dialogPane.setPrefHeight(450); Image image = new Image("image/sparrow-small.png", 50, 50, false, false); if (!image.isError()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index e837b7d2..b316726b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -14,7 +14,7 @@ import java.util.List; public class Config { private static final Logger log = LoggerFactory.getLogger(Config.class); - public static final String CONFIG_FILENAME = ".config"; + public static final String CONFIG_FILENAME = "config"; private Mode mode; private BitcoinUnit bitcoinUnit; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index 4ba1b528..a0a641e5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -30,7 +30,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { public List getServerVersion(Transport transport, String clientName, String[] supportedVersions) { try { JsonRpcClient client = new JsonRpcClient(transport); - return client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", "Sparrow").param("protocol_version", supportedVersions).execute(); + return client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", clientName).param("protocol_version", supportedVersions).execute(); } catch(JsonRpcException e) { throw new ElectrumServerRpcException("Error getting server version", e); } @@ -190,7 +190,11 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { batchRequest.add(targetBlock, "blockchain.estimatefee", targetBlock); } - return batchRequest.execute(); + try { + return batchRequest.execute(); + } catch(JsonRpcBatchException e) { + throw new ElectrumServerRpcException("Error getting fee estimates", e); + } } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index e9deaa43..9a23fd3f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -31,7 +31,7 @@ public class ElectrumServer { private static final Map subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>()); - private ElectrumServerRpc electrumServerRpc = new BatchedElectrumServerRpc(); + private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc(); private static synchronized Transport getTransport() throws ServerException { if(transport == null) { @@ -88,7 +88,6 @@ public class ElectrumServer { public List getServerVersion() throws ServerException { return electrumServerRpc.getServerVersion(getTransport(), "Sparrow", SUPPORTED_VERSIONS); - //return client.createRequest().returnAsList(String.class).method("server.version").id(1).params("Sparrow", "1.4").execute(); } public String getServerBanner() throws ServerException { @@ -480,7 +479,6 @@ public class ElectrumServer { } } - @SuppressWarnings("unchecked") public Map getReferencedTransactions(Set references) throws ServerException { Set txids = new LinkedHashSet<>(references.size()); for(Sha256Hash reference : references) { @@ -500,14 +498,18 @@ public class ElectrumServer { } public Map getFeeEstimates(List targetBlocks) throws ServerException { - Map targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks); + try { + Map targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks); - Map targetBlocksFeeRatesSats = new TreeMap<>(); - for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) { - targetBlocksFeeRatesSats.put(target, targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN / 1024); + Map targetBlocksFeeRatesSats = new TreeMap<>(); + for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) { + targetBlocksFeeRatesSats.put(target, targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN / 1024); + } + + return targetBlocksFeeRatesSats; + } catch(ElectrumServerRpcException e) { + throw new ServerException(e.getMessage(), e); } - - return targetBlocksFeeRatesSats; } public Sha256Hash broadcastTransaction(Transaction transaction) throws ServerException { @@ -523,7 +525,7 @@ public class ElectrumServer { return receivedTxid; } catch(ElectrumServerRpcException | IllegalStateException e) { - throw new ServerException(e.getMessage()); + throw new ServerException(e.getMessage(), e); } } @@ -543,6 +545,10 @@ public class ElectrumServer { return subscribedScriptHashes; } + public static boolean supportsBatching(List serverVersion) { + return serverVersion.size() > 0 && serverVersion.get(0).toLowerCase().contains("electrumx"); + } + public static class ServerVersionService extends Service> { @Override protected Task> createTask() { @@ -600,6 +606,14 @@ 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. + if(supportsBatching(serverVersion)) { + log.debug("Upgrading to batched JSON-RPC"); + electrumServerRpc = new BatchedElectrumServerRpc(); + } else { + electrumServerRpc = new SimpleElectrumServerRpc(); + } + BlockHeaderTip tip; if(subscribe) { tip = electrumServer.subscribeBlockHeaders(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java new file mode 100644 index 00000000..591ce8e0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -0,0 +1,208 @@ +package com.sparrowwallet.sparrow.net; + +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.github.arteam.simplejsonrpc.client.JsonRpcClient; +import com.github.arteam.simplejsonrpc.client.Transport; +import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.sparrowwallet.drongo.protocol.Transaction.DUST_RELAY_TX_FEE; + +public class SimpleElectrumServerRpc implements ElectrumServerRpc { + private static final Logger log = LoggerFactory.getLogger(SimpleElectrumServerRpc.class); + + @Override + public void ping(Transport transport) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + client.createRequest().method("server.ping").id(1).executeNullable(); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + throw new ElectrumServerRpcException("Error pinging server", e); + } + } + + @Override + public List getServerVersion(Transport transport, String clientName, String[] supportedVersions) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + //Using 1.4 as the version number as EPS tries to parse this number to a float + return client.createRequest().returnAsList(String.class).method("server.version").id(1).params(clientName, "1.4").execute(); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + throw new ElectrumServerRpcException("Error getting server version", e); + } + } + + @Override + public String getServerBanner(Transport transport) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute(); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + throw new ElectrumServerRpcException("Error getting server banner", e); + } + } + + @Override + public BlockHeaderTip subscribeBlockHeaders(Transport transport) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + return client.createRequest().returnAs(BlockHeaderTip.class).method("blockchain.headers.subscribe").id(1).execute(); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + throw new ElectrumServerRpcException("Error subscribing to block headers", e); + } + } + + @Override + public Map getScriptHashHistory(Transport transport, Map pathScriptHashes, boolean failOnError) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(String path : pathScriptHashes.keySet()) { + try { + ScriptHashTx[] scriptHashTxes = client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path).params(pathScriptHashes.get(path)).execute(); + result.put(path, scriptHashTxes); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + if(failOnError) { + throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); + } + + result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX}); + } + } + + return result; + } + + @Override + public Map getScriptHashMempool(Transport transport, Map pathScriptHashes, boolean failOnError) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(String path : pathScriptHashes.keySet()) { + try { + ScriptHashTx[] scriptHashTxes = client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_mempool").id(path).params(pathScriptHashes.get(path)).execute(); + result.put(path, scriptHashTxes); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + if(failOnError) { + throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); + } + + result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX}); + } + } + + return result; + } + + @Override + public Map subscribeScriptHashes(Transport transport, Map pathScriptHashes) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(String path : pathScriptHashes.keySet()) { + try { + client.createRequest().method("blockchain.scripthash.subscribe").id(path).params(pathScriptHashes.get(path)).executeNullable(); + result.put(path, ""); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + //Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed. + throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); + } + } + + return result; + } + + @Override + public Map getBlockHeaders(Transport transport, Set blockHeights) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(Integer blockHeight : blockHeights) { + try { + String blockHeader = client.createRequest().returnAs(String.class).method("blockchain.block.header").id(blockHeight).params(blockHeight).execute(); + result.put(blockHeight, blockHeader); + } catch(IllegalStateException | IllegalArgumentException e) { + log.warn("Failed to retrieve block header for block height: " + blockHeight + " (" + e.getMessage() + ")"); + } catch(JsonRpcException e) { + log.warn("Failed to retrieve block header for block height: " + blockHeight + " (" + e.getErrorMessage() + ")"); + } + } + + return result; + } + + @Override + public Map getTransactions(Transport transport, Set txids) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(String txid : txids) { + try { + String rawTxHex = client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(txid).params(txid).execute(); + result.put(txid, rawTxHex); + } catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) { + result.put(txid, Sha256Hash.ZERO_HASH.toString()); + } + } + + return result; + } + + @Override + public Map getVerboseTransactions(Transport transport, Set txids) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(String txid : txids) { + try { + VerboseTransaction verboseTransaction = client.createRequest().returnAs(VerboseTransaction.class).method("blockchain.transaction.get").id(txid).params(txid, true).execute(); + result.put(txid, verboseTransaction); + } catch(IllegalStateException | IllegalArgumentException e) { + log.warn("Error retrieving transaction: " + txid + " (" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()) + ")"); + } catch(JsonRpcException e) { + log.warn("Error retrieving transaction: " + txid + " (" + e.getErrorMessage() + ")"); + } + } + + return result; + } + + @Override + public Map getFeeEstimates(Transport transport, List targetBlocks) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(Integer targetBlock : targetBlocks) { + try { + Double targetBlocksFeeRateBtcKb = client.createRequest().returnAs(Double.class).method("blockchain.estimatefee").id(targetBlock).params(targetBlock).execute(); + result.put(targetBlock, targetBlocksFeeRateBtcKb); + } catch(IllegalStateException | IllegalArgumentException e) { + log.warn("Failed to retrieve fee rate for target blocks: " + targetBlock + " (" + e.getMessage() + ")"); + result.put(targetBlock, DUST_RELAY_TX_FEE); + } catch(JsonRpcException e) { + throw new ElectrumServerRpcException("Failed to retrieve fee rate for target blocks: " + targetBlock, e); + } + } + + return result; + } + + @Override + public String broadcastTransaction(Transport transport, String txHex) { + try { + JsonRpcClient client = new JsonRpcClient(transport); + return client.createRequest().returnAs(String.class).method("blockchain.transaction.broadcast").id(1).params(txHex).execute(); + } catch(IllegalStateException | IllegalArgumentException e) { + throw new ElectrumServerRpcException(e.getMessage(), e); + } catch(JsonRpcException e) { + throw new ElectrumServerRpcException(e.getErrorMessage().getMessage(), e); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index 46510c71..11481206 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -125,6 +125,7 @@ public class ServerPreferencesController extends PreferencesDetailController { } }); + testConnection.setDisable(ElectrumServer.isConnected()); testConnection.setOnAction(event -> { testResults.setText("Connecting to " + config.getElectrumServer() + "..."); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); @@ -199,6 +200,9 @@ public class ServerPreferencesController extends PreferencesDetailController { testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, Color.rgb(80, 161, 79))); if(serverVersion != null) { testResults.setText("Connected to " + serverVersion.get(0) + " on protocol version " + serverVersion.get(1)); + if(ElectrumServer.supportsBatching(serverVersion)) { + testResults.setText(testResults.getText() + "\nBatched RPC enabled."); + } } if(serverBanner != null) { testResults.setText(testResults.getText() + "\nServer Banner: " + serverBanner); diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml index e6d47df2..3d05f50e 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml @@ -33,7 +33,7 @@ - +