mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-05 05:46:44 +00:00
add keystone hww import and export
This commit is contained in:
parent
e99b1d4171
commit
4a0ecba716
14 changed files with 236 additions and 11 deletions
|
@ -896,7 +896,8 @@ public class AppController implements Initializable {
|
||||||
new Electrum(),
|
new Electrum(),
|
||||||
new SpecterDesktop(),
|
new SpecterDesktop(),
|
||||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||||
new PassportSinglesig());
|
new PassportSinglesig(),
|
||||||
|
new KeystoneSinglesig(), new KeystoneMultisig());
|
||||||
for(WalletImport importer : walletImporters) {
|
for(WalletImport importer : walletImporters) {
|
||||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||||
if(importer.isEncrypted(file) && password == null) {
|
if(importer.isEncrypted(file) && password == null) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
@ -172,11 +173,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
if(wallets != null) {
|
if(wallets != null) {
|
||||||
for(Wallet wallet : wallets) {
|
for(Wallet wallet : wallets) {
|
||||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||||
return wallet.getKeystores().get(0);
|
Keystore keystore = wallet.getKeystores().get(0);
|
||||||
|
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||||
|
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||||
|
keystore.setWalletModel(importer.getWalletModel());
|
||||||
|
return keystore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ImportException("Script type " + scriptType + " is not supported");
|
throw new ImportException("Script type " + scriptType.getDescription() + " is not supported in this QR. Check you are displaying the correct QR code.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -180,7 +180,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
decoder.receivePart(qrtext);
|
decoder.receivePart(qrtext);
|
||||||
Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete()));
|
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
|
||||||
|
|
||||||
if(decoder.getResult() != null) {
|
if(decoder.getResult() != null) {
|
||||||
URDecoder.Result urResult = decoder.getResult();
|
URDecoder.Result urResult = decoder.getResult();
|
||||||
|
@ -469,7 +469,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||||
Keystore keystore = new Keystore();
|
Keystore keystore = new Keystore();
|
||||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
|
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||||
keystore.setExtendedPublicKey(extendedKey);
|
keystore.setExtendedPublicKey(extendedKey);
|
||||||
wallet.getKeystores().add(keystore);
|
wallet.getKeystores().add(keystore);
|
||||||
wallets.add(wallet);
|
wallets.add(wallet);
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||||
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
||||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,13 +47,13 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||||
|
|
||||||
importAccordion = new Accordion();
|
importAccordion = new Accordion();
|
||||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
|
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig());
|
||||||
for(KeystoreFileImport importer : keystoreImporters) {
|
for(KeystoreFileImport importer : keystoreImporters) {
|
||||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||||
importAccordion.getPanes().add(importPane);
|
importAccordion.getPanes().add(importPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
|
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig());
|
||||||
for(WalletImport importer : walletImporters) {
|
for(WalletImport importer : walletImporters) {
|
||||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||||
importAccordion.getPanes().add(importPane);
|
importAccordion.getPanes().add(importPane);
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class KeystoneMultisig extends ColdcardMultisig {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Keystone Multisig";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WalletModel getWalletModel() {
|
||||||
|
return WalletModel.KEYSTONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||||
|
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||||
|
keystore.setLabel("Keystone");
|
||||||
|
keystore.setWalletModel(getWalletModel());
|
||||||
|
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreImportDescription() {
|
||||||
|
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Keystone.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||||
|
Wallet wallet = super.importWallet(inputStream, password);
|
||||||
|
for(Keystore keystore : wallet.getKeystores()) {
|
||||||
|
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Keystone"));
|
||||||
|
keystore.setWalletModel(WalletModel.KEYSTONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWalletImportDescription() {
|
||||||
|
return "Import file or QR created by using the Multisig Wallet > ... > Create Multisig Wallet feature on your Keystone.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWalletExportDescription() {
|
||||||
|
return "Export file or QR that can be read by your Keystone using the Multisig Wallet > ... > Import Multisig Wallet feature.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isWalletImportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeystoreImportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isWalletExportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.policy.Policy;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(KeystoneSinglesig.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Keystone";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreImportDescription() {
|
||||||
|
return "Import file or QR created by using the My Keystone > ... > Export Wallet feature on your Keystone. Make sure to set the Watch-only Wallet to Sparrow in the Settings first.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WalletModel getWalletModel() {
|
||||||
|
return WalletModel.KEYSTONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||||
|
try {
|
||||||
|
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor);
|
||||||
|
|
||||||
|
if(descriptor.isMultisig()) {
|
||||||
|
throw new IllegalArgumentException("Output descriptor describes a multisig wallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(descriptor.getScriptType() != scriptType) {
|
||||||
|
throw new IllegalArgumentException("Output descriptor describes a " + descriptor.getScriptType().getDescription() + " wallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey();
|
||||||
|
KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub);
|
||||||
|
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setLabel(getName());
|
||||||
|
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||||
|
keystore.setWalletModel(WalletModel.KEYSTONE);
|
||||||
|
keystore.setKeyDerivation(keyDerivation);
|
||||||
|
keystore.setExtendedPublicKey(xpub);
|
||||||
|
|
||||||
|
return keystore;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting Keystone keystore", e);
|
||||||
|
throw new ImportException("Error getting Keystone keystore", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWalletImportDescription() {
|
||||||
|
return getKeystoreImportDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||||
|
//Use default of P2WPKH
|
||||||
|
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||||
|
|
||||||
|
Wallet wallet = new Wallet();
|
||||||
|
wallet.setPolicyType(PolicyType.SINGLE);
|
||||||
|
wallet.setScriptType(ScriptType.P2WPKH);
|
||||||
|
wallet.getKeystores().add(keystore);
|
||||||
|
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
wallet.checkWallet();
|
||||||
|
} catch(InvalidWalletException e) {
|
||||||
|
throw new ImportException("Imported Keystone wallet was invalid: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEncrypted(File file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isWalletImportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeystoreImportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,9 +16,9 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig(), new SpecterDIY());
|
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
|
||||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new PassportMultisig(), new SpecterDIY());
|
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDIY());
|
||||||
}
|
}
|
||||||
|
|
||||||
for(KeystoreImport importer : importers) {
|
for(KeystoreImport importer : importers) {
|
||||||
|
|
|
@ -451,10 +451,16 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
export.setDisable(!event.getWallet().isValid());
|
export.setDisable(!event.getWallet().isValid());
|
||||||
scanDescriptorQR.setVisible(!event.getWallet().isValid());
|
scanDescriptorQR.setVisible(!event.getWallet().isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||||
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
updateBirthDate(event.getWallet());
|
updateBirthDate(event.getWallet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
src/main/resources/image/keystone.png
Normal file
BIN
src/main/resources/image/keystone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
src/main/resources/image/keystone@2x.png
Normal file
BIN
src/main/resources/image/keystone@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/main/resources/image/keystone@3x.png
Normal file
BIN
src/main/resources/image/keystone@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
|
@ -0,0 +1,33 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class KeystoneSinglesigTest extends IoTest {
|
||||||
|
@Test
|
||||||
|
public void testImport() throws ImportException {
|
||||||
|
KeystoneSinglesig keystoneSingleSig = new KeystoneSinglesig();
|
||||||
|
Keystore keystore = keystoneSingleSig.getKeystore(ScriptType.P2WPKH, getInputStream("keystone-singlesig-keystore-1.txt"), null);
|
||||||
|
|
||||||
|
Assert.assertEquals("Keystone", keystore.getLabel());
|
||||||
|
Assert.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||||
|
Assert.assertEquals("5271c071", keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
Assert.assertEquals(ExtendedKey.fromDescriptor("zpub6rcabYFcdr41zyUNRWRyHYs2Sm86E5XV8RjjRzTFYsiCngteeZnkwaF2xuhjmM6kpHjuNpFW42BMhzPmFwXt48e1FhddMB7xidZzN4SF24K"), keystore.getExtendedPublicKey());
|
||||||
|
Assert.assertTrue(keystore.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = ImportException.class)
|
||||||
|
public void testIncorrectScriptType() throws ImportException {
|
||||||
|
KeystoneSinglesig keystoneSingleSig = new KeystoneSinglesig();
|
||||||
|
Keystore keystore = keystoneSingleSig.getKeystore(ScriptType.P2SH_P2WPKH, getInputStream("keystone-singlesig-keystore-1.txt"), null);
|
||||||
|
|
||||||
|
Assert.assertEquals("Keystone", keystore.getLabel());
|
||||||
|
Assert.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||||
|
Assert.assertEquals("5271c071", keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
Assert.assertEquals(ExtendedKey.fromDescriptor("zpub6rcabYFcdr41zyUNRWRyHYs2Sm86E5XV8RjjRzTFYsiCngteeZnkwaF2xuhjmM6kpHjuNpFW42BMhzPmFwXt48e1FhddMB7xidZzN4SF24K"), keystore.getExtendedPublicKey());
|
||||||
|
Assert.assertTrue(keystore.isValid());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
wpkh([5271C071/84'/0'/0']zpub6rcabYFcdr41zyUNRWRyHYs2Sm86E5XV8RjjRzTFYsiCngteeZnkwaF2xuhjmM6kpHjuNpFW42BMhzPmFwXt48e1FhddMB7xidZzN4SF24K/1/*)
|
Loading…
Reference in a new issue