13 changed files with 794 additions and 417 deletions
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue