diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 45a8e83a..8551272a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1266,7 +1266,7 @@ public class AppController implements Initializable { public void sweepPrivateKey(ActionEvent event) { Wallet wallet = null; WalletForm selectedWalletForm = getSelectedWalletForm(); - if(selectedWalletForm != null) { + if(selectedWalletForm != null && selectedWalletForm.getWallet().isValid()) { wallet = selectedWalletForm.getWallet(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index 786fed6f..affcae3c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -26,7 +26,6 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.*; import org.controlsfx.control.textfield.CustomPasswordField; -import org.controlsfx.control.textfield.TextFields; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationSupport; @@ -697,17 +696,17 @@ public class DevicePane extends TitledDescriptionPane { } } - ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores); - walletDiscoveryService.setOnSucceeded(event -> { - importedKeystores.keySet().retainAll(walletDiscoveryService.getValue()); + ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores); + accountDiscoveryService.setOnSucceeded(event -> { + importedKeystores.keySet().retainAll(accountDiscoveryService.getValue()); EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores)); }); - walletDiscoveryService.setOnFailed(event -> { + accountDiscoveryService.setOnFailed(event -> { log.error("Failed to discover accounts", event.getSource().getException()); setError("Failed to discover accounts", event.getSource().getException().getMessage()); discoverKeystoresButton.setDisable(false); }); - walletDiscoveryService.start(); + accountDiscoveryService.start(); }); getXpubsService.setOnFailed(workerStateEvent -> { setError("Could not retrieve xpub", getXpubsService.getException().getMessage()); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java new file mode 100644 index 00000000..ff9ccdc2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java @@ -0,0 +1,72 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.WalletModel; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; + +import java.util.ArrayList; +import java.util.List; + +public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { + public MnemonicKeystoreDisplayPane(Keystore keystore) { + super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png"); + showHideLink.setVisible(false); + buttonBox.getChildren().clear(); + + showWordList(keystore.getSeed()); + } + + private void showWordList(DeterministicSeed seed) { + List words = seed.getMnemonicCode(); + setContent(getMnemonicWordsEntry(words.size())); + setExpanded(true); + + for(int i = 0; i < wordsPane.getChildren().size(); i++) { + WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i); + wordEntry.getEditor().setText(words.get(i)); + wordEntry.getEditor().setEditable(false); + } + } + + protected Node getMnemonicWordsEntry(int numWords) { + VBox vBox = new VBox(); + vBox.setSpacing(10); + + wordsPane = new TilePane(); + wordsPane.setPrefRows(numWords / 3); + wordsPane.setHgap(10); + wordsPane.setVgap(10); + wordsPane.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); + List wordEntries = new ArrayList<>(numWords); + for(int i = 0; i < numWords; i++) { + wordEntries.add(new WordEntry(i, wordEntryList)); + } + for(int i = 0; i < numWords - 1; i++) { + wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); + wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor()); + } + wordsPane.getChildren().addAll(wordEntries); + + vBox.getChildren().add(wordsPane); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().add(vBox); + return stackPane; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java index ff13c7ad..19809f9d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java @@ -3,30 +3,22 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; -import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.io.*; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleListProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport; import javafx.geometry.Insets; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.input.Clipboard; -import javafx.scene.layout.*; -import javafx.util.Callback; -import org.controlsfx.control.textfield.AutoCompletionBinding; -import org.controlsfx.control.textfield.CustomTextField; -import org.controlsfx.control.textfield.TextFields; -import org.controlsfx.glyphfont.Glyph; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import org.controlsfx.tools.Borders; import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationSupport; @@ -35,32 +27,22 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Optional; -public class MnemonicKeystoreImportPane extends TitledDescriptionPane { +public class MnemonicKeystoreImportPane extends MnemonicKeystorePane { protected final Wallet wallet; private final KeystoreMnemonicImport importer; - private SplitMenuButton enterMnemonicButton; private SplitMenuButton importButton; - private TilePane wordsPane; private Button generateButton; - private Label validLabel; - private Label invalidLabel; private Button calculateButton; private Button backButton; private Button nextButton; private Button confirmButton; private List generatedMnemonicCode; - private SimpleListProperty wordEntriesProperty; - private final SimpleStringProperty passphraseProperty = new SimpleStringProperty(); - private IntegerProperty defaultWordSizeProperty; - public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) { super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); this.wallet = wallet; @@ -70,46 +52,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { buttonBox.getChildren().add(importButton); } - public MnemonicKeystoreImportPane(Keystore keystore) { - super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png"); - this.wallet = null; - this.importer = null; - showHideLink.setVisible(false); - buttonBox.getChildren().clear(); - - showWordList(keystore.getSeed()); - } - - @Override - protected Control createButton() { - createEnterMnemonicButton(); - return enterMnemonicButton; - } - - private void createEnterMnemonicButton() { - enterMnemonicButton = new SplitMenuButton(); - enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT); - enterMnemonicButton.setText("Enter 24 Words"); - defaultWordSizeProperty = new SimpleIntegerProperty(24); - defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> { - enterMnemonicButton.setText("Enter " + newValue + " Words"); - }); - enterMnemonicButton.setOnAction(event -> { - enterMnemonic(defaultWordSizeProperty.get()); - }); - int[] numberWords = new int[] {24, 21, 18, 15, 12}; - for(int i = 0; i < numberWords.length; i++) { - MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words"); - final int words = numberWords[i]; - item.setOnAction(event -> { - defaultWordSizeProperty.set(words); - enterMnemonic(words); - }); - enterMnemonicButton.getItems().add(item); - } - enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty()); - } - private void createImportButton() { importButton = new SplitMenuButton(); importButton.setAlignment(Pos.CENTER_RIGHT); @@ -135,162 +77,81 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { importButton.setVisible(false); } - private void enterMnemonic(int numWords) { + protected void enterMnemonic(int numWords) { generatedMnemonicCode = null; - setDescription("Enter mnemonic word list"); - showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(numWords, false)); - setExpanded(true); + super.enterMnemonic(numWords); } - private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) { - VBox vBox = new VBox(); - vBox.setSpacing(10); + protected List createLeftButtons() { + generateButton = new Button("Generate New"); + generateButton.setOnAction(event -> { + generateNew(); + }); + generateButton.managedProperty().bind(generateButton.visibleProperty()); + generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet")); - wordsPane = new TilePane(); - wordsPane.setPrefRows(numWords/3); - wordsPane.setHgap(10); - wordsPane.setVgap(10); - wordsPane.setOrientation(Orientation.VERTICAL); + return List.of(generateButton); + } - List words = new ArrayList<>(); - for(int i = 0; i < numWords; i++) { - words.add(""); - } + protected List createRightButtons() { + confirmButton = new Button("Re-enter Words..."); + confirmButton.setOnAction(event -> { + confirmBackup(); + }); + confirmButton.managedProperty().bind(confirmButton.visibleProperty()); + confirmButton.setVisible(false); + confirmButton.setDefaultButton(true); + confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct")); - ObservableList wordEntryList = FXCollections.observableArrayList(words); - wordEntriesProperty = new SimpleListProperty<>(wordEntryList); - List wordEntries = new ArrayList<>(numWords); - for(int i = 0; i < numWords; i++) { - wordEntries.add(new WordEntry(i, wordEntryList)); - } - for(int i = 0; i < numWords - 1; i++) { - wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); - wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor()); - } - wordsPane.getChildren().addAll(wordEntries); + calculateButton = new Button("Create Keystore"); + calculateButton.setDisable(true); + calculateButton.setDefaultButton(true); + calculateButton.setOnAction(event -> { + prepareImport(); + }); + calculateButton.managedProperty().bind(calculateButton.visibleProperty()); + calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list")); - vBox.getChildren().add(wordsPane); + backButton = new Button("Back"); + backButton.setOnAction(event -> { + displayMnemonicCode(); + }); + backButton.managedProperty().bind(backButton.visibleProperty()); + backButton.setTooltip(new Tooltip("Go back to the generated word list")); + backButton.setVisible(false); - if(!displayWordsOnly) { - PassphraseEntry passphraseEntry = new PassphraseEntry(); - wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor()); - passphraseEntry.setPadding(new Insets(0, 26, 10, 10)); - vBox.getChildren().add(passphraseEntry); + nextButton = new Button("Confirm Backup..."); + nextButton.setOnAction(event -> { + confirmRecord(); + }); + nextButton.managedProperty().bind(nextButton.visibleProperty()); + nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list")); + nextButton.setVisible(false); + nextButton.setDefaultButton(true); - AnchorPane buttonPane = new AnchorPane(); - buttonPane.setPadding(new Insets(0, 26, 0, 10)); + return List.of(backButton, nextButton, confirmButton, calculateButton); + } - generateButton = new Button("Generate New"); - generateButton.setOnAction(event -> { - generateNew(); - }); - generateButton.managedProperty().bind(generateButton.visibleProperty()); - generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet")); - buttonPane.getChildren().add(generateButton); - AnchorPane.setLeftAnchor(generateButton, 0.0); - - validLabel = new Label("Valid checksum", getValidGlyph()); - validLabel.setContentDisplay(ContentDisplay.LEFT); - validLabel.setGraphicTextGap(5.0); - validLabel.managedProperty().bind(validLabel.visibleProperty()); - validLabel.setVisible(false); - buttonPane.getChildren().add(validLabel); - AnchorPane.setTopAnchor(validLabel, 5.0); - AnchorPane.setLeftAnchor(validLabel, 0.0); - - invalidLabel = new Label("Invalid checksum", getInvalidGlyph()); - invalidLabel.setContentDisplay(ContentDisplay.LEFT); - invalidLabel.setGraphicTextGap(5.0); - invalidLabel.managedProperty().bind(invalidLabel.visibleProperty()); - invalidLabel.setVisible(false); - buttonPane.getChildren().add(invalidLabel); - AnchorPane.setTopAnchor(invalidLabel, 5.0); - AnchorPane.setLeftAnchor(invalidLabel, 0.0); - - confirmButton = new Button("Re-enter Words..."); - confirmButton.setOnAction(event -> { - confirmBackup(); - }); - confirmButton.managedProperty().bind(confirmButton.visibleProperty()); - confirmButton.setVisible(false); - confirmButton.setDefaultButton(true); - confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct")); - - calculateButton = new Button("Create Keystore"); - calculateButton.setDisable(true); - calculateButton.setDefaultButton(true); - calculateButton.setOnAction(event -> { - prepareImport(); - }); - calculateButton.managedProperty().bind(calculateButton.visibleProperty()); - calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list")); - - backButton = new Button("Back"); - backButton.setOnAction(event -> { - displayMnemonicCode(); - }); - backButton.managedProperty().bind(backButton.visibleProperty()); - backButton.setTooltip(new Tooltip("Go back to the generated word list")); - backButton.setVisible(false); - - nextButton = new Button("Confirm Backup..."); - nextButton.setOnAction(event -> { - confirmRecord(); - }); - nextButton.managedProperty().bind(nextButton.visibleProperty()); - nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list")); - nextButton.setVisible(false); - nextButton.setDefaultButton(true); - - wordEntriesProperty.addListener((ListChangeListener) c -> { - boolean empty = true; - boolean validWords = true; - boolean validChecksum = false; - for(String word : wordEntryList) { - if(!word.isEmpty()) { - empty = false; - } - - if(!WordEntry.isValid(word)) { - validWords = false; - } + protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) { + if(!empty && validWords) { + try { + importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get()); + validChecksum = true; + } catch(ImportException e) { + if(e.getCause() instanceof MnemonicException.MnemonicTypeException) { + invalidLabel.setText("Unsupported Electrum seed"); + invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly.")); + } else { + invalidLabel.setText("Invalid checksum"); + invalidLabel.setTooltip(null); } - - if(!empty && validWords) { - try { - importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get()); - validChecksum = true; - } catch(ImportException e) { - if(e.getCause() instanceof MnemonicException.MnemonicTypeException) { - invalidLabel.setText("Unsupported Electrum seed"); - invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly.")); - } else { - invalidLabel.setText("Invalid checksum"); - invalidLabel.setTooltip(null); - } - } - } - - generateButton.setVisible(empty && generatedMnemonicCode == null); - calculateButton.setDisable(!validChecksum); - validLabel.setVisible(validChecksum); - invalidLabel.setVisible(!validChecksum && !empty); - }); - - HBox rightBox = new HBox(); - rightBox.setSpacing(10); - rightBox.getChildren().addAll(backButton, nextButton, confirmButton, calculateButton); - - buttonPane.getChildren().add(rightBox); - AnchorPane.setRightAnchor(rightBox, 0.0); - - vBox.getChildren().add(buttonPane); + } } - StackPane stackPane = new StackPane(); - stackPane.getChildren().add(vBox); - return stackPane; + generateButton.setVisible(empty && generatedMnemonicCode == null); + calculateButton.setDisable(!validChecksum); + validLabel.setVisible(validChecksum); + invalidLabel.setVisible(!validChecksum && !empty); } private void generateNew() { @@ -362,7 +223,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { private void confirmBackup() { setDescription("Confirm backup by re-entering words"); showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false)); + setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size())); setExpanded(true); backButton.setVisible(true); generateButton.setVisible(false); @@ -415,145 +276,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { } } - private static class WordEntry extends HBox { - private static List wordList; - private final TextField wordField; - private WordEntry nextEntry; - private TextField nextField; - - public WordEntry(int wordNumber, ObservableList wordEntryList) { - super(); - setAlignment(Pos.CENTER_RIGHT); - - setSpacing(10); - Label label = new Label((wordNumber+1) + "."); - label.setPrefWidth(22); - label.setAlignment(Pos.CENTER_RIGHT); - wordField = new TextField() { - @Override - public void paste() { - Clipboard clipboard = Clipboard.getSystemClipboard(); - if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) { - String[] words = clipboard.getString().split("[\\n\\s]"); - WordEntry entry = WordEntry.this; - for(String word : words) { - if(entry.nextField != null) { - entry.nextField.requestFocus(); - } - - entry.wordField.setText(word); - entry = entry.nextEntry; - if(entry == null) { - break; - } - } - } else { - super.paste(); - } - } - }; - wordField.setMaxWidth(100); - TextFormatter formatter = new TextFormatter<>((TextFormatter.Change change) -> { - String text = change.getText(); - // if text was added, fix the text to fit the requirements - if(!text.isEmpty()) { - String newText = text.replace(" ", "").toLowerCase(); - int carretPos = change.getCaretPosition() - text.length() + newText.length(); - change.setText(newText); - // fix caret position based on difference in originally added text and fixed text - change.selectRange(carretPos, carretPos); - } - return change; - }); - wordField.setTextFormatter(formatter); - - wordList = Bip39MnemonicCode.INSTANCE.getWordList(); - AutoCompletionBinding autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList)); - autoCompletionBinding.setDelay(50); - autoCompletionBinding.setOnAutoCompleted(event -> { - if(nextField != null) { - nextField.requestFocus(); - } - }); - - ValidationSupport validationSupport = new ValidationSupport(); - validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); - validationSupport.registerValidator(wordField, Validator.combine( - Validator.createEmptyValidator("Word is required"), - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue)) - )); - - wordField.textProperty().addListener((observable, oldValue, newValue) -> { - wordEntryList.set(wordNumber, newValue); - }); - - this.getChildren().addAll(label, wordField); - } - - public TextField getEditor() { - return wordField; - } - - public void setNextEntry(WordEntry nextEntry) { - this.nextEntry = nextEntry; - } - - public void setNextField(TextField field) { - this.nextField = field; - } - - 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 { - private final CustomTextField passphraseField; - - public PassphraseEntry() { - super(); - - setAlignment(Pos.CENTER_LEFT); - setSpacing(10); - Label passphraseLabel = new Label("Passphrase:"); - passphraseField = (CustomTextField) TextFields.createClearableTextField(); - passphraseProperty.bind(passphraseField.textProperty()); - passphraseField.setPromptText("Leave blank for none"); - - HelpLabel helpLabel = new HelpLabel(); - helpLabel.setStyle("-fx-padding: 0 0 0 0"); - helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!"); - - getChildren().addAll(passphraseLabel, passphraseField, helpLabel); - } - - public TextField getEditor() { - return passphraseField; - } - } - private Node getDerivationEntry(List derivation) { TextField derivationField = new TextField(); derivationField.setPromptText("Derivation path"); @@ -591,30 +313,4 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { return contentBox; } - - private void showWordList(DeterministicSeed seed) { - List words = seed.getMnemonicCode(); - setContent(getMnemonicWordsEntry(words.size(), true)); - setExpanded(true); - - for (int i = 0; i < wordsPane.getChildren().size(); i++) { - WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i); - wordEntry.getEditor().setText(words.get(i)); - wordEntry.getEditor().setEditable(false); - } - } - - public static Glyph getValidGlyph() { - Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE); - validGlyph.getStyleClass().add("valid-checksum"); - validGlyph.setFontSize(12); - return validGlyph; - } - - public static Glyph getInvalidGlyph() { - Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); - invalidGlyph.getStyleClass().add("invalid-checksum"); - invalidGlyph.setFontSize(12); - return invalidGlyph; - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java new file mode 100644 index 00000000..85f557cc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java @@ -0,0 +1,344 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import javafx.application.Platform; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleStringProperty; +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.input.Clipboard; +import javafx.scene.layout.*; +import javafx.util.Callback; +import org.controlsfx.control.textfield.AutoCompletionBinding; +import org.controlsfx.control.textfield.CustomTextField; +import org.controlsfx.control.textfield.TextFields; +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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class MnemonicKeystorePane extends TitledDescriptionPane { + protected SplitMenuButton enterMnemonicButton; + protected TilePane wordsPane; + protected Label validLabel; + protected Label invalidLabel; + + protected SimpleListProperty wordEntriesProperty; + protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty(); + protected IntegerProperty defaultWordSizeProperty; + + public MnemonicKeystorePane(String title, String description, String content, String imageUrl) { + super(title, description, content, imageUrl); + } + + @Override + protected Control createButton() { + createEnterMnemonicButton(); + return enterMnemonicButton; + } + + private void createEnterMnemonicButton() { + enterMnemonicButton = new SplitMenuButton(); + enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT); + enterMnemonicButton.setText("Enter 24 Words"); + defaultWordSizeProperty = new SimpleIntegerProperty(24); + defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> { + enterMnemonicButton.setText("Enter " + newValue + " Words"); + }); + enterMnemonicButton.setOnAction(event -> { + enterMnemonic(defaultWordSizeProperty.get()); + }); + int[] numberWords = new int[] {24, 21, 18, 15, 12}; + for(int i = 0; i < numberWords.length; i++) { + MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words"); + final int words = numberWords[i]; + item.setOnAction(event -> { + defaultWordSizeProperty.set(words); + enterMnemonic(words); + }); + enterMnemonicButton.getItems().add(item); + } + enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty()); + } + + protected void enterMnemonic(int numWords) { + setDescription("Enter mnemonic word list"); + showHideLink.setVisible(false); + setContent(getMnemonicWordsEntry(numWords)); + setExpanded(true); + } + + protected Node getMnemonicWordsEntry(int numWords) { + VBox vBox = new VBox(); + vBox.setSpacing(10); + + wordsPane = new TilePane(); + wordsPane.setPrefRows(numWords/3); + wordsPane.setHgap(10); + wordsPane.setVgap(10); + wordsPane.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); + List wordEntries = new ArrayList<>(numWords); + for(int i = 0; i < numWords; i++) { + wordEntries.add(new WordEntry(i, wordEntryList)); + } + for(int i = 0; i < numWords - 1; i++) { + wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); + wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor()); + } + wordsPane.getChildren().addAll(wordEntries); + + vBox.getChildren().add(wordsPane); + + PassphraseEntry passphraseEntry = new PassphraseEntry(); + wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor()); + passphraseEntry.setPadding(new Insets(0, 26, 10, 10)); + vBox.getChildren().add(passphraseEntry); + + AnchorPane buttonPane = new AnchorPane(); + buttonPane.setPadding(new Insets(0, 26, 0, 10)); + + HBox leftBox = new HBox(10); + leftBox.getChildren().addAll(createLeftButtons()); + + buttonPane.getChildren().add(leftBox); + AnchorPane.setLeftAnchor(leftBox, 0.0); + + validLabel = new Label("Valid checksum", getValidGlyph()); + validLabel.setContentDisplay(ContentDisplay.LEFT); + validLabel.setGraphicTextGap(5.0); + validLabel.managedProperty().bind(validLabel.visibleProperty()); + validLabel.setVisible(false); + buttonPane.getChildren().add(validLabel); + AnchorPane.setTopAnchor(validLabel, 5.0); + AnchorPane.setLeftAnchor(validLabel, 0.0); + + invalidLabel = new Label("Invalid checksum", getInvalidGlyph()); + invalidLabel.setContentDisplay(ContentDisplay.LEFT); + invalidLabel.setGraphicTextGap(5.0); + invalidLabel.managedProperty().bind(invalidLabel.visibleProperty()); + invalidLabel.setVisible(false); + buttonPane.getChildren().add(invalidLabel); + AnchorPane.setTopAnchor(invalidLabel, 5.0); + AnchorPane.setLeftAnchor(invalidLabel, 0.0); + + wordEntriesProperty.addListener((ListChangeListener) c -> { + boolean empty = true; + boolean validWords = true; + boolean validChecksum = false; + for(String word : wordEntryList) { + if(!word.isEmpty()) { + empty = false; + } + + if(!WordEntry.isValid(word)) { + validWords = false; + } + } + + onWordChange(empty, validWords, validChecksum); + }); + + HBox rightBox = new HBox(); + rightBox.setSpacing(10); + rightBox.getChildren().addAll(createRightButtons()); + + buttonPane.getChildren().add(rightBox); + AnchorPane.setRightAnchor(rightBox, 0.0); + + vBox.getChildren().add(buttonPane); + + Platform.runLater(() -> wordEntries.get(0).getEditor().requestFocus()); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().add(vBox); + return stackPane; + } + + protected List createLeftButtons() { + return Collections.emptyList(); + } + + protected List createRightButtons() { + return Collections.emptyList(); + } + + protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) { + //nothing by default + } + + protected static class WordEntry extends HBox { + private static List wordList; + private final TextField wordField; + private WordEntry nextEntry; + private TextField nextField; + + public WordEntry(int wordNumber, ObservableList wordEntryList) { + super(); + setAlignment(Pos.CENTER_RIGHT); + + setSpacing(10); + Label label = new Label((wordNumber+1) + "."); + label.setPrefWidth(22); + label.setAlignment(Pos.CENTER_RIGHT); + wordField = new TextField() { + @Override + public void paste() { + Clipboard clipboard = Clipboard.getSystemClipboard(); + if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) { + String[] words = clipboard.getString().split("[\\n\\s]"); + WordEntry entry = WordEntry.this; + for(String word : words) { + if(entry.nextField != null) { + entry.nextField.requestFocus(); + } + + entry.wordField.setText(word); + entry = entry.nextEntry; + if(entry == null) { + break; + } + } + } else { + super.paste(); + } + } + }; + wordField.setMaxWidth(100); + TextFormatter formatter = new TextFormatter<>((TextFormatter.Change change) -> { + String text = change.getText(); + // if text was added, fix the text to fit the requirements + if(!text.isEmpty()) { + String newText = text.replace(" ", "").toLowerCase(); + int carretPos = change.getCaretPosition() - text.length() + newText.length(); + change.setText(newText); + // fix caret position based on difference in originally added text and fixed text + change.selectRange(carretPos, carretPos); + } + return change; + }); + wordField.setTextFormatter(formatter); + + wordList = Bip39MnemonicCode.INSTANCE.getWordList(); + AutoCompletionBinding autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList)); + autoCompletionBinding.setDelay(50); + autoCompletionBinding.setOnAutoCompleted(event -> { + if(nextField != null) { + nextField.requestFocus(); + } + }); + + ValidationSupport validationSupport = new ValidationSupport(); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + validationSupport.registerValidator(wordField, Validator.combine( + Validator.createEmptyValidator("Word is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue)) + )); + + wordField.textProperty().addListener((observable, oldValue, newValue) -> { + wordEntryList.set(wordNumber, newValue); + }); + + this.getChildren().addAll(label, wordField); + } + + public TextField getEditor() { + return wordField; + } + + public void setNextEntry(WordEntry nextEntry) { + this.nextEntry = nextEntry; + } + + public void setNextField(TextField field) { + this.nextField = field; + } + + public static boolean isValid(String word) { + return wordList.contains(word); + } + } + + protected 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; + } + } + + protected class PassphraseEntry extends HBox { + private final CustomTextField passphraseField; + + public PassphraseEntry() { + super(); + + setAlignment(Pos.CENTER_LEFT); + setSpacing(10); + Label passphraseLabel = new Label("Passphrase:"); + passphraseField = (CustomTextField) TextFields.createClearableTextField(); + passphraseProperty.bind(passphraseField.textProperty()); + passphraseField.setPromptText("Leave blank for none"); + + HelpLabel helpLabel = new HelpLabel(); + helpLabel.setStyle("-fx-padding: 0 0 0 0"); + helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!"); + + getChildren().addAll(passphraseLabel, passphraseField, helpLabel); + } + + public TextField getEditor() { + return passphraseField; + } + } + + public static Glyph getValidGlyph() { + Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE); + validGlyph.getStyleClass().add("success"); + validGlyph.setFontSize(12); + return validGlyph; + } + + public static Glyph getInvalidGlyph() { + Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); + invalidGlyph.getStyleClass().add("failure"); + invalidGlyph.setFontSize(12); + return invalidGlyph; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java new file mode 100644 index 00000000..392bf21b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java @@ -0,0 +1,214 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.policy.Policy; +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.WalletImportEvent; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport; +import com.sparrowwallet.sparrow.net.ElectrumServer; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.util.StringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { + private static final Logger log = LoggerFactory.getLogger(MnemonicWalletKeystoreImportPane.class); + + private final KeystoreMnemonicImport importer; + + private Button discoverButton; + private Button importButton; + + public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) { + super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); + this.importer = importer; + } + + @Override + protected List createRightButtons() { + discoverButton = new Button("Discover Wallet"); + discoverButton.setDisable(true); + discoverButton.setDefaultButton(true); + discoverButton.managedProperty().bind(discoverButton.visibleProperty()); + discoverButton.setOnAction(event -> { + discoverWallet(); + }); + discoverButton.managedProperty().bind(discoverButton.visibleProperty()); + discoverButton.setTooltip(new Tooltip("Look for existing transactions from the provided word list")); + discoverButton.visibleProperty().bind(AppServices.onlineProperty()); + + importButton = new Button("Import Wallet"); + importButton.setDisable(true); + importButton.managedProperty().bind(importButton.visibleProperty()); + importButton.visibleProperty().bind(discoverButton.visibleProperty().not()); + importButton.setOnAction(event -> { + setContent(getScriptTypeEntry()); + setExpanded(true); + }); + + return List.of(discoverButton, importButton); + } + + @Override + protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) { + if(!empty && validWords) { + try { + importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get()); + validChecksum = true; + } catch(ImportException e) { + if(e.getCause() instanceof MnemonicException.MnemonicTypeException) { + invalidLabel.setText("Unsupported Electrum seed"); + invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly.")); + } else { + invalidLabel.setText("Invalid checksum"); + invalidLabel.setTooltip(null); + } + } + } + + discoverButton.setDisable(!validChecksum || !AppServices.isConnected()); + importButton.setDisable(!validChecksum); + validLabel.setVisible(validChecksum); + invalidLabel.setVisible(!validChecksum && !empty); + } + + private void discoverWallet() { + discoverButton.setDisable(true); + discoverButton.setMaxHeight(discoverButton.getHeight()); + ProgressIndicator progressIndicator = new ProgressIndicator(0); + progressIndicator.getStyleClass().add("button-progress"); + discoverButton.setGraphic(progressIndicator); + List wallets = new ArrayList<>(); + + List> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList()); + derivations.add(List.of(new ChildNumber(0, true))); + + for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) { + for(List derivation : derivations) { + try { + Wallet wallet = getWallet(scriptType, derivation); + wallets.add(wallet); + } catch(ImportException e) { + String errorMessage = e.getMessage(); + if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) { + errorMessage = "Invalid word list - checksum incorrect"; + } else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + setError("Import Error", errorMessage + "."); + discoverButton.setDisable(!AppServices.isConnected()); + } + } + } + + ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets); + progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty()); + walletDiscoveryService.setOnSucceeded(successEvent -> { + discoverButton.setGraphic(null); + Optional optWallet = walletDiscoveryService.getValue(); + if(optWallet.isPresent()) { + EventManager.get().post(new WalletImportEvent(optWallet.get())); + } else { + discoverButton.setDisable(false); + Optional optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES); + if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) { + setContent(getScriptTypeEntry()); + setExpanded(true); + } + } + }); + walletDiscoveryService.setOnFailed(failedEvent -> { + discoverButton.setGraphic(null); + log.error("Failed to discover wallets", failedEvent.getSource().getException()); + setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage()); + }); + walletDiscoveryService.start(); + } + + private Wallet getWallet(ScriptType scriptType, List derivation) throws ImportException { + Wallet wallet = new Wallet(""); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(scriptType); + Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get()); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1)); + return wallet; + } + + private Node getScriptTypeEntry() { + Label label = new Label("Script Type:"); + + HBox fieldBox = new HBox(5); + fieldBox.setAlignment(Pos.CENTER_RIGHT); + ComboBox scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))); + if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) { + scriptTypeComboBox.setValue(ScriptType.P2WPKH); + } + scriptTypeComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(ScriptType scriptType) { + return scriptType == null ? "" : scriptType.getDescription(); + } + + @Override + public ScriptType fromString(String string) { + return null; + } + }); + scriptTypeComboBox.setMaxWidth(170); + + HelpLabel helpLabel = new HelpLabel(); + helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing."); + fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel); + + Region region = new Region(); + HBox.setHgrow(region, Priority.SOMETIMES); + + Button importMnemonicButton = new Button("Import"); + importMnemonicButton.setOnAction(event -> { + showHideLink.setVisible(true); + setExpanded(false); + try { + ScriptType scriptType = scriptTypeComboBox.getValue(); + Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation()); + EventManager.get().post(new WalletImportEvent(wallet)); + } catch(ImportException e) { + log.error("Error importing mnemonic", 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); + } + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.CENTER_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().addAll(label, fieldBox, region, importMnemonicButton); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(60); + + return contentBox; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java index f7ca1711..e3995835 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java @@ -33,7 +33,7 @@ public class SeedDisplayDialog extends Dialog { Accordion keystoreAccordion = new Accordion(); scrollPane.setContent(keystoreAccordion); - MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore); + MnemonicKeystoreDisplayPane keystorePane = new MnemonicKeystoreDisplayPane(decryptedKeystore); keystorePane.setAnimated(false); keystoreAccordion.getPanes().add(keystorePane); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index b28c85c0..e5c6c01d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -31,6 +31,7 @@ public class WalletImportDialog extends Dialog { }); final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); AppServices.setStageIcon(dialogPane.getScene().getWindow()); StackPane stackPane = new StackPane(); @@ -40,7 +41,7 @@ public class WalletImportDialog extends Dialog { stackPane.getChildren().add(anchorPane); ScrollPane scrollPane = new ScrollPane(); - scrollPane.setPrefHeight(420); + scrollPane.setPrefHeight(520); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); anchorPane.getChildren().add(scrollPane); scrollPane.setFitToWidth(true); @@ -61,6 +62,10 @@ public class WalletImportDialog extends Dialog { } importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle())); + + MnemonicWalletKeystoreImportPane mnemonicImportPane = new MnemonicWalletKeystoreImportPane(new Bip39()); + importAccordion.getPanes().add(0, mnemonicImportPane); + scrollPane.setContent(importAccordion); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); @@ -75,7 +80,7 @@ public class WalletImportDialog extends Dialog { }); dialogPane.setPrefWidth(500); - dialogPane.setPrefHeight(500); + dialogPane.setPrefHeight(600); AppServices.moveToActiveWindowScreen(this); setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 9bdd8dd4..3645c2ee 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1518,18 +1518,71 @@ public class ElectrumServer { } } - public static class WalletDiscoveryService extends Service> { + public static class WalletDiscoveryService extends Service> { + private final List wallets; + + public WalletDiscoveryService(List wallets) { + this.wallets = wallets; + } + + @Override + protected Task> createTask() { + return new Task<>() { + protected Optional call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + + for(int i = 0; i < wallets.size(); i++) { + Wallet wallet = wallets.get(i); + updateProgress(i, wallets.size() + StandardAccount.values().length); + Map> nodeTransactionMap = new TreeMap<>(); + electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0); + if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) { + Wallet masterWalletCopy = wallet.copy(); + List searchAccounts = getStandardAccounts(wallet); + for(int j = 0; j < searchAccounts.size(); j++) { + StandardAccount standardAccount = searchAccounts.get(j); + Wallet childWallet = masterWalletCopy.addChildWallet(standardAccount); + Map> childTransactionMap = new TreeMap<>(); + electrumServer.getReferences(childWallet, childWallet.getNode(KeyPurpose.RECEIVE).getChildren(), childTransactionMap, 0); + if(childTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) { + wallet.addChildWallet(standardAccount); + } + updateProgress(i + j, wallets.size() + StandardAccount.values().length); + } + + return Optional.of(wallet); + } + } + + return Optional.empty(); + } + }; + } + + private List getStandardAccounts(Wallet wallet) { + List accounts = new ArrayList<>(); + for(StandardAccount account : StandardAccount.values()) { + if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) { + accounts.add(account); + } + } + + return accounts; + } + } + + public static class AccountDiscoveryService extends Service> { private final Wallet masterWalletCopy; private final List standardAccounts; private final Map importedKeystores; - public WalletDiscoveryService(Wallet masterWallet, List standardAccounts) { + public AccountDiscoveryService(Wallet masterWallet, List standardAccounts) { this.masterWalletCopy = masterWallet.copy(); this.standardAccounts = standardAccounts; this.importedKeystores = new HashMap<>(); } - public WalletDiscoveryService(Wallet masterWallet, Map importedKeystores) { + public AccountDiscoveryService(Wallet masterWallet, Map importedKeystores) { this.masterWalletCopy = masterWallet.copy(); this.standardAccounts = new ArrayList<>(importedKeystores.keySet()); this.importedKeystores = importedKeystores; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index c0bb2b9d..56e5657b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -485,19 +485,19 @@ public class SettingsController extends WalletFormController implements Initiali masterWallet.decrypt(key); if(discoverAccounts) { - ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); - walletDiscoveryService.setOnSucceeded(event -> { - addAndEncryptAccounts(masterWallet, walletDiscoveryService.getValue(), key); - if(walletDiscoveryService.getValue().isEmpty()) { + ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts); + accountDiscoveryService.setOnSucceeded(event -> { + addAndEncryptAccounts(masterWallet, accountDiscoveryService.getValue(), key); + if(accountDiscoveryService.getValue().isEmpty()) { AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. Note only the first 10 accounts are scanned.", Alert.AlertType.INFORMATION, ButtonType.OK); } }); - walletDiscoveryService.setOnFailed(event -> { + accountDiscoveryService.setOnFailed(event -> { log.error("Failed to discover accounts", event.getSource().getException()); addAndEncryptAccounts(masterWallet, Collections.emptyList(), key); AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); }); - walletDiscoveryService.start(); + accountDiscoveryService.start(); } else { addAndEncryptAccounts(masterWallet, standardAccounts, key); } @@ -518,18 +518,18 @@ public class SettingsController extends WalletFormController implements Initiali } } else { if(discoverAccounts) { - ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); - walletDiscoveryService.setOnSucceeded(event -> { - addAndSaveAccounts(masterWallet, walletDiscoveryService.getValue()); - if(walletDiscoveryService.getValue().isEmpty()) { + ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts); + accountDiscoveryService.setOnSucceeded(event -> { + addAndSaveAccounts(masterWallet, accountDiscoveryService.getValue()); + if(accountDiscoveryService.getValue().isEmpty()) { AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. Note only the first 10 accounts are scanned.", Alert.AlertType.INFORMATION, ButtonType.OK); } }); - walletDiscoveryService.setOnFailed(event -> { + accountDiscoveryService.setOnFailed(event -> { log.error("Failed to discover accounts", event.getSource().getException()); AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); }); - walletDiscoveryService.start(); + accountDiscoveryService.start(); } else { addAndSaveAccounts(masterWallet, standardAccounts); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css index e06b3884..20c221a5 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css +++ b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css @@ -107,10 +107,6 @@ -fx-fill: #e06c75; } -.root .invalid-checksum { - -fx-text-fill: #e06c75; -} - .root #noWalletsWarning .glyph-font { -fx-text-fill: #e06c75; } diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index 1f8d0789..309a2eb2 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -284,14 +284,19 @@ CellView > .text-input.text-field { -fx-text-fill: -fx-text-inner-color; } -.progress-indicator.progress-timer .percentage { +.progress-indicator.progress-timer .percentage, .progress-indicator.button-progress .percentage { -fx-fill: null; } -.progress-indicator.progress-timer { +.progress-indicator.progress-timer, .progress-indicator.button-progress { -fx-padding: 0 0 -16 0; } +.progress-indicator.button-progress { + -fx-scale-x: 0.6; + -fx-scale-y: 0.6; +} + .progress-indicator.progress-timer > .determinate-indicator > .tick { visibility: hidden; } diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css index 3cec5987..874cede1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css @@ -33,13 +33,6 @@ -fx-background-color: transparent; } -.valid-checksum { - -fx-text-fill: #50a14f; -} - -.invalid-checksum { - -fx-text-fill: rgb(202, 18, 67); -}