Skip to content

Commit

Permalink
Merge pull request #178 from DIG-Network/release/v0.0.1-alpha.191
Browse files Browse the repository at this point in the history
Release/v0.0.1 alpha.191
  • Loading branch information
MichaelTaylor3D authored Nov 1, 2024
2 parents 7e17f15 + d301863 commit 5833179
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 79 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
177 changes: 101 additions & 76 deletions src/utils/Udi.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}

Expand All @@ -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
);
}
Expand All @@ -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;
}
}

Expand Down

0 comments on commit 5833179

Please sign in to comment.