support range and pair crypto-hdkey path components

This commit is contained in:
Craig Raw 2023-05-29 14:35:46 +02:00
parent 9bc8f6b021
commit 99d779c657
8 changed files with 205 additions and 62 deletions

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.hummingbird.registry;
import co.nstant.in.cbor.model.*;
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
import java.math.BigInteger;
import java.util.ArrayList;
@ -38,7 +39,7 @@ public class CryptoKeypath extends RegistryItem {
StringJoiner joiner = new StringJoiner("/");
for(PathComponent component : components) {
joiner.add((component.isWildcard() ? "*" : component.getIndex()) + (component.isHardened() ? "'" : ""));
joiner.add(component.toString());
}
return joiner.toString();
}
@ -53,16 +54,7 @@ public class CryptoKeypath extends RegistryItem {
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);
}
map.put(new UnsignedInteger(COMPONENTS_KEY), componentArray);
map.put(new UnsignedInteger(COMPONENTS_KEY), PathComponent.toCbor(components));
if(sourceFingerprint != null) {
map.put(new UnsignedInteger(SOURCE_FINGERPRINT_KEY), new UnsignedInteger(new BigInteger(1, sourceFingerprint)));
}
@ -87,17 +79,7 @@ public class CryptoKeypath extends RegistryItem {
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));
}
}
components = PathComponent.fromCbor(map.get(key));
} else if(intKey == SOURCE_FINGERPRINT_KEY) {
sourceFingerprint = bigIntegerToBytes(((UnsignedInteger)map.get(key)).getValue(), 4);
} else if(intKey == DEPTH_KEY) {

View file

@ -1,37 +0,0 @@
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;
}
}

View file

@ -0,0 +1,27 @@
package com.sparrowwallet.hummingbird.registry.pathcomponent;
public class IndexPathComponent extends PathComponent {
private final int index;
private final boolean hardened;
public IndexPathComponent(int index, boolean hardened) {
this.index = index;
this.hardened = hardened;
if((index & HARDENED_BIT) != 0) {
throw new IllegalArgumentException("Invalid index " + index + " - most significant bit cannot be set");
}
}
public int getIndex() {
return index;
}
public boolean isHardened() {
return hardened;
}
public String toString() {
return index + (hardened ? "'" : "");
}
}

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.hummingbird.registry.pathcomponent;
public class PairPathComponent extends PathComponent {
private final IndexPathComponent external;
private final IndexPathComponent internal;
public PairPathComponent(IndexPathComponent external, IndexPathComponent internal) {
this.external = external;
this.internal = internal;
}
public IndexPathComponent getExternal() {
return external;
}
public IndexPathComponent getInternal() {
return internal;
}
public String toString() {
return "<" + external.toString() + ";" + internal.toString() + ">";
}
}

View file

