mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
wallet tx usb signing
This commit is contained in:
parent
ef5124a9b9
commit
1c03e1935e
9 changed files with 374 additions and 30 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 0466755883c19a9e679f5e937b2f7db55a73e05b
|
Subproject commit 15beeefcb687c77adddbfe5e2b74c75b2c6f2196
|
|
@ -48,6 +48,7 @@ import java.io.*;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class AppController implements Initializable {
|
public class AppController implements Initializable {
|
||||||
private static final int SERVER_PING_PERIOD = 10 * 1000;
|
private static final int SERVER_PING_PERIOD = 10 * 1000;
|
||||||
|
@ -107,6 +108,8 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||||
|
|
||||||
|
private static List<Device> devices;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -409,6 +412,10 @@ public class AppController implements Initializable {
|
||||||
return fiatCurrencyExchangeRate;
|
return fiatCurrencyExchangeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Device> getDevices() {
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<Wallet, Storage> getOpenWallets() {
|
public Map<Wallet, Storage> getOpenWallets() {
|
||||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
@ -628,7 +635,15 @@ public class AppController implements Initializable {
|
||||||
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
|
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
|
||||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
List<Device> devices = enumerateService.getValue();
|
List<Device> devices = enumerateService.getValue();
|
||||||
EventManager.get().post(new UsbDeviceEvent(devices));
|
|
||||||
|
//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();
|
enumerateService.start();
|
||||||
}
|
}
|
||||||
|
@ -836,6 +851,8 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void usbDevicesFound(UsbDeviceEvent event) {
|
public void usbDevicesFound(UsbDeviceEvent event) {
|
||||||
|
devices = Collections.unmodifiableList(event.getDevices());
|
||||||
|
|
||||||
UsbStatusButton usbStatus = null;
|
UsbStatusButton usbStatus = null;
|
||||||
for(Node node : statusBar.getRightItems()) {
|
for(Node node : statusBar.getRightItems()) {
|
||||||
if(node instanceof UsbStatusButton) {
|
if(node instanceof UsbStatusButton) {
|
||||||
|
|
|
@ -3,11 +3,13 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
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.KeystoreImportEvent;
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||||
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 com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
@ -32,6 +34,7 @@ import java.util.List;
|
||||||
public class DevicePane extends TitledDescriptionPane {
|
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 Device device;
|
private final Device device;
|
||||||
|
|
||||||
private CustomPasswordField pinField;
|
private CustomPasswordField pinField;
|
||||||
|
@ -39,6 +42,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private Button enterPinButton;
|
private Button enterPinButton;
|
||||||
private Button setPassphraseButton;
|
private Button setPassphraseButton;
|
||||||
private SplitMenuButton importButton;
|
private SplitMenuButton importButton;
|
||||||
|
private Button signButton;
|
||||||
|
|
||||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
@ -46,6 +50,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = deviceOperation;
|
this.deviceOperation = deviceOperation;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
|
this.psbt = null;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
|
|
||||||
setDefaultStatus();
|
setDefaultStatus();
|
||||||
|
@ -65,6 +70,30 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DevicePane(DeviceOperation deviceOperation, PSBT psbt, Device device) {
|
||||||
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
|
this.deviceOperation = deviceOperation;
|
||||||
|
this.wallet = null;
|
||||||
|
this.psbt = psbt;
|
||||||
|
this.device = device;
|
||||||
|
|
||||||
|
setDefaultStatus();
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
|
||||||
|
createSetPassphraseButton();
|
||||||
|
createSignButton();
|
||||||
|
|
||||||
|
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, signButton);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Control createButton() {
|
protected Control createButton() {
|
||||||
createUnlockButton();
|
createUnlockButton();
|
||||||
|
@ -100,6 +129,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
private void createImportButton() {
|
private void createImportButton() {
|
||||||
importButton = new SplitMenuButton();
|
importButton = new SplitMenuButton();
|
||||||
|
importButton.getStyleClass().add("default-button");
|
||||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
importButton.setText("Import Keystore");
|
importButton.setText("Import Keystore");
|
||||||
importButton.setOnAction(event -> {
|
importButton.setOnAction(event -> {
|
||||||
|
@ -120,6 +150,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
importButton.setVisible(false);
|
importButton.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createSignButton() {
|
||||||
|
signButton = new Button("Sign");
|
||||||
|
signButton.setDefaultButton(true);
|
||||||
|
signButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
signButton.setOnAction(event -> {
|
||||||
|
signButton.setDisable(true);
|
||||||
|
sign();
|
||||||
|
});
|
||||||
|
signButton.managedProperty().bind(signButton.visibleProperty());
|
||||||
|
signButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
@ -227,7 +269,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError("Incorrect PIN", null);
|
setError("Incorrect PIN", null);
|
||||||
enterPinButton.setDisable(false);
|
unlockButton.setDisable(false);
|
||||||
if(pinField != null) {
|
if(pinField != null) {
|
||||||
pinField.setText("");
|
pinField.setText("");
|
||||||
}
|
}
|
||||||
|
@ -315,14 +357,28 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
getXpubService.start();
|
getXpubService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sign() {
|
||||||
|
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||||
|
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
PSBT signedPsbt = signPSBTService.getValue();
|
||||||
|
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||||
|
});
|
||||||
|
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||||
|
setError(signPSBTService.getException().getMessage(), null);
|
||||||
|
signButton.setDisable(false);
|
||||||
|
});
|
||||||
|
signPSBTService.start();
|
||||||
|
}
|
||||||
|
|
||||||
private void showOperationButton() {
|
private void showOperationButton() {
|
||||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||||
importButton.setVisible(true);
|
importButton.setVisible(true);
|
||||||
showHideLink.setText("Show derivation...");
|
showHideLink.setText("Show derivation...");
|
||||||
showHideLink.setVisible(true);
|
showHideLink.setVisible(true);
|
||||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||||
} else {
|
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||||
//TODO: Support further device operations such as signing
|
signButton.setVisible(true);
|
||||||
|
showHideLink.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +419,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DeviceOperation {
|
public enum DeviceOperation {
|
||||||
IMPORT;
|
IMPORT, SIGN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
import com.sparrowwallet.sparrow.io.Hwi;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceSignDialog extends Dialog<PSBT> {
|
||||||
|
private final PSBT psbt;
|
||||||
|
private final Accordion deviceAccordion;
|
||||||
|
private final VBox scanBox;
|
||||||
|
private final Label scanLabel;
|
||||||
|
|
||||||
|
public DeviceSignDialog(PSBT psbt) {
|
||||||
|
this.psbt = psbt;
|
||||||
|
|
||||||
|
EventManager.get().register(this);
|
||||||
|
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
|
AnchorPane anchorPane = new AnchorPane();
|
||||||
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
|
scrollPane.setPrefHeight(280);
|
||||||
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
anchorPane.getChildren().add(scrollPane);
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||||
|
|
||||||
|
deviceAccordion = new Accordion();
|
||||||
|
scrollPane.setContent(deviceAccordion);
|
||||||
|
|
||||||
|
scanBox = new VBox();
|
||||||
|
scanBox.setSpacing(30);
|
||||||
|
scanBox.setAlignment(Pos.CENTER);
|
||||||
|
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||||
|
usb.setFontSize(50);
|
||||||
|
scanLabel = new Label("Connect Hardware Wallet");
|
||||||
|
Button button = new Button("Scan...");
|
||||||
|
button.setPrefSize(120, 60);
|
||||||
|
button.setOnAction(event -> {
|
||||||
|
scan();
|
||||||
|
});
|
||||||
|
scanBox.getChildren().addAll(usb, scanLabel, button);
|
||||||
|
scanBox.managedProperty().bind(scanBox.visibleProperty());
|
||||||
|
|
||||||
|
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||||
|
|
||||||
|
List<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() {
|
||||||
|
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
|
||||||
|
public void psbtSigned(PSBTSignedEvent event) {
|
||||||
|
if(psbt == event.getPsbt()) {
|
||||||
|
setResult(event.getSignedPsbt());
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
|
||||||
|
public class PSBTSignedEvent extends PSBTEvent {
|
||||||
|
private final PSBT signedPsbt;
|
||||||
|
|
||||||
|
public PSBTSignedEvent(PSBT psbt, PSBT signedPsbt) {
|
||||||
|
super(psbt);
|
||||||
|
this.signedPsbt = signedPsbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PSBT getSignedPsbt() {
|
||||||
|
return signedPsbt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ 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.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
|
@ -24,13 +26,15 @@ import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
public class Hwi {
|
public class Hwi {
|
||||||
|
private static boolean isPromptActive = false;
|
||||||
|
|
||||||
public List<Device> enumerate(String passphrase) throws ImportException {
|
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||||
try {
|
try {
|
||||||
List<String> command;
|
List<String> command;
|
||||||
if(passphrase != null) {
|
if(passphrase != null) {
|
||||||
command = List.of(getHwiExecutable().getAbsolutePath(), "--password", passphrase, "enumerate");
|
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
|
||||||
} else {
|
} else {
|
||||||
command = List.of(getHwiExecutable().getAbsolutePath(), "enumerate");
|
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
String output = execute(command);
|
String output = execute(command);
|
||||||
|
@ -43,7 +47,8 @@ public class Hwi {
|
||||||
|
|
||||||
public boolean promptPin(Device device) throws ImportException {
|
public boolean promptPin(Device device) throws ImportException {
|
||||||
try {
|
try {
|
||||||
String output = execute(getDeviceCommand(device, "promptpin"));
|
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
|
||||||
|
isPromptActive = true;
|
||||||
return wasSuccessful(output);
|
return wasSuccessful(output);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
throw new ImportException(e);
|
throw new ImportException(e);
|
||||||
|
@ -52,7 +57,7 @@ public class Hwi {
|
||||||
|
|
||||||
public boolean sendPin(Device device, String pin) throws ImportException {
|
public boolean sendPin(Device device, String pin) throws ImportException {
|
||||||
try {
|
try {
|
||||||
String output = execute(getDeviceCommand(device, "sendpin", pin));
|
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
|
||||||
return wasSuccessful(output);
|
return wasSuccessful(output);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
throw new ImportException(e);
|
throw new ImportException(e);
|
||||||
|
@ -63,9 +68,9 @@ public class Hwi {
|
||||||
try {
|
try {
|
||||||
String output;
|
String output;
|
||||||
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
|
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
|
||||||
output = execute(getDeviceCommand(device, passphrase, "getxpub", derivationPath));
|
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
|
||||||
} else {
|
} else {
|
||||||
output = execute(getDeviceCommand(device, "getxpub", derivationPath));
|
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||||
|
@ -79,16 +84,49 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
|
||||||
|
try {
|
||||||
|
String psbtBase64 = psbt.toBase64String();
|
||||||
|
|
||||||
|
String output;
|
||||||
|
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
|
||||||
|
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_TX, psbtBase64));
|
||||||
|
} else {
|
||||||
|
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||||
|
if(result.get("psbt") != null) {
|
||||||
|
String strPsbt = result.get("psbt").getAsString();
|
||||||
|
return PSBT.fromString(strPsbt);
|
||||||
|
} else {
|
||||||
|
JsonElement error = result.get("error");
|
||||||
|
if(error != null && error.getAsString().equals("sign_tx canceled")) {
|
||||||
|
throw new SignTransactionException("Signing cancelled");
|
||||||
|
} else if(error != null) {
|
||||||
|
throw new SignTransactionException("Error: " + error.getAsString());
|
||||||
|
} else {
|
||||||
|
throw new SignTransactionException("Could not retrieve PSBT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new SignTransactionException("Could not sign PSBT", e);
|
||||||
|
} catch(PSBTParseException e) {
|
||||||
|
throw new SignTransactionException("Could not parsed signed PSBT", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String execute(List<String> command) throws IOException {
|
private String execute(List<String> command) throws IOException {
|
||||||
|
isPromptActive = false;
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
Process process = processBuilder.start();
|
Process process = processBuilder.start();
|
||||||
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized File getHwiExecutable() {
|
private synchronized File getHwiExecutable(Command command) {
|
||||||
File hwiExecutable = Config.get().getHwi();
|
File hwiExecutable = Config.get().getHwi();
|
||||||
if(hwiExecutable != null && hwiExecutable.exists()) {
|
if(hwiExecutable != null && hwiExecutable.exists()) {
|
||||||
if(!testHwi(hwiExecutable)) {
|
if(command.isTestFirst() && !testHwi(hwiExecutable)) {
|
||||||
if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) {
|
if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) {
|
||||||
deleteDirectory(hwiExecutable.getParentFile());
|
deleteDirectory(hwiExecutable.getParentFile());
|
||||||
} else {
|
} else {
|
||||||
|
@ -203,16 +241,16 @@ public class Hwi {
|
||||||
return result.get("success").getAsBoolean();
|
return result.get("success").getAsBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getDeviceCommand(Device device, String command) throws IOException {
|
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
|
||||||
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command);
|
return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getDeviceCommand(Device device, String command, String data) throws IOException {
|
private List<String> getDeviceCommand(Device device, Command command, String data) throws IOException {
|
||||||
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command, data);
|
return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getDeviceCommand(Device device, String passphrase, String command, String data) throws IOException {
|
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String data) throws IOException {
|
||||||
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command, data);
|
return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EnumerateService extends Service<List<Device>> {
|
public static class EnumerateService extends Service<List<Device>> {
|
||||||
|
@ -244,15 +282,19 @@ public class Hwi {
|
||||||
protected Task<List<Device>> createTask() {
|
protected Task<List<Device>> createTask() {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected List<Device> call() throws ImportException {
|
protected List<Device> call() throws ImportException {
|
||||||
Hwi hwi = new Hwi();
|
if(!isPromptActive) {
|
||||||
return hwi.enumerate(passphrase);
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.enumerate(passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PromptPinService extends Service<Boolean> {
|
public static class PromptPinService extends Service<Boolean> {
|
||||||
private Device device;
|
private final Device device;
|
||||||
|
|
||||||
public PromptPinService(Device device) {
|
public PromptPinService(Device device) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
|
@ -270,8 +312,8 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SendPinService extends Service<Boolean> {
|
public static class SendPinService extends Service<Boolean> {
|
||||||
private Device device;
|
private final Device device;
|
||||||
private String pin;
|
private final String pin;
|
||||||
|
|
||||||
public SendPinService(Device device, String pin) {
|
public SendPinService(Device device, String pin) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
|
@ -290,9 +332,9 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GetXpubService extends Service<String> {
|
public static class GetXpubService extends Service<String> {
|
||||||
private Device device;
|
private final Device device;
|
||||||
private String passphrase;
|
private final String passphrase;
|
||||||
private String derivationPath;
|
private final String derivationPath;
|
||||||
|
|
||||||
public GetXpubService(Device device, String passphrase, String derivationPath) {
|
public GetXpubService(Device device, String passphrase, String derivationPath) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
|
@ -311,6 +353,28 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class SignPSBTService extends Service<PSBT> {
|
||||||
|
private final Device device;
|
||||||
|
private final String passphrase;
|
||||||
|
private final PSBT psbt;
|
||||||
|
|
||||||
|
public SignPSBTService(Device device, String passphrase, PSBT psbt) {
|
||||||
|
this.device = device;
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
this.psbt = psbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<PSBT> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected PSBT call() throws SignTransactionException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.signPSBT(device, passphrase, psbt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Gson getGson() {
|
public Gson getGson() {
|
||||||
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
|
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
|
||||||
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
|
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
|
||||||
|
@ -331,4 +395,33 @@ public class Hwi {
|
||||||
return WalletModel.valueOf(json.getAsJsonPrimitive().getAsString().toUpperCase());
|
return WalletModel.valueOf(json.getAsJsonPrimitive().getAsString().toUpperCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum Command {
|
||||||
|
ENUMERATE("enumerate", true),
|
||||||
|
PROMPT_PIN("promptpin", true),
|
||||||
|
SEND_PIN("sendpin", false),
|
||||||
|
GET_XPUB("getxpub", true),
|
||||||
|
SIGN_TX("signtx", true);
|
||||||
|
|
||||||
|
private final String command;
|
||||||
|
private final boolean testFirst;
|
||||||
|
|
||||||
|
Command(String command, boolean testFirst) {
|
||||||
|
this.command = command;
|
||||||
|
this.testFirst = testFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCommand() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTestFirst() {
|
||||||
|
return testFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
public class SignTransactionException extends Exception {
|
||||||
|
public SignTransactionException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignTransactionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignTransactionException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignTransactionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -484,6 +484,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
public void signPSBT(ActionEvent event) {
|
public void signPSBT(ActionEvent event) {
|
||||||
signSoftwareKeystores();
|
signSoftwareKeystores();
|
||||||
|
signUsbKeystores();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void signSoftwareKeystores() {
|
private void signSoftwareKeystores() {
|
||||||
|
@ -525,6 +526,20 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void signUsbKeystores() {
|
||||||
|
if(headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceSignDialog dlg = new DeviceSignDialog(headersForm.getPsbt());
|
||||||
|
Optional<PSBT> optionalSignedPsbt = dlg.showAndWait();
|
||||||
|
if(optionalSignedPsbt.isPresent()) {
|
||||||
|
PSBT signedPsbt = optionalSignedPsbt.get();
|
||||||
|
headersForm.getPsbt().combine(signedPsbt);
|
||||||
|
EventManager.get().post(new PSBTCombinedEvent(headersForm.getPsbt()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSignedKeystores(Wallet signingWallet) {
|
private void updateSignedKeystores(Wallet signingWallet) {
|
||||||
Map<PSBTInput, List<Keystore>> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt());
|
Map<PSBTInput, List<Keystore>> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt());
|
||||||
Optional<List<Keystore>> optSignedKeystores = signedKeystoresMap.values().stream().filter(list -> !list.isEmpty()).min(Comparator.comparingInt(List::size));
|
Optional<List<Keystore>> optSignedKeystores = signedKeystoresMap.values().stream().filter(list -> !list.isEmpty()).min(Comparator.comparingInt(List::size));
|
||||||
|
|
|
@ -272,7 +272,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
createButton.setDisable(walletTransaction == null || label.getText().isEmpty());
|
createButton.setDisable(walletTransaction == null || label.getText().isEmpty());
|
||||||
});
|
});
|
||||||
|
|
||||||
address.setText("32YSPMaUePf511u5adEckiNq8QLec9ksXX");
|
address.setText("19Sp9dLinHy3dKo2Xxj53ouuZWAoVGGhg8");
|
||||||
|
|
||||||
addValidation();
|
addValidation();
|
||||||
}
|
}
|
||||||
|
@ -309,9 +309,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
if(recipientAmount != null && recipientAmount > recipientDustThreshold && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) {
|
if(recipientAmount != null && recipientAmount > recipientDustThreshold && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) {
|
||||||
Wallet wallet = getWalletForm().getWallet();
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||||
|
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
||||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
boolean includeMempoolChange = Config.get().isIncludeMempoolChange();
|
boolean includeMempoolChange = Config.get().isIncludeMempoolChange();
|
||||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate(), getMinimumFeeRate(), userFee, sendAll, groupByAddress, includeMempoolChange);
|
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, sendAll, groupByAddress, includeMempoolChange);
|
||||||
walletTransactionProperty.setValue(walletTransaction);
|
walletTransactionProperty.setValue(walletTransaction);
|
||||||
insufficientInputsProperty.set(false);
|
insufficientInputsProperty.set(false);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue