wallet tx usb signing

This commit is contained in:
Craig Raw 2020-07-26 14:03:24 +02:00
parent ef5124a9b9
commit 1c03e1935e
9 changed files with 374 additions and 30 deletions

2
drongo

@ -1 +1 @@
Subproject commit 0466755883c19a9e679f5e937b2f7db55a73e05b Subproject commit 15beeefcb687c77adddbfe5e2b74c75b2c6f2196

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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