make test pass

This commit is contained in:
Kolezhniuk
2022-10-29 19:29:23 +02:00
parent c0f9f8d3ab
commit 4599ae4066
22 changed files with 3639 additions and 53 deletions

View File

@@ -2,5 +2,5 @@
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 120
"printWidth": 100
}

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}

23
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"cSpell.words": ["Iden"],
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#ab307e",
"activityBar.background": "#ab307e",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#25320e",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#ab307e",
"statusBar.background": "#832561",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#ab307e",
"statusBarItem.remoteBackground": "#832561",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#832561",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#83256199",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#832561"
}

46
index.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible"
content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<script type="module"
src="./dist/esm/index.mjs"></script>
<title>Document</title>
</head>
<body>
hello
</body>
<script>
// const claim = newClaim(new SchemaHash(), withFlagUpdatable(true));
// const { index, value } = claim.rawSlots();
// const indexHash = poseidonHash([
// index[0].toBigInt(),
// index[1].toBigInt(),
// index[2].toBigInt(),
// index[3].toBigInt()
// ]).then((hash) => {
// console.log(hash);
// console.assert('19905260441950906049955646784794273651462264973332746773406911374272567544299' == hash.toString());
// });
// const valueHash = await poseidonHash([
// value[0].toBigInt(),
// value[1].toBigInt(),
// value[2].toBigInt(),
// value[3].toBigInt()
// ]);
// expect().toEqual(
// indexHash.toString()
// );
// expect('2351654555892372227640888372176282444150254868378439619268573230312091195718').toEqual(
// valueHash.toString()
// );
</script>
</html>

1795
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "@js-iden3-core",
"name": "@iden3/js-iden3-core",
"version": "0.0.0-development",
"description": "Low level API to create and manipulate iden3 Claims.",
"exports": {
@@ -20,11 +20,12 @@
"dist/**/*"
],
"scripts": {
"clean": "rm -rf ./dist",
"clean": "rimraf ./dist",
"build": "npm run clean && npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p ./configs/tsconfig.esm.json && mv dist/esm/index.js dist/esm/index.mjs",
"build:cjs": "tsc -p ./configs/tsconfig.cjs.json",
"test": "jest --coverage",
"test:coverage": "jest --coverage",
"test": "jest",
"test:watch": "jest --watch",
"semantic-release": "semantic-release",
"lint": "eslint --fix --ext .js,.ts src/**",
@@ -40,27 +41,18 @@
},
"repository": {
"type": "git",
"url": "https://github.com/snyk-labs/modern-npm-package.git"
"url": "https://github.com/iden3/js-iden3-core.git"
},
"keywords": [
"npm",
"javascript",
"typescript",
"esm",
"cjs",
"nodejs",
"commonjs",
"ecmascript",
"beginner",
"example",
"demonstration"
"iden3",
"core"
],
"author": "Snyk Labs",
"author": "D.Kolezhniuk",
"license": "MIT",
"bugs": {
"url": "https://github.com/snyk-labs/modern-npm-package/issues"
"url": "https://github.com/iden3/js-iden3-core/issues"
},
"homepage": "https://github.com/snyk-labs/modern-npm-package#readme",
"homepage": "https://github.com/iden3/js-iden3-core#readme",
"devDependencies": {
"@types/jest": "^29.2.0",
"@typescript-eslint/eslint-plugin": "^5.41.0",
@@ -68,9 +60,17 @@
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.2.2",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"rollup": "^3.2.3",
"semantic-release": "^19.0.3",
"terser": "^5.15.1",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
},
"dependencies": {
"base58-js": "^1.0.4",
"circomlibjs": "^0.1.7",
"cross-sha256": "^1.2.0"
}
}

522
src/claim.ts Normal file
View File

