mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add bip39 wallet import with discovery using common script types and derivations
This commit is contained in:
parent
9ec57b1ef6
commit
91d491f5ec
13 changed files with 793 additions and 416 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue