mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add non batched electrum server rpc
This commit is contained in:
parent
79c2e29e34
commit
fa7d09c5a6
7 changed files with 247 additions and 19 deletions
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
return batchRequest.execute();
|
||||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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,14 +498,18 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
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;
|
||||
} 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<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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue