diff --git a/build.gradle b/build.gradle index 7d729ebd..d2d6858c 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ dependencies { mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp' 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"] + 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"] } jlink { @@ -63,7 +63,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"] + 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"] } addExtraDependencies("javafx") jpackage { diff --git a/drongo b/drongo index d394c25a..be0c4d11 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit d394c25a3c05c02d984b1f709623a311c2afb7a1 +Subproject commit be0c4d1176da41671c2629e65f812fd28fab202b diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index 950b415a..08be68eb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -41,9 +41,9 @@ public class MainApp extends Application { wallet.setScriptType(ScriptType.P2WPKH); KeystoreImportDialog dlg = new KeystoreImportDialog(wallet); - //dlg.showAndWait(); + dlg.showAndWait(); - stage.show(); + //stage.show(); } public static void main(String[] args) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index f4011bdc..c07c1d67 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -128,7 +128,8 @@ public class DevicePane extends TitledPane { listItem.getChildren().add(buttonBox); this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> { - listItem.setPrefWidth(newValue.getWidth()); + //Hack to force listItem to expand to full available width less border + listItem.setPrefWidth(newValue.getWidth() - 2); }); return listItem; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java new file mode 100644 index 00000000..de4fb658 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java @@ -0,0 +1,121 @@ +package com.sparrowwallet.sparrow.control; + +import com.google.gson.JsonParseException; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.io.KeystoreFileImport; +import com.sparrowwallet.sparrow.io.KeystoreImport; +import javafx.beans.property.SimpleStringProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import org.controlsfx.control.textfield.CustomPasswordField; +import org.controlsfx.control.textfield.TextFields; + +import java.io.*; + +public class FileKeystoreImportPane extends KeystoreImportPane { + private final KeystoreFileImport importer; + private Button importButton; + private final SimpleStringProperty password = new SimpleStringProperty(""); + + public FileKeystoreImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreFileImport importer) { + super(importAccordion, wallet, importer); + this.importer = importer; + } + + @Override + protected Node getTitle(KeystoreImport importer) { + Node title = super.getTitle(importer); + + setDescription("Keystore file import"); + + importButton = new Button("Import File..."); + importButton.setAlignment(Pos.CENTER_RIGHT); + importButton.setOnAction(event -> { + importFile(); + }); + buttonBox.getChildren().add(importButton); + + return title; + } + + private void importFile() { + Stage window = new Stage(); + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " keystore"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", "*.*"), + new FileChooser.ExtensionFilter("JSON", "*.json") + ); + + File file = fileChooser.showOpenDialog(window); + if(file != null) { + importFile(file, null); + } + } + + private void importFile(File file, String password) { + if(file.exists()) { + try { + if(importer.isEncrypted(file) && password == null) { + setDescription("Password Required"); + showHideLink.setVisible(false); + setContent(getPasswordEntry(file)); + importButton.setDisable(true); + setExpanded(true); + } else { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password); + EventManager.get().post(new KeystoreImportEvent(keystore)); + } + } catch (Exception e) { + String errorMessage = e.getMessage(); + if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + if(e instanceof ECKey.InvalidPasswordException || e.getCause() instanceof ECKey.InvalidPasswordException) { + errorMessage = "Invalid wallet password"; + } + if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) { + errorMessage = "File was not in JSON format"; + } + setError("Import Error", errorMessage); + importButton.setDisable(false); + } + } + } + + private Node getPasswordEntry(File file) { + CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField(); + passwordField.setPromptText("Wallet password"); + password.bind(passwordField.textProperty()); + HBox.setHgrow(passwordField, Priority.ALWAYS); + + Button importEncryptedButton = new Button("Import"); + importEncryptedButton.setOnAction(event -> { + showHideLink.setVisible(true); + setExpanded(false); + importFile(file, password.get()); + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(passwordField); + contentBox.getChildren().add(importEncryptedButton); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(60); + + return contentBox; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java deleted file mode 100644 index e748463e..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.sparrowwallet.sparrow.control; - -import com.google.gson.JsonParseException; -import com.sparrowwallet.drongo.crypto.ECKey; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.KeystoreImportEvent; -import com.sparrowwallet.sparrow.io.KeystoreFileImport; -import javafx.application.Platform; -import javafx.beans.property.SimpleStringProperty; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.Label; -import javafx.scene.control.TitledPane; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import org.controlsfx.control.textfield.CustomPasswordField; -import org.controlsfx.control.textfield.TextFields; - -import java.io.*; - -public class KeystoreFileImportPane extends TitledPane { - private final KeystoreImportAccordion importAccordion; - private final Wallet wallet; - private final KeystoreFileImport importer; - - private Label mainLabel; - private Label descriptionLabel; - private Hyperlink showHideLink; - private Button importButton; - - private final SimpleStringProperty password = new SimpleStringProperty(""); - - public KeystoreFileImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreFileImport importer) { - this.importAccordion = importAccordion; - this.wallet = wallet; - this.importer = importer; - - setPadding(Insets.EMPTY); - - setGraphic(getTitle()); - getStyleClass().add("importpane"); - setContent(getContentBox(importer.getKeystoreImportDescription())); - - removeArrow(); - } - - private void removeArrow() { - Platform.runLater(() -> { - Node arrow = this.lookup(".arrow"); - if (arrow != null) { - arrow.setVisible(false); - arrow.setManaged(false); - } else { - removeArrow(); - } - }); - } - - private Node getTitle() { - HBox listItem = new HBox(); - listItem.setPadding(new Insets(10, 20, 10, 10)); - listItem.setSpacing(10); - - HBox imageBox = new HBox(); - imageBox.setMinWidth(50); - imageBox.setMinHeight(50); - listItem.getChildren().add(imageBox); - - Image image = new Image("image/" + importer.getWalletModel().getType() + ".png", 50, 50, true, true); - if (!image.isError()) { - ImageView imageView = new ImageView(); - imageView.setImage(image); - imageBox.getChildren().add(imageView); - } - - VBox labelsBox = new VBox(); - labelsBox.setSpacing(5); - labelsBox.setAlignment(Pos.CENTER_LEFT); - this.mainLabel = new Label(); - mainLabel.setText(importer.getName()); - mainLabel.getStyleClass().add("main-label"); - labelsBox.getChildren().add(mainLabel); - - HBox descriptionBox = new HBox(); - descriptionBox.setSpacing(7); - labelsBox.getChildren().add(descriptionBox); - - descriptionLabel = new Label("Keystore file import"); - descriptionLabel.getStyleClass().add("description-label"); - showHideLink = new Hyperlink("View Details..."); - showHideLink.managedProperty().bind(showHideLink.visibleProperty()); - showHideLink.setOnAction(event -> { - if(showHideLink.getText().contains("View")) { - setExpanded(true); - showHideLink.setText("Hide Details..."); - } else { - setExpanded(false); - showHideLink.setText("View Details..."); - } - }); - descriptionBox.getChildren().addAll(descriptionLabel, showHideLink); - - listItem.getChildren().add(labelsBox); - HBox.setHgrow(labelsBox, Priority.ALWAYS); - - HBox buttonBox = new HBox(); - buttonBox.setAlignment(Pos.CENTER_RIGHT); - - importButton = new Button("Import File..."); - importButton.setAlignment(Pos.CENTER_RIGHT); - importButton.setOnAction(event -> { - importFile(); - }); - - buttonBox.getChildren().add(importButton); - listItem.getChildren().add(buttonBox); - - this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> { - listItem.setPrefWidth(newValue.getWidth()); - }); - - return listItem; - } - - private void importFile() { - Stage window = new Stage(); - - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " keystore"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("All Files", "*.*"), - new FileChooser.ExtensionFilter("JSON", "*.json") - ); - - File file = fileChooser.showOpenDialog(window); - if(file != null) { - importFile(file, null); - } - } - - private void importFile(File file, String password) { - if(file.exists()) { - try { - if(importer.isEncrypted(file) && password == null) { - descriptionLabel.getStyleClass().remove("description-error"); - descriptionLabel.getStyleClass().add("description-label"); - descriptionLabel.setText("Password Required"); - showHideLink.setVisible(false); - setContent(getPasswordEntry(file)); - importButton.setDisable(true); - setExpanded(true); - } else { - InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password); - EventManager.get().post(new KeystoreImportEvent(keystore)); - } - } catch (Exception e) { - descriptionLabel.getStyleClass().remove("description-label"); - descriptionLabel.getStyleClass().add("description-error"); - descriptionLabel.setText("Import Error"); - String errorMessage = e.getMessage(); - if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { - errorMessage = e.getCause().getMessage(); - } - if(e instanceof ECKey.InvalidPasswordException || e.getCause() instanceof ECKey.InvalidPasswordException) { - errorMessage = "Invalid wallet password"; - } - if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) { - errorMessage = "File was not in JSON format"; - } - setContent(getContentBox(errorMessage)); - setExpanded(true); - showHideLink.setText("Hide Details..."); - importButton.setDisable(false); - } - } - } - - private Node getContentBox(String message) { - Label details = new Label(message); - details.setWrapText(true); - - HBox contentBox = new HBox(); - contentBox.setAlignment(Pos.TOP_LEFT); - contentBox.getChildren().add(details); - contentBox.setPadding(new Insets(10, 30, 10, 30)); - - double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D); - double numLines = Math.max(1, width / 400); - double height = Math.max(60, numLines * 40); - contentBox.setPrefHeight(height); - - return contentBox; - } - - private Node getPasswordEntry(File file) { - CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField(); - passwordField.setPromptText("Wallet password"); - password.bind(passwordField.textProperty()); - HBox.setHgrow(passwordField, Priority.ALWAYS); - - Button importEncryptedButton = new Button("Import"); - importEncryptedButton.setOnAction(event -> { - showHideLink.setVisible(true); - setExpanded(false); - importFile(file, password.get()); - }); - - HBox contentBox = new HBox(); - contentBox.setAlignment(Pos.TOP_RIGHT); - contentBox.setSpacing(20); - contentBox.getChildren().add(passwordField); - contentBox.getChildren().add(importEncryptedButton); - contentBox.setPadding(new Insets(10, 30, 10, 30)); - contentBox.setPrefHeight(60); - - return contentBox; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportAccordion.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportAccordion.java index 7a249c71..b8ef94ff 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportAccordion.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportAccordion.java @@ -16,12 +16,12 @@ public class KeystoreImportAccordion extends Accordion { this.importers = importers; for(KeystoreImport importer : importers) { - KeystoreFileImportPane importPane = null; + KeystoreImportPane importPane = null; if(importer instanceof KeystoreFileImport) { - importPane = new KeystoreFileImportPane(this, wallet, (KeystoreFileImport)importer); + importPane = new FileKeystoreImportPane(this, wallet, (KeystoreFileImport)importer); } else if(importer instanceof KeystoreMnemonicImport) { - //TODO: Import from the new Bip39KeystoreImport + importPane = new MnemonicKeystoreImportPane(this, wallet, (KeystoreMnemonicImport)importer); } else { throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportPane.java new file mode 100644 index 00000000..73e5e6f4 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreImportPane.java @@ -0,0 +1,144 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.KeystoreImport; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.TitledPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +public abstract class KeystoreImportPane extends TitledPane { + protected final KeystoreImportAccordion importAccordion; + protected final Wallet wallet; + + private Label mainLabel; + private Label descriptionLabel; + protected Hyperlink showHideLink; + protected HBox buttonBox; + + public KeystoreImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreImport importer) { + this.importAccordion = importAccordion; + this.wallet = wallet; + + setPadding(Insets.EMPTY); + setGraphic(getTitle(importer)); + getStyleClass().add("importpane"); + setContent(getContentBox(importer.getKeystoreImportDescription())); + removeArrow(); + } + + private void removeArrow() { + Platform.runLater(() -> { + Node arrow = this.lookup(".arrow"); + if (arrow != null) { + arrow.setVisible(false); + arrow.setManaged(false); + } else { + removeArrow(); + } + }); + } + + protected Node getTitle(KeystoreImport importer) { + HBox listItem = new HBox(); + listItem.setPadding(new Insets(10, 20, 10, 10)); + listItem.setSpacing(10); + + HBox imageBox = new HBox(); + imageBox.setMinWidth(50); + imageBox.setMinHeight(50); + listItem.getChildren().add(imageBox); + + Image image = new Image("image/" + importer.getWalletModel().getType() + ".png", 50, 50, true, true); + if (!image.isError()) { + ImageView imageView = new ImageView(); + imageView.setImage(image); + imageBox.getChildren().add(imageView); + } + + VBox labelsBox = new VBox(); + labelsBox.setSpacing(5); + labelsBox.setAlignment(Pos.CENTER_LEFT); + this.mainLabel = new Label(); + mainLabel.setText(importer.getName()); + mainLabel.getStyleClass().add("main-label"); + labelsBox.getChildren().add(mainLabel); + + HBox descriptionBox = new HBox(); + descriptionBox.setSpacing(7); + labelsBox.getChildren().add(descriptionBox); + + descriptionLabel = new Label("Keystore Import"); + descriptionLabel.getStyleClass().add("description-label"); + showHideLink = new Hyperlink("Show Details..."); + showHideLink.managedProperty().bind(showHideLink.visibleProperty()); + showHideLink.setOnAction(event -> { + if(this.isExpanded()) { + setExpanded(false); + } else { + setExpanded(true); + } + }); + this.expandedProperty().addListener((observable, oldValue, newValue) -> { + if(newValue) { + showHideLink.setText(showHideLink.getText().replace("Show", "Hide")); + } else { + showHideLink.setText(showHideLink.getText().replace("Hide", "Show")); + } + }); + descriptionBox.getChildren().addAll(descriptionLabel, showHideLink); + + listItem.getChildren().add(labelsBox); + HBox.setHgrow(labelsBox, Priority.ALWAYS); + + buttonBox = new HBox(); + buttonBox.setAlignment(Pos.CENTER_RIGHT); + listItem.getChildren().add(buttonBox); + + this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> { + //Hack to force listItem to expand to full available width less border + listItem.setPrefWidth(newValue.getWidth() - 2); + }); + + return listItem; + } + + protected void setDescription(String text) { + descriptionLabel.getStyleClass().remove("description-error"); + descriptionLabel.getStyleClass().add("description-label"); + descriptionLabel.setText(text); + } + + protected void setError(String title, String detail) { + descriptionLabel.getStyleClass().remove("description-label"); + descriptionLabel.getStyleClass().add("description-error"); + descriptionLabel.setText(title); + setContent(getContentBox(detail)); + setExpanded(true); + } + + protected Node getContentBox(String message) { + Label details = new Label(message); + details.setWrapText(true); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_LEFT); + contentBox.getChildren().add(details); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + + double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D); + double numLines = Math.max(1, width / 400); + double height = Math.max(60, numLines * 40); + contentBox.setPrefHeight(height); + + return contentBox; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java new file mode 100644 index 00000000..7c2e37c0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java @@ -0,0 +1,312 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.wallet.Bip39Calculator; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.io.*; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.util.Callback; +import org.controlsfx.control.textfield.AutoCompletionBinding; +import org.controlsfx.control.textfield.CustomPasswordField; +import org.controlsfx.control.textfield.CustomTextField; +import org.controlsfx.control.textfield.TextFields; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class MnemonicKeystoreImportPane extends KeystoreImportPane { + private final KeystoreMnemonicImport importer; + + private SplitMenuButton enterMnemonicButton; + private SplitMenuButton importButton; + + private SimpleListProperty wordEntriesProperty; + private final SimpleStringProperty passphraseProperty = new SimpleStringProperty(); + + public MnemonicKeystoreImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreMnemonicImport importer) { + super(importAccordion, wallet, importer); + this.importer = importer; + } + + @Override + protected Node getTitle(KeystoreImport importer) { + Node title = super.getTitle(importer); + setDescription("Keystore file import"); + + createEnterMnemonicButton(); + createImportButton(); + buttonBox.getChildren().addAll(enterMnemonicButton, importButton); + + return title; + } + + private void createEnterMnemonicButton() { + enterMnemonicButton = new SplitMenuButton(); + enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT); + enterMnemonicButton.setText("Enter Mnemonic"); + enterMnemonicButton.setOnAction(event -> { + enterMnemonic(24); + }); + int[] numberWords = new int[] {24, 21, 18, 15, 12}; + for(int i = 0; i < numberWords.length; i++) { + MenuItem item = new MenuItem(numberWords[i] + " words"); + final int words = numberWords[i]; + item.setOnAction(event -> { + enterMnemonic(words); + }); + enterMnemonicButton.getItems().add(item); + } + enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty()); + } + + private void createImportButton() { + importButton = new SplitMenuButton(); + importButton.setAlignment(Pos.CENTER_RIGHT); + importButton.setText("Import Keystore"); + importButton.setOnAction(event -> { + importButton.setDisable(true); + importKeystore(wallet.getScriptType().getDefaultDerivation(), false); + }); + String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"}; + for(int i = 0; i < accounts.length; i++) { + MenuItem item = new MenuItem(accounts[i]); + final List derivation = wallet.getScriptType().getDefaultDerivation(i); + item.setOnAction(event -> { + importButton.setDisable(true); + importKeystore(derivation, false); + }); + importButton.getItems().add(item); + } + + + + importButton.managedProperty().bind(importButton.visibleProperty()); + importButton.setVisible(false); + } + + private void enterMnemonic(int numWords) { + setDescription("Enter mnemonic word list"); + showHideLink.setVisible(false); + setContent(getMnemonicWordsEntry(numWords)); + setExpanded(true); + } + + private Node getMnemonicWordsEntry(int numWords) { + VBox vBox = new VBox(); + vBox.setSpacing(10); + + TilePane tilePane = new TilePane(); + tilePane.setPrefRows(numWords/3); + tilePane.setHgap(10); + tilePane.setVgap(10); + tilePane.setOrientation(Orientation.VERTICAL); + + List words = new ArrayList<>(); + for(int i = 0; i < numWords; i++) { + words.add(""); + } + + ObservableList wordEntryList = FXCollections.observableArrayList(words); + wordEntriesProperty = new SimpleListProperty<>(wordEntryList); + for(int i = 0; i < numWords; i++) { + WordEntry wordEntry = new WordEntry(i, wordEntryList); + tilePane.getChildren().add(wordEntry); + } + + vBox.getChildren().add(tilePane); + + AnchorPane anchorPane = new AnchorPane(); + anchorPane.setPadding(new Insets(0, 32, 0, 10)); + + PassphraseEntry passphraseEntry = new PassphraseEntry(); + AnchorPane.setLeftAnchor(passphraseEntry, 0.0); + + Button okButton = new Button("Ok"); + okButton.setPrefWidth(70); + okButton.setDisable(true); + okButton.setOnAction(event -> { + prepareImport(); + }); + + wordEntriesProperty.addListener((ListChangeListener) c -> { + for(String word : wordEntryList) { + if(!WordEntry.isValid(word)) { + okButton.setDisable(true); + return; + } + } + + okButton.setDisable(false); + }); + + AnchorPane.setRightAnchor(okButton, 0.0); + + anchorPane.getChildren().addAll(passphraseEntry, okButton); + vBox.getChildren().add(anchorPane); + + return vBox; + } + + private void prepareImport() { + if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) { + setExpanded(false); + enterMnemonicButton.setVisible(false); + importButton.setVisible(true); + importButton.setDisable(false); + setDescription("Ready to import"); + showHideLink.setText("Show Derivation..."); + showHideLink.setVisible(true); + setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation())); + } + } + + private boolean importKeystore(List derivation, boolean dryrun) { + importButton.setDisable(true); + try { + Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get()); + if(!dryrun) { + EventManager.get().post(new KeystoreImportEvent(keystore)); + } + return true; + } catch (ImportException e) { + String errorMessage = e.getMessage(); + if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + setError("Import Error", errorMessage); + importButton.setDisable(false); + return false; + } + } + + private static class WordEntry extends HBox { + private static List wordList; + + public WordEntry(int wordNumber, ObservableList wordEntryList) { + super(); + setAlignment(Pos.CENTER_RIGHT); + + setSpacing(10); + Label label = new Label((wordNumber+1) + "."); + label.setPrefWidth(20); + label.setAlignment(Pos.CENTER_RIGHT); + TextField wordField = new TextField(); + wordField.setMaxWidth(100); + + Bip39Calculator bip39Calculator = new Bip39Calculator(); + wordList = bip39Calculator.getWordList(); + TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList)); + + ValidationSupport validationSupport = new ValidationSupport(); + validationSupport.registerValidator(wordField, Validator.combine( + Validator.createEmptyValidator("Word is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue)) + )); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + + wordField.textProperty().addListener((observable, oldValue, newValue) -> { + wordEntryList.set(wordNumber, newValue); + }); + + this.getChildren().addAll(label, wordField); + } + + public static boolean isValid(String word) { + return wordList.contains(word); + } + } + + private static class WordlistSuggestionProvider implements Callback> { + private final List wordList; + + public WordlistSuggestionProvider(List wordList) { + this.wordList = wordList; + } + + @Override + public Collection call(AutoCompletionBinding.ISuggestionRequest request) { + List suggestions = new ArrayList<>(); + if(!request.getUserText().isEmpty()) { + for(String word : wordList) { + if(word.startsWith(request.getUserText())) { + suggestions.add(word); + } + } + } + + return suggestions; + } + } + + private class PassphraseEntry extends HBox { + public PassphraseEntry() { + super(); + + setAlignment(Pos.CENTER_RIGHT); + setSpacing(10); + Label passphraseLabel = new Label("Passphrase:"); + CustomTextField passphraseField = (CustomTextField) TextFields.createClearableTextField(); + passphraseProperty.bind(passphraseField.textProperty()); + + getChildren().addAll(passphraseLabel, passphraseField); + } + } + + private Node getDerivationEntry(List derivation) { + TextField derivationField = new TextField(); + derivationField.setPromptText("Derivation path"); + derivationField.setText(KeyDerivation.writePath(derivation)); + HBox.setHgrow(derivationField, Priority.ALWAYS); + + ValidationSupport validationSupport = new ValidationSupport(); + validationSupport.registerValidator(derivationField, Validator.combine( + Validator.createEmptyValidator("Derivation is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue)) + )); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + + Button importDerivationButton = new Button("Import"); + importDerivationButton.setOnAction(event -> { + showHideLink.setVisible(true); + setExpanded(false); + List importDerivation = KeyDerivation.parsePath(derivationField.getText()); + importKeystore(importDerivation, false); + }); + + derivationField.textProperty().addListener((observable, oldValue, newValue) -> { + importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue)); + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(derivationField); + contentBox.getChildren().add(importDerivationButton); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(60); + + return contentBox; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Bip39.java b/src/main/java/com/sparrowwallet/sparrow/io/Bip39.java index 8b039609..423567f2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Bip39.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Bip39.java @@ -15,7 +15,7 @@ public class Bip39 implements KeystoreMnemonicImport { @Override public WalletModel getWalletModel() { - return WalletModel.SPARROW; + return WalletModel.SEED; } @Override @@ -25,8 +25,12 @@ public class Bip39 implements KeystoreMnemonicImport { @Override public Keystore getKeystore(List derivation, List mnemonicWords, String passphrase) throws ImportException { - Bip39Calculator bip39Calculator = new Bip39Calculator(); - byte[] seed = bip39Calculator.getSeed(mnemonicWords, passphrase); - return Keystore.fromSeed(seed, derivation); + try { + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(mnemonicWords, passphrase); + return Keystore.fromSeed(seed, derivation); + } catch (Exception e) { + throw new ImportException(e); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java index a0070150..77bb9632 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java @@ -37,8 +37,12 @@ public class KeystoreImportController implements Initializable { public void initializeView(Wallet wallet) { this.wallet = wallet; importMenu.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> { + if(selectedToggle == null) { + oldValue.setSelected(true); + return; + } + KeystoreSource importType = (KeystoreSource) selectedToggle.getUserData(); - System.out.println(importType); String fxmlName = importType.toString().toLowerCase(); if(importType == KeystoreSource.SW_SEED || importType == KeystoreSource.SW_WATCH) { fxmlName = "sw"; diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java index debc5c0a..a9da06e3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java @@ -31,8 +31,8 @@ public class KeystoreImportDialog extends Dialog { final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); dialogPane.getButtonTypes().addAll(cancelButtonType); - dialogPane.setPrefWidth(620); - dialogPane.setPrefHeight(500); + dialogPane.setPrefWidth(650); + dialogPane.setPrefHeight(600); setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null); } catch(IOException e) { diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java index 05e29d35..1443daa7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.keystoreimport; import com.sparrowwallet.sparrow.control.KeystoreImportAccordion; +import com.sparrowwallet.sparrow.io.Bip39; import com.sparrowwallet.sparrow.io.Electrum; import com.sparrowwallet.sparrow.io.KeystoreImport; import javafx.collections.FXCollections; @@ -13,7 +14,7 @@ public class SwController extends KeystoreImportDetailController { private KeystoreImportAccordion importAccordion; public void initializeView() { - List importers = List.of(new Electrum()); + List importers = List.of(new Bip39(), new Electrum()); importAccordion.setKeystoreImporters(getMasterController().getWallet(), FXCollections.observableList(importers)); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index 0554a883..976a786f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -31,6 +31,11 @@ public class WalletController extends WalletFormController implements Initializa public void initializeView() { walletMenu.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> { + if(selectedToggle == null) { + oldValue.setSelected(true); + return; + } + Function function = (Function)selectedToggle.getUserData(); boolean existing = false; diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css index 6acf4fab..7f34159e 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css @@ -3,12 +3,12 @@ } .list-menu { - -fx-pref-width: 130; + -fx-pref-width: 160; -fx-background-color: #3da0e3; } .list-item { - -fx-pref-width: 130; + -fx-pref-width: 160; -fx-padding: 0 20 0 20; -fx-background-color: #3da0e3; } @@ -36,6 +36,8 @@ .titled-pane > .title { -fx-background-color: white; -fx-padding: 0; + -fx-border-color: #e5e5e6; + /*-fx-border-width: 1;*/ } .titled-pane > .title > .arrow-button { diff --git a/src/main/resources/image/ledger.png b/src/main/resources/image/ledger.png index cf1af2f1..10131be7 100644 Binary files a/src/main/resources/image/ledger.png and b/src/main/resources/image/ledger.png differ diff --git a/src/main/resources/image/seed.png b/src/main/resources/image/seed.png new file mode 100644 index 00000000..ecd91127 Binary files /dev/null and b/src/main/resources/image/seed.png differ