mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-01-26 02:11:10 +00:00
replace hwi with lark
This commit is contained in:
parent
fb0fd013d9
commit
0e9d97c221
13 changed files with 199 additions and 549 deletions
|
@ -59,6 +59,7 @@ java {
|
|||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo'))
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
|
@ -585,6 +586,14 @@ extraJavaModuleInfo {
|
|||
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('com.sparrowwallet:hid4java', 'org.hid4java') {
|
||||
requires('com.sun.jna')
|
||||
exports('org.hid4java')
|
||||
exports('org.hid4java.jna')
|
||||
}
|
||||
module('com.sparrowwallet:usb4java', 'org.usb4java') {
|
||||
exports('org.usb4java')
|
||||
}
|
||||
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
||||
exports('com.jcraft.jzlib')
|
||||
}
|
||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit 7b9affb3de635de998ae11f542fdd4ebb9ba2b53
|
||||
Subproject commit 89a6b1296e75508eae498f0199928cff0b8a660c
|
2
lark
2
lark
|
@ -1 +1 @@
|
|||
Subproject commit 6108d1dbd1d6d6057a8c14fc1539ae07c273ea9a
|
||||
Subproject commit 9576ffbbeeab87777be42add91855a0221ae55c1
|
|
@ -778,7 +778,7 @@ public class DevicePane extends TitledDescriptionPane {
|
|||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt, OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
|
@ -820,7 +820,8 @@ public class DevicePane extends TitledDescriptionPane {
|
|||
}
|
||||
|
||||
private void displayAddress() {
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
|
||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
|
||||
displayAddressService.setOnSucceeded(successEvent -> {
|
||||
String address = displayAddressService.getValue();
|
||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.lark.HardwareClient;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -122,4 +123,18 @@ public class Device {
|
|||
public int hashCode() {
|
||||
return Objects.hash(type, path);
|
||||
}
|
||||
|
||||
public static Device fromHardwareClient(HardwareClient hardwareClient) {
|
||||
Device device = new Device();
|
||||
device.type = hardwareClient.getType();
|
||||
device.path = hardwareClient.getPath();
|
||||
device.model = hardwareClient.getModel();
|
||||
device.needsPinSent = hardwareClient.needsPinSent();
|
||||
device.needsPassphraseSent = hardwareClient.needsPassphraseSent();
|
||||
device.fingerprint = hardwareClient.fingerprint();
|
||||
device.card = hardwareClient.card();
|
||||
device.warnings = hardwareClient.warnings();
|
||||
device.error = hardwareClient.error();
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,43 @@
|
|||
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.Network;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.lark.DeviceException;
|
||||
import com.sparrowwallet.lark.Lark;
|
||||
import com.sparrowwallet.lark.bitbox02.BitBoxFileNoiseConfig;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CardNotPresentException;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String HWI_HOME_DIR = "hwi";
|
||||
private static final String HWI_VERSION_PREFIX = "hwi-";
|
||||
private static final String HWI_VERSION = "3.1.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
static {
|
||||
//deleteHwiDir();
|
||||
}
|
||||
|
||||
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||
List<Device> devices = new ArrayList<>();
|
||||
devices.addAll(enumerateUsb(passphrase));
|
||||
|
@ -51,45 +46,13 @@ public class Hwi {
|
|||
}
|
||||
|
||||
private List<Device> enumerateUsb(String passphrase) throws ImportException {
|
||||
String output = null;
|
||||
try {
|
||||
List<String> command;
|
||||
if(passphrase != null) {
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString()));
|
||||
} else {
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()));
|
||||
}
|
||||
|
||||
addChainType(command, true);
|
||||
|
||||
Lark lark = getLark(passphrase);
|
||||
isPromptActive = true;
|
||||
output = execute(command);
|
||||
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||
if(devices == null) {
|
||||
throw new ImportException("Error scanning, check devices are ready");
|
||||
}
|
||||
//Restore previous (pre v2.2.0) behaviour for Trezor One - don't default to an empty passphrase if one is not supplied
|
||||
Arrays.stream(devices).filter(device -> device.containsWarning("Using default passphrase of the empty string")).forEach(device -> {
|
||||
device.setFingerprint(null);
|
||||
device.setNeedsPassphraseSent(true);
|
||||
device.setError("Passphrase needs to be specified before the fingerprint information can be retrieved");
|
||||
});
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
log.error("Error executing " + HWI_VERSION_DIR, e);
|
||||
throw new ImportException("Error executing HWI", e);
|
||||
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
|
||||
} catch(Exception e) {
|
||||
if(output != null) {
|
||||
try {
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
JsonElement error = result.get("error");
|
||||
throw new ImportException(error.getAsString());
|
||||
} catch(Exception ex) {
|
||||
log.error("Error parsing JSON: " + output, e);
|
||||
throw new ImportException("Error scanning" + (output.isEmpty() ? ", check devices are ready" : ": " + output), e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
log.error("Error enumerating USB devices", e);
|
||||
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
|
@ -124,31 +87,43 @@ public class Hwi {
|
|||
|
||||
public boolean promptPin(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.promptPin(device.getType(), device.getPath());
|
||||
isPromptActive = true;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error prompting pin", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendPin(Device device, String pin) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.sendPin(device.getType(), device.getPath(), pin);
|
||||
isPromptActive = false;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error sending pin", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean togglePassphrase(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.TOGGLE_PASSPHRASE));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.togglePassphrase(device.getType(), device.getPath());
|
||||
isPromptActive = false;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error toggling passphrase", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,59 +138,33 @@ public class Hwi {
|
|||
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
|
||||
}
|
||||
|
||||
Lark lark = getLark(passphrase);
|
||||
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
|
||||
isPromptActive = false;
|
||||
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);
|
||||
return xpub.toString();
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error retrieving xpub", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
|
||||
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
|
||||
try {
|
||||
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
|
||||
}
|
||||
|
||||
String descriptor = outputDescriptor.toString();
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("address") != null) {
|
||||
return result.get("address").getAsString();
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new DisplayAddressException(error.getAsString());
|
||||
} else {
|
||||
throw new DisplayAddressException("Could not retrieve address");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new DisplayAddressException(e);
|
||||
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
|
||||
return lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
|
||||
} catch(DeviceException e) {
|
||||
throw new DisplayAddressException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error displaying address", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
|
@ -224,406 +173,73 @@ public class Hwi {
|
|||
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
|
||||
try {
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("signature") != null) {
|
||||
return result.get("signature").getAsString();
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new SignMessageException(error.getAsString());
|
||||
} else {
|
||||
throw new SignMessageException("Could not sign message");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new SignMessageException(e);
|
||||
Lark lark = getLark(passphrase);
|
||||
return lark.signMessage(device.getType(), device.getPath(), message, derivationPath);
|
||||
} catch(DeviceException e) {
|
||||
throw new SignMessageException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error signing message", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
|
||||
public PSBT signPSBT(Device device, String passphrase, PSBT psbt,
|
||||
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws SignTransactionException {
|
||||
try {
|
||||
String psbtBase64 = psbt.toBase64String();
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_TX), 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 && error.getAsString().equals("open failed")) {
|
||||
throw new SignTransactionException("Please reconnect your USB device");
|
||||
} else if(error != null) {
|
||||
throw new SignTransactionException(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 parse signed PSBT" + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
|
||||
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
|
||||
return lark.signTransaction(device.getType(), device.getPath(), psbt);
|
||||
} catch(DeviceException e) {
|
||||
throw new SignTransactionException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error signing PSBT", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private String execute(List<String> command) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
process = processBuilder.start();
|
||||
return getProcessOutput(process);
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
private Lark getLark() {
|
||||
return getLark(null);
|
||||
}
|
||||
|
||||
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
List<String> processArguments = new ArrayList<>(arguments);
|
||||
private Lark getLark(String passphrase) {
|
||||
return getLark(passphrase, null, null, null);
|
||||
}
|
||||
|
||||
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
|
||||
if(useStdin) {
|
||||
processArguments.add("--stdin");
|
||||
private Lark getLark(String passphrase, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
Lark lark = new Lark(AppServices.getHttpClientService());
|
||||
lark.setBitBoxNoiseConfig(new BitBoxFxNoiseConfig());
|
||||
if(passphrase != null) {
|
||||
lark.setPassphrase(passphrase);
|
||||
}
|
||||
|
||||
if(walletDescriptor != null && walletName != null) {
|
||||
if(walletRegistration != null) {
|
||||
lark.addWalletRegistration(walletDescriptor, walletName, walletRegistration);
|
||||
} else {
|
||||
processArguments.add(command.toString());
|
||||
processArguments.addAll(Arrays.asList(commandArguments));
|
||||
lark.addWalletName(walletDescriptor, walletName);
|
||||
}
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||
process = processBuilder.start();
|
||||
|
||||
if(useStdin) {
|
||||
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
writer.write(command.toString());
|
||||
for(String commandArgument : commandArguments) {
|
||||
writer.write(" \"");
|
||||
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
|
||||
writer.write("\"");
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
return getProcessOutput(process);
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
|
||||
return lark;
|
||||
}
|
||||
|
||||
private String getProcessOutput(Process process) throws IOException {
|
||||
String output;
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
output = CharStreams.toString(reader);
|
||||
}
|
||||
|
||||
if(output.isEmpty() && process.getErrorStream() != null) {
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
|
||||
String errorOutput = CharStreams.toString(reader);
|
||||
if(!errorOutput.isEmpty()) {
|
||||
throw new IOException(errorOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private synchronized File getHwiExecutable(Command command) {
|
||||
File hwiExecutable = Config.get().getHwi();
|
||||
if(hwiExecutable != null && hwiExecutable.exists()) {
|
||||
File homeDir = getHwiHomeDir();
|
||||
String tmpDir = System.getProperty("java.io.tmpdir");
|
||||
String hwiPath = hwiExecutable.getAbsolutePath();
|
||||
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
IOUtils.deleteDirectory(hwiExecutable.getParentFile());
|
||||
} else {
|
||||
hwiExecutable.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hwiExecutable == null || !hwiExecutable.exists()) {
|
||||
try {
|
||||
OsType osType = OsType.getCurrent();
|
||||
String osArch = System.getProperty("os.arch");
|
||||
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
|
||||
|
||||
//A PyInstaller --onefile expands into a new directory on every run triggering OSX Gatekeeper checks.
|
||||
//To avoid doing these with every invocation, use a --onedir packaging and expand into a temp folder on OSX
|
||||
//The check will still happen on first invocation, but will not thereafter
|
||||
//See https://github.com/bitcoin-core/HWI/issues/327 for details
|
||||
if(osType == OsType.MACOS) {
|
||||
InputStream inputStream;
|
||||
if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
|
||||
File hwiHomeDir = getHwiHomeDir();
|
||||
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
|
||||
IOUtils.deleteDirectory(hwiVersionDir);
|
||||
if(!hwiVersionDir.exists()) {
|
||||
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
|
||||
|
||||
File hwiExec = null;
|
||||
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
|
||||
TarArchiveEntry tarEntry = tarInput.getNextEntry();
|
||||
while(tarEntry != null) {
|
||||
if(tarEntry.isDirectory()) {
|
||||
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else if(tarEntry.isSymbolicLink()) {
|
||||
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
try(FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
if(tarEntry.getName().equals("hwi")) {
|
||||
hwiExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
tarEntry = tarInput.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
hwiExecutable = hwiExec;
|
||||
} else {
|
||||
InputStream inputStream;
|
||||
Path tempExecPath;
|
||||
if(osType == OsType.WINDOWS) {
|
||||
Files.createDirectories(getHwiHomeDir().toPath());
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/aarch64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
|
||||
File tempExec = tempExecPath.toFile();
|
||||
tempExec.deleteOnExit();
|
||||
try(OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec))) {
|
||||
ByteStreams.copy(inputStream, tempExecStream);
|
||||
inputStream.close();
|
||||
tempExecStream.flush();
|
||||
};
|
||||
|
||||
hwiExecutable = tempExec;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error initializing HWI", e);
|
||||
}
|
||||
|
||||
Config.get().setHwi(hwiExecutable);
|
||||
}
|
||||
|
||||
return hwiExecutable;
|
||||
}
|
||||
|
||||
private File getHwiHomeDir() {
|
||||
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
}
|
||||
|
||||
return new File(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
|
||||
private boolean testHwi(File hwiExecutable) {
|
||||
private static void deleteHwiDir() {
|
||||
try {
|
||||
List<String> command = List.of(hwiExecutable.getAbsolutePath(), "--version");
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
Process process = processBuilder.start();
|
||||
int exitValue = process.waitFor();
|
||||
return exitValue == 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.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: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
}
|
||||
|
||||
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
}
|
||||
|
||||
private boolean wasSuccessful(String output) throws ImportException {
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("error") != null) {
|
||||
throw new ImportException(result.get("error").getAsString());
|
||||
}
|
||||
|
||||
return result.get("success").getAsBoolean();
|
||||
}
|
||||
|
||||
private void deleteExtractionOnFailure(Process process, long after) {
|
||||
try {
|
||||
if(OsType.getCurrent() != OsType.MACOS && process != null && process.waitFor(100, TimeUnit.MILLISECONDS) && process.exitValue() != 0) {
|
||||
File extraction = getTemporaryExtraction(after);
|
||||
if(extraction != null) {
|
||||
IOUtils.deleteDirectory(extraction);
|
||||
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
File hwiHomeDir = new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
if(hwiHomeDir.exists()) {
|
||||
IOUtils.deleteDirectory(hwiHomeDir);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error deleting temporary extraction", e);
|
||||
log.error("Error deleting hwi directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getTemporaryExtraction(long after) {
|
||||
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
|
||||
if(!tmpDir.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File[] tmps = tmpDir.listFiles(file -> {
|
||||
if(!file.isDirectory() || file.lastModified() < after) {
|
||||
return false;
|
||||
}
|
||||
String name = file.getName();
|
||||
if(name.length() < 9 || !name.startsWith("_MEI")) {
|
||||
return false;
|
||||
}
|
||||
File hwilib = new File(file, "hwilib");
|
||||
return hwilib.exists();
|
||||
});
|
||||
|
||||
return tmps == null || tmps.length == 0 ? null : Arrays.stream(tmps).sorted(Comparator.comparingLong(File::lastModified)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
|
||||
addChainType(elements, false);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
|
||||
addChainType(elements, false);
|
||||
return elements;
|
||||
}
|
||||
|
||||
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()));
|
||||
addChainType(elements, true);
|
||||
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()));
|
||||
addChainType(elements, true);
|
||||
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", escape(passphrase), command.toString()));
|
||||
addChainType(elements, true);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private void addChainType(List<String> elements, boolean commandPresent) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.getCanonical()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getChainName(Network network) {
|
||||
if(network == Network.MAINNET) {
|
||||
return "main";
|
||||
} else if(network == Network.TESTNET) {
|
||||
return "test";
|
||||
}
|
||||
|
||||
return network.toString();
|
||||
}
|
||||
|
||||
private String escape(String passphrase) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.WINDOWS) {
|
||||
return passphrase.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public static class EnumerateService extends Service<List<Device>> {
|
||||
private final String passphrase;
|
||||
|
||||
|
@ -724,13 +340,29 @@ public class Hwi {
|
|||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final ScriptType scriptType;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final OutputDescriptor addressDescriptor;
|
||||
private final OutputDescriptor walletDescriptor;
|
||||
private final String walletName;
|
||||
private final byte[] walletRegistration;
|
||||
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.scriptType = scriptType;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.addressDescriptor = addressDescriptor;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = null;
|
||||
}
|
||||
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.scriptType = scriptType;
|
||||
this.addressDescriptor = addressDescriptor;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = walletRegistration;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -738,7 +370,7 @@ public class Hwi {
|
|||
return new Task<>() {
|
||||
protected String call() throws DisplayAddressException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
|
||||
return hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -816,11 +448,26 @@ public class Hwi {
|
|||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor walletDescriptor;
|
||||
private final String walletName;
|
||||
private final byte[] walletRegistration;
|
||||
|
||||
public SignPSBTService(Device device, String passphrase, PSBT psbt) {
|
||||
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.psbt = psbt;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = null;
|
||||
}
|
||||
|
||||
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.psbt = psbt;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = walletRegistration;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -828,72 +475,48 @@ public class Hwi {
|
|||
return new Task<>() {
|
||||
protected PSBT call() throws SignTransactionException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.signPSBT(device, passphrase, psbt);
|
||||
return hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Gson getGson() {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
|
||||
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
|
||||
return gsonBuilder.create();
|
||||
}
|
||||
|
||||
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
|
||||
@Override
|
||||
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString().toLowerCase(Locale.ROOT));
|
||||
private static final class BitBoxFxNoiseConfig extends BitBoxFileNoiseConfig {
|
||||
public BitBoxFxNoiseConfig() {
|
||||
super(Path.of(Storage.getSparrowDir().getAbsolutePath(), "lark", "bitbox02.json").toFile());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
|
||||
@Override
|
||||
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
String modelStr = json.getAsJsonPrimitive().getAsString();
|
||||
try {
|
||||
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
|
||||
} catch(Exception e) {
|
||||
for(WalletModel model : WalletModel.values()) {
|
||||
if(modelStr.startsWith(model.getType())) {
|
||||
return model;
|
||||
}
|
||||
public boolean showPairing(String code, DeviceResponse response) throws DeviceException {
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
AtomicBoolean confirmedDevice = new AtomicBoolean(false);
|
||||
AtomicBoolean confirmedApp = new AtomicBoolean(false);
|
||||
|
||||
Thread showPairingDeviceThread = new Thread(() -> {
|
||||
try {
|
||||
confirmedDevice.set(response.call());
|
||||
latch.countDown();
|
||||
} catch(DeviceException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
showPairingDeviceThread.start();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Optional<ButtonType> optConfirm = AppServices.showAlertDialog("Confirm Pairing", "Confirm the following code is shown on BitBox02:\n\n" + code, Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optConfirm.isPresent() && optConfirm.get() == ButtonType.YES) {
|
||||
confirmedApp.set(true);
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
throw new IllegalArgumentException("Could not determine wallet model for " + modelStr);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Command {
|
||||
ENUMERATE("enumerate", true),
|
||||
PROMPT_PIN("promptpin", true),
|
||||
SEND_PIN("sendpin", false),
|
||||
TOGGLE_PASSPHRASE("togglepassphrase", true),
|
||||
DISPLAY_ADDRESS("displayaddress", true),
|
||||
SIGN_MESSAGE("signmessage", true),
|
||||
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;
|
||||
return confirmedDevice.get() && confirmedApp.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,7 +233,8 @@ public class ReceiveController extends WalletFormController implements Initializ
|
|||
dlg.showAndWait();
|
||||
} else {
|
||||
Device actualDevice = possibleDevices.get(0);
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor,
|
||||
OutputDescriptor.getOutputDescriptor(walletForm.getWallet()), walletForm.getWallet().getFullName());
|
||||
displayAddressService.setOnFailed(failedEvent -> {
|
||||
Platform.runLater(() -> {
|
||||
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
|
||||
|
|
|
@ -65,4 +65,5 @@ open module com.sparrowwallet.sparrow {
|
|||
requires java.smartcardio;
|
||||
requires com.jcraft.jzlib;
|
||||
requires com.sparrowwallet.tern;
|
||||
requires com.sparrowwallet.lark;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue