upgrade to hwi-2.0.0-rc.2, display addresses on usb devices for multisig wallets

This commit is contained in:
Craig Raw 2021-03-10 11:31:19 +02:00
parent 58a31f435e
commit f0c239d625
9 changed files with 128 additions and 79 deletions

2
drongo

@ -1 +1 @@
Subproject commit 533791edcf38a6ca05857483df15a5c54a4554f0 Subproject commit e974c3f4223d1771aa59013c31311cf3d85e0915

View file

@ -1,22 +1,22 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent; import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import java.util.List; import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> { public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet; private final Wallet wallet;
private final KeyDerivation keyDerivation; private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(List<String> operationFingerprints, Wallet wallet, KeyDerivation keyDerivation) { public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(operationFingerprints); super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet; this.wallet = wallet;
this.keyDerivation = keyDerivation; this.outputDescriptor = outputDescriptor;
EventManager.get().register(this); EventManager.get().register(this);
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
@ -26,7 +26,7 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
@Override @Override
protected DevicePane getDevicePane(Device device) { protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, keyDerivation, device); return new DevicePane(wallet, outputDescriptor, device);
} }
@Subscribe @Subscribe

View file

@ -101,6 +101,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
}); });
enumerateService.setOnFailed(workerStateEvent -> { enumerateService.setOnFailed(workerStateEvent -> {
deviceAccordion.getPanes().clear();
scanBox.setVisible(true); scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage()); scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
}); });

View file

@ -2,6 +2,7 @@ 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.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
@ -15,7 +16,6 @@ import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent; 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.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -25,7 +25,6 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField; import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields; import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
@ -36,6 +35,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane { public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class); private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
@ -43,6 +43,7 @@ public class DevicePane extends TitledDescriptionPane {
private final DeviceOperation deviceOperation; private final DeviceOperation deviceOperation;
private final Wallet wallet; private final Wallet wallet;
private final PSBT psbt; private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation; private final KeyDerivation keyDerivation;
private final String message; private final String message;
private final Device device; private final Device device;
@ -63,6 +64,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.IMPORT; this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null; this.keyDerivation = null;
this.message = null; this.message = null;
this.device = device; this.device = device;
@ -83,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN; this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null; this.wallet = null;
this.psbt = psbt; this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null; this.keyDerivation = null;
this.message = null; this.message = null;
this.device = device; this.device = device;
@ -98,12 +101,13 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signButton); buttonBox.getChildren().addAll(setPassphraseButton, signButton);
} }
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) { public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS; this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.keyDerivation = keyDerivation; this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null; this.message = null;
this.device = device; this.device = device;
@ -123,6 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN_MESSAGE; this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation; this.keyDerivation = keyDerivation;
this.message = message; this.message = message;
this.device = device; this.device = device;
@ -233,7 +238,8 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty()); displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false); displayAddressButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) { List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
displayAddressButton.setDisable(true); displayAddressButton.setDisable(true);
} }
} }
@ -471,7 +477,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void displayAddress() { private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath()); Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
displayAddressService.setOnSucceeded(successEvent -> { displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue(); String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address)); EventManager.get().post(new AddressDisplayedEvent(address));

View file

@ -4,6 +4,7 @@ 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.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.psbt.PSBTParseException;
@ -29,7 +30,7 @@ import java.util.zip.ZipInputStream;
public class Hwi { public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class); private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String TEMP_FILE_PREFIX = "hwi-1.2.1-"; private static final String VERSION_PREFIX = "hwi-2.0.0-rc.2";
private static boolean isPromptActive = false; private static boolean isPromptActive = false;
@ -44,6 +45,9 @@ public class Hwi {
String output = execute(command); String output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class); Device[] devices = getGson().fromJson(output, Device[].class);
if(devices == null) {
throw new ImportException("Error scanning, check devices are ready");
}
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList()); return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
} catch(IOException e) { } catch(IOException e) {
throw new ImportException(e); throw new ImportException(e);
@ -84,32 +88,40 @@ public class Hwi {
if(result.get("xpub") != null) { if(result.get("xpub") != null) {
return result.get("xpub").getAsString(); return result.get("xpub").getAsString();
} else { } else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again."); JsonElement error = result.get("error");
if(error != null) {
throw new ImportException(error.getAsString());
} else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
}
} }
} catch(IOException e) { } catch(IOException e) {
throw new ImportException(e); throw new ImportException(e);
} }
} }
public String displayAddress(Device device, String passphrase, ScriptType scriptType, String derivationPath) throws DisplayAddressException { public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
try { try {
if(!List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH).contains(scriptType)) { if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single sig types supported"); throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
} }
String type = null; String descriptor = outputDescriptor.toString().replace("sortedmulti", "multi");
System.out.println(descriptor);
String addrType = "legacy";
if(scriptType == ScriptType.P2SH_P2WPKH) { if(scriptType == ScriptType.P2SH_P2WPKH) {
type = "--sh_wpkh"; addrType = "sh_wit";
} else if(scriptType == ScriptType.P2WPKH) { } else if(scriptType == ScriptType.P2WPKH) {
type = "--wpkh"; addrType = "wit";
} }
isPromptActive = false; isPromptActive = false;
String output; String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) { if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type)); output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor, "--addr-type", addrType));
} else { } else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type)); output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor, "--addr-type", addrType));
} }
JsonObject result = JsonParser.parseString(output).getAsJsonObject(); JsonObject result = JsonParser.parseString(output).getAsJsonObject();
@ -199,7 +211,7 @@ public class Hwi {
private synchronized File getHwiExecutable(Command command) { 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(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) { if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(VERSION_PREFIX) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent() == Platform.OSX) { if(Platform.getCurrent() == Platform.OSX) {
deleteDirectory(hwiExecutable.getParentFile()); deleteDirectory(hwiExecutable.getParentFile());
} else { } else {
@ -218,8 +230,8 @@ public class Hwi {
//The check will still happen on first invocation, but will not thereafter //The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details //See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) { if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.2.1-mac-amd64-signed.zip"); InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + VERSION_PREFIX + "-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); Path tempHwiDirPath = Files.createTempDirectory(VERSION_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile(); File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit(); //tempHwiDir.deleteOnExit();
log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath()); log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath());
@ -227,16 +239,20 @@ public class Hwi {
File tempExec = null; File tempExec = null;
ZipInputStream zis = new ZipInputStream(inputStream); ZipInputStream zis = new ZipInputStream(inputStream);
ZipEntry zipEntry = zis.getNextEntry(); ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) { while(zipEntry != null) {
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable); if(zipEntry.isDirectory()) {
//newFile.deleteOnExit(); newDirectory(tempHwiDir, zipEntry, ownerExecutableWritable);
FileOutputStream fos = new FileOutputStream(newFile); } else {
ByteStreams.copy(zis, new FileOutputStream(newFile)); File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
fos.flush(); //newFile.deleteOnExit();
fos.close(); FileOutputStream fos = new FileOutputStream(newFile);
ByteStreams.copy(zis, new FileOutputStream(newFile));
fos.flush();
fos.close();
if (zipEntry.getName().equals("hwi")) { if(zipEntry.getName().equals("hwi")) {
tempExec = newFile; tempExec = newFile;
}
} }
zipEntry = zis.getNextEntry(); zipEntry = zis.getNextEntry();
@ -250,10 +266,10 @@ public class Hwi {
Path tempExecPath; Path tempExecPath;
if(platform == Platform.WINDOWS) { if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe"); inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null); tempExecPath = Files.createTempFile(VERSION_PREFIX, null);
} else { } else {
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi"); inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); tempExecPath = Files.createTempFile(VERSION_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
} }
File tempExec = tempExecPath.toFile(); File tempExec = tempExecPath.toFile();
@ -299,6 +315,20 @@ public class Hwi {
return directoryToBeDeleted.delete(); return directoryToBeDeleted.delete();
} }
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, zipEntry.getName());
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destSubDirPath = destDir.getCanonicalPath();
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}
return destDir;
}
public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException { public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath(); String destDirPath = destinationDir.getCanonicalPath();
@ -306,7 +336,7 @@ public class Hwi {
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile(); File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destFilePath = destFile.getCanonicalPath(); String destFilePath = destFile.getCanonicalPath();
if (!destFilePath.startsWith(destDirPath + File.separator)) { if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
} }
@ -324,30 +354,41 @@ public class Hwi {
private List<String> getDeviceCommand(Device device, Command command) throws IOException { private List<String> getDeviceCommand(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString())); List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
if(Network.get() != Network.MAINNET) { addChainType(elements);
elements.add(elements.size() - 1, "--testnet");
}
return elements; return elements;
} }
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException { private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString())); List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
if(Network.get() != Network.MAINNET) { addChainType(elements);
elements.add(elements.size() - 1, "--testnet");
}
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList())); elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements; return elements;
} }
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException { private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString())); List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
if(Network.get() != Network.MAINNET) { addChainType(elements);
elements.add(elements.size() - 1, "--testnet");
}
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList())); elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements; return elements;
} }
private void addChainType(List<String> elements) {
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--chain");
elements.add(elements.size() - 1, getChainName(Network.get()));
}
}
private String getChainName(Network network) {
if(network == Network.MAINNET) {
return "main";
} else if(network == Network.TESTNET) {
return "test";
}
return network.toString();
}
public static class EnumerateService extends Service<List<Device>> { public static class EnumerateService extends Service<List<Device>> {
private final String passphrase; private final String passphrase;
@ -430,13 +471,13 @@ public class Hwi {
private final Device device; private final Device device;
private final String passphrase; private final String passphrase;
private final ScriptType scriptType; private final ScriptType scriptType;
private final String derivationPath; private final OutputDescriptor outputDescriptor;
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, String derivationPath) { public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
this.device = device; this.device = device;
this.passphrase = passphrase; this.passphrase = passphrase;
this.scriptType = scriptType; this.scriptType = scriptType;
this.derivationPath = derivationPath; this.outputDescriptor = outputDescriptor;
} }
@Override @Override
@ -444,7 +485,7 @@ public class Hwi {
return new Task<>() { return new Task<>() {
protected String call() throws DisplayAddressException { protected String call() throws DisplayAddressException {
Hwi hwi = new Hwi(); Hwi hwi = new Hwi();
return hwi.displayAddress(device, passphrase, scriptType, derivationPath); return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
} }
}; };
} }

