derive public keys from the seed when importing a sparrow wallet file

This commit is contained in:
Craig Raw
2026-09-16 10:21:38 +02:00
parent c4b53879cf
commit 1fb4e8bb48
3 changed files with 149 additions and 2 deletions
@@ -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) {
@@ -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<Path> paths = Files.walk(tempDir)) {
paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
}
}
}
@@ -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<Path> 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);
}
}