diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index f1327b58..b05646dc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -113,6 +113,8 @@ public class AppController implements Initializable { private ElectrumServer.ConnectionService connectionService; + private Hwi.ScheduledEnumerateService deviceEnumerateService; + private static Integer currentBlockHeight; public static boolean showTxHexProperty; @@ -291,6 +293,25 @@ public class AppController implements Initializable { return ratesService; } + private Hwi.ScheduledEnumerateService createDeviceEnumerateService() { + Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); + enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); + enumerateService.setOnSucceeded(workerStateEvent -> { + List devices = enumerateService.getValue(); + + //Null devices are returned if the app is currently prompting for a pin. Otherwise, the enumerate clears the pin screen + if(devices != null) { + //If another instance of HWI is currently accessing the usb interface, HWI returns empty device models. Ignore this run if that happens + List validDevices = devices.stream().filter(device -> device.getModel() != null).collect(Collectors.toList()); + if(validDevices.size() == devices.size()) { + Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); + } + } + }); + + return enumerateService; + } + public void setApplication(MainApp application) { this.application = application; } @@ -509,7 +530,7 @@ public class AppController implements Initializable { } public static List getDevices() { - return devices; + return devices == null ? new ArrayList<>() : devices; } public Map getOpenWallets() { @@ -739,24 +760,6 @@ public class AppController implements Initializable { EventManager.get().register(walletForm); controller.setWalletForm(walletForm); - if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) { - Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); - enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); - enumerateService.setOnSucceeded(workerStateEvent -> { - List devices = enumerateService.getValue(); - - //Null devices are returned if the app is currently prompting for a pin. Otherwise, the enumerate clears the pin screen - if(devices != null) { - //If another instance of HWI is currently accessing the usb interface, HWI returns empty device models. Ignore this run if that happens - List validDevices = devices.stream().filter(device -> device.getModel() != null).collect(Collectors.toList()); - if(validDevices.size() == devices.size()) { - EventManager.get().post(new UsbDeviceEvent(devices)); - } - } - }); - enumerateService.start(); - } - tabs.getTabs().add(tab); return tab; } catch(IOException e) { @@ -1080,6 +1083,36 @@ public class AppController implements Initializable { fiatCurrencyExchangeRate = event.getCurrencyRate(); } + @Subscribe + public void openWallets(OpenWalletsEvent event) { + boolean usbWallet = false; + for(Map.Entry entry : event.getWalletsMap().entrySet()) { + Wallet wallet = entry.getKey(); + Storage storage = entry.getValue(); + + if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) { + usbWallet = true; + + if(deviceEnumerateService == null) { + deviceEnumerateService = createDeviceEnumerateService(); + } + + if(deviceEnumerateService.getState() == Worker.State.CANCELLED) { + deviceEnumerateService.reset(); + } + + if(!deviceEnumerateService.isRunning()) { + deviceEnumerateService.start(); + } + } + } + + if(!usbWallet && deviceEnumerateService != null && deviceEnumerateService.isRunning()) { + deviceEnumerateService.cancel(); + EventManager.get().post(new UsbDeviceEvent(Collections.emptyList())); + } + } + @Subscribe public void requestOpenWallets(RequestOpenWalletsEvent event) { EventManager.get().post(new OpenWalletsEvent(getOpenWallets())); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java new file mode 100644 index 00000000..e43cc063 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java @@ -0,0 +1,35 @@ +package com.sparrowwallet.sparrow.control; + +import com.google.common.eventbus.Subscribe; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.AddressDisplayedEvent; +import com.sparrowwallet.sparrow.io.Device; + +import java.util.List; + +public class DeviceAddressDialog extends DeviceDialog { + private final Wallet wallet; + private final KeyDerivation keyDerivation; + + public DeviceAddressDialog(List devices, Wallet wallet, KeyDerivation keyDerivation) { + super(devices); + this.wallet = wallet; + this.keyDerivation = keyDerivation; + + EventManager.get().register(this); + } + + @Override + protected DevicePane getDevicePane(Device device) { + return new DevicePane(wallet, keyDerivation, device); + } + + @Subscribe + public void addressDisplayed(AddressDisplayedEvent event) { + EventManager.get().unregister(this); + setResult(event.getAddress()); + this.close(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java new file mode 100644 index 00000000..8c1b00f5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java @@ -0,0 +1,126 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppController; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.UsbDeviceEvent; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; +import com.sparrowwallet.sparrow.io.Device; +import com.sparrowwallet.sparrow.io.Hwi; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.controlsfx.glyphfont.Glyph; + +import java.util.List; + +public abstract class DeviceDialog extends Dialog { + private final List operationDevices; + private final Accordion deviceAccordion; + private final VBox scanBox; + private final Label scanLabel; + + public DeviceDialog() { + this(null); + } + + public DeviceDialog(List operationDevices) { + this.operationDevices = operationDevices; + + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); + + StackPane stackPane = new StackPane(); + dialogPane.setContent(stackPane); + + AnchorPane anchorPane = new AnchorPane(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setPrefHeight(280); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + anchorPane.getChildren().add(scrollPane); + scrollPane.setFitToWidth(true); + AnchorPane.setLeftAnchor(scrollPane, 0.0); + AnchorPane.setRightAnchor(scrollPane, 0.0); + + deviceAccordion = new Accordion(); + scrollPane.setContent(deviceAccordion); + + scanBox = new VBox(); + scanBox.setSpacing(30); + scanBox.setAlignment(Pos.CENTER); + Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB); + usb.setFontSize(50); + scanLabel = new Label("Connect Hardware Wallet"); + Button button = new Button("Scan..."); + button.setPrefSize(120, 60); + button.setOnAction(event -> { + scan(); + }); + scanBox.getChildren().addAll(usb, scanLabel, button); + scanBox.managedProperty().bind(scanBox.visibleProperty()); + + stackPane.getChildren().addAll(anchorPane, scanBox); + + List devices = AppController.getDevices(); + if(devices == null || devices.isEmpty()) { + scanBox.setVisible(true); + } else { + Platform.runLater(() -> setDevices(devices)); + scanBox.setVisible(false); + } + + final ButtonType rescanButtonType = new javafx.scene.control.ButtonType("Rescan", ButtonBar.ButtonData.NO); + final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().addAll(rescanButtonType, cancelButtonType); + + Button rescanButton = (Button) dialogPane.lookupButton(rescanButtonType); + rescanButton.managedProperty().bind(rescanButton.visibleProperty()); + rescanButton.visibleProperty().bind(scanBox.visibleProperty().not()); + rescanButton.addEventFilter(ActionEvent.ACTION, event -> { + scan(); + event.consume(); + }); + + dialogPane.setPrefWidth(500); + dialogPane.setPrefHeight(360); + } + + private void scan() { + Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null); + enumerateService.setOnSucceeded(workerStateEvent -> { + List devices = enumerateService.getValue(); + setDevices(devices); + Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); + }); + enumerateService.setOnFailed(workerStateEvent -> { + scanBox.setVisible(true); + scanLabel.setText(workerStateEvent.getSource().getException().getMessage()); + }); + enumerateService.start(); + } + + protected void setDevices(List devices) { + List dialogDevices = devices; + if(operationDevices != null && dialogDevices.containsAll(operationDevices)) { + dialogDevices = operationDevices; + } + + deviceAccordion.getPanes().clear(); + + if(dialogDevices.isEmpty()) { + scanBox.setVisible(true); + scanLabel.setText("No devices found"); + } else { + scanBox.setVisible(false); + for(Device device : dialogDevices) { + DevicePane devicePane = getDevicePane(device); + deviceAccordion.getPanes().add(devicePane); + } + } + } + + protected abstract DevicePane getDevicePane(Device device); +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index 6d0ee1b2..cd26d2a4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -8,6 +8,7 @@ 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.AddressDisplayedEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.io.Device; @@ -35,6 +36,7 @@ public class DevicePane extends TitledDescriptionPane { private final DeviceOperation deviceOperation; private final Wallet wallet; private final PSBT psbt; + private final KeyDerivation keyDerivation; private final Device device; private CustomPasswordField pinField; @@ -43,14 +45,16 @@ public class DevicePane extends TitledDescriptionPane { private Button setPassphraseButton; private SplitMenuButton importButton; private Button signButton; + private Button displayAddressButton; private final SimpleStringProperty passphrase = new SimpleStringProperty(""); - public DevicePane(DeviceOperation deviceOperation, Wallet wallet, Device device) { + public DevicePane(Wallet wallet, Device device) { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); - this.deviceOperation = deviceOperation; + this.deviceOperation = DeviceOperation.IMPORT; this.wallet = wallet; this.psbt = null; + this.keyDerivation = null; this.device = device; setDefaultStatus(); @@ -70,11 +74,12 @@ public class DevicePane extends TitledDescriptionPane { buttonBox.getChildren().addAll(setPassphraseButton, importButton); } - public DevicePane(DeviceOperation deviceOperation, PSBT psbt, Device device) { + public DevicePane(PSBT psbt, Device device) { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); - this.deviceOperation = deviceOperation; + this.deviceOperation = DeviceOperation.SIGN; this.wallet = null; this.psbt = psbt; + this.keyDerivation = null; this.device = device; setDefaultStatus(); @@ -94,6 +99,31 @@ public class DevicePane extends TitledDescriptionPane { buttonBox.getChildren().addAll(setPassphraseButton, signButton); } + public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) { + super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); + this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS; + this.wallet = wallet; + this.psbt = null; + this.keyDerivation = keyDerivation; + this.device = device; + + setDefaultStatus(); + showHideLink.setVisible(false); + + createSetPassphraseButton(); + createDisplayAddressButton(); + + if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) { + unlockButton.setVisible(true); + } else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) { + setPassphraseButton.setVisible(true); + } else { + showOperationButton(); + } + + buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton); + } + @Override protected Control createButton() { createUnlockButton(); @@ -153,6 +183,7 @@ public class DevicePane extends TitledDescriptionPane { signButton = new Button("Sign"); signButton.setDefaultButton(true); signButton.setAlignment(Pos.CENTER_RIGHT); + signButton.setMinWidth(44); signButton.setOnAction(event -> { signButton.setDisable(true); sign(); @@ -161,6 +192,22 @@ public class DevicePane extends TitledDescriptionPane { signButton.setVisible(false); } + private void createDisplayAddressButton() { + displayAddressButton = new Button("Display Address"); + displayAddressButton.setDefaultButton(true); + displayAddressButton.setAlignment(Pos.CENTER_RIGHT); + displayAddressButton.setOnAction(event -> { + displayAddressButton.setDisable(true); + displayAddress(); + }); + displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty()); + displayAddressButton.setVisible(false); + + if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) { + displayAddressButton.setDisable(true); + } + } + private void unlock(Device device) { if(device.getModel().equals(WalletModel.TREZOR_1)) { promptPin(); @@ -375,6 +422,20 @@ public class DevicePane extends TitledDescriptionPane { signPSBTService.start(); } + private void displayAddress() { + Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath()); + displayAddressService.setOnSucceeded(successEvent -> { + String address = displayAddressService.getValue(); + EventManager.get().post(new AddressDisplayedEvent(address)); + }); + displayAddressService.setOnFailed(failedEvent -> { + setError(displayAddressService.getException().getMessage(), null); + displayAddressButton.setDisable(false); + }); + setDescription("Check device for address"); + displayAddressService.start(); + } + private void showOperationButton() { if(deviceOperation.equals(DeviceOperation.IMPORT)) { importButton.setVisible(true); @@ -384,6 +445,9 @@ public class DevicePane extends TitledDescriptionPane { } else if(deviceOperation.equals(DeviceOperation.SIGN)) { signButton.setVisible(true); showHideLink.setVisible(false); + } else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) { + displayAddressButton.setVisible(true); + showHideLink.setVisible(false); } } @@ -424,6 +488,6 @@ public class DevicePane extends TitledDescriptionPane { } public enum DeviceOperation { - IMPORT, SIGN; + IMPORT, SIGN, DISPLAY_ADDRESS; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java index c93c2d03..f8857dc7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java @@ -2,124 +2,31 @@ package com.sparrowwallet.sparrow.control; import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.psbt.PSBT; -import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.PSBTSignedEvent; -import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.io.Device; -import com.sparrowwallet.sparrow.io.Hwi; -import javafx.event.ActionEvent; -import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import org.controlsfx.glyphfont.Glyph; import java.util.List; -public class DeviceSignDialog extends Dialog { +public class DeviceSignDialog extends DeviceDialog { private final PSBT psbt; - private final Accordion deviceAccordion; - private final VBox scanBox; - private final Label scanLabel; - public DeviceSignDialog(PSBT psbt) { + public DeviceSignDialog(List devices, PSBT psbt) { + super(devices); this.psbt = psbt; - EventManager.get().register(this); - - final DialogPane dialogPane = getDialogPane(); - dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); - - StackPane stackPane = new StackPane(); - dialogPane.setContent(stackPane); - - AnchorPane anchorPane = new AnchorPane(); - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setPrefHeight(280); - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - anchorPane.getChildren().add(scrollPane); - scrollPane.setFitToWidth(true); - AnchorPane.setLeftAnchor(scrollPane, 0.0); - AnchorPane.setRightAnchor(scrollPane, 0.0); - - deviceAccordion = new Accordion(); - scrollPane.setContent(deviceAccordion); - - scanBox = new VBox(); - scanBox.setSpacing(30); - scanBox.setAlignment(Pos.CENTER); - Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB); - usb.setFontSize(50); - scanLabel = new Label("Connect Hardware Wallet"); - Button button = new Button("Scan..."); - button.setPrefSize(120, 60); - button.setOnAction(event -> { - scan(); - }); - scanBox.getChildren().addAll(usb, scanLabel, button); - scanBox.managedProperty().bind(scanBox.visibleProperty()); - - stackPane.getChildren().addAll(anchorPane, scanBox); - - List devices = AppController.getDevices(); - if(devices == null || devices.isEmpty()) { - scanBox.setVisible(true); - } else { - setDevices(devices); - scanBox.setVisible(false); - } - - final ButtonType rescanButtonType = new javafx.scene.control.ButtonType("Rescan", ButtonBar.ButtonData.NO); - final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); - dialogPane.getButtonTypes().addAll(rescanButtonType, cancelButtonType); - - Button rescanButton = (Button) dialogPane.lookupButton(rescanButtonType); - rescanButton.managedProperty().bind(rescanButton.visibleProperty()); - rescanButton.visibleProperty().bind(scanBox.visibleProperty().not()); - rescanButton.addEventFilter(ActionEvent.ACTION, event -> { - scan(); - event.consume(); - }); - - dialogPane.setPrefWidth(500); - dialogPane.setPrefHeight(360); - - setResultConverter(dialogButton -> dialogButton != cancelButtonType ? psbt : null); + setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt); } - private void scan() { - Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null); - enumerateService.setOnSucceeded(workerStateEvent -> { - List devices = enumerateService.getValue(); - setDevices(devices); - }); - enumerateService.setOnFailed(workerStateEvent -> { - scanBox.setVisible(true); - scanLabel.setText(workerStateEvent.getSource().getException().getMessage()); - }); - enumerateService.start(); - } - - private void setDevices(List devices) { - deviceAccordion.getPanes().clear(); - - if(devices.isEmpty()) { - scanBox.setVisible(true); - scanLabel.setText("No devices found"); - } else { - scanBox.setVisible(false); - for(Device device : devices) { - DevicePane devicePane = new DevicePane(DevicePane.DeviceOperation.SIGN, psbt, device); - deviceAccordion.getPanes().add(devicePane); - } - } + @Override + protected DevicePane getDevicePane(Device device) { + return new DevicePane(psbt, device); } @Subscribe public void psbtSigned(PSBTSignedEvent event) { if(psbt == event.getPsbt()) { + EventManager.get().unregister(this); setResult(event.getSignedPsbt()); this.close(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java index d70add6a..476eab93 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -47,8 +47,6 @@ public class QRDisplayDialog extends Dialog { this.ur = ur; this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0); - EventManager.get().register(this); - final DialogPane dialogPane = getDialogPane(); StackPane stackPane = new StackPane(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index 8e54937d..4bcde41d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -64,6 +64,7 @@ public class WalletExportDialog extends Dialog { @Subscribe public void walletExported(WalletExportEvent event) { + EventManager.get().unregister(this); wallet = event.getWallet(); setResult(wallet); this.close(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index da9a1507..f7de69cf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -51,6 +51,7 @@ public class WalletImportDialog extends Dialog { @Subscribe public void walletImported(WalletImportEvent event) { + EventManager.get().unregister(this); wallet = event.getWallet(); setResult(wallet); this.close(); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/AddressDisplayedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/AddressDisplayedEvent.java new file mode 100644 index 00000000..0daaae8d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/AddressDisplayedEvent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.sparrow.event; + +/** + * This event is used by the DeviceAddressDialog to indicate that a USB device has displayed an address + * + */ +public class AddressDisplayedEvent { + private final String address; + + public AddressDisplayedEvent(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/UsbDeviceEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/UsbDeviceEvent.java index 86939c0d..2d77d135 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/UsbDeviceEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/UsbDeviceEvent.java @@ -5,7 +5,7 @@ import com.sparrowwallet.sparrow.io.Device; import java.util.List; public class UsbDeviceEvent { - private List devices; + private final List devices; public UsbDeviceEvent(List devices) { this.devices = devices; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Device.java b/src/main/java/com/sparrowwallet/sparrow/io/Device.java index 85f6b7fd..ba6c4480 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Device.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Device.java @@ -2,6 +2,8 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.wallet.WalletModel; +import java.util.Objects; + public class Device { private String type; private String path; @@ -61,4 +63,22 @@ public class Device { public String toString() { return getModel() + ":" + getPath(); } + + @Override + public boolean equals(Object o) { + if(this == o) { + return true; + } + if(o == null || getClass() != o.getClass()) { + return false; + } + Device device = (Device) o; + return Objects.equals(type, device.type) && + Objects.equals(path, device.path); + } + + @Override + public int hashCode() { + return Objects.hash(type, path); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/DisplayAddressException.java b/src/main/java/com/sparrowwallet/sparrow/io/DisplayAddressException.java new file mode 100644 index 00000000..d0f2fe1d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/DisplayAddressException.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.sparrow.io; + +public class DisplayAddressException extends Exception { + public DisplayAddressException() { + super(); + } + + public DisplayAddressException(String message) { + super(message); + } + + public DisplayAddressException(Throwable cause) { + super(cause); + } + + public DisplayAddressException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index 948cf274..04c93d93 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.gson.*; +import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.WalletModel; @@ -20,9 +21,8 @@ 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; +import java.util.*; +import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -88,6 +88,37 @@ public class Hwi { } } + public String displayAddress(Device device, String passphrase, ScriptType scriptType, String derivationPath) throws DisplayAddressException { + try { + if(!List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH).contains(scriptType)) { + throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single sig types supported"); + } + + String type = null; + if(scriptType == ScriptType.P2SH_P2WPKH) { + type = "--sh_wpkh"; + } else if(scriptType == ScriptType.P2WPKH) { + type = "--wpkh"; + } + + String output; + if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) { + output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type)); + } else { + output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type)); + } + + JsonObject result = JsonParser.parseString(output).getAsJsonObject(); + if(result.get("address") != null) { + return result.get("address").getAsString(); + } else { + throw new DisplayAddressException("Could not retrieve address"); + } + } catch(IOException e) { + throw new DisplayAddressException(e); + } + } + public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException { try { String psbtBase64 = psbt.toBase64String(); @@ -251,12 +282,16 @@ public class Hwi { return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()); } - private List getDeviceCommand(Device device, Command command, String data) throws IOException { - return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString(), data); + private List getDeviceCommand(Device device, Command command, String... commandData) throws IOException { + List elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString())); + elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList())); + return elements; } - private List getDeviceCommand(Device device, String passphrase, Command command, String data) throws IOException { - return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString(), data); + private List getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException { + List elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString())); + elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList())); + return elements; } public static class EnumerateService extends Service> { @@ -337,6 +372,30 @@ public class Hwi { } } + public static class DisplayAddressService extends Service { + private final Device device; + private final String passphrase; + private final ScriptType scriptType; + private final String derivationPath; + + public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, String derivationPath) { + this.device = device; + this.passphrase = passphrase; + this.scriptType = scriptType; + this.derivationPath = derivationPath; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected String call() throws DisplayAddressException { + Hwi hwi = new Hwi(); + return hwi.displayAddress(device, passphrase, scriptType, derivationPath); + } + }; + } + } + public static class GetXpubService extends Service { private final Device device; private final String passphrase; @@ -406,6 +465,7 @@ public class Hwi { ENUMERATE("enumerate", true), PROMPT_PIN("promptpin", true), SEND_PIN("sendpin", false), + DISPLAY_ADDRESS("displayaddress", true), GET_XPUB("getxpub", true), SIGN_TX("signtx", true); diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java index 598d8814..6e636021 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java @@ -14,7 +14,7 @@ public class HwUsbDevicesController extends KeystoreImportDetailController { public void initializeView(List devices) { for(Device device : devices) { - DevicePane devicePane = new DevicePane(DevicePane.DeviceOperation.IMPORT, getMasterController().getWallet(), device); + DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device); deviceAccordion.getPanes().add(devicePane); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbScanController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbScanController.java index a24feab5..ecdcac92 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbScanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbScanController.java @@ -1,7 +1,10 @@ package com.sparrowwallet.sparrow.keystoreimport; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.UsbDeviceEvent; import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Hwi; +import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -33,6 +36,7 @@ public class HwUsbScanController extends KeystoreImportDetailController { } else { getMasterController().showUsbDevices(devices); } + Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); }); enumerateService.setOnFailed(workerStateEvent -> { getMasterController().showUsbError(enumerateService.getException().getMessage()); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 33f0819d..735b0d4f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; +import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.ElectrumServer; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.wallet.TransactionEntry; @@ -627,15 +628,21 @@ public class HeadersController extends TransactionFormController implements Init } private void signUsbKeystores() { - if(headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) { + if(headersForm.getPsbt().isSigned()) { return; } - if(headersForm.getPsbt().isSigned()) { + List fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList()); + List signingDevices = AppController.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList()); + if(signingDevices.isEmpty() && headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) { return; } - DeviceSignDialog dlg = new DeviceSignDialog(headersForm.getPsbt()); + if(signingDevices.isEmpty()) { + signingDevices = AppController.getDevices().stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList()); + } + + DeviceSignDialog dlg = new DeviceSignDialog(signingDevices.isEmpty() ? null : signingDevices, headersForm.getPsbt()); Optional optionalSignedPsbt = dlg.showAndWait(); if(optionalSignedPsbt.isPresent()) { PSBT signedPsbt = optionalSignedPsbt.get(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java index df2215f0..d838fcf3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java @@ -6,20 +6,28 @@ import com.google.zxing.client.j2se.MatrixToImageConfig; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.control.CopyableLabel; -import com.sparrowwallet.sparrow.control.CopyableTextField; -import com.sparrowwallet.sparrow.control.ScriptArea; +import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.ReceiveToEvent; +import com.sparrowwallet.sparrow.event.UsbDeviceEvent; import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Device; +import com.sparrowwallet.sparrow.io.Hwi; +import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.image.Image; @@ -34,8 +42,10 @@ import java.io.ByteArrayOutputStream; import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.List; import java.util.ResourceBundle; import java.util.Set; +import java.util.stream.Collectors; public class ReceiveController extends WalletFormController implements Initializable { private static final Logger log = LoggerFactory.getLogger(ReceiveController.class); @@ -63,6 +73,9 @@ public class ReceiveController extends WalletFormController implements Initializ @FXML private CodeArea outputDescriptor; + @FXML + private Button displayAddress; + private NodeEntry currentEntry; @Override @@ -73,6 +86,9 @@ public class ReceiveController extends WalletFormController implements Initializ @Override public void initializeView() { initializeScriptField(scriptPubKeyArea); + + displayAddress.managedProperty().bind(displayAddress.visibleProperty()); + displayAddress.setVisible(false); } public void setNodeEntry(NodeEntry nodeEntry) { @@ -97,6 +113,8 @@ public class ReceiveController extends WalletFormController implements Initializ outputDescriptor.clear(); outputDescriptor.appendText(nodeEntry.getOutputDescriptor()); + + updateDisplayAddress(AppController.getDevices()); } private void updateLastUsed() { @@ -115,6 +133,33 @@ public class ReceiveController extends WalletFormController implements Initializ } } + private void updateDisplayAddress(List devices) { + //Can only display address for single sig wallets. See https://github.com/bitcoin-core/HWI/issues/224 + Wallet wallet = getWalletForm().getWallet(); + if(wallet.getPolicyType().equals(PolicyType.SINGLE)) { + List addressDevices = devices.stream().filter(device -> wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).collect(Collectors.toList()); + if(addressDevices.isEmpty()) { + addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList()); + } + + if(!addressDevices.isEmpty()) { + if(currentEntry != null) { + displayAddress.setVisible(true); + } + + displayAddress.setUserData(addressDevices); + return; + } else if(currentEntry != null && wallet.getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) { + displayAddress.setVisible(true); + displayAddress.setUserData(null); + return; + } + } + + displayAddress.setVisible(false); + displayAddress.setUserData(null); + } + private Image getQrCode(String address) { try { QRCodeWriter qrCodeWriter = new QRCodeWriter(); @@ -137,6 +182,36 @@ public class ReceiveController extends WalletFormController implements Initializ setNodeEntry(freshEntry); } + @SuppressWarnings("unchecked") + public void displayAddress(ActionEvent event) { + Wallet wallet = getWalletForm().getWallet(); + if(wallet.getPolicyType() == PolicyType.SINGLE && currentEntry != null) { + Keystore keystore = wallet.getKeystores().get(0); + KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(currentEntry.getNode().getDerivation()); + + List possibleDevices = (List)displayAddress.getUserData(); + if(possibleDevices != null && !possibleDevices.isEmpty()) { + if(possibleDevices.size() > 1 || possibleDevices.get(0).getNeedsPinSent() || possibleDevices.get(0).getNeedsPassphraseSent()) { + DeviceAddressDialog dlg = new DeviceAddressDialog(possibleDevices.size() == 1 ? List.of(possibleDevices.get(0)) : null, wallet, fullDerivation); + dlg.showAndWait(); + } else { + Device actualDevice = possibleDevices.get(0); + Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), fullDerivation.getDerivationPath()); + displayAddressService.setOnFailed(failedEvent -> { + Platform.runLater(() -> { + DeviceAddressDialog dlg = new DeviceAddressDialog(null, wallet, fullDerivation); + dlg.showAndWait(); + }); + }); + displayAddressService.start(); + } + } else { + DeviceAddressDialog dlg = new DeviceAddressDialog(null, wallet, fullDerivation); + dlg.showAndWait(); + } + } + } + public void clear() { if(currentEntry != null) { label.textProperty().unbindBidirectional(currentEntry.labelProperty()); @@ -189,4 +264,9 @@ public class ReceiveController extends WalletFormController implements Initializ } } } -} + + @Subscribe + public void usbDevicesFound(UsbDeviceEvent event) { + updateDisplayAddress(event.getDevices()); + } +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml index 68f5f64f..c5562b96 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml @@ -86,11 +86,19 @@ - + + + + + \ No newline at end of file