@ -0,0 +1,82 @@
package com.sparrowwallet.hummingbird.registry.pathcomponent;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.SimpleValue;
import co.nstant.in.cbor.model.UnsignedInteger;
import java.util.ArrayList;
import java.util.List;
public abstract class PathComponent {
public static final int HARDENED_BIT = 0x80000000;
public static DataItem toCbor(List<PathComponent> components) {
Array componentArray = new Array();
for(PathComponent pathComponent : components) {
if(pathComponent instanceof WildcardPathComponent) {
WildcardPathComponent wildcardPathComponent = (WildcardPathComponent)pathComponent;
componentArray.add(new Array());
componentArray.add(wildcardPathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
} else if(pathComponent instanceof RangePathComponent) {
RangePathComponent rangePathComponent = (RangePathComponent)pathComponent;
Array array = new Array();
array.add(new UnsignedInteger(rangePathComponent.getStart()));
array.add(new UnsignedInteger(rangePathComponent.getEnd()));
componentArray.add(array);
componentArray.add(rangePathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
} else if(pathComponent instanceof PairPathComponent) {
PairPathComponent pairPathComponent = (PairPathComponent)pathComponent;
Array array = new Array();
array.add(new UnsignedInteger(pairPathComponent.getExternal().getIndex()));
array.add(pairPathComponent.getExternal().isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
array.add(new UnsignedInteger(pairPathComponent.getInternal().getIndex()));
array.add(pairPathComponent.getInternal().isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
componentArray.add(array);
} else if(pathComponent instanceof IndexPathComponent) {
IndexPathComponent indexPathComponent = (IndexPathComponent)pathComponent;
componentArray.add(new UnsignedInteger(indexPathComponent.getIndex()));
componentArray.add(indexPathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE);
} else {
throw new IllegalArgumentException("Unknown path component of " + pathComponent.getClass());
}
}
return componentArray;
}
public static List<PathComponent> fromCbor(DataItem item) {
List<PathComponent> components = new ArrayList<>();
Array componentArray = (Array)item;
for(int i = 0; i < componentArray.getDataItems().size(); i++) {
DataItem component = componentArray.getDataItems().get(i);
if(component instanceof Array) {
Array subcomponentArray = (Array)component;
if(subcomponentArray.getDataItems().isEmpty()) {
boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE);
components.add(new WildcardPathComponent(hardened));
} else if(subcomponentArray.getDataItems().size() == 2) {
boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE);
UnsignedInteger startIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(0);
UnsignedInteger endIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(1);
components.add(new RangePathComponent(startIndex.getValue().intValue(), endIndex.getValue().intValue(), hardened));
} else if(subcomponentArray.getDataItems().size() == 4) {
UnsignedInteger externalIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(0);
boolean externalHardened = (subcomponentArray.getDataItems().get(1) == SimpleValue.TRUE);
IndexPathComponent externalPathComponent = new IndexPathComponent(externalIndex.getValue().intValue(), externalHardened);
UnsignedInteger internalIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(2);
boolean internalHardened = (subcomponentArray.getDataItems().get(3) == SimpleValue.TRUE);
IndexPathComponent internalPathComponent = new IndexPathComponent(internalIndex.getValue().intValue(), internalHardened);
components.add(new PairPathComponent(externalPathComponent, internalPathComponent));
}
} else if(component instanceof UnsignedInteger) {
UnsignedInteger index = (UnsignedInteger)component;
boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE);
components.add(new IndexPathComponent(index.getValue().intValue(), hardened));
}
}
return components;
}
}

View file

@ -0,0 +1,37 @@
package com.sparrowwallet.hummingbird.registry.pathcomponent;
public class RangePathComponent extends PathComponent {
private final int start;
private final int end;
private final boolean hardened;
public RangePathComponent(int start, int end, boolean hardened) {
this.start = start;
this.end = end;
this.hardened = hardened;
if((start & HARDENED_BIT) != 0 || (end & HARDENED_BIT) != 0) {
throw new IllegalArgumentException("Invalid range [" + start + ", " + end + "] - most significant bit cannot be set");
}
if(start >= end) {
throw new IllegalArgumentException("Invalid range [" + start + ", " + end + "] - start must be lower than end");
}
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
public boolean isHardened() {
return hardened;
}
public String toString() {
return "[" + start + (hardened ? "'" : "") + "-" + end + (hardened ? "'" : "") + "]";
}
}

View file

@ -0,0 +1,17 @@
package com.sparrowwallet.hummingbird.registry.pathcomponent;
public class WildcardPathComponent extends PathComponent {
private final boolean hardened;
public WildcardPathComponent(boolean hardened) {
this.hardened = hardened;
}
public boolean isHardened() {
return hardened;
}
public String toString() {
return "*";
}
}

View file

@ -3,9 +3,7 @@ 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 com.sparrowwallet.hummingbird.*;
import org.junit.Assert;
import org.junit.Test;
@ -159,4 +157,18 @@ public class CryptoAccountTest {
String encoded = UREncoder.encode(ur);
Assert.assertEquals("ur:crypto-account/oeadcyemrewytyaolntaadmutaaddloxaxhdclaxwmfmdeiamecsdsemgtvsjzcncygrkowtrontzschgezokstswkkscfmklrtauteyaahdcxiehfonurdppfyntapejpproypegrdawkgmaewejlsfdtsrfybdehcaflmtrlbdhpamtaaddyoyadlncsdwykaeykaeykaycynlytsnyltaadmhtaadmwtaaddloxaxhdclaostvelfemdyynwydwyaievosrgmambklovabdgypdglldvespsthysadamhpmjeinaahdcxntdllnaaeykoytdacygegwhgjsiyonpywmcmrpwphsvodsrerozsbyaxluzcoxdpamtaaddyoyadlncsehykaeykaeykaycypdbskeuytaadmwtaaddloxaxhdclaxzcfxeegdrpmogrgwkbzctlttweadkiengrwlhtprremouoluutqdpfbncedkynfhaahdcxjpwevdeogthttkmeswzcolcpsaahcfnshkhtehytclmnteatmoteadtlwynnftloamtaaddyoyadlncsghykaeykaeykaycybthlvytstaadmhtaaddloxaxhdclaxhhsnhdrpftdwuocntilydibehnecmovdfekpjkclcslasbhkpawsaddmcmmnahnyaahdcxlotedtndfymyltclhlmtpfsadscnhtztaolbnnkistaedegwfmmedreetnwmcycnamtaaddyoyadlfcsdpykaycyemrewytytaadmhtaadmetaaddloxaxhdclaxdwkswmztpytnswtsecnblfbayajkdldeclqzzolrsnhljedsgminetytbnahatbyaahdcxkkguwsvyimjkvwteytwztyswvendtpmncpasfrrylprnhtkblndrgrmkoyjtbkrpamtaaddyoyadlocsdyykaeykaeykadykaycyhkrpnddrtaadmetaaddloxaxhdclaohnhffmvsbndslrfgclpfjejyatbdpebacnzokotofxntaoemvskpaowmryfnotfgaahdcxdlnbvecentssfsssgylnhkrstoytecrdlyadrekirfaybglahltalsrfcaeerobwamtaaddyoyadlocsdyykaeykaeykaoykaycyhkrpnddrgdaogykb", encoded);
}
@Test
public void testPairPathComponent() throws Exception {
String ur = "ur:crypto-account/oeadcylpvefyjeaolttaadeetaadmutaaddlonaxhdclaxhkfzdphtkplevtqzprkgnnsacagesgctzmspytctdstocsbgmkamurrdpffmtsseaahdcxpdnbdysgfeaycfsegwmhwfjewzfwdmesrlqdhglagahkytcflbrtsedraefwbweyamtaaddyoeadlncsdwykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycypsjpsbfntaadeetaadmhtaadmwtaaddlonaxhdclaouypakomsrlhlqzvwlymesadevybkueqdoxhdcximrdhgfhtybwvyhdkeyklddpmwaahdcxwnvsaxgsdldtmurknyfpcedyiaiopautyafrpejoaxjncfhhhtpfdetteotnknsramtaaddyoeadlncsehykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyoeyagmmstaadeetaadmwtaaddlonaxhdclaorernoyonguhlfsdecnmohnuydwnnchjpuyroftdnaadkatcfkirdtkispsiajycxaahdcxlyutkbnymklbdskesbihbelysedwlupklfmhnntlfwsegsvwwspkghkgvwheiejoamtaaddyoeadlncsghykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycywyqzwzkptaadeetaadmhtaadnytaaddlonaxhdclaoiorpcxecynfloxtamhlsaarkkotpclcydahtamluhnimkertlgftknoerymobaneaahdcxrsatfdqdenvtspvdieindaztrpcazsknzsoywkghfhtyswdkkpjstbneayfncxoxamtaaddyoeadlfcsdpykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycylpvefyjetaadeetaadmhtaadmetaadnytaaddlonaxhdclaoldpfglrkwntlvwlarsdmnbzefmghkeeeoteerdpfframuoidstpacswmnnutcwkkaahdcxhkrozmjneceoldplnluetscyjnbdhsldenontncafgdkmdvlsnidamlnvwfzsstaamtaaddyoeadlocsdyykaeykaeykadykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyrhimryuotaadeetaadmetaadnytaaddlonaxhdclaowysfahwsqdclsnfmwmjlgoeerhzoteherosnmyhkinolrlspdadsasswlarpvtpfaahdcxwybnryoewshycwghlfhhwtbscxpyylenpkolmoftgymksbdpvsgtlbdefhrswnyaamtaaddyoeadlocsdyykaeykaeykaoykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyrhimryuotaadeetaadnltaaddlonaxhdclaxpmctzmjzemjspsplgrlgtyvssffrzcrlgogojnmncwbdtytelblyhlflmtdnkifdaahdcxgytkgujnjtaszejprdpmoskseehkamvlcersoxdlghsglagmjkjewmrlpfonwldwamtaaddyoeadlncshfykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycynsmwdtfzryrorncp";
URDecoder urDecoder = new URDecoder();
urDecoder.receivePart(ur);
URDecoder.Result urResult = urDecoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
if(urResult.ur.getRegistryType().equals(RegistryType.CRYPTO_ACCOUNT)) {
CryptoAccount cryptoAccount = (CryptoAccount)urResult.ur.decodeFromRegistry();
Assert.assertEquals("<0;1>/*", cryptoAccount.getOutputDescriptors().get(0).getHdKey().getChildren().getPath());
}
}
}
}