Craig Raw
5 years ago
8 changed files with 466 additions and 2 deletions
@ -1 +1 @@ |
|||||
Subproject commit 11978e1f48851cd964f1c5f52a29a8e2ea432432 |
Subproject commit a32410b53831b3e5b111dd8fc82291c7ab30c732 |
@ -0,0 +1,379 @@ |
|||||
|
package com.sparrowwallet.sparrow.io; |
||||
|
|
||||
|
import com.github.arteam.simplejsonrpc.client.*; |
||||
|
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; |
||||
|
import com.github.arteam.simplejsonrpc.client.generator.CurrentTimeIdGenerator; |
||||
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod; |
||||
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; |
||||
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; |
||||
|
import com.google.common.net.HostAndPort; |
||||
|
import com.sparrowwallet.drongo.KeyPurpose; |
||||
|
import com.sparrowwallet.drongo.Utils; |
||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
||||
|
import com.sparrowwallet.drongo.protocol.Transaction; |
||||
|
import com.sparrowwallet.drongo.wallet.TransactionReference; |
||||
|
import com.sparrowwallet.drongo.wallet.Wallet; |
||||
|
import javafx.concurrent.Service; |
||||
|
import javafx.concurrent.Task; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
|
||||
|
import javax.net.SocketFactory; |
||||
|
import javax.net.ssl.*; |
||||
|
import java.io.*; |
||||
|
import java.net.Socket; |
||||
|
import java.security.*; |
||||
|
import java.security.cert.Certificate; |
||||
|
import java.security.cert.CertificateException; |
||||
|
import java.security.cert.CertificateFactory; |
||||
|
import java.security.cert.X509Certificate; |
||||
|
import java.util.*; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public class ElectrumServer { |
||||
|
private static Transport transport; |
||||
|
|
||||
|
private synchronized Transport getTransport() throws ServerException { |
||||
|
if(transport == null) { |
||||
|
try { |
||||
|
String electrumServer = Config.get().getElectrumServer(); |
||||
|
File electrumServerCert = Config.get().getElectrumServerCert(); |
||||
|
|
||||
|
if(electrumServer == null) { |
||||
|
throw new ServerException("Electrum server URL not specified"); |
||||
|
} |
||||
|
|
||||
|
if(electrumServerCert != null && !electrumServerCert.exists()) { |
||||
|
throw new ServerException("Electrum server certificate file not found"); |
||||
|
} |
||||
|
|
||||
|
Protocol protocol = Protocol.getProtocol(electrumServer); |
||||
|
if(protocol == null) { |
||||
|
throw new ServerException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString()); |
||||
|
} |
||||
|
|
||||
|
HostAndPort server = protocol.getServerHostAndPort(electrumServer); |
||||
|
transport = protocol.getTransport(server, electrumServerCert); |
||||
|
} catch (Exception e) { |
||||
|
throw new ServerException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return transport; |
||||
|
} |
||||
|
|
||||
|
public String getServerVersion() throws ServerException { |
||||
|
JsonRpcClient client = new JsonRpcClient(getTransport()); |
||||
|
List<String> serverVersion = client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", "Sparrow").param("protocol_version", "1.4").execute(); |
||||
|
return serverVersion.get(1); |
||||
|
} |
||||
|
|
||||
|
public void getHistory(Wallet wallet) throws ServerException { |
||||
|
getHistory(wallet, KeyPurpose.RECEIVE); |
||||
|
getHistory(wallet, KeyPurpose.CHANGE); |
||||
|
} |
||||
|
|
||||
|
public void getHistory(Wallet wallet, KeyPurpose keyPurpose) throws ServerException { |
||||
|
getHistory(wallet.getNode(keyPurpose).getChildren()); |
||||
|
getMempool(wallet.getNode(keyPurpose).getChildren()); |
||||
|
} |
||||
|
|
||||
|
public void getHistory(Collection<Wallet.Node> nodes) throws ServerException { |
||||
|
getReferences("blockchain.scripthash.get_history", nodes); |
||||
|
} |
||||
|
|
||||
|
public void getMempool(Collection<Wallet.Node> nodes) throws ServerException { |
||||
|
getReferences("blockchain.scripthash.get_mempool", nodes); |
||||
|
} |
||||
|
|
||||
|
public void getReferences(String method, Collection<Wallet.Node> nodes) throws ServerException { |
||||
|
try { |
||||
|
JsonRpcClient client = new JsonRpcClient(getTransport()); |
||||
|
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class); |
||||
|
for(Wallet.Node node : nodes) { |
||||
|
batchRequest.add(node.getDerivationPath(), method, getScriptHash(node)); |
||||
|
} |
||||
|
Map<String, ScriptHashTx[]> result = batchRequest.execute(); |
||||
|
|
||||
|
for(String path : result.keySet()) { |
||||
|
ScriptHashTx[] txes = result.get(path); |
||||
|
|
||||
|
Optional<Wallet.Node> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst(); |
||||
|
if(optionalNode.isPresent()) { |
||||
|
Wallet.Node node = optionalNode.get(); |
||||
|
Set<TransactionReference> references = Arrays.stream(txes).map(ScriptHashTx::getTransactionReference).collect(Collectors.toSet()); |
||||
|
|
||||
|
for(TransactionReference reference : references) { |
||||
|
if(!node.getHistory().add(reference)) { |
||||
|
Optional<TransactionReference> optionalReference = node.getHistory().stream().filter(tr -> tr.getTransactionId().equals(reference.getTransactionId())).findFirst(); |
||||
|
if(optionalReference.isPresent()) { |
||||
|
TransactionReference existingReference = optionalReference.get(); |
||||
|
if(existingReference.getHeight() < reference.getHeight()) { |
||||
|
node.getHistory().remove(existingReference); |
||||
|
node.getHistory().add(reference); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (IllegalStateException e) { |
||||
|
throw new ServerException(e.getCause()); |
||||
|
} catch (Exception e) { |
||||
|
throw new ServerException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void getReferencedTransactions(Wallet wallet) throws ServerException { |
||||
|
getReferencedTransactions(wallet, KeyPurpose.RECEIVE); |
||||
|
getReferencedTransactions(wallet, KeyPurpose.CHANGE); |
||||
|
} |
||||
|
|
||||
|
public void getReferencedTransactions(Wallet wallet, KeyPurpose keyPurpose) throws ServerException { |
||||
|
Wallet.Node purposeNode = wallet.getNode(keyPurpose); |
||||
|
Set<TransactionReference> references = new HashSet<>(); |
||||
|
for(Wallet.Node addressNode : purposeNode.getChildren()) { |
||||
|
references.addAll(addressNode.getHistory()); |
||||
|
} |
||||
|
|
||||
|
Map<String, Transaction> transactionMap = getTransactions(references); |
||||
|
wallet.getTransactions().putAll(transactionMap); |
||||
|
} |
||||
|
|
||||
|
public Map<String, Transaction> getTransactions(Set<TransactionReference> references) throws ServerException { |
||||
|
try { |
||||
|
JsonRpcClient client = new JsonRpcClient(getTransport()); |
||||
|
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); |
||||
|
for(TransactionReference reference : references) { |
||||
|
batchRequest.add(reference.getTransactionId(), "blockchain.transaction.get", reference.getTransactionId()); |
||||
|
} |
||||
|
Map<String, String> result = batchRequest.execute(); |
||||
|
|
||||
|
Map<String, Transaction> transactionMap = new HashMap<>(); |
||||
|
for(String txid : result.keySet()) { |
||||
|
byte[] rawtx = Utils.hexToBytes(result.get(txid)); |
||||
|
Transaction transaction = new Transaction(rawtx); |
||||
|
transactionMap.put(txid, transaction); |
||||
|
} |
||||
|
|
||||
|
return transactionMap; |
||||
|
} catch (IllegalStateException e) { |
||||
|
throw new ServerException(e.getCause()); |
||||
|
} catch (Exception e) { |
||||
|
throw new ServerException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void getHistory(Wallet.Node node) throws ServerException { |
||||
|
getHistory(getScriptHash(node)); |
||||
|
} |
||||
|
|
||||
|
public void getHistory(String scriptHash) throws ServerException { |
||||
|
try { |
||||
|
JsonRpcClient client = new JsonRpcClient(getTransport()); |
||||
|
List<ScriptHashTx> txList = client.onDemand(ELectrumXService.class).getHistory(scriptHash); |
||||
|
for(ScriptHashTx tx : txList) { |
||||
|
System.out.println(tx); |
||||
|
} |
||||
|
} catch (IllegalStateException e) { |
||||
|
throw new ServerException(e.getCause()); |
||||
|
} catch (Exception e) { |
||||
|
throw new ServerException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String getScriptHash(Wallet.Node node) { |
||||
|
byte[] hash = Sha256Hash.hash(node.getOutputScript().getProgram()); |
||||
|
byte[] reversed = Utils.reverseBytes(hash); |
||||
|
return Utils.bytesToHex(reversed); |
||||
|
} |
||||
|
|
||||
|
private static class ScriptHashTx { |
||||
|
public int height; |
||||
|
public String tx_hash; |
||||
|
public long fee; |
||||
|
|
||||
|
public TransactionReference getTransactionReference() { |
||||
|
return new TransactionReference(tx_hash, height, fee); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "ScriptHashTx{" + |
||||
|
"height=" + height + |
||||
|
", tx_hash='" + tx_hash + '\'' + |
||||
|
", fee=" + fee + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@JsonRpcService |
||||
|
@JsonRpcId(CurrentTimeIdGenerator.class) |
||||
|
@JsonRpcParams(ParamsType.MAP) |
||||
|
private interface ELectrumXService { |
||||
|
|
||||
|
@JsonRpcMethod("blockchain.scripthash.get_history") |
||||
|
List<ScriptHashTx> getHistory(@JsonRpcParam("scripthash") String scriptHash); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private static class TcpTransport implements Transport { |
||||
|
private static final int DEFAULT_PORT = 50001; |
||||
|
|
||||
|
protected final HostAndPort server; |
||||
|
private final SocketFactory socketFactory; |
||||
|
|
||||
|
private Socket socket; |
||||
|
|
||||
|
public TcpTransport(HostAndPort server) { |
||||
|
this.server = server; |
||||
|
this.socketFactory = SocketFactory.getDefault(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public @NotNull String pass(@NotNull String request) throws IOException { |
||||
|
if(socket == null) { |
||||
|
socket = createSocket(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
writeRequest(socket, request); |
||||
|
} catch (IOException e) { |
||||
|
socket = createSocket(); |
||||
|
writeRequest(socket, request); |
||||
|
} |
||||
|
|
||||
|
return readResponse(socket); |
||||
|
} |
||||
|
|
||||
|
private void writeRequest(Socket socket, String request) throws IOException { |
||||
|
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))); |
||||
|
out.println(request); |
||||
|
out.flush(); |
||||
|
} |
||||
|
|
||||
|
private String readResponse(Socket socket) throws IOException { |
||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); |
||||
|
String response = in.readLine(); |
||||
|
|
||||
|
if(response == null) { |
||||
|
throw new IOException("Could not connect to server at " + Config.get().getElectrumServer()); |
||||
|
} |
||||
|
|
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
protected Socket createSocket() throws IOException { |
||||
|
return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class TcpOverTlsTransport extends TcpTransport { |
||||
|
private static final int DEFAULT_PORT = 50002; |
||||
|
|
||||
|
private final SSLSocketFactory sslSocketFactory; |
||||
|
|
||||
|
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException { |
||||
|
super(server); |
||||
|
|
||||
|
TrustManager[] trustAllCerts = new TrustManager[]{ |
||||
|
new X509TrustManager() { |
||||
|
public X509Certificate[] getAcceptedIssuers() { |
||||
|
return new X509Certificate[0]; |
||||
|
} |
||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {} |
||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
SSLContext sslContext = SSLContext.getInstance("TLS"); |
||||
|
sslContext.init(null, trustAllCerts, new SecureRandom()); |
||||
|
|
||||
|
this.sslSocketFactory = sslContext.getSocketFactory(); |
||||
|
} |
||||
|
|
||||
|
public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { |
||||
|
super(server); |
||||
|
|
||||
|
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile)); |
||||
|
|
||||
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
||||
|
keyStore.load(null, null); |
||||
|
keyStore.setCertificateEntry("electrumx", certificate); |
||||
|
|
||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
||||
|
trustManagerFactory.init(keyStore); |
||||
|
|
||||
|
SSLContext sslContext = SSLContext.getInstance("TLS"); |
||||
|
sslContext.init(null, trustManagerFactory.getTrustManagers(), null); |
||||
|
|
||||
|
sslSocketFactory = sslContext.getSocketFactory(); |
||||
|
} |
||||
|
|
||||
|
protected Socket createSocket() throws IOException { |
||||
|
SSLSocket sslSocket = (SSLSocket)sslSocketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); |
||||
|
sslSocket.startHandshake(); |
||||
|
|
||||
|
return sslSocket; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class TransactionHistoryService extends Service<Boolean> { |
||||
|
private final Wallet wallet; |
||||
|
|
||||
|
public TransactionHistoryService(Wallet wallet) { |
||||
|
this.wallet = wallet; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Task<Boolean> createTask() { |
||||
|
return new Task<>() { |
||||
|
protected Boolean call() throws ServerException { |
||||
|
ElectrumServer electrumServer = new ElectrumServer(); |
||||
|
electrumServer.getHistory(wallet); |
||||
|
electrumServer.getReferencedTransactions(wallet); |
||||
|
return true; |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public enum Protocol { |
||||
|
TCP { |
||||
|
@Override |
||||
|
public Transport getTransport(HostAndPort server, File serverCert) throws IOException { |
||||
|
return new TcpTransport(server); |
||||
|
} |
||||
|
}, |
||||
|
SSL{ |
||||
|
@Override |
||||
|
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { |
||||
|
if(serverCert != null && serverCert.exists()) { |
||||
|
return new TcpOverTlsTransport(server, serverCert); |
||||
|
} else { |
||||
|
return new TcpOverTlsTransport(server); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; |
||||
|
|
||||
|
public HostAndPort getServerHostAndPort(String url) { |
||||
|
return HostAndPort.fromString(url.substring(this.toUrlString().length())); |
||||
|
} |
||||
|
|
||||
|
public String toUrlString() { |
||||
|
return toString().toLowerCase() + "://"; |
||||
|
} |
||||
|
|
||||
|
public static Protocol getProtocol(String url) { |
||||
|
if(url.startsWith("tcp://")) { |
||||
|
return TCP; |
||||
|
} |
||||
|
if(url.startsWith("ssl://")) { |
||||
|
return SSL; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
package com.sparrowwallet.sparrow.io; |
||||
|
|
||||
|
public class ServerException extends Exception { |
||||
|
public ServerException() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public ServerException(String message) { |
||||
|
super(message); |
||||
|
} |
||||
|
|
||||
|
public ServerException(Throwable cause) { |
||||
|
super(cause); |
||||
|
} |
||||
|
|
||||
|
public ServerException(String message, Throwable cause) { |
||||
|
super(message, cause); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue