From 9adfcf5806a2d6b6ce6e7f57ceddd892230dc027 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 29 Apr 2020 12:03:44 +0200 Subject: [PATCH] usb keystore importing --- build.gradle | 3 +- drongo | 2 +- .../com/sparrowwallet/sparrow/MainApp.java | 15 +- .../sparrow/control/DeviceAccordion.java | 32 ++ .../sparrow/control/DevicePane.java | 388 ++++++++++++++++++ .../sparrow/control/EnumChoiceBox.java | 14 - .../sparrow/event/KeystoreImportEvent.java | 15 + .../sparrow/external/ColdcardSinglesig.java | 2 +- .../sparrow/external/Device.java | 64 +++ .../sparrowwallet/sparrow/external/Hwi.java | 251 +++++++++++ .../sparrow/glyphfont/FontAwesome5.java | 4 + .../sparrow/glyphfont/FontAwesome5Brands.java | 57 +++ .../KeystoreImportController.java | 72 ++++ .../KeystoreImportDetailController.java | 13 + .../keystoreimport/KeystoreImportDialog.java | 48 +++ .../keystoreimport/UsbDevicesController.java | 18 + .../keystoreimport/UsbScanController.java | 38 ++ .../sparrow/wallet/KeystoreController.java | 17 +- src/main/java/module-info.java | 2 + src/main/resources/bird.png | Bin 2724 -> 0 bytes .../sparrow/keystoreimport/keystoreimport.css | 60 +++ .../keystoreimport/keystoreimport.fxml | 52 +++ .../sparrow/keystoreimport/usb-devices.fxml | 14 + .../sparrow/keystoreimport/usb-error.fxml | 15 + .../sparrow/keystoreimport/usb.fxml | 17 + .../sparrow/wallet/keystore.fxml | 2 + src/main/resources/external/mac/hwi | Bin 0 -> 5936368 bytes src/main/resources/font/fa-brands-400.ttf | Bin 0 -> 132728 bytes src/main/resources/image/bitbox.png | Bin 0 -> 1423 bytes src/main/resources/image/coldcard.png | Bin 0 -> 5396 bytes src/main/resources/image/keepkey.png | Bin 0 -> 3803 bytes src/main/resources/image/ledger.png | Bin 0 -> 1470 bytes src/main/resources/image/trezor.png | Bin 0 -> 1562 bytes 33 files changed, 1196 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DeviceAccordion.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java delete mode 100644 src/main/java/com/sparrowwallet/sparrow/control/EnumChoiceBox.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/KeystoreImportEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/external/Device.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/external/Hwi.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5Brands.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDetailController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbDevicesController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbScanController.java delete mode 100755 src/main/resources/bird.png create mode 100644 src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css create mode 100644 src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.fxml create mode 100644 src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-devices.fxml create mode 100644 src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-error.fxml create mode 100644 src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb.fxml create mode 100755 src/main/resources/external/mac/hwi create mode 100644 src/main/resources/font/fa-brands-400.ttf create mode 100644 src/main/resources/image/bitbox.png create mode 100644 src/main/resources/image/coldcard.png create mode 100644 src/main/resources/image/keepkey.png create mode 100644 src/main/resources/image/ledger.png create mode 100644 src/main/resources/image/trezor.png diff --git a/build.gradle b/build.gradle index 1a99ef73..7d729ebd 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/drongo b/drongo index 294649de..ed056bc4 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 294649de669497283934933487d09e1dae9f3996 +Subproject commit ed056bc49fb919799f70a6d7ee2dd65a38c25b5d diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index 0fd75d6a..dd3468c9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceAccordion.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceAccordion.java new file mode 100644 index 00000000..464fb82c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceAccordion.java @@ -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 devices; + private DeviceOperation deviceOperation = DeviceOperation.IMPORT; + + public void setDevices(Wallet wallet, ObservableList 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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java new file mode 100644 index 00000000..29171ed8 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -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 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 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 derivation) { + if(device.getFingerprint() == null) { + Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get()); + enumerateService.setOnSucceeded(workerStateEvent -> { + List 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 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 + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EnumChoiceBox.java b/src/main/java/com/sparrowwallet/sparrow/control/EnumChoiceBox.java deleted file mode 100644 index 0764a28c..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/control/EnumChoiceBox.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sparrowwallet.sparrow.control; - -import javafx.beans.NamedArg; -import javafx.scene.control.ChoiceBox; - -public class EnumChoiceBox> extends ChoiceBox { - - public EnumChoiceBox(@NamedArg("enumType") String enumType) throws Exception { - Class enumClass = (Class) Class.forName(enumType); - getItems().setAll(enumClass.getEnumConstants()); - } -} - - diff --git a/src/main/java/com/sparrowwallet/sparrow/event/KeystoreImportEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/KeystoreImportEvent.java new file mode 100644 index 00000000..afa6afca --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/KeystoreImportEvent.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/external/ColdcardSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/external/ColdcardSinglesig.java index 9b41fbf9..a90fea4e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/external/ColdcardSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/external/ColdcardSinglesig.java @@ -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)); diff --git a/src/main/java/com/sparrowwallet/sparrow/external/Device.java b/src/main/java/com/sparrowwallet/sparrow/external/Device.java new file mode 100644 index 00000000..041ba30b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/external/Device.java @@ -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(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/external/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/external/Hwi.java new file mode 100644 index 00000000..457b4f15 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/external/Hwi.java @@ -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 enumerate(String passphrase) throws ImportException { + try { + List 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 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 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 getDeviceCommand(Device device, String command) throws IOException { + return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command); + } + + private List 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 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> { + private final String passphrase; + + public EnumerateService(String passphrase) { + this.passphrase = passphrase; + } + + @Override + protected Task> createTask() { + return new Task<>() { + protected List call() throws ImportException { + Hwi hwi = new Hwi(); + return hwi.enumerate(passphrase); + } + }; + } + } + + public static class PromptPinService extends Service { + private Device device; + + public PromptPinService(Device device) { + this.device = device; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws ImportException { + Hwi hwi = new Hwi(); + return hwi.promptPin(device); + } + }; + } + } + + public static class SendPinService extends Service { + private Device device; + private String pin; + + public SendPinService(Device device, String pin) { + this.device = device; + this.pin = pin; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws ImportException { + Hwi hwi = new Hwi(); + return hwi.sendPin(device, pin); + } + }; + } + } + + public static class GetXpubService extends Service { + 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 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 { + @Override + public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString().toLowerCase()); + } + } + + private static class DeviceModelDeserializer implements JsonDeserializer { + @Override + public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return WalletModel.valueOf(json.getAsJsonPrimitive().getAsString().toUpperCase()); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 02790583..bed8d015 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -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; diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5Brands.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5Brands.java new file mode 100644 index 00000000..d06e1537 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5Brands.java @@ -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. + *

+ * 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())); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java new file mode 100644 index 00000000..a8f73079 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java @@ -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 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); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDetailController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDetailController.java new file mode 100644 index 00000000..000a2de5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDetailController.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java new file mode 100644 index 00000000..8d5862ed --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java @@ -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 { + 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(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbDevicesController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbDevicesController.java new file mode 100644 index 00000000..e341ed60 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbDevicesController.java @@ -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 devices) { + deviceAccordion.setDeviceOperation(DeviceAccordion.DeviceOperation.IMPORT); + deviceAccordion.setDevices(getMasterController().getWallet(), FXCollections.observableList(devices)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbScanController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbScanController.java new file mode 100644 index 00000000..afe2e06f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/UsbScanController.java @@ -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 devices = enumerateService.getValue(); + getMasterController().showUsbDevices(devices); + }); + enumerateService.setOnFailed(workerStateEvent -> { + getMasterController().showUsbError(enumerateService.getException().getMessage()); + }); + enumerateService.start(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 3bf50aa9..c2038669 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -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 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()); + } + } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 66d3384e..2d4ac7d7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -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; } \ No newline at end of file diff --git a/src/main/resources/bird.png b/src/main/resources/bird.png deleted file mode 100755 index ae8b43f8fe685c50d6e3b05569aa66983b515752..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2724 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 eXYcI)%KihPB+hX1^3Koz0000 .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; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.fxml b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.fxml new file mode 100644 index 00000000..656939b9 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.fxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + +
+ diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-devices.fxml b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-devices.fxml new file mode 100644 index 00000000..b1479641 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-devices.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-error.fxml b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-error.fxml new file mode 100644 index 00000000..58ce3392 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/usb-error.fxml @@ -0,0 +1,15 @@ + + + + + + + + + + + +