mirror of
https://github.com/sparrowwallet/hummingbird.git
synced 2024-12-26 01:56:45 +00:00
support encoding registry objects to cbor and ur
This commit is contained in:
parent
e2fd1cd209
commit
c91cf12e77
22 changed files with 448 additions and 42 deletions
|
@ -0,0 +1,7 @@
|
|||
package com.sparrowwallet.hummingbird.registry;
|
||||
|
||||
import co.nstant.in.cbor.model.DataItem;
|
||||
|
||||
public interface CborSerializable {
|
||||
DataItem toCbor();
|
||||
}
|
|
@ -2,11 +2,12 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
|
||||
import co.nstant.in.cbor.model.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class CryptoAccount {
|
||||
public class CryptoAccount extends RegistryItem {
|
||||
public static final long MASTER_FINGERPRINT_KEY = 1;
|
||||
public static final long OUTPUT_DESCRIPTORS_KEY = 2;
|
||||
|
||||
|
@ -26,6 +27,22 @@ public class CryptoAccount {
|
|||
return outputDescriptors;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
map.put(new UnsignedInteger(MASTER_FINGERPRINT_KEY), new UnsignedInteger(new BigInteger(1, masterFingerprint)));
|
||||
Array array = new Array();
|
||||
for(CryptoOutput cryptoOutput : outputDescriptors) {
|
||||
array.add(cryptoOutput.toCbor());
|
||||
}
|
||||
map.put(new UnsignedInteger(OUTPUT_DESCRIPTORS_KEY), array);
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_ACCOUNT;
|
||||
}
|
||||
|
||||
public static CryptoAccount fromCbor(DataItem cbor) {
|
||||
Map cryptoAccountMap = (Map)cbor;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
|
||||
import co.nstant.in.cbor.model.*;
|
||||
|
||||
public class CryptoAddress {
|
||||
public class CryptoAddress extends RegistryItem {
|
||||
public static final long INFO = 1;
|
||||
public static final long TYPE = 2;
|
||||
public static final long DATA = 3;
|
||||
|
@ -29,9 +29,26 @@ public class CryptoAddress {
|
|||
return data;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
if(info != null) {
|
||||
map.put(new UnsignedInteger(INFO), info.toCbor());
|
||||
}
|
||||
if(type != null) {
|
||||
map.put(new UnsignedInteger(TYPE), new UnsignedInteger(type.ordinal()));
|
||||
}
|
||||
map.put(new UnsignedInteger(DATA), new ByteString(data));
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_ADDRESS;
|
||||
}
|
||||
|
||||
public static CryptoAddress fromCbor(DataItem item) {
|
||||
CryptoCoinInfo info = null;
|
||||
Type type = Type.P2PKH;
|
||||
Type type = null;
|
||||
byte[] data = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
|
|
|
@ -5,7 +5,7 @@ import co.nstant.in.cbor.model.*;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CryptoBip39 {
|
||||
public class CryptoBip39 extends RegistryItem {
|
||||
public static final long WORDS = 1;
|
||||
public static final long LANG = 2;
|
||||
|
||||
|
@ -25,9 +25,27 @@ public class CryptoBip39 {
|
|||
return language;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
Array wordsArray = new Array();
|
||||
for(String word : words) {
|
||||
wordsArray.add(new UnicodeString(word));
|
||||
}
|
||||
map.put(new UnsignedInteger(WORDS), wordsArray);
|
||||
if(language != null) {
|
||||
map.put(new UnsignedInteger(LANG), new UnicodeString(language));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_BIP39;
|
||||
}
|
||||
|
||||
public static CryptoBip39 fromCbor(DataItem item) {
|
||||
List<String> words = new ArrayList<>();
|
||||
String language = "en";
|
||||
String language = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
for(DataItem key : map.getKeys()) {
|
||||
|
|
|
@ -4,29 +4,45 @@ 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 class CryptoCoinInfo extends RegistryItem {
|
||||
public static final int TYPE_KEY = 1;
|
||||
public static final int NETWORK_KEY = 2;
|
||||
|
||||
private final int type;
|
||||
private final int network;
|
||||
private final Integer type;
|
||||
private final Integer network;
|
||||
|
||||
public CryptoCoinInfo(int type, int network) {
|
||||
public CryptoCoinInfo(Integer type, Integer network) {
|
||||
this.type = type;
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return Type.values()[type];
|
||||
return type == null ? Type.BITCOIN : Type.values()[type];
|
||||
}
|
||||
|
||||
public Network getNetwork() {
|
||||
return Network.values()[network];
|
||||
return network == null ? Network.MAINNET : Network.values()[network];
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
if(type != null) {
|
||||
map.put(new UnsignedInteger(TYPE_KEY), new UnsignedInteger(type));
|
||||
}
|
||||
if(network != null) {
|
||||
map.put(new UnsignedInteger(NETWORK_KEY), new UnsignedInteger(network));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_COIN_INFO;
|
||||
}
|
||||
|
||||
public static CryptoCoinInfo fromCbor(DataItem item) {
|
||||
int type = 0;
|
||||
int network = 0;
|
||||
Integer type = null;
|
||||
Integer network = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
for(DataItem key : map.getKeys()) {
|
||||
|
|
|
@ -2,36 +2,53 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
|
||||
import co.nstant.in.cbor.model.*;
|
||||
|
||||
public class CryptoECKey {
|
||||
public class CryptoECKey extends RegistryItem {
|
||||
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 Integer curve;
|
||||
private final Boolean privateKey;
|
||||
private final byte[] data;
|
||||
|
||||
public CryptoECKey(int curve, boolean privateKey, byte[] data) {
|
||||
public CryptoECKey(Integer curve, Boolean privateKey, byte[] data) {
|
||||
this.curve = curve;
|
||||
this.privateKey = privateKey;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int getCurve() {
|
||||
return curve;
|
||||
return curve == null ? 0 : curve;
|
||||
}
|
||||
|
||||
public boolean isPrivateKey() {
|
||||
return privateKey;
|
||||
return privateKey == null ? false : privateKey;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
if(curve != null) {
|
||||
map.put(new UnsignedInteger(CURVE), new UnsignedInteger(curve));
|
||||
}
|
||||
if(privateKey != null) {
|
||||
map.put(new UnsignedInteger(PRIVATE), privateKey ? SimpleValue.TRUE : SimpleValue.FALSE);
|
||||
}
|
||||
map.put(new UnsignedInteger(DATA), new ByteString(data));
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_ECKEY;
|
||||
}
|
||||
|
||||
public static CryptoECKey fromCbor(DataItem item) {
|
||||
int curve = 0;
|
||||
boolean privateKey = false;
|
||||
Integer curve = null;
|
||||
Boolean privateKey = null;
|
||||
byte[] data = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
|
|
|
@ -2,9 +2,10 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
|
||||
import co.nstant.in.cbor.model.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CryptoHDKey {
|
||||
public class CryptoHDKey extends RegistryItem {
|
||||
public static final int IS_MASTER_KEY = 1;
|
||||
public static final int IS_PRIVATE_KEY = 2;
|
||||
public static final int KEY_DATA_KEY = 3;
|
||||
|
@ -13,15 +14,19 @@ public class CryptoHDKey {
|
|||
public static final int ORIGIN_KEY = 6;
|
||||
public static final int CHILDREN_KEY = 7;
|
||||
public static final int PARENT_FINGERPRINT_KEY = 8;
|
||||
public static final int NAME_KEY = 9;
|
||||
public static final int NOTE_KEY = 10;
|
||||
|
||||
private final boolean master;
|
||||
private final boolean privateKey;
|
||||
private final Boolean privateKey;
|
||||
private final byte[] key;
|
||||
private final byte[] chainCode;
|
||||
private final CryptoCoinInfo useInfo;
|
||||
private final CryptoKeypath origin;
|
||||
private final CryptoKeypath children;
|
||||
private final byte[] parentFingerprint;
|
||||
private final String name;
|
||||
private final String note;
|
||||
|
||||
public CryptoHDKey(byte[] key, byte[] chainCode) {
|
||||
this.master = true;
|
||||
|
@ -32,9 +37,15 @@ public class CryptoHDKey {
|
|||
this.origin = null;
|
||||
this.children = null;
|
||||
this.parentFingerprint = null;
|
||||
this.name = null;
|
||||
this.note = null;
|
||||
}
|
||||
|
||||
public CryptoHDKey(boolean privateKey, byte[] key, byte[] chainCode, CryptoCoinInfo useInfo, CryptoKeypath origin, CryptoKeypath children, byte[] parentFingerprint) {
|
||||
public CryptoHDKey(Boolean privateKey, byte[] key, byte[] chainCode, CryptoCoinInfo useInfo, CryptoKeypath origin, CryptoKeypath children, byte[] parentFingerprint) {
|
||||
this(privateKey, key, chainCode, useInfo, origin, children, parentFingerprint, null, null);
|
||||
}
|
||||
|
||||
public CryptoHDKey(Boolean privateKey, byte[] key, byte[] chainCode, CryptoCoinInfo useInfo, CryptoKeypath origin, CryptoKeypath children, byte[] parentFingerprint, String name, String note) {
|
||||
this.master = false;
|
||||
this.privateKey = privateKey;
|
||||
this.key = key;
|
||||
|
@ -43,6 +54,8 @@ public class CryptoHDKey {
|
|||
this.origin = origin;
|
||||
this.children = children;
|
||||
this.parentFingerprint = parentFingerprint == null ? null : Arrays.copyOfRange(parentFingerprint, parentFingerprint.length - 4, parentFingerprint.length);
|
||||
this.name = name;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public boolean isMaster() {
|
||||
|
@ -50,7 +63,7 @@ public class CryptoHDKey {
|
|||
}
|
||||
|
||||
public boolean isPrivateKey() {
|
||||
return privateKey;
|
||||
return privateKey == null ? false : privateKey;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
|
@ -77,15 +90,72 @@ public class CryptoHDKey {
|
|||
return parentFingerprint;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
if(master) {
|
||||
map.put(new UnsignedInteger(IS_MASTER_KEY), SimpleValue.TRUE);
|
||||
map.put(new UnsignedInteger(KEY_DATA_KEY), new ByteString(key));
|
||||
map.put(new UnsignedInteger(CHAIN_CODE_KEY), new ByteString(chainCode));
|
||||
} else {
|
||||
if(privateKey != null) {
|
||||
map.put(new UnsignedInteger(IS_PRIVATE_KEY), privateKey ? SimpleValue.TRUE : SimpleValue.FALSE);
|
||||
}
|
||||
map.put(new UnsignedInteger(KEY_DATA_KEY), new ByteString(key));
|
||||
if(chainCode != null) {
|
||||
map.put(new UnsignedInteger(CHAIN_CODE_KEY), new ByteString(chainCode));
|
||||
}
|
||||
if(useInfo != null) {
|
||||
DataItem useInfoItem = useInfo.toCbor();
|
||||
useInfoItem.setTag(RegistryType.CRYPTO_COIN_INFO.getTag());
|
||||
map.put(new UnsignedInteger(USE_INFO_KEY), useInfoItem);
|
||||
}
|
||||
if(origin != null) {
|
||||
DataItem originItem = origin.toCbor();
|
||||
originItem.setTag(RegistryType.CRYPTO_KEYPATH.getTag());
|
||||
map.put(new UnsignedInteger(ORIGIN_KEY), originItem);
|
||||
}
|
||||
if(children != null) {
|
||||
DataItem childrenItem = children.toCbor();
|
||||
childrenItem.setTag(RegistryType.CRYPTO_KEYPATH.getTag());
|
||||
map.put(new UnsignedInteger(CHILDREN_KEY), childrenItem);
|
||||
}
|
||||
if(parentFingerprint != null) {
|
||||
map.put(new UnsignedInteger(PARENT_FINGERPRINT_KEY), new UnsignedInteger(new BigInteger(1, parentFingerprint)));
|
||||
}
|
||||
if(name != null) {
|
||||
map.put(new UnsignedInteger(NAME_KEY), new UnicodeString(name));
|
||||
}
|
||||
if(note != null) {
|
||||
map.put(new UnsignedInteger(NOTE_KEY), new UnicodeString(note));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_HDKEY;
|
||||
}
|
||||
|
||||
public static CryptoHDKey fromCbor(DataItem item) {
|
||||
boolean isMasterKey = false;
|
||||
boolean isPrivateKey = false;
|
||||
Boolean isPrivateKey = null;
|
||||
byte[] keyData = null;
|
||||
byte[] chainCode = null;
|
||||
CryptoCoinInfo useInfo = null;
|
||||
CryptoKeypath origin = null;
|
||||
CryptoKeypath children = null;
|
||||
byte[] parentFingerprint = null;
|
||||
String name = null;
|
||||
String note = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
for(DataItem key : map.getKeys()) {
|
||||
|
@ -107,6 +177,10 @@ public class CryptoHDKey {
|
|||
children = CryptoKeypath.fromCbor(map.get(uintKey));
|
||||
} else if(intKey == PARENT_FINGERPRINT_KEY) {
|
||||
parentFingerprint = ((UnsignedInteger)map.get(uintKey)).getValue().toByteArray();
|
||||
} else if(intKey == NAME_KEY) {
|
||||
name = ((UnicodeString)map.get(uintKey)).getString();
|
||||
} else if(intKey == NOTE_KEY) {
|
||||
note = ((UnicodeString)map.get(uintKey)).getString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,9 +189,13 @@ public class CryptoHDKey {
|
|||
}
|
||||
|
||||
if(isMasterKey) {
|
||||
if(chainCode == null) {
|
||||
throw new IllegalStateException("Chain code data is null");
|
||||
}
|
||||
|
||||
return new CryptoHDKey(keyData, chainCode);
|
||||
} else {
|
||||
return new CryptoHDKey(isPrivateKey, keyData, chainCode, useInfo, origin, children, parentFingerprint);
|
||||
return new CryptoHDKey(isPrivateKey, keyData, chainCode, useInfo, origin, children, parentFingerprint, name, note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
|
||||
import co.nstant.in.cbor.model.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class CryptoKeypath {
|
||||
public class CryptoKeypath extends RegistryItem {
|
||||
public static final int COMPONENTS_KEY = 1;
|
||||
public static final int SOURCE_FINGERPRINT_KEY = 2;
|
||||
public static final int DEPTH_KEY = 3;
|
||||
|
@ -50,6 +51,34 @@ public class CryptoKeypath {
|
|||
return depth;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
Array componentArray = new Array();
|
||||
for(PathComponent pathComponent : components) {
|
||||
if(pathComponent.isWildcard()) {
|
||||
componentArray.add(new Array());
|
||||
} else {
|
||||
componentArray.add(new UnsignedInteger(pathComponent.getIndex()));
|
||||
}
|
||||
componentArray.add(pathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
|
||||
}
|
||||
if(!componentArray.getDataItems().isEmpty()) {
|
||||
map.put(new UnsignedInteger(COMPONENTS_KEY), componentArray);
|
||||
}
|
||||
if(sourceFingerprint != null) {
|
||||
map.put(new UnsignedInteger(SOURCE_FINGERPRINT_KEY), new UnsignedInteger(new BigInteger(1, sourceFingerprint)));
|
||||
}
|
||||
if(depth != null) {
|
||||
map.put(new UnsignedInteger(DEPTH_KEY), new UnsignedInteger(depth));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_KEYPATH;
|
||||
}
|
||||
|
||||
public static CryptoKeypath fromCbor(DataItem item) {
|
||||
List<PathComponent> components = new ArrayList<>();
|
||||
byte[] sourceFingerprint = null;
|
||||
|
|
|
@ -8,7 +8,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CryptoOutput {
|
||||
public class CryptoOutput extends RegistryItem {
|
||||
private final List<ScriptExpression> scriptExpressions;
|
||||
|
||||
//Only one of the following will be not null
|
||||
|
@ -53,6 +53,37 @@ public class CryptoOutput {
|
|||
return multiKey;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
DataItem item = null;
|
||||
if(multiKey != null) {
|
||||
item = multiKey.toCbor();
|
||||
} else if(ecKey != null) {
|
||||
item = ecKey.toCbor();
|
||||
item.setTag(RegistryType.CRYPTO_ECKEY.getTag());
|
||||
} else if(hdKey != null) {
|
||||
item = hdKey.toCbor();
|
||||
item.setTag(RegistryType.CRYPTO_HDKEY.getTag());
|
||||
}
|
||||
|
||||
Tag tag = item.getTag();
|
||||
for(int i = scriptExpressions.size() - 1; i >= 0; i--) {
|
||||
Tag newTag = new Tag(scriptExpressions.get(i).getTagValue());
|
||||
if(tag == null) {
|
||||
item.setTag(newTag);
|
||||
} else {
|
||||
tag.setTag(newTag);
|
||||
}
|
||||
tag = newTag;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_OUTPUT;
|
||||
}
|
||||
|
||||
public static CryptoOutput fromCbor(DataItem cbor) {
|
||||
List<ScriptExpression> expressions = new ArrayList<>();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.sparrowwallet.hummingbird.registry;
|
|||
import co.nstant.in.cbor.model.ByteString;
|
||||
import co.nstant.in.cbor.model.DataItem;
|
||||
|
||||
public class CryptoPSBT {
|
||||
public class CryptoPSBT extends RegistryItem {
|
||||
private final byte[] psbt;
|
||||
|
||||
public CryptoPSBT(byte[] psbt) {
|
||||
|
@ -14,6 +14,15 @@ public class CryptoPSBT {
|
|||
return psbt;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
return new ByteString(psbt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_PSBT;
|
||||
}
|
||||
|
||||
public static CryptoPSBT fromCbor(DataItem item) {
|
||||
return new CryptoPSBT(((ByteString)item).getBytes());
|
||||
}
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
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 co.nstant.in.cbor.model.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class CryptoSeed {
|
||||
public static final long PAYLOAD = 1;
|
||||
public static final long BIRTHDATE = 2;
|
||||
public class CryptoSeed extends RegistryItem {
|
||||
public static final long PAYLOAD_KEY = 1;
|
||||
public static final long BIRTHDATE_KEY = 2;
|
||||
public static final long NAME_KEY = 3;
|
||||
public static final long NOTE_KEY = 4;
|
||||
|
||||
private final byte[] seed;
|
||||
private final Date birthdate;
|
||||
private final String name;
|
||||
private final String note;
|
||||
|
||||
public CryptoSeed(byte[] seed, Date birthdate) {
|
||||
this(seed, birthdate, null, null);
|
||||
}
|
||||
|
||||
public CryptoSeed(byte[] seed, Date birthdate, String name, String note) {
|
||||
this.seed = seed;
|
||||
this.birthdate = birthdate;
|
||||
this.name = name;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public byte[] getSeed() {
|
||||
|
@ -27,18 +34,54 @@ public class CryptoSeed {
|
|||
return birthdate;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
map.put(new UnsignedInteger(PAYLOAD_KEY), new ByteString(seed));
|
||||
if(birthdate != null) {
|
||||
DataItem birthdateItem = new UnsignedInteger(birthdate.getTime() / (1000 * 60 * 60 * 24));
|
||||
birthdateItem.setTag(100);
|
||||
map.put(new UnsignedInteger(BIRTHDATE_KEY), birthdateItem);
|
||||
}
|
||||
if(name != null) {
|
||||
map.put(new UnsignedInteger(NAME_KEY), new UnicodeString(name));
|
||||
}
|
||||
if(note != null) {
|
||||
map.put(new UnsignedInteger(NOTE_KEY), new UnicodeString(note));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryType getRegistryType() {
|
||||
return RegistryType.CRYPTO_SEED;
|
||||
}
|
||||
|
||||
public static CryptoSeed fromCbor(DataItem item) {
|
||||
byte[] seed = null;
|
||||
Date birthdate = null;
|
||||
String name = null;
|
||||
String note = null;
|
||||
|
||||
Map map = (Map)item;
|
||||
for(DataItem key : map.getKeys()) {
|
||||
UnsignedInteger uintKey = (UnsignedInteger)key;
|
||||
int intKey = uintKey.getValue().intValue();
|
||||
if(intKey == PAYLOAD) {
|
||||
if(intKey == PAYLOAD_KEY) {
|
||||
seed = ((ByteString)map.get(key)).getBytes();
|
||||
} else if(intKey == BIRTHDATE) {
|
||||
} else if(intKey == BIRTHDATE_KEY) {
|
||||
birthdate = new Date(((UnsignedInteger)map.get(key)).getValue().longValue() * 1000 * 60 * 60 * 24);
|
||||
} else if(intKey == NAME_KEY) {
|
||||
name = ((UnicodeString)map.get(key)).getString();
|
||||
} else if(intKey == NOTE_KEY) {
|
||||
note = ((UnicodeString)map.get(key)).getString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +89,6 @@ public class CryptoSeed {
|
|||
throw new IllegalStateException("Seed is null");
|
||||
}
|
||||
|
||||
return new CryptoSeed(seed, birthdate);
|
||||
return new CryptoSeed(seed, birthdate, name, note);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import co.nstant.in.cbor.model.UnsignedInteger;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MultiKey {
|
||||
public class MultiKey implements CborSerializable {
|
||||
public static final int THRESHOLD_KEY = 1;
|
||||
public static final int KEYS_KEY = 2;
|
||||
|
||||
|
@ -34,6 +34,27 @@ public class MultiKey {
|
|||
return hdKeys;
|
||||
}
|
||||
|
||||
public DataItem toCbor() {
|
||||
Map map = new Map();
|
||||
map.put(new UnsignedInteger(THRESHOLD_KEY), new UnsignedInteger(threshold));
|
||||
Array array = new Array();
|
||||
if(ecKeys != null && !ecKeys.isEmpty()) {
|
||||
for(CryptoECKey cryptoECKey : ecKeys) {
|
||||
DataItem eckeyItem = cryptoECKey.toCbor();
|
||||
eckeyItem.setTag(RegistryType.CRYPTO_ECKEY.getTag());
|
||||
array.add(eckeyItem);
|
||||
}
|
||||
} else if(hdKeys != null) {
|
||||
for(CryptoHDKey cryptoHDKey : hdKeys) {
|
||||
DataItem hdkeyItem = cryptoHDKey.toCbor();
|
||||
hdkeyItem.setTag(RegistryType.CRYPTO_HDKEY.getTag());
|
||||
array.add(hdkeyItem);
|
||||
}
|
||||
}
|
||||
map.put(new UnsignedInteger(KEYS_KEY), array);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static MultiKey fromCbor(DataItem item) {
|
||||
int threshold = 0;
|
||||
List<CryptoECKey> ecKeys = new ArrayList<>();
|
||||
|
@ -57,6 +78,10 @@ public class MultiKey {
|
|||
}
|
||||
}
|
||||
|
||||
if(ecKeys.isEmpty() && hdKeys.isEmpty()) {
|
||||
throw new IllegalStateException("One or more of eckey or hdkey must be specified");
|
||||
}
|
||||
|
||||
return new MultiKey(threshold, ecKeys, hdKeys);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.sparrowwallet.hummingbird.registry;
|
||||
|
||||
import co.nstant.in.cbor.CborEncoder;
|
||||
import co.nstant.in.cbor.CborException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public abstract class RegistryItem implements CborSerializable {
|
||||
public abstract RegistryType getRegistryType();
|
||||
|
||||
public UR toUR() {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
CborEncoder encoder = new CborEncoder(baos);
|
||||
encoder.encode(toCbor());
|
||||
return new UR(getRegistryType(), baos.toByteArray());
|
||||
} catch(CborException | UR.InvalidTypeException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
package com.sparrowwallet.hummingbird;
|
||||
|
||||
import co.nstant.in.cbor.CborEncoder;
|
||||
import co.nstant.in.cbor.CborException;
|
||||
import co.nstant.in.cbor.model.DataItem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class TestUtils {
|
||||
public static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
|
@ -21,4 +27,11 @@ public class TestUtils {
|
|||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
public static String encode(DataItem item) throws CborException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
CborEncoder encoder = new CborEncoder(baos);
|
||||
encoder.encode(item);
|
||||
return bytesToHex(baos.toByteArray());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,10 @@ public class CryptoAccountTest {
|
|||
Assert.assertEquals("48'/0'/0'/2'", cryptoOutput6.getHdKey().getOrigin().getPath());
|
||||
Assert.assertEquals("59b69b2a", TestUtils.bytesToHex(cryptoOutput6.getHdKey().getParentFingerprint()));
|
||||
Assert.assertNull(cryptoOutput6.getHdKey().getChildren());
|
||||
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoAccount.toCbor()));
|
||||
String ur = "ur:crypto-account/oeadcyemrewytyaolntaadmutaaddloxaxhdclaxwmfmdeiamecsdsemgtvsjzcncygrkowtrontzschgezokstswkkscfmklrtauteyaahdcxiehfonurdppfyntapejpproypegrdawkgmaewejlsfdtsrfybdehcaflmtrlbdhpamtaaddyoyadlncsdwykaeykaeykaycynlytsnyltaadmhtaadmwtaaddloxaxhdclaostvelfemdyynwydwyaievosrgmambklovabdgypdglldvespsthysadamhpmjeinaahdcxntdllnaaeykoytdacygegwhgjsiyonpywmcmrpwphsvodsrerozsbyaxluzcoxdpamtaaddyoyadlncsehykaeykaeykaycypdbskeuytaadmwtaaddloxaxhdclaxzcfxeegdrpmogrgwkbzctlttweadkiengrwlhtprremouoluutqdpfbncedkynfhaahdcxjpwevdeogthttkmeswzcolcpsaahcfnshkhtehytclmnteatmoteadtlwynnftloamtaaddyoyadlncsghykaeykaeykaycybthlvytstaadmhtaaddloxaxhdclaxhhsnhdrpftdwuocntilydibehnecmovdfekpjkclcslasbhkpawsaddmcmmnahnyaahdcxlotedtndfymyltclhlmtpfsadscnhtztaolbnnkistaedegwfmmedreetnwmcycnamtaaddyoyadlfcsdpykaycyemrewytytaadmhtaadmetaaddloxaxhdclaxdwkswmztpytnswtsecnblfbayajkdldeclqzzolrsnhljedsgminetytbnahatbyaahdcxkkguwsvyimjkvwteytwztyswvendtpmncpasfrrylprnhtkblndrgrmkoyjtbkrpamtaaddyoyadlocsdyykaeykaeykadykaycyhkrpnddrtaadmetaaddloxaxhdclaohnhffmvsbndslrfgclpfjejyatbdpebacnzokotofxntaoemvskpaowmryfnotfgaahdcxdlnbvecentssfsssgylnhkrstoytecrdlyadrekirfaybglahltalsrfcaeerobwamtaaddyoyadlocsdyykaeykaeykaoykaycyhkrpnddrgdaogykb";
|
||||
Assert.assertEquals(ur, cryptoAccount.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -17,5 +17,8 @@ public class CryptoAddressTest {
|
|||
List<DataItem> items = CborDecoder.decode(data);
|
||||
CryptoAddress cryptoAddress = CryptoAddress.fromCbor(items.get(0));
|
||||
Assert.assertEquals("77bff20c60e522dfaa3350c39b030a5d004e839a", TestUtils.bytesToHex(cryptoAddress.getData()));
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoAddress.toCbor()));
|
||||
String ur = "ur:crypto-address/oyaxghktrswzbnhnvwcpurpkeogdsrndaxbkhlaegllsnyolrsemgu";
|
||||
Assert.assertEquals(ur, cryptoAddress.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,14 @@ import java.util.List;
|
|||
public class CryptoBip39Test {
|
||||
@Test
|
||||
public void testSeed() throws CborException {
|
||||
String hex = "A2018C66736869656C646567726F75706565726F6465656177616B65646C6F636B6773617573616765646361736865676C6172656477617665646372657765666C616D6565676C6F76650262656E1947DA";
|
||||
String hex = "A2018C66736869656C646567726F75706565726F6465656177616B65646C6F636B6773617573616765646361736865676C6172656477617665646372657765666C616D6565676C6F76650262656E";
|
||||
byte[] data = TestUtils.hexToBytes(hex);
|
||||
List<DataItem> items = CborDecoder.decode(data);
|
||||
CryptoBip39 cryptoSeed = CryptoBip39.fromCbor(items.get(0));
|
||||
Assert.assertEquals(Arrays.asList("shield", "group", "erode", "awake", "lock", "sausage", "cash", "glare", "wave", "crew", "flame", "glove"), cryptoSeed.getWords());
|
||||
Assert.assertEquals("en", cryptoSeed.getLanguage());
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoSeed.toCbor()));
|
||||
String ur = "ur:crypto-bip39/oeadlkiyjkisinihjzieihiojpjlkpjoihihjpjlieihihhskthsjeihiejzjliajeiojkhskpjkhsioihieiahsjkisihiojzhsjpihiekthskoihieiajpihktihiyjzhsjnihihiojzjlkoihaoidihjtrkkndede";
|
||||
Assert.assertEquals(ur, cryptoSeed.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,5 +19,8 @@ public class CryptoECKeyTest {
|
|||
Assert.assertEquals(0, cryptoECKey.getCurve());
|
||||
Assert.assertTrue(cryptoECKey.isPrivateKey());
|
||||
Assert.assertEquals("8c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa", TestUtils.bytesToHex(cryptoECKey.getData()));
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoECKey.toCbor()));
|
||||
String ur = "ur:crypto-eckey/oeaoykaxhdcxlkahssqzwfvslofzoxwkrewngotktbmwjkwdcmnefsaaehrlolkskncnktlbaypkrphsmyid";
|
||||
Assert.assertEquals(ur, cryptoECKey.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@ public class CryptoHDKeyTest {
|
|||
Assert.assertTrue(cryptoHDKey.isMaster());
|
||||
Assert.assertEquals("00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", TestUtils.bytesToHex(cryptoHDKey.getKey()));
|
||||
Assert.assertEquals("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", TestUtils.bytesToHex(cryptoHDKey.getChainCode()));
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoHDKey.toCbor()));
|
||||
String ur = "ur:crypto-hdkey/otadykaxhdclaevswfdmjpfswpwkahcywspsmndwmusoskprbbehetchsnpfcybbmwrhchspfxjeecaahdcxltfszmlyrtdlgmhfcnzcctvwcmkbpsftgonbgauefsehgrqzdmvodizmweemtlaybakiylat";
|
||||
Assert.assertEquals(ur, cryptoHDKey.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -35,5 +38,8 @@ public class CryptoHDKeyTest {
|
|||
Assert.assertEquals("44'/1'/1'/0/1", cryptoHDKey.getOrigin().getPath());
|
||||
Assert.assertEquals("e9181cf3", TestUtils.bytesToHex(cryptoHDKey.getParentFingerprint()));
|
||||
Assert.assertNull(cryptoHDKey.getChildren());
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoHDKey.toCbor()));
|
||||
String ur = "ur:crypto-hdkey/onaxhdclaojlvoechgferkdpqdiabdrflawshlhdmdcemtfnlrctghchbdolvwsednvdztbgolaahdcxtottgostdkhfdahdlykkecbbweskrymwflvdylgerkloswtbrpfdbsticmwylklpahtaadehoyaoadamtaaddyoyadlecsdwykadykadykaewkadwkaycywlcscewfihbdaehn";
|
||||
Assert.assertEquals(ur, cryptoHDKey.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ public class CryptoOutputTest {
|
|||
CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0));
|
||||
Assert.assertEquals(Collections.singletonList(ScriptExpression.PUBLIC_KEY_HASH), cryptoOutput.getScriptExpressions());
|
||||
Assert.assertEquals("02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", TestUtils.bytesToHex(cryptoOutput.getEcKey().getData()));
|
||||
Assert.assertEquals(hex, TestUtils.encode(cryptoOutput.toCbor()));
|
||||
String ur = "ur:crypto-output/taadmutaadeyoyaxhdclaoswaalbmwfpwekijndyfefzjtmdrtketphhktmngrlkwsfnospypsasrhhhjonnvwtsqzwljy";
|
||||
Assert.assertEquals(ur, cryptoOutput.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -30,6 +33,9 @@ public class CryptoOutputTest {
|
|||
CryptoOutput cryptoOutput = CryptoOutput.fromCbor(items.get(0));
|
||||
Assert.assertEquals(Arrays.asList(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH), cryptoOutput.getScriptExpressions());
|
||||
Assert.assertEquals("03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556", TestUtils.bytesToHex(cryptoOutput.getEcKey().getData()));
|
||||
Assert.assertEquals(hex, TestUtils.encode(cryptoOutput.toCbor()));
|
||||
String ur = "ur:crypto-output/taadmhtaadmwtaadeyoyaxhdclaxzmytkgtlkphywyoxcxfeftbbecgmectelfynfldllpisoyludlahknbbhndtkphfhlehmust";
|
||||
Assert.assertEquals(ur, cryptoOutput.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -47,6 +53,10 @@ public class CryptoOutputTest {
|
|||
|
||||
CryptoECKey secondKey = cryptoOutput.getMultiKey().getEcKeys().get(1);
|
||||
Assert.assertEquals("03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe", TestUtils.bytesToHex(secondKey.getData()));
|
||||
|
||||
Assert.assertEquals(hex, TestUtils.encode(cryptoOutput.toCbor()));
|
||||
String ur = "ur:crypto-output/taadmhtaadmtoeadaoaolftaadeyoyaxhdclaodladvwvyhhsgeccapewflrfhrlbsfndlbkcwutahvwpeloleioksglwfvybkdradtaadeyoyaxhdclaxpstylrvowtstynguaspmchlenegonyryvtmsmtmsgshgvdbbsrhebybtztdisfrnpfadremh";
|
||||
Assert.assertEquals(ur, cryptoOutput.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -63,6 +73,9 @@ public class CryptoOutputTest {
|
|||
Assert.assertEquals("78412e3a", TestUtils.bytesToHex(cryptoOutput.getHdKey().getParentFingerprint()));
|
||||
Assert.assertEquals("1/*", cryptoOutput.getHdKey().getChildren().getPath());
|
||||
Assert.assertNull(cryptoOutput.getHdKey().getChildren().getSourceFingerprint());
|
||||
Assert.assertEquals(hex, TestUtils.encode(cryptoOutput.toCbor()));
|
||||
String ur = "ur:crypto-output/taadmutaaddlonaxhdclaotdqdinaeesjzmolfzsbbidlpiyhddlcximhltirfsptlvsmohscsamsgzoaxadwtaahdcxiaksataxbtgotictnybnqdoslsmdbztsmtryatjoialnolweuramsfdtolhtbadtamtaaddyoeadlncsdwykaeykaeykaocytegtqdfhattaaddyoyadlradwklawkaycyksfpdmftpyaaeelb";
|
||||
Assert.assertEquals(ur, cryptoOutput.toUR().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -92,5 +105,9 @@ public class CryptoOutputTest {
|
|||
Assert.assertNull(secondKey.getOrigin().getDepth());
|
||||
Assert.assertEquals("0/0/*", secondKey.getChildren().getPath());
|
||||
Assert.assertNull(secondKey.getChildren().getSourceFingerprint());
|
||||
|
||||
Assert.assertEquals(hex, TestUtils.encode(cryptoOutput.toCbor()));
|
||||
String ur = "ur:crypto-output/taadmetaadmtoeadadaolftaaddloxaxhdclaxsbsgptsolkltkndsmskiaelfhhmdimcnmnlgutzotecpsfveylgrbdhptbpsveosaahdcxhnganelacwldjnlschnyfxjyplrllfdrplpswdnbuyctlpwyfmmhgsgtwsrymtldamtaaddyoyaxaeattaaddyoyadlnadwkaewklawktaaddloxaxhdclaoztnnhtwtpslgndfnwpzedrlomnclchrdfsayntlplplojznslfjejecpptlgbgwdaahdcxwtmhnyzmpkkbvdpyvwutglbeahmktyuogusnjonththhdwpsfzvdfpdlcndlkensamtaaddyoeadlfaewkaocyrycmrnvwattaaddyoyadlnaewkaewklawkkkztdlon";
|
||||
Assert.assertEquals(ur, cryptoOutput.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
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 CryptoPSBTTest {
|
||||
@Test
|
||||
public void testEncode() throws CborException {
|
||||
String hex = "58A770736274FF01009A020000000258E87A21B56DAF0C23BE8E7070456C336F7CBAA5C8757924F545887BB2ABDD750000000000FFFFFFFF838D0427D0EC650A68AA46BB0B098AEA4422C071B2CA78352A077959D07CEA1D0100000000FFFFFFFF0270AAF00800000000160014D85C2B71D0060B09C9886AEB815E50991DDA124D00E1F5050000000016001400AEA9A2E5F0F876A588DF5546E8742D1D87008F000000000000000000";
|
||||
byte[] data = TestUtils.hexToBytes(hex);
|
||||
List<DataItem> items = CborDecoder.decode(data);
|
||||
CryptoPSBT cryptoPSBT = CryptoPSBT.fromCbor(items.get(0));
|
||||
String psbtHex = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000";
|
||||
Assert.assertArrayEquals(TestUtils.hexToBytes(psbtHex), cryptoPSBT.getPsbt());
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoPSBT.toCbor()));
|
||||
String ur = "ur:crypto-psbt/hdosjojkidjyzmadaenyaoaeaeaeaohdvsknclrejnpebncnrnmnjojofejzeojlkerdonspkpkkdkykfelokgprpyutkpaeaeaeaeaezmzmzmzmlslgaaditiwpihbkispkfgrkbdaslewdfycprtjsprsgksecdratkkhktikewdcaadaeaeaeaezmzmzmzmaojopkwtayaeaeaeaecmaebbtphhdnjstiambdassoloimwmlyhygdnlcatnbggtaevyykahaeaeaeaecmaebbaeplptoevwwtyakoonlourgofgvsjydpcaltaemyaeaeaeaeaeaeaeaeaebkgdcarh";
|
||||
Assert.assertEquals(ur, cryptoPSBT.toUR().toString());
|
||||
}
|
||||
}
|
|
@ -22,5 +22,8 @@ public class CryptoSeedTest {
|
|||
CryptoSeed cryptoSeed = CryptoSeed.fromCbor(items.get(0));
|
||||
Assert.assertEquals("c7098580125e2ab0981253468b2dbc52", TestUtils.bytesToHex(cryptoSeed.getSeed()));
|
||||
Assert.assertEquals("12 May 2020", dateFormat.format(cryptoSeed.getBirthdate()));
|
||||
Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoSeed.toCbor()));
|
||||
String ur = "ur:crypto-seed/oeadgdstaslplabghydrpfmkbggufgludprfgmaotpiecffltnlpqdenos";
|
||||
Assert.assertEquals(ur, cryptoSeed.toUR().toString());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue