mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add support for UR registry types crypto-address, crypto-hdkey, crypto-output and crypto-account
This commit is contained in:
parent
87fe0accd5
commit
68da8e6027
8 changed files with 309 additions and 36 deletions
|
@ -51,7 +51,7 @@ dependencies {
|
||||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('com.sparrowwallet:hummingbird:1.2')
|
implementation('com.sparrowwallet:hummingbird:1.3')
|
||||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||||
exclude group: 'com.google.android.tools', module: 'dx'
|
exclude group: 'com.google.android.tools', module: 'dx'
|
||||||
}
|
}
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit f3e1fe6df4d64e39fb0080151255adfd057e7dbc
|
Subproject commit 49799fc0c8b5245a7931d0437a68172f9b6efbbc
|
|
@ -569,8 +569,8 @@ public class AppController implements Initializable {
|
||||||
Tab tab = addTransactionTab(null, result.psbt);
|
Tab tab = addTransactionTab(null, result.psbt);
|
||||||
tabs.getSelectionModel().select(tab);
|
tabs.getSelectionModel().select(tab);
|
||||||
} else if(result.exception != null) {
|
} else if(result.exception != null) {
|
||||||
log.error("Error opening webcam", result.exception);
|
log.error("Error scanning QR", result.exception);
|
||||||
showErrorDialog("Error opening webcam", result.exception.getMessage());
|
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||||
} else {
|
} else {
|
||||||
AppController.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a transaction or PSBT");
|
AppController.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a transaction or PSBT");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.FileImport;
|
import com.sparrowwallet.sparrow.io.FileImport;
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
import com.sparrowwallet.sparrow.io.ImportException;
|
||||||
|
@ -27,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public abstract class FileImportPane extends TitledDescriptionPane {
|
public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
|
@ -36,6 +40,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
protected ButtonBase importButton;
|
protected ButtonBase importButton;
|
||||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||||
private final boolean scannable;
|
private final boolean scannable;
|
||||||
|
protected List<Wallet> wallets;
|
||||||
|
|
||||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||||
super(title, description, content, imageUrl);
|
super(title, description, content, imageUrl);
|
||||||
|
@ -132,7 +137,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
if(optionalResult.isPresent()) {
|
if(optionalResult.isPresent()) {
|
||||||
QRScanDialog.Result result = optionalResult.get();
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
if(result.payload != null) {
|
if(result.wallets != null) {
|
||||||
|
wallets = result.wallets;
|
||||||
|
try {
|
||||||
|
importFile(importer.getName(), null, null);
|
||||||
|
} catch(ImportException e) {
|
||||||
|
setError("Import Error", e.getMessage());
|
||||||
|
}
|
||||||
|
} else if(result.payload != null) {
|
||||||
try {
|
try {
|
||||||
importFile(importer.getName(), new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)), null);
|
importFile(importer.getName(), new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)), null);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
@ -146,10 +158,27 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
setError("Import Error", errorMessage);
|
setError("Import Error", errorMessage);
|
||||||
}
|
}
|
||||||
|
} else if(result.exception != null) {
|
||||||
|
log.error("Error importing QR", result.exception);
|
||||||
|
setError("Import Error", result.exception.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
|
||||||
|
if(wallets != null) {
|
||||||
|
for(Wallet wallet : wallets) {
|
||||||
|
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||||
|
return wallet.getKeystores().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ImportException("Script type " + scriptType + " is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
||||||
|
|
||||||
private Node getPasswordEntry(File file) {
|
private Node getPasswordEntry(File file) {
|
||||||
|
|
|
@ -20,7 +20,11 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||||
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
Keystore keystore = getScannedKeystore(wallet.getScriptType());
|
||||||
|
if(keystore == null) {
|
||||||
|
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||||
|
}
|
||||||
|
|
||||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,22 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.github.sarxos.webcam.WebcamResolution;
|
import com.github.sarxos.webcam.WebcamResolution;
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||||
|
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||||
|
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||||
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
import com.sparrowwallet.drongo.protocol.Base43;
|
import com.sparrowwallet.drongo.protocol.Base43;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.registry.*;
|
||||||
import com.sparrowwallet.sparrow.AppController;
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.hummingbird.ResultType;
|
import com.sparrowwallet.hummingbird.ResultType;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
@ -21,15 +31,17 @@ import javafx.scene.control.Dialog;
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.scene.control.DialogPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.controlsfx.tools.Borders;
|
import org.controlsfx.tools.Borders;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
|
||||||
|
|
||||||
private final URDecoder decoder;
|
private final URDecoder decoder;
|
||||||
private final WebcamService webcamService;
|
private final WebcamService webcamService;
|
||||||
private List<String> parts;
|
private List<String> parts;
|
||||||
|
@ -88,30 +100,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
if(decoder.getResult() != null) {
|
if(decoder.getResult() != null) {
|
||||||
URDecoder.Result urResult = decoder.getResult();
|
URDecoder.Result urResult = decoder.getResult();
|
||||||
if(urResult.type == ResultType.SUCCESS) {
|
if(urResult.type == ResultType.SUCCESS) {
|
||||||
//TODO: Confirm once UR type registry is updated
|
result = extractResultFromUR(urResult.ur);
|
||||||
if(urResult.ur.getType().contains(UR.BYTES_TYPE) || urResult.ur.getType().equals(UR.CRYPTO_PSBT_TYPE)) {
|
|
||||||
try {
|
|
||||||
PSBT psbt = new PSBT(urResult.ur.toBytes());
|
|
||||||
result = new Result(psbt);
|
|
||||||
return;
|
|
||||||
} catch(Exception e) {
|
|
||||||
//ignore, bytes not parsable as PSBT
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Transaction transaction = new Transaction(urResult.ur.toBytes());
|
|
||||||
result = new Result(transaction);
|
|
||||||
return;
|
|
||||||
} catch(Exception e) {
|
|
||||||
//ignore, bytes not parsable as tx
|
|
||||||
}
|
|
||||||
|
|
||||||
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction");
|
|
||||||
} else {
|
|
||||||
result = new Result("Cannot parse UR type of " + urResult.ur.getType());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
result = new Result(urResult.error);
|
result = new Result(new URException(urResult.error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(partMatcher.matches()) {
|
} else if(partMatcher.matches()) {
|
||||||
|
@ -143,7 +134,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
//ignore, bytes not parsable as tx
|
//ignore, bytes not parsable as tx
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new Result("Parsed QR parts were not a PSBT or transaction");
|
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PSBT psbt;
|
PSBT psbt;
|
||||||
|
@ -227,6 +218,180 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
result = new Result(qrtext);
|
result = new Result(qrtext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result extractResultFromUR(UR ur) {
|
||||||
|
try {
|
||||||
|
RegistryType urRegistryType = ur.getRegistryType();
|
||||||
|
|
||||||
|
if(urRegistryType.equals(RegistryType.BYTES)) {
|
||||||
|
byte[] urBytes = (byte[])ur.decodeFromRegistry();
|
||||||
|
try {
|
||||||
|
PSBT psbt = new PSBT(urBytes);
|
||||||
|
return new Result(psbt);
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, bytes not parsable as PSBT
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Transaction transaction = new Transaction(urBytes);
|
||||||
|
return new Result(transaction);
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, bytes not parsable as tx
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new Result(new URException("Parsed UR of type " + urRegistryType + " was not a PSBT or transaction"));
|
||||||
|
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
|
||||||
|
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
|
||||||
|
try {
|
||||||
|
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
|
||||||
|
return new Result(psbt);
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
|
||||||
|
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
|
||||||
|
}
|
||||||
|
} else if(urRegistryType.equals(RegistryType.CRYPTO_ADDRESS)) {
|
||||||
|
CryptoAddress cryptoAddress = (CryptoAddress)ur.decodeFromRegistry();
|
||||||
|
Address address = getAddress(cryptoAddress);
|
||||||
|
if(address != null) {
|
||||||
|
return new Result(BitcoinURI.fromAddress(address));
|
||||||
|
} else {
|
||||||
|
return new Result(new URException("Unknown " + urRegistryType + " type of " + cryptoAddress.getType()));
|
||||||
|
}
|
||||||
|
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
|
||||||
|
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
|
||||||
|
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
|
||||||
|
return new Result(extendedKey);
|
||||||
|
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
|
||||||
|
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
|
||||||
|
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||||
|
return new Result(outputDescriptor);
|
||||||
|
} else if(urRegistryType.equals(RegistryType.CRYPTO_ACCOUNT)) {
|
||||||
|
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
|
||||||
|
List<Wallet> wallets = getWallets(cryptoAccount);
|
||||||
|
return new Result(wallets);
|
||||||
|
} else {
|
||||||
|
log.error("Unsupported UR type " + urRegistryType);
|
||||||
|
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
|
||||||
|
}
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
log.error("Unknown UR type of " + ur.getType(), e);
|
||||||
|
return new Result(new URException("Unknown UR type of " + ur.getType(), e));
|
||||||
|
} catch(UR.InvalidCBORException e) {
|
||||||
|
log.error("Invalid CBOR in UR", e);
|
||||||
|
return new Result(new URException("Invalid CBOR in UR", e));
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error parsing UR CBOR", e);
|
||||||
|
return new Result(new URException("Error parsing UR CBOR", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getAddress(CryptoAddress cryptoAddress) {
|
||||||
|
Address address = null;
|
||||||
|
if(cryptoAddress.getType() == CryptoAddress.Type.P2PKH) {
|
||||||
|
address = new P2PKHAddress(cryptoAddress.getData());
|
||||||
|
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2SH) {
|
||||||
|
address = new P2SHAddress(cryptoAddress.getData());
|
||||||
|
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2WPKH) {
|
||||||
|
address = new P2WPKHAddress(cryptoAddress.getData());
|
||||||
|
}
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtendedKey getExtendedKey(CryptoHDKey cryptoHDKey) {
|
||||||
|
if(cryptoHDKey.isPrivateKey()) {
|
||||||
|
DeterministicKey prvKey = HDKeyDerivation.createMasterPrivKeyFromBytes(Arrays.copyOfRange(cryptoHDKey.getKey(), 1, 33), cryptoHDKey.getChainCode(), List.of(ChildNumber.ZERO));
|
||||||
|
return new ExtendedKey(prvKey, new byte[4], ChildNumber.ZERO);
|
||||||
|
} else {
|
||||||
|
ChildNumber lastChild = ChildNumber.ZERO;
|
||||||
|
int depth = 1;
|
||||||
|
byte[] parentFingerprint = new byte[4];
|
||||||
|
if(cryptoHDKey.getOrigin() != null) {
|
||||||
|
if(!cryptoHDKey.getOrigin().getComponents().isEmpty()) {
|
||||||
|
PathComponent lastComponent = cryptoHDKey.getOrigin().getComponents().get(cryptoHDKey.getOrigin().getComponents().size() - 1);
|
||||||
|
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
|
||||||
|
depth = cryptoHDKey.getOrigin().getComponents().size();
|
||||||
|
}
|
||||||
|
if(cryptoHDKey.getOrigin().getParentFingerprint() != null) {
|
||||||
|
parentFingerprint = cryptoHDKey.getOrigin().getParentFingerprint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint);
|
||||||
|
return new ExtendedKey(pubKey, parentFingerprint, lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
|
||||||
|
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
|
||||||
|
|
||||||
|
if(cryptoOutput.getMultiKey() != null) {
|
||||||
|
MultiKey multiKey = cryptoOutput.getMultiKey();
|
||||||
|
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||||
|
for(CryptoHDKey cryptoHDKey : multiKey.getHdKeys()) {
|
||||||
|
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
|
||||||
|
KeyDerivation keyDerivation = getKeyDerivation(cryptoHDKey.getOrigin());
|
||||||
|
extendedPublicKeys.put(extendedKey, keyDerivation);
|
||||||
|
}
|
||||||
|
return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys);
|
||||||
|
} else if(cryptoOutput.getEcKey() != null) {
|
||||||
|
throw new IllegalArgumentException("EC keys are currently unsupported");
|
||||||
|
} else if(cryptoOutput.getHdKey() != null) {
|
||||||
|
ExtendedKey extendedKey = getExtendedKey(cryptoOutput.getHdKey());
|
||||||
|
KeyDerivation keyDerivation = getKeyDerivation(cryptoOutput.getHdKey().getOrigin());
|
||||||
|
return new OutputDescriptor(scriptType, extendedKey, keyDerivation);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("CryptoOutput did not contain sufficient information");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Wallet> getWallets(CryptoAccount cryptoAccount) {
|
||||||
|
List<Wallet> wallets = new ArrayList<>();
|
||||||
|
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
|
||||||
|
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
|
||||||
|
Wallet wallet = new Wallet();
|
||||||
|
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||||
|
if(outputDescriptor.isMultisig()) {
|
||||||
|
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||||
|
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
|
||||||
|
keystore.setExtendedPublicKey(extendedKey);
|
||||||
|
wallet.getKeystores().add(keystore);
|
||||||
|
wallets.add(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptType getScriptType(List<ScriptExpression> expressions) {
|
||||||
|
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2PKH;
|
||||||
|
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2SH_P2WPKH;
|
||||||
|
} else if(List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2WPKH;
|
||||||
|
} else if(List.of(ScriptExpression.SCRIPT_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2SH;
|
||||||
|
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2SH_P2WSH;
|
||||||
|
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||||
|
return ScriptType.P2WSH;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unknown script of " + expressions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
|
||||||
|
if(cryptoKeypath != null) {
|
||||||
|
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getParentFingerprint()), cryptoKeypath.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Result {
|
public static class Result {
|
||||||
|
@ -234,6 +399,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
public final PSBT psbt;
|
public final PSBT psbt;
|
||||||
public final BitcoinURI uri;
|
public final BitcoinURI uri;
|
||||||
public final ExtendedKey extendedKey;
|
public final ExtendedKey extendedKey;
|
||||||
|
public final OutputDescriptor outputDescriptor;
|
||||||
|
public final List<Wallet> wallets;
|
||||||
public final String payload;
|
public final String payload;
|
||||||
public final Throwable exception;
|
public final Throwable exception;
|
||||||
|
|
||||||
|
@ -242,6 +409,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = null;
|
this.uri = null;
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -251,6 +420,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = psbt;
|
this.psbt = psbt;
|
||||||
this.uri = null;
|
this.uri = null;
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -260,6 +431,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -269,6 +442,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = BitcoinURI.fromAddress(address);
|
this.uri = BitcoinURI.fromAddress(address);
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -278,6 +453,30 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = null;
|
this.uri = null;
|
||||||
this.extendedKey = extendedKey;
|
this.extendedKey = extendedKey;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
|
this.payload = null;
|
||||||
|
this.exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(OutputDescriptor outputDescriptor) {
|
||||||
|
this.transaction = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.uri = null;
|
||||||
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = outputDescriptor;
|
||||||
|
this.wallets = null;
|
||||||
|
this.payload = null;
|
||||||
|
this.exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(List<Wallet> wallets) {
|
||||||
|
this.transaction = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.uri = null;
|
||||||
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = wallets;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -287,6 +486,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = null;
|
this.uri = null;
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
}
|
}
|
||||||
|
@ -296,8 +497,46 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
this.uri = null;
|
this.uri = null;
|
||||||
this.extendedKey = null;
|
this.extendedKey = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.wallets = null;
|
||||||
this.payload = null;
|
this.payload = null;
|
||||||
this.exception = exception;
|
this.exception = exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ScanException extends Exception {
|
||||||
|
public ScanException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class URException extends ScanException {
|
||||||
|
public URException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public URException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||||
import com.sparrowwallet.sparrow.AppController;
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
|
@ -620,7 +621,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
toggleButton.setSelected(false);
|
toggleButton.setSelected(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(UR.CRYPTO_PSBT_TYPE, headersForm.getPsbt().serialize());
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(RegistryType.CRYPTO_PSBT.toString(), headersForm.getPsbt().serialize());
|
||||||
qrDisplayDialog.show();
|
qrDisplayDialog.show();
|
||||||
} catch(UR.URException e) {
|
} catch(UR.URException e) {
|
||||||
log.error("Error creating PSBT UR", e);
|
log.error("Error creating PSBT UR", e);
|
||||||
|
|
|
@ -363,8 +363,8 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
if(result.extendedKey != null && result.extendedKey.getKey().isPubKeyOnly()) {
|
if(result.extendedKey != null && result.extendedKey.getKey().isPubKeyOnly()) {
|
||||||
xpub.setText(result.extendedKey.getExtendedKey());
|
xpub.setText(result.extendedKey.getExtendedKey());
|
||||||
} else if(result.exception != null) {
|
} else if(result.exception != null) {
|
||||||
log.error("Error opening webcam", result.exception);
|
log.error("Error scanning QR", result.exception);
|
||||||
AppController.showErrorDialog("Error opening webcam", result.exception.getMessage());
|
AppController.showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||||
} else {
|
} else {
|
||||||
AppController.showErrorDialog("Invalid QR Code", "QR Code did not contain a valid " + Network.get().getXpubHeader().getDisplayName());
|
AppController.showErrorDialog("Invalid QR Code", "QR Code did not contain a valid " + Network.get().getXpubHeader().getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue