mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
import seed via border wallets grid pattern
This commit is contained in:
parent
fd2b383dbc
commit
af532e7fc9
8 changed files with 280 additions and 2 deletions
|
@ -160,6 +160,7 @@ run {
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.io=com.google.gson",
|
"--add-opens=java.base/java.io=com.google.gson",
|
||||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"]
|
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"]
|
||||||
|
@ -208,6 +209,7 @@ jlink {
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.io=com.google.gson",
|
"--add-opens=java.base/java.io=com.google.gson",
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import org.controlsfx.control.spreadsheet.*;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||||
|
private final SpreadsheetView spreadsheetView;
|
||||||
|
|
||||||
|
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
|
||||||
|
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
public MnemonicGridDialog() {
|
||||||
|
DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||||
|
setDialogPane(dialogPane);
|
||||||
|
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!");
|
||||||
|
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
|
String[][] emptyWordGrid = new String[128][16];
|
||||||
|
Grid grid = getGrid(emptyWordGrid);
|
||||||
|
|
||||||
|
spreadsheetView = new SpreadsheetView(grid);
|
||||||
|
spreadsheetView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||||
|
try {
|
||||||
|
Field f = event.getClass().getDeclaredField(Platform.getCurrent() == Platform.OSX ? "metaDown" : "controlDown");
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(event, true);
|
||||||
|
} catch(IllegalAccessException | NoSuchFieldException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spreadsheetView.setId("grid");
|
||||||
|
spreadsheetView.setEditable(false);
|
||||||
|
spreadsheetView.setFixingColumnsAllowed(false);
|
||||||
|
spreadsheetView.setFixingRowsAllowed(false);
|
||||||
|
|
||||||
|
spreadsheetView.getSelectionModel().getSelectedCells().addListener(new ListChangeListener<>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(Change<? extends TablePosition> c) {
|
||||||
|
int numWords = c.getList().size();
|
||||||
|
wordsSelectedProperty.set(numWords == 11 || numWords == 23);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().add(spreadsheetView);
|
||||||
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
|
stackPane.widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue != null) {
|
||||||
|
for(SpreadsheetColumn column : spreadsheetView.getColumns()) {
|
||||||
|
column.setPrefWidth((newValue.doubleValue() - spreadsheetView.getRowHeaderWidth() - 3) / 17);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF", ButtonBar.ButtonData.LEFT);
|
||||||
|
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||||
|
|
||||||
|
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
|
||||||
|
okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty)));
|
||||||
|
|
||||||
|
setResultConverter((dialogButton) -> {
|
||||||
|
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||||
|
return data == ButtonBar.ButtonData.OK_DONE ? getSelectedWords() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogPane.setPrefWidth(850);
|
||||||
|
dialogPane.setPrefHeight(500);
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid getGrid(String[][] wordGrid) {
|
||||||
|
int rowCount = wordGrid.length;
|
||||||
|
int columnCount = wordGrid[0].length;
|
||||||
|
GridBase grid = new GridBase(rowCount, columnCount);
|
||||||
|
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||||
|
grid.getColumnHeaders().setAll("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P");
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||||
|
for(int j = 0; j < columnCount; j++) {
|
||||||
|
list.add(createCell(i, j, wordGrid[i][j]));
|
||||||
|
}
|
||||||
|
rows.add(list);
|
||||||
|
grid.getRowHeaders().add(String.format("%03d", i + 1));
|
||||||
|
}
|
||||||
|
grid.setRows(rows);
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpreadsheetCell createCell(int row, int column, String word) {
|
||||||
|
return SpreadsheetCellType.STRING.createCell(row, column, 1, 1, word == null ? "" : word);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getSelectedWords() {
|
||||||
|
List<String> abbreviations = spreadsheetView.getSelectionModel().getSelectedCells().stream()
|
||||||
|
.map(position -> (String)spreadsheetView.getGrid().getRows().get(position.getRow()).get(position.getColumn()).getItem()).collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> words = new ArrayList<>();
|
||||||
|
for(String abbreviation : abbreviations) {
|
||||||
|
for(String word : Bip39MnemonicCode.INSTANCE.getWordList()) {
|
||||||
|
if((abbreviation.length() == 3 && word.equals(abbreviation)) || (abbreviation.length() >= 4 && word.startsWith(abbreviation))) {
|
||||||
|
words.add(word);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(words.size() != abbreviations.size()) {
|
||||||
|
abbreviations.removeIf(abbr -> words.stream().anyMatch(w -> w.startsWith(abbr)));
|
||||||
|
throw new IllegalStateException("Could not find words for abbreviations: " + abbreviations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MnemonicGridDialogPane extends DialogPane {
|
||||||
|
@Override
|
||||||
|
protected Node createButton(ButtonType buttonType) {
|
||||||
|
Node button;
|
||||||
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
|
Button loadButton = new Button(buttonType.getText());
|
||||||
|
loadButton.setGraphicTextGap(5);
|
||||||
|
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||||
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
|
ButtonBar.setButtonData(loadButton, buttonData);
|
||||||
|
loadButton.setOnAction(event -> {
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Open PDF");
|
||||||
|
fileChooser.getExtensionFilters().addAll(
|
||||||
|
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||||
|
new FileChooser.ExtensionFilter("PDF", "*.pdf")
|
||||||
|
);
|
||||||
|
|
||||||
|
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
|
||||||
|
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||||
|
if(file != null) {
|
||||||
|
try(BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||||
|
String[][] wordGrid = PdfUtils.getWordGrid(inputStream);
|
||||||
|
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||||
|
initializedProperty.set(true);
|
||||||
|
} catch(Exception e) {
|
||||||
|
AppServices.showErrorDialog("Cannot load PDF", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button = loadButton;
|
||||||
|
} else {
|
||||||
|
button = super.createButton(buttonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
|
glyph.setFontSize(11);
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@ import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.ScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
import javafx.geometry.Orientation;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -20,6 +22,7 @@ import javafx.scene.control.*;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
import org.controlsfx.control.textfield.TextFields;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
@ -78,6 +81,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
enterMnemonicButton.getItems().add(item);
|
enterMnemonicButton.getItems().add(item);
|
||||||
}
|
}
|
||||||
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
||||||
|
MenuItem gridItem = new MenuItem("Border Wallets...");
|
||||||
|
gridItem.setOnAction(event -> {
|
||||||
|
showGrid();
|
||||||
|
});
|
||||||
|
enterMnemonicButton.getItems().add(gridItem);
|
||||||
|
|
||||||
MenuItem scanItem = new MenuItem("Scan QR...");
|
MenuItem scanItem = new MenuItem("Scan QR...");
|
||||||
scanItem.setOnAction(event -> {
|
scanItem.setOnAction(event -> {
|
||||||
scanQR();
|
scanQR();
|
||||||
|
@ -86,6 +95,42 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void showGrid() {
|
||||||
|
MnemonicGridDialog mnemonicGridDialog = new MnemonicGridDialog();
|
||||||
|
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
|
||||||
|
if(optWords.isPresent()) {
|
||||||
|
List<String> words = optWords.get();
|
||||||
|
setContent(getMnemonicWordsEntry(words.size() + 1, true));
|
||||||
|
setExpanded(true);
|
||||||
|
|
||||||
|
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||||
|
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||||
|
if(i < words.size()) {
|
||||||
|
wordEntry.getEditor().setText(words.get(i));
|
||||||
|
wordEntry.getEditor().setEditable(false);
|
||||||
|
} else {
|
||||||
|
ScheduledService<Void> service = new ScheduledService<>() {
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
service.setDelay(Duration.millis(500));
|
||||||
|
service.setOnSucceeded(event1 -> {
|
||||||
|
service.cancel();
|
||||||
|
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||||
|
});
|
||||||
|
service.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void scanQR() {
|
protected void scanQR() {
|
||||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.lowagie.text.pdf.PdfReader;
|
||||||
import com.lowagie.text.pdf.PdfWriter;
|
import com.lowagie.text.pdf.PdfWriter;
|
||||||
import com.lowagie.text.pdf.parser.PdfTextExtractor;
|
import com.lowagie.text.pdf.parser.PdfTextExtractor;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.UREncoder;
|
import com.sparrowwallet.hummingbird.UREncoder;
|
||||||
|
@ -25,8 +26,8 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.Locale;
|
import java.util.*;
|
||||||
import java.util.Scanner;
|
import java.util.List;
|
||||||
|
|
||||||
public class PdfUtils {
|
public class PdfUtils {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PdfUtils.class);
|
private static final Logger log = LoggerFactory.getLogger(PdfUtils.class);
|
||||||
|
@ -108,4 +109,33 @@ public class PdfUtils {
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||||
return new javafx.scene.image.Image(bais);
|
return new javafx.scene.image.Image(bais);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[][] getWordGrid(InputStream inputStream) {
|
||||||
|
try {
|
||||||
|
PdfReader pdfReader = new PdfReader(inputStream);
|
||||||
|
String allText = "";
|
||||||
|
for(int page = 1; page <= pdfReader.getNumberOfPages(); page++) {
|
||||||
|
PdfTextExtractor textExtractor = new PdfTextExtractor(pdfReader);
|
||||||
|
allText += textExtractor.getTextFromPage(page) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String[]> rows = new ArrayList<>();
|
||||||
|
Scanner scanner = new Scanner(allText);
|
||||||
|
while(scanner.hasNextLine()) {
|
||||||
|
String line = scanner.nextLine().trim();
|
||||||
|
String[] words = line.split(" ");
|
||||||
|
if(words.length > 16 && Utils.isNumber(words[0])) {
|
||||||
|
rows.add(Arrays.copyOfRange(words, 1, 17));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(rows.size() < 128) {
|
||||||
|
throw new IllegalArgumentException("Not a valid Border Wallets PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.toArray(new String[][]{new String[0]});
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new IllegalArgumentException("Not a valid Border Wallets PDF");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
src/main/resources/com/sparrowwallet/sparrow/grid.css
Normal file
5
src/main/resources/com/sparrowwallet/sparrow/grid.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#grid .spreadsheet-cell:selected,
|
||||||
|
#grid .spreadsheet-cell:focused:selected,
|
||||||
|
#grid .spreadsheet-cell:focused:selected:hover {
|
||||||
|
-fx-background-color: rgb(238, 210, 2);
|
||||||
|
}
|
BIN
src/main/resources/image/border-wallets.png
Normal file
BIN
src/main/resources/image/border-wallets.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
src/main/resources/image/border-wallets@2x.png
Normal file
BIN
src/main/resources/image/border-wallets@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/main/resources/image/border-wallets@3x.png
Normal file
BIN
src/main/resources/image/border-wallets@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
Loading…
Reference in a new issue