From 1f676927274d6a0353fd95bc730bdbdebf370c5f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 12 Sep 2022 15:44:47 +0200 Subject: [PATCH] add support for configuring server aliases, and switching servers via the tools menu --- .../sparrowwallet/sparrow/AppController.java | 64 +++++- .../sparrowwallet/sparrow/AppServices.java | 10 +- .../com/sparrowwallet/sparrow/MainApp.java | 2 +- .../sparrow/glyphfont/FontAwesome5.java | 1 + .../com/sparrowwallet/sparrow/io/Config.java | 130 ++++++++--- .../com/sparrowwallet/sparrow/io/Server.java | 99 ++++++++ .../com/sparrowwallet/sparrow/net/Bwt.java | 6 +- .../sparrow/net/ElectrumServer.java | 35 ++- .../sparrow/net/IpAddressMatcher.java | 2 +- .../sparrowwallet/sparrow/net/Protocol.java | 10 +- .../sparrow/net/PublicElectrumServer.java | 23 +- .../sparrow/net/TcpOverTlsTransport.java | 3 +- .../sparrow/net/TcpTransport.java | 6 +- .../preferences/ServerAliasDialog.java | 214 ++++++++++++++++++ .../ServerPreferencesController.java | 179 ++++++++++----- .../com/sparrowwallet/sparrow/app.fxml | 1 + .../sparrow/preferences/preferences.css | 4 + 17 files changed, 649 insertions(+), 140 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/Server.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index c3af76c5..35b75426 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -182,6 +182,9 @@ public class AppController implements Initializable { @FXML private MenuItem showPayNym; + @FXML + private Menu switchServer; + @FXML private CheckMenuItem preventSleep; private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty(); @@ -367,6 +370,7 @@ public class AppController implements Initializable { findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue); }); + configureSwitchServer(); setServerType(Config.get().getServerType()); serverToggle.setSelected(isConnected()); serverToggle.setDisable(Config.get().getServerType() == null); @@ -408,8 +412,7 @@ public class AppController implements Initializable { WelcomeDialog welcomeDialog = new WelcomeDialog(); Optional optionalMode = welcomeDialog.showAndWait(); if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) { - PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER); - preferencesDialog.showAndWait(); + openPreferences(PreferenceGroup.SERVER); } } @@ -902,7 +905,7 @@ public class AppController implements Initializable { private String getServerToggleTooltipText(Integer currentBlockHeight) { if(AppServices.isConnected()) { - return "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") + + return "Connected to " + Config.get().getServerDisplayName() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") + (Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : ""); } @@ -1284,13 +1287,17 @@ public class AppController implements Initializable { } public void openPreferences(ActionEvent event) { - PreferencesDialog preferencesDialog = new PreferencesDialog(); - preferencesDialog.showAndWait(); + openPreferences(PreferenceGroup.GENERAL); } public void openServerPreferences(ActionEvent event) { - PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER); + openPreferences(PreferenceGroup.SERVER); + } + + private void openPreferences(PreferenceGroup preferenceGroup) { + PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup); preferencesDialog.showAndWait(); + configureSwitchServer(); } public void signVerifyMessage(ActionEvent event) { @@ -1996,6 +2003,48 @@ public class AppController implements Initializable { return contextMenu; } + private void configureSwitchServer() { + switchServer.getItems().clear(); + + Config config = Config.get(); + if(config.getServerType() == ServerType.BITCOIN_CORE && config.getRecentCoreServers() != null && config.getRecentCoreServers().size() > 1) { + for(Server server : config.getRecentCoreServers()) { + switchServer.getItems().add(getSwitchServerMenuItem(ServerType.BITCOIN_CORE, server)); + } + } else if(config.getServerType() == ServerType.ELECTRUM_SERVER && config.getRecentElectrumServers() != null && config.getRecentElectrumServers().size() > 1) { + for(Server server : config.getRecentElectrumServers()) { + switchServer.getItems().add(getSwitchServerMenuItem(ServerType.ELECTRUM_SERVER, server)); + } + } + + switchServer.setVisible(!switchServer.getItems().isEmpty()); + } + + private CheckMenuItem getSwitchServerMenuItem(ServerType serverType, Server server) { + CheckMenuItem checkMenuItem = new CheckMenuItem(server.getDisplayName()); + boolean selected = (serverType == ServerType.BITCOIN_CORE ? server.equals(Config.get().getCoreServer()) : server.equals(Config.get().getElectrumServer())); + checkMenuItem.setSelected(selected); + checkMenuItem.setOnAction(event -> { + if(!selected) { + boolean online = onlineProperty().get(); + onlineProperty().set(false); + if(serverType == ServerType.BITCOIN_CORE) { + Config.get().setCoreServer(server); + } else if(serverType == ServerType.ELECTRUM_SERVER) { + Config.get().setElectrumServer(server); + } + Platform.runLater(() -> { + onlineProperty().set(online); + configureSwitchServer(); + }); + } else { + checkMenuItem.setSelected(true); + } + }); + + return checkMenuItem; + } + public void setServerType(ServerType serverType) { if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) { serverToggle.getStyleClass().add("public-server"); @@ -2424,11 +2473,12 @@ public class AppController implements Initializable { @Subscribe public void connection(ConnectionEvent event) { - String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight(); + String status = "Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight(); statusUpdated(new StatusEvent(status)); setServerToggleTooltip(event.getBlockHeight()); serverToggleStopAnimation(); setTorIcon(); + configureSwitchServer(); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 93146252..19f8bdb9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -203,7 +203,7 @@ public class AppServices { private void restartServices() { Config config = Config.get(); - if(config.hasServerAddress()) { + if(config.hasServer()) { restartService(connectionService); } @@ -268,7 +268,7 @@ public class AppServices { connectionService.setOnRunning(workerStateEvent -> { connectionService.setDelay(Duration.ZERO); if(!ElectrumServer.isConnected()) { - EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress())); + EventManager.get().post(new ConnectionStartEvent(Config.get().getServerDisplayName())); } }); connectionService.setOnSucceeded(successEvent -> { @@ -1157,7 +1157,9 @@ public class AppServices { @Subscribe public void requestConnect(RequestConnectEvent event) { - onlineProperty.set(true); + if(Config.get().hasServer()) { + onlineProperty.set(true); + } } @Subscribe @@ -1209,7 +1211,7 @@ public class AppServices { public void walletHistoryFailed(WalletHistoryFailedEvent event) { if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) { onlineProperty.set(false); - log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server..."); + log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server..."); Config.get().changePublicServer(); onlineProperty.set(true); } diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index f929d1d8..9170bc73 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -77,7 +77,7 @@ public class MainApp extends Application { } else if(Network.get() == Network.MAINNET) { Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER); List servers = PublicElectrumServer.getServers(); - Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl()); + Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getServer()); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index c816db53..f2510803 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -67,6 +67,7 @@ public class FontAwesome5 extends GlyphFont { SNOWFLAKE('\uf2dc'), SORT_NUMERIC_DOWN('\uf162'), SUN('\uf185'), + TAG('\uf02b'), THEATER_MASKS('\uf630'), TIMES_CIRCLE('\uf057'), TOGGLE_OFF('\uf204'), diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 6ac601c5..f6aeca16 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -53,14 +53,14 @@ public class Config { private Boolean hdCapture; private String webcamDevice; private ServerType serverType; - private String publicElectrumServer; - private String coreServer; - private List recentCoreServers; + private Server publicElectrumServer; + private Server coreServer; + private List recentCoreServers; private CoreAuthType coreAuthType; private File coreDataDir; private String coreAuth; - private String electrumServer; - private List recentElectrumServers; + private Server electrumServer; + private List recentElectrumServers; private File electrumServerCert; private boolean useProxy; private String proxyServer; @@ -77,6 +77,8 @@ public class Config { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(File.class, new FileSerializer()); gsonBuilder.registerTypeAdapter(File.class, new FileDeserializer()); + gsonBuilder.registerTypeAdapter(Server.class, new ServerSerializer()); + gsonBuilder.registerTypeAdapter(Server.class, new ServerDeserializer()); return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create(); } @@ -362,14 +364,18 @@ public class Config { flush(); } - public boolean hasServerAddress() { - return getServerAddress() != null && !getServerAddress().isEmpty(); + public boolean hasServer() { + return getServer() != null; } - public String getServerAddress() { + public Server getServer() { return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : (getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? getPublicElectrumServer() : getElectrumServer()); } + public String getServerDisplayName() { + return getServer() == null ? "server" : getServer().getDisplayName(); + } + public boolean requiresInternalTor() { if(isUseProxy()) { return false; @@ -379,57 +385,71 @@ public class Config { } public boolean requiresTor() { - if(!hasServerAddress()) { + if(!hasServer()) { return false; } - Protocol protocol = Protocol.getProtocol(getServerAddress()); - if(protocol == null) { - return false; - } - - return protocol.isOnionAddress(protocol.getServerHostAndPort(getServerAddress())); + return getServer().isOnionAddress(); } - public String getPublicElectrumServer() { + public Server getPublicElectrumServer() { return publicElectrumServer; } - public void setPublicElectrumServer(String publicElectrumServer) { + public void setPublicElectrumServer(Server publicElectrumServer) { this.publicElectrumServer = publicElectrumServer; flush(); } public void changePublicServer() { - List otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList()); + List otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getServer).filter(server -> !server.equals(getPublicElectrumServer())).collect(Collectors.toList()); if(!otherServers.isEmpty()) { setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size()))); } } - public String getCoreServer() { + public Server getCoreServer() { return coreServer; } - public void setCoreServer(String coreServer) { + public void setCoreServer(Server coreServer) { this.coreServer = coreServer; flush(); } - public List getRecentCoreServers() { - return recentCoreServers; + public List getRecentCoreServers() { + return recentCoreServers == null ? new ArrayList<>() : recentCoreServers; } - public void addRecentCoreServer(String coreServer) { + public boolean addRecentCoreServer(Server coreServer) { if(recentCoreServers == null) { recentCoreServers = new ArrayList<>(); } - if(!recentCoreServers.contains(coreServer)) { - recentCoreServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(coreServer))) - .findFirst().ifPresent(existingUrl -> recentCoreServers.remove(existingUrl)); + int index = getRecentCoreServers().indexOf(coreServer); + if(index < 0) { + recentCoreServers.removeIf(server -> server.getHost().equals(coreServer.getHost()) && server.getAlias() == null); recentCoreServers.add(coreServer); flush(); + return true; + } + + return false; + } + + public void removeRecentCoreServer(Server server) { + int index = getRecentCoreServers().indexOf(server); + if(index >= 0) { + recentCoreServers.remove(index); + flush(); + } + } + + public void setCoreServerAlias(Server server) { + int index = getRecentCoreServers().indexOf(server); + if(index >= 0) { + recentCoreServers.set(index, server); + flush(); } } @@ -460,37 +480,59 @@ public class Config { flush(); } - public String getElectrumServer() { + public Server getElectrumServer() { return electrumServer; } - public void setElectrumServer(String electrumServer) { + public void setElectrumServer(Server electrumServer) { this.electrumServer = electrumServer; flush(); } - public List getRecentElectrumServers() { - return recentElectrumServers; + public List getRecentElectrumServers() { + return recentElectrumServers == null ? new ArrayList<>() : recentElectrumServers; } - public void addRecentServer() { + public boolean addRecentServer() { if(serverType == ServerType.BITCOIN_CORE && coreServer != null) { - addRecentCoreServer(coreServer); + return addRecentCoreServer(coreServer); } else if(serverType == ServerType.ELECTRUM_SERVER && electrumServer != null) { - addRecentElectrumServer(electrumServer); + return addRecentElectrumServer(electrumServer); } + + return false; } - public void addRecentElectrumServer(String electrumServer) { + public boolean addRecentElectrumServer(Server electrumServer) { if(recentElectrumServers == null) { recentElectrumServers = new ArrayList<>(); } - if(!recentElectrumServers.contains(electrumServer)) { - recentElectrumServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(electrumServer))) - .findFirst().ifPresent(existingUrl -> recentElectrumServers.remove(existingUrl)); + int index = getRecentElectrumServers().indexOf(electrumServer); + if(index < 0) { + recentElectrumServers.removeIf(server -> server.getHost().equals(electrumServer.getHost()) && server.getAlias() == null); recentElectrumServers.add(electrumServer); flush(); + + return true; + } + + return false; + } + + public void removeRecentElectrumServer(Server server) { + int index = getRecentElectrumServers().indexOf(server); + if(index >= 0) { + recentElectrumServers.remove(index); + flush(); + } + } + + public void setElectrumServerAlias(Server server) { + int index = getRecentElectrumServers().indexOf(server); + if(index >= 0) { + recentElectrumServers.set(index, server); + flush(); } } @@ -595,4 +637,18 @@ public class Config { return new File(json.getAsJsonPrimitive().getAsString()); } } + + private static class ServerSerializer implements JsonSerializer { + @Override + public JsonElement serialize(Server src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + } + + private static class ServerDeserializer implements JsonDeserializer { + @Override + public Server deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Server.fromString(json.getAsJsonPrimitive().getAsString()); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Server.java b/src/main/java/com/sparrowwallet/sparrow/io/Server.java new file mode 100644 index 00000000..406ad6cc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/Server.java @@ -0,0 +1,99 @@ +package com.sparrowwallet.sparrow.io; + +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.net.Protocol; + +import java.util.Arrays; + +public class Server { + private final String url; + private final String alias; + + public Server(String url) { + this(url, null); + } + + public Server(String url, String alias) { + if(url == null) { + throw new IllegalArgumentException("Url cannot be null"); + } + + if(url.isEmpty()) { + throw new IllegalArgumentException("Url cannot be empty"); + } + + if(Protocol.getProtocol(url) == null) { + throw new IllegalArgumentException("Unknown protocol for url " + url + ", must be one of " + Arrays.toString(Protocol.values())); + } + + if(Protocol.getHost(url) == null) { + throw new IllegalArgumentException("Cannot determine host for url " + url); + } + + if(alias != null && alias.isEmpty()) { + throw new IllegalArgumentException("Server alias cannot be an empty string"); + } + + this.url = url; + this.alias = alias; + } + + public String getUrl() { + return url; + } + + public Protocol getProtocol() { + return Protocol.getProtocol(url); + } + + public HostAndPort getHostAndPort() { + return getProtocol().getServerHostAndPort(url); + } + + public String getHost() { + return getHostAndPort().getHost(); + } + + public boolean isOnionAddress() { + return Protocol.isOnionAddress(getHostAndPort()); + } + + public String getAlias() { + return alias; + } + + public String getDisplayName() { + return alias == null ? url : alias; + } + + public String toString() { + return url + (alias == null ? "" : "|" + alias); + } + + public static Server fromString(String server) { + String[] parts = server.split("\\|"); + if(parts.length >= 2) { + return new Server(parts[0], parts[1]); + } + + return new Server(parts[0], null); + } + + @Override + public boolean equals(Object o) { + if(this == o) { + return true; + } + if(o == null || getClass() != o.getClass()) { + return false; + } + + Server server = (Server)o; + return url.equals(server.url); + } + + @Override + public int hashCode() { + return url.hashCode(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java index 6b25bfae..d0905081 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -143,8 +143,8 @@ public class Bwt { bwtConfig.electrumSkipMerkle = true; Config config = Config.get(); - bwtConfig.bitcoindUrl = config.getCoreServer(); - if(bwtConfig.bitcoindUrl != null) { + if(config.getCoreServer() != null) { + bwtConfig.bitcoindUrl = config.getCoreServer().getUrl(); try { HostAndPort hostAndPort = Protocol.HTTP.getServerHostAndPort(bwtConfig.bitcoindUrl); if(hostAndPort.getHost().endsWith(".local")) { @@ -308,7 +308,7 @@ public class Bwt { Bwt.this.shutdown(); terminating = false; } else { - Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node at " + Config.get().getCoreServer() + "..."))); + Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node " + Config.get().getServerDisplayName() + "..."))); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 2b742123..13fb4175 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1,6 +1,5 @@ package com.sparrowwallet.sparrow.net; -import com.github.arteam.simplejsonrpc.client.Transport; import com.google.common.eventbus.Subscribe; import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.KeyPurpose; @@ -15,6 +14,7 @@ import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Server; import com.sparrowwallet.sparrow.paynym.PayNym; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; @@ -51,7 +51,7 @@ public class ElectrumServer { private static final Map> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>()); - private static String previousServerAddress; + private static Server previousServer; private static Map retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>()); @@ -59,14 +59,14 @@ public class ElectrumServer { private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc(); - private static String bwtElectrumServer; + private static Server bwtElectrumServer; private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed:[^\"]*)\".*"); private static synchronized CloseableTransport getTransport() throws ServerException { if(transport == null) { try { - String electrumServer = null; + Server electrumServer = null; File electrumServerCert = null; String proxyServer = null; @@ -78,8 +78,8 @@ public class ElectrumServer { throw new ServerConfigException("Could not connect to Bitcoin Core RPC"); } electrumServer = bwtElectrumServer; - if(previousServerAddress != null && previousServerAddress.contains(Bwt.ELECTRUM_HOST)) { - previousServerAddress = bwtElectrumServer; + if(previousServer != null && previousServer.getUrl().contains(Bwt.ELECTRUM_HOST)) { + previousServer = bwtElectrumServer; } } else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) { electrumServer = Config.get().getElectrumServer(); @@ -95,33 +95,30 @@ public class ElectrumServer { throw new ServerConfigException("Electrum server certificate file not found"); } - Protocol protocol = Protocol.getProtocol(electrumServer); - if(protocol == null) { - throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString()); - } + Protocol protocol = electrumServer.getProtocol(); //If changing server, don't rely on previous transaction history - if(previousServerAddress != null && !electrumServer.equals(previousServerAddress)) { + if(previousServer != null && !electrumServer.equals(previousServer)) { retrievedScriptHashes.clear(); retrievedTransactions.clear(); } - previousServerAddress = electrumServer; + previousServer = electrumServer; - HostAndPort server = protocol.getServerHostAndPort(electrumServer); - boolean localNetworkAddress = !protocol.isOnionAddress(server) && IpAddressMatcher.isLocalNetworkAddress(server.getHost()); + HostAndPort hostAndPort = electrumServer.getHostAndPort(); + boolean localNetworkAddress = !protocol.isOnionAddress(hostAndPort) && IpAddressMatcher.isLocalNetworkAddress(hostAndPort.getHost()); if(!localNetworkAddress && Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) { HostAndPort proxy = HostAndPort.fromString(proxyServer); if(electrumServerCert != null) { - transport = protocol.getTransport(server, electrumServerCert, proxy); + transport = protocol.getTransport(hostAndPort, electrumServerCert, proxy); } else { - transport = protocol.getTransport(server, proxy); + transport = protocol.getTransport(hostAndPort, proxy); } } else { if(electrumServerCert != null) { - transport = protocol.getTransport(server, electrumServerCert); + transport = protocol.getTransport(hostAndPort, electrumServerCert); } else { - transport = protocol.getTransport(server); + transport = protocol.getTransport(hostAndPort); } } } catch (Exception e) { @@ -1251,7 +1248,7 @@ public class ElectrumServer { @Subscribe public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) { if(this.isRunning()) { - ElectrumServer.bwtElectrumServer = Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr())); + ElectrumServer.bwtElectrumServer = new Server(Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr()))); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java b/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java index 36d3d882..58f7b3ca 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java @@ -97,7 +97,7 @@ public final class IpAddressMatcher { return InetAddress.getByName(address); } catch (UnknownHostException e) { - throw new IllegalArgumentException("Failed to parse address" + address, e); + throw new IllegalArgumentException("Failed to parse address: " + address, e); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index b3780f34..23dcd161 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -1,7 +1,7 @@ package com.sparrowwallet.sparrow.net; -import com.github.arteam.simplejsonrpc.client.Transport; import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.io.Server; import java.io.File; import java.io.IOException; @@ -121,6 +121,14 @@ public enum Protocol { return host != null && host.toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX); } + public static boolean isOnionAddress(Server server) { + if(server != null) { + return isOnionAddress(server.getHostAndPort()); + } + + return false; + } + public static boolean isOnionAddress(HostAndPort server) { return isOnionHost(server.getHost()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java index d8768b9c..02bfd799 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.net; import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.sparrow.io.Server; import java.util.Arrays; import java.util.List; @@ -15,23 +16,21 @@ public enum PublicElectrumServer { TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET); PublicElectrumServer(String name, String url, Network network) { - this.name = name; - this.url = url; + this.server = new Server(url, name); this.network = network; } public static final List SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET); - private final String name; - private final String url; + private final Server server; private final Network network; - public String getName() { - return name; + public Server getServer() { + return server; } public String getUrl() { - return url; + return server.getUrl(); } public Network getNetwork() { @@ -46,10 +45,10 @@ public enum PublicElectrumServer { return SUPPORTED_NETWORKS.contains(Network.get()); } - public static PublicElectrumServer fromUrl(String url) { - for(PublicElectrumServer server : values()) { - if(server.url.equals(url)) { - return server; + public static PublicElectrumServer fromServer(Server server) { + for(PublicElectrumServer publicServer : values()) { + if(publicServer.getServer().equals(server)) { + return publicServer; } } @@ -58,6 +57,6 @@ public enum PublicElectrumServer { @Override public String toString() { - return name; + return server.getAlias(); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java index 64a77121..e79fce82 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -111,7 +111,8 @@ public class TcpOverTlsTransport extends TcpTransport { protected boolean shouldSaveCertificate() { //Avoid saving the certificates for blockstream.info public servers - they change too often and encourage approval complacency - if(PublicElectrumServer.BLOCKSTREAM_INFO.getName().equals(server.getHost()) || PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getName().equals(server.getHost())) { + if(PublicElectrumServer.BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost()) + || PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())) { return false; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java index 9b551ce3..4394bd00 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -100,6 +100,10 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter { log.trace("Sending to electrum server at " + server + ": " + request); } + if(socket == null) { + throw new IllegalStateException("Socket connection has not been established."); + } + PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))); out.println(request); out.flush(); @@ -207,7 +211,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter { String response = readLine(in); if(response == null) { - throw new IOException("Could not connect to server at " + Config.get().getServerAddress()); + throw new IOException("Could not connect to server" + (Config.get().hasServer() ? " at " + Config.get().getServer().getUrl() : "")); } return response; diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java new file mode 100644 index 00000000..988c5bdc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java @@ -0,0 +1,214 @@ +package com.sparrowwallet.sparrow.preferences; + +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Server; +import com.sparrowwallet.sparrow.net.ServerType; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.event.Event; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.util.converter.DefaultStringConverter; +import org.controlsfx.glyphfont.Glyph; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.Collectors; + +public class ServerAliasDialog extends Dialog { + private final ServerType serverType; + private final TableView serverTable; + + public ServerAliasDialog(ServerType serverType) { + this.serverType = serverType; + + final DialogPane dialogPane = new ServerAliasDialogPane(); + setDialogPane(dialogPane); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + + setTitle("Server Aliases"); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + dialogPane.setHeaderText("Configure aliases for recently connected servers.\nNew servers are added to this list on successful connections."); + + Image image = new Image("/image/sparrow-small.png"); + dialogPane.setGraphic(new ImageView(image)); + + serverTable = new TableView<>(); + serverTable.setPlaceholder(new Label("No servers added yet")); + + TableColumn urlColumn = new TableColumn<>("URL"); + urlColumn.setMinWidth(300); + urlColumn.setCellValueFactory((TableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getUrl()); + }); + + TableColumn aliasColumn = new TableColumn<>("Alias"); + aliasColumn.setCellValueFactory((TableColumn.CellDataFeatures param) -> { + return param.getValue().labelProperty(); + }); + aliasColumn.setCellFactory(value -> new AliasCell()); + aliasColumn.setGraphic(getTagGlyph()); + + serverTable.getColumns().add(urlColumn); + serverTable.getColumns().add(aliasColumn); + serverTable.setEditable(true); + serverTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + List servers = serverType == ServerType.BITCOIN_CORE ? Config.get().getRecentCoreServers() : Config.get().getRecentElectrumServers(); + List serverEntries = servers.stream().map(server -> new ServerEntry(serverType, server)).collect(Collectors.toList()); + serverTable.setItems(FXCollections.observableArrayList(serverEntries)); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().add(serverTable); + dialogPane.setContent(stackPane); + + ButtonType selectButtonType = new ButtonType("Select", ButtonBar.ButtonData.APPLY); + ButtonType deleteButtonType = new ButtonType("Delete", ButtonBar.ButtonData.LEFT); + dialogPane.getButtonTypes().addAll(deleteButtonType, ButtonType.CLOSE, selectButtonType); + + Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); + Button deleteButton = (Button)dialogPane.lookupButton(deleteButtonType); + selectButton.setDefaultButton(true); + selectButton.setDisable(true); + deleteButton.setDisable(true); + serverTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + selectButton.setDisable(newValue == null); + deleteButton.setDisable(newValue == null); + }); + + setResultConverter(buttonType -> buttonType == selectButtonType ? serverTable.getSelectionModel().getSelectedItem().getServer() : null); + + dialogPane.setPrefWidth(680); + dialogPane.setPrefHeight(500); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + AppServices.moveToActiveWindowScreen(this); + } + + private static Glyph getTagGlyph() { + Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TAG); + glyph.setFontSize(12); + return glyph; + } + + private class ServerAliasDialogPane extends DialogPane { + @Override + protected Node createButton(ButtonType buttonType) { + Node button; + if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { + Button deleteButton = new Button(buttonType.getText()); + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(deleteButton, buttonData); + deleteButton.setOnAction(event -> { + ServerEntry serverEntry = serverTable.getSelectionModel().getSelectedItem(); + if(serverEntry != null) { + serverTable.getItems().remove(serverEntry); + if(serverType == ServerType.BITCOIN_CORE) { + Config.get().removeRecentCoreServer(serverEntry.getServer()); + } else { + Config.get().removeRecentElectrumServer(serverEntry.getServer()); + } + } + }); + + button = deleteButton; + } else { + button = super.createButton(buttonType); + } + + return button; + } + } + + private static class ServerEntry { + private final Server server; + private final StringProperty labelProperty = new SimpleStringProperty(); + + public ServerEntry(ServerType serverType, Server server) { + this.server = server; + labelProperty.set(server.getAlias()); + labelProperty.addListener((observable, oldValue, newValue) -> { + String alias = newValue; + if(alias != null && alias.isEmpty()) { + alias = null; + } + + Server newServer = new Server(server.getUrl(), alias); + if(serverType == ServerType.BITCOIN_CORE) { + Config.get().setCoreServerAlias(newServer); + } else { + Config.get().setElectrumServerAlias(newServer); + } + }); + } + + public String getUrl() { + return server.getHostAndPort().toString(); + } + + public StringProperty labelProperty() { + return labelProperty; + } + + public Server getServer() { + return new Server(server.getUrl(), labelProperty.get()); + } + } + + private static class AliasCell extends TextFieldTableCell { + public AliasCell() { + super(new DefaultStringConverter()); + } + + @Override + public void commitEdit(String label) { + if(label != null) { + label = label.trim(); + } + + // This block is necessary to support commit on losing focus, because + // the baked-in mechanism sets our editing state to false before we can + // intercept the loss of focus. The default commitEdit(...) method + // simply bails if we are not editing... + if (!isEditing() && !label.equals(getItem())) { + TableView table = getTableView(); + if(table != null) { + TableColumn column = getTableColumn(); + TableColumn.CellEditEvent event = new TableColumn.CellEditEvent<>( + table, new TablePosition<>(table, getIndex(), column), + TableColumn.editCommitEvent(), label + ); + Event.fireEvent(column, event); + } + } + + super.commitEdit(label); + } + + @Override + public void startEdit() { + super.startEdit(); + + try { + Field f = getClass().getSuperclass().getDeclaredField("textField"); + f.setAccessible(true); + TextField textField = (TextField)f.get(this); + textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { + if (!isNowFocused) { + commitEdit(getConverter().fromString(textField.getText())); + setText(getConverter().fromString(textField.getText())); + } + }); + } catch (Exception e) { + //ignore + } + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index f37d10ef..407852da 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -12,11 +12,13 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Server; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.*; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.text.Font; @@ -51,6 +53,8 @@ import java.util.Random; public class ServerPreferencesController extends PreferencesDetailController { private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class); + private static final Server MANAGE_ALIASES_SERVER = new Server("tcp://localhost", "Manage Aliases..."); + @FXML private ToggleGroup serverTypeToggleGroup; @@ -79,7 +83,7 @@ public class ServerPreferencesController extends PreferencesDetailController { private Form coreForm; @FXML - private ComboBox recentCoreServers; + private ComboBox recentCoreServers; @FXML private ComboBoxTextField coreHost; @@ -121,7 +125,7 @@ public class ServerPreferencesController extends PreferencesDetailController { private Form electrumForm; @FXML - private ComboBox recentElectrumServers; + private ComboBox recentElectrumServers; @FXML private ComboBoxTextField electrumHost; @@ -267,26 +271,52 @@ public class ServerPreferencesController extends PreferencesDetailController { } }); - recentCoreServers.setConverter(new UrlHostConverter()); - recentCoreServers.setItems(FXCollections.observableList(Config.get().getRecentCoreServers() == null ? new ArrayList<>() : Config.get().getRecentCoreServers())); + recentCoreServers.setCellFactory(value -> new ServerCell()); + recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers())); recentCoreServers.prefWidthProperty().bind(coreHost.widthProperty()); recentCoreServers.valueProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null && Protocol.getProtocol(newValue) != null) { - HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue); - coreHost.setText(hostAndPort.getHost()); - corePort.setText(Integer.toString(hostAndPort.getPort())); + if(newValue != null) { + if(newValue == MANAGE_ALIASES_SERVER) { + ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.BITCOIN_CORE); + Optional optServer = serverAliasDialog.showAndWait(); + recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers())); + Server selectedServer = optServer.orElseGet(() -> Config.get().getCoreServer()); + Platform.runLater(() -> recentCoreServers.setValue(selectedServer)); + } else if(newValue.getHostAndPort() != null) { + HostAndPort hostAndPort = newValue.getHostAndPort(); + corePort.setText(hostAndPort.hasPort() ? Integer.toString(hostAndPort.getPort()) : ""); + if(newValue.getAlias() != null) { + coreHost.setText(newValue.getAlias()); + } else { + coreHost.setText(hostAndPort.getHost()); + } + coreHost.positionCaret(coreHost.getText().length()); + } } }); - recentElectrumServers.setConverter(new UrlHostConverter()); - recentElectrumServers.setItems(FXCollections.observableList(Config.get().getRecentElectrumServers() == null ? new ArrayList<>() : Config.get().getRecentElectrumServers())); + recentElectrumServers.setCellFactory(value -> new ServerCell()); + recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers())); recentElectrumServers.prefWidthProperty().bind(electrumHost.widthProperty()); recentElectrumServers.valueProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null && Protocol.getProtocol(newValue) != null) { - HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue); - electrumHost.setText(hostAndPort.getHost()); - electrumPort.setText(Integer.toString(hostAndPort.getPort())); - electrumUseSsl.setSelected(Protocol.getProtocol(newValue) == Protocol.SSL); + if(newValue != null) { + if(newValue == MANAGE_ALIASES_SERVER) { + ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.ELECTRUM_SERVER); + Optional optServer = serverAliasDialog.showAndWait(); + recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers())); + Server selectedServer = optServer.orElseGet(() -> Config.get().getElectrumServer()); + Platform.runLater(() -> recentElectrumServers.setValue(selectedServer)); + } else if(newValue.getHostAndPort() != null) { + HostAndPort hostAndPort = newValue.getHostAndPort(); + electrumPort.setText(hostAndPort.hasPort() ? Integer.toString(hostAndPort.getPort()) : ""); + electrumUseSsl.setSelected(newValue.getProtocol() == Protocol.SSL); + if(newValue.getAlias() != null) { + electrumHost.setText(newValue.getAlias()); + } else { + electrumHost.setText(hostAndPort.getHost()); + } + electrumHost.positionCaret(electrumHost.getText().length()); + } } }); @@ -339,7 +369,7 @@ public class ServerPreferencesController extends PreferencesDetailController { setTestResultsFont(); testConnection.setOnAction(event -> { testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); - testResults.setText("Connecting to " + config.getServerAddress() + "..."); + testResults.setText("Connecting " + (config.hasServer() ? "to " + config.getServer().getUrl() : "") + "..."); if(Config.get().requiresInternalTor() && Tor.getDefault() == null) { startTor(); @@ -358,7 +388,7 @@ public class ServerPreferencesController extends PreferencesDetailController { testConnection.setVisible(true); }); - PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromUrl(config.getPublicElectrumServer()); + PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromServer(config.getPublicElectrumServer()); if(configPublicElectrumServer == null && PublicElectrumServer.supportedNetwork()) { List servers = PublicElectrumServer.getServers(); if(!servers.isEmpty()) { @@ -368,16 +398,16 @@ public class ServerPreferencesController extends PreferencesDetailController { publicElectrumServer.setValue(configPublicElectrumServer); } - String coreServer = config.getCoreServer(); + Server coreServer = config.getCoreServer(); if(coreServer != null) { - Protocol protocol = Protocol.getProtocol(coreServer); - - if(protocol != null) { - HostAndPort server = protocol.getServerHostAndPort(coreServer); - coreHost.setText(server.getHost()); - if(server.hasPort()) { - corePort.setText(Integer.toString(server.getPort())); - } + HostAndPort hostAndPort = coreServer.getHostAndPort(); + Server server = config.getRecentCoreServers().stream().filter(coreServer::equals).findFirst().orElse(null); + if(server != null) { + coreHost.setLeft(getGlyph(FontAwesome5.Glyph.TAG, null)); + } + coreHost.setText(server == null || server.getAlias() == null ? hostAndPort.getHost() : server.getAlias()); + if(hostAndPort.hasPort()) { + corePort.setText(Integer.toString(hostAndPort.getPort())); } } else { coreHost.setText("127.0.0.1"); @@ -396,21 +426,22 @@ public class ServerPreferencesController extends PreferencesDetailController { } } - String electrumServer = config.getElectrumServer(); + Server electrumServer = config.getElectrumServer(); if(electrumServer != null) { - Protocol protocol = Protocol.getProtocol(electrumServer); + Protocol protocol = electrumServer.getProtocol(); + boolean ssl = protocol.equals(Protocol.SSL); + electrumUseSsl.setSelected(ssl); + electrumCertificate.setDisable(!ssl); + electrumCertificateSelect.setDisable(!ssl); - if(protocol != null) { - boolean ssl = protocol.equals(Protocol.SSL); - electrumUseSsl.setSelected(ssl); - electrumCertificate.setDisable(!ssl); - electrumCertificateSelect.setDisable(!ssl); - - HostAndPort server = protocol.getServerHostAndPort(electrumServer); - electrumHost.setText(server.getHost()); - if(server.hasPort()) { - electrumPort.setText(Integer.toString(server.getPort())); - } + HostAndPort hostAndPort = electrumServer.getHostAndPort(); + Server server = config.getRecentElectrumServers().stream().filter(electrumServer::equals).findFirst().orElse(null); + if(server != null) { + electrumHost.setLeft(getGlyph(FontAwesome5.Glyph.TAG, null)); + } + electrumHost.setText(server == null || server.getAlias() == null ? hostAndPort.getHost() : server.getAlias()); + if(hostAndPort.hasPort()) { + electrumPort.setText(Integer.toString(hostAndPort.getPort())); } } @@ -449,7 +480,7 @@ public class ServerPreferencesController extends PreferencesDetailController { torService.setOnSucceeded(workerStateEvent -> { Tor.setDefault(torService.getValue()); torService.cancel(); - testResults.appendText("\nTor running, connecting to " + Config.get().getServerAddress() + "..."); + testResults.appendText("\nTor running, connecting to " + Config.get().getServer().getUrl() + "..."); startElectrumConnection(); }); torService.setOnFailed(workerStateEvent -> { @@ -495,6 +526,13 @@ public class ServerPreferencesController extends PreferencesDetailController { Config.get().setMode(Mode.ONLINE); connectionService.cancel(); useProxyOriginal = null; + if(Config.get().addRecentServer()) { + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers())); + } else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) { + recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers())); + } + } }); connectionService.setOnFailed(workerStateEvent -> { EventManager.get().unregister(connectionService); @@ -668,24 +706,32 @@ public class ServerPreferencesController extends PreferencesDetailController { @NotNull private ChangeListener getPublicElectrumServerListener(Config config) { return (observable, oldValue, newValue) -> { - config.setPublicElectrumServer(newValue.getUrl()); + config.setPublicElectrumServer(newValue.getServer()); }; } @NotNull private ChangeListener getBitcoinCoreListener(Config config) { return (observable, oldValue, newValue) -> { + Server existingServer = config.getRecentCoreServers().stream().filter(server -> coreHost.getText().equals(server.getAlias())).findFirst().orElse(null); + coreHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null)); setCoreServerInConfig(config); }; } private void setCoreServerInConfig(Config config) { + Server existingServer = config.getRecentCoreServers().stream().filter(server -> coreHost.getText().equals(server.getAlias())).findFirst().orElse(null); + if(existingServer != null) { + config.setCoreServer(existingServer); + return; + } + String hostAsString = getHost(coreHost.getText()); Integer portAsInteger = getPort(corePort.getText()); if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { - config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString, portAsInteger)); + config.setCoreServer(new Server(Protocol.HTTP.toUrlString(hostAsString, portAsInteger))); } else if(hostAsString != null) { - config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString)); + config.setCoreServer(new Server(Protocol.HTTP.toUrlString(hostAsString))); } } @@ -699,17 +745,25 @@ public class ServerPreferencesController extends PreferencesDetailController { @NotNull private ChangeListener getElectrumServerListener(Config config) { return (observable, oldValue, newValue) -> { + Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null); + electrumHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null)); setElectrumServerInConfig(config); }; } private void setElectrumServerInConfig(Config config) { + Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null); + if(existingServer != null) { + config.setElectrumServer(existingServer); + return; + } + String hostAsString = getHost(electrumHost.getText()); Integer portAsInteger = getPort(electrumPort.getText()); if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { - config.setElectrumServer(getProtocol().toUrlString(hostAsString, portAsInteger)); + config.setElectrumServer(new Server(getProtocol().toUrlString(hostAsString, portAsInteger))); } else if(hostAsString != null) { - config.setElectrumServer(getProtocol().toUrlString(hostAsString)); + config.setElectrumServer(new Server(getProtocol().toUrlString(hostAsString))); } } @@ -777,7 +831,7 @@ public class ServerPreferencesController extends PreferencesDetailController { } } - private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) { + private static Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) { Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName); glyph.setFontSize(12); if(styleClass != null) { @@ -813,6 +867,12 @@ public class ServerPreferencesController extends PreferencesDetailController { } } + private ObservableList getObservableServerList(List servers) { + ObservableList serverObservableList = FXCollections.observableList(new ArrayList<>(servers)); + serverObservableList.add(MANAGE_ALIASES_SERVER); + return serverObservableList; + } + @Subscribe public void bwtStatus(BwtStatusEvent event) { if(!(event instanceof BwtSyncStatusEvent)) { @@ -844,15 +904,28 @@ public class ServerPreferencesController extends PreferencesDetailController { }); } - private static class UrlHostConverter extends StringConverter { + private static class ServerCell extends ListCell { @Override - public String toString(String serverUrl) { - return serverUrl == null || Protocol.getProtocol(serverUrl) == null ? "" : Protocol.getProtocol(serverUrl).getServerHostAndPort(serverUrl).getHost(); - } + protected void updateItem(Server server, boolean empty) { + super.updateItem(server, empty); + if(server == null || empty) { + setText(""); + setGraphic(null); + } else { + String serverAlias = server.getAlias(); - @Override - public String fromString(String string) { - return null; + if(server == MANAGE_ALIASES_SERVER) { + setText(serverAlias); + setStyle("-fx-font-style: italic"); + setGraphic(null); + } else if(serverAlias != null) { + setText(serverAlias); + setGraphic(getGlyph(FontAwesome5.Glyph.TAG, null)); + } else { + setText(server.getHost()); + setGraphic(null); + } + } } } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 6491dac8..ad185827 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -117,6 +117,7 @@ + diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css index c42120ce..364be9f1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css @@ -51,4 +51,8 @@ #electrumUseSsl { -fx-padding: 4 0 2 0; +} + +#coreHost .left-pane, #electrumHost .left-pane { + -fx-padding: 0 3 0 6; } \ No newline at end of file