add support for configuring server aliases, and switching servers via the tools menu

This commit is contained in:
Craig Raw 2022-09-12 15:44:47 +02:00
parent bacbdb848b
commit 1f67692727
17 changed files with 649 additions and 140 deletions

View file

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

View file

@ -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,8 +1157,10 @@ public class AppServices {
@Subscribe
public void requestConnect(RequestConnectEvent event) {
if(Config.get().hasServer()) {
onlineProperty.set(true);
}
}
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
@ -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);
}

View file

@ -77,7 +77,7 @@ public class MainApp extends Application {
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
List<PublicElectrumServer> 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());
}
}
}

View file

@ -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'),

View file

@ -53,14 +53,14 @@ public class Config {
private Boolean hdCapture;
private String webcamDevice;
private ServerType serverType;
private String publicElectrumServer;
private String coreServer;
private List<String> recentCoreServers;
private Server publicElectrumServer;
private Server coreServer;
private List<Server> recentCoreServers;
private CoreAuthType coreAuthType;
private File coreDataDir;
private String coreAuth;
private String electrumServer;
private List<String> recentElectrumServers;
private Server electrumServer;
private List<Server> 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 getServer().isOnionAddress();
}
return protocol.isOnionAddress(protocol.getServerHostAndPort(getServerAddress()));
}
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<String> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
List<Server> 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<String> getRecentCoreServers() {
return recentCoreServers;
public List<Server> 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<String> getRecentElectrumServers() {
return recentElectrumServers;
public List<Server> 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);
}
public void addRecentElectrumServer(String electrumServer) {
return false;
}
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<Server> {
@Override
public JsonElement serialize(Server src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}
}
private static class ServerDeserializer implements JsonDeserializer<Server> {
@Override
public Server deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return Server.fromString(json.getAsJsonPrimitive().getAsString());
}
}
}

View file

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

View file

@ -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() + "...")));
}
}

View file

@ -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<String, List<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
private static String previousServerAddress;
private static Server previousServer;
private static Map<String, String> 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())));
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Server> {
private final ServerType serverType;
private final TableView<ServerEntry> 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<ServerEntry, String> urlColumn = new TableColumn<>("URL");
urlColumn.setMinWidth(300);
urlColumn.setCellValueFactory((TableColumn.CellDataFeatures<ServerEntry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getUrl());
});
TableColumn<ServerEntry, String> aliasColumn = new TableColumn<>("Alias");
aliasColumn.setCellValueFactory((TableColumn.CellDataFeatures<ServerEntry, String> 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<Server> servers = serverType == ServerType.BITCOIN_CORE ? Config.get().getRecentCoreServers() : Config.get().getRecentElectrumServers();
List<ServerEntry> 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<ServerEntry, String> {
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<ServerEntry> table = getTableView();
if(table != null) {
TableColumn<ServerEntry, String> column = getTableColumn();
TableColumn.CellEditEvent<ServerEntry, String> 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
}
}
}
}

View file

@ -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<String> recentCoreServers;
private ComboBox<Server> recentCoreServers;
@FXML
private ComboBoxTextField coreHost;
@ -121,7 +125,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
private Form electrumForm;
@FXML
private ComboBox<String> recentElectrumServers;
private ComboBox<Server> 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);
if(newValue != null) {
if(newValue == MANAGE_ALIASES_SERVER) {
ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.BITCOIN_CORE);
Optional<Server> 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());
corePort.setText(Integer.toString(hostAndPort.getPort()));
}
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);
if(newValue != null) {
if(newValue == MANAGE_ALIASES_SERVER) {
ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.ELECTRUM_SERVER);
Optional<Server> 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());
electrumPort.setText(Integer.toString(hostAndPort.getPort()));
electrumUseSsl.setSelected(Protocol.getProtocol(newValue) == Protocol.SSL);
}
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<PublicElectrumServer> 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);
if(protocol != null) {
Protocol protocol = electrumServer.getProtocol();
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<PublicElectrumServer> getPublicElectrumServerListener(Config config) {
return (observable, oldValue, newValue) -> {
config.setPublicElectrumServer(newValue.getUrl());
config.setPublicElectrumServer(newValue.getServer());
};
}
@NotNull
private ChangeListener<String> 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<String> 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<Server> getObservableServerList(List<Server> servers) {
ObservableList<Server> 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<String> {
private static class ServerCell extends ListCell<Server> {
@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);
}
}
}
}
}

View file

@ -117,6 +117,7 @@
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
<SeparatorMenuItem />
<Menu fx:id="switchServer" text="Switch Server"/>
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
<MenuItem fx:id="restart" mnemonicParsing="false" text="Restart" onAction="#restart" />

View file

@ -52,3 +52,7 @@
#electrumUseSsl {
-fx-padding: 4 0 2 0;
}
#coreHost .left-pane, #electrumHost .left-pane {
-fx-padding: 0 3 0 6;
}