View file

@ -9,7 +9,7 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
@ -17,10 +17,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.ReceiveToEvent; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.io.Hwi;
@ -153,26 +150,26 @@ public class ReceiveController extends WalletFormController implements Initializ
} }
private void updateDisplayAddress(List<Device> devices) { private void updateDisplayAddress(List<Device> devices) {
//Can only display address for single sig wallets. See https://github.com/bitcoin-core/HWI/issues/224
Wallet wallet = getWalletForm().getWallet(); Wallet wallet = getWalletForm().getWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) { OutputDescriptor walletDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
List<Device> addressDevices = devices.stream().filter(device -> wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).collect(Collectors.toList()); List<String> walletFingerprints = walletDescriptor.getExtendedPublicKeys().stream().map(extKey -> walletDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(addressDevices.isEmpty()) {
addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList());
}
if(!addressDevices.isEmpty()) { List<Device> addressDevices = devices.stream().filter(device -> walletFingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
if(currentEntry != null) { if(addressDevices.isEmpty()) {
displayAddress.setVisible(true); addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList());
} }
displayAddress.setUserData(addressDevices); if(!addressDevices.isEmpty()) {
return; if(currentEntry != null) {
} else if(currentEntry != null && wallet.getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
displayAddress.setVisible(true); displayAddress.setVisible(true);
displayAddress.setUserData(null);
return;
} }
displayAddress.setUserData(addressDevices);
return;
} else if(currentEntry != null && wallet.getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
displayAddress.setVisible(true);
displayAddress.setUserData(null);
return;
} }
displayAddress.setVisible(false); displayAddress.setVisible(false);
@ -204,28 +201,27 @@ public class ReceiveController extends WalletFormController implements Initializ
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void displayAddress(ActionEvent event) { public void displayAddress(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet(); Wallet wallet = getWalletForm().getWallet();
if(wallet.getPolicyType() == PolicyType.SINGLE && currentEntry != null) { if(currentEntry != null) {
Keystore keystore = wallet.getKeystores().get(0); OutputDescriptor addressDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), currentEntry.getNode().getKeyPurpose(), currentEntry.getNode().getIndex());
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(currentEntry.getNode().getDerivation());
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData(); List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) { if(possibleDevices != null && !possibleDevices.isEmpty()) {
if(possibleDevices.size() > 1 || possibleDevices.get(0).getNeedsPinSent() || possibleDevices.get(0).getNeedsPassphraseSent()) { if(possibleDevices.size() > 1 || possibleDevices.get(0).getNeedsPinSent() || possibleDevices.get(0).getNeedsPassphraseSent()) {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation); DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait(); dlg.showAndWait();
} else { } else {
Device actualDevice = possibleDevices.get(0); Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), fullDerivation.getDerivationPath()); Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
displayAddressService.setOnFailed(failedEvent -> { displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> { Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation); DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait(); dlg.showAndWait();
}); });
}); });
displayAddressService.start(); displayAddressService.start();
} }
} else { } else {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation); DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait(); dlg.showAndWait();
} }
} }
@ -261,6 +257,11 @@ public class ReceiveController extends WalletFormController implements Initializ
return duplicateGlyph; return duplicateGlyph;
} }
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
displayAddress.setUserData(null);
}
@Subscribe @Subscribe
public void receiveTo(ReceiveToEvent event) { public void receiveTo(ReceiveToEvent event) {
if(event.getReceiveEntry().getWallet().equals(getWalletForm().getWallet())) { if(event.getReceiveEntry().getWallet().equals(getWalletForm().getWallet())) {