mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
usb keystore importing
This commit is contained in:
parent
431a170ab0
commit
9adfcf5806
33 changed files with 1196 additions and 19 deletions
|
@ -16,7 +16,7 @@ repositories {
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = "14"
|
version = "14"
|
||||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ]
|
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
@ -32,6 +32,7 @@ dependencies {
|
||||||
implementation('com.google.code.gson:gson:2.8.6')
|
implementation('com.google.code.gson:gson:2.8.6')
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
|
implementation('org.apache.commons:commons-compress:1.20')
|
||||||
implementation('org.controlsfx:controlsfx:11.0.1' ) {
|
implementation('org.controlsfx:controlsfx:11.0.1' ) {
|
||||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 294649de669497283934933487d09e1dae9f3996
|
Subproject commit ed056bc49fb919799f70a6d7ee2dd65a38c25b5d
|
|
@ -1,6 +1,11 @@
|
||||||
package com.sparrowwallet.sparrow;
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
|
@ -14,6 +19,7 @@ public class MainApp extends Application {
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws Exception {
|
public void start(Stage stage) throws Exception {
|
||||||
GlyphFontRegistry.register(new FontAwesome5());
|
GlyphFontRegistry.register(new FontAwesome5());
|
||||||
|
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||||
|
|
||||||
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
|
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
|
||||||
Parent root = transactionLoader.load();
|
Parent root = transactionLoader.load();
|
||||||
|
@ -30,7 +36,14 @@ public class MainApp extends Application {
|
||||||
|
|
||||||
appController.initializeView();
|
appController.initializeView();
|
||||||
|
|
||||||
stage.show();
|
Wallet wallet = new Wallet();
|
||||||
|
wallet.setPolicyType(PolicyType.SINGLE);
|
||||||
|
wallet.setScriptType(ScriptType.P2PKH);
|
||||||
|
|
||||||
|
KeystoreImportDialog dlg = new KeystoreImportDialog(wallet);
|
||||||
|
dlg.showAndWait();
|
||||||
|
|
||||||
|
//stage.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.external.Device;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.Accordion;
|
||||||
|
|
||||||
|
public class DeviceAccordion extends Accordion {
|
||||||
|
private ObservableList<Device> devices;
|
||||||
|
private DeviceOperation deviceOperation = DeviceOperation.IMPORT;
|
||||||
|
|
||||||
|
public void setDevices(Wallet wallet, ObservableList<Device> devices) {
|
||||||
|
this.devices = devices;
|
||||||
|
|
||||||
|
for(Device device : devices) {
|
||||||
|
DevicePane devicePane = new DevicePane(this, wallet, device);
|
||||||
|
this.getPanes().add(devicePane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceOperation getDeviceOperation() {
|
||||||
|
return deviceOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceOperation(DeviceOperation deviceOperation) {
|
||||||
|
this.deviceOperation = deviceOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DeviceOperation {
|
||||||
|
IMPORT;
|
||||||
|
}
|
||||||
|
}
|
388
src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
Normal file
388
src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.ExtendedPublicKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.external.Device;
|
||||||
|
import com.sparrowwallet.sparrow.external.Hwi;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
import org.controlsfx.control.textfield.TextFields;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DevicePane extends TitledPane {
|
||||||
|
private final DeviceAccordion deviceAccordion;
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final Device device;
|
||||||
|
|
||||||
|
private Label mainLabel;
|
||||||
|
private Label statusLabel;
|
||||||
|
private CustomPasswordField pinField;
|
||||||
|
private CustomTextField passphraseField;
|
||||||
|
private Button unlockButton;
|
||||||
|
private Button enterPinButton;
|
||||||
|
private Button setPassphraseButton;
|
||||||
|
private SplitMenuButton importButton;
|
||||||
|
|
||||||
|
private final SimpleStringProperty status = new SimpleStringProperty("");
|
||||||
|
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
public DevicePane(DeviceAccordion deviceAccordion, Wallet wallet, Device device) {
|
||||||
|
super();
|
||||||
|
this.deviceAccordion = deviceAccordion;
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.device = device;
|
||||||
|
|
||||||
|
setPadding(new Insets(0, 0, 0, 0));
|
||||||
|
|
||||||
|
setGraphic(getTitle());
|
||||||
|
getStyleClass().add("devicepane");
|
||||||
|
setDefaultStatus();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Node arrow = this.lookup(".arrow");
|
||||||
|
if(arrow != null) {
|
||||||
|
arrow.setVisible(false);
|
||||||
|
arrow.setManaged(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDefaultStatus() {
|
||||||
|
setStatus(device.getNeedsPinSent() ? "Locked" : device.getNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getTitle() {
|
||||||
|
HBox listItem = new HBox();
|
||||||
|
listItem.setPadding(new Insets(10, 20, 10, 10));
|
||||||
|
listItem.setSpacing(10);
|
||||||
|
|
||||||
|
HBox imageBox = new HBox();
|
||||||
|
imageBox.setMinWidth(50);
|
||||||
|
imageBox.setMinHeight(50);
|
||||||
|
listItem.getChildren().add(imageBox);
|
||||||
|
|
||||||
|
Image image = new Image("image/" + device.getType() + ".png", 50, 50, true, true);
|
||||||
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setImage(image);
|
||||||
|
imageBox.getChildren().add(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
VBox labelsBox = new VBox();
|
||||||
|
labelsBox.setSpacing(5);
|
||||||
|
labelsBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
this.mainLabel = new Label();
|
||||||
|
mainLabel.setText(device.getModel().toDisplayString());
|
||||||
|
mainLabel.getStyleClass().add("devicelist-main-label");
|
||||||
|
labelsBox.getChildren().add(mainLabel);
|
||||||
|
|
||||||
|
this.statusLabel = new Label();
|
||||||
|
statusLabel.textProperty().bind(status);
|
||||||
|
|
||||||
|
labelsBox.getChildren().add(statusLabel);
|
||||||
|
statusLabel.getStyleClass().add("devicelist-status-label");
|
||||||
|
listItem.getChildren().add(labelsBox);
|
||||||
|
HBox.setHgrow(labelsBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
HBox buttonBox = new HBox();
|
||||||
|
buttonBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
createUnlockButton();
|
||||||
|
createSetPassphraseButton();
|
||||||
|
createImportButton();
|
||||||
|
|
||||||
|
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||||
|
unlockButton.setVisible(true);
|
||||||
|
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||||
|
setPassphraseButton.setVisible(true);
|
||||||
|
} else {
|
||||||
|
showOperationButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonBox.getChildren().addAll(unlockButton, setPassphraseButton, importButton);
|
||||||
|
listItem.getChildren().add(buttonBox);
|
||||||
|
|
||||||
|
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
listItem.setPrefWidth(newValue.getWidth());
|
||||||
|
});
|
||||||
|
|
||||||
|
return listItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createUnlockButton() {
|
||||||
|
unlockButton = new Button("Unlock");
|
||||||
|
unlockButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
unlockButton.setOnAction(event -> {
|
||||||
|
unlockButton.setDisable(true);
|
||||||
|
unlock(device);
|
||||||
|
});
|
||||||
|
unlockButton.managedProperty().bind(unlockButton.visibleProperty());
|
||||||
|
unlockButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSetPassphraseButton() {
|
||||||
|
setPassphraseButton = new Button("Set Passphrase");
|
||||||
|
setPassphraseButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
setPassphraseButton.setOnAction(event -> {
|
||||||
|
setPassphraseButton.setDisable(true);
|
||||||
|
setContent(getPassphraseEntry());
|
||||||
|
setExpanded(true);
|
||||||
|
});
|
||||||
|
setPassphraseButton.managedProperty().bind(setPassphraseButton.visibleProperty());
|
||||||
|
setPassphraseButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createImportButton() {
|
||||||
|
importButton = new SplitMenuButton();
|
||||||
|
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
importButton.setText("Import Keystore");
|
||||||
|
importButton.setOnAction(event -> {
|
||||||
|
importButton.setDisable(true);
|
||||||
|
importKeystore(wallet.getScriptType().getDefaultDerivation());
|
||||||
|
});
|
||||||
|
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||||
|
for(int i = 0; i < accounts.length; i++) {
|
||||||
|
MenuItem item = new MenuItem(accounts[i]);
|
||||||
|
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||||
|
item.setOnAction(event -> {
|
||||||
|
importButton.setDisable(true);
|
||||||
|
importKeystore(derivation);
|
||||||
|
});
|
||||||
|
importButton.getItems().add(item);
|
||||||
|
}
|
||||||
|
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||||
|
importButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unlock(Device device) {
|
||||||
|
if(device.getModel().equals(WalletModel.TREZOR_1)) {
|
||||||
|
promptPin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getPinEntry() {
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
vBox.setMaxHeight(120);
|
||||||
|
vBox.setSpacing(42);
|
||||||
|
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||||
|
enterPinButton = new Button("Enter PIN");
|
||||||
|
enterPinButton.setOnAction(event -> {
|
||||||
|
enterPinButton.setDisable(true);
|
||||||
|
sendPin(pinField.getText());
|
||||||
|
});
|
||||||
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
|
TilePane tilePane = new TilePane();
|
||||||
|
tilePane.setPrefColumns(3);
|
||||||
|
tilePane.setHgap(10);
|
||||||
|
tilePane.setVgap(10);
|
||||||
|
tilePane.setMaxWidth(150);
|
||||||
|
tilePane.setMaxHeight(120);
|
||||||
|
|
||||||
|
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||||
|
for(int i = 0; i < digits.length; i++) {
|
||||||
|
Button pinButton = new Button();
|
||||||
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||||
|
pinButton.setGraphic(circle);
|
||||||
|
pinButton.setUserData(digits[i]);
|
||||||
|
tilePane.getChildren().add(pinButton);
|
||||||
|
pinButton.setOnAction(event -> {
|
||||||
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
HBox contentBox = new HBox();
|
||||||
|
contentBox.setSpacing(50);
|
||||||
|
contentBox.getChildren().add(tilePane);
|
||||||
|
contentBox.getChildren().add(vBox);
|
||||||
|
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||||
|
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getPassphraseEntry() {
|
||||||
|
passphraseField = (CustomTextField)TextFields.createClearableTextField();
|
||||||
|
passphrase.bind(passphraseField.textProperty());
|
||||||
|
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button sendPassphraseButton = new Button("Send Passphrase");
|
||||||
|
sendPassphraseButton.setOnAction(event -> {
|
||||||
|
setExpanded(false);
|
||||||
|
sendPassphrase(passphrase.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox();
|
||||||
|
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||||
|
contentBox.setSpacing(20);
|
||||||
|
contentBox.getChildren().add(passphraseField);
|
||||||
|
contentBox.getChildren().add(sendPassphraseButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void promptPin() {
|
||||||
|
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
|
||||||
|
promptPinService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
Boolean result = promptPinService.getValue();
|
||||||
|
if(result) {
|
||||||
|
setContent(getPinEntry());
|
||||||
|
setExpanded(true);
|
||||||
|
} else {
|
||||||
|
setErrorStatus("Could not request PIN");
|
||||||
|
unlockButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
promptPinService.setOnFailed(workerStateEvent -> {
|
||||||
|
setErrorStatus(promptPinService.getException().getMessage());
|
||||||
|
unlockButton.setDisable(false);
|
||||||
|
});
|
||||||
|
promptPinService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPin(String pin) {
|
||||||
|
Hwi.SendPinService sendPinService = new Hwi.SendPinService(device, pin);
|
||||||
|
sendPinService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
Boolean result = sendPinService.getValue();
|
||||||
|
if(result) {
|
||||||
|
device.setNeedsPinSent(false);
|
||||||
|
setDefaultStatus();
|
||||||
|
setExpanded(false);
|
||||||
|
unlockButton.setVisible(false);
|
||||||
|
|
||||||
|
if(device.getNeedsPassphraseSent()) {
|
||||||
|
setPassphraseButton.setVisible(true);
|
||||||
|
setPassphraseButton.setDisable(true);
|
||||||
|
setContent(getPassphraseEntry());
|
||||||
|
setExpanded(true);
|
||||||
|
} else {
|
||||||
|
showOperationButton();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrorStatus("Incorrect PIN");
|
||||||
|
enterPinButton.setDisable(false);
|
||||||
|
if(pinField != null) {
|
||||||
|
pinField.setText("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sendPinService.setOnFailed(workerStateEvent -> {
|
||||||
|
setErrorStatus(sendPinService.getException().getMessage());
|
||||||
|
enterPinButton.setDisable(false);
|
||||||
|
});
|
||||||
|
sendPinService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPassphrase(String passphrase) {
|
||||||
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase);
|
||||||
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
List<Device> devices = enumerateService.getValue();
|
||||||
|
for (Device freshDevice : devices) {
|
||||||
|
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
|
||||||
|
device.setFingerprint(freshDevice.getFingerprint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(device.getFingerprint() != null) {
|
||||||
|
setPassphraseButton.setVisible(false);
|
||||||
|
device.setNeedsPassphraseSent(false);
|
||||||
|
setDefaultStatus();
|
||||||
|
showOperationButton();
|
||||||
|
} else {
|
||||||
|
setErrorStatus("Passphrase send failed");
|
||||||
|
setPassphraseButton.setDisable(false);
|
||||||
|
setPassphraseButton.setVisible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
enumerateService.setOnFailed(workerStateEvent -> {
|
||||||
|
setErrorStatus(enumerateService.getException().getMessage());
|
||||||
|
setPassphraseButton.setDisable(false);
|
||||||
|
setPassphraseButton.setVisible(true);
|
||||||
|
});
|
||||||
|
enumerateService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importKeystore(List<ChildNumber> derivation) {
|
||||||
|
if(device.getFingerprint() == null) {
|
||||||
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||||
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
List<Device> devices = enumerateService.getValue();
|
||||||
|
for (Device freshDevice : devices) {
|
||||||
|
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
|
||||||
|
device.setFingerprint(freshDevice.getFingerprint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importXpub(derivation);
|
||||||
|
});
|
||||||
|
enumerateService.setOnFailed(workerStateEvent -> {
|
||||||
|
setErrorStatus(enumerateService.getException().getMessage());
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
enumerateService.start();
|
||||||
|
} else {
|
||||||
|
importXpub(derivation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importXpub(List<ChildNumber> derivation) {
|
||||||
|
String derivationPath = KeyDerivation.writePath(derivation);
|
||||||
|
|
||||||
|
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
|
||||||
|
getXpubService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
String xpub = getXpubService.getValue();
|
||||||
|
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setLabel(device.getModel().toDisplayString() + " " + device.getFingerprint().toUpperCase());
|
||||||
|
keystore.setSource(KeystoreSource.HW_USB);
|
||||||
|
keystore.setWalletModel(device.getModel());
|
||||||
|
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||||
|
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(xpub));
|
||||||
|
|
||||||
|
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||||
|
});
|
||||||
|
getXpubService.setOnFailed(workerStateEvent -> {
|
||||||
|
setErrorStatus(getXpubService.getException().getMessage());
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
getXpubService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStatus(String statusMessage) {
|
||||||
|
statusLabel.getStyleClass().remove("status-error");
|
||||||
|
status.setValue(statusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setErrorStatus(String statusMessage) {
|
||||||
|
statusLabel.getStyleClass().add("status-error");
|
||||||
|
status.setValue(statusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showOperationButton() {
|
||||||
|
if(deviceAccordion.getDeviceOperation().equals(DeviceAccordion.DeviceOperation.IMPORT)) {
|
||||||
|
importButton.setVisible(true);
|
||||||
|
} else {
|
||||||
|
//TODO: Support further device operations such as signing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import javafx.beans.NamedArg;
|
|
||||||
import javafx.scene.control.ChoiceBox;
|
|
||||||
|
|
||||||
public class EnumChoiceBox<E extends Enum<E>> extends ChoiceBox<E> {
|
|
||||||
|
|
||||||
public EnumChoiceBox(@NamedArg("enumType") String enumType) throws Exception {
|
|
||||||
Class<E> enumClass = (Class<E>) Class.forName(enumType);
|
|
||||||
getItems().setAll(enumClass.getEnumConstants());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
|
||||||
|
public class KeystoreImportEvent {
|
||||||
|
private Keystore keystore;
|
||||||
|
|
||||||
|
public KeystoreImportEvent(Keystore keystore) {
|
||||||
|
this.keystore = keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Keystore getKeystore() {
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ public class ColdcardSinglesig implements SinglesigWalletImport {
|
||||||
String key = keyValue[0].trim();
|
String key = keyValue[0].trim();
|
||||||
String value = keyValue[1].trim();
|
String value = keyValue[1].trim();
|
||||||
|
|
||||||
if(!key.equals("m") && scriptType.getDefaultDerivation().startsWith(key)) {
|
if(!key.equals("m") && scriptType.getDefaultDerivationPath().startsWith(key)) {
|
||||||
ExtendedPublicKey extPubKey = ExtendedPublicKey.fromDescriptor(value);
|
ExtendedPublicKey extPubKey = ExtendedPublicKey.fromDescriptor(value);
|
||||||
Keystore keystore = new Keystore();
|
Keystore keystore = new Keystore();
|
||||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, key));
|
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, key));
|
||||||
|
|
64
src/main/java/com/sparrowwallet/sparrow/external/Device.java
vendored
Normal file
64
src/main/java/com/sparrowwallet/sparrow/external/Device.java
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package com.sparrowwallet.sparrow.external;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
|
||||||
|
public class Device {
|
||||||
|
private String type;
|
||||||
|
private String path;
|
||||||
|
private WalletModel model;
|
||||||
|
private Boolean needsPinSent;
|
||||||
|
private Boolean needsPassphraseSent;
|
||||||
|
private String fingerprint;
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletModel getModel() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModel(WalletModel model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getNeedsPinSent() {
|
||||||
|
return needsPinSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNeedsPinSent(Boolean needsPinSent) {
|
||||||
|
this.needsPinSent = needsPinSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getNeedsPassphraseSent() {
|
||||||
|
return needsPassphraseSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNeedsPassphraseSent(Boolean needsPassphraseSent) {
|
||||||
|
this.needsPassphraseSent = needsPassphraseSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFingerprint() {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFingerprint(String fingerprint) {
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return getModel() + ":" + getPath();
|
||||||
|
}
|
||||||
|
}
|
251
src/main/java/com/sparrowwallet/sparrow/external/Hwi.java
vendored
Normal file
251
src/main/java/com/sparrowwallet/sparrow/external/Hwi.java
vendored
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
package com.sparrowwallet.sparrow.external;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.google.common.io.CharStreams;
|
||||||
|
import com.google.gson.*;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Hwi implements KeystoreImport {
|
||||||
|
private static File hwiExecutable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Hardware Wallet";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreImportDescription() {
|
||||||
|
return "Imports a connected hardware wallet";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyType getPolicyType() {
|
||||||
|
return PolicyType.SINGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||||
|
try {
|
||||||
|
List<String> command;
|
||||||
|
if(passphrase != null) {
|
||||||
|
command = List.of(getHwiExecutable().getAbsolutePath(), "--password", passphrase, "enumerate");
|
||||||
|
} else {
|
||||||
|
command = List.of(getHwiExecutable().getAbsolutePath(), "enumerate");
|
||||||
|
}
|
||||||
|
|
||||||
|
String output = execute(command);
|
||||||
|
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||||
|
return Arrays.asList(devices);
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new ImportException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean promptPin(Device device) throws ImportException {
|
||||||
|
try {
|
||||||
|
String output = execute(getDeviceCommand(device, "promptpin"));
|
||||||
|
return wasSuccessful(output);
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new ImportException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sendPin(Device device, String pin) throws ImportException {
|
||||||
|
try {
|
||||||
|
String output = execute(getDeviceCommand(device, "sendpin", pin));
|
||||||
|
return wasSuccessful(output);
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new ImportException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||||
|
try {
|
||||||
|
String output;
|
||||||
|
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
|
||||||
|
output = execute(getDeviceCommand(device, passphrase, "getxpub", derivationPath));
|
||||||
|
} else {
|
||||||
|
output = execute(getDeviceCommand(device, "getxpub", derivationPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||||
|
if(result.get("xpub") != null) {
|
||||||
|
return result.get("xpub").getAsString();
|
||||||
|
} else {
|
||||||
|
throw new ImportException("Could not retrieve xpub");
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new ImportException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String execute(List<String> command) throws IOException {
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized File getHwiExecutable() throws IOException {
|
||||||
|
if(hwiExecutable == null) {
|
||||||
|
Platform platform = Platform.getCurrent();
|
||||||
|
System.out.println("/external/" + platform.getPlatformId().toLowerCase() + "/hwi");
|
||||||
|
InputStream inputStream = Hwi.class.getResourceAsStream("/external/" + platform.getPlatformId().toLowerCase() + "/hwi");
|
||||||
|
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
|
||||||
|
Path tempExecPath = Files.createTempFile("hwi", null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||||
|
File tempExec = tempExecPath.toFile();
|
||||||
|
System.out.println(tempExec.getAbsolutePath());
|
||||||
|
tempExec.deleteOnExit();
|
||||||
|
OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec));
|
||||||
|
ByteStreams.copy(new FramedLZ4CompressorInputStream(inputStream), tempExecStream);
|
||||||
|
inputStream.close();
|
||||||
|
tempExecStream.flush();
|
||||||
|
tempExecStream.close();
|
||||||
|
|
||||||
|
hwiExecutable = tempExec;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hwiExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean wasSuccessful(String output) throws ImportException {
|
||||||
|
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||||
|
if(result.get("error") != null) {
|
||||||
|
throw new ImportException(result.get("error").getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.get("success").getAsBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getDeviceCommand(Device device, String command) throws IOException {
|
||||||
|
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getDeviceCommand(Device device, String command, String data) throws IOException {
|
||||||
|
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getDeviceCommand(Device device, String passphrase, String command, String data) throws IOException {
|
||||||
|
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EnumerateService extends Service<List<Device>> {
|
||||||
|
private final String passphrase;
|
||||||
|
|
||||||
|
public EnumerateService(String passphrase) {
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Device>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected List<Device> call() throws ImportException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.enumerate(passphrase);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PromptPinService extends Service<Boolean> {
|
||||||
|
private Device device;
|
||||||
|
|
||||||
|
public PromptPinService(Device device) {
|
||||||
|
this.device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Boolean> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected Boolean call() throws ImportException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.promptPin(device);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SendPinService extends Service<Boolean> {
|
||||||
|
private Device device;
|
||||||
|
private String pin;
|
||||||
|
|
||||||
|
public SendPinService(Device device, String pin) {
|
||||||
|
this.device = device;
|
||||||
|
this.pin = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Boolean> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected Boolean call() throws ImportException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.sendPin(device, pin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GetXpubService extends Service<String> {
|
||||||
|
private Device device;
|
||||||
|
private String passphrase;
|
||||||
|
private String derivationPath;
|
||||||
|
|
||||||
|
public GetXpubService(Device device, String passphrase, String derivationPath) {
|
||||||
|
this.device = device;
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
this.derivationPath = derivationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<String> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected String call() throws ImportException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.getXpub(device, passphrase, derivationPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gson getGson() {
|
||||||
|
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
|
||||||
|
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
|
||||||
|
return gsonBuilder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(src.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
|
||||||
|
@Override
|
||||||
|
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return WalletModel.valueOf(json.getAsJsonPrimitive().getAsString().toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,10 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
* The individual glyphs offered by the FontAwesome5 font.
|
* The individual glyphs offered by the FontAwesome5 font.
|
||||||
*/
|
*/
|
||||||
public static enum Glyph implements INamedCharacter {
|
public static enum Glyph implements INamedCharacter {
|
||||||
|
CIRCLE('\uf111'),
|
||||||
|
EXCLAMATION_CIRCLE('\uf06a'),
|
||||||
|
LAPTOP('\uf109'),
|
||||||
|
SD_CARD('\uf7c2'),
|
||||||
WALLET('\uf555');
|
WALLET('\uf555');
|
||||||
|
|
||||||
private final char ch;
|
private final char ch;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.sparrowwallet.sparrow.glyphfont;
|
||||||
|
|
||||||
|
import org.controlsfx.glyphfont.FontAwesome;
|
||||||
|
import org.controlsfx.glyphfont.GlyphFont;
|
||||||
|
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||||
|
import org.controlsfx.glyphfont.INamedCharacter;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class FontAwesome5Brands extends GlyphFont {
|
||||||
|
public static String FONT_NAME = "Font Awesome 5 Brands Regular";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The individual glyphs offered by the FontAwesome5Brands font.
|
||||||
|
*/
|
||||||
|
public static enum Glyph implements INamedCharacter {
|
||||||
|
USB('\uf287');
|
||||||
|
|
||||||
|
private final char ch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a named Glyph mapped to the given character
|
||||||
|
*
|
||||||
|
* @param ch
|
||||||
|
*/
|
||||||
|
Glyph(char ch) {
|
||||||
|
this.ch = ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char getChar() {
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not call this constructor directly - instead access the
|
||||||
|
* {@link FontAwesome5Brands.Glyph} public static enumeration method to create the glyph nodes), or
|
||||||
|
* use the {@link GlyphFontRegistry} class to get access.
|
||||||
|
* <p>
|
||||||
|
* Note: Do not remove this public constructor since it is used by the service loader!
|
||||||
|
*/
|
||||||
|
public FontAwesome5Brands() {
|
||||||
|
this(FontAwesome5Brands.class.getResourceAsStream("/font/fa-brands-400.ttf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new FontAwesome5Brands instance which uses the provided font source.
|
||||||
|
*
|
||||||
|
* @param is
|
||||||
|
*/
|
||||||
|
public FontAwesome5Brands(InputStream is) {
|
||||||
|
super(FONT_NAME, 14, is, true);
|
||||||
|
registerAll(Arrays.asList(FontAwesome5Brands.Glyph.values()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.sparrowwallet.sparrow.keystoreimport;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
|
import com.sparrowwallet.sparrow.external.Device;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.fxml.Initializable;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
public class KeystoreImportController implements Initializable {
|
||||||
|
private Wallet wallet;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleGroup importMenu;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private StackPane importPane;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeView(Wallet wallet) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
importMenu.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> {
|
||||||
|
KeystoreSource importType = (KeystoreSource) selectedToggle.getUserData();
|
||||||
|
setImportPane(importType.toString().toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showUsbDevices(List<Device> devices) {
|
||||||
|
FXMLLoader loader = setImportPane("usb-devices");
|
||||||
|
UsbDevicesController controller = loader.getController();
|
||||||
|
controller.initializeView(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showUsbError(String message) {
|
||||||
|
FXMLLoader loader = setImportPane("usb-error");
|
||||||
|
UsbScanController controller = loader.getController();
|
||||||
|
controller.initializeView(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
FXMLLoader setImportPane(String fxmlName) {
|
||||||
|
importPane.getChildren().removeAll(importPane.getChildren());
|
||||||
|
|
||||||
|
try {
|
||||||
|
FXMLLoader importLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/" + fxmlName + ".fxml"));
|
||||||
|
Node importTypeNode = importLoader.load();
|
||||||
|
KeystoreImportDetailController controller = importLoader.getController();
|
||||||
|
controller.setMasterController(this);
|
||||||
|
importPane.getChildren().add(importTypeNode);
|
||||||
|
|
||||||
|
return importLoader;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Can't find pane", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.sparrowwallet.sparrow.keystoreimport;
|
||||||
|
|
||||||
|
public abstract class KeystoreImportDetailController {
|
||||||
|
private KeystoreImportController masterController;
|
||||||
|
|
||||||
|
public KeystoreImportController getMasterController() {
|
||||||
|
return masterController;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMasterController(KeystoreImportController masterController) {
|
||||||
|
this.masterController = masterController;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.sparrowwallet.sparrow.keystoreimport;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.control.ButtonBar;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Dialog;
|
||||||
|
import javafx.scene.control.DialogPane;
|
||||||
|
import org.controlsfx.tools.Borders;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||||
|
private final KeystoreImportController keystoreImportController;
|
||||||
|
private Keystore keystore;
|
||||||
|
|
||||||
|
public KeystoreImportDialog(Wallet wallet) {
|
||||||
|
EventManager.get().register(this);
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
||||||
|
try {
|
||||||
|
FXMLLoader ksiLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/keystoreimport.fxml"));
|
||||||
|
dialogPane.setContent(Borders.wrap(ksiLoader.load()).lineBorder().outerPadding(0).innerPadding(0).buildAll());
|
||||||
|
keystoreImportController = ksiLoader.getController();
|
||||||
|
keystoreImportController.initializeView(wallet);
|
||||||
|
|
||||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
dialogPane.setPrefWidth(620);
|
||||||
|
dialogPane.setPrefHeight(500);
|
||||||
|
|
||||||
|
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void keystoreImported(KeystoreImportEvent event) {
|
||||||
|
this.keystore = event.getKeystore();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.sparrowwallet.sparrow.keystoreimport;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.control.DeviceAccordion;
|
||||||
|
import com.sparrowwallet.sparrow.external.Device;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UsbDevicesController extends KeystoreImportDetailController {
|
||||||
|
@FXML
|
||||||
|
private DeviceAccordion deviceAccordion;
|
||||||
|
|
||||||
|
public void initializeView(List<Device> devices) {
|
||||||
|
deviceAccordion.setDeviceOperation(DeviceAccordion.DeviceOperation.IMPORT);
|
||||||
|
deviceAccordion.setDevices(getMasterController().getWallet(), FXCollections.observableList(devices));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.sparrowwallet.sparrow.keystoreimport;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.external.Device;
|
||||||
|
import com.sparrowwallet.sparrow.external.Hwi;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UsbScanController extends KeystoreImportDetailController {
|
||||||
|
@FXML
|
||||||
|
private Label message;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button scan;
|
||||||
|
|
||||||
|
public void initializeView(String updateMessage) {
|
||||||
|
message.setText(updateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scan(ActionEvent event) {
|
||||||
|
message.setText("Please check your device");
|
||||||
|
scan.setText("Scanning...");
|
||||||
|
scan.setDisable(true);
|
||||||
|
|
||||||
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||||
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
List<Device> devices = enumerateService.getValue();
|
||||||
|
getMasterController().showUsbDevices(devices);
|
||||||
|
});
|
||||||
|
enumerateService.setOnFailed(workerStateEvent -> {
|
||||||
|
getMasterController().showUsbError(enumerateService.getException().getMessage());
|
||||||
|
});
|
||||||
|
enumerateService.start();
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,10 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
|
||||||
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
|
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.Control;
|
||||||
|
@ -18,6 +20,7 @@ import org.controlsfx.validation.Validator;
|
||||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -36,7 +39,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
@FXML
|
@FXML
|
||||||
private TextField fingerprint;
|
private TextField fingerprint;
|
||||||
|
|
||||||
private ValidationSupport validationSupport = new ValidationSupport();
|
private final ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
@ -119,4 +122,16 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
|
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void importKeystore(ActionEvent event) {
|
||||||
|
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet());
|
||||||
|
Optional<Keystore> result = dlg.showAndWait();
|
||||||
|
if(result.isPresent()) {
|
||||||
|
Keystore keystore = result.get();
|
||||||
|
label.setText(keystore.getLabel());
|
||||||
|
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||||
|
xpub.setText(keystore.getExtendedPublicKey().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires java.desktop;
|
requires java.desktop;
|
||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
|
requires javafx.graphics;
|
||||||
requires org.controlsfx.controls;
|
requires org.controlsfx.controls;
|
||||||
requires org.fxmisc.richtext;
|
requires org.fxmisc.richtext;
|
||||||
requires tornadofx.controls;
|
requires tornadofx.controls;
|
||||||
|
@ -9,5 +10,6 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires com.google.common;
|
requires com.google.common;
|
||||||
requires flowless;
|
requires flowless;
|
||||||
requires com.google.gson;
|
requires com.google.gson;
|
||||||
|
requires org.apache.commons.compress;
|
||||||
requires javafx.swing;
|
requires javafx.swing;
|
||||||
}
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
60
src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css
vendored
Normal file
60
src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css
vendored
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
.dialog-pane .content {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu {
|
||||||
|
-fx-pref-width: 130;
|
||||||
|
-fx-background-color: #3da0e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
-fx-pref-width: 130;
|
||||||
|
-fx-padding: 0 20 0 20;
|
||||||
|
-fx-background-color: #3da0e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item * {
|
||||||
|
-fx-fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:hover {
|
||||||
|
-fx-background-color: #4aa7e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:selected {
|
||||||
|
-fx-background-color: #1e88cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#importPane {
|
||||||
|
-fx-background-color: -fx-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane > .title {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane > .title > .arrow-button {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane > .title > .arrow-button > .arrow {
|
||||||
|
visibility: hidden;
|
||||||
|
-fx-translate-x: -1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devicelist-main-label .text {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.devicelist-status-label .text {
|
||||||
|
-fx-fill: #a0a1a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error .text {
|
||||||
|
-fx-fill: #ca1243;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
|
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<BorderPane stylesheets="@../general.css, @keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.KeystoreImportController">
|
||||||
|
<padding>
|
||||||
|
<Insets top="0" left="0" right="0" bottom="0" />
|
||||||
|
</padding>
|
||||||
|
<left>
|
||||||
|
<VBox styleClass="list-menu">
|
||||||
|
<ToggleButton VBox.vgrow="ALWAYS" text="Connected Hardware Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
|
||||||
|
<toggleGroup>
|
||||||
|
<ToggleGroup fx:id="importMenu" />
|
||||||
|
</toggleGroup>
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="20" icon="USB" />
|
||||||
|
</graphic>
|
||||||
|
<userData>
|
||||||
|
<KeystoreSource fx:constant="HW_USB"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton VBox.vgrow="ALWAYS" text="Airgapped Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$importMenu">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SD_CARD" />
|
||||||
|
</graphic>
|
||||||
|
<userData>
|
||||||
|
<KeystoreSource fx:constant="HW_AIRGAPPED"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton VBox.vgrow="ALWAYS" text="Software Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$importMenu">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="LAPTOP" />
|
||||||
|
</graphic>
|
||||||
|
<userData>
|
||||||
|
<KeystoreSource fx:constant="SW_SEED"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
|
</VBox>
|
||||||
|
</left>
|
||||||
|
<center>
|
||||||
|
<StackPane fx:id="importPane">
|
||||||
|
|
||||||
|
</StackPane>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.DeviceAccordion?>
|
||||||
|
|
||||||
|
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbDevicesController">
|
||||||
|
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
|
||||||
|
<DeviceAccordion fx:id="deviceAccordion" />
|
||||||
|
</ScrollPane>
|
||||||
|
</AnchorPane>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
|
|
||||||
|
<VBox alignment="CENTER" spacing="30" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbScanController">
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="50" icon="EXCLAMATION_CIRCLE" />
|
||||||
|
<Label text="There was error connecting to the wallet:" />
|
||||||
|
<Label fx:id="message" />
|
||||||
|
<Button fx:id="scan" text="Scan Again..." wrapText="true" prefWidth="120" prefHeight="60" onAction="#scan"/>
|
||||||
|
</VBox>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<?import javafx.scene.text.Text?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
|
|
||||||
|
<VBox alignment="CENTER" spacing="30" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbScanController">
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="50" icon="USB" />
|
||||||
|
<Label fx:id="message" text="Connect Hardware Wallet" />
|
||||||
|
<Button fx:id="scan" text="Scan..." wrapText="true" prefWidth="120" prefHeight="60" onAction="#scan"/>
|
||||||
|
</VBox>
|
|
@ -18,6 +18,8 @@
|
||||||
<Fieldset inputGrow="SOMETIMES" text="">
|
<Fieldset inputGrow="SOMETIMES" text="">
|
||||||
<Field text="Label:">
|
<Field text="Label:">
|
||||||
<TextField fx:id="label" maxWidth="160"/>
|
<TextField fx:id="label" maxWidth="160"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS" />
|
||||||
|
<Button text="Import..." onAction="#importKeystore"/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field text="Master fingerprint:">
|
<Field text="Master fingerprint:">
|
||||||
<TextField fx:id="fingerprint" maxWidth="80"/>
|
<TextField fx:id="fingerprint" maxWidth="80"/>
|
||||||
|
|
BIN
src/main/resources/external/mac/hwi
vendored
Executable file
BIN
src/main/resources/external/mac/hwi
vendored
Executable file
Binary file not shown.
BIN
src/main/resources/font/fa-brands-400.ttf
Normal file
BIN
src/main/resources/font/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/image/bitbox.png
Normal file
BIN
src/main/resources/image/bitbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
src/main/resources/image/coldcard.png
Normal file
BIN
src/main/resources/image/coldcard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
src/main/resources/image/keepkey.png
Normal file
BIN
src/main/resources/image/keepkey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/main/resources/image/ledger.png
Normal file
BIN
src/main/resources/image/ledger.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
src/main/resources/image/trezor.png
Normal file
BIN
src/main/resources/image/trezor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in a new issue