@@ -0,0 +1,522 @@
import { SchemaHash } from './schemaHash';
import { ElemBytes } from './elemBytes';
import { Constants } from './constants';
import { Id } from './id';
import {
checkBigIntArrayInField,
checkBigIntInField,
getDateFromUnixTimestamp,
getUint32,
getUint64,
getUnixTimestamp,
poseidonHash,
putUint32,
putUint64 as getBytesFromUint64
} from './util';
/*
Claim structure
Index:
i_0: [ 128 bits ] claim schema
[ 32 bits ] option flags
[3] Subject:
000: A.1 Self
001: invalid
010: A.2.i OtherIden Index
011: A.2.v OtherIden Value
100: B.i Object Index
101: B.v Object Value
[1] Expiration: bool
[1] Updatable: bool
[27] 0
[ 32 bits ] version (optional?)
[ 61 bits ] 0 - reserved for future use
i_1: [ 248 bits] identity (case b) (optional)
[ 5 bits ] 0
i_2: [ 253 bits] 0
i_3: [ 253 bits] 0
Value:
v_0: [ 64 bits ] revocation nonce
[ 64 bits ] expiration date (optional)
[ 125 bits] 0 - reserved
v_1: [ 248 bits] identity (case c) (optional)
[ 5 bits ] 0
v_2: [ 253 bits] 0
v_3: [ 253 bits] 0
*/
// Option provides the ability to set different Claim's fields on construction
export type Option = (c: Claim) => void;
// WithFlagUpdatable sets claim's flag `updatable`
export function withFlagUpdatable(val: boolean): Option {
return (c: Claim) => c.setFlagUpdatable(val);
}
// WithVersion sets claim's version
export function withVersion(ver: number): Option {
return (c: Claim) => c.setVersion(ver);
}
// WithIndexId sets Id to claim's index
export function withIndexId(id: Id): Option {
return (c: Claim) => c.setIndexId(id);
}
// WithValueId sets Id to claim's value
export function withValueId(id: Id): Option {
return (c: Claim) => c.setValueId(id);
}
// WithId sets Id to claim's index or value depending on `pos`.
export function withId(id: Id, pos: IdPosition): Option {
return (c: Claim) => {
switch (pos) {
case IdPosition.Index:
c.setIndexId(id);
break;
case IdPosition.Value:
c.setValueId(id);
break;
default:
throw new Error(Constants.ERRORS.INCORRECT_ID_POSITION);
}
};
}
// WithRevocationNonce sets claim's revocation nonce.
export function withRevocationNonce(nonce: number): Option {
return (c: Claim) => c.setRevocationNonce(nonce);
}
// WithExpirationDate sets claim's expiration date to `dt`.
export function withExpirationDate(dt: Date): Option {
return (c: Claim) => c.setExpirationDate(dt);
}
// WithIndexData sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withIndexData(slotA: ElemBytes, slotB: ElemBytes): Option {
return (c: Claim) => c.setIndexData(slotA, slotB);
}
// WithIndexDataBytes sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withIndexDataBytes(slotA: Uint8Array | null, slotB: Uint8Array | null): Option {
return (c: Claim) => c.setIndexDataBytes(slotA, slotB);
}
// WithIndexDataInts sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withIndexDataInts(slotA: bigint | null, slotB: bigint | null): Option {
return (c: Claim) => c.setIndexDataInts(slotA, slotB);
}
// WithValueData sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withValueData(slotA: ElemBytes, slotB: ElemBytes): Option {
return (c: Claim) => c.setValueData(slotA, slotB);
}
// WithValueDataBytes sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withValueDataBytes(slotA: Uint8Array, slotB: Uint8Array): Option {
return (c: Claim) => c.setValueDataBytes(slotA, slotB);
}
// WithValueDataInts sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
export function withValueDataInts(slotA: bigint | null, slotB: bigint | null): Option {
return (c: Claim) => c.setValueDataInts(slotA, slotB);
}
// NewClaim creates new Claim with specified SchemaHash and any number of
// options. Using options you can specify any field in claim.
export function newClaim(sh: SchemaHash, ...args: Option[]): Claim {
const c = new Claim();
c.setSchemaHash(sh);
for (let i = 0; i < args.length; i++) {
const fn = args[i];
fn(c);
}
return c;
}
export enum SlotName {
IndexA = 'IndexA',
IndexB = 'IndexB',
ValueA = 'ValueA',
ValueB = 'ValueB'
}
// ErrSlotOverflow means some ElemBytes overflows Q Field. And wraps the name
// of overflowed slot.
export class ErrSlotOverflow extends Error {
constructor(msg: string) {
super(`Slot ${msg} not in field (too large)`);
Object.setPrototypeOf(this, ErrSlotOverflow.prototype);
}
}
// subjectFlag for the time being describes the location of Id (in index or value
// slots or nowhere at all).
//
// Values subjectFlagInvalid presents for backward compatibility and for now means nothing.
export enum SubjectFlag {
Self = 0b0,
Invalid = 0b1,
OtherIdenIndex = 0b10,
OtherIdenValue = 0b11
}
export enum IdPosition {
None = 0,
Index = 1,
Value = 2
}
export enum Flags {
ByteIdx = 16,
ExpirationBitIdx = 3,
UpdatableBitIdx = 4
}
export class Claim {
private _index: ElemBytes[] = [];
private _value: ElemBytes[] = [];
constructor() {
for (let i = 0; i < Constants.ELEM_BYTES_LENGTH; i++) {
this._index[i] = new ElemBytes();
this._value[i] = new ElemBytes();
}
}
// GetSchemaHash return copy of claim's schema hash.
getSchemaHash(): SchemaHash {
return new SchemaHash(this._index[0].bytes.slice(0, Constants.SCHEMA.HASH_LENGTH));
}
get value(): ElemBytes[] {
return this._value;
}
set value(value: ElemBytes[]) {
this._value = value;
}
get index(): ElemBytes[] {
return this._index;
}
set index(value: ElemBytes[]) {
this._index = value;
}
// SetSchemaHash updates claim's schema hash.
setSchemaHash(sh: SchemaHash) {
this._index[0] = new ElemBytes(
Uint8Array.from([...sh.bytes, ...new Array(Constants.SCHEMA.HASH_LENGTH).fill(0)])
);
}
setSubject(s: SubjectFlag) {
// clean first 3 bits
this._index[0].bytes[Flags.ByteIdx] &= 0b11111000;
this._index[0].bytes[Flags.ByteIdx] |= s;
}
private getSubject(): SubjectFlag {
let sbj = this._index[0].bytes[Flags.ByteIdx];
// clean all except first 3 bits
sbj &= 0b00000111;
return sbj as SubjectFlag;
}
private setFlagExpiration(val: boolean) {
if (val) {
this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.ExpirationBitIdx;
} else {
this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.ExpirationBitIdx);
}
}
private getFlagExpiration(): boolean {
const mask = 0b1 << Flags.ExpirationBitIdx;
return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0;
}
// GetIDPosition returns the position at which the Id is stored.
getIdPosition(): IdPosition {
switch (this.getSubject()) {
case SubjectFlag.Self:
return IdPosition.None;
case SubjectFlag.OtherIdenIndex:
return IdPosition.Index;
case SubjectFlag.OtherIdenValue:
return IdPosition.Value;
default:
throw new Error(Constants.ERRORS.INVALID_SUBJECT_POSITION);
}
}
// SetValueDataInts sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setValueDataInts(slotA: bigint | null, slotB: bigint | null): void {
this.setSlotInt(this._value[2], slotA, SlotName.ValueA);
this.setSlotInt(this._value[3], slotB, SlotName.ValueB);
}
// SetValueDataBytes sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setValueDataBytes(slotA: Uint8Array, slotB: Uint8Array): void {
this.setSlotBytes(this._value[2], slotA, SlotName.ValueA);
this.setSlotBytes(this._value[3], slotB, SlotName.ValueB);
}
// SetValueData sets data to value slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setValueData(slotA: ElemBytes, slotB: ElemBytes): void {
const slotsAsInts: bigint[] = [slotA.toBigInt(), slotB.toBigInt()];
if (!checkBigIntArrayInField(slotsAsInts)) {
throw new Error(Constants.ERRORS.DATA_OVERFLOW);
}
this._value[2] = slotA;
this._value[3] = slotB;
}
// SetIndexDataInts sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setIndexDataInts(slotA: bigint | null, slotB: bigint | null): void {
this.setSlotInt(this._index[2], slotA, SlotName.IndexA);
this.setSlotInt(this._index[3], slotB, SlotName.IndexB);
}
// SetIndexDataBytes sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setIndexDataBytes(slotA: Uint8Array | null, slotB: Uint8Array | null): void {
this.setSlotBytes(this._index[2], slotA, SlotName.IndexA);
this.setSlotBytes(this._index[3], slotB, SlotName.IndexB);
}
private setSlotBytes(slot: ElemBytes, value: Uint8Array | null, slotName: SlotName) {
slot = new ElemBytes(value);
if (!checkBigIntInField(slot.toBigInt())) {
throw new ErrSlotOverflow(slotName);
}
}
private setSlotInt(slot: ElemBytes, value: bigint | null, slotName: SlotName) {
if (!value) {
value = BigInt(0);
}
if (!checkBigIntInField(value)) {
throw new ErrSlotOverflow(slotName);
}
slot.setBigInt(value);
}
// SetIndexData sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
setIndexData(slotA: ElemBytes, slotB: ElemBytes) {
const slotsAsInts: bigint[] = [slotA.toBigInt(), slotB.toBigInt()];
if (!checkBigIntArrayInField(slotsAsInts)) {
throw new Error(Constants.ERRORS.DATA_OVERFLOW);
}
this._index[2] = slotA;
this._index[3] = slotB;
}
resetExpirationDate(): void {
this.setFlagExpiration(false);
const bytes = Array.from({ length: Constants.NONCE_BYTES_LENGTH }, () => 0);
const arr = Array.from(this._value[0].bytes);
arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes);
this._value[0] = new ElemBytes(Uint8Array.from(arr));
}
// GetExpirationDate returns expiration date and flag. Flag is true if
// expiration date is present, false if null.
getExpirationDate(): Date | null {
if (this.getFlagExpiration()) {
const unixTimestamp = getUint64(this._value[0].bytes.slice(8, 16));
return getDateFromUnixTimestamp(unixTimestamp);
}
return null;
}
// SetExpirationDate sets expiration date to dt
setExpirationDate(dt: Date) {
this.setFlagExpiration(true);
const bytes = getBytesFromUint64(getUnixTimestamp(dt));
const arr = Array.from(this._value[0].bytes);
arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes);
this._value[0] = new ElemBytes(Uint8Array.from(arr));
}
// GetRevocationNonce returns revocation nonce
getRevocationNonce(): number {
return getUint64(this._value[0].bytes.slice(0, 8));
}
// SetRevocationNonce sets claim's revocation nonce
setRevocationNonce(nonce: number): void {
const bytes = getBytesFromUint64(nonce);
if (bytes.length > Constants.NONCE_BYTES_LENGTH) {
throw new Error('Nonce length is not valid');
}
const arr = Array.from(this._value[0].bytes);
arr.splice(0, Constants.NONCE_BYTES_LENGTH, ...bytes);
this._value[0] = new ElemBytes(Uint8Array.from(arr));
}
getValueId(): Id {
return Id.fromBytes(this._value[1].bytes);
}
// SetValueId sets id to value. Removes id from index if any.
setValueId(id: Id): void {
this.resetIndexId();
this.setSubject(SubjectFlag.OtherIdenValue);
const arr = Array.from(this._index[1].bytes);
arr.splice(0, id.bytes.length, ...id.bytes);
this._value[1] = new ElemBytes(Uint8Array.from(arr));
}
private resetIndexId() {
this._index[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
}
private resetValueId(): void {
this._value[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
}
getIndexId(): Id {
return Id.fromBytes(this._index[1].bytes);
}
// SetIndexId sets id to index. Removes id from value if any.
setIndexId(id: Id): void {
this.resetValueId();
this.setSubject(SubjectFlag.OtherIdenIndex);
const arr = Array.from(this._index[1].bytes);
arr.splice(0, id.bytes.length, ...id.bytes);
this._index[1] = new ElemBytes(Uint8Array.from(arr));
}
// SetVersion sets claim's version
setVersion(ver: number) {
const bytes = putUint32(ver);
this._index[0].bytes[20] = bytes[0];
this._index[0].bytes[21] = bytes[1];
this._index[0].bytes[22] = bytes[2];
this._index[0].bytes[23] = bytes[3];
}
// GetVersion returns claim's version
getVersion(): number {
return getUint32(this._index[0].bytes.slice(20, 24));
}
// SetFlagUpdatable sets claim's flag `updatable`
setFlagUpdatable(val: boolean) {
if (val) {
this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.UpdatableBitIdx;
} else {
this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.UpdatableBitIdx);
}
}
// HIndex calculates the hash of the Index of the Claim
async hIndex(): Promise<bigint> {
return await poseidonHash(ElemBytes.elemBytesToInts(this._index));
}
// GetFlagUpdatable returns claim's flag `updatable`
getFlagUpdatable(): boolean {
const mask = 0b1 << Flags.UpdatableBitIdx;
return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0;
}
// HValue calculates the hash of the Value of the Claim
async hValue(): Promise<bigint> {
return await poseidonHash(ElemBytes.elemBytesToInts(this._value));
}
// HiHv returns the HIndex and HValue of the Claim
async hiHv(): Promise<{ hi: bigint; hv: bigint }> {
return { hi: await this.hIndex(), hv: await this.hValue() };
}
// resetId deletes Id from index and from value.
resetId(): void {
this.resetIndexId();
this.resetValueId();
this.setSubject(SubjectFlag.Self);
}
// GetId returns Id from claim's index of value.
// Returns error ErrNoId if Id is not set.
getId(): Id {
switch (this.getSubject()) {
case SubjectFlag.OtherIdenIndex:
return this.getIndexId();
case SubjectFlag.OtherIdenValue:
return this.getValueId();
default:
throw new Error(Constants.ERRORS.NO_ID);
}
}
// RawSlots returns raw bytes of claim's index and value
rawSlots(): { index: ElemBytes[]; value: ElemBytes[] } {
return {
index: this._index,
value: this._value
};
}
// RawSlotsAsInts returns slots as []bigint
rawSlotsAsInts(): bigint[] {
return [...ElemBytes.elemBytesToInts(this._index), ...ElemBytes.elemBytesToInts(this._value)];
}
clone(): Claim {
return JSON.parse(JSON.stringify(this));
}
marshalJson(): string {
return JSON.stringify(this.rawSlotsAsInts().map((b) => b.toString()));
}
unMarshalJson(b: string): Claim {
const ints: bigint[] = JSON.parse(b).map((s: string) => BigInt(s));
if (ints.length !== this._index.length + this._value.length) {
throw new Error("invalid number of claim's slots");
}
this._index = [];
this._value = [];
for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < ints.length / 2; i++, j++) {
this._index[i] = new ElemBytes();
this._index[i].setBigInt(ints[i]);
this._value[i] = new ElemBytes();
this._value[i].setBigInt(ints[j]);
}
return this;
}
marshalBinary(): Uint8Array {
const getBytes = (src: ElemBytes[]) =>
src.reduce((acc: number[], cur: ElemBytes) => {
return [...acc, ...cur.bytes];
}, []);
return Uint8Array.from(getBytes(this._index).concat(getBytes(this._value)));
}
unMarshalBinary(data: Uint8Array): void {
const wantLen = 2 * Constants.ELEM_BYTES_LENGTH * Constants.BYTES_LENGTH;
if (data.length !== wantLen) {
throw new Error('unexpected length of input data');
}
this._index = [];
this._value = [];
for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < Constants.ELEM_BYTES_LENGTH; i++, j++) {
this._index[i] = new ElemBytes(
data.slice(i * Constants.BYTES_LENGTH, (i + 1) * Constants.BYTES_LENGTH)
);
this._value[i] = new ElemBytes(
data.slice(j * Constants.BYTES_LENGTH, (j + 1) * Constants.BYTES_LENGTH)
);
}
}
}

31
src/constants.ts Normal file
View File

@@ -0,0 +1,31 @@
export const Constants = Object.freeze({
ERRORS: {
// ErrDataOverflow means that given *big.Int value does not fit in Field Q
// e.g. greater than Q constant:
// Q constant: 21888242871839275222246405745257275088548364400416034343698204186575808495617
DATA_OVERFLOW: 'data does not fits SNARK size',
// ErrIncorrectIDPosition means that passed position is not one of predefined:
// IDPositionIndex or IDPositionValue
INCORRECT_ID_POSITION: 'incorrect ID position',
// throws when ID not found in the Claim.
NO_ID: 'ID is not set',
// throws when subject position flags sets in invalid value.
INVALID_SUBJECT_POSITION: 'invalid subject position'
},
SCHEMA: {
HASH_LENGTH: 16
},
BYTES_LENGTH: 32,
ELEM_BYTES_LENGTH: 4,
NONCE_BYTES_LENGTH: 8,
Q: BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'),
ID: {
TYPE_DEFAULT: Uint8Array.from([0x00, 0x00]),
TYPE_READONLY: Uint8Array.from([0b00000000, 0b00000001]),
ID_LENGTH: 31
},
DID: {
DID_SCHEMA: 'did',
DID_METHOD_IDEN3: 'iden3'
}
});

230
src/did.ts Normal file
View File

@@ -0,0 +1,230 @@
import { Id } from './id';
import { Constants } from './constants';
export enum Blockchain {
Ethereum = 'eth',
Polygon = 'polygon',
Unknown = 'unknown',
NoChain = ''
}
export enum NetworkId {
Main = 'main',
Mumbai = 'mumbai',
Goerli = 'goerli',
Unknown = 'unknown',
NoNetwork = ''
}
export enum DidMethod {
Iden3 = 'iden3'
}
export const DIDMethodByte: { [key: string]: number } = {
[DidMethod.Iden3]: 0b00000001
};
// DIDNetworkFlag is a structure to represent DID blockchain and network id
export class DIDNetworkFlag {
constructor(public readonly blockchain: Blockchain, public readonly networkId: NetworkId) {}
toString(): string {
return `${this.blockchain || '_'}:${this.networkId || '_'}`;
}
static fromString(s: string): DIDNetworkFlag {
const [blockchain, networkId] = s.split(':');
return new DIDNetworkFlag(
blockchain.replace('_', '') as Blockchain,
networkId.replace('_', '') as NetworkId
);
}
}
// DIDMethodNetwork is map for did methods and their blockchain networks
export const DIDMethodNetwork: {
[k: string]: { [k: string]: number };
} = {
[DidMethod.Iden3]: {
'_:_': 0b00000000,
'polygon:main': 0b00010000 | 0b00000001,
'polygon:mumbai': 0b00010000 | 0b00000010,
'ethereum:main': 0b00100000 | 0b00000001,
'ethereum:goerli': 0b00100000 | 0b00000010
}
};
// BuildDIDType builds bytes type from chain and network
export function buildDIDType(
method: string,
blockchain: Blockchain,
network: NetworkId
): Uint8Array {
const fb = DIDMethodByte[method];
if (!fb) {
throw new Error(`method ${method} is not defined in core lib`);
}
const methodFn = DIDMethodNetwork[method];
if (!methodFn) {
throw new Error(`method ${method} is not defined in core lib`);
}
const sb: number | undefined = methodFn[new DIDNetworkFlag(blockchain, network).toString()];
if (!sb) {
throw new Error(`blockchain ${blockchain} and network ${network} is not defined in core lib`);
}
return Uint8Array.from([fb, sb]);
}
// FindNetworkIDForDIDMethodByValue finds network by byte value
export function findNetworkIDForDIDMethodByValue(method: string, byteNumber: number): NetworkId {
const methodMap = DIDMethodNetwork[method];
if (!methodMap) {
throw new Error(`did method ${method} is not defined in core lib`);
}
for (const [key, value] of Object.entries(methodMap)) {
if (value === byteNumber) {
return DIDNetworkFlag.fromString(key).networkId;
}
}
return NetworkId.Unknown;
}
// findBlockchainForDIDMethodByValue finds blockchain type by byte value
export function findBlockchainForDIDMethodByValue(
method: DidMethod,
byteNumber: number
): Blockchain {
const methodMap = DIDMethodNetwork[method];
if (!methodMap) {
throw new Error(`did method ${method} is not defined in core lib`);
}
for (const [key, value] of Object.entries(methodMap)) {
if (value === byteNumber) {
return DIDNetworkFlag.fromString(key).blockchain;
}
}
return Blockchain.Unknown;
}
// findDIDMethodByValue finds did method by its byte value
export function findDIDMethodByValue(byteNumber: number): DidMethod {
for (const [key, value] of Object.entries(DIDMethodByte)) {
if (value === byteNumber) {
return key as DidMethod;
}
}
throw new Error(`bytes ${byteNumber} are not defined in core lib as valid did method`);
}
export type DIDOptions = (did: DID) => void;
// WithNetwork sets Blockchain and NetworkID (eth:main)
export function withNetwork(blockchain: Blockchain, network: NetworkId): DIDOptions {
return (did: DID) => {
did.networkId = network;
did.blockchain = blockchain;
};
}
// DID Decentralized Identifiers (DIDs)
// https://w3c.github.io/did-core/#did-syntax
export class DID {
public method: DidMethod = DidMethod.Iden3;
public id: Id = new Id(new Uint8Array(2), new Uint8Array(27));
public blockchain: Blockchain = Blockchain.Unknown;
public networkId: NetworkId = NetworkId.Unknown;
// toString did as a string
toString(): string {
if (this.blockchain == '') {
return [Constants.DID.DID_SCHEMA, DidMethod.Iden3, this.id.string()].join(':');
}
return [
Constants.DID.DID_SCHEMA,
DidMethod.Iden3,
this.blockchain,
this.networkId,
this.id.string()
]
.filter((i) => !!i)
.join(':');
}
// ParseDIDFromID returns did from ID
static parseFromId(id: Id): DID {
const did = new DID();
did.id = id;
const typ = id.type();
did.method = findDIDMethodByValue(typ[0]);
did.blockchain = findBlockchainForDIDMethodByValue(did.method, typ[1]);
did.networkId = findNetworkIDForDIDMethodByValue(did.method, typ[1]);
return did;
}
// ParseDID method parse string and extract DID if string is valid Iden3 identifier
static parse(s: string): DID {
const args = s.split(':');
const did = new DID();
did.method = args[1] as DidMethod;
switch (args.length) {
case 5:
// validate id
did.id = Id.fromString(args[4]);
did.blockchain = args[2] as Blockchain;
did.networkId = args[3] as NetworkId;
break;
case 3:
// validate readonly id
did.id = Id.fromString(args[2]);
did.blockchain = Blockchain.NoChain;
did.networkId = NetworkId.NoNetwork;
break;
}
// check did method defined in core lib
const methodByte = DIDMethodByte[did.method];
if (!methodByte) {
throw new Error(`DIDMethodByte: did method ${did.method} is not defined in core lib`);
}
// check did network defined in core lib for did method
const method = DIDMethodNetwork[did.method];
if (!method) {
throw new Error(`DIDMethodNetwork: did method ${did.method} is not defined in core lib`);
}
const byte: number = method[new DIDNetworkFlag(did.blockchain, did.networkId).toString()];
if (!byte?.toString()) {
throw new Error(
`blockchain network "${did.blockchain} ${did.networkId}" is not defined for ${did.method} did method`
);
}
// check id contains did network and method
const d = DID.parseFromId(did.id);
if (d.method !== did.method) {
throw new Error(
`did method of core identity ${did.method} differs from given did method ${did.method}`
);
}
if (d.networkId !== did.networkId) {
throw new Error(
`network method of core identity ${d.networkId} differs from given did network specific id ${did.networkId}`
);
}
if (d.blockchain !== did.blockchain) {
throw new Error(
`blockchain network of core identity ${d.blockchain} differs from given did blockchain network ${did.blockchain}`
);
}
return did;
}
static newDID(didStr: string, ...args: DIDOptions[]): DID {
const did = new DID();
did.id = Id.fromString(didStr);
args.filter((opt) => !!opt).forEach((arg) => arg(did));
return did;
}
}

126
src/elemBytes.ts Normal file
View File

@@ -0,0 +1,126 @@
import { Constants } from './constants';
import { sha256 } from 'cross-sha256';
import { checkBigIntInField, fromLittleEndian, toLittleEndian } from './util';
import { Hex } from './hex';
export class BytesHelper {
static intToBytes(int: bigint): Uint8Array {
return BytesHelper.intToNBytes(int, Constants.BYTES_LENGTH);
}
static intToNBytes(int: bigint, n: number): Uint8Array {
return Uint8Array.from(toLittleEndian(int, n));
}
static checkChecksum(bytes: Uint8Array): boolean {
const { typ, genesis, checksum } = BytesHelper.decomposeBytes(bytes);
if (!checksum.length || JSON.stringify(Uint8Array.from([0, 0])) === JSON.stringify(checksum)) {
return false;
}
const c = BytesHelper.calculateChecksum(typ, genesis);
return JSON.stringify(c) === JSON.stringify(checksum);
}
static decomposeBytes(b: Uint8Array): {
typ: Uint8Array;
genesis: Uint8Array;
checksum: Uint8Array;
} {
const offset = 2;
const len = b.length - offset;
return {
typ: b.slice(0, offset),
genesis: b.slice(offset, len),
checksum: b.slice(-offset)
};
}
static calculateChecksum(typ: Uint8Array, genesis: Uint8Array): Uint8Array {
const toChecksum = [...typ, ...genesis];
const s: number = toChecksum.reduce((acc, cur) => acc + cur, 0);
const checksum = [s >> 8, s & 0xff];
return Uint8Array.from(checksum.reverse());
}
static hashBytes(str: string): Uint8Array {
const hash = new sha256().update(str).digest();
return new Uint8Array(hash);
}
static hexToBytes(str: string): Uint8Array {
const buffer = Buffer.from(str, 'hex');
return Uint8Array.from(buffer);
}
static bytesToHex(bytes: Uint8Array) {
const hex: string[] = [];
for (let i = 0; i < bytes.length; i++) {
const current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xf).toString(16));
}
return hex.join('');
}
static bytesToInt(bytes: Uint8Array): bigint {
return fromLittleEndian(bytes);
}
}
export class ElemBytes {
private _bytes = new Uint8Array(Constants.BYTES_LENGTH);
constructor(bytes?: Uint8Array | null) {
if (bytes) {
this._bytes = bytes;
}
if (this._bytes.length !== Constants.BYTES_LENGTH) {
throw new Error('Invalid bytes length');
}
}
get bytes(): Uint8Array {
return this._bytes;
}
set bytes(value: Uint8Array) {
this._bytes = value;
}
toBigInt(): bigint {
return BytesHelper.bytesToInt(this._bytes);
}
setBigInt(n: bigint): ElemBytes {
if (!checkBigIntInField(n)) {
throw new Error(Constants.ERRORS.DATA_OVERFLOW);
}
this._bytes = BytesHelper.intToBytes(n);
return this;
}
slotFromHex(hex: string): ElemBytes {
const bytes = Hex.decodeString(hex);
if (bytes.length !== Constants.BYTES_LENGTH) {
throw new Error('Invalid bytes length');
}
this._bytes.set(bytes, 0);
return this;
}
hex(): string {
return Hex.encodeString(this._bytes);
}
// ElemBytesToInts converts slice of ElemBytes to slice of *big.Int
static elemBytesToInts(elements: ElemBytes[]): bigint[] {
const result: bigint[] = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
result.push(element.toBigInt());
}
return result;
}
}

