diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index dcbe07a..d74765c 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.PolicyType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static com.sparrowwallet.drongo.policy.PolicyType.*; @@ -58,6 +55,16 @@ public enum ScriptType { throw new ProtocolException("No script derived output script for non pay to script type"); } + @Override + public String getOutputDescriptor(ECKey key) { + return "pk(" + Utils.bytesToHex(key.getPubKey()) + ")"; + } + + @Override + public String getOutputDescriptor(Script script) { + throw new ProtocolException("No script derived output descriptor for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -128,6 +135,16 @@ public enum ScriptType { throw new ProtocolException("No script derived output script for non pay to script type"); } + @Override + public String getOutputDescriptor(ECKey key) { + return "pkh(" + Utils.bytesToHex(key.getPubKey()) + ")"; + } + + @Override + public String getOutputDescriptor(Script script) { + throw new ProtocolException("No script derived output descriptor for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -217,6 +234,34 @@ public enum ScriptType { return new Script(chunks); } + @Override + public String getOutputDescriptor(ECKey key) { + throw new ProtocolException("No single key output descriptor for multisig script type"); + } + + @Override + public String getOutputDescriptor(Script script) { + if(!isScriptType(script)) { + throw new IllegalArgumentException("Can only create output descriptor from multisig script"); + } + + int threshold = getThreshold(script); + ECKey[] pubKeys = getPublicKeysFromScript(script); + + List pubKeyBytes = new ArrayList<>(); + for(ECKey key : pubKeys) { + pubKeyBytes.add(key.getPubKey()); + } + pubKeyBytes.sort(new Utils.LexicographicByteArrayComparator()); + + StringJoiner joiner = new StringJoiner(","); + for(byte[] pubKey : pubKeyBytes) { + joiner.add(Utils.bytesToHex(pubKey)); + } + + return "multi(" + threshold + "," + joiner.toString() + ")"; + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -306,6 +351,20 @@ public enum ScriptType { return getOutputScript(Utils.sha256hash160(script.getProgram())); } + @Override + public String getOutputDescriptor(ECKey key) { + throw new ProtocolException("No single key output descriptor for script hash type"); + } + + @Override + public String getOutputDescriptor(Script script) { + if(!MULTISIG.isScriptType(script)) { + throw new IllegalArgumentException("Can only create output descriptor from multisig script"); + } + + return "sh(" + MULTISIG.getOutputDescriptor(script) + ")"; + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -382,6 +441,16 @@ public enum ScriptType { throw new ProtocolException("Provided script is not a P2WPKH script"); } + @Override + public String getOutputDescriptor(ECKey key) { + return "sh(wpkh(" + Utils.bytesToHex(key.getPubKey()) + "))"; + } + + @Override + public String getOutputDescriptor(Script script) { + throw new ProtocolException("No script derived output descriptor for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { return P2SH.isScriptType(script); @@ -430,6 +499,20 @@ public enum ScriptType { return P2SH.getOutputScript(p2wshScript); } + @Override + public String getOutputDescriptor(ECKey key) { + throw new ProtocolException("No single key output descriptor for script hash type"); + } + + @Override + public String getOutputDescriptor(Script script) { + if(!MULTISIG.isScriptType(script)) { + throw new IllegalArgumentException("Can only create output descriptor from multisig script"); + } + + return "sh(wsh(" + MULTISIG.getOutputDescriptor(script) + "))"; + } + @Override public boolean isScriptType(Script script) { return P2SH.isScriptType(script); @@ -480,6 +563,16 @@ public enum ScriptType { throw new ProtocolException("No script derived output script for non pay to script type"); } + @Override + public String getOutputDescriptor(ECKey key) { + return "wpkh(" + Utils.bytesToHex(key.getPubKey()) + ")"; + } + + @Override + public String getOutputDescriptor(Script script) { + throw new ProtocolException("No script derived output descriptor for non pay to script type"); + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -540,6 +633,20 @@ public enum ScriptType { return getOutputScript(Sha256Hash.hash(script.getProgram())); } + @Override + public String getOutputDescriptor(ECKey key) { + throw new ProtocolException("No single key output descriptor for script hash type"); + } + + @Override + public String getOutputDescriptor(Script script) { + if(!MULTISIG.isScriptType(script)) { + throw new IllegalArgumentException("Can only create output descriptor from multisig script"); + } + + return "wsh(" + MULTISIG.getOutputDescriptor(script) + ")"; + } + @Override public boolean isScriptType(Script script) { List chunks = script.chunks; @@ -630,6 +737,10 @@ public enum ScriptType { throw new UnsupportedOperationException("Only defined for MULTISIG script type"); } + public abstract String getOutputDescriptor(ECKey key); + + public abstract String getOutputDescriptor(Script script); + public abstract boolean isScriptType(Script script); public abstract byte[] getHashFromScript(Script script); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 17b86d5..6f989b2 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -23,9 +23,7 @@ public class Wallet { private ScriptType scriptType; private Policy defaultPolicy; private List keystores = new ArrayList<>(); - private final List accountNodes = new ArrayList<>(); - - private transient int lookAhead = DEFAULT_LOOKAHEAD; + private final Set accountNodes = new TreeSet<>(); public Wallet() { } @@ -82,6 +80,10 @@ public class Wallet { this.keystores = keystores; } + private Set getAccountNodes() { + return accountNodes; + } + public Node getNode(KeyPurpose keyPurpose) { Node purposeNode; Optional optionalPurposeNode = accountNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst(); @@ -92,12 +94,18 @@ public class Wallet { purposeNode = optionalPurposeNode.get(); } - purposeNode.fillToLookAhead(getLookAhead()); + purposeNode.fillToIndex(getLookAhead(purposeNode) - 1); return purposeNode; } - public int getLookAhead() { + public int getLookAhead(Node node) { //TODO: Calculate using seen transactions + int lookAhead = DEFAULT_LOOKAHEAD; + Integer maxIndex = node.getHighestIndex(); + if(maxIndex != null) { + lookAhead = Math.max(maxIndex + lookAhead/2, lookAhead); + } + return lookAhead; } @@ -115,11 +123,16 @@ public class Wallet { Node node = getNode(keyPurpose); if(index >= node.getChildren().size()) { - lookAhead = index; - node.fillToLookAhead(lookAhead); + node.fillToIndex(index); } - return node.getChildren().get(index); + for(Node childNode : node.getChildren()) { + if(childNode.getIndex() == index) { + return childNode; + } + } + + throw new IllegalStateException("Could not fill nodes to index " + index); } public Address getAddress(KeyPurpose keyPurpose, int index) { @@ -150,6 +163,20 @@ public class Wallet { } } + public String getOutputDescriptor(KeyPurpose keyPurpose, int index) { + if(policyType == PolicyType.SINGLE) { + Keystore keystore = getKeystores().get(0); + DeterministicKey key = keystore.getKey(keyPurpose, index); + return scriptType.getOutputDescriptor(key); + } else if(policyType == PolicyType.MULTI) { + List pubKeys = getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList()); + Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); + return scriptType.getOutputDescriptor(script); + } else { + throw new UnsupportedOperationException("Cannot determine output descriptor for custom policies"); + } + } + public boolean isValid() { if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) { return false; @@ -245,6 +272,21 @@ public class Wallet { for(Keystore keystore : keystores) { copy.getKeystores().add(keystore.copy()); } + for(Wallet.Node node : accountNodes) { + Node nodeCopy = copy.copyNode(node); + copy.getAccountNodes().add(nodeCopy); + } + return copy; + } + + private Node copyNode(Node node) { + Node copy = new Node(node.derivationPath); + copy.setLabel(node.label); + copy.setAmount(node.amount); + for(Node child : node.getChildren()) { + copy.getChildren().add(copyNode(child)); + } + return copy; } @@ -302,21 +344,19 @@ public class Wallet { } } - public class Node { + public class Node implements Comparable { private final String derivationPath; private String label; private Long amount; - private final List children = new ArrayList<>(); + private Set children = new TreeSet<>(); - private final transient KeyPurpose keyPurpose; - private final transient int index; - private final transient List derivation; + private transient KeyPurpose keyPurpose; + private transient int index = -1; + private transient List derivation; public Node(String derivationPath) { this.derivationPath = derivationPath; - this.derivation = KeyDerivation.parsePath(derivationPath); - this.keyPurpose = KeyPurpose.fromChildNumber(derivation.get(0)); - this.index = derivation.get(derivation.size() - 1).num(); + parseDerivation(); } public Node(KeyPurpose keyPurpose) { @@ -337,14 +377,36 @@ public class Wallet { return derivationPath; } + private void parseDerivation() { + this.derivation = KeyDerivation.parsePath(derivationPath); + this.keyPurpose = KeyPurpose.fromChildNumber(derivation.get(0)); + this.index = derivation.get(derivation.size() - 1).num(); + } + public int getIndex() { + if(index < 0) { + parseDerivation(); + } + return index; } public KeyPurpose getKeyPurpose() { + if(keyPurpose == null) { + parseDerivation(); + } + return keyPurpose; } + public List getDerivation() { + if(derivation == null) { + parseDerivation(); + } + + return derivation; + } + public String getLabel() { return label; } @@ -361,10 +423,14 @@ public class Wallet { this.amount = amount; } - public List getChildren() { + public Set getChildren() { return children; } + public void setChildren(Set children) { + this.children = children; + } + public Address getAddress() { return Wallet.this.getAddress(keyPurpose, index); } @@ -373,15 +439,26 @@ public class Wallet { return Wallet.this.getOutputScript(keyPurpose, index); } - public void fillToLookAhead(int lookAhead) { - for(int i = 0; i < lookAhead; i++) { + public String getOutputDescriptor() { + return Wallet.this.getOutputDescriptor(keyPurpose, index); + } + + public void fillToIndex(int index) { + for(int i = 0; i <= index; i++) { Node node = new Node(getKeyPurpose(), i); - if(!getChildren().contains(node)) { - getChildren().add(node); - } + getChildren().add(node); } } + public Integer getHighestIndex() { + Node highestNode = null; + for(Node childNode : getChildren()) { + highestNode = childNode; + } + + return highestNode == null ? null : highestNode.index; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -394,5 +471,10 @@ public class Wallet { public int hashCode() { return Objects.hash(derivationPath); } + + @Override + public int compareTo(Node node) { + return getIndex() - node.getIndex(); + } } }