mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
address and output script derivation support
This commit is contained in:
parent
785040898b
commit
de70f44535
6 changed files with 416 additions and 24 deletions
|
@ -29,6 +29,12 @@ public class KeyDerivation {
|
||||||
return Collections.unmodifiableList(derivation);
|
return Collections.unmodifiableList(derivation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeyDerivation extend(ChildNumber childNumber) {
|
||||||
|
List<ChildNumber> extendedDerivation = new ArrayList<>(derivation);
|
||||||
|
extendedDerivation.add(childNumber);
|
||||||
|
return new KeyDerivation(masterFingerprint, writePath(extendedDerivation));
|
||||||
|
}
|
||||||
|
|
||||||
public static List<ChildNumber> parsePath(String path) {
|
public static List<ChildNumber> parsePath(String path) {
|
||||||
return parsePath(path, 0);
|
return parsePath(path, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,4 +284,23 @@ public class Utils {
|
||||||
hmacSha512.doFinal(out, 0);
|
hmacSha512.doFinal(out, 0);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class LexicographicByteArrayComparator implements Comparator<byte[]> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sparrowwallet.drongo.protocol;
|
package com.sparrowwallet.drongo.protocol;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.*;
|
import com.sparrowwallet.drongo.address.*;
|
||||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
@ -23,6 +24,16 @@ public enum ScriptType {
|
||||||
return new P2PKAddress(pubKey);
|
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
|
@Override
|
||||||
public Address[] getAddresses(Script script) {
|
public Address[] getAddresses(Script script) {
|
||||||
return new Address[] { getAddress(getPublicKeyFromScript(script).getPubKey()) };
|
return new Address[] { getAddress(getPublicKeyFromScript(script).getPubKey()) };
|
||||||
|
@ -37,6 +48,16 @@ public enum ScriptType {
|
||||||
return new Script(chunks);
|
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
|
@Override
|
||||||
public boolean isScriptType(Script script) {
|
public boolean isScriptType(Script script) {
|
||||||
List<ScriptChunk> chunks = script.chunks;
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
@ -75,6 +96,16 @@ public enum ScriptType {
|
||||||
return new P2PKHAddress(pubKeyHash);
|
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
|
@Override
|
||||||
public Script getOutputScript(byte[] pubKeyHash) {
|
public Script getOutputScript(byte[] pubKeyHash) {
|
||||||
List<ScriptChunk> chunks = new ArrayList<>();
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
@ -87,6 +118,16 @@ public enum ScriptType {
|
||||||
return new Script(chunks);
|
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
|
@Override
|
||||||
public boolean isScriptType(Script script) {
|
public boolean isScriptType(Script script) {
|
||||||
List<ScriptChunk> chunks = script.chunks;
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
@ -124,6 +165,16 @@ public enum ScriptType {
|
||||||
throw new ProtocolException("No single address for multisig script type");
|
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
|
@Override
|
||||||
public Address[] getAddresses(Script script) {
|
public Address[] getAddresses(Script script) {
|
||||||
return Arrays.stream(getPublicKeysFromScript(script)).map(pubKey -> new P2PKAddress(pubKey.getPubKey())).toArray(Address[]::new);
|
return Arrays.stream(getPublicKeysFromScript(script)).map(pubKey -> new P2PKAddress(pubKey.getPubKey())).toArray(Address[]::new);
|
||||||
|
@ -131,16 +182,37 @@ public enum ScriptType {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Script getOutputScript(byte[] bytes) {
|
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<ECKey> 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<ECKey> 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<ECKey> pubKeys) {
|
||||||
|
List<byte[]> pubKeyBytes = new ArrayList<>();
|
||||||
|
for(ECKey key : pubKeys) {
|
||||||
|
pubKeyBytes.add(key.getPubKey());
|
||||||
|
}
|
||||||
|
pubKeyBytes.sort(new Utils.LexicographicByteArrayComparator());
|
||||||
|
|
||||||
List<ScriptChunk> chunks = new ArrayList<>();
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
chunks.add(new ScriptChunk(Script.encodeToOpN(threshold), null));
|
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(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));
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null));
|
||||||
return new Script(chunks);
|
return new Script(chunks);
|
||||||
}
|
}
|
||||||
|
@ -200,20 +272,40 @@ public enum ScriptType {
|
||||||
},
|
},
|
||||||
P2SH("P2SH", "m/45'/0'/0'") {
|
P2SH("P2SH", "m/45'/0'/0'") {
|
||||||
@Override
|
@Override
|
||||||
public Address getAddress(byte[] bytes) {
|
public Address getAddress(byte[] scriptHash) {
|
||||||
return new P2SHAddress(bytes);
|
return new P2SHAddress(scriptHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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<ScriptChunk> chunks = new ArrayList<>();
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
|
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));
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null));
|
||||||
|
|
||||||
return new Script(chunks);
|
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
|
@Override
|
||||||
public boolean isScriptType(Script script) {
|
public boolean isScriptType(Script script) {
|
||||||
List<ScriptChunk> chunks = script.chunks;
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
@ -251,13 +343,43 @@ public enum ScriptType {
|
||||||
},
|
},
|
||||||
P2SH_P2WPKH("P2SH-P2WPKH", "m/49'/0'/0'") {
|
P2SH_P2WPKH("P2SH-P2WPKH", "m/49'/0'/0'") {
|
||||||
@Override
|
@Override
|
||||||
public Address getAddress(byte[] bytes) {
|
public Address getAddress(byte[] scriptHash) {
|
||||||
return P2SH.getAddress(bytes);
|
return P2SH.getAddress(scriptHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Script getOutputScript(byte[] bytes) {
|
public Address getAddress(ECKey key) {
|
||||||
return P2SH.getOutputScript(bytes);
|
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
|
@Override
|
||||||
|
@ -277,13 +399,35 @@ public enum ScriptType {
|
||||||
},
|
},
|
||||||
P2SH_P2WSH("P2SH-P2WSH", "m/48'/0'/0'/1'") {
|
P2SH_P2WSH("P2SH-P2WSH", "m/48'/0'/0'/1'") {
|
||||||
@Override
|
@Override
|
||||||
public Address getAddress(byte[] bytes) {
|
public Address getAddress(byte[] scriptHash) {
|
||||||
return P2SH.getAddress(bytes);
|
return P2SH.getAddress(scriptHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Script getOutputScript(byte[] bytes) {
|
public Address getAddress(ECKey key) {
|
||||||
return P2SH.getOutputScript(bytes);
|
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
|
@Override
|
||||||
|
@ -303,19 +447,39 @@ public enum ScriptType {
|
||||||
},
|
},
|
||||||
P2WPKH("P2WPKH", "m/84'/0'/0'") {
|
P2WPKH("P2WPKH", "m/84'/0'/0'") {
|
||||||
@Override
|
@Override
|
||||||
public Address getAddress(byte[] bytes) {
|
public Address getAddress(byte[] pubKeyHash) {
|
||||||
return new P2WPKHAddress(bytes);
|
return new P2WPKHAddress(pubKeyHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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<ScriptChunk> chunks = new ArrayList<>();
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
chunks.add(new ScriptChunk(OP_0, null));
|
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);
|
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
|
@Override
|
||||||
public boolean isScriptType(Script script) {
|
public boolean isScriptType(Script script) {
|
||||||
List<ScriptChunk> chunks = script.chunks;
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
@ -343,19 +507,39 @@ public enum ScriptType {
|
||||||
},
|
},
|
||||||
P2WSH("P2WSH", "m/48'/0'/0'/2'") {
|
P2WSH("P2WSH", "m/48'/0'/0'/2'") {
|
||||||
@Override
|
@Override
|
||||||
public Address getAddress(byte[] bytes) {
|
public Address getAddress(byte[] scriptHash) {
|
||||||
return new P2WSHAddress(bytes);
|
return new P2WSHAddress(scriptHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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<ScriptChunk> chunks = new ArrayList<>();
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
chunks.add(new ScriptChunk(OP_0, null));
|
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);
|
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
|
@Override
|
||||||
public boolean isScriptType(Script script) {
|
public boolean isScriptType(Script script) {
|
||||||
List<ScriptChunk> chunks = script.chunks;
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
@ -432,8 +616,20 @@ public enum ScriptType {
|
||||||
|
|
||||||
public abstract Address getAddress(byte[] bytes);
|
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(byte[] bytes);
|
||||||
|
|
||||||
|
public abstract Script getOutputScript(ECKey key);
|
||||||
|
|
||||||
|
public abstract Script getOutputScript(Script script);
|
||||||
|
|
||||||
|
public Script getOutputScript(int threshold, List<ECKey> pubKeys) {
|
||||||
|
throw new UnsupportedOperationException("Only defined for MULTISIG script type");
|
||||||
|
}
|
||||||
|
|
||||||
public abstract boolean isScriptType(Script script);
|
public abstract boolean isScriptType(Script script);
|
||||||
|
|
||||||
public abstract byte[] getHashFromScript(Script script);
|
public abstract byte[] getHashFromScript(Script script);
|
||||||
|
|
|
@ -103,6 +103,24 @@ public class Keystore {
|
||||||
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getReceivingKey(int keyIndex) {
|
||||||
|
List<ChildNumber> 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<ChildNumber> 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() {
|
public boolean isValid() {
|
||||||
if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) {
|
if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
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.crypto.Key;
|
||||||
import com.sparrowwallet.drongo.policy.Policy;
|
import com.sparrowwallet.drongo.policy.Policy;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class Wallet {
|
public class Wallet {
|
||||||
|
|
||||||
|
@ -70,6 +75,34 @@ public class Wallet {
|
||||||
this.keystores = keystores;
|
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<ECKey> 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<ECKey> 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() {
|
public boolean isValid() {
|
||||||
if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) {
|
if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -66,4 +66,124 @@ public class WalletTest {
|
||||||
Assert.assertEquals("Electrum 1", eekeystore.getLabel());
|
Assert.assertEquals("Electrum 1", eekeystore.getLabel());
|
||||||
Assert.assertEquals("Electrum 2", eekeystore2.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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue