From 1c03e1935edbe380ee5d2df2f89df7e9783321df Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sun, 26 Jul 2020 14:03:24 +0200 Subject: [PATCH] wallet tx usb signing --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 19 ++- .../sparrow/control/DevicePane.java | 64 +++++++- .../sparrow/control/DeviceSignDialog.java | 127 ++++++++++++++++ .../sparrow/event/PSBTSignedEvent.java | 16 ++ .../com/sparrowwallet/sparrow/io/Hwi.java | 137 +++++++++++++++--- .../sparrow/io/SignTransactionException.java | 19 +++ .../transaction/HeadersController.java | 15 ++ .../sparrow/wallet/SendController.java | 5 +- 9 files changed, 374 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/PSBTSignedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/SignTransactionException.java diff --git a/drongo b/drongo index 04667558..15beeefc 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 0466755883c19a9e679f5e937b2f7db55a73e05b +Subproject commit 15beeefcb687c77adddbfe5e2b74c75b2c6f2196 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index b2f599d2..9e680ef2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -48,6 +48,7 @@ import java.io.*; import java.net.URL; import java.text.ParseException; import java.util.*; +import java.util.stream.Collectors; public class AppController implements Initializable { private static final int SERVER_PING_PERIOD = 10 * 1000; @@ -107,6 +108,8 @@ public class AppController implements Initializable { private static CurrencyRate fiatCurrencyExchangeRate; + private static List devices; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -409,6 +412,10 @@ public class AppController implements Initializable { return fiatCurrencyExchangeRate; } + public static List getDevices() { + return devices; + } + public Map getOpenWallets() { Map openWallets = new LinkedHashMap<>(); @@ -628,7 +635,15 @@ public class AppController implements Initializable { enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); enumerateService.setOnSucceeded(workerStateEvent -> { List 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 validDevices = devices.stream().filter(device -> device.getModel() != null).collect(Collectors.toList()); + if(validDevices.size() == devices.size()) { + EventManager.get().post(new UsbDeviceEvent(devices)); + } + } }); enumerateService.start(); } @@ -836,6 +851,8 @@ public class AppController implements Initializable { @Subscribe public void usbDevicesFound(UsbDeviceEvent event) { + devices = Collections.unmodifiableList(event.getDevices()); + UsbStatusButton usbStatus = null; for(Node node : statusBar.getRightItems()) { if(node instanceof UsbStatusButton) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index b1982858..342b6491 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -3,11 +3,13 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.drongo.wallet.WalletModel; @@ -32,6 +34,7 @@ import java.util.List; public class DevicePane extends TitledDescriptionPane { private final DeviceOperation deviceOperation; private final Wallet wallet; + private final PSBT psbt; private final Device device; private CustomPasswordField pinField; @@ -39,6 +42,7 @@ public class DevicePane extends TitledDescriptionPane { private Button enterPinButton; private Button setPassphraseButton; private SplitMenuButton importButton; + private Button signButton; private final SimpleStringProperty passphrase = new SimpleStringProperty(""); @@ -46,6 +50,7 @@ public class DevicePane extends TitledDescriptionPane { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); this.deviceOperation = deviceOperation; this.wallet = wallet; + this.psbt = null; this.device = device; setDefaultStatus(); @@ -65,6 +70,30 @@ public class DevicePane extends TitledDescriptionPane { 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 protected Control createButton() { createUnlockButton(); @@ -100,6 +129,7 @@ public class DevicePane extends TitledDescriptionPane { private void createImportButton() { importButton = new SplitMenuButton(); + importButton.getStyleClass().add("default-button"); importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setText("Import Keystore"); importButton.setOnAction(event -> { @@ -120,6 +150,18 @@ public class DevicePane extends TitledDescriptionPane { 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) { if(device.getModel().equals(WalletModel.TREZOR_1)) { promptPin(); @@ -227,7 +269,7 @@ public class DevicePane extends TitledDescriptionPane { } } else { setError("Incorrect PIN", null); - enterPinButton.setDisable(false); + unlockButton.setDisable(false); if(pinField != null) { pinField.setText(""); } @@ -315,14 +357,28 @@ public class DevicePane extends TitledDescriptionPane { 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() { if(deviceOperation.equals(DeviceOperation.IMPORT)) { importButton.setVisible(true); showHideLink.setText("Show derivation..."); showHideLink.setVisible(true); setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation())); - } else { - //TODO: Support further device operations such as signing + } else if(deviceOperation.equals(DeviceOperation.SIGN)) { + signButton.setVisible(true); + showHideLink.setVisible(false); } } @@ -363,6 +419,6 @@ public class DevicePane extends TitledDescriptionPane { } public enum DeviceOperation { - IMPORT; + IMPORT, SIGN; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java new file mode 100644 index 00000000..c93c2d03 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java @@ -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 { + 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 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 devices = enumerateService.getValue(); + setDevices(devices); + }); + enumerateService.setOnFailed(workerStateEvent -> { + scanBox.setVisible(true); + scanLabel.setText(workerStateEvent.getSource().getException().getMessage()); + }); + enumerateService.start(); + } + + private void setDevices(List devices) { + deviceAccordion.getPanes().clear(); + + if(devices.isEmpty()) { + scanBox.setVisible(true); + scanLabel.setText("No devices found"); + } else { + scanBox.setVisible(false); + for(Device device : devices) { + DevicePane devicePane = new DevicePane(DevicePane.DeviceOperation.SIGN, psbt, device); + deviceAccordion.getPanes().add(devicePane); + } + } + } + + @Subscribe + public void psbtSigned(PSBTSignedEvent event) { + if(psbt == event.getPsbt()) { + setResult(event.getSignedPsbt()); + this.close(); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/PSBTSignedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/PSBTSignedEvent.java new file mode 100644 index 00000000..b0fc2440 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/PSBTSignedEvent.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index 60f13cd3..741544f6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.io; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.gson.*; +import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.WalletModel; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; @@ -24,13 +26,15 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class Hwi { + private static boolean isPromptActive = false; + public List enumerate(String passphrase) throws ImportException { try { List command; if(passphrase != null) { - command = List.of(getHwiExecutable().getAbsolutePath(), "--password", passphrase, "enumerate"); + command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString()); } else { - command = List.of(getHwiExecutable().getAbsolutePath(), "enumerate"); + command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()); } String output = execute(command); @@ -43,7 +47,8 @@ public class Hwi { public boolean promptPin(Device device) throws ImportException { try { - String output = execute(getDeviceCommand(device, "promptpin")); + String output = execute(getDeviceCommand(device, Command.PROMPT_PIN)); + isPromptActive = true; return wasSuccessful(output); } catch(IOException e) { throw new ImportException(e); @@ -52,7 +57,7 @@ public class Hwi { public boolean sendPin(Device device, String pin) throws ImportException { try { - String output = execute(getDeviceCommand(device, "sendpin", pin)); + String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin)); return wasSuccessful(output); } catch(IOException e) { throw new ImportException(e); @@ -63,9 +68,9 @@ public class Hwi { try { String output; 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 { - output = execute(getDeviceCommand(device, "getxpub", derivationPath)); + output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath)); } 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 command) throws IOException { + isPromptActive = false; ProcessBuilder processBuilder = new ProcessBuilder(command); Process process = processBuilder.start(); 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(); if(hwiExecutable != null && hwiExecutable.exists()) { - if(!testHwi(hwiExecutable)) { + if(command.isTestFirst() && !testHwi(hwiExecutable)) { if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) { deleteDirectory(hwiExecutable.getParentFile()); } else { @@ -203,16 +241,16 @@ public class Hwi { return result.get("success").getAsBoolean(); } - private List getDeviceCommand(Device device, String command) throws IOException { - return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command); + private List getDeviceCommand(Device device, Command command) throws IOException { + return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()); } - private List getDeviceCommand(Device device, String command, String data) throws IOException { - return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command, data); + private List getDeviceCommand(Device device, Command command, String data) throws IOException { + return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString(), data); } - private List getDeviceCommand(Device device, String passphrase, String command, String data) throws IOException { - return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command, data); + private List getDeviceCommand(Device device, String passphrase, Command command, String data) throws IOException { + return List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString(), data); } public static class EnumerateService extends Service> { @@ -244,15 +282,19 @@ public class Hwi { protected Task> createTask() { return new Task<>() { protected List call() throws ImportException { - Hwi hwi = new Hwi(); - return hwi.enumerate(passphrase); + if(!isPromptActive) { + Hwi hwi = new Hwi(); + return hwi.enumerate(passphrase); + } + + return null; } }; } } public static class PromptPinService extends Service { - private Device device; + private final Device device; public PromptPinService(Device device) { this.device = device; @@ -270,8 +312,8 @@ public class Hwi { } public static class SendPinService extends Service { - private Device device; - private String pin; + private final Device device; + private final String pin; public SendPinService(Device device, String pin) { this.device = device; @@ -290,9 +332,9 @@ public class Hwi { } public static class GetXpubService extends Service { - private Device device; - private String passphrase; - private String derivationPath; + private final Device device; + private final String passphrase; + private final String derivationPath; public GetXpubService(Device device, String passphrase, String derivationPath) { this.device = device; @@ -311,6 +353,28 @@ public class Hwi { } } + public static class SignPSBTService extends Service { + 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 createTask() { + return new Task<>() { + protected PSBT call() throws SignTransactionException { + Hwi hwi = new Hwi(); + return hwi.signPSBT(device, passphrase, psbt); + } + }; + } + } + public Gson getGson() { GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer()); @@ -331,4 +395,33 @@ public class Hwi { 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; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SignTransactionException.java b/src/main/java/com/sparrowwallet/sparrow/io/SignTransactionException.java new file mode 100644 index 00000000..fa7d0b96 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/SignTransactionException.java @@ -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); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 583a00c1..a6a1d6d3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -484,6 +484,7 @@ public class HeadersController extends TransactionFormController implements Init public void signPSBT(ActionEvent event) { signSoftwareKeystores(); + signUsbKeystores(); } 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 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) { Map> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt()); Optional> optSignedKeystores = signedKeystoresMap.values().stream().filter(list -> !list.isEmpty()).min(Comparator.comparingInt(List::size)); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index c0411b76..24fa83f8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -272,7 +272,7 @@ public class SendController extends WalletFormController implements Initializabl createButton.setDisable(walletTransaction == null || label.getText().isEmpty()); }); - address.setText("32YSPMaUePf511u5adEckiNq8QLec9ksXX"); + address.setText("19Sp9dLinHy3dKo2Xxj53ouuZWAoVGGhg8"); addValidation(); } @@ -309,9 +309,10 @@ public class SendController extends WalletFormController implements Initializabl if(recipientAmount != null && recipientAmount > recipientDustThreshold && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) { Wallet wallet = getWalletForm().getWallet(); Long userFee = userFeeSet.get() ? getFeeValueSats() : null; + Integer currentBlockHeight = AppController.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); 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); insufficientInputsProperty.set(false);