From 4a0ecba71649c8e1547681f72e612f4163fe6a3c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 10 Jun 2021 16:37:41 +0200 Subject: [PATCH] add keystone hww import and export --- .../sparrowwallet/sparrow/AppController.java | 3 +- .../sparrow/control/FileImportPane.java | 9 +- .../sparrow/control/QRScanDialog.java | 4 +- .../sparrow/control/WalletExportDialog.java | 2 +- .../sparrow/control/WalletImportDialog.java | 4 +- .../sparrow/io/KeystoneMultisig.java | 70 +++++++++++ .../sparrow/io/KeystoneSinglesig.java | 109 ++++++++++++++++++ .../keystoreimport/HwAirgappedController.java | 4 +- .../sparrow/wallet/SettingsController.java | 8 +- src/main/resources/image/keystone.png | Bin 0 -> 2545 bytes src/main/resources/image/keystone@2x.png | Bin 0 -> 3809 bytes src/main/resources/image/keystone@3x.png | Bin 0 -> 6494 bytes .../sparrow/io/KeystoneSinglesigTest.java | 33 ++++++ .../io/keystone-singlesig-keystore-1.txt | 1 + 14 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/KeystoneMultisig.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java create mode 100644 src/main/resources/image/keystone.png create mode 100644 src/main/resources/image/keystone@2x.png create mode 100644 src/main/resources/image/keystone@3x.png create mode 100644 src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java create mode 100644 src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-keystore-1.txt diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index fb30b375..3ad0027f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -896,7 +896,8 @@ public class AppController implements Initializable { new Electrum(), new SpecterDesktop(), new CoboVaultSinglesig(), new CoboVaultMultisig(), - new PassportSinglesig()); + new PassportSinglesig(), + new KeystoneSinglesig(), new KeystoneMultisig()); 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/FileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java index 2c6e1ec1..6d2919c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java @@ -4,6 +4,7 @@ import com.google.gson.JsonParseException; import com.sparrowwallet.drongo.crypto.InvalidPasswordException; 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.sparrow.AppServices; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -172,11 +173,15 @@ public abstract class FileImportPane extends TitledDescriptionPane { if(wallets != null) { for(Wallet wallet : wallets) { 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; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index 27d81dd2..6670a9d3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -180,7 +180,7 @@ public class QRScanDialog extends Dialog { } } else { decoder.receivePart(qrtext); - Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete())); + Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0)); if(decoder.getResult() != null) { URDecoder.Result urResult = decoder.getResult(); @@ -469,7 +469,7 @@ public class QRScanDialog extends Dialog { ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey(); wallet.setScriptType(outputDescriptor.getScriptType()); 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); wallet.getKeystores().add(keystore); wallets.add(wallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index f52eb4df..aea52bf5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -43,7 +43,7 @@ public class WalletExportDialog extends Dialog { if(wallet.getPolicyType() == PolicyType.SINGLE) { exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow()); } 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 { 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 51e3177d..b00c002e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -47,13 +47,13 @@ public class WalletImportDialog extends Dialog { AnchorPane.setRightAnchor(scrollPane, 0.0); importAccordion = new Accordion(); - List keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig()); + List keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig()); for(KeystoreFileImport importer : keystoreImporters) { FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer); importAccordion.getPanes().add(importPane); } - List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig()); + List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig()); for(WalletImport importer : walletImporters) { FileWalletImportPane importPane = new FileWalletImportPane(importer); importAccordion.getPanes().add(importPane); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoneMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneMultisig.java new file mode 100644 index 00000000..16d49f1a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneMultisig.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java new file mode 100644 index 00000000..0a1149c0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java index 37e68084..16432a65 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(), 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)) { - 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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index c8e11d21..b3eedb94 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -451,10 +451,16 @@ public class SettingsController extends WalletFormController implements Initiali } @Subscribe - public void walletAddressesChanged(WalletAddressesChangedEvent event) { + public void walletSettingsChanged(WalletSettingsChangedEvent event) { if(event.getWalletId().equals(walletForm.getWalletId())) { export.setDisable(!event.getWallet().isValid()); scanDescriptorQR.setVisible(!event.getWallet().isValid()); + } + } + + @Subscribe + public void walletAddressesChanged(WalletAddressesChangedEvent event) { + if(event.getWalletId().equals(walletForm.getWalletId())) { updateBirthDate(event.getWallet()); } } diff --git a/src/main/resources/image/keystone.png b/src/main/resources/image/keystone.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b20445b57e4e27df97ed23beb2445cefe06447 GIT binary patch literal 2545 zcmZ`*30M=?7M>7x%OZObNeGIdl1YMygb*+Z`@V>P3k@(pFd=C&2v~uKV8v?aLkofu zMAQ~RP?56AqPB>Z{mFZv^04}evP7#Qf{1hy5b}6<^WC{;&iVgy&OP_s`Eu0P+eJ;) zP!#|GH8)p=KeCb}Q&|yt8arfOLl%_4--QNLbQ+ByFSnpzwBLj;f~uo{_s6iF)UAaceL79hXC%|lkna78BKb$<0_ zB|>Zt6Xx=Q^0RXJhmP{0Iy1)PcehZ#}qpD)DSPDNfitNvDUZ5jy_P7`r z=8-`#At3>uV1?)Mqd}srtu06(fh3Y8g0K`M#lg%(%Q%7Qypzv<7?2=>&*H%>ZX8zP zml@8DhwX7VNub3spJ!ymVjNyPpDm3N8396Uhy%sJ0+5I&f-CWm-7XU21Pj$b>H;P* zc?gNHA_+=lEp6+6SQ5(@X)r753p9yko+d5g{N5;$5oAGpcq|kF%V-gsw8k`MG$^B{ zfGeUY$o|M)+(=eb5`zgtR7q1=61G|r34x!RiUKafELB&^rt=|5TLyBuZ0hnZo#)CT zWXO_Qkfn@Z1`9a_0dkB~YXSva(Xb*hu!*U{>%jWU}c+3chN?eE~qmn^OicY3; zIXpfjkmiP@_J6wN_+WoXz-7lvT9Rr*S|G?OmlJ&WETjRMQUa2XG=QvbIey9ijyM)f z{TyPMdO0g($-b19ta5=s0VVfH=3dQTTB#PfwNzE)L_dE4sH#S~7$Ne#$a7WPFCb7$%V zo$Up4?fF*kn;6CGF6x?>JF*mhu9eg1PF*!+aNqme%fSt)zm01fXZd&j(04{|Lzri? z4uKI^t-mdZJ$NsFTKTG5Ns7XwohH?aN32i`#xRHH!zi;6y}_o7<)EzXXL(gVbxHL{ ztDf{+H`yyv@6i>G5-D|xAER$m&eDEayXinrGP7Xv)YBJxHy=>k)(^y?9Z+u%8i>Xo z6>U_{P7gypt!ncG_b=Ly zJ?Yf6JN!&l^9)S9F0S2EBl^y;!GXr^>oV(j+J2Yzv;WpgQO%gyZXMDLZG4={>F+3M zt4nX$f2-nHQKq|InqAAk8%EKiz9l+uD*Dz++@mw`fnjM`&oMUQkewv7?_2adpfIFYt@K8t;c+-AX?heH3jfSIobK)FGc=&9t6SE3=T0+w-#&q8 zQ#*hC#_V6~2Kj{(?iFy^vx7K!`OTC6Epe~mqM)2; zM~rd3K>2$gSJ<8zun8`UJ_@hwB3Ewd3-?i%yy3gLzvedAsE9kECUvAZUvm4S` zcHO)9#dUSlCjK1D27bC|%7gQ5+dj|ItAFU*j0ET(xm{uVyx2f-S}QxbfBo3$t^s4~ z*?PsPK|xFlq}f}cxryAvOjR*Lxp$ltcXRtG>OX|${weBxyH=B7?)9B^EjQAlFhw0+ zZAhzk3B6h6(KJ${sifC4vZLs2E#{-VyMJg}uj)@hMOSsFG{CKQ6XI9LevtfgadYx! JRM5UT^f#cSskHzA literal 0 HcmV?d00001 diff --git a/src/main/resources/image/keystone@2x.png b/src/main/resources/image/keystone@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ddf04df8613f2b3de0dcfdaacd88affe29cab3bb GIT binary patch literal 3809 zcmZ`+2|Sc*7oRZ`LzGC_W5^ma#+J#xHWZnIWL0Lv(`q@D%5S@!`IpKP|DKE1O+zhstkws!Z=%!xDT zurmn&*v`)yKtRS#K>z?O<7@9kc0yYsv4lWnPj7-3PB|Br)*dI=$aHuYqgoK1P(c1@!F*f~9XFlmb zPmsw$NEnPlp(s;Sl?lY-FcmE=Ef^dDLm-rx2qjWDp6nT>geOUTbMnKFF^+^K`Ua7G z33v&XUr#SW2w4XTWd-_qeaq7u`!h~Z2r*zIiZ>R93%~{9@MIE9MOg**J05ejKZ$tK z_i8Zf0t@pDVv^v$CE>z+e=X}DESBXzX=GoY|Ddrf-)I{}{I)i2Z!D4&;^l|Kk~e9Y zwv8GadLD;uQftG0i`HhY4~Zst`}%|%dy;Xwtfo?eYbdF}?SC|tHf#&#S9LeoMnoK| zE$s<}0Nt%!`i;98v5Cy81#*MoWbDh_1rl=`byeVSZP;%uzhyfACv%f!E7OP&Kp@%$ zd17(8D&K=`Qf)%B%8oQ51O^dtq>b#Dx&2GG74L+>kq7}Htfth}M0_V~R&FI&6MdO2 z=(#~)=CcuCvu-Q?mwyMmFIo3Th%M@^EazYLUukVtekW+dSSM-I$@+F|H?+)itINgQ z(;x2uT`q;Qb~NT&Bo<|CU>^pYzIiIip;4^)>VT4~bMYRq{bWp>WA3(Gn(pn&1Yz!Z zj#%NlO~ujaj%brqb6R276+c(3U7%uhfb{tjcB8igdkPaO9cco%dJn8o-gan@OT>Mf zJ)aHq+($3`@ds%}Cq;|$dFGb;e7nxVp@r4(1FC!ve!+4$_#t5JJ`ps%(qMM=ce)!C*kA2@ho7*uk@;37g5PMsGA`M|mQIJ&Z& z$5!kkgGrv*J9kq(*1txdVmNeSu0sS(v`X0yK|)tr5cGn{^Uk7Df?ORU!wU|bjvZjj zXX|>=10uOSio_CYdiA;q)kE&^EEQ(2ER*&sBk$a&8>QA zVn>!idTR7&*{Y_TNyLk_xH7tm1+O(X?X7I$N6bkvwMp>7cv<$2rzx>FG0)5f+-Hi! z>v_1xJ-T#VQlNu*>xY)&IAqpTg0__$HEyOyHn=-bWp!S=s)FAM1vGS5H-8vk;;tWm zx|9SB>CW^y00Al*riS|~Bz!h_lk;asRSVhYXjU^rDw}_t7RLX8=j>?sVL2K9hrX9Z zHP>JPDZvMy>^SzqZ(3kRY^sb?LGa){a?Ar@*@9C2%PSDT7W>GHcLAZ;-8h-}&o+@8UjcMGM_o;|Jz`4WSAuq$!e;qW!Uf1y%%i(&` z=*v)=ZV2)yecs=Nz)Do6QaHo1yNdyipK9nRa2V{fjH8nyB`-_^HROYPw~KkV8x){) zOLiWq)x(yiJh*GVSpJlK7%H7SV-jHIk~DF?sa+@{o#PRW^`Ul_u4w}$R{y}LQ992`zMA@EO{p9`#)%@ zxSGI~yo6}hY7x=6Q*6=G3cY4?v7!xS3!JDceVlr}(C~fHeQCNyPGQs49=!{J$1eE{ zWz|#cUn6gdR?Y{y?4f_coQ$0N`kt@N^4(W|p_a0#E^6rtAI|aP^zEW$yoV+d?{bEp z&rI!VG0^4fX)w(lMU8xS1ol$AXb*SBiHdf+nTCgMxAS*BUeDYXx*wjYKPaWohhYR2 z%K9l|;BohEnzqs+lKMqHMhH(*!@TsZ$7iF_#5XiYEf;R^+_O6Ezn|?N>gYQ^-IzIP z8;qa7W@OqxO)h6t$X(D*?#|PXHs6m%qeq2>g{_(%(w9Hu?z>$V{R@L#TU=%vt=#v0RnLoTe|m2FlO31z>fID!u%5UiK@~5K=X_Ig*`-L`#?rIetE1VK$yMpgMzb!BCx zsZM@DK>_~|`rf^Jegza%g?5tN7jJ}bNV;jL$Nyvx-RF&e&0sJFV{*9U>jnk})WJS{ zVQHBMRYOe93$E;lw<7P9$GDZdDn5893ytYW-vP=C?W*lp%|>Qi9+$^H44(aadEs#u zWf@;b11iNAI9XTTPcD|t?H$9k4)@iy4qIjOM*$6Yh9^qm^qzobmhEj~sl<+JDp<9l zaDgGm@!FL0>59WiFul6K}wu_ z>kz^5u8PzDv-tAWLjiar%KwZAchmxOfVEx+w|iGrUOsmu^tE$u zEvO=$Yya!639(hZV$+)@D+(5S)04Q(U0wSXnpTcdbKi0e&GIhlnK~S}H%QS}o10A2 zNUpJxLBGmBA#k5h+WE1l$!SJfuYumO_m`C`ote3rl4abr*!B$d;Qqf#-5U97LG!xj z9%G92M>AvhugZ-O$sdHmN8A+TkVK)fNzX5S=V}NmkVLkeL4!^wtFkjfRYcUhz$x{N zXDQbyq6x3_p9Zf_+ryaD)2nb(8#BD~E@Cc{+`2CD8CC(rk5hXUiXF7ria6sui7!I@xl)Qj*l-_xvch zd;qGMAonC{4S{@6AIfD76V^CzI?cf%5IS*qa`fGNZVU))oWm8 z)+}?frNsKAd2yvk{e$$(IZ&^=cwf0fdhRgjRU75iv}u9B3%9AkPE?lPM1{0eOaZDyh*lGd>6yvWv6s}E%$!V`^j954H zx!2vuC&ZSvbG!GC%YPsl{yjG}V#s@Vs_})J<8cc}lQp`!rgVWTesT?Y4N?mUrTTPn zT6PQ3KkH{Q;<>qQ_g$IjEtrCg0f2&kmhO@2=X^H0m?SceLRpK$_pKlj6vV}C(e0`( zD_20XHXTv)tG1V05a@3;Q%3(#``{IQvMl!owjtrb0Jt>rU z%TnlktBqCTo;fYQTCv@k!^FQQ;dZSflJYSFM&v6YgzML}V&`AjQH|kUQEsc}h>OXF zLP!(@a~8-W`PV)FIClx9(jm8&PvHtt)o&FGdTF8Ax6^Cba#P;)WXfS(Zsm3+yugxe zCJw=oZ@5!aqvc<%@z(G}fPHqJwgMO+5ZROuRe->cJ?uVul?(d3Dcni*eGL8Cnag0) zGY82+kW_worbuR-FE45u-EJ~(DdH_y7j$e ze^Sc&?8t#T1-(ZjJ#qjO*f<{j)nti(eb)TrD<{3PZXL)o*Ti$z5m3$gr-m}IGA=Rn Gp#C4Hmn7o= literal 0 HcmV?d00001 diff --git a/src/main/resources/image/keystone@3x.png b/src/main/resources/image/keystone@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2aaf7caa10202e103404e0d06f19202b219f2119 GIT binary patch literal 6494 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 literal 0 HcmV?d00001 diff --git a/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java b/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java new file mode 100644 index 00000000..8690b0d6 --- /dev/null +++ b/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java @@ -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()); + } +} diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-keystore-1.txt b/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-keystore-1.txt new file mode 100644 index 00000000..84a3d5a6 --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-keystore-1.txt @@ -0,0 +1 @@ +wpkh([5271C071/84'/0'/0']zpub6rcabYFcdr41zyUNRWRyHYs2Sm86E5XV8RjjRzTFYsiCngteeZnkwaF2xuhjmM6kpHjuNpFW42BMhzPmFwXt48e1FhddMB7xidZzN4SF24K/1/*) \ No newline at end of file