diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java b/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java index 823a6b64..6271b100 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.IOUtils; +import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.AppServices; @@ -117,8 +118,13 @@ public class Sparrow implements WalletImport, WalletExport { } } + storage.restorePublicKeysFromSeed(wallet, null); + for(Wallet childWallet : wallet.getChildWallets()) { + storage.restorePublicKeysFromSeed(childWallet, null); + } + return wallet; - } catch(IOException | StorageException e) { + } catch(IOException | StorageException | MnemonicException e) { throw new ImportException("Error importing Sparrow wallet", e); } finally { if(storage != null) { diff --git a/src/test/java/com/sparrowwallet/sparrow/io/DbPersistenceTest.java b/src/test/java/com/sparrowwallet/sparrow/io/DbPersistenceTest.java index ac5fa47d..4cc776e1 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/DbPersistenceTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/DbPersistenceTest.java @@ -24,6 +24,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; import java.util.Comparator; +import java.util.stream.Stream; public class DbPersistenceTest { private Path tempDir; @@ -36,7 +37,9 @@ public class DbPersistenceTest { @AfterEach public void tearDown() throws Exception { if(tempDir != null) { - Files.walk(tempDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + try(Stream paths = Files.walk(tempDir)) { + paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } } } diff --git a/src/test/java/com/sparrowwallet/sparrow/io/SparrowTest.java b/src/test/java/com/sparrowwallet/sparrow/io/SparrowTest.java new file mode 100644 index 00000000..aa82122e --- /dev/null +++ b/src/test/java/com/sparrowwallet/sparrow/io/SparrowTest.java @@ -0,0 +1,138 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.crypto.EncryptionType; +import com.sparrowwallet.drongo.crypto.Key; +import com.sparrowwallet.drongo.policy.Policy; +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +public class SparrowTest { + private static final String TEST_MNEMONIC = "response seminar brave tip suit recall often sound stick owner lottery motion"; + private static final String TEST_XPUB = "xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy"; + private static final String DERIVATION = "m/84'/0'/0'"; + + private Path tempDir; + + @BeforeEach + public void setUp() throws Exception { + tempDir = Files.createTempDirectory("sprw-sparrow-import"); + } + + @AfterEach + public void tearDown() throws Exception { + if(tempDir != null) { + try(Stream paths = Files.walk(tempDir)) { + paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + } + + private Wallet createSeedWallet() throws Exception { + Wallet wallet = new Wallet("Seed Wallet"); + wallet.setPolicyType(PolicyType.SINGLE_HD); + wallet.setScriptType(ScriptType.P2WPKH); + + DeterministicSeed seed = new DeterministicSeed(TEST_MNEMONIC, "", 0, DeterministicSeed.Type.BIP39); + Keystore keystore = Keystore.fromSeed(seed, PolicyType.SINGLE_HD, KeyDerivation.parsePath(DERIVATION)); + keystore.setLabel("Keystore 1"); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null)); + + return wallet; + } + + private Wallet createWatchOnlyWallet() { + Wallet wallet = new Wallet("Watch Only Wallet"); + wallet.setPolicyType(PolicyType.SINGLE_HD); + wallet.setScriptType(ScriptType.P2WPKH); + + Keystore keystore = new Keystore("Keystore 1"); + keystore.setSource(KeystoreSource.SW_WATCH); + keystore.setWalletModel(WalletModel.SPARROW); + keystore.setKeyDerivation(new KeyDerivation("60bcd3a7", DERIVATION)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(TEST_XPUB)); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null)); + + return wallet; + } + + private File saveWallet(Wallet wallet, CharSequence password) throws Exception { + Storage storage = new Storage(PersistenceType.DB, tempDir.resolve(wallet.getName() + "." + PersistenceType.DB.getExtension()).toFile()); + storage.setKeyDeriver(new Argon2KeyDeriver()); + + if(password == null) { + storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); + } else { + ECKey encryptionFullKey = storage.getKeyDeriver().deriveECKey(password); + Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + wallet.encrypt(key); + storage.setEncryptionPubKey(ECKey.fromPublicOnly(encryptionFullKey)); + } + + try { + storage.saveWallet(wallet); + } finally { + storage.closeAndWait(); + } + + return storage.getWalletFile(); + } + + private Wallet importWallet(File walletFile, String password) throws Exception { + try(ByteArrayInputStream inputStream = new ByteArrayInputStream(Files.readAllBytes(walletFile.toPath()))) { + return new Sparrow().importWallet(inputStream, password); + } + } + + @Test + public void encryptedSeedWalletImports() throws Exception { + Wallet wallet = createSeedWallet(); + ExtendedKey expectedXpub = wallet.getKeystores().getFirst().getExtendedPublicKey(); + File walletFile = saveWallet(wallet, "pass"); + + Wallet imported = importWallet(walletFile, "pass"); + Assertions.assertEquals(expectedXpub, imported.getKeystores().getFirst().getExtendedPublicKey()); + Assertions.assertDoesNotThrow(imported::checkWallet); + } + + @Test + public void unencryptedSeedWalletImports() throws Exception { + Wallet wallet = createSeedWallet(); + ExtendedKey expectedXpub = wallet.getKeystores().getFirst().getExtendedPublicKey(); + File walletFile = saveWallet(wallet, null); + + Wallet imported = importWallet(walletFile, null); + Assertions.assertEquals(expectedXpub, imported.getKeystores().getFirst().getExtendedPublicKey()); + Assertions.assertDoesNotThrow(imported::checkWallet); + } + + @Test + public void watchOnlyWalletImports() throws Exception { + File walletFile = saveWallet(createWatchOnlyWallet(), null); + + Wallet imported = importWallet(walletFile, null); + Assertions.assertEquals(ExtendedKey.fromDescriptor(TEST_XPUB), imported.getKeystores().getFirst().getExtendedPublicKey()); + Assertions.assertDoesNotThrow(imported::checkWallet); + } +}