diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index eb199f03..649402e4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -18,6 +18,7 @@ import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.*; +import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionView; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java b/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java index 654002fe..25bba22d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java @@ -140,13 +140,17 @@ public class TitledDescriptionPane extends TitledPane { } private void removeArrow() { + removeArrow(0); + } + + private void removeArrow(int count) { Platform.runLater(() -> { Node arrow = this.lookup(".arrow"); if (arrow != null) { arrow.setVisible(false); arrow.setManaged(false); - } else { - removeArrow(); + } else if(count < 20) { + removeArrow(count+1); } }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java index bbd757c4..0f5aa09a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java @@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import java.util.List; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BlockHeaderTip.java b/src/main/java/com/sparrowwallet/sparrow/net/BlockHeaderTip.java new file mode 100644 index 00000000..4b833efd --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/BlockHeaderTip.java @@ -0,0 +1,18 @@ +package com.sparrowwallet.sparrow.net; + +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.BlockHeader; + +class BlockHeaderTip { + public int height; + public String hex; + + public BlockHeader getBlockHeader() { + if(hex == null) { + return null; + } + + byte[] blockHeaderBytes = Utils.hexToBytes(hex); + return new BlockHeader(blockHeaderBytes); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java similarity index 73% rename from src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java rename to src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 793d0676..229c2f5e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1,47 +1,27 @@ -package com.sparrowwallet.sparrow.io; +package com.sparrowwallet.sparrow.net; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.github.arteam.simplejsonrpc.client.*; import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; -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.github.arteam.simplejsonrpc.server.JsonRpcServer; import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.wallet.*; -import com.sparrowwallet.sparrow.AppController; -import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; -import com.sparrowwallet.sparrow.event.NewBlockEvent; -import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.ServerException; import com.sparrowwallet.sparrow.wallet.SendController; -import javafx.application.Platform; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.SocketFactory; -import javax.net.ssl.*; import java.io.*; -import java.net.InetSocketAddress; -import java.net.Proxy; -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.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; public class ElectrumServer { @@ -630,283 +610,8 @@ public class ElectrumServer { return Utils.bytesToHex(reversed); } - private static class ScriptHashTx { - public static final ScriptHashTx ERROR_TX = new ScriptHashTx() { - @Override - public BlockTransactionHash getBlockchainTransactionHash() { - return UNFETCHABLE_BLOCK_TRANSACTION; - } - }; - - public int height; - public String tx_hash; - public long fee; - - public BlockTransactionHash getBlockchainTransactionHash() { - Sha256Hash hash = Sha256Hash.wrap(tx_hash); - return new BlockTransaction(hash, height, null, fee, null); - } - - @Override - public String toString() { - return "ScriptHashTx{height=" + height + ", tx_hash='" + tx_hash + '\'' + ", fee=" + fee + '}'; - } - } - - private static class BlockHeaderTip { - public int height; - public String hex; - - public BlockHeader getBlockHeader() { - if(hex == null) { - return null; - } - - byte[] blockHeaderBytes = Utils.hexToBytes(hex); - return new BlockHeader(blockHeaderBytes); - } - } - - @JsonIgnoreProperties(ignoreUnknown=true) - private static class VerboseTransaction { - public String blockhash; - public long blocktime; - public int confirmations; - public String hash; - public String hex; - public int locktime; - public long size; - public String txid; - public int version; - - public int getHeight() { - Integer currentHeight = AppController.getCurrentBlockHeight(); - if(currentHeight != null) { - return currentHeight - confirmations + 1; - } - - return -1; - } - - public Date getDate() { - return new Date(blocktime * 1000); - } - - public BlockTransaction getBlockTransaction() { - return new BlockTransaction(Sha256Hash.wrap(txid), getHeight(), getDate(), 0L, new Transaction(Utils.hexToBytes(hex)), blockhash == null ? null : Sha256Hash.wrap(blockhash)); - } - } - - @JsonRpcService - public static class SubscriptionService { - @JsonRpcMethod("blockchain.headers.subscribe") - public void newBlockHeaderTip(@JsonRpcParam("header") final BlockHeaderTip header) { - Platform.runLater(() -> EventManager.get().post(new NewBlockEvent(header.height, header.getBlockHeader()))); - } - - @JsonRpcMethod("blockchain.scripthash.subscribe") - public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcParam("status") final String status) { - String oldStatus = subscribedScriptHashes.put(scriptHash, status); - if(Objects.equals(oldStatus, status)) { - log.warn("Received script hash status update, but status has not changed"); - } - - Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash))); - } - } - - public static class TcpTransport implements Transport, Closeable { - public static final int DEFAULT_PORT = 50001; - - protected final HostAndPort server; - protected final SocketFactory socketFactory; - - private Socket socket; - - private String response; - - private final ReentrantLock clientRequestLock = new ReentrantLock(); - private boolean running = false; - private boolean reading = true; - - private final JsonRpcServer jsonRpcServer = new JsonRpcServer(); - private final SubscriptionService subscriptionService = new SubscriptionService(); - - public TcpTransport(HostAndPort server) { - this.server = server; - this.socketFactory = SocketFactory.getDefault(); - } - - @Override - public @NotNull String pass(@NotNull String request) throws IOException { - clientRequestLock.lock(); - try { - writeRequest(request); - return readResponse(); - } finally { - clientRequestLock.unlock(); - } - } - - private void writeRequest(String request) throws IOException { - PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))); - out.println(request); - out.flush(); - } - - private synchronized String readResponse() { - while(reading) { - try { - wait(); - } catch (InterruptedException e) { - //Restore interrupt status and continue - Thread.currentThread().interrupt(); - } - } - - reading = true; - - notifyAll(); - return response; - } - - public synchronized void readInputLoop() throws ServerException { - while(running) { - try { - String received = readInputStream(); - if(received.contains("method")) { - //Handle subscription notification - jsonRpcServer.handle(received, subscriptionService); - } else { - //Handle client's response - response = received; - reading = false; - notifyAll(); - wait(); - } - } catch(InterruptedException e) { - //Restore interrupt status and continue - Thread.currentThread().interrupt(); - } catch(IOException e) { - if(running) { - throw new ServerException(e); - } - } - } - } - - protected String readInputStream() 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; - } - - public void connect() throws ServerException { - try { - socket = createSocket(); - running = true; - } catch (IOException e) { - throw new ServerException(e); - } - } - - public boolean isConnected() { - return socket != null && running; - } - - protected Socket createSocket() throws IOException { - return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); - } - - @Override - public void close() throws IOException { - if(socket != null) { - running = false; - socket.close(); - } - } - } - - public static class TcpOverTlsTransport extends TcpTransport { - public static final int DEFAULT_PORT = 50002; - - protected 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 ProxyTcpOverTlsTransport extends TcpOverTlsTransport { - public static final int DEFAULT_PROXY_PORT = 1080; - - private final HostAndPort proxy; - - public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException { - super(server); - this.proxy = proxy; - } - - public ProxyTcpOverTlsTransport(HostAndPort server, File crtFile, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - super(server, crtFile); - this.proxy = proxy; - } - - @Override - protected Socket createSocket() throws IOException { - InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT)); - Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr)); - underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT))); - SSLSocket sslSocket = (SSLSocket)sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true); - sslSocket.startHandshake(); - - return sslSocket; - } + static Map getSubscribedScriptHashes() { + return subscribedScriptHashes; } public static class ServerVersionService extends Service> { @@ -1180,88 +885,4 @@ public class ElectrumServer { }; } } - - public enum Protocol { - TCP { - @Override - public Transport getTransport(HostAndPort server) { - return new TcpTransport(server); - } - - @Override - public Transport getTransport(HostAndPort server, File serverCert) { - return new TcpTransport(server); - } - - @Override - public Transport getTransport(HostAndPort server, HostAndPort proxy) { - throw new UnsupportedOperationException("TCP protocol does not support proxying"); - } - - @Override - public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { - throw new UnsupportedOperationException("TCP protocol does not support proxying"); - } - }, - SSL{ - @Override - public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { - return new TcpOverTlsTransport(server); - } - - @Override - public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - return new TcpOverTlsTransport(server, serverCert); - } - - @Override - public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException { - return new ProxyTcpOverTlsTransport(server, proxy); - } - - @Override - public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - return new ProxyTcpOverTlsTransport(server, serverCert, proxy); - } - }; - - public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException; - - public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; - - public abstract Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; - - public abstract Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) 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 String toUrlString(String host) { - return toUrlString(HostAndPort.fromHost(host)); - } - - public String toUrlString(String host, int port) { - return toUrlString(HostAndPort.fromParts(host, port)); - } - - public String toUrlString(HostAndPort hostAndPort) { - return toUrlString() + hostAndPort.toString(); - } - - public static Protocol getProtocol(String url) { - if(url.startsWith("tcp://")) { - return TCP; - } - if(url.startsWith("ssl://")) { - return SSL; - } - - return null; - } - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java new file mode 100644 index 00000000..87b3f791 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -0,0 +1,95 @@ +package com.sparrowwallet.sparrow.net; + +import com.github.arteam.simplejsonrpc.client.Transport; +import com.google.common.net.HostAndPort; + +import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public enum Protocol { + TCP { + @Override + public Transport getTransport(HostAndPort server) { + return new TcpTransport(server); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert) { + return new TcpTransport(server); + } + + @Override + public Transport getTransport(HostAndPort server, HostAndPort proxy) { + throw new UnsupportedOperationException("TCP protocol does not support proxying"); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { + throw new UnsupportedOperationException("TCP protocol does not support proxying"); + } + }, + SSL { + @Override + public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + return new TcpOverTlsTransport(server); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + return new TcpOverTlsTransport(server, serverCert); + } + + @Override + public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException { + return new ProxyTcpOverTlsTransport(server, proxy); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + return new ProxyTcpOverTlsTransport(server, serverCert, proxy); + } + }; + + public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException; + + public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; + + public abstract Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; + + public abstract Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) 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 String toUrlString(String host) { + return toUrlString(HostAndPort.fromHost(host)); + } + + public String toUrlString(String host, int port) { + return toUrlString(HostAndPort.fromParts(host, port)); + } + + public String toUrlString(HostAndPort hostAndPort) { + return toUrlString() + hostAndPort.toString(); + } + + public static Protocol getProtocol(String url) { + if(url.startsWith("tcp://")) { + return TCP; + } + if(url.startsWith("ssl://")) { + return SSL; + } + + return null; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java new file mode 100644 index 00000000..be5e46c3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java @@ -0,0 +1,41 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; + +import javax.net.ssl.SSLSocket; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { + public static final int DEFAULT_PROXY_PORT = 1080; + + private final HostAndPort proxy; + + public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException { + super(server); + this.proxy = proxy; + } + + public ProxyTcpOverTlsTransport(HostAndPort server, File crtFile, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + super(server, crtFile); + this.proxy = proxy; + } + + @Override + protected Socket createSocket() throws IOException { + InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT)); + Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr)); + underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT))); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true); + sslSocket.startHandshake(); + + return sslSocket; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java new file mode 100644 index 00000000..f1c0281e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java @@ -0,0 +1,28 @@ +package com.sparrowwallet.sparrow.net; + +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.BlockTransactionHash; + +class ScriptHashTx { + public static final ScriptHashTx ERROR_TX = new ScriptHashTx() { + @Override + public BlockTransactionHash getBlockchainTransactionHash() { + return ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION; + } + }; + + public int height; + public String tx_hash; + public long fee; + + public BlockTransactionHash getBlockchainTransactionHash() { + Sha256Hash hash = Sha256Hash.wrap(tx_hash); + return new BlockTransaction(hash, height, null, fee, null); + } + + @Override + public String toString() { + return "ScriptHashTx{height=" + height + ", tx_hash='" + tx_hash + '\'' + ", fee=" + fee + '}'; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java new file mode 100644 index 00000000..71edffed --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java @@ -0,0 +1,33 @@ +package com.sparrowwallet.sparrow.net; + +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.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.NewBlockEvent; +import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent; +import javafx.application.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +@JsonRpcService +public class SubscriptionService { + private static final Logger log = LoggerFactory.getLogger(SubscriptionService.class); + + @JsonRpcMethod("blockchain.headers.subscribe") + public void newBlockHeaderTip(@JsonRpcParam("header") final BlockHeaderTip header) { + Platform.runLater(() -> EventManager.get().post(new NewBlockEvent(header.height, header.getBlockHeader()))); + } + + @JsonRpcMethod("blockchain.scripthash.subscribe") + public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcParam("status") final String status) { + String oldStatus = ElectrumServer.getSubscribedScriptHashes().put(scriptHash, status); + if(Objects.equals(oldStatus, status)) { + log.warn("Received script hash status update, but status has not changed"); + } + + Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash))); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java new file mode 100644 index 00000000..f45f4f6a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -0,0 +1,68 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; + +import javax.net.ssl.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +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; + +public class TcpOverTlsTransport extends TcpTransport { + public static final int DEFAULT_PORT = 50002; + + protected 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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java new file mode 100644 index 00000000..627373dc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -0,0 +1,130 @@ +package com.sparrowwallet.sparrow.net; + +import com.github.arteam.simplejsonrpc.client.Transport; +import com.github.arteam.simplejsonrpc.server.JsonRpcServer; +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.ServerException; +import org.jetbrains.annotations.NotNull; + +import javax.net.SocketFactory; +import java.io.*; +import java.net.Socket; +import java.util.concurrent.locks.ReentrantLock; + +public class TcpTransport implements Transport, Closeable { + public static final int DEFAULT_PORT = 50001; + + protected final HostAndPort server; + protected final SocketFactory socketFactory; + + private Socket socket; + + private String response; + + private final ReentrantLock clientRequestLock = new ReentrantLock(); + private boolean running = false; + private boolean reading = true; + + private final JsonRpcServer jsonRpcServer = new JsonRpcServer(); + private final SubscriptionService subscriptionService = new SubscriptionService(); + + public TcpTransport(HostAndPort server) { + this.server = server; + this.socketFactory = SocketFactory.getDefault(); + } + + @Override + public @NotNull String pass(@NotNull String request) throws IOException { + clientRequestLock.lock(); + try { + writeRequest(request); + return readResponse(); + } finally { + clientRequestLock.unlock(); + } + } + + private void writeRequest(String request) throws IOException { + PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))); + out.println(request); + out.flush(); + } + + private synchronized String readResponse() { + while(reading) { + try { + wait(); + } catch(InterruptedException e) { + //Restore interrupt status and continue + Thread.currentThread().interrupt(); + } + } + + reading = true; + + notifyAll(); + return response; + } + + public synchronized void readInputLoop() throws ServerException { + while(running) { + try { + String received = readInputStream(); + if(received.contains("method")) { + //Handle subscription notification + jsonRpcServer.handle(received, subscriptionService); + } else { + //Handle client's response + response = received; + reading = false; + notifyAll(); + wait(); + } + } catch(InterruptedException e) { + //Restore interrupt status and continue + Thread.currentThread().interrupt(); + } catch(IOException e) { + if(running) { + throw new ServerException(e); + } + } + } + } + + protected String readInputStream() 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; + } + + public void connect() throws ServerException { + try { + socket = createSocket(); + running = true; + } catch(IOException e) { + throw new ServerException(e); + } + } + + public boolean isConnected() { + return socket != null && running; + } + + protected Socket createSocket() throws IOException { + return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); + } + + @Override + public void close() throws IOException { + if(socket != null) { + running = false; + socket.close(); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java new file mode 100644 index 00000000..800bdd43 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java @@ -0,0 +1,40 @@ +package com.sparrowwallet.sparrow.net; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.sparrow.AppController; + +import java.util.Date; + +@JsonIgnoreProperties(ignoreUnknown = true) +class VerboseTransaction { + public String blockhash; + public long blocktime; + public int confirmations; + public String hash; + public String hex; + public int locktime; + public long size; + public String txid; + public int version; + + public int getHeight() { + Integer currentHeight = AppController.getCurrentBlockHeight(); + if(currentHeight != null) { + return currentHeight - confirmations + 1; + } + + return -1; + } + + public Date getDate() { + return new Date(blocktime * 1000); + } + + public BlockTransaction getBlockTransaction() { + return new BlockTransaction(Sha256Hash.wrap(txid), getHeight(), getDate(), 0L, new Transaction(Utils.hexToBytes(hex)), blockhash == null ? null : Sha256Hash.wrap(blockhash)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index b2814ddd..46510c71 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -6,7 +6,8 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.net.Protocol; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.concurrent.WorkerStateEvent; @@ -154,10 +155,10 @@ public class ServerPreferencesController extends PreferencesDetailController { String electrumServer = config.getElectrumServer(); if(electrumServer != null) { - ElectrumServer.Protocol protocol = ElectrumServer.Protocol.getProtocol(electrumServer); + Protocol protocol = Protocol.getProtocol(electrumServer); if(protocol != null) { - boolean ssl = protocol.equals(ElectrumServer.Protocol.SSL); + boolean ssl = protocol.equals(Protocol.SSL); useSsl.setSelected(ssl); certificate.setDisable(!ssl); certificateSelect.setDisable(!ssl); @@ -270,8 +271,8 @@ public class ServerPreferencesController extends PreferencesDetailController { }; } - private ElectrumServer.Protocol getProtocol() { - return (useSsl.isSelected() ? ElectrumServer.Protocol.SSL : ElectrumServer.Protocol.TCP); + private Protocol getProtocol() { + return (useSsl.isSelected() ? Protocol.SSL : Protocol.TCP); } private String getHost(String text) { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 86d6e171..a365c97e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -12,9 +12,8 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.io.Device; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.io.Storage; -import com.sparrowwallet.sparrow.wallet.TransactionEntry; import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -647,9 +646,7 @@ public class HeadersController extends TransactionFormController implements Init return; } - if(signingDevices.isEmpty()) { - signingDevices = AppController.getDevices().stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList()); - } + signingDevices.addAll(AppController.getDevices().stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList())); DeviceSignDialog dlg = new DeviceSignDialog(signingDevices.isEmpty() ? null : signingDevices, headersForm.getPsbt()); dlg.initModality(Modality.NONE); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java index e010c2b4..f03516d8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java @@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.wallet.BlockTransaction; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import javafx.fxml.FXMLLoader; import javafx.scene.Node; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java index b816e368..1e83d2ad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java @@ -14,12 +14,11 @@ import com.sparrowwallet.sparrow.control.ScriptArea; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent; import com.sparrowwallet.sparrow.event.ViewTransactionEvent; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; -import org.fxmisc.richtext.CodeArea; import tornadofx.control.Field; import tornadofx.control.Fieldset; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/PageForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/PageForm.java index e6155fd9..cf717720 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/PageForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/PageForm.java @@ -1,6 +1,6 @@ package com.sparrowwallet.sparrow.transaction; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import javafx.scene.Node; import java.io.IOException; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java index 2e01a442..e7f5b561 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java @@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.TransactionHexArea; import com.sparrowwallet.sparrow.event.*; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 0401177f..c57ed0d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -7,7 +7,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; -import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.io.Storage; import javafx.application.Platform; import org.slf4j.Logger;