mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +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 {
|
||||
version = "14"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ]
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
java {
|
||||
|
@ -32,6 +32,7 @@ dependencies {
|
|||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('org.apache.commons:commons-compress:1.20')
|
||||
implementation('org.controlsfx:controlsfx:11.0.1' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
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;
|
||||
|
||||
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.FontAwesome5Brands;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
|
@ -14,6 +19,7 @@ public class MainApp extends Application {
|
|||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
|
||||
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
|
||||
Parent root = transactionLoader.load();
|
||||
|
@ -30,7 +36,14 @@ public class MainApp extends Application {
|
|||
|
||||
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) {
|
||||
|
|
|
@ -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 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);
|
||||
Keystore keystore = new Keystore();
|
||||
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.
|
||||
*/
|
||||
public static enum Glyph implements INamedCharacter {
|
||||
CIRCLE('\uf111'),
|
||||
EXCLAMATION_CIRCLE('\uf06a'),
|
||||
LAPTOP('\uf109'),
|
||||
SD_CARD('\uf7c2'),
|
||||
WALLET('\uf555');
|
||||
|
||||
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.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
|
||||
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Control;
|
||||
|
@ -18,6 +20,7 @@ import org.controlsfx.validation.Validator;
|
|||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -36,7 +39,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
@FXML
|
||||
private TextField fingerprint;
|
||||
|
||||
private ValidationSupport validationSupport = new ValidationSupport();
|
||||
private final ValidationSupport validationSupport = new ValidationSupport();
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
@ -119,4 +122,16 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
|
||||
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 javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires javafx.graphics;
|
||||
requires org.controlsfx.controls;
|
||||
requires org.fxmisc.richtext;
|
||||
requires tornadofx.controls;
|
||||
|
@ -9,5 +10,6 @@ open module com.sparrowwallet.sparrow {
|
|||
requires com.google.common;
|
||||
requires flowless;
|
||||
requires com.google.gson;
|
||||
requires org.apache.commons.compress;
|
||||
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="">
|
||||
<Field text="Label:">
|
||||
<TextField fx:id="label" maxWidth="160"/>
|
||||
<Pane HBox.hgrow="ALWAYS" />
|
||||
<Button text="Import..." onAction="#importKeystore"/>
|
||||
</Field>
|
||||
<Field text="Master fingerprint:">
|
||||
<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