55
src/hex.ts Normal file
View File

@@ -0,0 +1,55 @@
export class Hex {
static readonly HEX_TABLE = '0123456789abcdef';
static textEncoder = new TextEncoder();
static encodeLength(n: number): number {
return n * 2;
}
static encode(src: Uint8Array): Uint8Array {
const dst = new Uint8Array(Hex.encodeLength(src.length));
let j = 0;
for (let i = 0; i < src.length; i++) {
dst[j] = Hex.HEX_TABLE[src[i] >> 4].charCodeAt(0);
dst[j + 1] = Hex.HEX_TABLE[src[i] & 0x0f].charCodeAt(0);
j += 2;
}
return dst;
}
static decodeString(s: string): Uint8Array {
return Hex.decode(s);
}
static fromHexChar(c: number): number {
if ('0'.charCodeAt(0) <= c && c <= '9'.charCodeAt(0)) {
return c - '0'.charCodeAt(0);
} else if ('a'.charCodeAt(0) <= c && c <= 'f'.charCodeAt(0)) {
return c - 'a'.charCodeAt(0) + 10;
}
if ('A'.charCodeAt(0) <= c && c <= 'F'.charCodeAt(0)) {
return c - 'A'.charCodeAt(0) + 10;
}
throw new Error(`Invalid byte char ${c}`);
}
private static decode(src: string): Uint8Array {
let i = 0;
let j = 1;
const dst: number[] = [];
for (; j < src.length; j += 2) {
const a = Hex.fromHexChar(src[j - 1].charCodeAt(0));
const b = Hex.fromHexChar(src[j].charCodeAt(0));
dst[i] = (a << 4) | b;
i++;
}
if (src.length % 2 == 1) {
throw new Error('Invalid hex string');
}
return Uint8Array.from(dst);
}
static encodeString(b: Uint8Array): string {
return new TextDecoder().decode(Hex.encode(b));
}
}

97
src/id.ts Normal file
View File

@@ -0,0 +1,97 @@
import { Constants } from './constants';
const base58Js = require('base58-js');
import { fromLittleEndian, poseidonHash } from './util';
import { BytesHelper } from './elemBytes';
// ID is a byte array with
// [ type | root_genesis | checksum ]
// [2 bytes | 27 bytes | 2 bytes ]
// where the root_genesis are the first 28 bytes from the hash root_genesis
export class Id {
private _bytes: Uint8Array;
private readonly _checksum: Uint8Array;
constructor(typ: Uint8Array, genesis: Uint8Array) {
this._checksum = BytesHelper.calculateChecksum(typ, genesis);
this._bytes = Uint8Array.from([...typ, ...genesis, ...this._checksum]);
}
private static getFromBytes(bytes: Uint8Array): Id {
const { typ, genesis }: { typ: Uint8Array; genesis: Uint8Array } =
BytesHelper.decomposeBytes(bytes);
return new Id(typ, genesis);
}
checksum(): Uint8Array {
return this._checksum;
}
string(): string {
return base58Js.binary_to_base58(this._bytes);
}
get bytes(): Uint8Array {
return this._bytes;
}
set bytes(b: Uint8Array) {
this._bytes = b;
}
type(): Uint8Array {
return this._bytes.slice(0, 2);
}
bigInt(): bigint {
return fromLittleEndian(this._bytes);
}
equal(id: Id): boolean {
return JSON.stringify(this._bytes) === JSON.stringify(id.bytes);
}
marshal(): Uint8Array {
return new TextEncoder().encode(this.string());
}
static unMarshal(b: Uint8Array): Id {
return Id.fromString(new TextDecoder().decode(b));
}
static fromBytes(b: Uint8Array): Id {
const bytes = b ?? Uint8Array.from([]);
if (bytes.length !== Constants.ID.ID_LENGTH) {
throw new Error('fromBytes error: byte array incorrect length');
}
if (bytes.every((i: number) => i === 0)) {
throw new Error('fromBytes error: byte array empty');
}
const id = Id.getFromBytes(bytes);
if (!BytesHelper.checkChecksum(bytes)) {
throw new Error('fromBytes error: checksum error');
}
return id;
}
static fromString(s: string): Id {
const bytes = base58Js.base58_to_binary(s);
return Id.fromBytes(bytes);
}
static idFromInt(bigInt: bigint): Id {
const b = BytesHelper.intToBytes(bigInt);
return Id.fromBytes(b);
}
static async profileId(id: Id, nonce: bigint): Promise<Id> {
const bigIntHash = await poseidonHash([id.bigInt(), nonce]);
const { typ } = BytesHelper.decomposeBytes(id.bytes);
const genesis = BytesHelper.intToNBytes(bigIntHash, 27);
return new Id(typ, genesis);
}
}

3
src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module 'circomlibjs' {
export function buildPoseidon(): Promise<any>;
}

View File

@@ -1,9 +1,8 @@
export function helloWorld() {
const message = 'Hello World from my example modern npm package!';
return message;
}
export function goodBye() {
const message = 'Goodbye from my example modern npm package!';
return message;
}
export * from './claim';
export * from './constants';
export * from './did';
export * from './elemBytes';
export * from './hex';
export * from './id';
export * from './schemaHash';
export * from './util';

67
src/schemaHash.ts Normal file
View File

@@ -0,0 +1,67 @@
import { Hex } from './hex';
import { Constants } from './constants';
import { BytesHelper } from './elemBytes';
export class SchemaHash {
private _bytes: Uint8Array = new Uint8Array(Constants.SCHEMA.HASH_LENGTH);
/**
* Constructor
* @param bytes
*/
constructor(bytes?: Uint8Array) {
if (bytes) {
this._bytes = bytes;
}
if (this.bytes.length !== Constants.SCHEMA.HASH_LENGTH) {
throw new Error(`Schema hash must be ${Constants.SCHEMA.HASH_LENGTH} bytes long`);
}
}
get bytes(): Uint8Array {
return this._bytes;
}
/**
* MarshalText returns HEX representation of SchemaHash.
* @returns {Uint8Array} 32 bytes//
*/
marshalTextBytes(): Uint8Array {
return Hex.encode(this.bytes);
}
marshalText(): string {
return new TextDecoder().decode(this.marshalTextBytes());
}
/**
* NewSchemaHashFromHex creates new SchemaHash from hex string
* @param s
* @returns {SchemaHash}
*/
static newSchemaHashFromHex(s: string): SchemaHash {
const schemaEncodedBytes = Hex.decodeString(s);
if (schemaEncodedBytes.length !== Constants.SCHEMA.HASH_LENGTH) {
throw new Error(`invalid schema hash length: ${schemaEncodedBytes.length}`);
}
return new SchemaHash(schemaEncodedBytes);
}
/**
* NewSchemaHashFromInt creates new SchemaHash from big.Int
* @param i
* @returns
*/
static newSchemaHashFromInt(i: bigint): SchemaHash {
return new SchemaHash(BytesHelper.intToBytes(i));
}
/**
* Convert SchemaHash to big.Int
* @returns {bigint}
*/
bigInt(): bigint {
return BytesHelper.bytesToInt(this.bytes);
}
}

78
src/util.ts Normal file
View File

@@ -0,0 +1,78 @@
import { Constants } from './constants';
const buildPoseidon = require('circomlibjs');
export function fromLittleEndian(bytes: Uint8Array): bigint {
const n256 = BigInt(256);
let result = BigInt(0);
let base = BigInt(1);
bytes.forEach((byte) => {
result += base * BigInt(byte);
base = base * n256;
});
return result;
}
export function fromBigEndian(bytes: Uint8Array): bigint {
return fromLittleEndian(bytes.reverse());
}
export function toLittleEndian(bigNumber: bigint, len = 31): Uint8Array {
const n256 = BigInt(256);
const result = new Uint8Array(len);
let i = 0;
while (bigNumber > BigInt(0)) {
result[i] = Number(bigNumber % n256);
bigNumber = bigNumber / n256;
i += 1;
}
return result;
}
export function toBigEndian(bigNumber: bigint): Uint8Array {
return toLittleEndian(bigNumber).reverse();
}
export function putUint32(n: number): Uint8Array {
const buf = new ArrayBuffer(4);
const view = new DataView(buf);
view.setUint32(0, n, true);
return new Uint8Array(buf);
}
export function getUint32(arr: Uint8Array): number {
const buf = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
return new DataView(buf).getUint32(0, true);
}
export function putUint64(n: number): Uint8Array {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setBigUint64(0, BigInt(n), true);
return new Uint8Array(buf);
}
export function getUint64(arr: Uint8Array): number {
const buf = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
return Number(new DataView(buf).getBigUint64(0, true));
}
export function getUnixTimestamp(d: Date): number {
return Math.floor(d.getTime() / 1000);
}
export function getDateFromUnixTimestamp(n: number): Date {
return new Date(n * 1000);
}
// checkBigIntInField checks if given *big.Int fits in a Field Q element
export function checkBigIntInField(a: bigint): boolean {
return a < Constants.Q;
}
export function checkBigIntArrayInField(arr: bigint[]): boolean {
return arr.every((n) => checkBigIntInField(n));
}
export async function poseidonHash(input: number[] | bigint[] | Uint8Array): Promise<bigint> {
const poseidon = await buildPoseidon();
return poseidon.F.toObject(poseidon(input));
}

375
tests/claim.test.ts Normal file
View File

@@ -0,0 +1,375 @@
import { IdPosition, SubjectFlag } from './../src/claim';
import { BytesHelper, ElemBytes } from './../src/elemBytes';
import {
Claim,
newClaim,
withExpirationDate,
withFlagUpdatable,
withIndexData,
withIndexDataBytes,
withIndexDataInts,
withRevocationNonce,
withValueData,
withValueDataInts,
withVersion
} from '../src/claim';
import { SchemaHash } from '../src/schemaHash';
import { poseidonHash } from '../src/util';
import { Hex } from '../src/hex';
import { Constants } from '../src/constants';
import { Id } from '../src/id';
describe('claim test', () => {
it('new claim', () => {
const claim = newClaim(new SchemaHash(), withFlagUpdatable(true));
expect(claim.value.length).toEqual(4);
for (let i = 1; i < 4; i++) {
expect(claim.index[i].bytes.every((b) => b === 0)).toBeTruthy();
}
for (let i = 0; i < 32; i++) {
if (i === 16) {
expect(claim.index[0].bytes[i]).toEqual(0b10000);
} else {
expect(claim.index[0].bytes[i]).toEqual(0);
}
}
const dt = claim.getExpirationDate();
expect(dt).toBeNull();
});
it('raw slots', async () => {
const claim = newClaim(new SchemaHash(), withFlagUpdatable(true));
const { index, value } = claim.rawSlots();
const indexHash = await poseidonHash([
index[0].toBigInt(),
index[1].toBigInt(),
index[2].toBigInt(),
index[3].toBigInt()
]);
const valueHash = await poseidonHash([
value[0].toBigInt(),
value[1].toBigInt(),
value[2].toBigInt(),
value[3].toBigInt()
]);
expect('19905260441950906049955646784794273651462264973332746773406911374272567544299').toEqual(
indexHash.toString()
);
expect('2351654555892372227640888372176282444150254868378439619268573230312091195718').toEqual(
valueHash.toString()
);
});
it('getSchemaHash', async () => {
const sc = new SchemaHash(
Uint8Array.from(Array.from({ length: 16 }, () => Math.floor(Math.random() * 16)))
);
expect(sc.bytes.length).toEqual(16);
const claim = newClaim(sc);
expect(sc.bytes).toEqual(claim.index[0].bytes.slice(0, sc.bytes.length));
const shFromClaim = claim.getSchemaHash();
const shFromClaimHexBytes = shFromClaim.marshalText();
expect(Hex.encodeString(sc.bytes)).toEqual(shFromClaimHexBytes);
});
it('getFlagUpdatable', async () => {
const sc = new SchemaHash();
let claim = newClaim(sc);
expect(claim.getFlagUpdatable()).toBeFalsy();
claim.setFlagUpdatable(true);
expect(claim.getFlagUpdatable()).toBeTruthy();
claim.setFlagUpdatable(false);
expect(claim.getFlagUpdatable()).toBeFalsy();
claim = newClaim(sc, withFlagUpdatable(true));
expect(claim.getFlagUpdatable()).toBeTruthy();
claim = newClaim(sc, withFlagUpdatable(false));
expect(claim.getFlagUpdatable()).toBeFalsy();
});
it('getVersion', async () => {
const sc = new SchemaHash();
const maxUint32 = Math.pow(2, 32) - 1;
const claim = newClaim(sc, withVersion(maxUint32));
expect(maxUint32).toEqual(claim.getVersion());
const randomInt = Math.floor(Math.random() * maxUint32);
claim.setVersion(randomInt);
expect(randomInt).toEqual(claim.getVersion());
});
it('getRevocationNonce', () => {
const sc = new SchemaHash();
const nonce = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const claim = newClaim(sc, withRevocationNonce(nonce));
expect(nonce).toEqual(claim.getRevocationNonce());
const nonce2 = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
claim.setRevocationNonce(nonce2);
expect(nonce2).toEqual(claim.getRevocationNonce());
});
it('expirationDate', () => {
const sh = new SchemaHash();
const expDate = new Date();
expDate.setSeconds(0, 0);
const c1 = newClaim(sh, withExpirationDate(expDate));
let expDate2 = c1.getExpirationDate();
expect(expDate2).not.toBeNull();
expect(expDate2?.getTime()).toEqual(expDate.getTime());
c1.resetExpirationDate();
expDate2 = c1.getExpirationDate();
expect(expDate2).toBeNull();
const expDate3 = new Date(expDate);
expDate3.setSeconds(expDate3.getSeconds() + 10);
expDate2 = c1.getExpirationDate();
c1.setExpirationDate(expDate3);
expDate2 = c1.getExpirationDate();
expect(expDate2?.getTime()).toEqual(expDate3.getTime());
});
it('test int size', () => {
const iX = BigInt(
'16243864111864693853212588481963275789994876191154110553066821559749894481761'
);
const iY = BigInt(
'7078462697308959301666117070269719819629678436794910510259518359026273676830'
);
const vX = BigInt(
'12448278679517811784508557734102986855579744384337338465055621486538311281772'
);
const vY = BigInt(
'9260608685281348956030279125705000716237952776955782848598673606545494194823'
);
const ixSlot = new ElemBytes().setBigInt(iX);
const iySlot = new ElemBytes().setBigInt(iY);
const vxSlot = new ElemBytes().setBigInt(vX);
const vySlot = new ElemBytes().setBigInt(vY);
newClaim(new SchemaHash(), withIndexData(ixSlot, iySlot), withValueData(vxSlot, vySlot));
expect(ixSlot.toBigInt().toString()).toEqual(iX.toString());
expect(iySlot.toBigInt().toString()).toEqual(iY.toString());
expect(vxSlot.toBigInt().toString()).toEqual(vX.toString());
expect(vySlot.toBigInt().toString()).toEqual(vY.toString());
});
it('new data slot from int', () => {
const ds = new ElemBytes().setBigInt(
BigInt('16243864111864693853212588481963275789994876191154110553066821559749894481761')
);
const expected = new ElemBytes(
Uint8Array.from([
0x61, 0x27, 0xa0, 0xeb, 0x58, 0x7a, 0x6c, 0x2b, 0x4a, 0xa8, 0xc1, 0x2e, 0xf5, 0x01, 0xb2,
0xdb, 0xd0, 0x9c, 0xb1, 0xa5, 0x9c, 0x83, 0x42, 0x57, 0x91, 0xa5, 0x20, 0xbf, 0x86, 0xb3,
0xe9, 0x23
])
);
expect(ds.bytes).toEqual(expected.bytes);
expect(() =>
new ElemBytes().setBigInt(
BigInt('9916243864111864693853212588481963275789994876191154110553066821559749894481761')
)
).toThrow(new Error(Constants.ERRORS.DATA_OVERFLOW));
});
it('index data ints', () => {
const expSlot = new ElemBytes();
expSlot.setBigInt(BigInt(0));
const value = BigInt(64);
const claim = newClaim(new SchemaHash(), withIndexDataInts(value, null));
expect(expSlot.bytes).toEqual(claim.index[3].bytes);
const claim2 = newClaim(new SchemaHash(), withIndexDataInts(null, value));
expect(expSlot.bytes).toEqual(claim2.index[2].bytes);
});
it('value data ints', () => {
const expSlot = new ElemBytes();
expSlot.setBigInt(BigInt(0));
const value = BigInt(64);
const claim = newClaim(new SchemaHash(), withValueDataInts(value, null));
expect(expSlot.bytes).toEqual(claim.value[3].bytes);
const claim2 = newClaim(new SchemaHash(), withValueDataInts(null, value));
expect(expSlot.bytes).toEqual(claim2.value[2].bytes);
});
it('with index data bytes', () => {
const iX = BigInt(
'124482786795178117845085577341029868555797443843373384650556214865383112817'
);
const expSlot = new ElemBytes();
expSlot.setBigInt(BigInt(0));
const claim = newClaim(new SchemaHash(), withIndexDataBytes(BytesHelper.intToBytes(iX), null));
expect(expSlot.bytes).toEqual(claim.index[3].bytes);
});
describe('serialization', () => {
it('with strings', () => {
const input = `[
"15163995036539824738096525342132337704181738148399168403057770094395141110111",
"3206594817839378626027676511482956481343861686313501795018892230311002175077",
"7420031054231607091230846181053275837604749850669737756447914128096832575029",
"6843256246667081694694856844555135410358903741435158507252727716055448769466",
"18335061644187980192028500482619331449203987338928612566250871337402164885236",
"4747739418092571675618239353368909204965774632269590366651599441049750269324",
"10060277146294090095035892104009266064127776406104429246320070556972379481946",
"5835715034681704899254417398745238273415614452113785384300119694985241103333"
]`;
const expected = new Claim();
expected.index = [
new ElemBytes().slotFromHex(
'5fb90badb37c5821b6d95526a41a9504680b4e7c8b763a1b1d49d4955c848621'
),
new ElemBytes().slotFromHex(
'65f606f6a63b7f3dfd2567c18979e4d60f26686d9bf2fb26c901ff354cde1607'
),
new ElemBytes().slotFromHex(
'35d6042c4160f38ee9e2a9f3fb4ffb0019b454d522b5ffa17604193fb8966710'
),
new ElemBytes().slotFromHex(
'ba53af19779cb2948b6570ffa0b773963c130ad797ddeafe4e3ad29b5125210f'
)
];
expected.value = [
new ElemBytes().slotFromHex(
'f4b6f44090a32711f3208e4e4b89cb5165ce64002cbd9c2887aa113df2468928'
),
new ElemBytes().slotFromHex(
'8ced323cb76f0d3fac476c9fb03fc9228fbae88fd580663a0454b68312207f0a'
),
new ElemBytes().slotFromHex(
'5a27db029de37ae37a42318813487685929359ca8c5eb94e152dc1af42ea3d16'
),
new ElemBytes().slotFromHex(
'e50be1a6dc1d5768e8537988fddce562e9b948c918bba3e933e5c400cde5e60c'
)
];
const claim = new Claim().unMarshalJson(input);
expect(claim).toEqual(expected);
const result = expected.marshalJson();
expect(JSON.parse(input)).toEqual(JSON.parse(result));
});
it('with binary', () => {
const binDataStr = [
'5fb90badb37c5821b6d95526a41a9504680b4e7c8b763a1b1d49d4955c848621',
'65f606f6a63b7f3dfd2567c18979e4d60f26686d9bf2fb26c901ff354cde1607',
'35d6042c4160f38ee9e2a9f3fb4ffb0019b454d522b5ffa17604193fb8966710',
'ba53af19779cb2948b6570ffa0b773963c130ad797ddeafe4e3ad29b5125210f',
'f4b6f44090a32711f3208e4e4b89cb5165ce64002cbd9c2887aa113df2468928',
'8ced323cb76f0d3fac476c9fb03fc9228fbae88fd580663a0454b68312207f0a',
'5a27db029de37ae37a42318813487685929359ca8c5eb94e152dc1af42ea3d16',
'e50be1a6dc1d5768e8537988fddce562e9b948c918bba3e933e5c400cde5e60c'
].join('');
const binData = Hex.decodeString(binDataStr);
const want = new Claim();
want.index = [
new ElemBytes().slotFromHex(
'5fb90badb37c5821b6d95526a41a9504680b4e7c8b763a1b1d49d4955c848621'
),
new ElemBytes().slotFromHex(
'65f606f6a63b7f3dfd2567c18979e4d60f26686d9bf2fb26c901ff354cde1607'
),
new ElemBytes().slotFromHex(
'35d6042c4160f38ee9e2a9f3fb4ffb0019b454d522b5ffa17604193fb8966710'
),
new ElemBytes().slotFromHex(
'ba53af19779cb2948b6570ffa0b773963c130ad797ddeafe4e3ad29b5125210f'
)
];
want.value = [
new ElemBytes().slotFromHex(
'f4b6f44090a32711f3208e4e4b89cb5165ce64002cbd9c2887aa113df2468928'
),
new ElemBytes().slotFromHex(
'8ced323cb76f0d3fac476c9fb03fc9228fbae88fd580663a0454b68312207f0a'
),
new ElemBytes().slotFromHex(
'5a27db029de37ae37a42318813487685929359ca8c5eb94e152dc1af42ea3d16'
),
new ElemBytes().slotFromHex(
'e50be1a6dc1d5768e8537988fddce562e9b948c918bba3e933e5c400cde5e60c'
)
];
const result = new Claim();
result.unMarshalBinary(binData);
expect(want).toEqual(result);
const marshalResult = want.marshalBinary();
expect(binData).toEqual(marshalResult);
});
});
describe('work with id', () => {
it('id position', () => {
const tests = [
{
name: 'self claim',
claim: () => newClaim(new SchemaHash()),
expectedPosition: IdPosition.None
},
{
name: 'subject stored in index',
claim: () => {
const claim = newClaim(new SchemaHash());
const genesis32bytes = BytesHelper.hashBytes('genesistest');
const genesis = genesis32bytes.slice(0, 27);
claim.setIndexId(new Id(Constants.ID.TYPE_DEFAULT, genesis));
return claim;
},
expectedPosition: IdPosition.Index
},
{
name: 'subject stored in value',
claim: () => {
const claim = newClaim(new SchemaHash());
const genesis32bytes = BytesHelper.hashBytes('genesistest');
const genesis = genesis32bytes.slice(0, 27);
claim.setValueId(new Id(Constants.ID.TYPE_DEFAULT, genesis));
return claim;
},
expectedPosition: IdPosition.Value
}
];
tests.forEach((test) => {
const c = test.claim();
const position = c.getIdPosition();
expect(position).toEqual(test.expectedPosition);
});
});
it('id position error case', () => {
const tests = [
{
name: 'invalid position',
claim: () => {
const c = newClaim(new SchemaHash());
c.setSubject(SubjectFlag.Invalid);
return c;
},
expectedPosition: IdPosition.None,
expectedError: new Error(Constants.ERRORS.INVALID_SUBJECT_POSITION)
}
];
tests.forEach((tt) => {
const c = tt.claim();
expect(() => c.getIdPosition()).toThrow(tt.expectedError);
});
});
});
});

48
tests/did.test.ts Normal file
View File

@@ -0,0 +1,48 @@
import { DID, NetworkId, DIDMethodByte, DidMethod, withNetwork, Blockchain } from './../src/did';
describe('DID tests', () => {
const tests = [
{
description: 'Test readonly did',
identifier: 'tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa',
did: 'did:iden3:tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa',
options: () => null
},
{
description: 'Test eth did',
identifier: 'zyaYCrj27j7gJfrBboMW49HFRSkQznyy12ABSVzTy',
did: 'did:iden3:eth:main:zyaYCrj27j7gJfrBboMW49HFRSkQznyy12ABSVzTy',
options: withNetwork(Blockchain.Ethereum, NetworkId.Main)
},
{
description: 'Test polygon did',
identifier: 'wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ',
did: 'did:iden3:polygon:mumbai:wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ',
options: withNetwork(Blockchain.Polygon, NetworkId.Mumbai)
}
];
tests.forEach((test) => {
it(test.description, () => {
const got = DID.newDID(test.identifier, test.options);
expect(got.toString()).toEqual(test.did);
});
});
it('parse DID', () => {
// did
let didStr = 'did:iden3:polygon:mumbai:wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ';
let did = DID.parse(didStr);
expect('wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ').toEqual(did.id.string());
expect(NetworkId.Mumbai).toEqual(did.networkId);
expect(Blockchain.Polygon).toEqual(did.blockchain);
// readonly did
didStr = 'did:iden3:tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa';
did = DID.parse(didStr);
expect('tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa').toEqual(did.id.string());
expect('').toEqual(did.networkId);
expect('').toEqual(did.blockchain);
expect([DIDMethodByte[DidMethod.Iden3], 0b0]).toMatchObject(did.id.type());
});
});

84
tests/id.test.ts Normal file
View File

@@ -0,0 +1,84 @@
import { Constants } from './../src/constants';
import { Id } from '../src/id';
import { Hex } from './../src/hex';
import { Blockchain, buildDIDType, DidMethod, NetworkId } from '../src/did';
import { BytesHelper } from '../src/elemBytes';
describe('id tests', () => {
it('id parses', () => {
// Generate ID0
const typ0 = Hex.decodeString('0000').slice(0, 2);
const genesis0 = BytesHelper.hashBytes('genesistest').slice(0, 27);
const id0 = new Id(typ0, genesis0);
// Check ID0
expect('114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E').toEqual(id0.string());
// Generate ID1
const typ1 = Hex.decodeString('0001').slice(0, 2);
const genesis1 = BytesHelper.hashBytes('genesistest').slice(0, 27);
const id1 = new Id(typ1, genesis1);
// Check ID1
expect('1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4').toEqual(id1.string());
const emptyChecksum = [0x00, 0x00];
expect(id0.bytes.slice(-2)).not.toEqual(emptyChecksum);
const id0FromBytes = Id.fromBytes(id0.bytes);
expect(id0.bytes).toEqual(id0FromBytes.bytes);
expect(id0.string()).toEqual(id0FromBytes.string());
expect('114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E').toEqual(id0FromBytes.string());
const id1FromBytes = Id.fromBytes(id1.bytes);
expect(id1.bytes).toEqual(id1FromBytes.bytes);
expect(id1.string()).toEqual(id1FromBytes.string());
expect('1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4').toEqual(id1FromBytes.string());
const id0FromString = Id.fromString(id0.string());
expect(id0.bytes).toEqual(id0FromString.bytes);
expect(id0.string()).toEqual(id0FromString.string());
expect('114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E').toEqual(id0FromString.string());
});
it('marshal/unmarshal', () => {
const id = Id.fromString('11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv');
const idj = id.marshal();
expect('11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv').toEqual(Id.unMarshal(idj).string());
const idp = Id.unMarshal(idj);
expect(id).toEqual(idp);
});
it('Id as DID', () => {
const typ = buildDIDType(DidMethod.Iden3, Blockchain.Polygon, NetworkId.Mumbai);
const genesis1 = BytesHelper.hashBytes('genesistes1t2').slice(0, 27);
const id = new Id(typ, genesis1);
expect(() => id).not.toThrow();
});
it('id checksum', () => {
const typ = Constants.ID.TYPE_DEFAULT;
const genesis = BytesHelper.hashBytes('genesistest').slice(0, 27);
let id = new Id(typ, genesis);
const checksum = id.checksum();
expect(BytesHelper.calculateChecksum(typ, genesis)).toEqual(checksum);
expect(BytesHelper.checkChecksum(id.bytes)).toBeTruthy();
// check that if we change the checksum, returns false on CheckChecksum
id = new Id(typ, genesis);
id.bytes = Uint8Array.from([...id.bytes.slice(0, 29), ...[0x00, 0x01]]);
expect(BytesHelper.checkChecksum(id.bytes)).toBeFalsy();
// check that if we change the type, returns false on CheckChecksum
id = new Id(typ, genesis);
id.bytes = Uint8Array.from([...[0x00, 0x01], ...id.bytes.slice(-29)]);
expect(BytesHelper.checkChecksum(id.bytes)).toBeFalsy();
// check that if we change the genesis, returns false on CheckChecksum
id = new Id(typ, genesis);
const changedGenesis = BytesHelper.hashBytes('changedgenesis').slice(0, 27);
id.bytes = Uint8Array.from([...id.bytes.slice(0, 2), ...changedGenesis, ...id.bytes.slice(-2)]);
expect(BytesHelper.checkChecksum(id.bytes)).toBeFalsy();
// test with a empty id
const empty = new Uint8Array(31);
expect(() => Id.fromBytes(empty)).toThrow(new Error('fromBytes error: byte array empty'));
});
});

View File

@@ -1,9 +0,0 @@
import { helloWorld } from '../src';
describe('Hello World Function', () => {
it('should return the hello world message', () => {
const expected = 'Hello World from my example modern npm package!';
const actual = helloWorld();
expect(actual).toEqual(expected);
});
});

31
tests/schemaHash.test.ts Normal file
View File

@@ -0,0 +1,31 @@
import { SchemaHash } from '../src/schemaHash';
import { Hex } from '../src/hex';
describe('schema hash', () => {
it('should return the hex value', () => {
const source = Uint8Array.from([
82, 253, 252, 7, 33, 130, 101, 79, 22, 63, 95, 15, 154, 98, 29, 114
]);
expect(source.length).toEqual(16);
const actual = Hex.encode(source);
const expected = Uint8Array.from([
53, 50, 102, 100, 102, 99, 48, 55, 50, 49, 56, 50, 54, 53, 52, 102, 49, 54, 51, 102, 53, 102,
48, 102, 57, 97, 54, 50, 49, 100, 55, 50
]);
expect(expected.length).toEqual(32);
expect(actual).toEqual(expected);
});
it('new schema hash from hex', () => {
const hash = 'ca938857241db9451ea329256b9c06e5';
const got = SchemaHash.newSchemaHashFromHex(hash);
const exp = Hex.decodeString(hash);
expect(exp).toEqual(got.bytes);
});
it('big int check', () => {
const schema = SchemaHash.newSchemaHashFromHex('ca938857241db9451ea329256b9c06e5');
const exp = BigInt('304427537360709784173770334266246861770');
const got = schema.bigInt();
expect(got).toEqual(exp);
});
});

3
types/circomlibjs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module 'circomlibjs' {
export function buildPoseidon(): Promise<any>;
}