diff --git a/example/index.ts b/example/index.ts index 6a7472c..431103d 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,13 +1,10 @@ -import { readFileSync } from "fs" -import { join } from "path" -import { stdin, stdout } from "process" +import { readFileSync } from "fs"; +import { join } from "path"; +import { stdin, stdout } from "process"; import { createInterface } from "readline"; -import { - type ILogObj, - Logger -} from "tslog" -import xdg from "xdg-portable" -import { Wallet, getBytes } from "ethers" +import { type ILogObj, Logger } from "tslog"; +import xdg from "xdg-portable"; +import { Wallet, getBytes } from "ethers"; import { createClient, formatEther, @@ -15,16 +12,22 @@ import { Annotation, Tagged, type AccountData, -} from "golem-base-sdk" + BTL_PRESETS, + ANNOTATION_KEYS, + isValidEntityKey, + isValidAddress, +} from "golem-base-sdk"; // Path to a golembase wallet -const walletPath = join(xdg.config(), 'golembase', 'wallet.json'); -const keystore = readFileSync(walletPath, 'utf8'); +const walletPath = join(xdg.config(), "golembase", "wallet.json"); +const keystore = readFileSync(walletPath, "utf8"); /** * Read password either from piped stdin or interactively from the terminal. */ -async function readPassword(prompt: string = "Enter wallet password: "): Promise { +async function readPassword( + prompt: string = "Enter wallet password: " +): Promise { if (stdin.isTTY) { // Interactive prompt const rl = createInterface({ @@ -51,16 +54,19 @@ async function readPassword(prompt: string = "Enter wallet password: "): Promise } } -const encoder = new TextEncoder() -const decoder = new TextDecoder() +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); const log = new Logger({ type: "pretty", minLevel: 3, -}) +}); // Utility function to filter with async callbacks -async function asyncFilter(arr: T[], callback: (item: T) => Promise): Promise { +async function asyncFilter( + arr: T[], + callback: (item: T) => Promise +): Promise { const results: T[] = []; for (const item of arr) { if (await callback(item)) { @@ -68,195 +74,245 @@ async function asyncFilter(arr: T[], callback: (item: T) => Promise) } } return results; -}; +} async function main() { log.info("Attempting to decrypt wallet", walletPath); const wallet = Wallet.fromEncryptedJsonSync(keystore, await readPassword()); log.info("Successfully decrypted wallet for account", wallet.address); - const key: AccountData = new Tagged("privatekey", getBytes(wallet.privateKey)) + const key: AccountData = new Tagged( + "privatekey", + getBytes(wallet.privateKey) + ); const client = { local: await createClient( 1337, key, - 'http://localhost:8545', - 'ws://localhost:8545', - log, + "http://localhost:8545", + "ws://localhost:8545", + log ), demo: await createClient( 1337, key, - 'https://api.golembase.demo.golem-base.io', - 'wss://ws-api.golembase.demo.golem-base.io', - log, + "https://api.golembase.demo.golem-base.io", + "wss://ws-api.golembase.demo.golem-base.io", + log ), kaolin: await createClient( 600606, key, - 'https://rpc.kaolin.holesky.golem-base.io', - 'wss://ws.rpc.kaolin.holesky.golem-base.io', - log, + "https://rpc.kaolin.holesky.golem-base.io", + "wss://ws.rpc.kaolin.holesky.golem-base.io", + log ), - }.kaolin + }.kaolin; async function numOfEntitiesOwned(): Promise { - return (await client.getEntitiesOfOwner(await client.getOwnerAddress())).length + return (await client.getEntitiesOfOwner(await client.getOwnerAddress())) + .length; } - const block = await client.getRawClient().httpClient.getBlockNumber() + const block = await client.getRawClient().httpClient.getBlockNumber(); const unsubscribe = client.watchLogs({ fromBlock: block, onCreated: (args) => { - log.info("Got creation event:", args) + log.info("Got creation event:", args); }, onUpdated: (args) => { - log.info("Got update event:", args) + log.info("Got update event:", args); }, onExtended: (args) => { - log.info("Got extension event:", args) + log.info("Got extension event:", args); }, onDeleted: (args) => { - log.info("Got deletion event:", args) + log.info("Got deletion event:", args); }, onError: (error) => { - log.error("Got error:", error) + log.error("Got error:", error); }, pollingInterval: 500, transport: "http", - }) + }); + + log.info("Address used:", await client.getOwnerAddress()); + log.info("Number of entities owned:", await numOfEntitiesOwned()); - log.info("Address used:", await client.getOwnerAddress()) - log.info("Number of entities owned:", await numOfEntitiesOwned()) + log.info(""); + log.info("*********************"); + log.info("* Creating entities *"); + log.info("*********************"); + log.info(""); - log.info("") - log.info("*********************") - log.info("* Creating entities *") - log.info("*********************") - log.info("") + const ownerAddress = await client.getOwnerAddress(); + log.info("Owner address validation:", isValidAddress(ownerAddress)); const creates: GolemBaseCreate[] = [ { data: encoder.encode("foo"), - btl: 25, - stringAnnotations: [new Annotation("key", "foo")], - numericAnnotations: [new Annotation("ix", 1)] + btl: BTL_PRESETS.SHORT, + stringAnnotations: [ + Annotation.createString(ANNOTATION_KEYS.TYPE, "example"), + new Annotation("key", "foo"), + ], + numericAnnotations: [ + Annotation.createTimestamp(), + new Annotation("ix", 1), + ], }, { data: encoder.encode("bar"), btl: 2, - stringAnnotations: [new Annotation("key", "bar")], - numericAnnotations: [new Annotation("ix", 2)] + stringAnnotations: [ + Annotation.createString(ANNOTATION_KEYS.TYPE, "example"), + new Annotation("key", "bar"), + ], + numericAnnotations: [ + Annotation.createNumeric(ANNOTATION_KEYS.PRIORITY, 1), + new Annotation("ix", 2), + ], }, { data: encoder.encode("qux"), - btl: 50, - stringAnnotations: [new Annotation("key", "qux")], - numericAnnotations: [new Annotation("ix", 2)] - } - ] - const receipts = await client.createEntities(creates) - - log.info("Number of entities owned:", await numOfEntitiesOwned()) - - log.info("") - log.info("*************************") - log.info("* Deleting first entity *") - log.info("*************************") - log.info("") - - await client.deleteEntities([receipts[0].entityKey]) - log.info("Number of entities owned:", await numOfEntitiesOwned()) - - log.info("") - log.info("*****************************") - log.info("* Updating the third entity *") - log.info("*****************************") - log.info("") + btl: BTL_PRESETS.MEDIUM, + stringAnnotations: [ + Annotation.createString(ANNOTATION_KEYS.TYPE, "example"), + new Annotation("key", "qux"), + ], + numericAnnotations: [ + Annotation.createTimestamp("created_at"), + new Annotation("ix", 2), + ], + }, + ]; + const receipts = await client.createEntities(creates); + + // Demonstrate validation of created entity keys + receipts.forEach((receipt, index) => { + log.info(`Created entity ${index + 1}:`); + log.info(` Key: ${receipt.entityKey}`); + log.info(` Valid key format: ${isValidEntityKey(receipt.entityKey)}`); + log.info(` Expires at block: ${receipt.expirationBlock}`); + }); + + log.info("Number of entities owned:", await numOfEntitiesOwned()); + + log.info(""); + log.info("*************************"); + log.info("* Deleting first entity *"); + log.info("*************************"); + log.info(""); + + await client.deleteEntities([receipts[0].entityKey]); + log.info("Number of entities owned:", await numOfEntitiesOwned()); + + log.info(""); + log.info("*****************************"); + log.info("* Updating the third entity *"); + log.info("*****************************"); + log.info(""); log.info( "The third entity before the update:", await client.getEntityMetaData(receipts[2].entityKey), "\nStorage value:", - decoder.decode(await client.getStorageValue(receipts[2].entityKey)), - ) - - log.info("Updating the entity...") - await client.updateEntities([{ - entityKey: receipts[2].entityKey, - btl: 40, - data: encoder.encode("foobar"), - stringAnnotations: [new Annotation("key", "qux"), new Annotation("foo", "bar")], - numericAnnotations: [new Annotation("ix", 2)] - }]) + decoder.decode(await client.getStorageValue(receipts[2].entityKey)) + ); + + log.info("Updating the entity..."); + await client.updateEntities([ + { + entityKey: receipts[2].entityKey, + btl: 40, + data: encoder.encode("foobar"), + stringAnnotations: [ + new Annotation("key", "qux"), + new Annotation("foo", "bar"), + ], + numericAnnotations: [new Annotation("ix", 2)], + }, + ]); log.info( "The third entity after the update:", await client.getEntityMetaData(receipts[2].entityKey), "\nStorage value:", - decoder.decode(await client.getStorageValue(receipts[2].entityKey)), - ) + decoder.decode(await client.getStorageValue(receipts[2].entityKey)) + ); - log.info("Number of entities owned:", await numOfEntitiesOwned()) + log.info("Number of entities owned:", await numOfEntitiesOwned()); - log.info("") - log.info("*****************************************") - log.info("* Extending the BTL of the third entity *") - log.info("*****************************************") - log.info("") + log.info(""); + log.info("*****************************************"); + log.info("* Extending the BTL of the third entity *"); + log.info("*****************************************"); + log.info(""); log.info( "The third entity before the extension:", await client.getEntityMetaData(receipts[2].entityKey), "\nStorage value:", - decoder.decode(await client.getStorageValue(receipts[2].entityKey)), - ) + decoder.decode(await client.getStorageValue(receipts[2].entityKey)) + ); - log.info("Extending the BTL of the entity...") - await client.extendEntities([{ - entityKey: receipts[2].entityKey, - numberOfBlocks: 40, - }]) + log.info("Extending the BTL of the entity..."); + await client.extendEntities([ + { + entityKey: receipts[2].entityKey, + numberOfBlocks: 40, + }, + ]); log.info( "The third entity after the extension:", await client.getEntityMetaData(receipts[2].entityKey), "\nStorage value:", - decoder.decode(await client.getStorageValue(receipts[2].entityKey)), - ) + decoder.decode(await client.getStorageValue(receipts[2].entityKey)) + ); - log.info("Number of entities owned:", await numOfEntitiesOwned()) + log.info("Number of entities owned:", await numOfEntitiesOwned()); - log.info("") - log.info("*******************************") - log.info("* Deleting remaining entities *") - log.info("*******************************") - log.info("") + log.info(""); + log.info("*******************************"); + log.info("* Deleting remaining entities *"); + log.info("*******************************"); + log.info(""); // Figure out whether we still need to delete anything - const toDelete = (await asyncFilter( - await client.queryEntities("ix = 1 || ix = 2 || ix = 3"), - async result => { - const metadata = await client.getEntityMetaData(result.entityKey) - return metadata.owner === (await client.getOwnerAddress()).toLocaleLowerCase() - } - )).map(result => result.entityKey) - - log.info("Entities to delete:", toDelete) + const toDelete = ( + await asyncFilter( + await client.queryEntities("ix = 1 || ix = 2 || ix = 3"), + async (result) => { + const metadata = await client.getEntityMetaData(result.entityKey); + return ( + metadata.owner === + (await client.getOwnerAddress()).toLocaleLowerCase() + ); + } + ) + ).map((result) => result.entityKey); + + log.info("Entities to delete:", toDelete); if (toDelete.length !== 0) { - await client.deleteEntities(toDelete) + await client.deleteEntities(toDelete); } - log.info("Number of entities owned:", await numOfEntitiesOwned()) - - log.info("Current balance: ", formatEther(await client.getRawClient().httpClient.getBalance({ - address: await client.getOwnerAddress(), - blockTag: 'latest' - }))) + log.info("Number of entities owned:", await numOfEntitiesOwned()); - await (new Promise(resolve => setTimeout(resolve, 500))) - - unsubscribe() + log.info( + "Current balance: ", + formatEther( + await client.getRawClient().httpClient.getBalance({ + address: await client.getOwnerAddress(), + blockTag: "latest", + }) + ) + ); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + unsubscribe(); } -main() +main(); diff --git a/src/index.ts b/src/index.ts index 1d7dfb3..a7a4306 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,9 @@ import { // Export all high-level client functionality export * from "./client" +// Export utility functions for validation and common operations +export * from "./utils" + // Export internal implementation for advanced use cases export * as internal from "./internal/client" @@ -110,6 +113,60 @@ export class Annotation { this.key = key this.value = value } + + /** + * Creates a string-valued annotation. + * + * @param key - The annotation key + * @param value - The string value + * @returns A new StringAnnotation instance + * + * @example + * ```typescript + * const typeAnnotation = Annotation.createString("type", "user-profile"); + * ``` + * + * @public + */ + static createString(key: string, value: string): StringAnnotation { + return new Annotation(key, value) + } + + /** + * Creates a numeric-valued annotation. + * + * @param key - The annotation key + * @param value - The numeric value + * @returns A new NumericAnnotation instance + * + * @example + * ```typescript + * const priorityAnnotation = Annotation.createNumeric("priority", 1); + * ``` + * + * @public + */ + static createNumeric(key: string, value: number): NumericAnnotation { + return new Annotation(key, value) + } + + /** + * Creates a timestamp annotation with the current time. + * + * @param key - The annotation key (defaults to "timestamp") + * @returns A new NumericAnnotation with current timestamp + * + * @example + * ```typescript + * const timestampAnnotation = Annotation.createTimestamp(); + * const customTimestamp = Annotation.createTimestamp("created_at"); + * ``` + * + * @public + */ + static createTimestamp(key: string = "timestamp"): NumericAnnotation { + return new Annotation(key, Date.now()) + } } /** diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5fabc0e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,215 @@ +/** + * @fileoverview Utility functions for GolemBase TypeScript SDK + * + * This module provides validation helpers and utility functions for working with + * GolemBase entities, addresses, and data types. + * + * @author Golem Base Community + * @version 1.0.0 + */ + +import { type Hex } from "./index" + +/** + * Validates if a string is a properly formatted hexadecimal value. + * + * A valid hex string must: + * - Start with '0x' + * - Contain only valid hexadecimal characters (0-9, a-f, A-F) + * - Have an even length (excluding '0x' prefix) + * + * @param value - The string to validate + * @returns True if the string is a valid hex format + * + * @example + * ```typescript + * isValidHex("0x1234abcd"); // true + * isValidHex("0x123"); // false (odd length) + * isValidHex("1234abcd"); // false (no '0x' prefix) + * isValidHex("0xGGHH"); // false (invalid characters) + * ``` + * + * @public + */ +export function isValidHex(value: string): value is Hex { + if (typeof value !== 'string') { + return false; + } + + // Must start with '0x' + if (!value.startsWith('0x')) { + return false; + } + + // Must have even length after '0x' (each byte = 2 hex chars) + const hexPart = value.slice(2); + if (hexPart.length % 2 !== 0 || hexPart.length === 0) { + return false; + } + + // Must contain only valid hex characters + return /^[0-9a-fA-F]*$/.test(hexPart); +} + +/** + * Validates if a string is a properly formatted GolemBase entity key. + * + * A valid entity key must: + * - Be a valid hex string (see isValidHex) + * - Be exactly 32 bytes long (66 characters including '0x') + * + * @param key - The string to validate as an entity key + * @returns True if the string is a valid entity key format + * + * @example + * ```typescript + * const validKey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + * isValidEntityKey(validKey); // true + * + * isValidEntityKey("0x1234"); // false (too short) + * isValidEntityKey("invalid"); // false (not hex) + * ``` + * + * @public + */ +export function isValidEntityKey(key: string): key is Hex { + return isValidHex(key) && key.length === 66; // 2 + 64 = 66 chars for 32 bytes +} + +/** + * Validates if a string is a properly formatted Ethereum address. + * + * A valid Ethereum address must: + * - Be a valid hex string (see isValidHex) + * - Be exactly 20 bytes long (42 characters including '0x') + * + * @param address - The string to validate as an Ethereum address + * @returns True if the string is a valid Ethereum address format + * + * @example + * ```typescript + * const validAddress = "0x742d35Cc9e1e3FbD000de0e98a3b8b8c0d3b2F8e"; + * isValidAddress(validAddress); // true + * + * isValidAddress("0x742d35Cc"); // false (too short) + * isValidAddress("742d35Cc9e1e3FbD000de0e98a3b8b8c0d3b2F8e"); // false (no '0x') + * ``` + * + * @public + */ +export function isValidAddress(address: string): address is Hex { + return isValidHex(address) && address.length === 42; // 2 + 40 = 42 chars for 20 bytes +} + +/** + * Validates if a string is a valid annotation key. + * + * A valid annotation key must: + * - Be a non-empty string + * - Not consist only of whitespace + * - Be a string type + * + * @param key - The string to validate as an annotation key + * @returns True if the string is a valid annotation key + * + * @example + * ```typescript + * isValidAnnotationKey("type"); // true + * isValidAnnotationKey("my-key"); // true + * isValidAnnotationKey(""); // false + * isValidAnnotationKey(" "); // false + * isValidAnnotationKey(123); // false + * ``` + * + * @public + */ +export function isValidAnnotationKey(key: any): key is string { + return typeof key === 'string' && key.trim().length > 0; +} + +/** + * Validates if a BTL (Block-to-Live) value is within acceptable bounds. + * + * A valid BTL must: + * - Be a positive integer + * - Be greater than 0 + * - Be less than or equal to the maximum safe integer + * + * @param btl - The BTL value to validate + * @returns True if the BTL is valid + * + * @example + * ```typescript + * isValidBTL(1000); // true + * isValidBTL(0); // false + * isValidBTL(-100); // false + * isValidBTL(1.5); // false + * ``` + * + * @public + */ +export function isValidBTL(btl: any): btl is number { + return typeof btl === 'number' && + Number.isInteger(btl) && + btl > 0 && + btl <= Number.MAX_SAFE_INTEGER; +} + +/** + * Predefined BTL (Block-to-Live) values for common use cases. + * Based on ~12 second block times (Ethereum average). + * + * @public + */ +export const BTL_PRESETS = { + /** Short-lived entities: ~15 minutes (100 blocks) */ + SHORT: 100, + /** Medium-lived entities: ~4 hours (1,200 blocks) */ + MEDIUM: 1200, + /** Long-lived entities: ~1 day (7,200 blocks) */ + LONG: 7200, + /** Very long-lived entities: ~30 days (216,000 blocks) */ + PERMANENT: 216000, +} as const; + +/** + * Commonly used annotation keys for consistent metadata labeling. + * + * @public + */ +export const ANNOTATION_KEYS = { + /** Entity type classification */ + TYPE: 'type', + /** Entity category for grouping */ + CATEGORY: 'category', + /** Priority level (typically numeric) */ + PRIORITY: 'priority', + /** Creation or modification timestamp */ + TIMESTAMP: 'timestamp', + /** Entity version identifier */ + VERSION: 'version', + /** Current status of the entity */ + STATUS: 'status', + /** Entity owner or creator */ + OWNER: 'owner', + /** Tags for easy searching */ + TAG: 'tag', +} as const; + +/** + * Type guard to check if a value is a valid BTL preset. + * + * @param value - The value to check + * @returns True if the value is one of the predefined BTL presets + * + * @example + * ```typescript + * isBTLPreset(BTL_PRESETS.SHORT); // true + * isBTLPreset(1000); // false + * ``` + * + * @public + */ +export function isBTLPreset(value: number): value is typeof BTL_PRESETS[keyof typeof BTL_PRESETS] { + return Object.values(BTL_PRESETS).includes(value as any); +} diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000..2d4e887 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,264 @@ +import { expect } from "chai" +import { describe, it } from "node:test" +import { + isValidHex, + isValidEntityKey, + isValidAddress, + isValidAnnotationKey, + isValidBTL, + BTL_PRESETS, + ANNOTATION_KEYS, + isBTLPreset, + Annotation, +} from "../src/index" +import { + generateRandomEntityKey, + generateRandomAddress, + createTestStringAnnotation, + createTestNumericAnnotation, + generateRandomBTL, +} from "./utils" + +describe("GolemBase SDK Utils", () => { + describe("isValidHex", () => { + it("should validate correct hex strings", () => { + expect(isValidHex("0x1234")).to.be.true + expect(isValidHex("0xabcdef")).to.be.true + expect(isValidHex("0xABCDEF")).to.be.true + expect(isValidHex("0x1234567890abcdef")).to.be.true + }) + + it("should reject invalid hex strings", () => { + expect(isValidHex("1234")).to.be.false // no 0x prefix + expect(isValidHex("0x123")).to.be.false // odd length + expect(isValidHex("0xGHIJ")).to.be.false // invalid characters + expect(isValidHex("")).to.be.false // empty string + expect(isValidHex("0x")).to.be.false // just prefix + }) + + it("should reject non-string inputs", () => { + expect(isValidHex(123 as any)).to.be.false + expect(isValidHex(null as any)).to.be.false + expect(isValidHex(undefined as any)).to.be.false + }) + }) + + describe("isValidEntityKey", () => { + it("should validate 32-byte hex strings", () => { + const validKey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + expect(isValidEntityKey(validKey)).to.be.true + }) + + it("should reject incorrect length hex strings", () => { + expect(isValidEntityKey("0x1234")).to.be.false // too short + expect(isValidEntityKey("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12")).to.be.false // too long + }) + + it("should reject invalid hex strings", () => { + expect(isValidEntityKey("invalid")).to.be.false + expect(isValidEntityKey("")).to.be.false + }) + + it("should work with generated random keys", () => { + for (let i = 0; i < 10; i++) { + const key = generateRandomEntityKey() + expect(isValidEntityKey(key)).to.be.true + } + }) + }) + + describe("isValidAddress", () => { + it("should validate 20-byte hex strings", () => { + const validAddress = "0x742d35Cc9e1e3FbD000de0e98a3b8b8c0d3b2F8e" + expect(isValidAddress(validAddress)).to.be.true + }) + + it("should reject incorrect length hex strings", () => { + expect(isValidAddress("0x1234")).to.be.false // too short + expect(isValidAddress("0x742d35Cc9e1e3FbD000de0e98a3b8b8c0d3b2F8e12")).to.be.false // too long + }) + + it("should work with generated random addresses", () => { + for (let i = 0; i < 10; i++) { + const address = generateRandomAddress() + expect(isValidAddress(address)).to.be.true + } + }) + }) + + describe("isValidAnnotationKey", () => { + it("should validate proper string keys", () => { + expect(isValidAnnotationKey("type")).to.be.true + expect(isValidAnnotationKey("my-key")).to.be.true + expect(isValidAnnotationKey("key123")).to.be.true + expect(isValidAnnotationKey("a")).to.be.true + }) + + it("should reject empty or whitespace-only keys", () => { + expect(isValidAnnotationKey("")).to.be.false + expect(isValidAnnotationKey(" ")).to.be.false + expect(isValidAnnotationKey("\t\n")).to.be.false + }) + + it("should reject non-string inputs", () => { + expect(isValidAnnotationKey(123)).to.be.false + expect(isValidAnnotationKey(null)).to.be.false + expect(isValidAnnotationKey(undefined)).to.be.false + expect(isValidAnnotationKey({})).to.be.false + }) + }) + + describe("isValidBTL", () => { + it("should validate positive integers", () => { + expect(isValidBTL(1)).to.be.true + expect(isValidBTL(100)).to.be.true + expect(isValidBTL(1000)).to.be.true + }) + + it("should reject zero and negative numbers", () => { + expect(isValidBTL(0)).to.be.false + expect(isValidBTL(-1)).to.be.false + expect(isValidBTL(-100)).to.be.false + }) + + it("should reject non-integer numbers", () => { + expect(isValidBTL(1.5)).to.be.false + expect(isValidBTL(3.14159)).to.be.false + }) + + it("should reject non-number inputs", () => { + expect(isValidBTL("100")).to.be.false + expect(isValidBTL(null)).to.be.false + expect(isValidBTL(undefined)).to.be.false + }) + + it("should work with generated random BTL values", () => { + for (let i = 0; i < 10; i++) { + const btl = generateRandomBTL() + expect(isValidBTL(btl)).to.be.true + } + }) + }) + + describe("BTL_PRESETS", () => { + it("should contain expected preset values", () => { + expect(BTL_PRESETS.SHORT).to.equal(100) + expect(BTL_PRESETS.MEDIUM).to.equal(1200) + expect(BTL_PRESETS.LONG).to.equal(7200) + expect(BTL_PRESETS.PERMANENT).to.equal(216000) + }) + + it("all presets should be valid BTL values", () => { + Object.values(BTL_PRESETS).forEach(preset => { + expect(isValidBTL(preset)).to.be.true + }) + }) + }) + + describe("isBTLPreset", () => { + it("should recognize preset values", () => { + expect(isBTLPreset(BTL_PRESETS.SHORT)).to.be.true + expect(isBTLPreset(BTL_PRESETS.MEDIUM)).to.be.true + expect(isBTLPreset(BTL_PRESETS.LONG)).to.be.true + expect(isBTLPreset(BTL_PRESETS.PERMANENT)).to.be.true + }) + + it("should reject non-preset values", () => { + expect(isBTLPreset(999)).to.be.false + expect(isBTLPreset(1)).to.be.false + expect(isBTLPreset(50000)).to.be.false + }) + }) + + describe("ANNOTATION_KEYS", () => { + it("should contain expected key constants", () => { + expect(ANNOTATION_KEYS.TYPE).to.equal('type') + expect(ANNOTATION_KEYS.CATEGORY).to.equal('category') + expect(ANNOTATION_KEYS.PRIORITY).to.equal('priority') + expect(ANNOTATION_KEYS.TIMESTAMP).to.equal('timestamp') + expect(ANNOTATION_KEYS.VERSION).to.equal('version') + expect(ANNOTATION_KEYS.STATUS).to.equal('status') + expect(ANNOTATION_KEYS.OWNER).to.equal('owner') + expect(ANNOTATION_KEYS.TAG).to.equal('tag') + }) + + it("all annotation keys should be valid", () => { + Object.values(ANNOTATION_KEYS).forEach(key => { + expect(isValidAnnotationKey(key)).to.be.true + }) + }) + }) + + describe("Annotation static methods", () => { + describe("createString", () => { + it("should create string annotations", () => { + const annotation = Annotation.createString("type", "test") + expect(annotation.key).to.equal("type") + expect(annotation.value).to.equal("test") + }) + }) + + describe("createNumeric", () => { + it("should create numeric annotations", () => { + const annotation = Annotation.createNumeric("priority", 5) + expect(annotation.key).to.equal("priority") + expect(annotation.value).to.equal(5) + }) + }) + + describe("createTimestamp", () => { + it("should create timestamp annotation with default key", () => { + const annotation = Annotation.createTimestamp() + expect(annotation.key).to.equal("timestamp") + expect(typeof annotation.value).to.equal("number") + expect(annotation.value).to.be.greaterThan(0) + }) + + it("should create timestamp annotation with custom key", () => { + const annotation = Annotation.createTimestamp("created_at") + expect(annotation.key).to.equal("created_at") + expect(typeof annotation.value).to.equal("number") + }) + + it("should create timestamps close to current time", () => { + const before = Date.now() + const annotation = Annotation.createTimestamp() + const after = Date.now() + + expect(annotation.value).to.be.at.least(before) + expect(annotation.value).to.be.at.most(after) + }) + }) + }) + + describe("Test utilities", () => { + describe("createTestStringAnnotation", () => { + it("should create random annotations when no params provided", () => { + const annotation = createTestStringAnnotation() + expect(isValidAnnotationKey(annotation.key)).to.be.true + expect(typeof annotation.value).to.equal("string") + expect(annotation.value.length).to.be.greaterThan(0) + }) + + it("should use provided key and value", () => { + const annotation = createTestStringAnnotation("test_key", "test_value") + expect(annotation.key).to.equal("test_key") + expect(annotation.value).to.equal("test_value") + }) + }) + + describe("createTestNumericAnnotation", () => { + it("should create random annotations when no params provided", () => { + const annotation = createTestNumericAnnotation() + expect(isValidAnnotationKey(annotation.key)).to.be.true + expect(typeof annotation.value).to.equal("number") + }) + + it("should use provided key and value", () => { + const annotation = createTestNumericAnnotation("test_key", 42) + expect(annotation.key).to.equal("test_key") + expect(annotation.value).to.equal(42) + }) + }) + }) +}) diff --git a/test/utils.ts b/test/utils.ts index 91b2a63..08b7266 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,12 +1,97 @@ +import { Annotation, type Hex } from "../src/index"; + export function generateRandomBytes(length: number): Uint8Array { - return new TextEncoder().encode(generateRandomString(length)) + return new TextEncoder().encode(generateRandomString(length)); } export function generateRandomString(length: number): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; } + +/** + * Generates a random valid entity key (32 bytes as hex string). + * + * @returns A valid entity key in hex format + */ +export function generateRandomEntityKey(): Hex { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + return `0x${Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join("")}` as Hex; +} + +/** + * Generates a random valid Ethereum address (20 bytes as hex string). + * + * @returns A valid Ethereum address in hex format + */ +export function generateRandomAddress(): Hex { + const bytes = crypto.getRandomValues(new Uint8Array(20)); + return `0x${Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join("")}` as Hex; +} + +/** + * Creates a test string annotation with random or provided values. + * + * @param key - The annotation key (optional, generates random if not provided) + * @param value - The annotation value (optional, generates random if not provided) + * @returns A new string annotation + */ +export function createTestStringAnnotation( + key?: string, + value?: string +): Annotation { + return new Annotation( + key ?? `test_key_${generateRandomString(8)}`, + value ?? generateRandomString(16) + ); +} + +/** + * Creates a test numeric annotation with random or provided values. + * + * @param key - The annotation key (optional, generates random if not provided) + * @param value - The annotation value (optional, generates random if not provided) + * @returns A new numeric annotation + */ +export function createTestNumericAnnotation( + key?: string, + value?: number +): Annotation { + return new Annotation( + key ?? `test_num_${generateRandomString(8)}`, + value ?? Math.floor(Math.random() * 1000) + ); +} + +/** + * Creates test data for GolemBase entity creation. + * + * @param size - Size of the data in bytes (default: 32) + * @returns Random test data as Uint8Array + */ +export function createTestEntityData(size: number = 32): Uint8Array { + return generateRandomBytes(size); +} + +/** + * Generates a random valid BTL (Block-to-Live) value. + * + * @param min - Minimum BTL value (default: 1) + * @param max - Maximum BTL value (default: 10000) + * @returns A random BTL value within the specified range + */ +export function generateRandomBTL( + min: number = 1, + max: number = 10000 +): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +}