mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46: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.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<Device> 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<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
|
||||
|
@ -628,7 +635,15 @@ public class AppController implements Initializable {
|
|||
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();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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<Device> enumerate(String passphrase) throws ImportException {
|
||||
try {
|
||||
List<String> 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<String> 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<String> getDeviceCommand(Device device, String command) throws IOException {
|
||||
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command);
|
||||
private List<String> 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<String> 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<String> 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<String> 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<String> 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<List<Device>> {
|
||||
|
@ -244,15 +282,19 @@ public class Hwi {
|
|||
protected Task<List<Device>> createTask() {
|
||||
return new Task<>() {
|
||||
protected List<Device> call() throws ImportException {
|
||||
if(!isPromptActive) {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.enumerate(passphrase);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class PromptPinService extends Service<Boolean> {
|
||||
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<Boolean> {
|
||||
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<String> {
|
||||
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<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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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<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) {
|
||||
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));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue