Skip to content

Commit

Permalink
Merge pull request #163 from DIG-Network/release/v0.0.1-alpha.175
Browse files Browse the repository at this point in the history
Release/v0.0.1 alpha.175
  • Loading branch information
MichaelTaylor3D authored Oct 30, 2024
2 parents e93e342 + 273f761 commit 7337251
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 2 deletions.
15 changes: 14 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"crypto-js": "^4.2.0",
"figures": "^6.1.0",
"fs-extra": "^11.2.0",
"hi-base32": "^0.5.1",
"ignore": "^5.3.2",
"inquirer": "^10.2.2",
"lodash": "^4.17.21",
Expand All @@ -53,7 +54,9 @@
"proper-lockfile": "^4.1.2",
"superagent": "^10.1.0",
"redis": "^4.7.0",
"unzipper": "^0.12.3"
"superagent": "^10.0.0",
"unzipper": "^0.12.3",
"urns": "^0.6.0"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
Expand Down
141 changes: 141 additions & 0 deletions src/utils/Udi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as urns from 'urns';
import { createHash } from 'crypto';
import { encode as base32Encode, decode as base32Decode } from 'hi-base32';

//
// This class encapsulates the concept of a Universal Data Identifier (UDI) which is a
// standardized way to identify resources across the distributed DIG mesh network.
// The UDI is formatted as follows:
// urn:dig:chainName:storeId:rootHash/resourceKey
// The UDI can be used to uniquely identify resources across the DIG network.
//
class Udi {
readonly chainName: string;
readonly storeId: Buffer;
readonly rootHash: Buffer | null;
readonly resourceKey: string | null;
static readonly nid: string = "dig";
static readonly namespace: string = `urn:${Udi.nid}`;

constructor(
chainName: string,
storeId: string | Buffer,
rootHash: string | Buffer | null = null,
resourceKey: string | null = null
) {
if (!storeId) {
throw new Error("storeId cannot be empty");
}
this.chainName = chainName || "chia";
this.storeId = Udi.convertToBuffer(storeId);
this.rootHash = rootHash ? Udi.convertToBuffer(rootHash) : null;
this.resourceKey = resourceKey;
}

static convertToBuffer(input: string | Buffer): Buffer {
if (Buffer.isBuffer(input)) {
return input;
}

if (Udi.isHex(input)) {
return Buffer.from(input, 'hex');
}

if (Udi.isBase32(input)) {
return Buffer.from(base32Decode(input, false)); // Decode as UTF-8
}

throw new Error("Invalid input encoding. Must be 32-byte hex or Base32 string.");
}

static isHex(input: string): boolean {
return /^[a-fA-F0-9]{64}$/.test(input);
}

static isBase32(input: string): boolean {
return /^[a-z2-7]{52}$/.test(input.toLowerCase());
}

withRootHash(rootHash: string | Buffer | null): Udi {
return new Udi(this.chainName, this.storeId, rootHash, this.resourceKey);
}

withResourceKey(resourceKey: string | null): Udi {
return new Udi(this.chainName, this.storeId, this.rootHash, resourceKey);
}

static fromUrn(urn: string): Udi {
const parsedUrn = urns.parseURN(urn);
if (parsedUrn.nid.toLowerCase() !== Udi.nid) {
throw new Error(`Invalid nid: ${parsedUrn.nid}`);
}

const parts = parsedUrn.nss.split(':');
if (parts.length < 2) {
throw new Error(`Invalid UDI format: ${parsedUrn.nss}`);
}

const chainName = parts[0];
const storeId = parts[1].split('/')[0];

let rootHash: string | null = null;
if (parts.length > 2) {
rootHash = parts[2].split('/')[0];
}

const pathParts = parsedUrn.nss.split('/');
let resourceKey: string | null = null;
if (pathParts.length > 1) {
resourceKey = pathParts.slice(1).join('/');
}

return new Udi(chainName, storeId, rootHash, resourceKey);
}

toUrn(encoding: 'hex' | 'base32' = 'hex'): string {
const storeIdStr = this.bufferToString(this.storeId, encoding);
let urn = `${Udi.namespace}:${this.chainName}:${storeIdStr}`;

if (this.rootHash) {
const rootHashStr = this.bufferToString(this.rootHash, encoding);
urn += `:${rootHashStr}`;
}

if (this.resourceKey) {
urn += `/${this.resourceKey}`;
}

return urn;
}

bufferToString(buffer: Buffer, encoding: 'hex' | 'base32'): string {
return encoding === 'hex'
? buffer.toString('hex')
: base32Encode(buffer).toLowerCase().replace(/=+$/, '');
}

equals(other: Udi): boolean {
return (
this.storeId.equals(other.storeId) &&
this.chainName === other.chainName &&
(this.rootHash && other.rootHash ? this.rootHash.equals(other.rootHash) : this.rootHash === other.rootHash) &&
this.resourceKey === other.resourceKey
);
}

toString(): string {
return this.toUrn();
}

clone(): Udi {
return new Udi(this.chainName, this.storeId, this.rootHash, this.resourceKey);
}

hashCode(): string {
const hash = createHash('sha256');
hash.update(this.toUrn());
return hash.digest('hex');
}
}

export { Udi };
68 changes: 68 additions & 0 deletions tests/udi.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect } from 'chai';
import { Udi } from '../src/utils/Udi';

describe('Udi', () => {
it('should initialize correctly with all parameters', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
expect(udi.chainName).to.equal('chia');
expect(udi.storeId).to.equal('store1');
expect(udi.rootHash).to.equal('rootHash1');
expect(udi.resourceKey).to.equal('resourceKey1');
});

it('should initialize correctly with optional parameters', () => {
const udi = new Udi('chia', 'store1');
expect(udi.chainName).to.equal('chia');
expect(udi.storeId).to.equal('store1');
expect(udi.rootHash).to.be.null;
expect(udi.resourceKey).to.be.null;
});

it('should create a new Udi with a different rootHash using withRootHash', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const newUdi = udi.withRootHash('newRootHash');
expect(newUdi.rootHash).to.equal('newRootHash');
expect(newUdi.resourceKey).to.equal('resourceKey1');
});

it('should create a new Udi with a different resourceKey using withResourceKey', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const newUdi = udi.withResourceKey('newResourceKey');
expect(newUdi.resourceKey).to.equal('newResourceKey');
expect(newUdi.rootHash).to.equal('rootHash1');
});

it('should create a Udi from a valid URN', () => {
const urn = 'urn:dig:chia:store1:rootHash1/resourceKey1';
const udi = Udi.fromUrn(urn);
expect(udi.chainName).to.equal('chia');
expect(udi.storeId).to.equal('store1');
expect(udi.rootHash).to.equal('rootHash1');
expect(udi.resourceKey).to.equal('resourceKey1');
});

it('should throw an error for an invalid URN namespace', () => {
const urn = 'urn:invalid:chia:store1:rootHash1/resourceKey1';
expect(() => Udi.fromUrn(urn)).to.throw('Invalid namespace: invalid');
});

it('should convert a Udi to a URN string', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const urn = udi.toUrn();
expect(urn).to.equal('urn:dig:chia:store1:rootHash1/resourceKey1');
});

it('should correctly compare two Udi instances using equals', () => {
const udi1 = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const udi2 = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const udi3 = new Udi('chia', 'store2', 'rootHash1', 'resourceKey1');
expect(udi1.equals(udi2)).to.be.true;
expect(udi1.equals(udi3)).to.be.false;
});

it('should convert a Udi to a string using toString', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const str = udi.toString();
expect(str).to.equal('urn:dig:chia:store1:rootHash1/resourceKey1');
});
});

0 comments on commit 7337251

Please sign in to comment.