mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +00:00
add welcome and preferences dialogs
This commit is contained in:
parent
6731823bef
commit
1e250193fd
27 changed files with 1184 additions and 40 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
11
build.gradle
11
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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Mode> 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();
|
||||
}
|
||||
|
||||
|
|
5
src/main/java/com/sparrowwallet/sparrow/Mode.java
Normal file
5
src/main/java/com/sparrowwallet/sparrow/Mode.java
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow;
|
||||
|
||||
public enum Mode {
|
||||
OFFLINE, ONLINE
|
||||
}
|
|
@ -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<Object> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ public class WalletNameDialog extends Dialog<String> {
|
|||
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);
|
||||
|
|
|
@ -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<Mode> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
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());
|
||||
List<String> serverVersion = client.createRequest().returnAsList(String.class).method("server.version").id(1).param("client_name", "Sparrow").param("protocol_version", "1.4").execute();
|
||||
return serverVersion.get(1);
|
||||
client.createRequest().returnAs(Void.class).method("server.ping").id(1).execute();
|
||||
}
|
||||
|
||||
public List<String> 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());
|
||||
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<WalletNode, Set<BlockTransactionHash>> 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<List<String>> {
|
||||
@Override
|
||||
protected Task<List<String>> createTask() {
|
||||
return new Task<List<String>>() {
|
||||
protected List<String> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.getServerVersion();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServerBannerService extends Service<String> {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
protected String call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.getServerBanner();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class PingService extends Service<Boolean> {
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
electrumServer.ping();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransactionHistoryService extends Service<Boolean> {
|
||||
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, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
if(serverCert != null && serverCert.exists()) {
|
||||
return new TcpOverTlsTransport(server, serverCert);
|
||||
} else {
|
||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
return new TcpOverTlsTransport(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new TcpOverTlsTransport(server, serverCert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, serverCert, proxy);
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public HostAndPort getServerHostAndPort(String url) {
|
||||
return HostAndPort.fromString(url.substring(this.toUrlString().length()));
|
||||
}
|
||||
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.preferences;
|
||||
|
||||
public enum PreferenceGroup {
|
||||
GENERAL, SERVER;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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<Void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
BIN
src/main/resources/.DS_Store
vendored
Normal file
BIN
src/main/resources/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.preferences.PreferenceGroup?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
<BorderPane stylesheets="@../general.css, @preferences.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.preferences.PreferencesController">
|
||||
<padding>
|
||||
<Insets top="0" left="0" right="0" bottom="0" />
|
||||
</padding>
|
||||
<left>
|
||||
<VBox styleClass="list-menu">
|
||||
<ToggleButton VBox.vgrow="ALWAYS" text="Server" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$preferencesMenu">
|
||||
<toggleGroup>
|
||||
<ToggleGroup fx:id="preferencesMenu" />
|
||||
</toggleGroup>
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SERVER" />
|
||||
</graphic>
|
||||
<userData>
|
||||
<PreferenceGroup fx:constant="SERVER"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
</VBox>
|
||||
</left>
|
||||
<center>
|
||||
<StackPane fx:id="preferencesPane">
|
||||
|
||||
</StackPane>
|
||||
</center>
|
||||
</BorderPane>
|
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import tornadofx.control.Form?>
|
||||
<?import tornadofx.control.Fieldset?>
|
||||
<?import tornadofx.control.Field?>
|
||||
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.ServerPreferencesController">
|
||||
<padding>
|
||||
<Insets left="25.0" right="25.0" top="25.0" />
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="100" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints />
|
||||
</rowConstraints>
|
||||
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Electrum Server">
|
||||
<Field text="URL:">
|
||||
<TextField fx:id="host" promptText="e.g. 127.0.0.1"/>
|
||||
<TextField fx:id="port" promptText="e.g. 50002" prefWidth="80" />
|
||||
</Field>
|
||||
<Field text="Use SSL:">
|
||||
<UnlabeledToggleSwitch fx:id="useSsl"/>
|
||||
</Field>
|
||||
<Field text="Certificate:" styleClass="label-button">
|
||||
<TextField fx:id="certificate" editable="false" promptText="Optional server certificate (.crt)"/>
|
||||
<Button fx:id="certificateSelect" maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Ed">
|
||||
<graphic>
|
||||
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset inputGrow="SOMETIMES" text="Proxy">
|
||||
<Field text="Use Proxy:">
|
||||
<UnlabeledToggleSwitch fx:id="useProxy"/>
|
||||
</Field>
|
||||
<Field text="Proxy URL:">
|
||||
<TextField fx:id="proxyHost" />
|
||||
<TextField fx:id="proxyPort" prefWidth="80" />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
|
||||
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<Button fx:id="testConnection" text="Test Connection">
|
||||
<graphic>
|
||||
<Glyph fontFamily="FontAwesome" icon="QUESTION_CIRCLE" prefWidth="13" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</StackPane>
|
||||
|
||||
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="2">
|
||||
<padding>
|
||||
<Insets top="10.0" bottom="20.0"/>
|
||||
</padding>
|
||||
<TextArea fx:id="testResults" editable="false" wrapText="true"/>
|
||||
</StackPane>
|
||||
|
||||
</GridPane>
|
|
@ -10,12 +10,12 @@
|
|||
<?import org.controlsfx.control.SegmentedButton?>
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import java.lang.String?>
|
||||
<?import org.controlsfx.control.ToggleSwitch?>
|
||||
<?import com.sparrowwallet.sparrow.control.RelativeTimelockSpinner?>
|
||||
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
|
||||
<?import com.sparrowwallet.sparrow.control.IdLabel?>
|
||||
<?import com.sparrowwallet.sparrow.control.CoinLabel?>
|
||||
<?import com.sparrowwallet.sparrow.control.AddressLabel?>
|
||||
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
|
||||
|
||||
<GridPane hgap="10.0" vgap="10.0" stylesheets="@input.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController">
|
||||
<padding>
|
||||
|
@ -90,7 +90,7 @@
|
|||
<CopyableLabel fx:id="signatures" />
|
||||
</Field>
|
||||
<Field text="RBF:">
|
||||
<ToggleSwitch fx:id="rbf"/>
|
||||
<UnlabeledToggleSwitch fx:id="rbf"/>
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
|
|
BIN
src/main/resources/image/.DS_Store
vendored
Normal file
BIN
src/main/resources/image/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
src/main/resources/image/sparrow-small.png
Normal file
BIN
src/main/resources/image/sparrow-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
127
src/main/resources/image/sparrow.svg
Normal file
127
src/main/resources/image/sparrow.svg
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Vectornator for iOS (http://vectornator.io/) --><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" viewBox="0 0 472.646 589">
|
||||
<metadata>
|
||||
<vectornator:setting key="DynamicGuides" value="1"/>
|
||||
<vectornator:setting key="CMYKEnabledKey" value="0"/>
|
||||
<vectornator:setting key="Units" value="Points"/>
|
||||
<vectornator:setting key="RulersVisible" value="1"/>
|
||||
<vectornator:setting key="SnapToGuides" value="1"/>
|
||||
<vectornator:setting key="GridSpacing" value="36"/>
|
||||
</metadata>
|
||||
<defs>
|
||||
<path d="M0+0L472.646+0L472.646+589L0+589L0+0Z" id="Mask"/>
|
||||
</defs>
|
||||
<g id="Untitled" vectornator:layerName="Untitled">
|
||||
<path d="M438.26+269.704L431.407+280.358L472.645+295.31L472.645+291.106L446.023+267.188L438.26+269.704Z" opacity="1" fill="#d1d3d3"/>
|
||||
<path d="M425.612+299.312L436.32+298.817L456.732+297.318L472.646+295.311L431.408+280.359L419.107+275.902L425.612+299.312Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M438.26+269.704L431.407+280.358L419.107+275.901L426.428+273.532L438.26+269.704Z" opacity="1" fill="#aba89e"/>
|
||||
<path d="M425.612+299.312L361.204+299.312L398.039+282.487L425.612+299.312Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M426.428+273.532L413.377+264.257L409.403+261.432L437.216+239.682L446.024+267.188L438.26+269.704L426.428+273.532Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M411.397+268.192L407.783+273.238L402.389+264.618L402.456+264.551L411.397+268.192Z" opacity="1" fill="#ffa400"/>
|
||||
<path d="M409.403+261.433L413.378+264.257L411.397+268.192L402.456+264.552L406.632+260.804L409.403+261.433Z" opacity="1" fill="#ff5000"/>
|
||||
<path d="M437.216+239.682L409.403+261.432L399.685+219.859L437.015+239.066L437.216+239.682Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M407.783+273.238L411.397+268.192L413.378+264.257L426.429+273.532L419.107+275.902L407.14+274.135L407.783+273.238Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M402.389+264.618L407.783+273.238L407.14+274.135L396.178+270.2L402.389+264.618Z" opacity="1" fill="#80622d"/>
|
||||
<path d="M407.14+274.135L419.107+275.902L398.186+282.42L397.691+282.286L397.677+282.273L358.848+271.458L396.178+270.2L407.14+274.135Z" opacity="1" fill="#d3ccbd"/>
|
||||
<path d="M402.455+264.552L402.389+264.619L398.922+259.064L406.632+260.804L402.455+264.552Z" opacity="1" fill="#ef7622"/>
|
||||
<path d="M398.922+259.064L402.389+264.619L396.178+270.2L395.429+262.557L398.922+259.064Z" opacity="1" fill="#d49000"/>
|
||||
<path d="M399.685+219.859L409.403+261.433L406.632+260.803L398.922+259.064L391.989+257.484L398.641+219.926L399.685+219.859Z" opacity="1" fill="#d8d1ca"/>
|
||||
<path d="M398.641+219.926L391.989+257.484L374.776+221.359L398.641+219.926Z" opacity="1" fill="#bcb9af"/>
|
||||
<path d="M398.186+282.42L419.107+275.901L425.612+299.312L398.039+282.487L398.186+282.42Z" opacity="1" fill="#d8d1ca"/>
|
||||
<path d="M398.038+282.487L361.203+299.312L360.373+299.312L357.59+271.498L358.848+271.458L397.677+282.273L397.69+282.286L398.038+282.487Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M360.373+299.312L392.845+334.5L386.487+341.648L386.166+342.947L385.952+343.094L331.944+314.531L360.373+299.312Z" opacity="1" fill="#d1d3d3"/>
|
||||
<path d="M374.776+221.359L391.989+257.484L357.617+270.963L374.776+221.359Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M386.113+343.174L379.769+368.766L368.566+406.391L329.402+383.208L385.953+343.094L386.113+343.174Z" opacity="1" fill="#555759"/>
|
||||
<path d="M385.952+343.094L329.401+383.208L331.944+314.531L385.952+343.094Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M356.399+270.775L324.235+265.689L343.214+246.709L348.649+241.275L374.776+221.358L357.617+270.962L356.399+270.775Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M368.566+406.391L367.468+407.984L367.455+407.984L295.23+409.094L295.216+409.094L295.083+408.814L294.641+407.876L329.402+383.208L368.566+406.391Z" opacity="1" fill="#d1d3d3"/>
|
||||
<path d="M367.454+407.984L343.147+443.025L319.228+456.437L295.229+409.094L367.454+407.984Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M361.203+299.312L425.611+299.312L412.895+310.529L404.195+321.732L392.845+334.5L360.373+299.312L361.203+299.312Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M357.59+271.498L360.373+299.311L331.944+314.53L324.235+265.689L356.399+270.775L357.549+271.096L357.59+271.498Z" opacity="1" fill="#bcb9af"/>
|
||||
<path d="M357.59+271.498L358.848+271.458L357.55+271.097L357.59+271.498Z" opacity="1" fill="#383a35"/>
|
||||
<path d="M391.989+257.484L398.923+259.064L395.429+262.557L396.178+270.2L358.848+271.458L357.55+271.096L356.399+270.775L357.617+270.963L391.989+257.484Z" opacity="1" fill="#333e48"/>
|
||||
<g opacity="1" vectornator:mask="#Mask">
|
||||
<clipPath id="ClipPath">
|
||||
<use xlink:href="#Mask" fill="none" overflow="visible"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#ClipPath)">
|
||||
<g opacity="1">
|
||||
<path d="M368.07+495.066C368.566+496.565+367.562+501.771+367.562+501.771L361.365+509.467L363.064+502.762L365.567+497.26L362.569+489.564L348.154+483.354L351.861+490.555L355.863+501.771L351.352+505.265L344.647+513.978L346.655+505.773L351.352+499.763C351.352+499.763+348.649+494.062+348.154+492.054C347.645+490.059+338.945+482.845+338.945+482.845L336.442+478.348L350.188+476.42L350.202+476.42L369.569+481.855L369.569+492.054L367.066+485.348L358.353+482.35L365.567+488.052C365.567+488.052+367.562+493.553+368.07+495.066" opacity="1" fill="#333e48"/>
|
||||
<path d="M350.188+476.42L336.442+478.348L313.527+463.932L345.651+475.149L350.188+476.42Z" opacity="1" fill="#7f7f73"/>
|
||||
<path d="M336.937+129.485L344.647+155.612L314.892+239.348L304.813+267.697L308.575+221.105L310.663+195.218L312.644+170.711L313.527+171.581L313.192+169.774L336.937+129.485Z" opacity="1" fill="#4f5858"/>
|
||||
<path d="M344.647+155.612L336.937+219.364L324.235+265.689L314.892+239.347L344.647+155.612Z" opacity="1" fill="#4b4f54"/>
|
||||
<path d="M336.937+219.364L343.214+246.709L324.235+265.689L336.937+219.364Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M336.937+129.485L313.192+169.774L307.611+139.711L307.611+139.698L308.267+102.314C315.026+108.752+319.336+113.008+319.738+113.785C321.932+117.987+336.937+129.485+336.937+129.485" opacity="1" fill="#7d868c"/>
|
||||
<path d="M336.442+478.348L322.241+474.64L302.819+466.141L313.527+463.932L336.442+478.348Z" opacity="1" fill="#d3ccbd"/>
|
||||
<path d="M331.945+314.53L329.402+383.208L278.472+373.972L267.697+351.351L331.289+314.905L331.945+314.53Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M330.927+314.289L331.288+314.905L267.697+351.351L299.539+313.941L300.316+313.031L330.927+314.289Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M329.402+383.208L294.641+407.876L278.472+373.973L329.402+383.208Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M320.728+495.561L322.736+486.057L336.442+478.348L338.945+482.845L325.734+488.56L324.235+494.557L326.738+501.771L320.728+495.561Z" opacity="1" fill="#707c7c"/>
|
||||
<path d="M314.892+239.347L324.235+265.689L304.813+267.696L314.892+239.347Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M302.337+292.592L304.813+267.696L324.234+265.689L300.449+311.692L302.337+292.592Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M319.229+456.437L313.527+463.932L302.819+466.141L289.528+459.395L319.229+456.437Z" opacity="1" fill="#707c7c"/>
|
||||
<path d="M311.533+525.676L305.817+515.477L297.813+508.972L319.684+513.71L323.231+516.976C323.231+516.976+327.233+522.678+327.742+525.181C328.237+527.684+329.241+533.386+329.241+535.394C329.241+537.388+321.237+543.599+321.237+543.599L325.239+537.897L322.736+525.676L312.523+518.475C312.523+518.475+317.529+531.392+318.024+533.386C318.533+535.394+316.03+540.092+314.022+540.601C312.095+541.082+306.206+546.262+305.831+546.583L313.527+535.394L311.533+525.676Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M319.684+513.71L297.813+508.972L296.488+496.483L296.609+496.564L303.314+500.272L314.022+508.477L319.684+513.71Z" opacity="1" fill="#707c7c"/>
|
||||
<path d="M312.644+170.71L310.663+195.218L199.247+122.284L169.319+98.3652L259.171+134.156L311.854+169.947L311.88+169.96L312.644+170.71Z" opacity="1" fill="#909c9c"/>
|
||||
<path d="M311.854+169.947L259.171+134.156L199.943+93.908L176.828+66.241L167.124+54.034L167.124+45.829L172.825+41.827L197.734+58.237L311.854+169.947Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M310.662+195.218L308.574+221.104L209.954+153.671L208.951+153.404L176.827+129.485L175.328+117.786L180.026+114.28L199.246+122.284L310.662+195.218Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M308.267+102.314L307.611+139.698L307.611+139.711L307.584+141.464L199.568+24.2927L308.267+102.314Z" opacity="1" fill="#909c9c"/>
|
||||
<path d="M308.267+102.314L199.568+24.2927L188.031+5.20669L191.738-0.000312805L200.746+2.99869C200.746+2.99869+278.405+73.8707+308.267+102.314" opacity="1" fill="#5c6670"/>
|
||||
<path d="M200.238+25.4177L199.569+24.2927L307.585+141.464L307.611+139.711L313.192+169.773L312.644+170.711L311.881+169.961L311.854+169.947L197.735+58.2367L178.327+31.1197L176.319+21.4157L183.533+15.4057L200.238+25.4177Z" opacity="1" fill="#4f5858"/>
|
||||
<path d="M304.653+267.603L220.663+190.238L209.446+178.527L203.664+168.568L203.678+168.568L308.575+221.104L304.813+267.696L304.653+267.603Z" opacity="1" fill="#4b4f54"/>
|
||||
<path d="M304.813+267.697L302.337+292.593L251.783+239.576L304.653+267.604L304.813+267.697Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M304.653+267.603L251.783+239.575L223.862+210.65L215.162+197.439L214.653+188.739L220.663+190.238L304.653+267.603Z" opacity="1" fill="#383a35"/>
|
||||
<path d="M330.927+314.289L300.316+313.031L300.449+311.692L324.235+265.689L331.945+314.53L331.288+314.905L330.927+314.289Z" opacity="1" fill="#555759"/>
|
||||
<path d="M300.316+313.031L299.54+313.941L298.134+313.887L300.316+313.031Z" opacity="1" fill="#a3402a"/>
|
||||
<path d="M300.316+313.031L298.134+313.887L209.553+310.474L233.794+255.556L300.316+313.031Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M299.539+313.942L267.696+351.352L225.2+370.399L222.657+368.07L203.45+366.062L195.245+361.645L248.275+333.444L298.134+313.888L299.539+313.942Z" opacity="1" fill="#555759"/>
|
||||
<path d="M298.134+313.888L248.276+333.444L209.553+310.489L209.553+310.475L298.134+313.888Z" opacity="1" fill="#c3c6c8"/>
|
||||
<path d="M294.614+503.27L281.778+487.088L296.488+496.484L297.813+508.972L292.111+517.98L291.107+526.68L296.609+536.893L288.604+530.883L286.41+519.185L289.608+512.479L294.614+503.27Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M296.475+496.377L296.488+496.483L281.778+487.088L280.399+473.141L296.475+496.377Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M289.019+459.421L288.43+459.315L295.216+409.094L295.23+409.094L319.229+456.437L289.528+459.395L289.113+459.435L289.033+459.421L289.019+459.421Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M295.216+409.094L288.43+459.315L205.444+444.725L295.216+409.094Z" opacity="1" fill="#a0a1a2"/>
|
||||
<path d="M295.216+409.094L205.444+444.725L254.299+386.903L295.216+409.094Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M295.216+409.094L254.299+386.903L278.472+373.973L294.641+407.876L294.613+407.89L295.082+408.814L295.216+409.094Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M289.113+459.435L289.528+459.395L302.819+466.141L280.399+473.141L289.113+459.435Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M289.113+459.435L280.399+473.141L281.778+487.088L280.399+485.348L269.692+475.149L289.113+459.435Z" opacity="1" fill="#d3ccbd"/>
|
||||
<path d="M289.113+459.435L269.692+475.149L263.495+476.849L250.043+459.595L288.418+459.421L289.019+459.421L289.033+459.421L289.113+459.435Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M288.43+459.315L288.417+459.421L250.042+459.595L250.002+459.595L211.333+459.756L211.32+459.756L205.444+444.725L288.43+459.315Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M267.697+351.352L278.472+373.973L254.3+386.903L267.697+351.352Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M252.158+478.535L263.495+476.848C263.495+476.848+233.366+547.573+227.021+563.073L252.158+478.535Z" opacity="1" fill="#555759"/>
|
||||
<path d="M199.943+93.9079L259.172+134.156L169.319+98.3649L165.116+88.6609L167.124+81.6609L174.325+81.6609L200.746+94.8719L199.943+93.9079Z" opacity="1" fill="#707c7c"/>
|
||||
<path d="M254.299+386.902L205.444+444.724L214.653+405.895L224.999+371.081L225.254+371.161L254.299+386.902Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M252.158+478.535L227.021+563.073C226.325+564.786+225.924+565.817+225.87+566.018C225.361+568.013+215.162+577.222+215.162+577.222L204.95+577.717L252.158+478.535Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M250.002+459.595L250.042+459.595L263.494+476.849L252.157+478.535L229.858+481.855L250.002+459.595Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M250.002+459.595L229.858+481.855L223.313+477.21L216.152+472.137L211.32+459.77L211.333+459.756L250.002+459.595Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M248.276+333.444L195.245+361.645L195.232+361.645L193.237+360.561L185.836+352.25L209.553+310.489L248.276+333.444Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M227.864+250.43L233.794+255.556L179.465+251.126L159.106+249.466L158.049+249.185L111.041+201.482L202.567+229.362L227.864+250.43Z" opacity="1" fill="#bcb9af"/>
|
||||
<path d="M229.858+481.855L202.647+580.367C200.465+582.777+197.293+585.935+195.74+585.935C193.237+585.935+180.535+588.424+180.535+588.424L175.114+588.599L229.858+481.855Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M229.858+481.855L175.114+588.599L163.228+589.001L153.403+587.929L151.49+585.039L223.313+477.21L229.858+481.855Z" opacity="1" fill="#555759"/>
|
||||
<path d="M222.55+210.209L227.863+250.431L202.566+229.363L149.937+185.527L222.55+210.209Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M225.254+371.161L225+371.081L225.2+370.398L267.697+351.351L254.299+386.902L225.254+371.161Z" opacity="1" fill="#8a8a8d"/>
|
||||
<path d="M223.862+210.651L251.783+239.576L302.338+292.593L300.45+311.693L300.316+313.032L233.794+255.557L227.864+250.431L222.55+210.209L223.862+210.651Z" opacity="1" fill="#555759"/>
|
||||
<path d="M223.313+477.21L151.49+585.038L142.696+585.935L131.493+581.719L127.491+572.215L223.313+477.21Z" opacity="1" fill="#8a8c8c"/>
|
||||
<path d="M223.313+477.21L127.491+572.215L211.32+459.77L216.152+472.137L223.313+477.21Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M211.333+459.756L211.32+459.77L211.32+459.756L211.333+459.756Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M211.32+459.756L211.32+459.77L110.371+555.872L148.906+509.467L205.444+444.725L211.32+459.756Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M211.32+459.77L127.49+572.215L117.787+573.714L108.578+568.013L109.582+556.81L110.344+555.886L110.371+555.873L211.32+459.77Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M233.793+255.557L209.553+310.475L209.54+310.475L209.178+310.261L179.464+251.126L233.793+255.557Z" opacity="1" fill="#7d868c"/>
|
||||
<path d="M209.54+310.475L209.553+310.475L209.553+310.489L185.835+352.249L185.032+351.352L169.319+351.861L163.122+347.859L156.415+341.153L151.41+333.444L152.788+332.895L209.54+310.475Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M209.178+310.261L209.539+310.475L152.787+332.895L168.448+286.115L209.178+310.261Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M204.949+577.717C204.949+577.717+203.985+578.895+202.647+580.366L229.858+481.855L252.157+478.535L204.949+577.717Z" opacity="1" fill="#949a90"/>
|
||||
<path d="M202.941+167.324L202.446+152.908L208.951+153.404L209.955+153.671L308.574+221.104L203.677+168.569L203.664+168.569L202.941+167.324Z" opacity="1" fill="#3e4543"/>
|
||||
<path d="M149.937+185.527L202.566+229.362L111.041+201.482L36.1253+155.117L149.923+185.527L149.937+185.527Z" opacity="1" fill="#d1d3d3"/>
|
||||
<path d="M168.449+286.115L167.164+286.115L86.1585+286.115C86.1585+286.115+57.5415+270.2+57.0325+268.688C56.5375+267.189+52.0405+257.485+52.0405+257.485L53.0315+250.98L133.929+269.277L135.361+269.598L168.449+286.115Z" opacity="1" fill="#d1d3d3"/>
|
||||
<path d="M168.449+286.115L152.788+332.895L151.41+333.444L139.203+332.44L129.499+328.946L123.288+322.227L120.986+312.858L167.164+286.115L168.449+286.115Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M167.163+286.115L120.985+312.858L120.785+312.028L94.8714+308.521L88.6614+298.817L86.1584+286.115L167.163+286.115Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M111.041+201.482L158.048+249.186L156.309+248.717L35.7248+187.602L18.2038+173.32L4.0018+154.608L0.495804+143.204L2.9988+137.703L15.7008+144.904L36.1258+155.117L111.041+201.482Z" opacity="1" fill="#333e48"/>
|
||||
<path d="M133.929+269.276L53.031+250.979L30.116+244.273L153.217+247.901L156.308+248.717L158.048+249.186L159.106+249.466L133.929+269.276Z" opacity="1" fill="#5c6670"/>
|
||||
<path d="M133.929+269.276L159.106+249.466L179.464+251.126L209.179+310.261L168.449+286.114L135.361+269.597L133.929+269.276Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M149.924+185.527L36.1256+155.117L9.7036+122.284L8.6996+113.784L12.2066+109.582L54.0346+141.196C54.0346+141.196+108.578+167.819+111.576+169.827C114.575+171.821+136.7+181.03+136.7+181.03L149.924+185.527Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M153.217+247.901L38.9766+217.464L40.1276+217.651L11.7116+202.941L2.9986+190.239L-0.000396729+179.531L4.00259+175.328L37.1156+188.74L35.7246+187.602L156.309+248.717L153.217+247.901Z" opacity="1" fill="#99999a"/>
|
||||
<path d="M38.9632+217.464L38.9772+217.464L153.216+247.901L30.1162+244.274L18.7122+232.575L14.7102+218.856L16.2092+213.649L38.9632+217.464Z" opacity="1" fill="#333e48"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
Loading…
Reference in a new issue