From 1e250193fd4be40f24e9b3fd94bc48d4b5a36cb9 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 5 Jun 2020 13:29:43 +0200 Subject: [PATCH] add welcome and preferences dialogs --- .DS_Store | Bin 6148 -> 6148 bytes build.gradle | 11 +- .../sparrowwallet/sparrow/AppController.java | 3 + .../com/sparrowwallet/sparrow/MainApp.java | 33 +- .../java/com/sparrowwallet/sparrow/Mode.java | 5 + .../sparrow/control/TextFieldValidator.java | 102 ++++++ .../control/UnlabeledToggleSwitch.java | 16 + .../sparrow/control/WalletNameDialog.java | 2 +- .../sparrow/control/WelcomeDialog.java | 105 ++++++ .../sparrow/glyphfont/FontAwesome5.java | 3 + .../com/sparrowwallet/sparrow/io/Config.java | 31 ++ .../sparrow/io/ElectrumServer.java | 189 ++++++++++- .../sparrow/preferences/PreferenceGroup.java | 5 + .../preferences/PreferencesController.java | 78 +++++ .../PreferencesDetailController.java | 20 ++ .../preferences/PreferencesDialog.java | 33 ++ .../ServerPreferencesController.java | 306 ++++++++++++++++++ src/main/resources/.DS_Store | Bin 0 -> 6148 bytes .../com/sparrowwallet/sparrow/general.css | 10 +- .../sparrow/preferences/preferences.css | 34 ++ .../sparrow/preferences/preferences.fxml | 36 +++ .../sparrow/preferences/server.fxml | 71 ++++ .../sparrow/transaction/input.fxml | 4 +- src/main/resources/image/.DS_Store | Bin 0 -> 6148 bytes src/main/resources/image/sparrow-small.png | Bin 0 -> 8353 bytes src/main/resources/{ => image}/sparrow.png | Bin src/main/resources/image/sparrow.svg | 127 ++++++++ 27 files changed, 1184 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/Mode.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/UnlabeledToggleSwitch.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/PreferenceGroup.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDetailController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java create mode 100644 src/main/resources/.DS_Store create mode 100644 src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css create mode 100644 src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.fxml create mode 100644 src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml create mode 100644 src/main/resources/image/.DS_Store create mode 100644 src/main/resources/image/sparrow-small.png rename src/main/resources/{ => image}/sparrow.png (100%) create mode 100644 src/main/resources/image/sparrow.svg diff --git a/.DS_Store b/.DS_Store index 650b9d96ff8e5f16bcef47226861f415aa0e9d89..72c56e329aa82eb1550d55df0eb102f001c0053b 100644 GIT binary patch delta 47 zcmZoMXfc@J&nU7nU^g?P$YdUt_dLAV%p5k@s}R} DS|$%* delta 35 rcmZoMXfc@J&nUbxU^g?P@MIpA_mgc{IX6qQzG0f!AikNM<1aq|$`lJ# diff --git a/build.gradle b/build.gradle index a5cdf285..1578f9dd 100644 --- a/build.gradle +++ b/build.gradle @@ -48,8 +48,15 @@ dependencies { mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp' +compileJava { + options.with { + fork = true + compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"]) + } +} + run { - applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow"] + applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"] } jlink { @@ -64,7 +71,7 @@ jlink { options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png'] launcher { name = 'sparrow' - jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow"] + jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"] } addExtraDependencies("javafx") jpackage { diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 47d29e8e..6fd672e0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -70,6 +70,9 @@ public class AppController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); + + ElectrumServer.PingService pingService = new ElectrumServer.PingService(); + //pingService. } void initializeView() { diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index ff3e7f50..fce946c1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -1,10 +1,11 @@ package com.sparrowwallet.sparrow; -import com.sparrowwallet.drongo.policy.PolicyType; -import com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.control.WelcomeDialog; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.preferences.PreferenceGroup; +import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -13,6 +14,8 @@ import javafx.scene.image.Image; import javafx.stage.Stage; import org.controlsfx.glyphfont.GlyphFontRegistry; +import java.util.Optional; + public class MainApp extends Application { @Override @@ -20,6 +23,21 @@ public class MainApp extends Application { GlyphFontRegistry.register(new FontAwesome5()); GlyphFontRegistry.register(new FontAwesome5Brands()); + Mode mode = Config.get().getMode(); + if(true || mode == null) { + WelcomeDialog welcomeDialog = new WelcomeDialog(getHostServices()); + Optional optionalMode = welcomeDialog.showAndWait(); + if(optionalMode.isPresent()) { + mode = optionalMode.get(); + Config.get().setMode(mode); + + if(mode.equals(Mode.ONLINE)) { + PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER); + preferencesDialog.showAndWait(); + } + } + } + FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml")); Parent root = transactionLoader.load(); AppController appController = transactionLoader.getController(); @@ -31,17 +49,10 @@ public class MainApp extends Application { stage.setMinWidth(650); stage.setMinHeight(700); stage.setScene(scene); - stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/sparrow.png"))); + stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png"))); appController.initializeView(); - Wallet wallet = new Wallet(); - wallet.setPolicyType(PolicyType.SINGLE); - wallet.setScriptType(ScriptType.P2WPKH); - -// KeystoreImportDialog dlg = new KeystoreImportDialog(wallet); -// dlg.showAndWait(); - stage.show(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/Mode.java b/src/main/java/com/sparrowwallet/sparrow/Mode.java new file mode 100644 index 00000000..5c60e48d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/Mode.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow; + +public enum Mode { + OFFLINE, ONLINE +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java b/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java new file mode 100644 index 00000000..3abde444 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/TextFieldValidator.java @@ -0,0 +1,102 @@ +package com.sparrowwallet.sparrow.control; + +import java.text.DecimalFormatSymbols; +import java.util.regex.Pattern; + +import javafx.beans.NamedArg; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.TextFormatter.Change; + +public class TextFieldValidator { + + private static final String CURRENCY_SYMBOL = DecimalFormatSymbols.getInstance().getCurrencySymbol(); + private static final char DECIMAL_SEPARATOR = DecimalFormatSymbols.getInstance().getDecimalSeparator(); + + private final Pattern INPUT_PATTERN; + + public TextFieldValidator(@NamedArg("modus") ValidationModus modus, @NamedArg("countOf") int countOf) { + this(modus.createPattern(countOf)); + } + + public TextFieldValidator(@NamedArg("regex") String regex) { + this(Pattern.compile(regex)); + } + + public TextFieldValidator(Pattern inputPattern) { + INPUT_PATTERN = inputPattern; + } + + public static TextFieldValidator maxFractionDigits(int countOf) { + return new TextFieldValidator(maxFractionPattern(countOf)); + } + + public static TextFieldValidator maxIntegers(int countOf) { + return new TextFieldValidator(maxIntegerPattern(countOf)); + } + + public static TextFieldValidator integersOnly() { + return new TextFieldValidator(integersOnlyPattern()); + } + + public TextFormatter getFormatter() { + return new TextFormatter<>(this::validateChange); + } + + private Change validateChange(Change c) { + if (validate(c.getControlNewText())) { + return c; + } + return null; + } + + public boolean validate(String input) { + return INPUT_PATTERN.matcher(input).matches(); + } + + private static Pattern maxFractionPattern(int countOf) { + return Pattern.compile("\\d*(\\\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?"); + } + + private static Pattern maxCurrencyFractionPattern(int countOf) { + return Pattern.compile("^\\\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?\\s?\\\\" + CURRENCY_SYMBOL + "?"); + } + + private static Pattern maxIntegerPattern(int countOf) { + return Pattern.compile("\\d{0," + countOf + "}"); + } + + private static Pattern integersOnlyPattern() { + return Pattern.compile("\\d*"); + } + + public enum ValidationModus { + MAX_CURRENCY_FRACTION_DIGITS { + @Override + public Pattern createPattern(int countOf) { + return maxCurrencyFractionPattern(countOf); + } + }, + + MAX_FRACTION_DIGITS { + @Override + public Pattern createPattern(int countOf) { + return maxFractionPattern(countOf); + } + }, + MAX_INTEGERS { + @Override + public Pattern createPattern(int countOf) { + return maxIntegerPattern(countOf); + } + }, + + INTEGERS_ONLY { + @Override + public Pattern createPattern(int countOf) { + return integersOnlyPattern(); + } + }; + + public abstract Pattern createPattern(int countOf); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/UnlabeledToggleSwitch.java b/src/main/java/com/sparrowwallet/sparrow/control/UnlabeledToggleSwitch.java new file mode 100644 index 00000000..b255121f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/UnlabeledToggleSwitch.java @@ -0,0 +1,16 @@ +package com.sparrowwallet.sparrow.control; + +import impl.org.controlsfx.skin.ToggleSwitchSkin; +import javafx.scene.control.Skin; +import org.controlsfx.control.ToggleSwitch; + +public class UnlabeledToggleSwitch extends ToggleSwitch { + @Override protected Skin createDefaultSkin() { + return new ToggleSwitchSkin(this) { + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) - 20; + } + }; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java index 69ad3d9f..c959fdce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java @@ -23,7 +23,7 @@ public class WalletNameDialog extends Dialog { this.name = (CustomTextField)TextFields.createClearableTextField(); final DialogPane dialogPane = getDialogPane(); - setTitle("Wallet Password"); + setTitle("Wallet Name"); dialogPane.setHeaderText("Enter a name for this wallet:"); dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java new file mode 100644 index 00000000..9e27189d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java @@ -0,0 +1,105 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppController; +import com.sparrowwallet.sparrow.Mode; +import javafx.application.HostServices; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import org.controlsfx.control.HyperlinkLabel; +import org.controlsfx.control.StatusBar; +import org.controlsfx.control.ToggleSwitch; + +public class WelcomeDialog extends Dialog { + private static final String[] ELECTRUM_SERVERS = new String[]{ + "ElectrumX", "https://github.com/spesmilo/electrumx", + "electrs", "https://github.com/romanz/electrs", + "esplora-electrs", "https://github.com/Blockstream/electrs", + "Electrum Personal Server", "https://github.com/chris-belcher/electrum-personal-server", + "Bitcoin Wallet Tracker", "https://github.com/shesek/bwt"}; + + private final HostServices hostServices; + + public WelcomeDialog(HostServices services) { + this.hostServices = services; + + final DialogPane dialogPane = getDialogPane(); + + setTitle("Welcome to Sparrow"); + dialogPane.setHeaderText("Welcome to Sparrow!"); + dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); + dialogPane.setPrefWidth(600); + dialogPane.setPrefHeight(500); + + Image image = new Image("image/sparrow-small.png", 50, 50, false, false); + if (!image.isError()) { + ImageView imageView = new ImageView(); + imageView.setSmooth(false); + imageView.setImage(image); + dialogPane.setGraphic(imageView); + } + + final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Now", ButtonBar.ButtonData.OK_DONE); + final ButtonType offlineButtonType = new javafx.scene.control.ButtonType("Configure Later or Use Offline", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().addAll(onlineButtonType, offlineButtonType); + + final VBox content = new VBox(20); + content.setPadding(new Insets(20, 20, 20, 20)); + content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator.")); + content.getChildren().add(createParagraph("For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy. Examples include:")); + + VBox linkBox = new VBox(); + for(int i = 0; i < ELECTRUM_SERVERS.length; i+=2) { + linkBox.getChildren().add(createBulletedLink(ELECTRUM_SERVERS[i], ELECTRUM_SERVERS[i+1])); + } + content.getChildren().add(linkBox); + + content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar:")); + content.getChildren().add(createStatusBar(onlineButtonType, offlineButtonType)); + + dialogPane.setContent(content); + + setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE); + } + + private Label createParagraph(String text) { + Label label = new Label(text); + label.setWrapText(true); + + return label; + } + + private HyperlinkLabel createBulletedLink(String name, String url) { + HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + name + "]"); + label.setOnAction(event -> { + hostServices.showDocument(url); + }); + + return label; + } + + private StatusBar createStatusBar(ButtonType onlineButtonType, ButtonType offlineButtonType) { + StatusBar statusBar = new StatusBar(); + statusBar.setText("Online Mode"); + statusBar.getRightItems().add(createToggle(statusBar, onlineButtonType, offlineButtonType)); + + return statusBar; + } + + private ToggleSwitch createToggle(StatusBar statusBar, ButtonType onlineButtonType, ButtonType offlineButtonType) { + ToggleSwitch toggleSwitch = new UnlabeledToggleSwitch(); + toggleSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + Button onlineButton = (Button) getDialogPane().lookupButton(onlineButtonType); + onlineButton.setDefaultButton(newValue); + Button offlineButton = (Button) getDialogPane().lookupButton(offlineButtonType); + offlineButton.setDefaultButton(!newValue); + statusBar.setText(newValue ? "Online Mode" : "Offline Mode"); + }); + + toggleSwitch.setSelected(true); + return toggleSwitch; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 684e728f..a132eb4e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -15,13 +15,16 @@ public class FontAwesome5 extends GlyphFont { * The individual glyphs offered by the FontAwesome5 font. */ public static enum Glyph implements INamedCharacter { + CHECK_CIRCLE('\uf058'), CIRCLE('\uf111'), EXCLAMATION_CIRCLE('\uf06a'), + ELLIPSIS_H('\uf141'), EYE('\uf06e'), KEY('\uf084'), LAPTOP('\uf109'), LOCK('\uf023'), LOCK_OPEN('\uf3c1'), + QUESTION_CIRCLE('\uf059'), SD_CARD('\uf7c2'), WALLET('\uf555'); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 6c7bb2d4..6848295f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.io; import com.google.gson.*; +import com.sparrowwallet.sparrow.Mode; import java.io.*; import java.lang.reflect.Type; @@ -8,10 +9,13 @@ import java.lang.reflect.Type; public class Config { public static final String CONFIG_FILENAME = ".config"; + private Mode mode; private Integer keyDerivationPeriod; private File hwi; private String electrumServer; private File electrumServerCert; + private boolean useProxy; + private String proxyServer; private static Config INSTANCE; @@ -54,6 +58,15 @@ public class Config { return INSTANCE; } + public Mode getMode() { + return mode; + } + + public void setMode(Mode mode) { + this.mode = mode; + flush(); + } + public Integer getKeyDerivationPeriod() { return keyDerivationPeriod; } @@ -90,6 +103,24 @@ public class Config { flush(); } + public boolean isUseProxy() { + return useProxy; + } + + public void setUseProxy(boolean useProxy) { + this.useProxy = useProxy; + flush(); + } + + public String getProxyServer() { + return proxyServer; + } + + public void setProxyServer(String proxyServer) { + this.proxyServer = proxyServer; + flush(); + } + private void flush() { Gson gson = getGson(); try { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java index cb8a661b..e8dec78a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java @@ -14,6 +14,8 @@ import org.jetbrains.annotations.NotNull; import javax.net.SocketFactory; import javax.net.ssl.*; import java.io.*; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.Socket; import java.security.*; import java.security.cert.Certificate; @@ -24,13 +26,16 @@ import java.util.*; import java.util.stream.Collectors; public class ElectrumServer { + private static final String[] SUPPORTED_VERSIONS = new String[]{"1.3", "1.4.2"}; + private static Transport transport; - private synchronized Transport getTransport() throws ServerException { + private static synchronized Transport getTransport() throws ServerException { if(transport == null) { try { String electrumServer = Config.get().getElectrumServer(); File electrumServerCert = Config.get().getElectrumServerCert(); + String proxyServer = Config.get().getProxyServer(); if(electrumServer == null) { throw new ServerException("Electrum server URL not specified"); @@ -46,7 +51,21 @@ public class ElectrumServer { } HostAndPort server = protocol.getServerHostAndPort(electrumServer); - transport = protocol.getTransport(server, electrumServerCert); + + if(Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) { + HostAndPort proxy = HostAndPort.fromString(proxyServer); + if(electrumServerCert != null) { + transport = protocol.getTransport(server, electrumServerCert, proxy); + } else { + transport = protocol.getTransport(server, proxy); + } + } else { + if(electrumServerCert != null) { + transport = protocol.getTransport(server, electrumServerCert); + } else { + transport = protocol.getTransport(server); + } + } } catch (Exception e) { throw new ServerException(e); } @@ -55,10 +74,31 @@ public class ElectrumServer { return transport; } - public String getServerVersion() throws ServerException { + public void ping() throws ServerException { + JsonRpcClient client = new JsonRpcClient(getTransport()); + client.createRequest().returnAs(Void.class).method("server.ping").id(1).execute(); + } + + public List getServerVersion() throws ServerException { + JsonRpcClient client = new JsonRpcClient(getTransport()); + return client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", "Sparrow").param("protocol_version", SUPPORTED_VERSIONS).execute(); + } + + public String getServerBanner() throws ServerException { JsonRpcClient client = new JsonRpcClient(getTransport()); - List serverVersion = client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", "Sparrow").param("protocol_version", "1.4").execute(); - return serverVersion.get(1); + return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute(); + } + + public static synchronized void closeActiveConnection() throws ServerException { + try { + if(transport != null) { + Closeable closeableTransport = (Closeable)transport; + closeableTransport.close(); + transport = null; + } + } catch (IOException e) { + throw new ServerException(e); + } } public Map> getHistory(Wallet wallet) throws ServerException { @@ -327,11 +367,11 @@ public class ElectrumServer { } } - private static class TcpTransport implements Transport { - private static final int DEFAULT_PORT = 50001; + public static class TcpTransport implements Transport, Closeable { + public static final int DEFAULT_PORT = 50001; protected final HostAndPort server; - private final SocketFactory socketFactory; + protected final SocketFactory socketFactory; private Socket socket; @@ -376,12 +416,19 @@ public class ElectrumServer { protected Socket createSocket() throws IOException { return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); } + + @Override + public void close() throws IOException { + if(socket != null) { + socket.close(); + } + } } - private static class TcpOverTlsTransport extends TcpTransport { - private static final int DEFAULT_PORT = 50002; + public static class TcpOverTlsTransport extends TcpTransport { + public static final int DEFAULT_PORT = 50002; - private final SSLSocketFactory sslSocketFactory; + protected final SSLSocketFactory sslSocketFactory; public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException { super(server); @@ -428,6 +475,70 @@ public class ElectrumServer { } } + public static class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { + public static final int DEFAULT_PROXY_PORT = 1080; + + private HostAndPort proxy; + + public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException { + super(server); + this.proxy = proxy; + } + + public ProxyTcpOverTlsTransport(HostAndPort server, File crtFile, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + super(server, crtFile); + this.proxy = proxy; + } + + @Override + protected Socket createSocket() throws IOException { + InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT)); + Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr)); + underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT))); + SSLSocket sslSocket = (SSLSocket)sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true); + sslSocket.startHandshake(); + + return sslSocket; + } + } + + public static class ServerVersionService extends Service> { + @Override + protected Task> createTask() { + return new Task>() { + protected List call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + return electrumServer.getServerVersion(); + } + }; + } + } + + public static class ServerBannerService extends Service { + @Override + protected Task createTask() { + return new Task<>() { + protected String call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + return electrumServer.getServerBanner(); + } + }; + } + } + + public static class PingService extends Service { + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + electrumServer.ping(); + return true; + } + }; + } + } + public static class TransactionHistoryService extends Service { private final Wallet wallet; @@ -452,23 +563,55 @@ public class ElectrumServer { public enum Protocol { TCP { @Override - public Transport getTransport(HostAndPort server, File serverCert) throws IOException { + public Transport getTransport(HostAndPort server) { + return new TcpTransport(server); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert) { return new TcpTransport(server); } + + @Override + public Transport getTransport(HostAndPort server, HostAndPort proxy) { + throw new UnsupportedOperationException("TCP protocol does not support proxying"); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { + throw new UnsupportedOperationException("TCP protocol does not support proxying"); + } }, SSL{ + @Override + public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + return new TcpOverTlsTransport(server); + } + @Override public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - if(serverCert != null && serverCert.exists()) { - return new TcpOverTlsTransport(server, serverCert); - } else { - return new TcpOverTlsTransport(server); - } + return new TcpOverTlsTransport(server, serverCert); + } + + @Override + public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException { + return new ProxyTcpOverTlsTransport(server, proxy); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + return new ProxyTcpOverTlsTransport(server, serverCert, proxy); } }; + public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException; + public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; + public abstract Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; + + public abstract Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; + public HostAndPort getServerHostAndPort(String url) { return HostAndPort.fromString(url.substring(this.toUrlString().length())); } @@ -477,6 +620,18 @@ public class ElectrumServer { return toString().toLowerCase() + "://"; } + public String toUrlString(String host) { + return toUrlString(HostAndPort.fromHost(host)); + } + + public String toUrlString(String host, int port) { + return toUrlString(HostAndPort.fromParts(host, port)); + } + + public String toUrlString(HostAndPort hostAndPort) { + return toUrlString() + hostAndPort.toString(); + } + public static Protocol getProtocol(String url) { if(url.startsWith("tcp://")) { return TCP; diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferenceGroup.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferenceGroup.java new file mode 100644 index 00000000..7f0c8533 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferenceGroup.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.preferences; + +public enum PreferenceGroup { + GENERAL, SERVER; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java new file mode 100644 index 00000000..5d3ffa1b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java @@ -0,0 +1,78 @@ +package com.sparrowwallet.sparrow.preferences; + +import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppController; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDetailController; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.StackPane; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class PreferencesController implements Initializable { + private Config config; + + @FXML + private ToggleGroup preferencesMenu; + + @FXML + private StackPane preferencesPane; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public Config getConfig() { + return config; + } + + public void initializeView(Config config) { + this.config = config; + preferencesMenu.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> { + if(selectedToggle == null) { + oldValue.setSelected(true); + return; + } + + PreferenceGroup preferenceGroup = (PreferenceGroup) selectedToggle.getUserData(); + String fxmlName = preferenceGroup.toString().toLowerCase(); + setPreferencePane(fxmlName); + }); + } + + public void selectGroup(PreferenceGroup preferenceGroup) { + for(Toggle toggle : preferencesMenu.getToggles()) { + if(toggle.getUserData().equals(preferenceGroup)) { + Platform.runLater(() -> preferencesMenu.selectToggle(toggle)); + return; + } + } + } + + FXMLLoader setPreferencePane(String fxmlName) { + preferencesPane.getChildren().removeAll(preferencesPane.getChildren()); + + try { + FXMLLoader preferencesDetailLoader = new FXMLLoader(AppController.class.getResource("preferences/" + fxmlName + ".fxml")); + Node preferenceGroupNode = preferencesDetailLoader.load(); + PreferencesDetailController controller = preferencesDetailLoader.getController(); + controller.setMasterController(this); + controller.initializeView(config); + preferencesPane.getChildren().add(preferenceGroupNode); + + return preferencesDetailLoader; + } catch (IOException e) { + throw new IllegalStateException("Can't find pane", e); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDetailController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDetailController.java new file mode 100644 index 00000000..d1630baa --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDetailController.java @@ -0,0 +1,20 @@ +package com.sparrowwallet.sparrow.preferences; + +import com.sparrowwallet.sparrow.io.Config; +import javafx.fxml.Initializable; + +public abstract class PreferencesDetailController { + private PreferencesController masterController; + + public PreferencesController getMasterController() { + return masterController; + } + + void setMasterController(PreferencesController masterController) { + this.masterController = masterController; + } + + public void initializeView(Config config) { + + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java new file mode 100644 index 00000000..877183b3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java @@ -0,0 +1,33 @@ +package com.sparrowwallet.sparrow.preferences; + +import com.sparrowwallet.sparrow.AppController; +import com.sparrowwallet.sparrow.io.Config; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import org.controlsfx.tools.Borders; + +import java.io.IOException; + +public class PreferencesDialog extends Dialog { + public PreferencesDialog(PreferenceGroup initialGroup) { + final DialogPane dialogPane = getDialogPane(); + + try { + FXMLLoader preferencesLoader = new FXMLLoader(AppController.class.getResource("preferences/preferences.fxml")); + dialogPane.setContent(Borders.wrap(preferencesLoader.load()).lineBorder().outerPadding(0).innerPadding(0).buildAll()); + PreferencesController preferencesController = preferencesLoader.getController(); + preferencesController.initializeView(Config.get()); + preferencesController.selectGroup(initialGroup); + + final ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().addAll(closeButtonType); + dialogPane.setPrefWidth(650); + dialogPane.setPrefHeight(500); + } catch(IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java new file mode 100644 index 00000000..df2cdf8e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -0,0 +1,306 @@ +package com.sparrowwallet.sparrow.preferences; + +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.control.TextFieldValidator; +import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.ElectrumServer; +import com.sparrowwallet.sparrow.io.ServerException; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Control; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.paint.Color; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import org.controlsfx.glyphfont.Glyph; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; +import org.jetbrains.annotations.NotNull; + +import javax.net.ssl.SSLHandshakeException; +import java.io.File; +import java.io.FileInputStream; +import java.security.cert.CertificateFactory; +import java.util.List; + +public class ServerPreferencesController extends PreferencesDetailController { + @FXML + private TextField host; + + @FXML + private TextField port; + + @FXML + private UnlabeledToggleSwitch useSsl; + + @FXML + private TextField certificate; + + @FXML + private Button certificateSelect; + + @FXML + private UnlabeledToggleSwitch useProxy; + + @FXML + private TextField proxyHost; + + @FXML + private TextField proxyPort; + + @FXML + private Button testConnection; + + @FXML + private TextArea testResults; + + private final ValidationSupport validationSupport = new ValidationSupport(); + + @Override + public void initializeView(Config config) { + Platform.runLater(this::setupValidation); + + port.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); + proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); + + host.textProperty().addListener(getElectrumServerListener(config)); + port.textProperty().addListener(getElectrumServerListener(config)); + + proxyHost.textProperty().addListener(getProxyListener(config)); + proxyPort.textProperty().addListener(getProxyListener(config)); + + useSsl.selectedProperty().addListener((observable, oldValue, newValue) -> { + setElectrumServerInConfig(config); + certificate.setDisable(!newValue); + certificateSelect.setDisable(!newValue); + }); + + certificate.textProperty().addListener((observable, oldValue, newValue) -> { + File crtFile = getCertificate(newValue); + if(crtFile != null) { + config.setElectrumServerCert(crtFile); + } else { + config.setElectrumServerCert(null); + } + }); + + certificateSelect.setOnAction(event -> { + Stage window = new Stage(); + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select Electrum Server certificate"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", "*.*"), + new FileChooser.ExtensionFilter("CRT", "*.crt") + ); + + File file = fileChooser.showOpenDialog(window); + if(file != null) { + certificate.setText(file.getAbsolutePath()); + } + }); + + useProxy.selectedProperty().addListener((observable, oldValue, newValue) -> { + config.setUseProxy(newValue); + proxyHost.setText(proxyHost.getText() + " "); + proxyHost.setText(proxyHost.getText().trim()); + proxyHost.setDisable(!newValue); + proxyPort.setDisable(!newValue); + + if(newValue) { + useSsl.setSelected(true); + useSsl.setDisable(true); + } else { + useSsl.setDisable(false); + } + }); + + testConnection.setOnAction(event -> { + try { + ElectrumServer.closeActiveConnection(); + } catch (ServerException e) { + testResults.setText("Failed to disconnect:\n" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage())); + } + + ElectrumServer.ServerVersionService serverVersionService = new ElectrumServer.ServerVersionService(); + serverVersionService.setOnSucceeded(successEvent -> { + List serverVersion = serverVersionService.getValue(); + testResults.setText("Connected to " + serverVersion.get(0) + " on protocol version " + serverVersion.get(1)); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, Color.rgb(80, 161, 79))); + + ElectrumServer.ServerBannerService serverBannerService = new ElectrumServer.ServerBannerService(); + serverBannerService.setOnSucceeded(bannerSuccessEvent -> { + testResults.setText(testResults.getText() + "\nServer Banner: " + serverBannerService.getValue()); + }); + serverBannerService.setOnFailed(bannerFailEvent -> { + testResults.setText(testResults.getText() + "\nServer Banner: None"); + }); + serverBannerService.start(); + }); + serverVersionService.setOnFailed(failEvent -> { + Throwable e = failEvent.getSource().getException(); + String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) { + reason = "SSL Handshake Error\n" + reason; + } + + testResults.setText("Could not connect:\n\n" + reason); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, Color.rgb(202, 18, 67))); + }); + testResults.setText("Connecting to " + config.getElectrumServer() + "..."); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); + serverVersionService.start(); + }); + + String electrumServer = config.getElectrumServer(); + if(electrumServer != null) { + ElectrumServer.Protocol protocol = ElectrumServer.Protocol.getProtocol(electrumServer); + + if(protocol != null) { + boolean ssl = protocol.equals(ElectrumServer.Protocol.SSL); + useSsl.setSelected(ssl); + certificate.setDisable(!ssl); + certificateSelect.setDisable(!ssl); + + HostAndPort server = protocol.getServerHostAndPort(electrumServer); + host.setText(server.getHost()); + if(server.hasPort()) { + port.setText(Integer.toString(server.getPort())); + } + } + } + + File certificateFile = config.getElectrumServerCert(); + if(certificateFile != null) { + certificate.setText(certificateFile.getAbsolutePath()); + } + + useProxy.setSelected(config.isUseProxy()); + proxyHost.setDisable(!config.isUseProxy()); + proxyPort.setDisable(!config.isUseProxy()); + + if(config.isUseProxy()) { + useSsl.setSelected(true); + useSsl.setDisable(true); + } + + String proxyServer = config.getProxyServer(); + if(proxyServer != null) { + HostAndPort server = HostAndPort.fromString(proxyServer); + proxyHost.setText(server.getHost()); + if(server.hasPort()) { + proxyPort.setText(Integer.toString(server.getPort())); + } + } + } + + private void setupValidation() { + validationSupport.registerValidator(host, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid host name", getHost(newValue) == null) + )); + + validationSupport.registerValidator(port, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) + )); + + validationSupport.registerValidator(proxyHost, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Proxy host required", useProxy.isSelected() && newValue.isEmpty()), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid host name", getHost(newValue) == null) + )); + + validationSupport.registerValidator(proxyPort, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid proxy port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) + )); + + validationSupport.registerValidator(certificate, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid certificate file", newValue != null && !newValue.isEmpty() && getCertificate(newValue) == null) + )); + + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + } + + @NotNull + private ChangeListener getElectrumServerListener(Config config) { + return (observable, oldValue, newValue) -> { + setElectrumServerInConfig(config); + }; + } + + private void setElectrumServerInConfig(Config config) { + String hostAsString = getHost(host.getText()); + Integer portAsInteger = getPort(port.getText()); + if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { + config.setElectrumServer(getProtocol().toUrlString(hostAsString, portAsInteger)); + } else if(hostAsString != null) { + config.setElectrumServer(getProtocol().toUrlString(hostAsString)); + } + } + + @NotNull + private ChangeListener getProxyListener(Config config) { + return (observable, oldValue, newValue) -> { + String hostAsString = getHost(proxyHost.getText()); + Integer portAsInteger = getPort(proxyPort.getText()); + if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { + config.setProxyServer(HostAndPort.fromParts(hostAsString, portAsInteger).toString()); + } else if(hostAsString != null) { + config.setProxyServer(HostAndPort.fromHost(hostAsString).toString()); + } + }; + } + + private ElectrumServer.Protocol getProtocol() { + return (useSsl.isSelected() ? ElectrumServer.Protocol.SSL : ElectrumServer.Protocol.TCP); + } + + private String getHost(String text) { + try { + return HostAndPort.fromHost(text).getHost(); + } catch(IllegalArgumentException e) { + return null; + } + } + + private Integer getPort(String text) { + try { + return Integer.parseInt(text); + } catch(NumberFormatException e) { + return null; + } + } + + private File getCertificate(String crtFileLocation) { + try { + File crtFile = new File(crtFileLocation); + if(!crtFile.exists()) { + return null; + } + + CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile)); + return crtFile; + } catch (Exception e) { + return null; + } + } + + private Glyph getGlyph(FontAwesome5.Glyph glyphName, Color color) { + Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName); + glyph.setFontSize(13); + if(color != null) { + glyph.setColor(color); + } + + return glyph; + } + + private static boolean isValidPort(int port) { + return port >= 0 && port <= 65535; + } +} diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..43571ed0baa5ae03ed6efb5ab3eb99f64453385b GIT binary patch literal 6148 zcmeHK%}T>S5ZbiZkUDajJfQTyq{dS*7yK3q!@>kx|1OE19M7l6mAb zl8%$PS*uxd)4Da|gnr<;>3-;Ejolz#kAij(Z>5`V7NiFlyqhEu1Ss3}_QIf3wAEA_~}emp~L6 zZH>7?=mFs>6;P#eeZ}A^9sEMa*&1_&DxGmTGStx{bA3bMa&+(u8P2$^kXmAZ82HG* zkb1Q6{9ph2{{JzFo`?Zr;9oJoi)+qW4W^{e)`7|4St~$KKv6I*SNN6!hCGTP7LVd7 bP$}RSXaL$8bA{jmp&tQB12x3Jk23HIleb|J literal 0 HcmV?d00001 diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index 8a01cfed..7e56f45f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -30,10 +30,6 @@ -fx-padding: -20 0 0 0; } -.toggle-switch { - -fx-translate-x: -20px; -} - .tab-error > .tab-container { -fx-effect: dropshadow(three-pass-box, rgba(202, 18, 67, .6), 7, 0, 0, 0); } @@ -84,18 +80,18 @@ -fx-border-color: transparent; } -.titled-description-pane .hyperlink { +.hyperlink { -fx-padding: 0; -fx-border-width: 0; -fx-fill: #1e88cf; } -.titled-description-pane .hyperlink:visited { +.hyperlink:visited { -fx-text-fill: #1e88cf; -fx-underline: false; } -.titled-description-pane .hyperlink:hover:visited { +.hyperlink:hover:visited { -fx-underline: true; } diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css new file mode 100644 index 00000000..86d0a67d --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.css @@ -0,0 +1,34 @@ +.dialog-pane .content { + -fx-padding: 0; +} + +.list-menu { + -fx-pref-width: 160; + -fx-background-color: #3da0e3; +} + +.list-item { + -fx-pref-width: 160; + -fx-padding: 0 20 0 20; + -fx-background-color: #3da0e3; +} + +.list-item * { + -fx-fill: #fff; +} + +.list-item:hover { + -fx-background-color: #4aa7e5; +} + +.list-item:selected { + -fx-background-color: #1e88cf; +} + +#preferencesPane { + -fx-background-color: -fx-background; +} + +.scroll-pane { + -fx-background-color: transparent; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.fxml new file mode 100644 index 00000000..1234188b --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/preferences.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml new file mode 100644 index 00000000..b750bdcc --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ + + + + + + + + +