diff --git a/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java b/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java index 0b82c56..a292b36 100644 --- a/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java +++ b/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java @@ -29,6 +29,12 @@ public class KeyDerivation { return Collections.unmodifiableList(derivation); } + public KeyDerivation extend(ChildNumber childNumber) { + List extendedDerivation = new ArrayList<>(derivation); + extendedDerivation.add(childNumber); + return new KeyDerivation(masterFingerprint, writePath(extendedDerivation)); + } + public static List parsePath(String path) { return parsePath(path, 0); } diff --git a/src/main/java/com/sparrowwallet/drongo/Utils.java b/src/main/java/com/sparrowwallet/drongo/Utils.java index 2410f8a..827e777 100644 --- a/src/main/java/com/sparrowwallet/drongo/Utils.java +++ b/src/main/java/com/sparrowwallet/drongo/Utils.java @@ -284,4 +284,23 @@ public class Utils { hmacSha512.doFinal(out, 0); return out; } + + public static class LexicographicByteArrayComparator implements Comparator { + @Override + public int compare(byte[] left, byte[] right) { + int minLength = Math.min(left.length, right.length); + for (int i = 0; i < minLength; i++) { + int result = compare(left[i], right[i]); + if (result != 0) { + return result; + } + } + + return left.length - right.length; + } + + public static int compare(byte a, byte b) { + return Byte.toUnsignedInt(a) - Byte.toUnsignedInt(b); + } + } } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index 46531d7..dcbe07a 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -1,6 +1,7 @@ package com.sparrowwallet.drongo.protocol; import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.*; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; @@ -23,6 +24,16 @@ public enum ScriptType { return new P2PKAddress(pubKey); } + @Override + public Address getAddress(ECKey key) { + return getAddress(key.getPubKey()); + } + + @Override + public Address getAddress(Script script) { + throw new ProtocolException("No script derived address for non pay to script type"); + } + @Override public Address[] getAddresses(Script script) { return new Address[] { getAddress(getPublicKeyFromScript(script).getPubKey()) }; @@ -37,6 +48,16 @@ public enum ScriptType { return new Script(chunks); } + @Override + public Script getOutputScript(ECKey key) { + return getOutputScript(key.getPubKey()); + } + + @Override + public Script getOutputScript(Script script) { + throw new ProtocolException("No script derived output script for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -75,6 +96,16 @@ public enum ScriptType { return new P2PKHAddress(pubKeyHash); } + @Override + public Address getAddress(ECKey key) { + return getAddress(key.getPubKeyHash()); + } + + @Override + public Address getAddress(Script script) { + throw new ProtocolException("No script derived address for non pay to script type"); + } + @Override public Script getOutputScript(byte[] pubKeyHash) { List chunks = new ArrayList<>(); @@ -87,6 +118,16 @@ public enum ScriptType { return new Script(chunks); } + @Override + public Script getOutputScript(ECKey key) { + return getOutputScript(key.getPubKeyHash()); + } + + @Override + public Script getOutputScript(Script script) { + throw new ProtocolException("No script derived output script for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -124,6 +165,16 @@ public enum ScriptType { throw new ProtocolException("No single address for multisig script type"); } + @Override + public Address getAddress(Script script) { + throw new ProtocolException("No single address for multisig script type"); + } + + @Override + public Address getAddress(ECKey key) { + throw new ProtocolException("No single key address for multisig script type"); + } + @Override public Address[] getAddresses(Script script) { return Arrays.stream(getPublicKeysFromScript(script)).map(pubKey -> new P2PKAddress(pubKey.getPubKey())).toArray(Address[]::new); @@ -131,16 +182,37 @@ public enum ScriptType { @Override public Script getOutputScript(byte[] bytes) { - throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, byte[] pubKey1, byte[] pubKey2, ...)"); + throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, List pubKeys)"); } - public Script getOutputScript(int threshold, byte[] ...pubKeys) { + @Override + public Script getOutputScript(ECKey key) { + throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, List pubKeys)"); + } + + @Override + public Script getOutputScript(Script script) { + if(isScriptType(script)) { + return script; + } + + throw new ProtocolException("No script derived output script for non pay to script type"); + } + + @Override + public Script getOutputScript(int threshold, List pubKeys) { + List pubKeyBytes = new ArrayList<>(); + for(ECKey key : pubKeys) { + pubKeyBytes.add(key.getPubKey()); + } + pubKeyBytes.sort(new Utils.LexicographicByteArrayComparator()); + List chunks = new ArrayList<>(); chunks.add(new ScriptChunk(Script.encodeToOpN(threshold), null)); - for(byte[] pubKey : pubKeys) { + for(byte[] pubKey : pubKeyBytes) { chunks.add(new ScriptChunk(pubKey.length, pubKey)); } - chunks.add(new ScriptChunk(Script.encodeToOpN(pubKeys.length), null)); + chunks.add(new ScriptChunk(Script.encodeToOpN(pubKeys.size()), null)); chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null)); return new Script(chunks); } @@ -200,20 +272,40 @@ public enum ScriptType { }, P2SH("P2SH", "m/45'/0'/0'") { @Override - public Address getAddress(byte[] bytes) { - return new P2SHAddress(bytes); + public Address getAddress(byte[] scriptHash) { + return new P2SHAddress(scriptHash); } @Override - public Script getOutputScript(byte[] bytes) { + public Address getAddress(ECKey key) { + throw new ProtocolException("No single key address for script hash type"); + } + + @Override + public Address getAddress(Script script) { + return getAddress(Utils.sha256hash160(script.getProgram())); + } + + @Override + public Script getOutputScript(byte[] scriptHash) { List chunks = new ArrayList<>(); chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null)); - chunks.add(new ScriptChunk(bytes.length, bytes)); + chunks.add(new ScriptChunk(scriptHash.length, scriptHash)); chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null)); return new Script(chunks); } + @Override + public Script getOutputScript(ECKey key) { + throw new ProtocolException("No single key output script for script hash type"); + } + + @Override + public Script getOutputScript(Script script) { + return getOutputScript(Utils.sha256hash160(script.getProgram())); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -251,13 +343,43 @@ public enum ScriptType { }, P2SH_P2WPKH("P2SH-P2WPKH", "m/49'/0'/0'") { @Override - public Address getAddress(byte[] bytes) { - return P2SH.getAddress(bytes); + public Address getAddress(byte[] scriptHash) { + return P2SH.getAddress(scriptHash); } @Override - public Script getOutputScript(byte[] bytes) { - return P2SH.getOutputScript(bytes); + public Address getAddress(ECKey key) { + Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash()); + return P2SH.getAddress(p2wpkhScript); + } + + @Override + public Address getAddress(Script script) { + if(P2WPKH.isScriptType(script)) { + return P2SH.getAddress(script); + } + + throw new ProtocolException("Provided script is not a P2WPKH script"); + } + + @Override + public Script getOutputScript(byte[] scriptHash) { + return P2SH.getOutputScript(scriptHash); + } + + @Override + public Script getOutputScript(ECKey key) { + Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash()); + return P2SH.getOutputScript(p2wpkhScript); + } + + @Override + public Script getOutputScript(Script script) { + if(P2WPKH.isScriptType(script)) { + return P2SH.getOutputScript(script); + } + + throw new ProtocolException("Provided script is not a P2WPKH script"); } @Override @@ -277,13 +399,35 @@ public enum ScriptType { }, P2SH_P2WSH("P2SH-P2WSH", "m/48'/0'/0'/1'") { @Override - public Address getAddress(byte[] bytes) { - return P2SH.getAddress(bytes); + public Address getAddress(byte[] scriptHash) { + return P2SH.getAddress(scriptHash); } @Override - public Script getOutputScript(byte[] bytes) { - return P2SH.getOutputScript(bytes); + public Address getAddress(ECKey key) { + throw new ProtocolException("No single key address for wrapped witness script hash type"); + } + + @Override + public Address getAddress(Script script) { + Script p2wshScript = P2WSH.getOutputScript(script); + return P2SH.getAddress(p2wshScript); + } + + @Override + public Script getOutputScript(byte[] scriptHash) { + return P2SH.getOutputScript(scriptHash); + } + + @Override + public Script getOutputScript(ECKey key) { + throw new ProtocolException("No single key output script for wrapped witness script hash type"); + } + + @Override + public Script getOutputScript(Script script) { + Script p2wshScript = P2WSH.getOutputScript(script); + return P2SH.getOutputScript(p2wshScript); } @Override @@ -303,19 +447,39 @@ public enum ScriptType { }, P2WPKH("P2WPKH", "m/84'/0'/0'") { @Override - public Address getAddress(byte[] bytes) { - return new P2WPKHAddress(bytes); + public Address getAddress(byte[] pubKeyHash) { + return new P2WPKHAddress(pubKeyHash); } @Override - public Script getOutputScript(byte[] bytes) { + public Address getAddress(ECKey key) { + return getAddress(key.getPubKeyHash()); + } + + @Override + public Address getAddress(Script script) { + throw new ProtocolException("No script derived address for non pay to script type"); + } + + @Override + public Script getOutputScript(byte[] pubKeyHash) { List chunks = new ArrayList<>(); chunks.add(new ScriptChunk(OP_0, null)); - chunks.add(new ScriptChunk(bytes.length, bytes)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); return new Script(chunks); } + @Override + public Script getOutputScript(ECKey key) { + return getOutputScript(key.getPubKeyHash()); + } + + @Override + public Script getOutputScript(Script script) { + throw new ProtocolException("No script derived output script for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -343,19 +507,39 @@ public enum ScriptType { }, P2WSH("P2WSH", "m/48'/0'/0'/2'") { @Override - public Address getAddress(byte[] bytes) { - return new P2WSHAddress(bytes); + public Address getAddress(byte[] scriptHash) { + return new P2WSHAddress(scriptHash); } @Override - public Script getOutputScript(byte[] bytes) { + public Address getAddress(ECKey key) { + throw new ProtocolException("No single key address for witness script hash type"); + } + + @Override + public Address getAddress(Script script) { + return getAddress(Sha256Hash.hash(script.getProgram())); + } + + @Override + public Script getOutputScript(byte[] scriptHash) { List chunks = new ArrayList<>(); chunks.add(new ScriptChunk(OP_0, null)); - chunks.add(new ScriptChunk(bytes.length, bytes)); + chunks.add(new ScriptChunk(scriptHash.length, scriptHash)); return new Script(chunks); } + @Override + public Script getOutputScript(ECKey key) { + throw new ProtocolException("No single key output script for witness script hash type"); + } + + @Override + public Script getOutputScript(Script script) { + return getOutputScript(Sha256Hash.hash(script.getProgram())); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -432,8 +616,20 @@ public enum ScriptType { public abstract Address getAddress(byte[] bytes); + public abstract Address getAddress(ECKey key); + + public abstract Address getAddress(Script script); + public abstract Script getOutputScript(byte[] bytes); + public abstract Script getOutputScript(ECKey key); + + public abstract Script getOutputScript(Script script); + + public Script getOutputScript(int threshold, List pubKeys) { + throw new UnsupportedOperationException("Only defined for MULTISIG script type"); + } + public abstract boolean isScriptType(Script script); public abstract byte[] getHashFromScript(Script script); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index bdb347d..1421124 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -103,6 +103,24 @@ public class Keystore { return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1)); } + public DeterministicKey getReceivingKey(int keyIndex) { + List receivingDerivation = List.of(extendedPublicKey.getKeyChildNumber(), new ChildNumber(0), new ChildNumber(keyIndex)); + return extendedPublicKey.getKey(receivingDerivation); + } + + public KeyDerivation getReceivingDerivation(int keyIndex) { + return getKeyDerivation().extend(new ChildNumber(0)).extend(new ChildNumber(keyIndex)); + } + + public DeterministicKey getChangeKey(int keyIndex) { + List receivingDerivation = List.of(extendedPublicKey.getKeyChildNumber(), new ChildNumber(1), new ChildNumber(keyIndex)); + return extendedPublicKey.getKey(receivingDerivation); + } + + public KeyDerivation getChangeDerivation(int keyIndex) { + return getKeyDerivation().extend(new ChildNumber(1)).extend(new ChildNumber(keyIndex)); + } + public boolean isValid() { if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) { return false; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 24791c2..17ebbfd 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -1,11 +1,16 @@ package com.sparrowwallet.drongo.wallet; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.crypto.DeterministicKey; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.Key; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; import java.util.*; +import java.util.stream.Collectors; public class Wallet { @@ -70,6 +75,34 @@ public class Wallet { this.keystores = keystores; } + public Address getReceivingAddress(int index) { + if(policyType == PolicyType.SINGLE) { + Keystore keystore = getKeystores().get(0); + DeterministicKey key = keystore.getReceivingKey(index); + return scriptType.getAddress(key); + } else if(policyType == PolicyType.MULTI) { + List pubKeys = getKeystores().stream().map(keystore -> keystore.getReceivingKey(index)).collect(Collectors.toList()); + Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); + return scriptType.getAddress(script); + } else { + throw new UnsupportedOperationException("Cannot determine receiving addresses for custom policies"); + } + } + + public Address getChangeAddress(int index) { + if(policyType == PolicyType.SINGLE) { + Keystore keystore = getKeystores().get(0); + DeterministicKey key = keystore.getChangeKey(index); + return scriptType.getAddress(key); + } else if(policyType == PolicyType.MULTI) { + List pubKeys = getKeystores().stream().map(keystore -> keystore.getChangeKey(index)).collect(Collectors.toList()); + Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); + return scriptType.getAddress(script); + } else { + throw new UnsupportedOperationException("Cannot determine change addresses for custom policies"); + } + } + public boolean isValid() { if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) { return false; diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java index fce3d5e..402b5c5 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java @@ -66,4 +66,124 @@ public class WalletTest { Assert.assertEquals("Electrum 1", eekeystore.getLabel()); Assert.assertEquals("Electrum 2", eekeystore2.getLabel()); } + + @Test + public void p2pkhDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(ScriptType.P2PKH); + Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation()); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); + + Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getReceivingAddress(1).toString()); + } + + @Test + public void p2shP2wpkhDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(ScriptType.P2SH_P2WPKH); + Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation()); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1)); + + Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getReceivingAddress(1).toString()); + } + + @Test + public void p2wpkhDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(ScriptType.P2WPKH); + Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation()); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1)); + + Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getReceivingAddress(1).toString()); + } + + @Test + public void p2shDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + + String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn"; + DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39); + + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.MULTI); + wallet.setScriptType(ScriptType.P2SH); + Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore); + Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore2); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2)); + + Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getChangeAddress(1).toString()); + } + + @Test + public void p2shP2wshDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + + String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn"; + DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39); + + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.MULTI); + wallet.setScriptType(ScriptType.P2SH_P2WSH); + Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore); + Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore2); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2)); + + Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getChangeAddress(1).toString()); + } + + @Test + public void p2wshDerivationTest() throws MnemonicException { + String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; + DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39); + + String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn"; + DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39); + + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.MULTI); + wallet.setScriptType(ScriptType.P2WSH); + Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore); + Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation()); + Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString()); + Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString()); + wallet.getKeystores().add(keystore2); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2)); + + Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getChangeAddress(1).toString()); + } }