From d6386ba86a085cef88ef41ebd4a66f228ec79aa0 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 10 Nov 2020 08:54:00 +0200 Subject: [PATCH] add ur registry objects --- .../com/sparrowwallet/hummingbird/UR.java | 54 +++++++-- .../sparrowwallet/hummingbird/UREncoder.java | 4 +- .../hummingbird/registry/CryptoAccount.java | 41 +++++++ .../hummingbird/registry/CryptoAddress.java | 60 ++++++++++ .../hummingbird/registry/CryptoBip39.java | 52 +++++++++ .../hummingbird/registry/CryptoCoinInfo.java | 53 +++++++++ .../hummingbird/registry/CryptoECKey.java | 56 +++++++++ .../hummingbird/registry/CryptoHDKey.java | 110 ++++++++++++++++++ .../hummingbird/registry/CryptoKeypath.java | 83 +++++++++++++ .../hummingbird/registry/CryptoOutput.java | 84 +++++++++++++ .../hummingbird/registry/CryptoPSBT.java | 20 ++++ .../hummingbird/registry/CryptoSeed.java | 51 ++++++++ .../hummingbird/registry/MultiKey.java | 62 ++++++++++ .../hummingbird/registry/PathComponent.java | 37 ++++++ .../hummingbird/registry/RegistryType.java | 63 ++++++++++ .../registry/ScriptExpression.java | 40 +++++++ .../registry/CryptoAccountTest.java | 79 +++++++++++++ .../registry/CryptoAddressTest.java | 21 ++++ .../hummingbird/registry/CryptoBip39Test.java | 22 ++++ .../hummingbird/registry/CryptoECKeyTest.java | 23 ++++ .../hummingbird/registry/CryptoHDKeyTest.java | 39 +++++++ .../registry/CryptoOutputTest.java | 93 +++++++++++++++ .../hummingbird/registry/CryptoSeedTest.java | 26 +++++ 23 files changed, 1164 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAddress.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoBip39.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoCoinInfo.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoECKey.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoOutput.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoPSBT.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/CryptoSeed.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/MultiKey.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/RegistryType.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/ScriptExpression.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAddressTest.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoBip39Test.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoECKeyTest.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoOutputTest.java create mode 100644 src/test/java/com/sparrowwallet/hummingbird/registry/CryptoSeedTest.java diff --git a/src/main/java/com/sparrowwallet/hummingbird/UR.java b/src/main/java/com/sparrowwallet/hummingbird/UR.java index 26e103e..0426672 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/UR.java +++ b/src/main/java/com/sparrowwallet/hummingbird/UR.java @@ -6,8 +6,8 @@ import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.ByteString; import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.registry.*; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.List; @@ -18,12 +18,14 @@ import java.util.Objects; */ public class UR { public static final String UR_PREFIX = "ur"; - public static final String BYTES_TYPE = "bytes"; - public static final String CRYPTO_PSBT_TYPE = "crypto-psbt"; private final String type; private final byte[] data; + public UR(RegistryType registryType, byte[] data) throws InvalidTypeException { + this(registryType.toString(), data); + } + public UR(String type, byte[] data) throws InvalidTypeException { if(!isURType(type)) { throw new InvalidTypeException("Invalid UR type: " + type); @@ -37,14 +39,52 @@ public class UR { return type; } - public byte[] getCbor() { + public RegistryType getRegistryType() { + return RegistryType.fromString(type); + } + + public byte[] getCborBytes() { return data; } + public Object decodeFromRegistry() throws InvalidCBORException { + RegistryType registryType = getRegistryType(); + + try { + List dataItems = CborDecoder.decode(getCborBytes()); + DataItem item = dataItems.get(0); + + if(registryType == RegistryType.BYTES) { + return ((ByteString)item).getBytes(); + } else if(registryType == RegistryType.CRYPTO_SEED) { + return CryptoSeed.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_BIP39) { + return CryptoBip39.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_HDKEY) { + return CryptoHDKey.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_KEYPATH) { + return CryptoKeypath.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_COIN_INFO) { + return CryptoCoinInfo.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_ECKEY) { + return CryptoECKey.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_ADDRESS) { + return CryptoAddress.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_OUTPUT) { + return CryptoOutput.fromCbor(item); + } else if(registryType == RegistryType.CRYPTO_PSBT) { + return CryptoPSBT.fromCbor(item); + } + } catch(CborException e) { + throw new InvalidCBORException(e.getMessage()); + } + + return null; + } + public byte[] toBytes() throws InvalidCBORException { try { - ByteArrayInputStream bais = new ByteArrayInputStream(getCbor()); - List dataItems = new CborDecoder(bais).decode(); + List dataItems = CborDecoder.decode(getCborBytes()); if(!(dataItems.get(0) instanceof ByteString)) { throw new IllegalArgumentException("First element of CBOR is not a byte string"); } @@ -72,7 +112,7 @@ public class UR { } public static UR fromBytes(byte[] data) throws InvalidTypeException, InvalidCBORException { - return fromBytes(BYTES_TYPE, data); + return fromBytes(RegistryType.BYTES.toString(), data); } public static UR fromBytes(String type, byte[] data) throws InvalidTypeException, InvalidCBORException { diff --git a/src/main/java/com/sparrowwallet/hummingbird/UREncoder.java b/src/main/java/com/sparrowwallet/hummingbird/UREncoder.java index 8117184..82262bd 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/UREncoder.java +++ b/src/main/java/com/sparrowwallet/hummingbird/UREncoder.java @@ -15,7 +15,7 @@ public class UREncoder { public UREncoder(UR ur, int maxFragmentLen, int minFragmentLen, long firstSeqNum) { this.ur = ur; - this.fountainEncoder = new FountainEncoder(ur.getCbor(), maxFragmentLen, minFragmentLen, firstSeqNum); + this.fountainEncoder = new FountainEncoder(ur.getCborBytes(), maxFragmentLen, minFragmentLen, firstSeqNum); } public boolean isComplete() { @@ -48,7 +48,7 @@ public class UREncoder { } public static String encode(UR ur) { - String encoded = Bytewords.encode(ur.getCbor(), Bytewords.Style.MINIMAL); + String encoded = Bytewords.encode(ur.getCborBytes(), Bytewords.Style.MINIMAL); return encodeUR(ur.getType(), encoded); } diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java new file mode 100644 index 0000000..ebf6de9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java @@ -0,0 +1,41 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CryptoAccount { + public static final long MASTER_FINGERPRINT_KEY = 1; + public static final long OUTPUT_DESCRIPTORS_KEY = 2; + + private final byte[] masterFingerprint; + private final List outputDescriptors; + + public CryptoAccount(byte[] masterFingerprint, List outputDescriptors) { + this.masterFingerprint = Arrays.copyOfRange(masterFingerprint, masterFingerprint.length - 4, masterFingerprint.length); + this.outputDescriptors = outputDescriptors; + } + + public byte[] getMasterFingerprint() { + return masterFingerprint; + } + + public List getOutputDescriptors() { + return outputDescriptors; + } + + public static CryptoAccount fromCbor(DataItem cbor) { + Map cryptoAccountMap = (Map)cbor; + + UnsignedInteger uintMasterFingerprint = (UnsignedInteger)cryptoAccountMap.get(new UnsignedInteger(MASTER_FINGERPRINT_KEY)); + Array outputDescriptors = (Array)cryptoAccountMap.get(new UnsignedInteger(OUTPUT_DESCRIPTORS_KEY)); + List cryptoOutputs = new ArrayList<>(outputDescriptors.getDataItems().size()); + for(DataItem item : outputDescriptors.getDataItems()) { + cryptoOutputs.add(CryptoOutput.fromCbor(item)); + } + + return new CryptoAccount(uintMasterFingerprint.getValue().toByteArray(), cryptoOutputs); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAddress.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAddress.java new file mode 100644 index 0000000..e4aaa6c --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAddress.java @@ -0,0 +1,60 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +public class CryptoAddress { + public static final long INFO = 1; + public static final long TYPE = 2; + public static final long DATA = 3; + + private final CryptoCoinInfo info; + private final Type type; + private final byte[] data; + + public CryptoAddress(CryptoCoinInfo info, Type type, byte[] data) { + this.info = info; + this.type = type; + this.data = data; + } + + public CryptoCoinInfo getInfo() { + return info; + } + + public Type getType() { + return type; + } + + public byte[] getData() { + return data; + } + + public static CryptoAddress fromCbor(DataItem item) { + CryptoCoinInfo info = null; + Type type = null; + byte[] data = null; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == INFO) { + info = CryptoCoinInfo.fromCbor(map.get(key)); + } else if(intKey == TYPE) { + type = Type.values()[((UnsignedInteger)map.get(key)).getValue().intValue()]; + } else if(intKey == DATA) { + data = ((ByteString)map.get(key)).getBytes(); + } + } + + if(data == null) { + throw new IllegalStateException("Data is null"); + } + + return new CryptoAddress(info, type, data); + } + + public enum Type { + P2PKH, P2SH, P2WPKH + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoBip39.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoBip39.java new file mode 100644 index 0000000..4146903 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoBip39.java @@ -0,0 +1,52 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +import java.util.ArrayList; +import java.util.List; + +public class CryptoBip39 { + public static final long WORDS = 1; + public static final long LANG = 2; + + private final List words; + private final String language; + + public CryptoBip39(List words, String language) { + this.words = words; + this.language = language; + } + + public List getWords() { + return words; + } + + public String getLanguage() { + return language; + } + + public static CryptoBip39 fromCbor(DataItem item) { + List words = new ArrayList<>(); + String language = "en"; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == WORDS) { + Array wordsArray = (Array)map.get(key); + for(DataItem wordItem : wordsArray.getDataItems()) { + words.add(((UnicodeString)wordItem).getString()); + } + } else if(intKey == LANG) { + language = ((UnicodeString)map.get(key)).getString(); + } + } + + if(words.isEmpty()) { + throw new IllegalStateException("No BIP39 words"); + } + + return new CryptoBip39(words, language); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoCoinInfo.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoCoinInfo.java new file mode 100644 index 0000000..5ae36d0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoCoinInfo.java @@ -0,0 +1,53 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.UnsignedInteger; + +public class CryptoCoinInfo { + public static final int TYPE_KEY = 1; + public static final int NETWORK_KEY = 2; + + private final int type; + private final int network; + + public CryptoCoinInfo(int type, int network) { + this.type = type; + this.network = network; + } + + public Type getType() { + return Type.values()[type]; + } + + public Network getNetwork() { + return Network.values()[network]; + } + + public static CryptoCoinInfo fromCbor(DataItem item) { + int type = 0; + int network = 0; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + + if(intKey == TYPE_KEY) { + type = ((UnsignedInteger)map.get(key)).getValue().intValue(); + } else if(intKey == NETWORK_KEY) { + network = ((UnsignedInteger)map.get(key)).getValue().intValue(); + } + } + + return new CryptoCoinInfo(type, network); + } + + public enum Type { + BITCOIN + } + + public enum Network { + MAINNET, TESTNET + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoECKey.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoECKey.java new file mode 100644 index 0000000..41b96a2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoECKey.java @@ -0,0 +1,56 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +public class CryptoECKey { + public static final long CURVE = 1; + public static final long PRIVATE = 2; + public static final long DATA = 3; + + private final int curve; + private final boolean privateKey; + private final byte[] data; + + public CryptoECKey(int curve, boolean privateKey, byte[] data) { + this.curve = curve; + this.privateKey = privateKey; + this.data = data; + } + + public int getCurve() { + return curve; + } + + public boolean isPrivateKey() { + return privateKey; + } + + public byte[] getData() { + return data; + } + + public static CryptoECKey fromCbor(DataItem item) { + int curve = 0; + boolean privateKey = false; + byte[] data = null; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == CURVE) { + curve = ((UnsignedInteger)map.get(key)).getValue().intValue(); + } else if(intKey == PRIVATE) { + privateKey = (map.get(key) == SimpleValue.TRUE); + } else if(intKey == DATA) { + data = ((ByteString)map.get(key)).getBytes(); + } + } + + if(data == null) { + throw new IllegalStateException("Data is null"); + } + + return new CryptoECKey(curve, privateKey, data); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java new file mode 100644 index 0000000..043a102 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java @@ -0,0 +1,110 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +public class CryptoHDKey { + public static final int IS_MASTER_KEY = 1; + public static final int IS_PRIVATE_KEY = 2; + public static final int KEY_DATA_KEY = 3; + public static final int CHAIN_CODE_KEY = 4; + public static final int USE_INFO_KEY = 5; + public static final int ORIGIN_KEY = 6; + public static final int CHILDREN_KEY = 7; + + private final boolean master; + private final boolean privateKey; + private final byte[] key; + private final byte[] chainCode; + private final CryptoCoinInfo useInfo; + private final CryptoKeypath origin; + private final CryptoKeypath children; + + public CryptoHDKey(byte[] key, byte[] chainCode) { + this.master = true; + this.privateKey = true; + this.key = key; + this.chainCode = chainCode; + this.useInfo = null; + this.origin = null; + this.children = null; + } + + public CryptoHDKey(boolean privateKey, byte[] key, byte[] chainCode, CryptoCoinInfo useInfo, CryptoKeypath origin, CryptoKeypath children) { + this.master = false; + this.privateKey = privateKey; + this.key = key; + this.chainCode = chainCode; + this.useInfo = useInfo; + this.origin = origin; + this.children = children; + } + + public boolean isMaster() { + return master; + } + + public boolean isPrivateKey() { + return privateKey; + } + + public byte[] getKey() { + return key; + } + + public byte[] getChainCode() { + return chainCode; + } + + public CryptoCoinInfo getUseInfo() { + return useInfo; + } + + public CryptoKeypath getOrigin() { + return origin; + } + + public CryptoKeypath getChildren() { + return children; + } + + public static CryptoHDKey fromCbor(DataItem item) { + boolean isMasterKey = false; + boolean isPrivateKey = false; + byte[] keyData = null; + byte[] chainCode = null; + CryptoCoinInfo useInfo = null; + CryptoKeypath origin = null; + CryptoKeypath children = null; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == IS_MASTER_KEY) { + isMasterKey = (map.get(uintKey) == SimpleValue.TRUE); + } else if(intKey == IS_PRIVATE_KEY) { + isPrivateKey = (map.get(uintKey) == SimpleValue.TRUE); + } else if(intKey == KEY_DATA_KEY) { + keyData = ((ByteString)map.get(uintKey)).getBytes(); + } else if(intKey == CHAIN_CODE_KEY) { + chainCode = ((ByteString)map.get(uintKey)).getBytes(); + } else if(intKey == USE_INFO_KEY) { + useInfo = CryptoCoinInfo.fromCbor(map.get(uintKey)); + } else if(intKey == ORIGIN_KEY) { + origin = CryptoKeypath.fromCbor(map.get(uintKey)); + } else if(intKey == CHILDREN_KEY) { + children = CryptoKeypath.fromCbor(map.get(uintKey)); + } + } + + if(keyData == null) { + throw new IllegalStateException("Key data is null"); + } + + if(isMasterKey) { + return new CryptoHDKey(keyData, chainCode); + } else { + return new CryptoHDKey(isPrivateKey, keyData, chainCode, useInfo, origin, children); + } + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java new file mode 100644 index 0000000..e4bc3d6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java @@ -0,0 +1,83 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +public class CryptoKeypath { + public static final int COMPONENTS_KEY = 1; + public static final int PARENT_FINGERPRINT_KEY = 2; + public static final int DEPTH_KEY = 3; + + private final List components; + private final byte[] parentFingerprint; + private final Integer depth; + + public CryptoKeypath(List components, byte[] parentFingerprint) { + this(components, parentFingerprint, 0); + } + + public CryptoKeypath(List components, byte[] parentFingerprint, Integer depth) { + this.components = components; + this.parentFingerprint = parentFingerprint == null ? null : Arrays.copyOfRange(parentFingerprint, parentFingerprint.length - 4, parentFingerprint.length); + this.depth = depth; + } + + public List getComponents() { + return components; + } + + public String getPath() { + if(components.isEmpty()) { + return null; + } + + StringJoiner joiner = new StringJoiner("/"); + for(PathComponent component : components) { + joiner.add((component.isWildcard() ? "*" : component.getIndex()) + (component.isHardened() ? "'" : "")); + } + return joiner.toString(); + } + + public byte[] getParentFingerprint() { + return parentFingerprint; + } + + public Integer getDepth() { + return depth; + } + + public static CryptoKeypath fromCbor(DataItem item) { + List components = new ArrayList<>(); + byte[] parentFingerprint = null; + Integer depth = null; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == COMPONENTS_KEY) { + Array componentArray = (Array)map.get(key); + for(int i = 0; i < componentArray.getDataItems().size(); i+=2) { + boolean hardened = (componentArray.getDataItems().get(i+1) == SimpleValue.TRUE); + DataItem pathSeg = componentArray.getDataItems().get(i); + if(pathSeg instanceof UnsignedInteger) { + UnsignedInteger uintIndex = (UnsignedInteger)pathSeg; + components.add(new PathComponent(uintIndex.getValue().intValue(), hardened)); + } else if(pathSeg instanceof Array) { + components.add(new PathComponent(hardened)); + } + } + } else if(intKey == PARENT_FINGERPRINT_KEY) { + parentFingerprint = ((UnsignedInteger)map.get(key)).getValue().toByteArray(); + } else if(intKey == DEPTH_KEY) { + depth = ((UnsignedInteger)map.get(key)).getValue().intValue(); + } + } + + return new CryptoKeypath(components, parentFingerprint, depth); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoOutput.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoOutput.java new file mode 100644 index 0000000..29903c1 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoOutput.java @@ -0,0 +1,84 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.Tag; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CryptoOutput { + private final List scriptExpressions; + + //Only one of the following will be not null + private final CryptoECKey ecKey; + private final CryptoHDKey hdKey; + private final MultiKey multiKey; + + public CryptoOutput(List scriptExpressions, CryptoECKey ecKey) { + this.scriptExpressions = scriptExpressions; + this.ecKey = ecKey; + this.hdKey = null; + this.multiKey = null; + } + + public CryptoOutput(List scriptExpressions, CryptoHDKey hdKey) { + this.scriptExpressions = scriptExpressions; + this.ecKey = null; + this.hdKey = hdKey; + this.multiKey = null; + } + + public CryptoOutput(List scriptExpressions, MultiKey multiKey) { + this.scriptExpressions = scriptExpressions; + this.ecKey = null; + this.hdKey = null; + this.multiKey = multiKey; + } + + public List getScriptExpressions() { + return scriptExpressions; + } + + public CryptoECKey getEcKey() { + return ecKey; + } + + public CryptoHDKey getHdKey() { + return hdKey; + } + + public MultiKey getMultiKey() { + return multiKey; + } + + public static CryptoOutput fromCbor(DataItem cbor) { + List expressions = new ArrayList<>(); + + Tag tag = cbor.getTag(); + do { + if(tag.getValue() != RegistryType.CRYPTO_HDKEY.getTag() && tag.getValue() != RegistryType.CRYPTO_ECKEY.getTag()) { + expressions.add(ScriptExpression.fromTagValue(tag.getValue())); + } + tag = tag.getTag(); + } while(tag != null); + + boolean isMultiKey = expressions.get(0) == ScriptExpression.MULTISIG || expressions.get(0) == ScriptExpression.SORTED_MULTISIG; + Collections.reverse(expressions); + + Map map = (Map)cbor; + if(isMultiKey) { + MultiKey multiKey = MultiKey.fromCbor(map); + return new CryptoOutput(expressions, multiKey); + } else if(cbor.getTag().getValue() == RegistryType.CRYPTO_ECKEY.getTag()) { + CryptoECKey cryptoECKey = CryptoECKey.fromCbor(map); + return new CryptoOutput(expressions, cryptoECKey); + } else if(cbor.getTag().getValue() == RegistryType.CRYPTO_HDKEY.getTag()) { + CryptoHDKey cryptoHDKey = CryptoHDKey.fromCbor(map); + return new CryptoOutput(expressions, cryptoHDKey); + } + + throw new IllegalStateException("Unknown tag for data item: " + cbor.getTag().getValue()); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoPSBT.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoPSBT.java new file mode 100644 index 0000000..5f33714 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoPSBT.java @@ -0,0 +1,20 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; + +public class CryptoPSBT { + private final byte[] psbt; + + public CryptoPSBT(byte[] psbt) { + this.psbt = psbt; + } + + public byte[] getPsbt() { + return psbt; + } + + public static CryptoPSBT fromCbor(DataItem item) { + return new CryptoPSBT(((ByteString)item).getBytes()); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoSeed.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoSeed.java new file mode 100644 index 0000000..815c59e --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoSeed.java @@ -0,0 +1,51 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.UnsignedInteger; + +import java.util.Date; + +public class CryptoSeed { + public static final long PAYLOAD = 1; + public static final long BIRTHDATE = 2; + + private final byte[] seed; + private final Date birthdate; + + public CryptoSeed(byte[] seed, Date birthdate) { + this.seed = seed; + this.birthdate = birthdate; + } + + public byte[] getSeed() { + return seed; + } + + public Date getBirthdate() { + return birthdate; + } + + public static CryptoSeed fromCbor(DataItem item) { + byte[] seed = null; + Date birthdate = null; + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger uintKey = (UnsignedInteger)key; + int intKey = uintKey.getValue().intValue(); + if(intKey == PAYLOAD) { + seed = ((ByteString)map.get(key)).getBytes(); + } else if(intKey == BIRTHDATE) { + birthdate = new Date(((UnsignedInteger)map.get(key)).getValue().longValue() * 1000 * 60 * 60 * 24); + } + } + + if(seed == null) { + throw new IllegalStateException("Seed is null"); + } + + return new CryptoSeed(seed, birthdate); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/MultiKey.java b/src/main/java/com/sparrowwallet/hummingbird/registry/MultiKey.java new file mode 100644 index 0000000..22b3ca7 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/MultiKey.java @@ -0,0 +1,62 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.UnsignedInteger; + +import java.util.ArrayList; +import java.util.List; + +public class MultiKey { + public static final int THRESHOLD_KEY = 1; + public static final int KEYS_KEY = 2; + + private final int threshold; + private final List ecKeys; + private final List hdKeys; + + public MultiKey(int threshold, List ecKeys, List hdKeys) { + this.threshold = threshold; + this.ecKeys = ecKeys; + this.hdKeys = hdKeys; + } + + public int getThreshold() { + return threshold; + } + + public List getEcKeys() { + return ecKeys; + } + + public List getHdKeys() { + return hdKeys; + } + + public static MultiKey fromCbor(DataItem item) { + int threshold = 0; + List ecKeys = new ArrayList<>(); + List hdKeys = new ArrayList<>(); + + Map map = (Map)item; + for(DataItem key : map.getKeys()) { + UnsignedInteger intKey = (UnsignedInteger)key; + if(intKey.getValue().intValue() == THRESHOLD_KEY) { + threshold = ((UnsignedInteger)map.get(key)).getValue().intValue(); + } + if(intKey.getValue().intValue() == KEYS_KEY) { + Array keysArray = (Array)map.get(key); + for(DataItem keyExp : keysArray.getDataItems()) { + if(keyExp.getTag().getValue() == RegistryType.CRYPTO_ECKEY.getTag()) { + ecKeys.add(CryptoECKey.fromCbor(keyExp)); + } else if(keyExp.getTag().getValue() == RegistryType.CRYPTO_HDKEY.getTag()) { + hdKeys.add(CryptoHDKey.fromCbor(keyExp)); + } + } + } + } + + return new MultiKey(threshold, ecKeys, hdKeys); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java new file mode 100644 index 0000000..faf395e --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java @@ -0,0 +1,37 @@ +package com.sparrowwallet.hummingbird.registry; + +public class PathComponent { + public static final int HARDENED_BIT = 0x80000000; + + private final int index; + private final boolean wildcard; + private final boolean hardened; + + public PathComponent(int index, boolean hardened) { + this.index = index; + this.wildcard = false; + this.hardened = hardened; + + if((index & HARDENED_BIT) != 0) { + throw new IllegalArgumentException("Invalid index " + index + " - most significant bit cannot be set"); + } + } + + public PathComponent(boolean hardened) { + this.index = 0; + this.wildcard = true; + this.hardened = hardened; + } + + public int getIndex() { + return index; + } + + public boolean isWildcard() { + return wildcard; + } + + public boolean isHardened() { + return hardened; + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryType.java b/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryType.java new file mode 100644 index 0000000..41645c5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryType.java @@ -0,0 +1,63 @@ +package com.sparrowwallet.hummingbird.registry; + +public enum RegistryType { + BYTES("bytes", null, byte[].class), + CBOR_PNG("cbor-png", null, null), + CBOR_SVG("cbor-svg", null, null), + COSE_SIGN("cose-sign", 98, null), + COSE_SIGN1("cose-sign1", 18, null), + COSE_ENCRYPT("cose-encrypt", 96, null), + COSE_ENCRYPT0("cose-encrypt0", 16, null), + COSE_MAC("cose-mac", 97, null), + COSE_MAC0("cose-mac0", 17, null), + COSE_KEY("cose-key", null, null), + COSE_KEYSET("cose-keyset", null, null), + CRYPTO_SEED("crypto-seed", 300, CryptoSeed.class), + CRYPTO_BIP39("crypto-bip39", 301, CryptoBip39.class), + CRYPTO_HDKEY("crypto-hdkey", 303, CryptoHDKey.class), + CRYPTO_KEYPATH("crypto-keypath", 304, CryptoKeypath.class), + CRYPTO_COIN_INFO("crypto-coin-info", 305, CryptoCoinInfo.class), + CRYPTO_ECKEY("crypto-eckey", 306, CryptoECKey.class), + CRYPTO_ADDRESS("crypto-address", 307, CryptoAddress.class), + CRYPTO_OUTPUT("crypto-output", 308, CryptoOutput.class), + CRYPTO_SSKR("crypto-sskr", 309, null), + CRYPTO_PSBT("crypto-psbt", 310, CryptoPSBT.class), + CRYPTO_ACCOUNT("crypto-account", 311, CryptoAccount.class); + + private final String type; + private final Integer tag; + private final Class registryClass; + + private RegistryType(String type, Integer tag, Class registryClass) { + this.type = type; + this.tag = tag; + this.registryClass = registryClass; + } + + public String getType() { + return type; + } + + public Integer getTag() { + return tag; + } + + public Class getRegistryClass() { + return registryClass; + } + + @Override + public String toString() { + return type; + } + + public static RegistryType fromString(String type) { + for(RegistryType registryType : values()) { + if(registryType.toString().equals(type.toLowerCase())) { + return registryType; + } + } + + throw new IllegalArgumentException("Unknown UR registry type: " + type); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/ScriptExpression.java b/src/main/java/com/sparrowwallet/hummingbird/registry/ScriptExpression.java new file mode 100644 index 0000000..8a901c9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/ScriptExpression.java @@ -0,0 +1,40 @@ +package com.sparrowwallet.hummingbird.registry; + +public enum ScriptExpression { + SCRIPT_HASH(400, "sh"), + WITNESS_SCRIPT_HASH(401, "wsh"), + PUBLIC_KEY(402, "pk"), + PUBLIC_KEY_HASH(403, "pkh"), + WITNESS_PUBLIC_KEY_HASH(404, "wpkh"), + COMBO(405, "combo"), + MULTISIG(406, "multi"), + SORTED_MULTISIG(407, "sorted"), + ADDRESS(307, "addr"), + RAW_SCRIPT(408, "raw"); + + private final int tagValue; + private final String expression; + + private ScriptExpression(int tagValue, String expression) { + this.tagValue = tagValue; + this.expression = expression; + } + + public int getTagValue() { + return tagValue; + } + + public String getExpression() { + return expression; + } + + public static ScriptExpression fromTagValue(long value) { + for(ScriptExpression expression : ScriptExpression.values()) { + if(expression.tagValue == value) { + return expression; + } + } + + throw new IllegalArgumentException("Unknown tag value " + value); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java new file mode 100644 index 0000000..6230d05 --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java @@ -0,0 +1,79 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import com.sparrowwallet.hummingbird.UR; +import com.sparrowwallet.hummingbird.UREncoder; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoAccountTest { + @Test + public void testSeed() throws CborException { + String hex = "A2011A37B5EED40286D90193D9012FA303582103EB3E2863911826374DE86C231A4B76F0B89DFA174AFB78D7F478199884D9DD320458206456A5DF2DB0F6D9AF72B2A1AF4B25F45200ED6FCC29C3440B311D4796B70B5B06D90130A20186182CF500F500F5021A99F9CDF7D90190D90194D9012FA303582102C7E4823730F6EE2CF864E2C352060A88E60B51A84E89E4C8C75EC22590AD6B690458209D2F86043276F9251A4A4F577166A5ABEB16B6EC61E226B5B8FA11038BFDA42D06D90130A201861831F500F500F5021AA80F7CDBD90194D9012FA303582103FD433450B6924B4F7EFDD5D1ED017D364BE95AB2B592DC8BDDB3B00C1C24F63F04582072EDE7334D5ACF91C6FDA622C205199C595A31F9218ED30792D301D5EE9E3A8806D90130A201861854F500F500F5021A0D5DE1D7D90190D9012FA3035821035CCD58B63A2CDC23D0812710603592E7457573211880CB59B1EF012E168E059A04582088D3299B448F87215D96B0C226235AFC027F9E7DC700284F3E912A34DAEB1A2306D90130A20182182DF5021A37B5EED4D90190D90191D9012FA3035821032C78EBFCABDAC6D735A0820EF8732F2821B4FB84CD5D6B26526938F90C0507110458207953EFE16A73E5D3F9F2D4C6E49BD88E22093BBD85BE5A7E862A4B98A16E0AB606D90130A201881830F500F500F501F5021A59B69B2AD90191D9012FA30358210260563EE80C26844621B06B74070BAF0E23FB76CE439D0237E87502EBBD3CA3460458202FA0E41C9DC43DC4518659BFCEF935BA8101B57DBC0812805DD983BC1D34B81306D90130A201881830F500F500F502F5021A59B69B2A"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoAccount cryptoAccount = CryptoAccount.fromCbor(items.get(0)); + Assert.assertEquals("37b5eed4", TestUtils.bytesToHex(cryptoAccount.getMasterFingerprint())); + + CryptoOutput cryptoOutput1 = cryptoAccount.getOutputDescriptors().get(0); + Assert.assertEquals(List.of(ScriptExpression.PUBLIC_KEY_HASH), cryptoOutput1.getScriptExpressions()); + Assert.assertEquals("03eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd32", TestUtils.bytesToHex(cryptoOutput1.getHdKey().getKey())); + Assert.assertEquals("6456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b", TestUtils.bytesToHex(cryptoOutput1.getHdKey().getChainCode())); + Assert.assertEquals("44'/0'/0'", cryptoOutput1.getHdKey().getOrigin().getPath()); + Assert.assertEquals("99f9cdf7", TestUtils.bytesToHex(cryptoOutput1.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput1.getHdKey().getChildren()); + + CryptoOutput cryptoOutput2 = cryptoAccount.getOutputDescriptors().get(1); + Assert.assertEquals(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH), cryptoOutput2.getScriptExpressions()); + Assert.assertEquals("02c7e4823730f6ee2cf864e2c352060a88e60b51a84e89e4c8c75ec22590ad6b69", TestUtils.bytesToHex(cryptoOutput2.getHdKey().getKey())); + Assert.assertEquals("9d2f86043276f9251a4a4f577166a5abeb16b6ec61e226b5b8fa11038bfda42d", TestUtils.bytesToHex(cryptoOutput2.getHdKey().getChainCode())); + Assert.assertEquals("49'/0'/0'", cryptoOutput2.getHdKey().getOrigin().getPath()); + Assert.assertEquals("a80f7cdb", TestUtils.bytesToHex(cryptoOutput2.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput2.getHdKey().getChildren()); + + CryptoOutput cryptoOutput3 = cryptoAccount.getOutputDescriptors().get(2); + Assert.assertEquals(List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH), cryptoOutput3.getScriptExpressions()); + Assert.assertEquals("03fd433450b6924b4f7efdd5d1ed017d364be95ab2b592dc8bddb3b00c1c24f63f", TestUtils.bytesToHex(cryptoOutput3.getHdKey().getKey())); + Assert.assertEquals("72ede7334d5acf91c6fda622c205199c595a31f9218ed30792d301d5ee9e3a88", TestUtils.bytesToHex(cryptoOutput3.getHdKey().getChainCode())); + Assert.assertEquals("84'/0'/0'", cryptoOutput3.getHdKey().getOrigin().getPath()); + Assert.assertEquals("0d5de1d7", TestUtils.bytesToHex(cryptoOutput3.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput3.getHdKey().getChildren()); + + CryptoOutput cryptoOutput4 = cryptoAccount.getOutputDescriptors().get(3); + Assert.assertEquals(List.of(ScriptExpression.SCRIPT_HASH), cryptoOutput4.getScriptExpressions()); + Assert.assertEquals("035ccd58b63a2cdc23d0812710603592e7457573211880cb59b1ef012e168e059a", TestUtils.bytesToHex(cryptoOutput4.getHdKey().getKey())); + Assert.assertEquals("88d3299b448f87215d96b0c226235afc027f9e7dc700284f3e912a34daeb1a23", TestUtils.bytesToHex(cryptoOutput4.getHdKey().getChainCode())); + Assert.assertEquals("45'", cryptoOutput4.getHdKey().getOrigin().getPath()); + Assert.assertEquals("37b5eed4", TestUtils.bytesToHex(cryptoOutput4.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput4.getHdKey().getChildren()); + + CryptoOutput cryptoOutput5 = cryptoAccount.getOutputDescriptors().get(4); + Assert.assertEquals(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH), cryptoOutput5.getScriptExpressions()); + Assert.assertEquals("032c78ebfcabdac6d735a0820ef8732f2821b4fb84cd5d6b26526938f90c050711", TestUtils.bytesToHex(cryptoOutput5.getHdKey().getKey())); + Assert.assertEquals("7953efe16a73e5d3f9f2d4c6e49bd88e22093bbd85be5a7e862a4b98a16e0ab6", TestUtils.bytesToHex(cryptoOutput5.getHdKey().getChainCode())); + Assert.assertEquals("48'/0'/0'/1'", cryptoOutput5.getHdKey().getOrigin().getPath()); + Assert.assertEquals("59b69b2a", TestUtils.bytesToHex(cryptoOutput5.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput5.getHdKey().getChildren()); + + CryptoOutput cryptoOutput6 = cryptoAccount.getOutputDescriptors().get(5); + Assert.assertEquals(List.of(ScriptExpression.WITNESS_SCRIPT_HASH), cryptoOutput6.getScriptExpressions()); + Assert.assertEquals("0260563ee80c26844621b06b74070baf0e23fb76ce439d0237e87502ebbd3ca346", TestUtils.bytesToHex(cryptoOutput6.getHdKey().getKey())); + Assert.assertEquals("2fa0e41c9dc43dc4518659bfcef935ba8101b57dbc0812805dd983bc1d34b813", TestUtils.bytesToHex(cryptoOutput6.getHdKey().getChainCode())); + Assert.assertEquals("48'/0'/0'/2'", cryptoOutput6.getHdKey().getOrigin().getPath()); + Assert.assertEquals("59b69b2a", TestUtils.bytesToHex(cryptoOutput6.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoOutput6.getHdKey().getChildren()); + } + + @Test + public void testAccount() throws Exception { + byte[] cbor = TestUtils.hexToBytes("A2011A37B5EED40286D90193D9012FA303582103EB3E2863911826374DE86C231A4B76F0B89DFA174AFB78D7F478199884D9DD320458206456A5DF2DB0F6D9AF72B2A1AF4B25F45200ED6FCC29C3440B311D4796B70B5B06D90130A20186182CF500F500F5021A99F9CDF7D90190D90194D9012FA303582102C7E4823730F6EE2CF864E2C352060A88E60B51A84E89E4C8C75EC22590AD6B690458209D2F86043276F9251A4A4F577166A5ABEB16B6EC61E226B5B8FA11038BFDA42D06D90130A201861831F500F500F5021AA80F7CDBD90194D9012FA303582103FD433450B6924B4F7EFDD5D1ED017D364BE95AB2B592DC8BDDB3B00C1C24F63F04582072EDE7334D5ACF91C6FDA622C205199C595A31F9218ED30792D301D5EE9E3A8806D90130A201861854F500F500F5021A0D5DE1D7D90190D9012FA3035821035CCD58B63A2CDC23D0812710603592E7457573211880CB59B1EF012E168E059A04582088D3299B448F87215D96B0C226235AFC027F9E7DC700284F3E912A34DAEB1A2306D90130A20182182DF5021A37B5EED4D90190D90191D9012FA3035821032C78EBFCABDAC6D735A0820EF8732F2821B4FB84CD5D6B26526938F90C0507110458207953EFE16A73E5D3F9F2D4C6E49BD88E22093BBD85BE5A7E862A4B98A16E0AB606D90130A201881830F500F500F501F5021A59B69B2AD90191D9012FA30358210260563EE80C26844621B06B74070BAF0E23FB76CE439D0237E87502EBBD3CA3460458202FA0E41C9DC43DC4518659BFCEF935BA8101B57DBC0812805DD983BC1D34B81306D90130A201881830F500F500F502F5021A59B69B2A"); + UR ur = new UR("crypto-account", cbor); + String encoded = UREncoder.encode(ur); + Assert.assertEquals("ur:crypto-account/oeadcyemrewytyaolntaadmutaaddlotaxhdclaxwmfmdeiamecsdsemgtvsjzcncygrkowtrontzschgezokstswkkscfmklrtauteyaahdcxiehfonurdppfyntapejpproypegrdawkgmaewejlsfdtsrfybdehcaflmtrlbdhpamtaaddyoeadlncsdwykaeykaeykaocynlytsnyltaadmhtaadmwtaaddlotaxhdclaostvelfemdyynwydwyaievosrgmambklovabdgypdglldvespsthysadamhpmjeinaahdcxntdllnaaeykoytdacygegwhgjsiyonpywmcmrpwphsvodsrerozsbyaxluzcoxdpamtaaddyoeadlncsehykaeykaeykaocypdbskeuytaadmwtaaddlotaxhdclaxzcfxeegdrpmogrgwkbzctlttweadkiengrwlhtprremouoluutqdpfbncedkynfhaahdcxjpwevdeogthttkmeswzcolcpsaahcfnshkhtehytclmnteatmoteadtlwynnftloamtaaddyoeadlncsghykaeykaeykaocybthlvytstaadmhtaaddlotaxhdclaxhhsnhdrpftdwuocntilydibehnecmovdfekpjkclcslasbhkpawsaddmcmmnahnyaahdcxlotedtndfymyltclhlmtpfsadscnhtztaolbnnkistaedegwfmmedreetnwmcycnamtaaddyoeadlfcsdpykaocyemrewytytaadmhtaadmetaaddlotaxhdclaxdwkswmztpytnswtsecnblfbayajkdldeclqzzolrsnhljedsgminetytbnahatbyaahdcxkkguwsvyimjkvwteytwztyswvendtpmncpasfrrylprnhtkblndrgrmkoyjtbkrpamtaaddyoeadlocsdyykaeykaeykadykaocyhkrpnddrtaadmetaaddlotaxhdclaohnhffmvsbndslrfgclpfjejyatbdpebacnzokotofxntaoemvskpaowmryfnotfgaahdcxdlnbvecentssfsssgylnhkrstoytecrdlyadrekirfaybglahltalsrfcaeerobwamtaaddyoeadlocsdyykaeykaeykaoykaocyhkrpnddrasqdckhh", encoded); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAddressTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAddressTest.java new file mode 100644 index 0000000..96984ad --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAddressTest.java @@ -0,0 +1,21 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoAddressTest { + @Test + public void testAddress() throws CborException { + String hex = "A1035477BFF20C60E522DFAA3350C39B030A5D004E839A"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoAddress cryptoAddress = CryptoAddress.fromCbor(items.get(0)); + Assert.assertEquals("77bff20c60e522dfaa3350c39b030a5d004e839a", TestUtils.bytesToHex(cryptoAddress.getData())); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoBip39Test.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoBip39Test.java new file mode 100644 index 0000000..1c87eb0 --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoBip39Test.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoBip39Test { + @Test + public void testSeed() throws CborException { + String hex = "A2018C66736869656C646567726F75706565726F6465656177616B65646C6F636B6773617573616765646361736865676C6172656477617665646372657765666C616D6565676C6F76650262656E1947DA"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoBip39 cryptoSeed = CryptoBip39.fromCbor(items.get(0)); + Assert.assertEquals(List.of("shield", "group", "erode", "awake", "lock", "sausage", "cash", "glare", "wave", "crew", "flame", "glove"), cryptoSeed.getWords()); + Assert.assertEquals("en", cryptoSeed.getLanguage()); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoECKeyTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoECKeyTest.java new file mode 100644 index 0000000..250574a --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoECKeyTest.java @@ -0,0 +1,23 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoECKeyTest { + @Test + public void testSeed() throws CborException { + String hex = "A202F50358208C05C4B4F3E88840A4F4B5F155CFD69473EA169F3D0431B7A6787A23777F08AA"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoECKey cryptoECKey = CryptoECKey.fromCbor(items.get(0)); + Assert.assertEquals(0, cryptoECKey.getCurve()); + Assert.assertTrue(cryptoECKey.isPrivateKey()); + Assert.assertEquals("8c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa", TestUtils.bytesToHex(cryptoECKey.getData())); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java new file mode 100644 index 0000000..41fd290 --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java @@ -0,0 +1,39 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoHDKeyTest { + @Test + public void testMasterKey() throws CborException { + String hex = "A301F503582100E8F32E723DECF4051AEFAC8E2C93C9C5B214313817CDB01A1494B917C8436B35045820873DFF81C02F525623FD1FE5167EAC3A55A049DE3D314BB42EE227FFED37D508"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoHDKey cryptoHDKey = CryptoHDKey.fromCbor(items.get(0)); + Assert.assertTrue(cryptoHDKey.isMaster()); + Assert.assertEquals("00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", TestUtils.bytesToHex(cryptoHDKey.getKey())); + Assert.assertEquals("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", TestUtils.bytesToHex(cryptoHDKey.getChainCode())); + } + + @Test + public void testPublicTestnet() throws CborException { + String hex = "A4035821026FE2355745BB2DB3630BBC80EF5D58951C963C841F54170BA6E5C12BE7FC12A6045820CED155C72456255881793514EDC5BD9447E7F74ABB88C6D6B6480FD016EE8C8505D90131A1020106D90130A2018A182CF501F501F500F401F4021AE9181CF3"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoHDKey cryptoHDKey = CryptoHDKey.fromCbor(items.get(0)); + Assert.assertFalse(cryptoHDKey.isMaster()); + Assert.assertFalse(cryptoHDKey.isPrivateKey()); + Assert.assertEquals("026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6", TestUtils.bytesToHex(cryptoHDKey.getKey())); + Assert.assertEquals("ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c85", TestUtils.bytesToHex(cryptoHDKey.getChainCode())); + Assert.assertEquals(cryptoHDKey.getUseInfo().getNetwork(), CryptoCoinInfo.Network.TESTNET); + Assert.assertEquals("44'/1'/1'/0/1", cryptoHDKey.getOrigin().getPath()); + Assert.assertEquals("e9181cf3", TestUtils.bytesToHex(cryptoHDKey.getOrigin().getParentFingerprint())); + Assert.assertNull(cryptoHDKey.getChildren()); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoOutputTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoOutputTest.java new file mode 100644 index 0000000..e33e7e3 --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoOutputTest.java @@ -0,0 +1,93 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CryptoOutputTest { + @Test + public void testP2PKHECKey() throws CborException { + String hex = "d90193d90132a103582102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0)); + Assert.assertEquals(List.of(ScriptExpression.PUBLIC_KEY_HASH), cryptoOutput.getScriptExpressions()); + Assert.assertEquals("02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", TestUtils.bytesToHex(cryptoOutput.getEcKey().getData())); + } + + @Test + public void testP2SHP2WPKHECKey() throws CborException { + String hex = "d90190d90194d90132a103582103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0)); + Assert.assertEquals(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH), cryptoOutput.getScriptExpressions()); + Assert.assertEquals("03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556", TestUtils.bytesToHex(cryptoOutput.getEcKey().getData())); + } + + @Test + public void testMultiECKey() throws CborException { + String hex = "d90190d90196a201020282d90132a1035821022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01d90132a103582103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0)); + Assert.assertEquals(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.MULTISIG), cryptoOutput.getScriptExpressions()); + Assert.assertNull(cryptoOutput.getHdKey()); + Assert.assertEquals(2, cryptoOutput.getMultiKey().getThreshold()); + + CryptoECKey firstKey = cryptoOutput.getMultiKey().getEcKeys().get(0); + Assert.assertEquals("022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01", TestUtils.bytesToHex(firstKey.getData())); + + CryptoECKey secondKey = cryptoOutput.getMultiKey().getEcKeys().get(1); + Assert.assertEquals("03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe", TestUtils.bytesToHex(secondKey.getData())); + } + + @Test + public void testP2PKH() throws CborException { + String hex = "d90193d9012fa403582102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0045820637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e2906d90130a20186182cf500f500f5021ad34db33f07d90130a1018401f480f4"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0)); + Assert.assertEquals(List.of(ScriptExpression.PUBLIC_KEY_HASH), cryptoOutput.getScriptExpressions()); + Assert.assertEquals("02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0", TestUtils.bytesToHex(cryptoOutput.getHdKey().getKey())); + Assert.assertEquals("637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29", TestUtils.bytesToHex(cryptoOutput.getHdKey().getChainCode())); + Assert.assertEquals("44'/0'/0'", cryptoOutput.getHdKey().getOrigin().getPath()); + Assert.assertEquals("d34db33f", TestUtils.bytesToHex(cryptoOutput.getHdKey().getOrigin().getParentFingerprint())); + Assert.assertEquals("1/*", cryptoOutput.getHdKey().getChildren().getPath()); + Assert.assertNull(cryptoOutput.getHdKey().getChildren().getParentFingerprint()); + } + + @Test + public void testMulti() throws CborException { + String hex = "d90191d90196a201010282d9012fa403582103cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a704582060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968906d90130a1030007d90130a1018601f400f480f4d9012fa403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0)); + Assert.assertEquals(List.of(ScriptExpression.WITNESS_SCRIPT_HASH, ScriptExpression.MULTISIG), cryptoOutput.getScriptExpressions()); + Assert.assertNull(cryptoOutput.getHdKey()); + Assert.assertEquals(1, cryptoOutput.getMultiKey().getThreshold()); + + CryptoHDKey firstKey = cryptoOutput.getMultiKey().getHdKeys().get(0); + Assert.assertEquals("03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7", TestUtils.bytesToHex(firstKey.getKey())); + Assert.assertEquals("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689", TestUtils.bytesToHex(firstKey.getChainCode())); + Assert.assertNull(firstKey.getOrigin().getPath()); + Assert.assertNull(firstKey.getOrigin().getParentFingerprint()); + Assert.assertEquals(Integer.valueOf(0), firstKey.getOrigin().getDepth()); + Assert.assertEquals("1/0/*", firstKey.getChildren().getPath()); + Assert.assertNull(firstKey.getChildren().getParentFingerprint()); + + CryptoHDKey secondKey = cryptoOutput.getMultiKey().getHdKeys().get(1); + Assert.assertEquals("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea", TestUtils.bytesToHex(secondKey.getKey())); + Assert.assertEquals("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c", TestUtils.bytesToHex(secondKey.getChainCode())); + Assert.assertEquals("0", secondKey.getOrigin().getPath()); + Assert.assertEquals("bd16bee5", TestUtils.bytesToHex(secondKey.getOrigin().getParentFingerprint())); + Assert.assertNull(secondKey.getOrigin().getDepth()); + Assert.assertEquals("0/0/*", secondKey.getChildren().getPath()); + Assert.assertNull(secondKey.getChildren().getParentFingerprint()); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoSeedTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoSeedTest.java new file mode 100644 index 0000000..a2c436e --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoSeedTest.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.hummingbird.registry; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.sparrowwallet.hummingbird.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +public class CryptoSeedTest { + private final DateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy"); + + @Test + public void testSeed() throws CborException { + String hex = "A20150C7098580125E2AB0981253468B2DBC5202D8641947DA"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoSeed cryptoSeed = CryptoSeed.fromCbor(items.get(0)); + Assert.assertEquals("c7098580125e2ab0981253468b2dbc52", TestUtils.bytesToHex(cryptoSeed.getSeed())); + Assert.assertEquals("12 May 2020", dateFormat.format(cryptoSeed.getBirthdate())); + } +}