Skip to content

Commit

Permalink
add non batched electrum server rpc
Browse files Browse the repository at this point in the history
  • Loading branch information
craigraw committed Aug 10, 2020
1 parent 79c2e29 commit fa7d09c
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@

public class WelcomeDialog extends Dialog<Mode> {
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;

Expand All @@ -31,7 +29,7 @@ public WelcomeDialog(HostServices services) {
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()) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/sparrowwallet/sparrow/io/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void ping(Transport transport) {
public List<String> 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);
}
Expand Down Expand Up @@ -190,7 +190,11 @@ public Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> t
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
Expand Down
34 changes: 24 additions & 10 deletions src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ElectrumServer {

private static final Map<String, String> 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) {
Expand Down Expand Up @@ -88,7 +88,6 @@ public void ping() throws ServerException {

public List<String> 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 {
Expand Down Expand Up @@ -480,7 +479,6 @@ public void calculateNodeHistory(Wallet wallet, Map<WalletNode, Set<BlockTransac
}
}

@SuppressWarnings("unchecked")
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references) throws ServerException {
Set<String> txids = new LinkedHashSet<>(references.size());
for(Sha256Hash reference : references) {
Expand All @@ -500,14 +498,18 @@ public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Has
}

public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
try {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);

Map<Integer, Double> targetBlocksFeeRatesSats = new TreeMap<>();
for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) {
targetBlocksFeeRatesSats.put(target, targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN / 1024);
}
Map<Integer, Double> targetBlocksFeeRatesSats = new TreeMap<>();
for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) {
targetBlocksFeeRatesSats.put(target, targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN / 1024);
}

return targetBlocksFeeRatesSats;
return targetBlocksFeeRatesSats;
} catch(ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e);
}
}

public Sha256Hash broadcastTransaction(Transaction transaction) throws ServerException {
Expand All @@ -523,7 +525,7 @@ public Sha256Hash broadcastTransaction(Transaction transaction) throws ServerExc

return receivedTxid;
} catch(ElectrumServerRpcException | IllegalStateException e) {
throw new ServerException(e.getMessage());
throw new ServerException(e.getMessage(), e);
}
}

Expand All @@ -543,6 +545,10 @@ static Map<String, String> getSubscribedScriptHashes() {
return subscribedScriptHashes;
}

public static boolean supportsBatching(List<String> serverVersion) {
return serverVersion.size() > 0 && serverVersion.get(0).toLowerCase().contains("electrumx");
}

public static class ServerVersionService extends Service<List<String>> {
@Override
protected Task<List<String>> createTask() {
Expand Down Expand Up @@ -600,6 +606,14 @@ protected FeeRatesUpdatedEvent call() throws ServerException {
List<String> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<String, ScriptHashTx[]> 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<String, ScriptHashTx[]> getScriptHashMempool(Transport transport, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<String, ScriptHashTx[]> 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<String, String> subscribeScriptHashes(Transport transport, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<String, String> 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<Integer, String> getBlockHeaders(Transport transport, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<Integer, String> 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<String, String> getTransactions(Transport transport, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<String, String> 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<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<String, VerboseTransaction> 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<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks) {
JsonRpcClient client = new JsonRpcClient(transport);

Map<Integer, Double> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public void initializeView(Config config) {
}
});

testConnection.setDisable(ElectrumServer.isConnected());
testConnection.setOnAction(event -> {
testResults.setText("Connecting to " + config.getElectrumServer() + "...");
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
Expand Down Expand Up @@ -199,6 +200,9 @@ private void showConnectionSuccess(List<String> serverVersion, String serverBann
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<UnlabeledToggleSwitch fx:id="useSsl"/>
</Field>
<Field text="Certificate:" styleClass="label-button">
<TextField fx:id="certificate" editable="false" promptText="Optional server certificate (.crt)"/>
<TextField fx:id="certificate" promptText="Optional server certificate (.crt)"/>
<Button fx:id="certificateSelect" maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Ed">
<graphic>
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
Expand Down

0 comments on commit fa7d09c

Please sign in to comment.