add bip39 wallet import with discovery using common script types and derivations

This commit is contained in:
Craig Raw 2022-02-15 13:36:55 +02:00
parent 9ec57b1ef6
commit 91d491f5ec
13 changed files with 793 additions and 416 deletions

View file

@ -1266,7 +1266,7 @@ public class AppController implements Initializable {
public void sweepPrivateKey(ActionEvent event) { public void sweepPrivateKey(ActionEvent event) {
Wallet wallet = null; Wallet wallet = null;
WalletForm selectedWalletForm = getSelectedWalletForm(); WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) { if(selectedWalletForm != null && selectedWalletForm.getWallet().isValid()) {
wallet = selectedWalletForm.getWallet(); wallet = selectedWalletForm.getWallet();
} }

View file

@ -26,7 +26,6 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField; import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
@ -697,17 +696,17 @@ public class DevicePane extends TitledDescriptionPane {
} }
} }
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores); ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores);
walletDiscoveryService.setOnSucceeded(event -> { accountDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue()); importedKeystores.keySet().retainAll(accountDiscoveryService.getValue());
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores)); EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
}); });
walletDiscoveryService.setOnFailed(event -> { accountDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException()); log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage()); setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setDisable(false); discoverKeystoresButton.setDisable(false);
}); });
walletDiscoveryService.start(); accountDiscoveryService.start();
}); });
getXpubsService.setOnFailed(workerStateEvent -> { getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage()); setError("Could not retrieve xpub", getXpubsService.getException().getMessage());

View file

@ -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<String> 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<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> 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;
}
}

View file

@ -3,30 +3,22 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType; 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.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
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.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.layout.HBox;
import javafx.scene.layout.*; import javafx.scene.layout.Priority;
import javafx.util.Callback; import javafx.scene.layout.Region;
import org.controlsfx.control.textfield.AutoCompletionBinding; import javafx.scene.layout.StackPane;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders; import org.controlsfx.tools.Borders;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
@ -35,32 +27,22 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane { public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected final Wallet wallet; protected final Wallet wallet;
private final KeystoreMnemonicImport importer; private final KeystoreMnemonicImport importer;
private SplitMenuButton enterMnemonicButton;
private SplitMenuButton importButton; private SplitMenuButton importButton;
private TilePane wordsPane;
private Button generateButton; private Button generateButton;
private Label validLabel;
private Label invalidLabel;
private Button calculateButton; private Button calculateButton;
private Button backButton; private Button backButton;
private Button nextButton; private Button nextButton;
private Button confirmButton; private Button confirmButton;
private List<String> generatedMnemonicCode; private List<String> generatedMnemonicCode;
private SimpleListProperty<String> wordEntriesProperty;
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
private IntegerProperty defaultWordSizeProperty;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) { public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet; this.wallet = wallet;
@ -70,46 +52,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
buttonBox.getChildren().add(importButton); 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() { private void createImportButton() {
importButton = new SplitMenuButton(); importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setAlignment(Pos.CENTER_RIGHT);
@ -135,162 +77,81 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importButton.setVisible(false); importButton.setVisible(false);
} }
private void enterMnemonic(int numWords) { protected void enterMnemonic(int numWords) {
generatedMnemonicCode = null; generatedMnemonicCode = null;
setDescription("Enter mnemonic word list"); super.enterMnemonic(numWords);
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords, false));
setExpanded(true);
} }
private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) { protected List<Node> createLeftButtons() {
VBox vBox = new VBox(); generateButton = new Button("Generate New");
vBox.setSpacing(10); 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(); return List.of(generateButton);
wordsPane.setPrefRows(numWords/3); }
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>(); protected List<Node> createRightButtons() {
for(int i = 0; i < numWords; i++) { confirmButton = new Button("Re-enter Words...");
words.add(""); 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<String> wordEntryList = FXCollections.observableArrayList(words); calculateButton = new Button("Create Keystore");
wordEntriesProperty = new SimpleListProperty<>(wordEntryList); calculateButton.setDisable(true);
List<WordEntry> wordEntries = new ArrayList<>(numWords); calculateButton.setDefaultButton(true);
for(int i = 0; i < numWords; i++) { calculateButton.setOnAction(event -> {
wordEntries.add(new WordEntry(i, wordEntryList)); prepareImport();
} });
for(int i = 0; i < numWords - 1; i++) { calculateButton.managedProperty().bind(calculateButton.visibleProperty());
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
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) { nextButton = new Button("Confirm Backup...");
PassphraseEntry passphraseEntry = new PassphraseEntry(); nextButton.setOnAction(event -> {
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor()); confirmRecord();
passphraseEntry.setPadding(new Insets(0, 26, 10, 10)); });
vBox.getChildren().add(passphraseEntry); 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(); return List.of(backButton, nextButton, confirmButton, calculateButton);
buttonPane.setPadding(new Insets(0, 26, 0, 10)); }
generateButton = new Button("Generate New"); protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
generateButton.setOnAction(event -> { if(!empty && validWords) {
generateNew(); try {
}); importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
generateButton.managedProperty().bind(generateButton.visibleProperty()); validChecksum = true;
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet")); } catch(ImportException e) {
buttonPane.getChildren().add(generateButton); if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
AnchorPane.setLeftAnchor(generateButton, 0.0); 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."));
validLabel = new Label("Valid checksum", getValidGlyph()); } else {
validLabel.setContentDisplay(ContentDisplay.LEFT); invalidLabel.setText("Invalid checksum");
validLabel.setGraphicTextGap(5.0); invalidLabel.setTooltip(null);
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<String>) 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;
}
} }
}
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(); generateButton.setVisible(empty && generatedMnemonicCode == null);
stackPane.getChildren().add(vBox); calculateButton.setDisable(!validChecksum);
return stackPane; validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
} }
private void generateNew() { private void generateNew() {
@ -362,7 +223,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void confirmBackup() { private void confirmBackup() {
setDescription("Confirm backup by re-entering words"); setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false); showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false)); setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
setExpanded(true); setExpanded(true);
backButton.setVisible(true); backButton.setVisible(true);
generateButton.setVisible(false); generateButton.setVisible(false);
@ -415,145 +276,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
} }
} }
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> 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<String> 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<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
public WordlistSuggestionProvider(List<String> wordList) {
this.wordList = wordList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
List<String> 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<ChildNumber> derivation) { private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField(); TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path"); derivationField.setPromptText("Derivation path");
@ -591,30 +313,4 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return contentBox; return contentBox;
} }
private void showWordList(DeterministicSeed seed) {
List<String> 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;
}
} }

View file

@ -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<String> 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<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> 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<String>) 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<Node> createLeftButtons() {
return Collections.emptyList();
}
protected List<Node> 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<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> 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<String> 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<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
public WordlistSuggestionProvider(List<String> wordList) {
this.wordList = wordList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
List<String> 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;
}
}

View file

@ -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<Node> 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<Wallet> wallets = new ArrayList<>();
List<List<ChildNumber>> 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<ChildNumber> 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<Wallet> optWallet = walletDiscoveryService.getValue();
if(optWallet.isPresent()) {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> 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<ChildNumber> 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<ScriptType> 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;
}
}

View file

@ -33,7 +33,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
Accordion keystoreAccordion = new Accordion(); Accordion keystoreAccordion = new Accordion();
scrollPane.setContent(keystoreAccordion); scrollPane.setContent(keystoreAccordion);
MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore); MnemonicKeystoreDisplayPane keystorePane = new MnemonicKeystoreDisplayPane(decryptedKeystore);
keystorePane.setAnimated(false); keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane); keystoreAccordion.getPanes().add(keystorePane);

View file

@ -31,6 +31,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
}); });
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane(); StackPane stackPane = new StackPane();
@ -40,7 +41,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane); stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane(); ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(420); scrollPane.setPrefHeight(520);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane); anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
@ -61,6 +62,10 @@ public class WalletImportDialog extends Dialog<Wallet> {
} }
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle())); importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
MnemonicWalletKeystoreImportPane mnemonicImportPane = new MnemonicWalletKeystoreImportPane(new Bip39());
importAccordion.getPanes().add(0, mnemonicImportPane);
scrollPane.setContent(importAccordion); scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
@ -75,7 +80,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
}); });
dialogPane.setPrefWidth(500); dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(500); dialogPane.setPrefHeight(600);
AppServices.moveToActiveWindowScreen(this); AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null); setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);

View file

