From 582065e7f0cf868f6386fb57db1d6b9fa31f11a3 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 9 Dec 2020 17:16:44 +0200 Subject: [PATCH] add specter diy keystore import --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 2 +- .../sparrow/control/WalletExportDialog.java | 4 +- .../sparrow/control/WalletImportDialog.java | 2 +- .../sparrowwallet/sparrow/io/SpecterDIY.java | 64 +++++++++++++++++++ .../io/{Specter.java => SpecterDesktop.java} | 8 +-- .../keystoreimport/HwAirgappedController.java | 4 +- .../sparrow/io/SpecterDIYTest.java | 24 +++++++ ...ecterTest.java => SpecterDesktopTest.java} | 10 +-- .../sparrow/io/specter-diy-keystore.txt | 1 + 10 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java rename src/main/java/com/sparrowwallet/sparrow/io/{Specter.java => SpecterDesktop.java} (95%) create mode 100644 src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java rename src/test/java/com/sparrowwallet/sparrow/io/{SpecterTest.java => SpecterDesktopTest.java} (84%) create mode 100644 src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-keystore.txt diff --git a/drongo b/drongo index deb45687..05674097 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit deb45687c08d4ff824ab7559233249c345d9c86a +Subproject commit 05674097428d25de043310f8ecddf06d998b3943 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 2daa232c..0cec90b4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -668,7 +668,7 @@ public class AppController implements Initializable { } private boolean attemptImportWallet(File file, SecureString password) { - List walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(), new Electrum(), new Specter(), new CoboVaultSinglesig(), new CoboVaultMultisig()); + List walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(), new Electrum(), new SpecterDesktop(), new CoboVaultSinglesig(), new CoboVaultMultisig()); for(WalletImport importer : walletImporters) { try(FileInputStream inputStream = new FileInputStream(file)) { if(importer.isEncrypted(file) && password == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index 46d1db6c..7eb197ec 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -41,9 +41,9 @@ public class WalletExportDialog extends Dialog { List exporters; if(wallet.getPolicyType() == PolicyType.SINGLE) { - exporters = List.of(new Electrum(), new Specter()); + exporters = List.of(new Electrum(), new SpecterDesktop()); } else if(wallet.getPolicyType() == PolicyType.MULTI) { - exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new Specter()); + exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop()); } else { throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index f3bf7d69..4ea8af2d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -45,7 +45,7 @@ public class WalletImportDialog extends Dialog { importAccordion.getPanes().add(importPane); } - List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new Specter()); + List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop()); for(WalletImport importer : walletImporters) { FileWalletImportPane importPane = new FileWalletImportPane(importer); importAccordion.getPanes().add(importPane); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java new file mode 100644 index 00000000..8265b27a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java @@ -0,0 +1,64 @@ +package com.sparrowwallet.sparrow.io; + +import com.google.common.io.CharStreams; +import com.sparrowwallet.drongo.OutputDescriptor; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class SpecterDIY implements KeystoreFileImport { + @Override + public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + try { + String text = CharStreams.toString(new InputStreamReader(inputStream)); + String outputDesc = "sh(" + text + ")"; + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(outputDesc); + Wallet wallet = outputDescriptor.toWallet(); + + if(wallet.getKeystores().size() != 1) { + throw new ImportException("Could not determine keystore from import"); + } + + Keystore keystore = wallet.getKeystores().get(0); + keystore.setLabel(getName()); + keystore.setWalletModel(WalletModel.SPECTER_DIY); + keystore.setSource(KeystoreSource.HW_AIRGAPPED); + + return keystore; + } catch(IOException e) { + throw new ImportException(e); + } + } + + @Override + public boolean isKeystoreImportScannable() { + return true; + } + + @Override + public String getKeystoreImportDescription() { + return "Import file or QR created by using the Master Public Keys feature on your Specter DIY device. Note the default is P2WPKH for Single Signature, and P2WSH for Multi Signature."; + } + + @Override + public boolean isEncrypted(File file) { + return false; + } + + @Override + public String getName() { + return "Specter DIY"; + } + + @Override + public WalletModel getWalletModel() { + return WalletModel.SPECTER_DIY; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Specter.java b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java similarity index 95% rename from src/main/java/com/sparrowwallet/sparrow/io/Specter.java rename to src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java index 0ea9b83f..b9dc7c52 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Specter.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java @@ -13,7 +13,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -public class Specter implements WalletImport, WalletExport { +public class SpecterDesktop implements WalletImport, WalletExport { @Override public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException { try { @@ -70,7 +70,7 @@ public class Specter implements WalletImport, WalletExport { throw new ImportException(e); } - throw new ImportException("File was not a valid Specter wallet"); + throw new ImportException("File was not a valid Specter Desktop wallet"); } @Override @@ -85,12 +85,12 @@ public class Specter implements WalletImport, WalletExport { @Override public String getName() { - return "Specter"; + return "Specter Desktop"; } @Override public WalletModel getWalletModel() { - return WalletModel.SPECTER; + return WalletModel.SPECTER_DESKTOP; } public static class SpecterWallet { diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java index d66735e7..11621179 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java @@ -16,9 +16,9 @@ public class HwAirgappedController extends KeystoreImportDetailController { public void initializeView() { List importers = Collections.emptyList(); if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) { - importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig()); + importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new SpecterDIY()); } else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) { - importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig()); + importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new SpecterDIY()); } for(KeystoreImport importer : importers) { diff --git a/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java new file mode 100644 index 00000000..9e40e703 --- /dev/null +++ b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java @@ -0,0 +1,24 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import org.junit.Assert; +import org.junit.Test; + +public class SpecterDIYTest extends IoTest { + @Test + public void testImport() throws ImportException { + Network.set(Network.TESTNET); + SpecterDIY specterDIY = new SpecterDIY(); + Keystore keystore = specterDIY.getKeystore(ScriptType.P2WPKH, getInputStream("specter-diy-keystore.txt"), null); + + Assert.assertEquals("Specter DIY", keystore.getLabel()); + Assert.assertEquals("m/84'/1'/0'", keystore.getKeyDerivation().getDerivationPath()); + Assert.assertEquals("b317ec86", keystore.getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals(ExtendedKey.fromDescriptor("vpub5YHLPnkkpPW1ecL7Di7Gv2wDHDtBNqRdt17gMULpxJ27ZA1MmW7xbZjdg1S7d5JKaJ8CiZEmRUHrEB6CGuLomA6ioVa1Pcke6fEb5CzDBU1"), keystore.getExtendedPublicKey()); + Assert.assertTrue(keystore.isValid()); + Network.set(Network.MAINNET); + } +} diff --git a/src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDesktopTest.java similarity index 84% rename from src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java rename to src/test/java/com/sparrowwallet/sparrow/io/SpecterDesktopTest.java index b50b390c..07d4ca98 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDesktopTest.java @@ -6,11 +6,11 @@ import com.sparrowwallet.drongo.wallet.Wallet; import org.junit.Assert; import org.junit.Test; -public class SpecterTest extends IoTest { +public class SpecterDesktopTest extends IoTest { @Test public void testImport() throws ImportException { - Specter specter = new Specter(); - Wallet wallet = specter.importWallet(getInputStream("specter-wallet.json"), null); + SpecterDesktop specterDesktop = new SpecterDesktop(); + Wallet wallet = specterDesktop.importWallet(getInputStream("specter-wallet.json"), null); Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType()); Assert.assertEquals(ScriptType.P2SH_P2WPKH, wallet.getScriptType()); @@ -24,8 +24,8 @@ public class SpecterTest extends IoTest { @Test public void testMultisigImport() throws ImportException { - Specter specter = new Specter(); - Wallet wallet = specter.importWallet(getInputStream("specter-multisig-wallet.json"), null); + SpecterDesktop specterDesktop = new SpecterDesktop(); + Wallet wallet = specterDesktop.importWallet(getInputStream("specter-multisig-wallet.json"), null); Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType()); Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType()); diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-keystore.txt b/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-keystore.txt new file mode 100644 index 00000000..9913f4f8 --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-keystore.txt @@ -0,0 +1 @@ +[b317ec86/84h/1h/0h]vpub5YHLPnkkpPW1ecL7Di7Gv2wDHDtBNqRdt17gMULpxJ27ZA1MmW7xbZjdg1S7d5JKaJ8CiZEmRUHrEB6CGuLomA6ioVa1Pcke6fEb5CzDBU1 \ No newline at end of file