mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
display address on device, device support improvements
This commit is contained in:
parent
8aed206a34
commit
84d08fea18
18 changed files with 530 additions and 150 deletions
|
@ -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()));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scan() {
|
@Override
|
||||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
protected DevicePane getDevicePane(Device device) {
|
||||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
return new DevicePane(psbt, device);
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
<Button fx:id="nextAddress" graphicTextGap="5" text="Get Next Address" defaultButton="true" AnchorPane.rightAnchor="10" onAction="#getNewAddress">
|
<HBox AnchorPane.rightAnchor="10">
|
||||||
<graphic>
|
<Button fx:id="displayAddress" graphicTextGap="5" text="Display Address" AnchorPane.rightAnchor="10" onAction="#displayAddress">
|
||||||
<Glyph fontFamily="FontAwesome" icon="ARROW_DOWN" fontSize="12" />
|
<graphic>
|
||||||
</graphic>
|
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="12" icon="USB" />
|
||||||
</Button>
|
</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">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="FontAwesome" icon="ARROW_DOWN" fontSize="12" />
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
</bottom>
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
Loading…
Reference in a new issue