|
|
|
@@ -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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|