mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +00:00
add multisig output descriptors
This commit is contained in:
parent
e8f9e329a7
commit
45154359e9
6 changed files with 408 additions and 182 deletions
185
src/main/java/com/craigraw/drongo/ExtendedPublicKey.java
Normal file
185
src/main/java/com/craigraw/drongo/ExtendedPublicKey.java
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.crypto.*;
|
||||||
|
import com.craigraw.drongo.protocol.Base58;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ExtendedPublicKey {
|
||||||
|
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
|
||||||
|
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
|
||||||
|
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
|
||||||
|
private static final int bip32HeaderP2WHSHPub = 0x2AA7ED3; // The 4 byte header that serializes in base58 to "Zpub"
|
||||||
|
|
||||||
|
private int parentFingerprint;
|
||||||
|
private String keyDerivationPath;
|
||||||
|
private DeterministicKey pubKey;
|
||||||
|
private String childDerivationPath;
|
||||||
|
private ChildNumber pubKeyChildNumber;
|
||||||
|
|
||||||
|
private DeterministicHierarchy hierarchy;
|
||||||
|
|
||||||
|
public ExtendedPublicKey(int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
|
||||||
|
this.parentFingerprint = parentFingerprint;
|
||||||
|
this.keyDerivationPath = keyDerivationPath;
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
this.childDerivationPath = childDerivationPath;
|
||||||
|
this.pubKeyChildNumber = pubKeyChildNumber;
|
||||||
|
|
||||||
|
this.hierarchy = new DeterministicHierarchy(pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getParentFingerprint() {
|
||||||
|
return parentFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getKeyDerivation() {
|
||||||
|
return parsePath(keyDerivationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getPubKey() {
|
||||||
|
return pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChildDerivation() {
|
||||||
|
return getChildDerivation(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChildDerivation(int wildCardReplacement) {
|
||||||
|
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean describesMultipleAddresses() {
|
||||||
|
return childDerivationPath.endsWith("/*");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
|
||||||
|
if(describesMultipleAddresses()) {
|
||||||
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
|
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
|
||||||
|
if(describesMultipleAddresses()) {
|
||||||
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
|
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
|
||||||
|
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
|
||||||
|
List<ChildNumber> path = new ArrayList<>();
|
||||||
|
path.add(firstChild);
|
||||||
|
path.addAll(parsePath(derivationPath, wildCardReplacement));
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getKey(List<ChildNumber> path) {
|
||||||
|
return hierarchy.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChildNumber> parsePath(String path) {
|
||||||
|
return parsePath(path, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
|
||||||
|
String[] parsedNodes = path.replace("M", "").split("/");
|
||||||
|
List<ChildNumber> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String n : parsedNodes) {
|
||||||
|
n = n.replaceAll(" ", "");
|
||||||
|
if (n.length() == 0) continue;
|
||||||
|
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
|
||||||
|
if (isHard) n = n.substring(0, n.length() - 1);
|
||||||
|
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
|
||||||
|
int nodeNumber = Integer.parseInt(n);
|
||||||
|
nodes.add(new ChildNumber(nodeNumber, isHard));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(getExtendedPublicKey());
|
||||||
|
builder.append(childDerivationPath);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtendedPublicKey() {
|
||||||
|
return Base58.encodeChecked(getExtendedPublicKeyBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getExtendedPublicKeyBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(78);
|
||||||
|
buffer.putInt(bip32HeaderP2PKHXPub);
|
||||||
|
|
||||||
|
List<ChildNumber> childPath = parsePath(childDerivationPath);
|
||||||
|
int depth = 5 - childPath.size();
|
||||||
|
buffer.put((byte)depth);
|
||||||
|
|
||||||
|
buffer.putInt(parentFingerprint);
|
||||||
|
|
||||||
|
buffer.putInt(pubKeyChildNumber.i());
|
||||||
|
|
||||||
|
buffer.put(pubKey.getChainCode());
|
||||||
|
buffer.put(pubKey.getPubKey());
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExtendedPublicKey fromDescriptor(String keyDerivationPath, String extPubKey, String childDerivationPath) {
|
||||||
|
byte[] serializedKey = Base58.decodeChecked(extPubKey);
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
||||||
|
int header = buffer.getInt();
|
||||||
|
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub || header == bip32HeaderP2WHSHPub)) {
|
||||||
|
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
|
||||||
|
final int parentFingerprint = buffer.getInt();
|
||||||
|
final int i = buffer.getInt();
|
||||||
|
ChildNumber childNumber;
|
||||||
|
List<ChildNumber> path;
|
||||||
|
|
||||||
|
if(depth == 0) {
|
||||||
|
//Poorly formatted extended public key, add first child path element
|
||||||
|
childNumber = new ChildNumber(0, false);
|
||||||
|
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
||||||
|
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
||||||
|
} else {
|
||||||
|
childNumber = new ChildNumber(i, false);
|
||||||
|
}
|
||||||
|
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
|
||||||
|
|
||||||
|
byte[] chainCode = new byte[32];
|
||||||
|
buffer.get(chainCode);
|
||||||
|
byte[] data = new byte[33];
|
||||||
|
buffer.get(data);
|
||||||
|
if(buffer.hasRemaining()) {
|
||||||
|
throw new IllegalArgumentException("Found unexpected data in key");
|
||||||
|
}
|
||||||
|
|
||||||
|
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
|
||||||
|
return new ExtendedPublicKey(parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,113 +1,114 @@
|
||||||
package com.craigraw.drongo;
|
package com.craigraw.drongo;
|
||||||
|
|
||||||
import com.craigraw.drongo.address.Address;
|
import com.craigraw.drongo.address.*;
|
||||||
import com.craigraw.drongo.address.P2PKHAddress;
|
|
||||||
import com.craigraw.drongo.address.P2SHAddress;
|
|
||||||
import com.craigraw.drongo.address.P2WPKHAddress;
|
|
||||||
import com.craigraw.drongo.crypto.ChildNumber;
|
import com.craigraw.drongo.crypto.ChildNumber;
|
||||||
import com.craigraw.drongo.crypto.DeterministicKey;
|
import com.craigraw.drongo.crypto.DeterministicKey;
|
||||||
import com.craigraw.drongo.crypto.ECKey;
|
|
||||||
import com.craigraw.drongo.crypto.LazyECPoint;
|
|
||||||
import com.craigraw.drongo.protocol.Base58;
|
|
||||||
import com.craigraw.drongo.protocol.Script;
|
import com.craigraw.drongo.protocol.Script;
|
||||||
import org.slf4j.Logger;
|
import com.craigraw.drongo.protocol.ScriptChunk;
|
||||||
import org.slf4j.LoggerFactory;
|
import com.craigraw.drongo.protocol.ScriptOpCodes;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class OutputDescriptor {
|
public class OutputDescriptor {
|
||||||
private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class);
|
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?");
|
||||||
|
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
||||||
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
|
|
||||||
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
|
|
||||||
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
|
|
||||||
|
|
||||||
private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?");
|
|
||||||
|
|
||||||
private String script;
|
private String script;
|
||||||
private int parentFingerprint;
|
private int multisigThreshold;
|
||||||
private String keyDerivationPath;
|
private List<ExtendedPublicKey> extendedPublicKeys;
|
||||||
private DeterministicKey pubKey;
|
|
||||||
private String childDerivationPath;
|
|
||||||
private ChildNumber pubKeyChildNumber;
|
|
||||||
|
|
||||||
public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
|
public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey) {
|
||||||
|
this(script, Collections.singletonList(extendedPublicKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputDescriptor(String script, List<ExtendedPublicKey> extendedPublicKeys) {
|
||||||
|
this(script, 0, extendedPublicKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputDescriptor(String script, int multisigThreshold, List<ExtendedPublicKey> extendedPublicKeys) {
|
||||||
this.script = script;
|
this.script = script;
|
||||||
this.parentFingerprint = parentFingerprint;
|
this.multisigThreshold = multisigThreshold;
|
||||||
this.keyDerivationPath = keyDerivationPath;
|
this.extendedPublicKeys = extendedPublicKeys;
|
||||||
this.pubKey = pubKey;
|
}
|
||||||
this.childDerivationPath = childDerivationPath;
|
|
||||||
this.pubKeyChildNumber = pubKeyChildNumber;
|
public List<ExtendedPublicKey> getExtendedPublicKeys() {
|
||||||
|
return extendedPublicKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultisig() {
|
||||||
|
return extendedPublicKeys.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtendedPublicKey getSingletonExtendedPublicKey() {
|
||||||
|
if(isMultisig()) {
|
||||||
|
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
return extendedPublicKeys.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getScript() {
|
public String getScript() {
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getParentFingerprint() {
|
public boolean describesMultipleAddresses() {
|
||||||
return parentFingerprint;
|
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
|
||||||
}
|
if(!pubKey.describesMultipleAddresses()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getKeyDerivation() {
|
return true;
|
||||||
return parsePath(keyDerivationPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeterministicKey getPubKey() {
|
|
||||||
return pubKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getChildDerivation() {
|
public List<ChildNumber> getChildDerivation() {
|
||||||
return getChildDerivation(0);
|
List<ChildNumber> lastDerivation = null;
|
||||||
}
|
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
|
||||||
|
List<ChildNumber> derivation = pubKey.getChildDerivation();
|
||||||
|
if(lastDerivation != null && !lastDerivation.subList(1, lastDerivation.size()).equals(derivation.subList(1, derivation.size()))) {
|
||||||
|
throw new IllegalStateException("Cannot determine multisig derivation: constituent derivations do not match");
|
||||||
|
}
|
||||||
|
lastDerivation = derivation;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getChildDerivation(int wildCardReplacement) {
|
return lastDerivation;
|
||||||
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean describesMultipleAddresses() {
|
|
||||||
return childDerivationPath.endsWith("/*");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
|
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
|
||||||
if(describesMultipleAddresses()) {
|
if(isMultisig()) {
|
||||||
if(childDerivationPath.endsWith("0/*")) {
|
List<ChildNumber> path = new ArrayList<>();
|
||||||
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
path.add(new ChildNumber(0));
|
||||||
}
|
path.add(new ChildNumber(wildCardReplacement));
|
||||||
|
return path;
|
||||||
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) {
|
|
||||||
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
|
return getSingletonExtendedPublicKey().getReceivingDerivation(wildCardReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
|
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
|
||||||
if(describesMultipleAddresses()) {
|
if(isMultisig()) {
|
||||||
if(childDerivationPath.endsWith("0/*")) {
|
List<ChildNumber> path = new ArrayList<>();
|
||||||
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
path.add(new ChildNumber(1));
|
||||||
}
|
path.add(new ChildNumber(wildCardReplacement));
|
||||||
|
return path;
|
||||||
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
|
|
||||||
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
|
return getSingletonExtendedPublicKey().getChangeDerivation(wildCardReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
|
public Address getAddress(List<ChildNumber> path) {
|
||||||
List<ChildNumber> path = new ArrayList<>();
|
if(isMultisig()) {
|
||||||
path.add(firstChild);
|
Script script = getMultisigScript(path);
|
||||||
path.addAll(parsePath(derivationPath, wildCardReplacement));
|
return getAddress(script);
|
||||||
|
}
|
||||||
|
|
||||||
return path;
|
DeterministicKey childKey = getSingletonExtendedPublicKey().getKey(path);
|
||||||
|
return getAddress(childKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Address getAddress(DeterministicKey childKey) {
|
public Address getAddress(DeterministicKey childKey) {
|
||||||
|
@ -127,105 +128,110 @@ public class OutputDescriptor {
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Address getAddress(Script multisigScript) {
|
||||||
|
Address address = null;
|
||||||
|
if(script.equals("sh(multi")) {
|
||||||
|
address = P2SHAddress.fromProgram(multisigScript.getProgram());
|
||||||
|
} else if(script.equals("wsh(multi")) {
|
||||||
|
address = P2WSHAddress.fromProgram(multisigScript.getProgram());
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Cannot determine address for multisig script " + script);
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Script getMultisigScript(List<ChildNumber> path) {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
|
||||||
|
|
||||||
|
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
|
||||||
|
List<ChildNumber> keyPath = null;
|
||||||
|
if(path.get(0).num() == 0) {
|
||||||
|
keyPath = pubKey.getReceivingDerivation(path.get(1).num());
|
||||||
|
} else if(path.get(0).num() == 1) {
|
||||||
|
keyPath = pubKey.getChangeDerivation(path.get(1).num());
|
||||||
|
} else {
|
||||||
|
keyPath = pubKey.getChildDerivation(path.get(1).num());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pubKeyBytes = pubKey.getKey(keyPath).getPubKey();
|
||||||
|
chunks.add(new ScriptChunk(pubKeyBytes.length, pubKeyBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.add(new ScriptChunk(Script.encodeToOpN(extendedPublicKeys.size()), null));
|
||||||
|
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||||
public static OutputDescriptor getOutputDescriptor(String descriptor) {
|
public static OutputDescriptor getOutputDescriptor(String descriptor) {
|
||||||
String script;
|
if(descriptor.startsWith("pkh") || descriptor.startsWith("xpub")) {
|
||||||
String keyDerivationPath ="";
|
return new OutputDescriptor("pkh", getExtendedPublicKeys(descriptor));
|
||||||
String extPubKey = null;
|
} else if(descriptor.startsWith("wpkh") || descriptor.startsWith("zpub")) {
|
||||||
String childDerivationPath = "/0/*";
|
return new OutputDescriptor("wpkh", getExtendedPublicKeys(descriptor));
|
||||||
|
} else if(descriptor.startsWith("sh(wpkh") || descriptor.startsWith("ypub")) {
|
||||||
Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor);
|
return new OutputDescriptor("sh(wpkh", getExtendedPublicKeys(descriptor));
|
||||||
if(matcher.matches()) {
|
} else if(descriptor.startsWith("sh(multi") || descriptor.startsWith("Ypub")) {
|
||||||
script = matcher.group(1);
|
return new OutputDescriptor("sh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor));
|
||||||
if(matcher.group(2) != null) {
|
} else if(descriptor.startsWith("wsh(multi") || descriptor.startsWith("Zpub")) {
|
||||||
keyDerivationPath = matcher.group(2);
|
return new OutputDescriptor("wsh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor));
|
||||||
}
|
|
||||||
|
|
||||||
extPubKey = matcher.group(3);
|
|
||||||
if(matcher.group(4) != null) {
|
|
||||||
childDerivationPath = matcher.group(4);
|
|
||||||
}
|
|
||||||
} else if (descriptor.startsWith("xpub")) {
|
|
||||||
extPubKey = descriptor;
|
|
||||||
script = "pkh";
|
|
||||||
} else if(descriptor.startsWith("ypub")) {
|
|
||||||
extPubKey = descriptor;
|
|
||||||
script = "sh(wpkh";
|
|
||||||
} else if(descriptor.startsWith("zpub")) {
|
|
||||||
extPubKey = descriptor;
|
|
||||||
script = "wpkh";
|
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
|
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
byte[] serializedKey = Base58.decodeChecked(extPubKey);
|
private static int getMultsigThreshold(String descriptor) {
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
Matcher matcher = MULTI_PATTERN.matcher(descriptor);
|
||||||
int header = buffer.getInt();
|
if(matcher.find()) {
|
||||||
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) {
|
String threshold = matcher.group(1);
|
||||||
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
return Integer.parseInt(threshold);
|
||||||
}
|
|
||||||
|
|
||||||
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
|
|
||||||
final int parentFingerprint = buffer.getInt();
|
|
||||||
final int i = buffer.getInt();
|
|
||||||
ChildNumber childNumber;
|
|
||||||
List<ChildNumber> path;
|
|
||||||
|
|
||||||
if(depth == 0) {
|
|
||||||
//Poorly formatted extended public key, add first child path element
|
|
||||||
childNumber = new ChildNumber(0, false);
|
|
||||||
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
|
||||||
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
|
||||||
} else {
|
} else {
|
||||||
childNumber = new ChildNumber(i, false);
|
throw new IllegalArgumentException("Could not find multisig threshold in output descriptor:" + descriptor);
|
||||||
}
|
}
|
||||||
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
|
|
||||||
|
|
||||||
//Remove account level for depth 4 keys
|
|
||||||
if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) {
|
|
||||||
log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available");
|
|
||||||
childDerivationPath = "/*";
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] chainCode = new byte[32];
|
|
||||||
buffer.get(chainCode);
|
|
||||||
byte[] data = new byte[33];
|
|
||||||
buffer.get(data);
|
|
||||||
if(buffer.hasRemaining()) {
|
|
||||||
throw new IllegalArgumentException("Found unexpected data in key");
|
|
||||||
}
|
|
||||||
|
|
||||||
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
|
|
||||||
return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ChildNumber> parsePath(String path) {
|
private static List<ExtendedPublicKey> getExtendedPublicKeys(String descriptor) {
|
||||||
return parsePath(path, 0);
|
List<ExtendedPublicKey> keys = new ArrayList<>();
|
||||||
}
|
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
|
||||||
|
while(matcher.find()) {
|
||||||
|
String keyDerivationPath ="";
|
||||||
|
String extPubKey = null;
|
||||||
|
String childDerivationPath = "/0/*";
|
||||||
|
|
||||||
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
|
if(matcher.group(1) != null) {
|
||||||
String[] parsedNodes = path.replace("M", "").split("/");
|
keyDerivationPath = matcher.group(1);
|
||||||
List<ChildNumber> nodes = new ArrayList<>();
|
}
|
||||||
|
|
||||||
for (String n : parsedNodes) {
|
extPubKey = matcher.group(2);
|
||||||
n = n.replaceAll(" ", "");
|
if(matcher.group(3) != null) {
|
||||||
if (n.length() == 0) continue;
|
childDerivationPath = matcher.group(3);
|
||||||
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
|
}
|
||||||
if (isHard) n = n.substring(0, n.length() - 1);
|
|
||||||
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
|
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(keyDerivationPath, extPubKey, childDerivationPath);
|
||||||
int nodeNumber = Integer.parseInt(n);
|
keys.add(extendedPublicKey);
|
||||||
nodes.add(new ChildNumber(nodeNumber, isHard));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(script);
|
builder.append(script);
|
||||||
builder.append("(");
|
builder.append("(");
|
||||||
builder.append(getExtendedPublicKey());
|
|
||||||
builder.append(childDerivationPath);
|
if(isMultisig()) {
|
||||||
|
StringJoiner joiner = new StringJoiner(",");
|
||||||
|
joiner.add(Integer.toString(multisigThreshold));
|
||||||
|
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
|
||||||
|
joiner.add(pubKey.toString());
|
||||||
|
}
|
||||||
|
builder.append(joiner.toString());
|
||||||
|
} else {
|
||||||
|
builder.append(getSingletonExtendedPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
builder.append(")");
|
builder.append(")");
|
||||||
|
|
||||||
if(script.contains("(")){
|
if(script.contains("(")){
|
||||||
|
@ -234,26 +240,4 @@ public class OutputDescriptor {
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExtendedPublicKey() {
|
|
||||||
return Base58.encodeChecked(getExtendedPublicKeyBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getExtendedPublicKeyBytes() {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(78);
|
|
||||||
buffer.putInt(bip32HeaderP2PKHXPub);
|
|
||||||
|
|
||||||
List<ChildNumber> childPath = parsePath(childDerivationPath);
|
|
||||||
int depth = 5 - childPath.size();
|
|
||||||
buffer.put((byte)depth);
|
|
||||||
|
|
||||||
buffer.putInt(parentFingerprint);
|
|
||||||
|
|
||||||
buffer.putInt(pubKeyChildNumber.i());
|
|
||||||
|
|
||||||
buffer.put(pubKey.getChainCode());
|
|
||||||
buffer.put(pubKey.getPubKey());
|
|
||||||
|
|
||||||
return buffer.array();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,32 +11,30 @@ public class WatchWallet {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private OutputDescriptor outputDescriptor;
|
private OutputDescriptor outputDescriptor;
|
||||||
private DeterministicHierarchy hierarchy;
|
|
||||||
|
|
||||||
private HashMap<Address,List<ChildNumber>> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2);
|
private HashMap<Address,List<ChildNumber>> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2);
|
||||||
|
|
||||||
public WatchWallet(String name, String descriptor) {
|
public WatchWallet(String name, String descriptor) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
|
this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
|
||||||
this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialiseAddresses() {
|
public void initialiseAddresses() {
|
||||||
if(outputDescriptor.describesMultipleAddresses()) {
|
if(outputDescriptor.describesMultipleAddresses()) {
|
||||||
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
||||||
List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index);
|
List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index);
|
||||||
Address address = getAddress(receivingDerivation);
|
Address address = getReceivingAddress(index);
|
||||||
addresses.put(address, receivingDerivation);
|
addresses.put(address, receivingDerivation);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
|
||||||
List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index);
|
List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index);
|
||||||
Address address = getAddress(changeDerivation);
|
Address address = getChangeAddress(index);
|
||||||
addresses.put(address, changeDerivation);
|
addresses.put(address, changeDerivation);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<ChildNumber> derivation = outputDescriptor.getChildDerivation();
|
List<ChildNumber> derivation = outputDescriptor.getChildDerivation();
|
||||||
Address address = getAddress(derivation);
|
Address address = outputDescriptor.getAddress(derivation);
|
||||||
addresses.put(address, derivation);
|
addresses.put(address, derivation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,8 +59,11 @@ public class WatchWallet {
|
||||||
return getAddress(outputDescriptor.getChangeDerivation(index));
|
return getAddress(outputDescriptor.getChangeDerivation(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress(List<ChildNumber> path) {
|
public OutputDescriptor getOutputDescriptor() {
|
||||||
DeterministicKey childKey = hierarchy.get(path);
|
return outputDescriptor;
|
||||||
return outputDescriptor.getAddress(childKey);
|
}
|
||||||
|
|
||||||
|
public Address getAddress(List<ChildNumber> path) {
|
||||||
|
return outputDescriptor.getAddress(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
src/main/java/com/craigraw/drongo/address/P2WSHAddress.java
Normal file
34
src/main/java/com/craigraw/drongo/address/P2WSHAddress.java
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package com.craigraw.drongo.address;
|
||||||
|
|
||||||
|
import com.craigraw.drongo.protocol.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.craigraw.drongo.address.P2WPKHAddress.HRP;
|
||||||
|
|
||||||
|
public class P2WSHAddress extends Address {
|
||||||
|
public P2WSHAddress(byte[] pubKeyHash) {
|
||||||
|
super(pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return Bech32.encode(HRP, getVersion(), pubKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Script getOutputScript() {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null));
|
||||||
|
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static P2WSHAddress fromProgram(byte[] program) {
|
||||||
|
return new P2WSHAddress(Sha256Hash.hash(program));
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ public class OutputDescriptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void iancolemanP2PKH() {
|
public void iancolemanP2PKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*");
|
||||||
Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString());
|
Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ public class OutputDescriptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void iancolemanP2SHP2WPKH() {
|
public void iancolemanP2SHP2WPKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*");
|
||||||
Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString());
|
Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ public class WatchWalletTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void iancolemanP2PKH() {
|
public void iancolemanP2PKH() {
|
||||||
WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z");
|
WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*");
|
||||||
|
|
||||||
Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString());
|
Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString());
|
||||||
Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString());
|
Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString());
|
||||||
|
@ -33,7 +33,7 @@ public class WatchWalletTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void iancolemanP2SHP2WPKH() {
|
public void iancolemanP2SHP2WPKH() {
|
||||||
WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os");
|
WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*");
|
||||||
|
|
||||||
Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString());
|
Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString());
|
||||||
Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString());
|
Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString());
|
||||||
|
@ -55,4 +55,26 @@ public class WatchWalletTest {
|
||||||
Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString());
|
Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString());
|
||||||
Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString());
|
Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2WSHMulti() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/*,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/*))");
|
||||||
|
|
||||||
|
Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("bc1qzw9j02k6l7z598edcgjh5mks507xevhk34rmnerxv45ptsluf0pqyxmyve", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2WSHMulti2() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,Zpub6yhnqjTYE82fc2U1UukQW6qEYsCcNoqsyPWPvL6Qi22YopXv8nD1a44zN87aUQcJr4YdE6DJKEA4xuBr5dzBQHZCBsbiUH7NAcFBgPyx3LB/0/*,Zpub74eXjdXzGCRsixpRAZT3U8ssQ15uhUa1dCFdRJvY3L2qo18He71qWUfpxfbL9e2EYuWKe1tH7qzgUSRVTAektLDVRKwCbAtyRW5j2yhqLiD/0/*))");
|
||||||
|
|
||||||
|
Assert.assertEquals("bc1qa842ug2njv36ycnhq8wjcg6wxjv7p7h4v0tnl40u6nfxxxffyjnq409pr9", wallet.getReceivingAddress(0).toString());
|
||||||
|
Assert.assertEquals("bc1q3auk6c8f77dda0w8y9dz4yd3wqhkf4eufzk8x2quszvzzcyjk6rqgz70pd", wallet.getChangeAddress(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void electrumP2WSHMultiSingle() {
|
||||||
|
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/0,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/0))");
|
||||||
|
Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getAddress(wallet.getOutputDescriptor().getChildDerivation()).toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue