diff --git a/CHANGELOG.md b/CHANGELOG.md index ba35b93..26b87e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.0.1-alpha.191](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.190...v0.0.1-alpha.191) (2024-11-01) + + +### Features + +* add base64 getters to udi class ([ca2f498](https://github.com/DIG-Network/dig-chia-sdk/commit/ca2f49810278d01bd460ae6929a4e6d58ea98aa7)) + ### [0.0.1-alpha.190](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.189...v0.0.1-alpha.190) (2024-11-01) diff --git a/package-lock.json b/package-lock.json index 56bde94..105d3c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.190", + "version": "0.0.1-alpha.191", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.190", + "version": "0.0.1-alpha.191", "license": "ISC", "dependencies": { "@dignetwork/datalayer-driver": "^0.1.29", diff --git a/package.json b/package.json index 670a3a4..858b155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dignetwork/dig-sdk", - "version": "0.0.1-alpha.190", + "version": "0.0.1-alpha.191", "description": "", "type": "commonjs", "main": "./dist/index.js", diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index 6544c9c..28d0953 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -1,102 +1,108 @@ -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 format: urn:dig:chainName:storeId:rootHash/resourceKey -// This allows unique resource identification across the DIG network. -// +import * as urns from "urns"; +import { createHash } from "crypto"; +import { encode as base32Encode, decode as base32Decode } from "hi-base32"; + class Udi { readonly chainName: string; - private readonly _storeId: Buffer; - private readonly _rootHash: Buffer | null; + private readonly _storeIdHex: string; + private readonly _rootHashHex: string | 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, + storeId: string, + rootHash: string | 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._storeIdHex = Udi.verifyAndFormatHex(storeId); + this._rootHashHex = rootHash ? Udi.verifyAndFormatHex(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.toUpperCase(), false)); // Decode with uppercase + static verifyAndFormatHex(input: string): string { + if (!/^[a-fA-F0-9]{64}$/.test(input)) { + throw new Error("Input must be a 64-character hex string."); } - - 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.toUpperCase()); - } - - 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); + return input; } static fromUrn(urn: string): Udi { const parsedUrn = urns.parseURN(urn); - if (parsedUrn.nid.toLowerCase() !== Udi.nid) { + if (parsedUrn.nid !== Udi.nid) { throw new Error(`Invalid nid: ${parsedUrn.nid}`); } - const parts = parsedUrn.nss.split(':'); + 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]; + const storeIdHex = Udi.convertToHex(parts[1].split("/")[0]); - let rootHash: string | null = null; + let rootHashHex: string | null = null; if (parts.length > 2) { - rootHash = parts[2].split('/')[0]; + rootHashHex = Udi.convertToHex(parts[2].split("/")[0]); } - const pathParts = parsedUrn.nss.split('/'); + const pathParts = parsedUrn.nss.split("/"); let resourceKey: string | null = null; if (pathParts.length > 1) { - resourceKey = pathParts.slice(1).join('/'); + resourceKey = pathParts.slice(1).join("/"); } - return new Udi(chainName, storeId, rootHash, resourceKey); + return new Udi(chainName, storeIdHex, rootHashHex, resourceKey); } - toUrn(encoding: 'hex' | 'base32' | 'base64' = 'hex'): string { - const storeIdStr = this.bufferToString(this._storeId, encoding); + static convertToHex(input: string): string { + // Attempt hex conversion first + if (/^[a-fA-F0-9]{64}$/.test(input)) return input; + + // Convert from Base32 + try { + const paddedInput = Udi.addBase32Padding(input.toUpperCase()); + const buffer = Buffer.from(base32Decode(paddedInput, false)); + return buffer.toString("hex"); + } catch (e) { + console.warn("Base32 decoding failed, trying Base64 encoding..."); + } + + // Convert from Base64 + try { + const standardBase64 = Udi.addBase64Padding(Udi.toStandardBase64(input)); + const buffer = Buffer.from(standardBase64, "base64"); + return buffer.toString("hex"); + } catch (e) { + throw new Error("Invalid input encoding. Must be 32-byte hex, Base32, or Base64 string."); + } + } + + static addBase32Padding(input: string): string { + const paddingNeeded = (8 - (input.length % 8)) % 8; + return input + "=".repeat(paddingNeeded); + } + + static toStandardBase64(base64UrlSafe: string): string { + return base64UrlSafe.replace(/-/g, "+").replace(/_/g, "/"); + } + + static addBase64Padding(base64: string): string { + const paddingNeeded = (4 - (base64.length % 4)) % 4; + return base64 + "=".repeat(paddingNeeded); + } + + toUrn(encoding: "hex" | "base32" | "base64" = "hex"): string { + const storeIdStr = this.formatBufferAsEncoding(this._storeIdHex, encoding); let urn = `${Udi.namespace}:${this.chainName}:${storeIdStr}`; - if (this._rootHash) { - const rootHashStr = this.bufferToString(this._rootHash, encoding); + if (this._rootHashHex) { + const rootHashStr = this.formatBufferAsEncoding(this._rootHashHex, encoding); urn += `:${rootHashStr}`; } @@ -107,24 +113,29 @@ class Udi { return urn; } - bufferToString(buffer: Buffer, encoding: 'hex' | 'base32' | 'base64'): string { - switch (encoding) { - case 'hex': - return buffer.toString('hex'); - case 'base32': - return base32Encode(buffer).toUpperCase().replace(/=+$/, ''); // Convert to uppercase and remove padding - case 'base64': - return buffer.toString('base64').toLowerCase(); // Convert to lowercase - default: - throw new Error("Unsupported encoding"); + private formatBufferAsEncoding(hexString: string, encoding: "hex" | "base32" | "base64"): string { + const buffer = Buffer.from(hexString, "hex"); + if (encoding === "hex") { + return hexString; + } else if (encoding === "base32") { + return base32Encode(buffer).replace(/=+$/, ""); // Strip padding for Base32 + } else if (encoding === "base64") { + return Udi.toBase64UrlSafe(buffer.toString("base64")); // Convert to URL-safe Base64 } + throw new Error("Unsupported encoding type"); + } + + static toBase64UrlSafe(base64Standard: string): string { + return base64Standard.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } equals(other: Udi): boolean { return ( - this._storeId.equals(other._storeId) && + this._storeIdHex === other._storeIdHex && this.chainName === other.chainName && - (this._rootHash && other._rootHash ? this._rootHash.equals(other._rootHash) : this._rootHash === other._rootHash) && + (this._rootHashHex && other._rootHashHex + ? this._rootHashHex === other._rootHashHex + : this._rootHashHex === other._rootHashHex) && this.resourceKey === other.resourceKey ); } @@ -134,23 +145,37 @@ class Udi { } clone(): Udi { - return new Udi(this.chainName, this._storeId, this._rootHash, this.resourceKey); + return new Udi(this.chainName, this._storeIdHex, this._rootHashHex, this.resourceKey); } hashCode(): string { - const hash = createHash('sha256'); + const hash = createHash("sha256"); hash.update(this.toUrn()); - return hash.digest('hex'); + return hash.digest("hex"); } - // Getter for storeId as a hex string get storeId(): string { - return this._storeId.toString('hex'); + return this._storeIdHex; } - // Getter for rootHash as a hex string get rootHash(): string | null { - return this._rootHash ? this._rootHash.toString('hex') : null; + return this._rootHashHex; + } + + get storeIdBase32(): string { + return this.formatBufferAsEncoding(this._storeIdHex, "base32"); + } + + get rootHashBase32(): string | null { + return this._rootHashHex ? this.formatBufferAsEncoding(this._rootHashHex, "base32") : null; + } + + get storeIdBase64(): string { + return this.formatBufferAsEncoding(this._storeIdHex, "base64"); + } + + get rootHashBase64(): string | null { + return this._rootHashHex ? this.formatBufferAsEncoding(this._rootHashHex, "base64") : null; } }