display address on device, device support improvements

This commit is contained in:
Craig Raw 2020-08-07 14:56:01 +02:00
parent 8aed206a34
commit 84d08fea18
18 changed files with 530 additions and 150 deletions

View file

@ -113,6 +113,8 @@ public class AppController implements Initializable {
private ElectrumServer.ConnectionService connectionService; private ElectrumServer.ConnectionService connectionService;
private Hwi.ScheduledEnumerateService deviceEnumerateService;
private static Integer currentBlockHeight; private static Integer currentBlockHeight;
public static boolean showTxHexProperty; public static boolean showTxHexProperty;
@ -291,6 +293,25 @@ public class AppController implements Initializable {
return ratesService; return ratesService;
} }
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> 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<Device> 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) { public void setApplication(MainApp application) {
this.application = application; this.application = application;
} }
@ -509,7 +530,7 @@ public class AppController implements Initializable {
} }
public static List<Device> getDevices() { public static List<Device> getDevices() {
return devices; return devices == null ? new ArrayList<>() : devices;
} }
public Map<Wallet, Storage> getOpenWallets() { public Map<Wallet, Storage> getOpenWallets() {
@ -739,24 +760,6 @@ public class AppController implements Initializable {
EventManager.get().register(walletForm); EventManager.get().register(walletForm);
controller.setWalletForm(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<Device> 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<Device> 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); tabs.getTabs().add(tab);
return tab; return tab;
} catch(IOException e) { } catch(IOException e) {
@ -1080,6 +1083,36 @@ public class AppController implements Initializable {
fiatCurrencyExchangeRate = event.getCurrencyRate(); fiatCurrencyExchangeRate = event.getCurrencyRate();
} }
@Subscribe
public void openWallets(OpenWalletsEvent event) {
boolean usbWallet = false;
for(Map.Entry<Wallet, Storage> 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 @Subscribe
public void requestOpenWallets(RequestOpenWalletsEvent event) { public void requestOpenWallets(RequestOpenWalletsEvent event) {
EventManager.get().post(new OpenWalletsEvent(getOpenWallets())); EventManager.get().post(new OpenWalletsEvent(getOpenWallets()));

View file

@ -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<String> {
private final Wallet wallet;
private final KeyDerivation keyDerivation;
public DeviceAddressDialog(List<Device> 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();
}
}

View file

@ -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<R> extends Dialog<R> {
private final List<Device> operationDevices;
private final Accordion deviceAccordion;
private final VBox scanBox;
private final Label scanLabel;
public DeviceDialog() {
this(null);
}
public DeviceDialog(List<Device> 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<Device> 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<Device> 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<Device> devices) {
List<Device> 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);
}

View file

@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
@ -35,6 +36,7 @@ public class DevicePane extends TitledDescriptionPane {
private final DeviceOperation deviceOperation; private final DeviceOperation deviceOperation;
private final Wallet wallet; private final Wallet wallet;
private final PSBT psbt; private final PSBT psbt;
private final KeyDerivation keyDerivation;
private final Device device; private final Device device;
private CustomPasswordField pinField; private CustomPasswordField pinField;
@ -43,14 +45,16 @@ public class DevicePane extends TitledDescriptionPane {
private Button setPassphraseButton; private Button setPassphraseButton;
private SplitMenuButton importButton; private SplitMenuButton importButton;
private Button signButton; private Button signButton;
private Button displayAddressButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty(""); 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"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = deviceOperation; this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.keyDerivation = null;
this.device = device; this.device = device;
setDefaultStatus(); setDefaultStatus();
@ -70,11 +74,12 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, importButton); 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"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = deviceOperation; this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null; this.wallet = null;
this.psbt = psbt; this.psbt = psbt;
this.keyDerivation = null;
this.device = device; this.device = device;
setDefaultStatus(); setDefaultStatus();
@ -94,6 +99,31 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signButton); 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 @Override
protected Control createButton() { protected Control createButton() {
createUnlockButton(); createUnlockButton();
@ -153,6 +183,7 @@ public class DevicePane extends TitledDescriptionPane {
signButton = new Button("Sign"); signButton = new Button("Sign");
signButton.setDefaultButton(true); signButton.setDefaultButton(true);
signButton.setAlignment(Pos.CENTER_RIGHT); signButton.setAlignment(Pos.CENTER_RIGHT);
signButton.setMinWidth(44);
signButton.setOnAction(event -> { signButton.setOnAction(event -> {
signButton.setDisable(true); signButton.setDisable(true);
sign(); sign();
@ -161,6 +192,22 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setVisible(false); 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) { private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) { if(device.getModel().equals(WalletModel.TREZOR_1)) {
promptPin(); promptPin();
@ -375,6 +422,20 @@ public class DevicePane extends TitledDescriptionPane {
signPSBTService.start(); 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() { private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) { if(deviceOperation.equals(DeviceOperation.IMPORT)) {
importButton.setVisible(true); importButton.setVisible(true);
@ -384,6 +445,9 @@ public class DevicePane extends TitledDescriptionPane {
} else if(deviceOperation.equals(DeviceOperation.SIGN)) { } else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setVisible(true); signButton.setVisible(true);
showHideLink.setVisible(false); 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 { public enum DeviceOperation {
IMPORT, SIGN; IMPORT, SIGN, DISPLAY_ADDRESS;
} }
} }

View file

@ -2,124 +2,31 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device; 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; import java.util.List;
public class DeviceSignDialog extends Dialog<PSBT> { public class DeviceSignDialog extends DeviceDialog<PSBT> {
private final PSBT psbt; private final PSBT psbt;
private final Accordion deviceAccordion;
private final VBox scanBox;
private final Label scanLabel;
public DeviceSignDialog(PSBT psbt) { public DeviceSignDialog(List<Device> devices, PSBT psbt) {
super(devices);
this.psbt = psbt; this.psbt = psbt;
EventManager.get().register(this); EventManager.get().register(this);
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt);
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<Device> 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); @Override
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); protected DevicePane getDevicePane(Device device) {
dialogPane.getButtonTypes().addAll(rescanButtonType, cancelButtonType); return new DevicePane(psbt, device);
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);
}
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
setDevices(devices);
});
enumerateService.setOnFailed(workerStateEvent -> {
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
enumerateService.start();
}
private void setDevices(List<Device> 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);
}
}
} }
@Subscribe @Subscribe
public void psbtSigned(PSBTSignedEvent event) { public void psbtSigned(PSBTSignedEvent event) {
if(psbt == event.getPsbt()) { if(psbt == event.getPsbt()) {
EventManager.get().unregister(this);
setResult(event.getSignedPsbt()); setResult(event.getSignedPsbt());
this.close(); this.close();
} }

View file

@ -47,8 +47,6 @@ public class QRDisplayDialog extends Dialog<UR> {
this.ur = ur; this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0); this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
EventManager.get().register(this);
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane(); StackPane stackPane = new StackPane();

View file

@ -64,6 +64,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
@Subscribe @Subscribe
public void walletExported(WalletExportEvent event) { public void walletExported(WalletExportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet(); wallet = event.getWallet();
setResult(wallet); setResult(wallet);
this.close(); this.close();

View file

@ -51,6 +51,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
@Subscribe @Subscribe
public void walletImported(WalletImportEvent event) { public void walletImported(WalletImportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet(); wallet = event.getWallet();
setResult(wallet); setResult(wallet);
this.close(); this.close();

View file

@ -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;
}
}

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.sparrow.io.Device;
import java.util.List; import java.util.List;
public class UsbDeviceEvent { public class UsbDeviceEvent {
private List<Device> devices; private final List<Device> devices;
public UsbDeviceEvent(List<Device> devices) { public UsbDeviceEvent(List<Device> devices) {
this.devices = devices; this.devices = devices;

View file

@ -2,6 +2,8 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import java.util.Objects;
public class Device { public class Device {
private String type; private String type;
private String path; private String path;
@ -61,4 +63,22 @@ public class Device {
public String toString() { public String toString() {
return getModel() + ":" + getPath(); 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);
}
} }

View file

@ -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);
}
}

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams; import com.google.common.io.CharStreams;
import com.google.gson.*; import com.google.gson.*;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
@ -20,9 +21,8 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.PosixFilePermissions;
import java.util.Arrays; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import java.util.Set;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; 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 { public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
try { try {
String psbtBase64 = psbt.toBase64String(); 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()); return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString());
} }
private List<String> getDeviceCommand(Device device, Command command, String data) throws IOException { private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString(), data); List<String> 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<String> getDeviceCommand(Device device, String passphrase, Command command, String data) throws IOException { private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString(), data); List<String> 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<List<Device>> { public static class EnumerateService extends Service<List<Device>> {
@ -337,6 +372,30 @@ public class Hwi {
} }
} }
public static class DisplayAddressService extends Service<String> {
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<String> 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<String> { public static class GetXpubService extends Service<String> {
private final Device device; private final Device device;
private final String passphrase; private final String passphrase;
@ -406,6 +465,7 @@ public class Hwi {
ENUMERATE("enumerate", true), ENUMERATE("enumerate", true),
PROMPT_PIN("promptpin", true), PROMPT_PIN("promptpin", true),
SEND_PIN("sendpin", false), SEND_PIN("sendpin", false),
DISPLAY_ADDRESS("displayaddress", true),
GET_XPUB("getxpub", true), GET_XPUB("getxpub", true),
SIGN_TX("signtx", true); SIGN_TX("signtx", true);

View file

@ -14,7 +14,7 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
public void initializeView(List<Device> devices) { public void initializeView(List<Device> devices) {
for(Device device : 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); deviceAccordion.getPanes().add(devicePane);
} }
} }

View file

@ -1,7 +1,10 @@
package com.sparrowwallet.sparrow.keystoreimport; 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.Device;
import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.io.Hwi;
import javafx.application.Platform;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -33,6 +36,7 @@ public class HwUsbScanController extends KeystoreImportDetailController {
} else { } else {
getMasterController().showUsbDevices(devices); getMasterController().showUsbDevices(devices);
} }
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
}); });
enumerateService.setOnFailed(workerStateEvent -> { enumerateService.setOnFailed(workerStateEvent -> {
getMasterController().showUsbError(enumerateService.getException().getMessage()); getMasterController().showUsbError(enumerateService.getException().getMessage());

View file

@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.ElectrumServer; import com.sparrowwallet.sparrow.io.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.TransactionEntry; import com.sparrowwallet.sparrow.wallet.TransactionEntry;
@ -627,15 +628,21 @@ public class HeadersController extends TransactionFormController implements Init
} }
private void signUsbKeystores() { private void signUsbKeystores() {
if(headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
return;
}
if(headersForm.getPsbt().isSigned()) { if(headersForm.getPsbt().isSigned()) {
return; return;
} }
DeviceSignDialog dlg = new DeviceSignDialog(headersForm.getPsbt()); List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList());
List<Device> 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;
}
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<PSBT> optionalSignedPsbt = dlg.showAndWait(); Optional<PSBT> optionalSignedPsbt = dlg.showAndWait();
if(optionalSignedPsbt.isPresent()) { if(optionalSignedPsbt.isPresent()) {
PSBT signedPsbt = optionalSignedPsbt.get(); PSBT signedPsbt = optionalSignedPsbt.get();

View file

@ -6,20 +6,28 @@ import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; 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.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.control.CopyableTextField;
import com.sparrowwallet.sparrow.control.ScriptArea;
import com.sparrowwallet.sparrow.event.ReceiveToEvent; import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent; import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; 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.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.image.Image; import javafx.scene.image.Image;
@ -34,8 +42,10 @@ import java.io.ByteArrayOutputStream;
import java.net.URL; import java.net.URL;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
public class ReceiveController extends WalletFormController implements Initializable { public class ReceiveController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(ReceiveController.class); private static final Logger log = LoggerFactory.getLogger(ReceiveController.class);
@ -63,6 +73,9 @@ public class ReceiveController extends WalletFormController implements Initializ
@FXML @FXML
private CodeArea outputDescriptor; private CodeArea outputDescriptor;
@FXML
private Button displayAddress;
private NodeEntry currentEntry; private NodeEntry currentEntry;
@Override @Override
@ -73,6 +86,9 @@ public class ReceiveController extends WalletFormController implements Initializ
@Override @Override
public void initializeView() { public void initializeView() {
initializeScriptField(scriptPubKeyArea); initializeScriptField(scriptPubKeyArea);
displayAddress.managedProperty().bind(displayAddress.visibleProperty());
displayAddress.setVisible(false);
} }
public void setNodeEntry(NodeEntry nodeEntry) { public void setNodeEntry(NodeEntry nodeEntry) {
@ -97,6 +113,8 @@ public class ReceiveController extends WalletFormController implements Initializ
outputDescriptor.clear(); outputDescriptor.clear();
outputDescriptor.appendText(nodeEntry.getOutputDescriptor()); outputDescriptor.appendText(nodeEntry.getOutputDescriptor());
updateDisplayAddress(AppController.getDevices());
} }
private void updateLastUsed() { private void updateLastUsed() {
@ -115,6 +133,33 @@ public class ReceiveController extends WalletFormController implements Initializ
} }
} }
private void updateDisplayAddress(List<Device> 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<Device> 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) { private Image getQrCode(String address) {
try { try {
QRCodeWriter qrCodeWriter = new QRCodeWriter(); QRCodeWriter qrCodeWriter = new QRCodeWriter();
@ -137,6 +182,36 @@ public class ReceiveController extends WalletFormController implements Initializ
setNodeEntry(freshEntry); 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<Device> possibleDevices = (List<Device>)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() { public void clear() {
if(currentEntry != null) { if(currentEntry != null) {
label.textProperty().unbindBidirectional(currentEntry.labelProperty()); 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());
}
} }

View file

@ -86,11 +86,19 @@
<padding> <padding>
<Insets left="25.0" right="25.0" bottom="25.0" /> <Insets left="25.0" right="25.0" bottom="25.0" />
</padding> </padding>
<HBox AnchorPane.rightAnchor="10">
<Button fx:id="displayAddress" graphicTextGap="5" text="Display Address" AnchorPane.rightAnchor="10" onAction="#displayAddress">
<graphic>
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="12" icon="USB" />
</graphic>
</Button>
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
<Button fx:id="nextAddress" graphicTextGap="5" text="Get Next Address" defaultButton="true" AnchorPane.rightAnchor="10" onAction="#getNewAddress"> <Button fx:id="nextAddress" graphicTextGap="5" text="Get Next Address" defaultButton="true" AnchorPane.rightAnchor="10" onAction="#getNewAddress">
<graphic> <graphic>
<Glyph fontFamily="FontAwesome" icon="ARROW_DOWN" fontSize="12" /> <Glyph fontFamily="FontAwesome" icon="ARROW_DOWN" fontSize="12" />
</graphic> </graphic>
</Button> </Button>
</HBox>
</AnchorPane> </AnchorPane>
</bottom> </bottom>
</BorderPane> </BorderPane>