mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
generate border wallets grid from seed words
This commit is contained in:
parent
faa5a11c94
commit
6063b02113
6 changed files with 259 additions and 15 deletions
|
@ -27,6 +27,7 @@ import java.io.FileInputStream;
|
|||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -34,6 +35,8 @@ import java.util.stream.Collectors;
|
|||
public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
|
||||
private final int GRID_COLUMN_COUNT = 16;
|
||||
|
||||
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
|
@ -43,11 +46,11 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
|||
setTitle("Border Wallets Grid");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, and select 11 or 23 words in the grid.\nThe order of selection is important!");
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid. Note the order of selection is important!");
|
||||
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
String[][] emptyWordGrid = new String[128][16];
|
||||
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
||||
Grid grid = getGrid(emptyWordGrid);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
|
@ -87,9 +90,15 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
|||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF...", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP);
|
||||
dialogPane.getButtonTypes().add(generateButtonType);
|
||||
|
||||
final ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear Selection", ButtonBar.ButtonData.OTHER);
|
||||
dialogPane.getButtonTypes().add(clearButtonType);
|
||||
|
||||
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty)));
|
||||
|
||||
|
@ -166,6 +175,24 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
|||
return words;
|
||||
}
|
||||
|
||||
private String[][] toGrid(List<String> words) {
|
||||
String[][] grid = new String[words.size()/GRID_COLUMN_COUNT][GRID_COLUMN_COUNT];
|
||||
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
for(String word : words) {
|
||||
String abbr = word.length() < 4 ? word : word.substring(0, 4);
|
||||
grid[row][col] = abbr;
|
||||
col++;
|
||||
if(col >= GRID_COLUMN_COUNT) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private class MnemonicGridDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
|
@ -198,6 +225,36 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
|||
});
|
||||
|
||||
button = loadButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP) {
|
||||
Button generateButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(generateButton, buttonData);
|
||||
generateButton.setOnAction(event -> {
|
||||
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(12);
|
||||
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> mnemonicWords = optWords.get();
|
||||
List<String> shuffledWordList = shuffle(mnemonicWords);
|
||||
String[][] wordGrid = toGrid(shuffledWordList);
|
||||
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||
initializedProperty.set(true);
|
||||
|
||||
if(seedEntryDialog.isGenerated()) {
|
||||
//TODO: Save grid PDF
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = generateButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OTHER) {
|
||||
Button clearButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(clearButton, buttonData);
|
||||
clearButton.setOnAction(event -> {
|
||||
spreadsheetView.getSelectionModel().clearSelection();
|
||||
});
|
||||
|
||||
button = clearButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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;
|
||||
|
@ -25,7 +24,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) {
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||
private final BooleanProperty validProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private boolean generated;
|
||||
|
||||
public MnemonicKeystoreEntryPane(int numWords) {
|
||||
super(DeterministicSeed.Type.BIP39.getName(), "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
defaultWordSizeProperty.set(numWords);
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords, false, true));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Node> createRightButtons() {
|
||||
Button button = new Button("Next");
|
||||
button.setVisible(false);
|
||||
return List.of(button);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Bip39MnemonicCode.INSTANCE.check(wordEntriesProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
validProperty.set(validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
public void generateNew() {
|
||||
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
|
||||
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
|
||||
|
||||
SecureRandom secureRandom;
|
||||
try {
|
||||
secureRandom = SecureRandom.getInstanceStrong();
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
|
||||
displayMnemonicCode(deterministicSeed);
|
||||
generated = true;
|
||||
}
|
||||
|
||||
private void displayMnemonicCode(DeterministicSeed deterministicSeed) {
|
||||
setDescription("Write down these words");
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(deterministicSeed.getMnemonicCode().get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return validProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty validProperty() {
|
||||
return validProperty;
|
||||
}
|
||||
|
||||
public boolean isGenerated() {
|
||||
return generated;
|
||||
}
|
||||
}
|
|
@ -223,7 +223,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
|||
private void confirmBackup() {
|
||||
setDescription("Confirm backup by re-entering words");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), true, false));
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
|
|
|
@ -81,7 +81,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
|||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem gridItem = new MenuItem("Border Wallets...");
|
||||
MenuItem gridItem = new MenuItem("Border Wallets Grid...");
|
||||
gridItem.setOnAction(event -> {
|
||||
showGrid();
|
||||
});
|
||||
|
@ -100,7 +100,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
|||
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> words = optWords.get();
|
||||
setContent(getMnemonicWordsEntry(words.size() + 1, true));
|
||||
setContent(getMnemonicWordsEntry(words.size() + 1, true, true));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
|
@ -150,7 +150,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
|||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size(), true));
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
|
@ -163,11 +163,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
|||
protected void enterMnemonic(int numWords) {
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords, true));
|
||||
setContent(getMnemonicWordsEntry(numWords, true, true));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) {
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
|
@ -196,10 +196,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
|||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
if(showPassphrase) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase);
|
||||
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));
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SeedEntryDialog extends Dialog<List<String>> {
|
||||
private final MnemonicKeystoreEntryPane keystorePane;
|
||||
|
||||
public SeedEntryDialog(int numWords) {
|
||||
final DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
int lines = numWords / 3;
|
||||
int height = lines * 40;
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
AnchorPane anchorPane = new AnchorPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.getStyleClass().add("edge-to-edge");
|
||||
scrollPane.setPrefHeight(104 + height);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
keystorePane = new MnemonicKeystoreEntryPane(numWords);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.disableProperty().bind(keystorePane.validProperty().not());
|
||||
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate New", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(generateButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? keystorePane.wordEntriesProperty.get() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(180 + height);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||
}
|
||||
|
||||
private class MnemonicGridDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button generateButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(generateButton, buttonData);
|
||||
generateButton.setOnAction(event -> {
|
||||
keystorePane.generateNew();
|
||||
});
|
||||
|
||||
button = generateButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isGenerated() {
|
||||
return keystorePane.isGenerated();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue