add non batched electrum server rpc

This commit is contained in:
Craig Raw 2020-08-10 18:58:47 +02:00
parent 79c2e29e34
commit fa7d09c5a6
7 changed files with 247 additions and 19 deletions

View file

@ -14,11 +14,9 @@ import org.controlsfx.control.ToggleSwitch;
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;
@ -31,7 +29,7 @@ public class WelcomeDialog extends Dialog<Mode> {
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()) {

View file

@ -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;

View file

@ -30,7 +30,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
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);
}
@ -190,7 +190,11 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
batchRequest.add(targetBlock, "blockchain.estimatefee", targetBlock);
}
try {
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
throw new ElectrumServerRpcException("Error getting fee estimates", e);
}
}
@Override

View file

@ -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) {
@ -88,7 +88,6 @@ public class ElectrumServer {
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 {
@ -480,7 +479,6 @@ public class ElectrumServer {
}
}
@SuppressWarnings("unchecked")
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references) throws ServerException {
Set<String> txids = new LinkedHashSet<>(references.size());
for(Sha256Hash reference : references) {
@ -500,6 +498,7 @@ public class ElectrumServer {
}
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
try {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
Map<Integer, Double> targetBlocksFeeRatesSats = new TreeMap<>();
@ -508,6 +507,9 @@ public class ElectrumServer {
}
return targetBlocksFeeRatesSats;
} catch(ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e);
}
}
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<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() {
@ -600,6 +606,14 @@ 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.
if(supportsBatching(serverVersion)) {
log.debug("Upgrading to batched JSON-RPC");
electrumServerRpc = new BatchedElectrumServerRpc();
} else {
electrumServerRpc = new SimpleElectrumServerRpc();
}
BlockHeaderTip tip;
if(subscribe) {
tip = electrumServer.subscribeBlockHeaders();

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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" />