@ -1518,18 +1518,71 @@ public class ElectrumServer {
} }
} }
public static class WalletDiscoveryService extends Service<List<StandardAccount>> { public static class WalletDiscoveryService extends Service<Optional<Wallet>> {
private final List<Wallet> wallets;
public WalletDiscoveryService(List<Wallet> wallets) {
this.wallets = wallets;
}
@Override
protected Task<Optional<Wallet>> createTask() {
return new Task<>() {
protected Optional<Wallet> 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<WalletNode, Set<BlockTransactionHash>> 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<StandardAccount> searchAccounts = getStandardAccounts(wallet);
for(int j = 0; j < searchAccounts.size(); j++) {
StandardAccount standardAccount = searchAccounts.get(j);
Wallet childWallet = masterWalletCopy.addChildWallet(standardAccount);
Map<WalletNode, Set<BlockTransactionHash>> 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<StandardAccount> getStandardAccounts(Wallet wallet) {
List<StandardAccount> 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<List<StandardAccount>> {
private final Wallet masterWalletCopy; private final Wallet masterWalletCopy;
private final List<StandardAccount> standardAccounts; private final List<StandardAccount> standardAccounts;
private final Map<StandardAccount, Keystore> importedKeystores; private final Map<StandardAccount, Keystore> importedKeystores;
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) { public AccountDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
this.masterWalletCopy = masterWallet.copy(); this.masterWalletCopy = masterWallet.copy();
this.standardAccounts = standardAccounts; this.standardAccounts = standardAccounts;
this.importedKeystores = new HashMap<>(); this.importedKeystores = new HashMap<>();
} }
public WalletDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) { public AccountDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) {
this.masterWalletCopy = masterWallet.copy(); this.masterWalletCopy = masterWallet.copy();
this.standardAccounts = new ArrayList<>(importedKeystores.keySet()); this.standardAccounts = new ArrayList<>(importedKeystores.keySet());
this.importedKeystores = importedKeystores; this.importedKeystores = importedKeystores;

View file

@ -485,19 +485,19 @@ public class SettingsController extends WalletFormController implements Initiali
masterWallet.decrypt(key); masterWallet.decrypt(key);
if(discoverAccounts) { if(discoverAccounts) {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts);
walletDiscoveryService.setOnSucceeded(event -> { accountDiscoveryService.setOnSucceeded(event -> {
addAndEncryptAccounts(masterWallet, walletDiscoveryService.getValue(), key); addAndEncryptAccounts(masterWallet, accountDiscoveryService.getValue(), key);
if(walletDiscoveryService.getValue().isEmpty()) { 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); 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()); log.error("Failed to discover accounts", event.getSource().getException());
addAndEncryptAccounts(masterWallet, Collections.emptyList(), key); addAndEncryptAccounts(masterWallet, Collections.emptyList(), key);
AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage());
}); });
walletDiscoveryService.start(); accountDiscoveryService.start();
} else { } else {
addAndEncryptAccounts(masterWallet, standardAccounts, key); addAndEncryptAccounts(masterWallet, standardAccounts, key);
} }
@ -518,18 +518,18 @@ public class SettingsController extends WalletFormController implements Initiali
} }
} else { } else {
if(discoverAccounts) { if(discoverAccounts) {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts);
walletDiscoveryService.setOnSucceeded(event -> { accountDiscoveryService.setOnSucceeded(event -> {
addAndSaveAccounts(masterWallet, walletDiscoveryService.getValue()); addAndSaveAccounts(masterWallet, accountDiscoveryService.getValue());
if(walletDiscoveryService.getValue().isEmpty()) { 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); 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()); log.error("Failed to discover accounts", event.getSource().getException());
AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage());
}); });
walletDiscoveryService.start(); accountDiscoveryService.start();
} else { } else {
addAndSaveAccounts(masterWallet, standardAccounts); addAndSaveAccounts(masterWallet, standardAccounts);
} }

View file

@ -107,10 +107,6 @@
-fx-fill: #e06c75; -fx-fill: #e06c75;
} }
.root .invalid-checksum {
-fx-text-fill: #e06c75;
}
.root #noWalletsWarning .glyph-font { .root #noWalletsWarning .glyph-font {
-fx-text-fill: #e06c75; -fx-text-fill: #e06c75;
} }

View file

@ -284,14 +284,19 @@ CellView > .text-input.text-field {
-fx-text-fill: -fx-text-inner-color; -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; -fx-fill: null;
} }
.progress-indicator.progress-timer { .progress-indicator.progress-timer, .progress-indicator.button-progress {
-fx-padding: 0 0 -16 0; -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 { .progress-indicator.progress-timer > .determinate-indicator > .tick {
visibility: hidden; visibility: hidden;
} }

View file

@ -33,13 +33,6 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.valid-checksum {
-fx-text-fill: #50a14f;
}
.invalid-checksum {
-fx-text-fill: rgb(202, 18, 67);
}