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

View file

@ -101,6 +101,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
deviceAccordion.getPanes().clear();
scanBox.setVisible(true);
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.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
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.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
@ -25,7 +25,6 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@ -36,6 +35,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
@ -43,6 +43,7 @@ public class DevicePane extends TitledDescriptionPane {
private final DeviceOperation deviceOperation;
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final Device device;
@ -63,6 +64,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
@ -83,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
@ -98,12 +101,13 @@ public class DevicePane extends TitledDescriptionPane {
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");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.device = device;
@ -123,6 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.device = device;
@ -233,7 +238,8 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
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);
}
}
@ -471,7 +477,7 @@ public class DevicePane extends TitledDescriptionPane {
}
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 -> {
String address = displayAddressService.getValue();
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.gson.*;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
@ -29,7 +30,7 @@ import java.util.zip.ZipInputStream;
public class Hwi {
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;
@ -44,6 +45,9 @@ public class Hwi {
String output = execute(command);
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());
} catch(IOException e) {
throw new ImportException(e);
@ -83,33 +87,41 @@ public class Hwi {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
} else {
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) {
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 {
if(!List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single sig types supported");
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
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) {
type = "--sh_wpkh";
addrType = "sh_wit";
} else if(scriptType == ScriptType.P2WPKH) {
type = "--wpkh";
addrType = "wit";
}
isPromptActive = false;
String output;
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 {
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();
@ -199,7 +211,7 @@ public class Hwi {
private synchronized File getHwiExecutable(Command command) {
File hwiExecutable = Config.get().getHwi();
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) {
deleteDirectory(hwiExecutable.getParentFile());
} else {
@ -218,8 +230,8 @@ public class Hwi {
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.2.1-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + VERSION_PREFIX + "-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(VERSION_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit();
log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath());
@ -227,7 +239,10 @@ public class Hwi {
File tempExec = null;
ZipInputStream zis = new ZipInputStream(inputStream);
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
while(zipEntry != null) {
if(zipEntry.isDirectory()) {
newDirectory(tempHwiDir, zipEntry, ownerExecutableWritable);
} else {
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
//newFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream(newFile);
@ -235,9 +250,10 @@ public class Hwi {
fos.flush();
fos.close();
if (zipEntry.getName().equals("hwi")) {
if(zipEntry.getName().equals("hwi")) {
tempExec = newFile;
}
}
zipEntry = zis.getNextEntry();
}
@ -250,10 +266,10 @@ public class Hwi {
Path tempExecPath;
if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
tempExecPath = Files.createTempFile(VERSION_PREFIX, null);
} else {
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();
@ -299,6 +315,20 @@ public class Hwi {
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 {
String destDirPath = destinationDir.getCanonicalPath();
@ -306,7 +336,7 @@ public class Hwi {
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
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());
}
@ -324,30 +354,41 @@ public class Hwi {
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()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
return elements;
}
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()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
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()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
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>> {
private final String passphrase;
@ -430,13 +471,13 @@ public class Hwi {
private final Device device;
private final String passphrase;
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.passphrase = passphrase;
this.scriptType = scriptType;
this.derivationPath = derivationPath;
this.outputDescriptor = outputDescriptor;
}
@Override
@ -444,7 +485,7 @@ public class Hwi {
return new Task<>() {
protected String call() throws DisplayAddressException {
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.sparrowwallet.drongo.KeyDerivation;
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.Keystore;
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.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
@ -153,10 +150,11 @@ public class ReceiveController extends WalletFormController implements Initializ
}
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();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
List<Device> addressDevices = devices.stream().filter(device -> wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).collect(Collectors.toList());
OutputDescriptor walletDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
List<String> walletFingerprints = walletDescriptor.getExtendedPublicKeys().stream().map(extKey -> walletDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
List<Device> addressDevices = devices.stream().filter(device -> walletFingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
if(addressDevices.isEmpty()) {
addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList());
}
@ -173,7 +171,6 @@ public class ReceiveController extends WalletFormController implements Initializ
displayAddress.setUserData(null);
return;
}
}
displayAddress.setVisible(false);
displayAddress.setUserData(null);
@ -204,28 +201,27 @@ public class ReceiveController extends WalletFormController implements Initializ
@SuppressWarnings("unchecked")
public void displayAddress(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet();
if(wallet.getPolicyType() == PolicyType.SINGLE && currentEntry != null) {
Keystore keystore = wallet.getKeystores().get(0);
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(currentEntry.getNode().getDerivation());
if(currentEntry != null) {
OutputDescriptor addressDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), currentEntry.getNode().getKeyPurpose(), currentEntry.getNode().getIndex());
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) {
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();
} else {
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 -> {
Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
});
});
displayAddressService.start();
}
} else {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
}
}
@ -261,6 +257,11 @@ public class ReceiveController extends WalletFormController implements Initializ
return duplicateGlyph;
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
displayAddress.setUserData(null);
}
@Subscribe
public void receiveTo(ReceiveToEvent event) {
if(event.getReceiveEntry().getWallet().equals(getWalletForm().getWallet())) {