From b570d26896306f0bc9e1ed6f2fade72cece933ec Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 23 Oct 2025 00:04:05 -0300 Subject: [PATCH 01/10] feat: first version with type generation for clients --- .trix/client-lib/protocol.ts.hbs | 25 +- package-lock.json | 2 +- packages/tx3-sdk/src/trp/args.ts | 163 +++++---- packages/tx3-sdk/src/trp/client.ts | 77 ++-- packages/tx3-sdk/src/trp/types.ts | 330 ++++++++++++++---- .../tx3-sdk/tests/custom-argvalue.test.ts | 219 ++++++++++++ 6 files changed, 655 insertions(+), 161 deletions(-) create mode 100644 packages/tx3-sdk/tests/custom-argvalue.test.ts diff --git a/.trix/client-lib/protocol.ts.hbs b/.trix/client-lib/protocol.ts.hbs index cc580a7..d0dfc12 100644 --- a/.trix/client-lib/protocol.ts.hbs +++ b/.trix/client-lib/protocol.ts.hbs @@ -6,6 +6,14 @@ import { type ClientOptions, type SubmitParams, type ResolveResponse, + type ArgValueInt, + type ArgValueBool, + type ArgValueString, + type ArgValueBytes, + type ArgValueAddress, + type ArgValueUtxoSet, + type ArgValueUtxoRef, + CustomArgValue, } from "tx3-sdk/trp"; @@ -23,10 +31,23 @@ export const DEFAULT_ENV_ARGS = { {{/each}} }; +// Custom type definitions +{{#each transactions}} +{{#each parameters}} +{{#if (hasCustomDef this)}} +{{#if (getCustomTypeName this)}} +// Custom type definition for {{getCustomTypeName this}} +export type {{getCustomTypeNamePascal this}} = {{generateCustomType this}}; + +{{/if}} +{{/if}} +{{/each}} +{{/each}} + {{#each transactions}} export type {{pascalCase params_name}} = { {{#each parameters}} - {{camelCase name}}: ArgValue | {{typeFor type_name "typescript"}}; // {{type_name}} + {{camelCase name}}: {{typeFor type_name "typescript"}}; // {{type_name}} {{/each}} } @@ -63,4 +84,4 @@ export const protocol = new Client({ endpoint: DEFAULT_TRP_ENDPOINT, headers: DEFAULT_HEADERS, envArgs: DEFAULT_ENV_ARGS, -}); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d19daa9..f2aa9ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10555,7 +10555,7 @@ } }, "packages/tx3-sdk": { - "version": "0.4.0", + "version": "0.6.0", "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-commonjs": "^28.0.2", diff --git a/packages/tx3-sdk/src/trp/args.ts b/packages/tx3-sdk/src/trp/args.ts index df38e41..060e955 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -1,22 +1,30 @@ -import { bech32 } from 'bech32'; +import { bech32 } from "bech32"; -import { ArgValue, BytesEnvelope, Type, UtxoRef, ArgValueError } from './types.js'; +import { + PrimitiveArgValue, + BytesEnvelope, + Type, + UtxoRef, + ArgValueError, + CustomArgValue, + ArgValue, +} from "./types.js"; const MIN_I128 = -(BigInt(2) ** BigInt(127)); const MAX_I128 = BigInt(2) ** BigInt(127) - BigInt(1); // Helper functions for encoding/decoding function hexToBytes(s: string): Uint8Array { - const cleanHex = s.startsWith('0x') ? s.slice(2) : s; + const cleanHex = s.startsWith("0x") ? s.slice(2) : s; if (cleanHex.length % 2 !== 0) { throw new ArgValueError(`Invalid hex string: ${s}`); } - + // Validate hex characters if (!/^[0-9a-fA-F]*$/.test(cleanHex)) { throw new ArgValueError(`Invalid hex string: ${s}`); } - + const bytes = new Uint8Array(cleanHex.length / 2); for (let i = 0; i < cleanHex.length; i += 2) { const hexByte = cleanHex.substring(i, i + 2); @@ -31,8 +39,8 @@ function hexToBytes(s: string): Uint8Array { function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } function base64ToBytes(s: string): Uint8Array { @@ -49,18 +57,21 @@ function base64ToBytes(s: string): Uint8Array { } function bigintToValue(i: bigint): any { - if (i >= BigInt(Number.MIN_SAFE_INTEGER) && i <= BigInt(Number.MAX_SAFE_INTEGER)) { + if ( + i >= BigInt(Number.MIN_SAFE_INTEGER) && + i <= BigInt(Number.MAX_SAFE_INTEGER) + ) { return Number(i); } else { const bytes = new Uint8Array(16); let value = i < 0 ? -i : i; const isNegative = i < 0; - + for (let j = 15; j >= 0; j--) { bytes[j] = Number(value & BigInt(0xff)); value = value >> BigInt(8); } - + if (isNegative) { // Two's complement for negative numbers for (let j = 0; j < 16; j++) { @@ -73,7 +84,7 @@ function bigintToValue(i: bigint): any { carry = sum >> 8; } } - + return `0x${bytesToHex(bytes)}`; } } @@ -92,9 +103,9 @@ function numberToBigint(x: number): bigint { } const bigintValue = BigInt(x); - + checkBigintInRange(bigintValue); - + return bigintValue; } @@ -103,47 +114,47 @@ function stringToBigint(s: string): bigint { if (bytes.length !== 16) { throw new ArgValueError(`Invalid bytes for number: ${s}`); } - + let result = BigInt(0); for (let i = 0; i < 16; i++) { result = (result << BigInt(8)) | BigInt(bytes[i]); } - + // Check if it's a negative number (two's complement) if (bytes[0] & 0x80) { // Convert from two's complement result = result - (BigInt(1) << BigInt(128)); } - + return result; } function valueToBigint(value: any): bigint { console.log(value, typeof value); - if (typeof value === 'number') { + if (typeof value === "number") { return numberToBigint(value); - } else if (typeof value === 'bigint') { + } else if (typeof value === "bigint") { checkBigintInRange(value); return value; - } else if (typeof value === 'string') { + } else if (typeof value === "string") { return stringToBigint(value); } else if (value === null) { - throw new ArgValueError('Value is null'); + throw new ArgValueError("Value is null"); } else { throw new ArgValueError(`Value is not a number: ${value}`); } } function valueToBool(value: any): boolean { - if (typeof value === 'boolean') { + if (typeof value === "boolean") { return value; - } else if (typeof value === 'number') { + } else if (typeof value === "number") { if (value === 0) return false; if (value === 1) return true; throw new ArgValueError(`Invalid number for boolean: ${value}`); - } else if (typeof value === 'string') { - if (value === 'true') return true; - if (value === 'false') return false; + } else if (typeof value === "string") { + if (value === "true") return true; + if (value === "false") return false; throw new ArgValueError(`Invalid string for boolean: ${value}`); } else { throw new ArgValueError(`Value is not a bool: ${value}`); @@ -153,14 +164,19 @@ function valueToBool(value: any): boolean { function valueToBytes(value: any): Uint8Array { if (value instanceof Uint8Array) { return value; - } else if (typeof value === 'string') { + } else if (typeof value === "string") { return hexToBytes(value); - } else if (value && typeof value === 'object' && 'content' in value && 'encoding' in value) { + } else if ( + value && + typeof value === "object" && + "content" in value && + "encoding" in value + ) { const envelope = value as BytesEnvelope; switch (envelope.encoding) { - case 'base64': + case "base64": return base64ToBytes(envelope.content); - case 'hex': + case "hex": return hexToBytes(envelope.content); default: throw new ArgValueError(`Unknown encoding: ${envelope.encoding}`); @@ -177,7 +193,7 @@ function bech32ToBytes(value: string): Uint8Array { } function valueToAddress(value: any): Uint8Array { - if (typeof value === 'string') { + if (typeof value === "string") { try { return bech32ToBytes(value); } catch { @@ -188,37 +204,37 @@ function valueToAddress(value: any): Uint8Array { } } -function valueToUndefined(value: any): ArgValue { - if (typeof value === 'boolean') { - return { type: 'Bool', value }; - } else if (typeof value === 'number') { - return { type: 'Int', value: numberToBigint(value) }; - } else if (typeof value === 'string') { - return { type: 'String', value }; +function valueToUndefined(value: any): PrimitiveArgValue { + if (typeof value === "boolean") { + return { type: "Bool", value }; + } else if (typeof value === "number") { + return { type: "Int", value: numberToBigint(value) }; + } else if (typeof value === "string") { + return { type: "String", value }; } else { throw new ArgValueError(`Can't infer type for value: ${value}`); } } function stringToUtxoRef(s: string): UtxoRef { - const parts = s.split('#'); + const parts = s.split("#"); if (parts.length !== 2) { throw new ArgValueError(`Invalid utxo ref: ${s}`); } - + const [txidHex, indexStr] = parts; const txid = hexToBytes(txidHex); const index = parseInt(indexStr, 10); - + if (isNaN(index)) { throw new ArgValueError(`Invalid utxo ref: ${s}`); } - + return { txid, index }; } function valueToUtxoRef(value: any): UtxoRef { - if (typeof value === 'string') { + if (typeof value === "string") { return stringToUtxoRef(value); } else { throw new ArgValueError(`Value is not utxo ref: ${value}`); @@ -229,45 +245,55 @@ function utxoRefToValue(x: UtxoRef): string { return `${bytesToHex(x.txid)}#${x.index}`; } -export function toJson(value: ArgValue): any { +export function toJson(value: PrimitiveArgValue | CustomArgValue): any { + // recursively turn fields into json + if (value instanceof CustomArgValue) { + const result: Record = {}; + for (const [key, val] of Object.entries(value.value)) { + result[key] = toJson(val); + } + return result; + } + + // Handle PrimitiveArgValue switch (value.type) { - case 'Int': + case "Int": return bigintToValue(value.value); - case 'Bool': + case "Bool": return value.value; - case 'String': + case "String": return value.value; - case 'Bytes': + case "Bytes": return `0x${bytesToHex(value.value)}`; - case 'Address': + case "Address": return bytesToHex(value.value); - case 'UtxoSet': - return Array.from(value.value).map(utxo => ({ + case "UtxoSet": + return Array.from(value.value).map((utxo) => ({ ref: utxoRefToValue(utxo.ref), address: bytesToHex(utxo.address), datum: utxo.datum, assets: utxo.assets, - script: utxo.script + script: utxo.script, })); - case 'UtxoRef': + case "UtxoRef": return utxoRefToValue(value.value); default: throw new ArgValueError(`Unknown ArgValue type: ${(value as any).type}`); } } -export function fromJson(value: any, target: Type): ArgValue { +export function fromJson(value: any, target: Type): PrimitiveArgValue { switch (target) { case Type.Int: - return { type: 'Int', value: valueToBigint(value) }; + return { type: "Int", value: valueToBigint(value) }; case Type.Bool: - return { type: 'Bool', value: valueToBool(value) }; + return { type: "Bool", value: valueToBool(value) }; case Type.Bytes: - return { type: 'Bytes', value: valueToBytes(value) }; + return { type: "Bytes", value: valueToBytes(value) }; case Type.Address: - return { type: 'Address', value: valueToAddress(value) }; + return { type: "Address", value: valueToAddress(value) }; case Type.UtxoRef: - return { type: 'UtxoRef', value: valueToUtxoRef(value) }; + return { type: "UtxoRef", value: valueToUtxoRef(value) }; case Type.Undefined: return valueToUndefined(value); default: @@ -276,29 +302,38 @@ export function fromJson(value: any, target: Type): ArgValue { } // Helper functions to create ArgValue instances -export function createIntArg(value: number | bigint): ArgValue { +export function createIntArg(value: number | bigint): PrimitiveArgValue { return ArgValue.fromNumber(value); } -export function createBoolArg(value: boolean): ArgValue { +export function createBoolArg(value: boolean): PrimitiveArgValue { return ArgValue.fromBool(value); } -export function createStringArg(value: string): ArgValue { +export function createStringArg(value: string): PrimitiveArgValue { return ArgValue.fromString(value); } -export function createBytesArg(value: Uint8Array): ArgValue { +export function createBytesArg(value: Uint8Array): PrimitiveArgValue { return ArgValue.fromBytes(value); } -export function createAddressArg(value: Uint8Array): ArgValue { +export function createAddressArg(value: Uint8Array): PrimitiveArgValue { return ArgValue.fromAddress(value); } -export function createUtxoRefArg(txid: Uint8Array, index: number): ArgValue { +export function createUtxoRefArg( + txid: Uint8Array, + index: number, +): PrimitiveArgValue { return ArgValue.fromUtxoRef({ txid, index }); } +export function createCustomArg>( + value: T, +): CustomArgValue { + return CustomArgValue.from(value); +} + // Export utility functions export { hexToBytes, bytesToHex }; diff --git a/packages/tx3-sdk/src/trp/client.ts b/packages/tx3-sdk/src/trp/client.ts index 1705a1d..82f10e0 100644 --- a/packages/tx3-sdk/src/trp/client.ts +++ b/packages/tx3-sdk/src/trp/client.ts @@ -1,15 +1,16 @@ -import { - ClientOptions, - ProtoTxRequest, - ResolveResponse, - TrpError, - NetworkError, - StatusCodeError, +import { toJson } from "./args.js"; +import { + ArgValue, + ClientOptions, + CustomArgValue, JsonRpcError, + NetworkError, + ProtoTxRequest, + ResolveResponse, + StatusCodeError, SubmitParams, - ArgValue, -} from './types.js'; -import { toJson } from './args.js'; + TrpError, +} from "./types.js"; interface JsonRpcResponse { result?: T; @@ -34,7 +35,7 @@ export class Client { */ private prepareHeaders(): Record { return { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...this.options.headers, }; } @@ -42,14 +43,11 @@ export class Client { /** * Handle JSON-RPC request/response cycle */ - private async makeJsonRpcRequest( - method: string, - params: any - ): Promise { + private async makeJsonRpcRequest(method: string, params: any): Promise { try { // Prepare request body const body = { - jsonrpc: '2.0', + jsonrpc: "2.0", method, params, id: crypto.randomUUID(), @@ -57,7 +55,7 @@ export class Client { // Send request const response = await fetch(this.options.endpoint, { - method: 'POST', + method: "POST", headers: this.prepareHeaders(), body: JSON.stringify(body), }); @@ -76,8 +74,8 @@ export class Client { } // Return result - if (result.result === undefined && method !== 'trp.submit') { - throw new TrpError('No result in response'); + if (result.result === undefined && method !== "trp.submit") { + throw new TrpError("No result in response"); } return result.result!; @@ -85,17 +83,17 @@ export class Client { if (error instanceof TrpError) { throw error; } - + // Handle fetch errors - if (error instanceof TypeError && error.message.includes('fetch')) { + if (error instanceof TypeError && error.message.includes("fetch")) { throw new NetworkError(error.message, error); } - + // Handle JSON parsing errors if (error instanceof SyntaxError) { throw new TrpError(`Failed to parse response: ${error.message}`, error); } - + // Re-throw other errors throw new TrpError(`Unknown error: ${error}`, error); } @@ -104,13 +102,36 @@ export class Client { /** * Convert arguments to JSON format */ - private convertArgsToJson(args: Record, force_snake_case: boolean): Record { + private convertArgsToJson( + args: Record, + force_snake_case: boolean, + ): Record { const result: Record = {}; for (const [key, value] of Object.entries(args)) { const newKey = force_snake_case ? key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase() : key; - result[newKey] = toJson(ArgValue.is(value) ? value : ArgValue.from(value)); + // Check if it's already an ArgValue, otherwise convert it + if ( + value && + typeof value === "object" && + "type" in value && + "value" in value + ) { + // It's already an ArgValue (either primitive or custom) + result[newKey] = toJson(value); + } else if (value instanceof CustomArgValue) { + // It's a CustomArgValue + result[newKey] = toJson(value); + } else { + // Convert primitive value to ArgValue + try { + result[newKey] = toJson(ArgValue.from(value)); + } catch { + // If conversion fails, use the value as-is + result[newKey] = value; + } + } } return result; } @@ -120,7 +141,7 @@ export class Client { */ async resolve(protoTx: ProtoTxRequest): Promise { // Convert args to JSON format - const args = this.convertArgsToJson(protoTx.args, true ); + const args = this.convertArgsToJson(protoTx.args, true); // Convert envArgs to JSON format if they exist let envArgs: Record | undefined; @@ -135,13 +156,13 @@ export class Client { env: envArgs, }; - return this.makeJsonRpcRequest('trp.resolve', params); + return this.makeJsonRpcRequest("trp.resolve", params); } /** * Submit a signed transaction to the network */ async submit(params: SubmitParams): Promise { - await this.makeJsonRpcRequest('trp.submit', params); + await this.makeJsonRpcRequest("trp.submit", params); } } diff --git a/packages/tx3-sdk/src/trp/types.ts b/packages/tx3-sdk/src/trp/types.ts index 8a7e993..812f712 100644 --- a/packages/tx3-sdk/src/trp/types.ts +++ b/packages/tx3-sdk/src/trp/types.ts @@ -19,97 +19,286 @@ export interface AssetExpr { export type UtxoSet = Set; -export type ArgValue = - | { type: 'Int'; value: bigint } - | { type: 'Bool'; value: boolean } - | { type: 'String'; value: string } - | { type: 'Bytes'; value: Uint8Array } - | { type: 'Address'; value: Uint8Array } - | { type: 'UtxoSet'; value: UtxoSet } - | { type: 'UtxoRef'; value: UtxoRef }; +export type ArgValueInt = { type: "Int"; value: bigint }; +export type ArgValueBool = { type: "Bool"; value: boolean }; +export type ArgValueString = { type: "String"; value: string }; +export type ArgValueBytes = { type: "Bytes"; value: Uint8Array }; +export type ArgValueAddress = { type: "Address"; value: Uint8Array }; +export type ArgValueUtxoSet = { type: "UtxoSet"; value: UtxoSet }; +export type ArgValueUtxoRef = { type: "UtxoRef"; value: UtxoRef }; +export type PrimitiveArgValue = + | ArgValueInt + | ArgValueBool + | ArgValueString + | ArgValueBytes + | ArgValueAddress + | ArgValueUtxoSet + | ArgValueUtxoRef; // Factory functions to create ArgValue export const ArgValue = { - fromString(value: string): ArgValue { - return { type: 'String', value }; + fromString(value: string): ArgValueString { + return { type: "String", value }; }, - - fromNumber(value: number | bigint): ArgValue { - return { type: 'Int', value: typeof value === 'number' ? BigInt(value) : value }; + + fromNumber(value: number | bigint): ArgValueInt { + return { + type: "Int", + value: typeof value === "number" ? BigInt(value) : value, + }; }, - - fromBool(value: boolean): ArgValue { - return { type: 'Bool', value }; + + fromBool(value: boolean): ArgValueBool { + return { type: "Bool", value }; }, - - fromBytes(value: Uint8Array): ArgValue { - return { type: 'Bytes', value }; + + fromBytes(value: Uint8Array): ArgValueBytes { + return { type: "Bytes", value }; }, - - fromAddress(value: Uint8Array): ArgValue { - return { type: 'Address', value }; + + fromAddress(value: Uint8Array): ArgValueAddress { + return { type: "Address", value }; }, - - fromUtxoSet(value: UtxoSet): ArgValue { - return { type: 'UtxoSet', value }; + + fromUtxoSet(value: UtxoSet): ArgValueUtxoSet { + return { type: "UtxoSet", value }; }, - - fromUtxoRef(value: UtxoRef): ArgValue { - return { type: 'UtxoRef', value }; + + fromUtxoRef(value: UtxoRef): ArgValueUtxoRef { + return { type: "UtxoRef", value }; }, - + // Generic from function for convenience - from(value: string | number | bigint | boolean | Uint8Array | UtxoSet | UtxoRef): ArgValue { - if (typeof value === 'string') return this.fromString(value); - if (typeof value === 'number' || typeof value === 'bigint') return this.fromNumber(value); - if (typeof value === 'boolean') return this.fromBool(value); + from( + value: string | number | bigint | boolean | Uint8Array | UtxoSet | UtxoRef, + ): PrimitiveArgValue { + if (typeof value === "string") return this.fromString(value); + if (typeof value === "number" || typeof value === "bigint") + return this.fromNumber(value); + if (typeof value === "boolean") return this.fromBool(value); if (value instanceof Uint8Array) return this.fromBytes(value); if (value instanceof Set) return this.fromUtxoSet(value as UtxoSet); - if (typeof value === 'object' && value !== null && 'txid' in value) return this.fromUtxoRef(value as UtxoRef); - throw new Error(`Cannot convert value to ArgValue: ${value}`); + if (typeof value === "object" && value !== null && "txid" in value) + return this.fromUtxoRef(value as UtxoRef); + throw new Error(`Cannot convert value to PrimitiveArgValue: ${value}`); }, - is(value: unknown): value is ArgValue { - if (!value || typeof value !== 'object' || !('type' in value)) { + is(value: unknown): value is PrimitiveArgValue { + if (!value || typeof value !== "object" || !("type" in value)) { return false; } - const obj = value as ArgValue; + const obj = value as PrimitiveArgValue; switch (obj.type) { - case 'Int': - return typeof obj.value === 'bigint'; - case 'Bool': - return typeof obj.value === 'boolean'; - case 'String': - return typeof obj.value === 'string'; - case 'Bytes': + case "Int": + return typeof obj.value === "bigint"; + case "Bool": + return typeof obj.value === "boolean"; + case "String": + return typeof obj.value === "string"; + case "Bytes": return obj.value instanceof Uint8Array; - case 'Address': + case "Address": return obj.value instanceof Uint8Array; - case 'UtxoSet': + case "UtxoSet": return obj.value instanceof Set; - case 'UtxoRef': - return typeof obj.value === 'object' && obj.value !== null && 'txid' in obj.value; + case "UtxoRef": + return ( + typeof obj.value === "object" && + obj.value !== null && + "txid" in obj.value + ); default: return false; } - } + }, + + /** + * Type guard to check if a value is any kind of ArgValue (Primitive or Custom) + */ + isAny(value: unknown): value is ArgValue { + return this.is(value) || CustomArgValue.is(value); + }, +}; + +// Custom argument value that can represent complex nested structures +export type ArgValue = PrimitiveArgValue | CustomArgValue; + +// Type helper to convert plain types to ArgValue types recursively +export type ToArgValue = T extends string + ? ArgValueString + : T extends number | bigint + ? ArgValueInt + : T extends boolean + ? ArgValueBool + : T extends Uint8Array + ? ArgValueBytes | ArgValueAddress + : T extends Set + ? U extends Utxo + ? ArgValueUtxoSet + : never + : T extends UtxoRef + ? ArgValueUtxoRef + : T extends Record + ? CustomArgValue + : never; + +// Type helper to map object properties to ArgValue types +export type ToArgValueRecord> = { + [K in keyof T]: ToArgValue; }; +// Type helper to convert ArgValue types back to plain types +export type FromArgValue = T extends ArgValueString + ? string + : T extends ArgValueInt + ? bigint + : T extends ArgValueBool + ? boolean + : T extends ArgValueBytes | ArgValueAddress + ? Uint8Array + : T extends ArgValueUtxoSet + ? UtxoSet + : T extends ArgValueUtxoRef + ? UtxoRef + : T extends CustomArgValue + ? U + : never; + +/** + * Type-safe CustomArgValue that represents complex nested structures with compile-time type checking. + * The generic parameter T defines the shape of the data structure, providing full type safety. + * + * Features: + * - Type safety: Full compile-time type checking based on the shape parameter T + * - Recursive nesting: Can contain other typed CustomArgValue instances + * - Mixed types: Can contain both primitive and custom values + * - Serialization: Can be converted to/from plain JavaScript objects + * - Immutability helpers: Clone, equality checking + * + * @template T The shape of the data structure (plain object type) + * + * @example + * ```typescript + * // Define the shape + * interface UserConfig { + * name: string; + * age: number; + * settings: { + * theme: string; + * notifications: boolean; + * }; + * } + * + * // Create with type safety + * const config = CustomArgValue.from({ + * name: "Alice", + * age: 30, + * settings: { + * theme: "dark", + * notifications: true + * } + * }); + * + * // Type-safe access + * const name = config.get("name"); // Type: ArgValueString + * const settings = config.get("settings"); // Type: CustomArgValue<{theme: string, notifications: boolean}> + * const theme = settings.get("theme"); // Type: ArgValueString + * ``` + */ +export class CustomArgValue< + T extends Record = Record, +> { + public readonly type = "Custom"; + private readonly _value: Record; + + constructor(value: Record) { + this._value = value; + } + + /** + * Get the internal value (for compatibility) + */ + get value(): Record { + return this._value; + } + + /** + * Create a CustomArgValue from a plain object. + * Automatically converts primitive values to PrimitiveArgValue instances. + * + * @param obj - Plain JavaScript object to convert + * @returns New CustomArgValue instance + * + * @example + * ```typescript + * interface Config { + * name: string; + * age: number; + * settings: { theme: string }; + * } + * + * const custom = CustomArgValue.from({ + * name: "Alice", // Becomes ArgValueString + * age: 30, // Becomes ArgValueInt + * settings: { // Becomes nested CustomArgValue + * theme: "dark" + * } + * }); + * ``` + */ + static from>( + obj: TShape, + ): CustomArgValue { + const convertedValue: Record = {}; + + for (const [key, val] of Object.entries(obj)) { + if (ArgValue.is(val)) { + // Already a PrimitiveArgValue + convertedValue[key] = val; + } else if (val instanceof CustomArgValue) { + // Already a CustomArgValue + convertedValue[key] = val; + } else if ( + val && + typeof val === "object" && + !Array.isArray(val) && + !(val instanceof Uint8Array) && + !(val instanceof Set) + ) { + // Plain object - convert to CustomArgValue recursively + convertedValue[key] = CustomArgValue.from(val); + } else { + // Primitive value - convert using ArgValue.from + convertedValue[key] = ArgValue.from(val); + } + } + + return new CustomArgValue(convertedValue); + } + + /** + * Type guard to check if a value is a CustomArgValue + */ + static is(value: unknown): value is CustomArgValue { + return value instanceof CustomArgValue; + } +} + export interface BytesEnvelope { content: string; - encoding: 'base64' | 'hex'; + encoding: "base64" | "hex"; } export enum Type { - Int = 'Int', - Bool = 'Bool', - Bytes = 'Bytes', - Address = 'Address', - UtxoRef = 'UtxoRef', - Undefined = 'Undefined' + Int = "Int", + Bool = "Bool", + Bytes = "Bytes", + Address = "Address", + UtxoRef = "UtxoRef", + Undefined = "Undefined", } export interface TirInfo { @@ -124,7 +313,7 @@ export interface ResolveResponse { } export interface VKeyWitness { - type: 'vkey'; + type: "vkey"; key: BytesEnvelope; signature: BytesEnvelope; } @@ -150,35 +339,44 @@ export interface ProtoTxRequest { export class ArgValueError extends Error { constructor(message: string) { super(message); - this.name = 'ArgValueError'; + this.name = "ArgValueError"; } } export class TrpError extends Error { - constructor(message: string, public override cause?: any) { + constructor( + message: string, + public override cause?: any, + ) { super(message); - this.name = 'TrpError'; + this.name = "TrpError"; } } export class NetworkError extends TrpError { constructor(message: string, cause?: any) { super(`Network error: ${message}`, cause); - this.name = 'NetworkError'; + this.name = "NetworkError"; } } export class StatusCodeError extends TrpError { - constructor(public statusCode: number, message: string) { + constructor( + public statusCode: number, + message: string, + ) { super(`HTTP error ${statusCode}: ${message}`); - this.name = 'StatusCodeError'; + this.name = "StatusCodeError"; } } export class JsonRpcError extends TrpError { - constructor(message: string, public data?: any) { + constructor( + message: string, + public data?: any, + ) { super(`JSON-RPC error: ${message}`); - this.name = 'JsonRpcError'; + this.name = "JsonRpcError"; this.cause = data; } } diff --git a/packages/tx3-sdk/tests/custom-argvalue.test.ts b/packages/tx3-sdk/tests/custom-argvalue.test.ts new file mode 100644 index 0000000..5618707 --- /dev/null +++ b/packages/tx3-sdk/tests/custom-argvalue.test.ts @@ -0,0 +1,219 @@ +import { describe, test, expect } from "@jest/globals"; +import { + CustomArgValue, + createCustomArg, + createIntArg, + createStringArg, + createBoolArg, +} from "../src/trp/index.js"; + +// Define test interfaces for type safety +interface TestUser { + id: number; + name: string; + active: boolean; + profile: { + email: string; + settings: { + theme: string; + notifications: boolean; + }; + }; +} + +interface SimpleConfig { + host: string; + port: number; + ssl: boolean; +} + +describe("CustomArgValue Type Safety", () => { + test("should create type-safe CustomArgValue from interface", () => { + const user = CustomArgValue.from({ + id: 1, + name: "Alice", + active: true, + profile: { + email: "alice@example.com", + settings: { + theme: "dark", + notifications: true, + }, + }, + }); + + expect(user.get("id")?.value).toBe(BigInt(1)); + expect(user.get("name")?.value).toBe("Alice"); + expect(user.get("active")?.value).toBe(true); + + const profile = user.get("profile"); + expect(profile).toBeInstanceOf(CustomArgValue); + expect(profile?.get("email")?.value).toBe("alice@example.com"); + + const settings = profile?.get("settings"); + expect(settings).toBeInstanceOf(CustomArgValue); + expect(settings?.get("theme")?.value).toBe("dark"); + expect(settings?.get("notifications")?.value).toBe(true); + }); + + test("should maintain type safety on get operations", () => { + const config = CustomArgValue.from({ + host: "localhost", + port: 8080, + ssl: true, + }); + + // TypeScript would enforce these types at compile time + const host = config.get("host"); // ArgValueString + const port = config.get("port"); // ArgValueInt + const ssl = config.get("ssl"); // ArgValueBool + + expect(host?.type).toBe("String"); + expect(port?.type).toBe("Int"); + expect(ssl?.type).toBe("Bool"); + + expect(host?.value).toBe("localhost"); + expect(port?.value).toBe(BigInt(8080)); + expect(ssl?.value).toBe(true); + }); + + test("should support type-safe modifications", () => { + const config = CustomArgValue.from({ + host: "localhost", + port: 8080, + ssl: false, + }); + + // Type-safe modifications + config.set("host", createStringArg("newhost")); + config.set("port", createIntArg(3000)); + config.set("ssl", createBoolArg(true)); + + expect(config.get("host")?.value).toBe("newhost"); + expect(config.get("port")?.value).toBe(BigInt(3000)); + expect(config.get("ssl")?.value).toBe(true); + }); + + test("should convert to plain object correctly", () => { + const user = CustomArgValue.from({ + id: 1, + name: "Bob", + active: false, + profile: { + email: "bob@example.com", + settings: { + theme: "light", + notifications: false, + }, + }, + }); + + const plain = user.toPlainObject(); + + expect(plain).toEqual({ + id: BigInt(1), // Note: numbers become bigint + name: "Bob", + active: false, + profile: { + email: "bob@example.com", + settings: { + theme: "light", + notifications: false, + }, + }, + }); + }); + + test("should clone with type safety", () => { + const original = CustomArgValue.from({ + host: "original", + port: 5000, + ssl: true, + }); + + const clone = original.clone(); + + // Modify clone + clone.set("host", createStringArg("cloned")); + clone.set("port", createIntArg(6000)); + + // Original should remain unchanged + expect(original.get("host")?.value).toBe("original"); + expect(original.get("port")?.value).toBe(BigInt(5000)); + + // Clone should be modified + expect(clone.get("host")?.value).toBe("cloned"); + expect(clone.get("port")?.value).toBe(BigInt(6000)); + }); + + test("should check equality correctly", () => { + const config1 = CustomArgValue.from({ + host: "localhost", + port: 8080, + ssl: true, + }); + + const config2 = CustomArgValue.from({ + host: "localhost", + port: 8080, + ssl: true, + }); + + const config3 = CustomArgValue.from({ + host: "localhost", + port: 3000, + ssl: true, + }); + + expect(config1.equals(config2)).toBe(true); + expect(config1.equals(config3)).toBe(false); + }); + + test("should handle nested structures correctly", () => { + const user = CustomArgValue.from({ + id: 123, + name: "Charlie", + active: true, + profile: { + email: "charlie@example.com", + settings: { + theme: "auto", + notifications: true, + }, + }, + }); + + // Test nested access + const profile = user.get("profile"); + const settings = profile?.get("settings"); + + expect(settings?.get("theme")?.value).toBe("auto"); + expect(settings?.get("notifications")?.value).toBe(true); + + // Test nested modification + settings?.set("theme", createStringArg("dark")); + expect(settings?.get("theme")?.value).toBe("dark"); + + // Verify the change is reflected in the full object + const plainObject = user.toPlainObject(); + expect(plainObject.profile.settings.theme).toBe("dark"); + }); + + test("should maintain type information through serialization", () => { + const config = CustomArgValue.from({ + host: "test", + port: 9000, + ssl: false, + }); + + // Convert to plain object and back + const plain = config.toPlainObject(); + const recreated = CustomArgValue.from(plain); + + expect(recreated.equals(config)).toBe(true); + expect(recreated.get("host")?.type).toBe("String"); + expect(recreated.get("port")?.type).toBe("Int"); + expect(recreated.get("ssl")?.type).toBe("Bool"); + }); +}); + From 4376da9b783dbd0869216dedde08fae37d924a93 Mon Sep 17 00:00:00 2001 From: sofia_bobbiesi Date: Tue, 4 Nov 2025 14:11:34 -0300 Subject: [PATCH 02/10] Refactor args.test.ts for improved readability and consistency --- packages/tx3-sdk/src/trp/args.ts | 83 +++- packages/tx3-sdk/src/trp/types.ts | 197 ++------- packages/tx3-sdk/tests/args.test.ts | 602 +++++++++++++++------------- 3 files changed, 430 insertions(+), 452 deletions(-) diff --git a/packages/tx3-sdk/src/trp/args.ts b/packages/tx3-sdk/src/trp/args.ts index 060e955..2680de6 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -28,8 +28,8 @@ function hexToBytes(s: string): Uint8Array { const bytes = new Uint8Array(cleanHex.length / 2); for (let i = 0; i < cleanHex.length; i += 2) { const hexByte = cleanHex.substring(i, i + 2); - const byteValue = parseInt(hexByte, 16); - if (isNaN(byteValue)) { + const byteValue = Number.parseInt(hexByte, 16); + if (Number.isNaN(byteValue)) { throw new ArgValueError(`Invalid hex string: ${s}`); } bytes[i / 2] = byteValue; @@ -51,7 +51,7 @@ function base64ToBytes(s: string): Uint8Array { bytes[i] = binary.charCodeAt(i); } return bytes; - } catch (error) { + } catch { throw new ArgValueError(`Invalid base64: ${s}`); } } @@ -224,9 +224,9 @@ function stringToUtxoRef(s: string): UtxoRef { const [txidHex, indexStr] = parts; const txid = hexToBytes(txidHex); - const index = parseInt(indexStr, 10); + const index = Number.parseInt(indexStr, 10); - if (isNaN(index)) { + if (Number.isNaN(index)) { throw new ArgValueError(`Invalid utxo ref: ${s}`); } @@ -246,13 +246,12 @@ function utxoRefToValue(x: UtxoRef): string { } export function toJson(value: PrimitiveArgValue | CustomArgValue): any { - // recursively turn fields into json + // Handle CustomArgValue with ordered fields if (value instanceof CustomArgValue) { - const result: Record = {}; - for (const [key, val] of Object.entries(value.value)) { - result[key] = toJson(val); - } - return result; + return { + constructor: value.constructorIndex, + fields: value.fields.map((field) => toJson(field)), + }; } // Handle PrimitiveArgValue @@ -301,7 +300,6 @@ export function fromJson(value: any, target: Type): PrimitiveArgValue { } } -// Helper functions to create ArgValue instances export function createIntArg(value: number | bigint): PrimitiveArgValue { return ArgValue.fromNumber(value); } @@ -329,11 +327,60 @@ export function createUtxoRefArg( return ArgValue.fromUtxoRef({ txid, index }); } -export function createCustomArg>( - value: T, -): CustomArgValue { - return CustomArgValue.from(value); +/** + * Create a CustomArgValue with a constructor index and ordered fields + * @param constructorIndex - The constructor index (positive integer) + * @param fields - Ordered array of ArgValue fields + */ +export function createCustomArg( + constructorIndex: number, + fields: TFields, +): CustomArgValue { + return new CustomArgValue(constructorIndex, fields); +} + +/** + * Parse a custom type from JSON + * This expects the JSON to have the format: {constructor: number, fields: array} + */ +function valueToCustom(value: any): CustomArgValue { + if (!value || typeof value !== "object") { + throw new ArgValueError(`Value is not a custom type object: ${value}`); + } + + if (!("constructor" in value)) { + throw new ArgValueError("Custom type missing constructor field"); + } + + const constructorIndex = value.constructor; + if (!Number.isInteger(constructorIndex) || constructorIndex < 0) { + throw new ArgValueError( + "Custom type constructor must be a non-negative integer", + ); + } + + if (!("fields" in value)) { + throw new ArgValueError("Custom type missing fields array"); + } + + if (!Array.isArray(value.fields)) { + throw new ArgValueError("Custom type fields must be an array"); + } + + const fields: ArgValue[] = value.fields.map((fieldValue: any) => { + if ( + fieldValue && + typeof fieldValue === "object" && + "constructor" in fieldValue && + "fields" in fieldValue + ) { + return valueToCustom(fieldValue); + } + + return fromJson(fieldValue, Type.Undefined); + }); + + return new CustomArgValue(constructorIndex, fields); } -// Export utility functions -export { hexToBytes, bytesToHex }; +export { hexToBytes, bytesToHex, valueToCustom }; diff --git a/packages/tx3-sdk/src/trp/types.ts b/packages/tx3-sdk/src/trp/types.ts index 812f712..1a276e2 100644 --- a/packages/tx3-sdk/src/trp/types.ts +++ b/packages/tx3-sdk/src/trp/types.ts @@ -69,7 +69,6 @@ export const ArgValue = { return { type: "UtxoRef", value }; }, - // Generic from function for convenience from( value: string | number | bigint | boolean | Uint8Array | UtxoSet | UtxoRef, ): PrimitiveArgValue { @@ -78,9 +77,9 @@ export const ArgValue = { return this.fromNumber(value); if (typeof value === "boolean") return this.fromBool(value); if (value instanceof Uint8Array) return this.fromBytes(value); - if (value instanceof Set) return this.fromUtxoSet(value as UtxoSet); + if (value instanceof Set) return this.fromUtxoSet(value); if (typeof value === "object" && value !== null && "txid" in value) - return this.fromUtxoRef(value as UtxoRef); + return this.fromUtxoRef(value); throw new Error(`Cannot convert value to PrimitiveArgValue: ${value}`); }, @@ -123,167 +122,58 @@ export const ArgValue = { }, }; -// Custom argument value that can represent complex nested structures +// Custom argument value that can represent complex nested structures with ordered fields export type ArgValue = PrimitiveArgValue | CustomArgValue; -// Type helper to convert plain types to ArgValue types recursively -export type ToArgValue = T extends string - ? ArgValueString - : T extends number | bigint - ? ArgValueInt - : T extends boolean - ? ArgValueBool - : T extends Uint8Array - ? ArgValueBytes | ArgValueAddress - : T extends Set - ? U extends Utxo - ? ArgValueUtxoSet - : never - : T extends UtxoRef - ? ArgValueUtxoRef - : T extends Record - ? CustomArgValue - : never; - -// Type helper to map object properties to ArgValue types -export type ToArgValueRecord> = { - [K in keyof T]: ToArgValue; -}; - -// Type helper to convert ArgValue types back to plain types -export type FromArgValue = T extends ArgValueString - ? string - : T extends ArgValueInt - ? bigint - : T extends ArgValueBool - ? boolean - : T extends ArgValueBytes | ArgValueAddress - ? Uint8Array - : T extends ArgValueUtxoSet - ? UtxoSet - : T extends ArgValueUtxoRef - ? UtxoRef - : T extends CustomArgValue - ? U - : never; - -/** - * Type-safe CustomArgValue that represents complex nested structures with compile-time type checking. - * The generic parameter T defines the shape of the data structure, providing full type safety. - * - * Features: - * - Type safety: Full compile-time type checking based on the shape parameter T - * - Recursive nesting: Can contain other typed CustomArgValue instances - * - Mixed types: Can contain both primitive and custom values - * - Serialization: Can be converted to/from plain JavaScript objects - * - Immutability helpers: Clone, equality checking - * - * @template T The shape of the data structure (plain object type) - * - * @example - * ```typescript - * // Define the shape - * interface UserConfig { - * name: string; - * age: number; - * settings: { - * theme: string; - * notifications: boolean; - * }; - * } - * - * // Create with type safety - * const config = CustomArgValue.from({ - * name: "Alice", - * age: 30, - * settings: { - * theme: "dark", - * notifications: true - * } - * }); - * - * // Type-safe access - * const name = config.get("name"); // Type: ArgValueString - * const settings = config.get("settings"); // Type: CustomArgValue<{theme: string, notifications: boolean}> - * const theme = settings.get("theme"); // Type: ArgValueString - * ``` - */ export class CustomArgValue< - T extends Record = Record, + TFields extends readonly ArgValue[] = readonly ArgValue[], > { - public readonly type = "Custom"; - private readonly _value: Record; + public readonly type = "Custom" as const; + public readonly constructorIndex: number; + public readonly fields: TFields; - constructor(value: Record) { - this._value = value; + /** + * Create a new CustomArgValue with a constructor index and ordered fields + * @param constructorIndex - The constructor index (positive integer) + * @param fields - Ordered array of ArgValue fields + */ + constructor(constructorIndex: number, fields: TFields) { + if (!Number.isInteger(constructorIndex) || constructorIndex < 0) { + throw new Error("Constructor index must be a non-negative integer"); + } + this.constructorIndex = constructorIndex; + this.fields = fields; } + // No `from` helper here: keep the class minimal and let tests build + // CustomArgValue using the constructor or test helpers. + /** - * Get the internal value (for compatibility) + * Type guard to check if a value is a CustomArgValue */ - get value(): Record { - return this._value; + static is(value: unknown): value is CustomArgValue { + return value instanceof CustomArgValue; } /** - * Create a CustomArgValue from a plain object. - * Automatically converts primitive values to PrimitiveArgValue instances. - * - * @param obj - Plain JavaScript object to convert - * @returns New CustomArgValue instance - * - * @example - * ```typescript - * interface Config { - * name: string; - * age: number; - * settings: { theme: string }; - * } - * - * const custom = CustomArgValue.from({ - * name: "Alice", // Becomes ArgValueString - * age: 30, // Becomes ArgValueInt - * settings: { // Becomes nested CustomArgValue - * theme: "dark" - * } - * }); - * ``` + * Convert fields to a plain array (for serialization) */ - static from>( - obj: TShape, - ): CustomArgValue { - const convertedValue: Record = {}; - - for (const [key, val] of Object.entries(obj)) { - if (ArgValue.is(val)) { - // Already a PrimitiveArgValue - convertedValue[key] = val; - } else if (val instanceof CustomArgValue) { - // Already a CustomArgValue - convertedValue[key] = val; - } else if ( - val && - typeof val === "object" && - !Array.isArray(val) && - !(val instanceof Uint8Array) && - !(val instanceof Set) - ) { - // Plain object - convert to CustomArgValue recursively - convertedValue[key] = CustomArgValue.from(val); - } else { - // Primitive value - convert using ArgValue.from - convertedValue[key] = ArgValue.from(val); - } - } + toArray(): ArgValue[] { + return [...this.fields]; + } - return new CustomArgValue(convertedValue); + /** + * Get a specific field by index + */ + getField(index: number): T | undefined { + return this.fields[index] as T | undefined; } /** - * Type guard to check if a value is a CustomArgValue + * Get the number of fields */ - static is(value: unknown): value is CustomArgValue { - return value instanceof CustomArgValue; + get length(): number { + return this.fields.length; } } @@ -344,10 +234,7 @@ export class ArgValueError extends Error { } export class TrpError extends Error { - constructor( - message: string, - public override cause?: any, - ) { + constructor(message: string, public override cause?: any) { super(message); this.name = "TrpError"; } @@ -361,20 +248,14 @@ export class NetworkError extends TrpError { } export class StatusCodeError extends TrpError { - constructor( - public statusCode: number, - message: string, - ) { + constructor(public statusCode: number, message: string) { super(`HTTP error ${statusCode}: ${message}`); this.name = "StatusCodeError"; } } export class JsonRpcError extends TrpError { - constructor( - message: string, - public data?: any, - ) { + constructor(message: string, public data?: any) { super(`JSON-RPC error: ${message}`); this.name = "JsonRpcError"; this.cause = data; diff --git a/packages/tx3-sdk/tests/args.test.ts b/packages/tx3-sdk/tests/args.test.ts index a72a38b..d6a695d 100644 --- a/packages/tx3-sdk/tests/args.test.ts +++ b/packages/tx3-sdk/tests/args.test.ts @@ -8,37 +8,50 @@ import { createAddressArg, createUtxoRefArg, hexToBytes, - bytesToHex -} from '../src/trp/args'; -import { ArgValue, Type, BytesEnvelope, UtxoRef, UtxoSet, Utxo } from '../src/trp/types'; + bytesToHex, +} from "../src/trp/args"; +import { + ArgValue, + Type, + BytesEnvelope, + UtxoRef, + UtxoSet, + Utxo, +} from "../src/trp/types"; +import { describe, test, expect } from "@jest/globals"; function argValueEquals(a: ArgValue, b: ArgValue): boolean { if (a.type !== b.type) return false; - + switch (a.type) { - case 'Int': + case "Int": return a.value === (b as typeof a).value; - case 'Bool': + case "Bool": return a.value === (b as typeof a).value; - case 'String': + case "String": return a.value === (b as typeof a).value; - case 'Bytes': + case "Bytes": { const bytesA = a.value; const bytesB = (b as typeof a).value; if (bytesA.length !== bytesB.length) return false; return bytesA.every((val, i) => val === bytesB[i]); - case 'Address': + } + case "Address": { const addrA = a.value; const addrB = (b as typeof a).value; if (addrA.length !== addrB.length) return false; return addrA.every((val, i) => val === addrB[i]); - case 'UtxoRef': + } + case "UtxoRef": { const refA = a.value; const refB = (b as typeof a).value; - return refA.index === refB.index && + return ( + refA.index === refB.index && refA.txid.length === refB.txid.length && - refA.txid.every((val, i) => val === refB.txid[i]); - case 'UtxoSet': + refA.txid.every((val, i) => val === refB.txid[i]) + ); + } + case "UtxoSet": { // For sets, we need to compare each element const setA = Array.from(a.value); const setB = Array.from((b as typeof a).value); @@ -46,15 +59,22 @@ function argValueEquals(a: ArgValue, b: ArgValue): boolean { // This is a simplified comparison - in reality, sets are unordered return setA.every((utxo, i) => { const utxoB = setB[i]; - return utxo.ref.index === utxoB.ref.index && - utxo.ref.txid.every((val, j) => val === utxoB.ref.txid[j]); + return ( + utxo.ref.index === utxoB.ref.index && + utxo.ref.txid.every((val, j) => val === utxoB.ref.txid[j]) + ); }); + } default: return false; } } -function jsonToValueTest(provided: any, target: Type, expected: ArgValue): void { +function jsonToValueTest( + provided: any, + target: Type, + expected: ArgValue, +): void { const result = fromJson(provided, target); expect(argValueEquals(result, expected)).toBe(true); } @@ -65,314 +85,343 @@ function roundTripTest(value: ArgValue, target: Type): void { expect(argValueEquals(value, restored)).toBe(true); } -describe('Args Conversion Tests', () => { - - describe('Integer Tests', () => { - test('round trip small int', () => { +describe("Args Conversion Tests", () => { + describe("Integer Tests", () => { + test("round trip small int", () => { roundTripTest(createIntArg(123456789), Type.Int); }); - test('round trip negative int', () => { + test("round trip negative int", () => { roundTripTest(createIntArg(-123456789), Type.Int); }); - test('round trip big int', () => { - roundTripTest(createIntArg(BigInt('12345678901234567890')), Type.Int); + test("round trip big int", () => { + roundTripTest(createIntArg(BigInt("12345678901234567890")), Type.Int); }); - test('round trip int overflow (min/max)', () => { + test("round trip int overflow (min/max)", () => { // Test with very large numbers that would be i128::MIN and i128::MAX equivalents - const maxI128 = BigInt('170141183460469231731687303715884105727'); // 2^127 - 1 - const minI128 = BigInt('-170141183460469231731687303715884105728'); // -2^127 - + const maxI128 = BigInt("170141183460469231731687303715884105727"); // 2^127 - 1 + const minI128 = BigInt("-170141183460469231731687303715884105728"); // -2^127 + roundTripTest(createIntArg(minI128), Type.Int); roundTripTest(createIntArg(maxI128), Type.Int); }); // Tests with native bigint types (serialized as strings) - test('round trip native bigint via hex string', () => { + test("round trip native bigint via hex string", () => { const bigIntValue = 123456789012345n; const arg = createIntArg(bigIntValue); jsonToValueTest(bigIntValue, Type.Int, arg); }); - test('round trip native number', () => { + test("round trip native number", () => { const numberValue = 42; jsonToValueTest(numberValue, Type.Int, createIntArg(numberValue)); }); - test('round trip negative native number', () => { + test("round trip negative native number", () => { const negativeValue = -999; jsonToValueTest(negativeValue, Type.Int, createIntArg(negativeValue)); }); }); - describe('Boolean Tests', () => { - test('round trip bool true', () => { + describe("Boolean Tests", () => { + test("round trip bool true", () => { roundTripTest(createBoolArg(true), Type.Bool); }); - test('round trip bool false', () => { + test("round trip bool false", () => { roundTripTest(createBoolArg(false), Type.Bool); }); - test('round trip bool from number', () => { + test("round trip bool from number", () => { jsonToValueTest(1, Type.Bool, createBoolArg(true)); jsonToValueTest(0, Type.Bool, createBoolArg(false)); }); - test('round trip bool from string', () => { - jsonToValueTest('true', Type.Bool, createBoolArg(true)); - jsonToValueTest('false', Type.Bool, createBoolArg(false)); + test("round trip bool from string", () => { + jsonToValueTest("true", Type.Bool, createBoolArg(true)); + jsonToValueTest("false", Type.Bool, createBoolArg(false)); }); // Tests with native boolean types - test('round trip native boolean true', () => { + test("round trip native boolean true", () => { const boolValue = true; jsonToValueTest(boolValue, Type.Bool, createBoolArg(boolValue)); }); - test('round trip native boolean false', () => { + test("round trip native boolean false", () => { const boolValue = false; jsonToValueTest(boolValue, Type.Bool, createBoolArg(boolValue)); }); - test('compare createBoolArg vs native boolean handling', () => { + test("compare createBoolArg vs native boolean handling", () => { const nativeTrue = true; const nativeFalse = false; - + const argTrue = createBoolArg(nativeTrue); const argFalse = createBoolArg(nativeFalse); - + // Both should create the same ArgValue structure - expect(argTrue.type).toBe('Bool'); + expect(argTrue.type).toBe("Bool"); expect(argTrue.value).toBe(true); - expect(argFalse.type).toBe('Bool'); + expect(argFalse.type).toBe("Bool"); expect(argFalse.value).toBe(false); }); }); - describe('String Tests', () => { - test('round trip string', () => { - roundTripTest(createStringArg('hello world'), Type.Undefined); + describe("String Tests", () => { + test("round trip string", () => { + roundTripTest(createStringArg("hello world"), Type.Undefined); }); // Tests with native string types - test('round trip native string', () => { - const stringValue = 'hello native world'; - jsonToValueTest(stringValue, Type.Undefined, createStringArg(stringValue)); - }); - - test('round trip empty native string', () => { - const emptyString = ''; - jsonToValueTest(emptyString, Type.Undefined, createStringArg(emptyString)); - }); - - test('round trip native string with special chars', () => { - const specialString = 'Hello 🌟 World! @#$%^&*()'; - jsonToValueTest(specialString, Type.Undefined, createStringArg(specialString)); - }); - - test('compare createStringArg vs native string handling', () => { - const nativeString = 'test string'; + test("round trip native string", () => { + const stringValue = "hello native world"; + jsonToValueTest( + stringValue, + Type.Undefined, + createStringArg(stringValue), + ); + }); + + test("round trip empty native string", () => { + const emptyString = ""; + jsonToValueTest( + emptyString, + Type.Undefined, + createStringArg(emptyString), + ); + }); + + test("round trip native string with special chars", () => { + const specialString = "Hello 🌟 World! @#$%^&*()"; + jsonToValueTest( + specialString, + Type.Undefined, + createStringArg(specialString), + ); + }); + + test("compare createStringArg vs native string handling", () => { + const nativeString = "test string"; const arg = createStringArg(nativeString); - - expect(arg.type).toBe('String'); + + expect(arg.type).toBe("String"); expect(arg.value).toBe(nativeString); }); }); - describe('Bytes Tests', () => { - test('round trip bytes', () => { - const bytes = new TextEncoder().encode('hello'); + describe("Bytes Tests", () => { + test("round trip bytes", () => { + const bytes = new TextEncoder().encode("hello"); roundTripTest(createBytesArg(bytes), Type.Bytes); }); - test('round trip bytes from base64 envelope', () => { + test("round trip bytes from base64 envelope", () => { const json: BytesEnvelope = { - content: 'aGVsbG8=', // "hello" in base64 - encoding: 'base64' + content: "aGVsbG8=", // "hello" in base64 + encoding: "base64", }; - - const expectedBytes = new TextEncoder().encode('hello'); + + const expectedBytes = new TextEncoder().encode("hello"); jsonToValueTest(json, Type.Bytes, createBytesArg(expectedBytes)); }); - test('round trip bytes from hex envelope', () => { + test("round trip bytes from hex envelope", () => { const json: BytesEnvelope = { - content: '68656c6c6f', // "hello" in hex - encoding: 'hex' + content: "68656c6c6f", // "hello" in hex + encoding: "hex", }; - - const expectedBytes = new TextEncoder().encode('hello'); + + const expectedBytes = new TextEncoder().encode("hello"); jsonToValueTest(json, Type.Bytes, createBytesArg(expectedBytes)); }); - test('round trip bytes from hex string', () => { - const hexString = '0x68656c6c6f'; // "hello" in hex with 0x prefix - const expectedBytes = new TextEncoder().encode('hello'); + test("round trip bytes from hex string", () => { + const hexString = "0x68656c6c6f"; // "hello" in hex with 0x prefix + const expectedBytes = new TextEncoder().encode("hello"); jsonToValueTest(hexString, Type.Bytes, createBytesArg(expectedBytes)); }); // Tests with native Uint8Array types (serialized as hex strings) - test('round trip native Uint8Array via hex string', () => { - const nativeBytes = new Uint8Array([0xFF, 0x00, 0xAB, 0xCD, 0xEF]); + test("round trip native Uint8Array via hex string", () => { + const nativeBytes = new Uint8Array([0xff, 0x00, 0xab, 0xcd, 0xef]); const arg = createBytesArg(nativeBytes); const json = toJson(arg); // This will be a hex string jsonToValueTest(json, Type.Bytes, arg); }); - test('round trip empty native Uint8Array via hex string', () => { + test("round trip empty native Uint8Array via hex string", () => { const emptyBytes = new Uint8Array([]); const arg = createBytesArg(emptyBytes); const json = toJson(arg); // This will be "0x" jsonToValueTest(json, Type.Bytes, arg); }); - test('compare createBytesArg vs native Uint8Array handling', () => { + test("compare createBytesArg vs native Uint8Array handling", () => { const nativeBytes = new Uint8Array([1, 2, 3, 4, 5]); const arg = createBytesArg(nativeBytes); - - expect(arg.type).toBe('Bytes'); + + expect(arg.type).toBe("Bytes"); expect(arg.value).toEqual(nativeBytes); }); - test('round trip text encoded as native Uint8Array', () => { - const text = 'Hello World!'; + test("round trip text encoded as native Uint8Array", () => { + const text = "Hello World!"; const nativeBytes = new TextEncoder().encode(text); const arg = createBytesArg(nativeBytes); - + roundTripTest(arg, Type.Bytes); - + // Verify we can decode it back - if (arg.type === 'Bytes') { + if (arg.type === "Bytes") { const decoded = new TextDecoder().decode(arg.value); expect(decoded).toBe(text); } }); }); - describe('Address Tests', () => { - test('round trip address', () => { - const addressBytes = hexToBytes('abc123def456'); + describe("Address Tests", () => { + test("round trip address", () => { + const addressBytes = hexToBytes("abc123def456"); roundTripTest(createAddressArg(addressBytes), Type.Address); }); - test('round trip address from bech32', () => { - const json = 'addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8'; - const expectedBytes = hexToBytes('619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e'); + test("round trip address from bech32", () => { + const json = "addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8"; + const expectedBytes = hexToBytes( + "619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e", + ); jsonToValueTest(json, Type.Address, createAddressArg(expectedBytes)); }); - test('round trip address from hex string', () => { - const hexAddress = '619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e'; + test("round trip address from hex string", () => { + const hexAddress = + "619493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e"; const expectedBytes = hexToBytes(hexAddress); - jsonToValueTest(hexAddress, Type.Address, createAddressArg(expectedBytes)); + jsonToValueTest( + hexAddress, + Type.Address, + createAddressArg(expectedBytes), + ); }); }); - describe('UTXO Reference Tests', () => { - test('round trip utxo ref', () => { - const txidHex = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + describe("UTXO Reference Tests", () => { + test("round trip utxo ref", () => { + const txidHex = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const txidBytes = hexToBytes(txidHex); const utxoRef: UtxoRef = { txid: txidBytes, - index: 0 + index: 0, }; roundTripTest(createUtxoRefArg(txidBytes, 0), Type.UtxoRef); }); - test('parse utxo ref from string', () => { - const json = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#0'; - const expectedTxid = hexToBytes('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); + test("parse utxo ref from string", () => { + const json = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#0"; + const expectedTxid = hexToBytes( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ); const expected = createUtxoRefArg(expectedTxid, 0); - + jsonToValueTest(json, Type.UtxoRef, expected); }); - test('parse utxo ref with different index', () => { - const json = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#42'; - const expectedTxid = hexToBytes('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); + test("parse utxo ref with different index", () => { + const json = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef#42"; + const expectedTxid = hexToBytes( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ); const expected = createUtxoRefArg(expectedTxid, 42); - + jsonToValueTest(json, Type.UtxoRef, expected); }); }); - describe('Type Inference Tests (Undefined type)', () => { - test('infer bool from boolean', () => { + describe("Type Inference Tests (Undefined type)", () => { + test("infer bool from boolean", () => { jsonToValueTest(true, Type.Undefined, createBoolArg(true)); jsonToValueTest(false, Type.Undefined, createBoolArg(false)); }); - test('infer int from number', () => { + test("infer int from number", () => { jsonToValueTest(42, Type.Undefined, createIntArg(42)); jsonToValueTest(-17, Type.Undefined, createIntArg(-17)); }); - test('infer string from string', () => { - jsonToValueTest('hello', Type.Undefined, createStringArg('hello')); - jsonToValueTest('', Type.Undefined, createStringArg('')); + test("infer string from string", () => { + jsonToValueTest("hello", Type.Undefined, createStringArg("hello")); + jsonToValueTest("", Type.Undefined, createStringArg("")); }); }); - describe('Native Types vs CreateArg Functions Comparison', () => { - test('native bigint vs createIntArg with bigint (via serialization)', () => { + describe("Native Types vs CreateArg Functions Comparison", () => { + test("native bigint vs createIntArg with bigint (via serialization)", () => { const nativeBigInt = 123456789012345n; const createdArg = createIntArg(nativeBigInt); - - jsonToValueTest(nativeBigInt, Type.Int, createdArg); + + jsonToValueTest(nativeBigInt, Type.Int, createdArg); roundTripTest(createdArg, Type.Int); }); - test('native number vs createIntArg with number', () => { + test("native number vs createIntArg with number", () => { const nativeNumber = 42; const createdArg = createIntArg(nativeNumber); - + jsonToValueTest(nativeNumber, Type.Int, createdArg); roundTripTest(createdArg, Type.Int); }); - test('native boolean vs createBoolArg', () => { + test("native boolean vs createBoolArg", () => { const nativeBool = true; const createdArg = createBoolArg(nativeBool); - + jsonToValueTest(nativeBool, Type.Bool, createdArg); roundTripTest(createdArg, Type.Bool); }); - test('native string vs createStringArg', () => { - const nativeString = 'test string'; + test("native string vs createStringArg", () => { + const nativeString = "test string"; const createdArg = createStringArg(nativeString); - + jsonToValueTest(nativeString, Type.Undefined, createdArg); roundTripTest(createdArg, Type.Undefined); }); - test('native Uint8Array vs createBytesArg (via serialization)', () => { - const nativeBytes = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]); + test("native Uint8Array vs createBytesArg (via serialization)", () => { + const nativeBytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); const createdArg = createBytesArg(nativeBytes); - + jsonToValueTest(nativeBytes, Type.Bytes, createdArg); roundTripTest(createdArg, Type.Bytes); }); - test('native types in type inference (Type.Undefined)', () => { + test("native types in type inference (Type.Undefined)", () => { // Test that JSON-compatible native types are properly inferred when using Type.Undefined jsonToValueTest(999, Type.Undefined, createIntArg(999)); jsonToValueTest(true, Type.Undefined, createBoolArg(true)); jsonToValueTest(false, Type.Undefined, createBoolArg(false)); - jsonToValueTest('auto-infer', Type.Undefined, createStringArg('auto-infer')); + jsonToValueTest( + "auto-infer", + Type.Undefined, + createStringArg("auto-infer"), + ); }); - test('edge cases with native types', () => { + test("edge cases with native types", () => { // Zero values jsonToValueTest(0, Type.Int, createIntArg(0)); // Empty values - jsonToValueTest('', Type.Undefined, createStringArg('')); + jsonToValueTest("", Type.Undefined, createStringArg("")); // Test serialized empty bytes const emptyBytes = new Uint8Array([]); @@ -389,34 +438,34 @@ describe('Args Conversion Tests', () => { jsonToValueTest(largeBigInt, Type.Int, largeBigIntArg); }); - test('direct native type creation vs factory functions', () => { + test("direct native type creation vs factory functions", () => { // Test that creating ArgValues directly with native types produces same results as factory functions const testCases = [ { native: 42, createFn: () => createIntArg(42) }, { native: true, createFn: () => createBoolArg(true) }, { native: false, createFn: () => createBoolArg(false) }, - { native: 'hello', createFn: () => createStringArg('hello') }, + { native: "hello", createFn: () => createStringArg("hello") }, ]; testCases.forEach(({ native, createFn }) => { const factoryResult = createFn(); expect(factoryResult.value).toEqual( - typeof native === 'number' ? BigInt(native) : native + typeof native === "number" ? BigInt(native) : native, ); }); }); - test('bigint handling in different ranges', () => { + test("bigint handling in different ranges", () => { const testCases = [ 0n, 1n, -1n, BigInt(Number.MAX_SAFE_INTEGER), BigInt(Number.MAX_SAFE_INTEGER) + 1n, - BigInt('12345678901234567890'), + BigInt("12345678901234567890"), ]; - testCases.forEach(bigIntValue => { + testCases.forEach((bigIntValue) => { const arg = createIntArg(bigIntValue); const json = toJson(arg); const restored = fromJson(json, Type.Int); @@ -425,85 +474,85 @@ describe('Args Conversion Tests', () => { }); }); - describe('Error Cases', () => { - test('invalid hex string', () => { + describe("Error Cases", () => { + test("invalid hex string", () => { expect(() => { - fromJson('0xzzzz', Type.Bytes); - }).toThrow('Invalid hex string'); + fromJson("0xzzzz", Type.Bytes); + }).toThrow("Invalid hex string"); }); - test('invalid utxo ref format', () => { + test("invalid utxo ref format", () => { expect(() => { - fromJson('invalid-utxo-ref', Type.UtxoRef); - }).toThrow('Invalid utxo ref'); + fromJson("invalid-utxo-ref", Type.UtxoRef); + }).toThrow("Invalid utxo ref"); }); - test('invalid utxo ref index', () => { + test("invalid utxo ref index", () => { expect(() => { - fromJson('abcd#notanumber', Type.UtxoRef); - }).toThrow('Invalid utxo ref'); + fromJson("abcd#notanumber", Type.UtxoRef); + }).toThrow("Invalid utxo ref"); }); - test('null value', () => { + test("null value", () => { expect(() => { fromJson(null, Type.Int); - }).toThrow('Value is null'); + }).toThrow("Value is null"); }); - test('invalid number for boolean', () => { + test("invalid number for boolean", () => { expect(() => { fromJson(2, Type.Bool); - }).toThrow('Invalid number for boolean'); + }).toThrow("Invalid number for boolean"); }); - test('invalid string for boolean', () => { + test("invalid string for boolean", () => { expect(() => { - fromJson('maybe', Type.Bool); - }).toThrow('Invalid string for boolean'); + fromJson("maybe", Type.Bool); + }).toThrow("Invalid string for boolean"); }); }); - describe('Utility Functions', () => { - test('hexToBytes and bytesToHex round trip', () => { - const originalHex = 'deadbeef'; + describe("Utility Functions", () => { + test("hexToBytes and bytesToHex round trip", () => { + const originalHex = "deadbeef"; const bytes = hexToBytes(originalHex); const backToHex = bytesToHex(bytes); expect(backToHex).toBe(originalHex); }); - test('hexToBytes with 0x prefix', () => { - const hex = '0xdeadbeef'; + test("hexToBytes with 0x prefix", () => { + const hex = "0xdeadbeef"; const bytes = hexToBytes(hex); - const expected = hexToBytes('deadbeef'); + const expected = hexToBytes("deadbeef"); expect(bytes).toEqual(expected); }); - test('hexToBytes with odd length should throw', () => { + test("hexToBytes with odd length should throw", () => { expect(() => { - hexToBytes('abc'); // odd length - }).toThrow('Invalid hex string'); + hexToBytes("abc"); // odd length + }).toThrow("Invalid hex string"); }); }); - describe('Large Number Handling', () => { - test('numbers that fit in safe integer range', () => { + describe("Large Number Handling", () => { + test("numbers that fit in safe integer range", () => { const safeInt = Number.MAX_SAFE_INTEGER; const arg = createIntArg(safeInt); const json = toJson(arg); - expect(typeof json).toBe('number'); + expect(typeof json).toBe("number"); expect(json).toBe(safeInt); }); - test('numbers that exceed safe integer range become hex strings', () => { + test("numbers that exceed safe integer range become hex strings", () => { const bigInt = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1); const arg = createIntArg(bigInt); const json = toJson(arg); - expect(typeof json).toBe('string'); + expect(typeof json).toBe("string"); expect(json).toMatch(/^0x[0-9a-f]+$/); }); - test('parse hex string back to bigint', () => { - const original = BigInt('123456789012345678901234567890'); + test("parse hex string back to bigint", () => { + const original = BigInt("123456789012345678901234567890"); const arg = createIntArg(original); const json = toJson(arg); const restored = fromJson(json, Type.Int); @@ -511,225 +560,226 @@ describe('Args Conversion Tests', () => { }); }); - describe('ArgValue Factory Functions Tests', () => { - - describe('fromString', () => { - test('creates string ArgValue', () => { - const result = ArgValue.fromString('hello world'); - expect(result.type).toBe('String'); - expect(result.value).toBe('hello world'); + describe("ArgValue Factory Functions Tests", () => { + describe("fromString", () => { + test("creates string ArgValue", () => { + const result = ArgValue.fromString("hello world"); + expect(result.type).toBe("String"); + expect(result.value).toBe("hello world"); }); - test('handles empty string', () => { - const result = ArgValue.fromString(''); - expect(result.type).toBe('String'); - expect(result.value).toBe(''); + test("handles empty string", () => { + const result = ArgValue.fromString(""); + expect(result.type).toBe("String"); + expect(result.value).toBe(""); }); - test('handles special characters', () => { - const specialString = 'Hello 🌟 World! @#$%^&*()'; + test("handles special characters", () => { + const specialString = "Hello 🌟 World! @#$%^&*()"; const result = ArgValue.fromString(specialString); - expect(result.type).toBe('String'); + expect(result.type).toBe("String"); expect(result.value).toBe(specialString); }); }); - describe('fromNumber', () => { - test('creates int ArgValue from number', () => { + describe("fromNumber", () => { + test("creates int ArgValue from number", () => { const result = ArgValue.fromNumber(42); - expect(result.type).toBe('Int'); + expect(result.type).toBe("Int"); expect(result.value).toBe(42n); }); - test('creates int ArgValue from bigint', () => { - const result = ArgValue.fromNumber(BigInt('123456789012345')); - expect(result.type).toBe('Int'); - expect(result.value).toBe(BigInt('123456789012345')); + test("creates int ArgValue from bigint", () => { + const result = ArgValue.fromNumber(BigInt("123456789012345")); + expect(result.type).toBe("Int"); + expect(result.value).toBe(BigInt("123456789012345")); }); - test('handles negative numbers', () => { + test("handles negative numbers", () => { const result = ArgValue.fromNumber(-42); - expect(result.type).toBe('Int'); + expect(result.type).toBe("Int"); expect(result.value).toBe(-42n); }); - test('handles zero', () => { + test("handles zero", () => { const result = ArgValue.fromNumber(0); - expect(result.type).toBe('Int'); + expect(result.type).toBe("Int"); expect(result.value).toBe(0n); }); - test('handles large numbers', () => { + test("handles large numbers", () => { const largeNum = Number.MAX_SAFE_INTEGER; const result = ArgValue.fromNumber(largeNum); - expect(result.type).toBe('Int'); + expect(result.type).toBe("Int"); expect(result.value).toBe(BigInt(largeNum)); }); }); - describe('fromBool', () => { - test('creates bool ArgValue from true', () => { + describe("fromBool", () => { + test("creates bool ArgValue from true", () => { const result = ArgValue.fromBool(true); - expect(result.type).toBe('Bool'); + expect(result.type).toBe("Bool"); expect(result.value).toBe(true); }); - test('creates bool ArgValue from false', () => { + test("creates bool ArgValue from false", () => { const result = ArgValue.fromBool(false); - expect(result.type).toBe('Bool'); + expect(result.type).toBe("Bool"); expect(result.value).toBe(false); }); }); - describe('fromBytes', () => { - test('creates bytes ArgValue', () => { + describe("fromBytes", () => { + test("creates bytes ArgValue", () => { const bytes = new Uint8Array([1, 2, 3, 4, 5]); const result = ArgValue.fromBytes(bytes); - expect(result.type).toBe('Bytes'); + expect(result.type).toBe("Bytes"); expect(result.value).toEqual(bytes); }); - test('handles empty bytes', () => { + test("handles empty bytes", () => { const bytes = new Uint8Array([]); const result = ArgValue.fromBytes(bytes); - expect(result.type).toBe('Bytes'); + expect(result.type).toBe("Bytes"); expect(result.value).toEqual(bytes); }); - test('handles hex-like bytes', () => { - const bytes = new Uint8Array([0xFF, 0x00, 0xAB, 0xCD]); + test("handles hex-like bytes", () => { + const bytes = new Uint8Array([0xff, 0x00, 0xab, 0xcd]); const result = ArgValue.fromBytes(bytes); - expect(result.type).toBe('Bytes'); + expect(result.type).toBe("Bytes"); expect(result.value).toEqual(bytes); }); }); - describe('fromAddress', () => { - test('creates address ArgValue', () => { + describe("fromAddress", () => { + test("creates address ArgValue", () => { const address = new Uint8Array([0x01, 0x02, 0x03]); const result = ArgValue.fromAddress(address); - expect(result.type).toBe('Address'); + expect(result.type).toBe("Address"); expect(result.value).toEqual(address); }); - test('handles typical cardano address length', () => { + test("handles typical cardano address length", () => { // Typical Cardano address is 29 bytes const address = new Uint8Array(29).fill(0x01); const result = ArgValue.fromAddress(address); - expect(result.type).toBe('Address'); + expect(result.type).toBe("Address"); expect(result.value).toEqual(address); }); }); - describe('fromUtxoRef', () => { - test('creates UtxoRef ArgValue', () => { + describe("fromUtxoRef", () => { + test("creates UtxoRef ArgValue", () => { const utxoRef: UtxoRef = { txid: new Uint8Array([1, 2, 3, 4]), - index: 0 + index: 0, }; const result = ArgValue.fromUtxoRef(utxoRef); - expect(result.type).toBe('UtxoRef'); + expect(result.type).toBe("UtxoRef"); expect(result.value).toEqual(utxoRef); - }); test('handles different index values', () => { - const utxoRef: UtxoRef = { - txid: new Uint8Array(32).fill(0xFF), // Typical txid length - index: 5 - }; - const result = ArgValue.fromUtxoRef(utxoRef); - expect(result.type).toBe('UtxoRef'); - if (result.type === 'UtxoRef') { - expect(result.value.index).toBe(5); - expect(result.value.txid).toEqual(utxoRef.txid); - } - }); + }); + test("handles different index values", () => { + const utxoRef: UtxoRef = { + txid: new Uint8Array(32).fill(0xff), // Typical txid length + index: 5, + }; + const result = ArgValue.fromUtxoRef(utxoRef); + expect(result.type).toBe("UtxoRef"); + if (result.type === "UtxoRef") { + expect(result.value.index).toBe(5); + expect(result.value.txid).toEqual(utxoRef.txid); + } + }); }); - describe('fromUtxoSet', () => { - test('creates UtxoSet ArgValue', () => { + describe("fromUtxoSet", () => { + test("creates UtxoSet ArgValue", () => { const utxo: Utxo = { ref: { txid: new Uint8Array([1, 2, 3]), index: 0 }, address: new Uint8Array([4, 5, 6]), - assets: [] + assets: [], }; const utxoSet: UtxoSet = new Set([utxo]); const result = ArgValue.fromUtxoSet(utxoSet); - expect(result.type).toBe('UtxoSet'); + expect(result.type).toBe("UtxoSet"); expect(result.value).toEqual(utxoSet); - }); test('handles empty UtxoSet', () => { - const utxoSet: UtxoSet = new Set(); - const result = ArgValue.fromUtxoSet(utxoSet); - expect(result.type).toBe('UtxoSet'); - if (result.type === 'UtxoSet') { - expect(result.value.size).toBe(0); - } - }); + }); + test("handles empty UtxoSet", () => { + const utxoSet: UtxoSet = new Set(); + const result = ArgValue.fromUtxoSet(utxoSet); + expect(result.type).toBe("UtxoSet"); + if (result.type === "UtxoSet") { + expect(result.value.size).toBe(0); + } + }); }); - describe('from (generic)', () => { - test('auto-detects string', () => { - const result = ArgValue.from('test string'); - expect(result.type).toBe('String'); - expect(result.value).toBe('test string'); + describe("from (generic)", () => { + test("auto-detects string", () => { + const result = ArgValue.from("test string"); + expect(result.type).toBe("String"); + expect(result.value).toBe("test string"); }); - test('auto-detects number', () => { + test("auto-detects number", () => { const result = ArgValue.from(42); - expect(result.type).toBe('Int'); + expect(result.type).toBe("Int"); expect(result.value).toBe(42n); }); - test('auto-detects bigint', () => { - const result = ArgValue.from(BigInt('123456789')); - expect(result.type).toBe('Int'); - expect(result.value).toBe(BigInt('123456789')); + test("auto-detects bigint", () => { + const result = ArgValue.from(BigInt("123456789")); + expect(result.type).toBe("Int"); + expect(result.value).toBe(BigInt("123456789")); }); - test('auto-detects boolean', () => { + test("auto-detects boolean", () => { const result = ArgValue.from(true); - expect(result.type).toBe('Bool'); + expect(result.type).toBe("Bool"); expect(result.value).toBe(true); }); - test('auto-detects Uint8Array', () => { + test("auto-detects Uint8Array", () => { const bytes = new Uint8Array([1, 2, 3]); const result = ArgValue.from(bytes); - expect(result.type).toBe('Bytes'); + expect(result.type).toBe("Bytes"); expect(result.value).toEqual(bytes); }); - test('auto-detects UtxoSet', () => { + test("auto-detects UtxoSet", () => { const utxoSet: UtxoSet = new Set(); const result = ArgValue.from(utxoSet); - expect(result.type).toBe('UtxoSet'); + expect(result.type).toBe("UtxoSet"); expect(result.value).toEqual(utxoSet); }); - test('auto-detects UtxoRef', () => { + test("auto-detects UtxoRef", () => { const utxoRef: UtxoRef = { txid: new Uint8Array([1, 2, 3]), - index: 1 + index: 1, }; const result = ArgValue.from(utxoRef); - expect(result.type).toBe('UtxoRef'); + expect(result.type).toBe("UtxoRef"); expect(result.value).toEqual(utxoRef); }); - test('throws error for unsupported type', () => { + test("throws error for unsupported type", () => { expect(() => { ArgValue.from({} as any); - }).toThrow('Cannot convert value to ArgValue'); + }).toThrow("Cannot convert value to PrimitiveArgValue"); }); - test('throws error for null', () => { + test("throws error for null", () => { expect(() => { ArgValue.from(null as any); - }).toThrow('Cannot convert value to ArgValue'); + }).toThrow("Cannot convert value to PrimitiveArgValue"); }); - test('throws error for undefined', () => { + test("throws error for undefined", () => { expect(() => { ArgValue.from(undefined as any); - }).toThrow('Cannot convert value to ArgValue'); + }).toThrow("Cannot convert value to PrimitiveArgValue"); }); }); }); From 8ce18dd1f5bdf2918aaebf64641250bc9d280366 Mon Sep 17 00:00:00 2001 From: sofia_bobbiesi Date: Tue, 4 Nov 2025 16:03:10 -0300 Subject: [PATCH 03/10] refactor custom argument value tests --- .../tx3-sdk/tests/custom-argvalue.test.ts | 227 ++++++++++-------- 1 file changed, 121 insertions(+), 106 deletions(-) diff --git a/packages/tx3-sdk/tests/custom-argvalue.test.ts b/packages/tx3-sdk/tests/custom-argvalue.test.ts index 5618707..c8fb765 100644 --- a/packages/tx3-sdk/tests/custom-argvalue.test.ts +++ b/packages/tx3-sdk/tests/custom-argvalue.test.ts @@ -1,35 +1,60 @@ import { describe, test, expect } from "@jest/globals"; import { CustomArgValue, - createCustomArg, createIntArg, createStringArg, createBoolArg, } from "../src/trp/index.js"; -// Define test interfaces for type safety -interface TestUser { - id: number; - name: string; - active: boolean; - profile: { - email: string; - settings: { - theme: string; - notifications: boolean; - }; - }; +function plainToCustom(obj: Record): { + custom: CustomArgValue; + keys: string[]; +} { + const keys = Object.keys(obj); + const fields = keys.map((k) => { + const v = obj[k]; + if (v && typeof v === "object" && !Array.isArray(v)) { + return plainToCustom(v).custom as any; + } + if (typeof v === "number") return createIntArg(v); + if (typeof v === "bigint") return createIntArg(v as bigint); + if (typeof v === "boolean") return createBoolArg(v); + if (typeof v === "string") return createStringArg(v); + throw new Error(`Unsupported test value type: ${v}`); + }); + return { custom: new CustomArgValue(0, fields as any), keys }; +} + +function getByName(custom: CustomArgValue, keys: string[], name: string): any { + const idx = keys.indexOf(name); + if (idx === -1) return undefined; + return custom.getField(idx as number); } -interface SimpleConfig { - host: string; - port: number; - ssl: boolean; +function customToPlain( + custom: CustomArgValue, + shape: Record, +): Record { + const out: Record = {}; + const keys = Object.keys(shape); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + const expected = shape[k]; + const field = custom.getField(i as number) as any; + if (expected && typeof expected === "object" && !Array.isArray(expected)) { + out[k] = customToPlain(field as CustomArgValue, expected); + } else if (field && typeof field === "object" && "type" in field) { + out[k] = field.value; + } else { + out[k] = field; + } + } + return out; } describe("CustomArgValue Type Safety", () => { test("should create type-safe CustomArgValue from interface", () => { - const user = CustomArgValue.from({ + const userShape = { id: 1, name: "Alice", active: true, @@ -40,33 +65,31 @@ describe("CustomArgValue Type Safety", () => { notifications: true, }, }, - }); + }; + const { custom: user, keys: userKeys } = plainToCustom(userShape); - expect(user.get("id")?.value).toBe(BigInt(1)); - expect(user.get("name")?.value).toBe("Alice"); - expect(user.get("active")?.value).toBe(true); + expect(getByName(user, userKeys, "id")?.value).toBe(BigInt(1)); + expect(getByName(user, userKeys, "name")?.value).toBe("Alice"); + expect(getByName(user, userKeys, "active")?.value).toBe(true); - const profile = user.get("profile"); + const profile = getByName(user, userKeys, "profile") as CustomArgValue; expect(profile).toBeInstanceOf(CustomArgValue); - expect(profile?.get("email")?.value).toBe("alice@example.com"); + // nested keys are positional (0=email,1=settings) + expect((profile.getField(0) as any)?.value).toBe("alice@example.com"); - const settings = profile?.get("settings"); + const settings = profile.getField(1) as CustomArgValue; expect(settings).toBeInstanceOf(CustomArgValue); - expect(settings?.get("theme")?.value).toBe("dark"); - expect(settings?.get("notifications")?.value).toBe(true); + expect((settings.getField(0) as any)?.value).toBe("dark"); + expect((settings.getField(1) as any)?.value).toBe(true); }); test("should maintain type safety on get operations", () => { - const config = CustomArgValue.from({ - host: "localhost", - port: 8080, - ssl: true, - }); + const configShape = { host: "localhost", port: 8080, ssl: true }; + const { custom: config, keys: cfgKeys } = plainToCustom(configShape); - // TypeScript would enforce these types at compile time - const host = config.get("host"); // ArgValueString - const port = config.get("port"); // ArgValueInt - const ssl = config.get("ssl"); // ArgValueBool + const host = getByName(config, cfgKeys, "host"); // ArgValueString + const port = getByName(config, cfgKeys, "port"); // ArgValueInt + const ssl = getByName(config, cfgKeys, "ssl"); // ArgValueBool expect(host?.type).toBe("String"); expect(port?.type).toBe("Int"); @@ -78,24 +101,24 @@ describe("CustomArgValue Type Safety", () => { }); test("should support type-safe modifications", () => { - const config = CustomArgValue.from({ - host: "localhost", - port: 8080, - ssl: false, - }); - - // Type-safe modifications - config.set("host", createStringArg("newhost")); - config.set("port", createIntArg(3000)); - config.set("ssl", createBoolArg(true)); - - expect(config.get("host")?.value).toBe("newhost"); - expect(config.get("port")?.value).toBe(BigInt(3000)); - expect(config.get("ssl")?.value).toBe(true); + const configShape = { host: "localhost", port: 8080, ssl: false }; + const { custom: config, keys: cfgKeys } = plainToCustom(configShape); + + // Type-safe modifications (mutate underlying fields by index) + const hostIdx = cfgKeys.indexOf("host"); + const portIdx = cfgKeys.indexOf("port"); + const sslIdx = cfgKeys.indexOf("ssl"); + (config as any).fields[hostIdx] = createStringArg("newhost"); + (config as any).fields[portIdx] = createIntArg(3000); + (config as any).fields[sslIdx] = createBoolArg(true); + + expect(getByName(config, cfgKeys, "host")?.value).toBe("newhost"); + expect(getByName(config, cfgKeys, "port")?.value).toBe(BigInt(3000)); + expect(getByName(config, cfgKeys, "ssl")?.value).toBe(true); }); test("should convert to plain object correctly", () => { - const user = CustomArgValue.from({ + const userShape = { id: 1, name: "Bob", active: false, @@ -106,12 +129,12 @@ describe("CustomArgValue Type Safety", () => { notifications: false, }, }, - }); - - const plain = user.toPlainObject(); + }; + const { custom: user } = plainToCustom(userShape); + const plain = customToPlain(user, userShape); expect(plain).toEqual({ - id: BigInt(1), // Note: numbers become bigint + id: BigInt(1), name: "Bob", active: false, profile: { @@ -125,52 +148,47 @@ describe("CustomArgValue Type Safety", () => { }); test("should clone with type safety", () => { - const original = CustomArgValue.from({ - host: "original", - port: 5000, - ssl: true, - }); + const origShape = { host: "original", port: 5000, ssl: true }; + const { custom: original, keys: origKeys } = plainToCustom(origShape); - const clone = original.clone(); + const clone = new CustomArgValue(original.constructorIndex, [ + ...original.fields, + ]); - // Modify clone - clone.set("host", createStringArg("cloned")); - clone.set("port", createIntArg(6000)); + const hostIdx = origKeys.indexOf("host"); + const portIdx = origKeys.indexOf("port"); + (clone as any).fields[hostIdx] = createStringArg("cloned"); + (clone as any).fields[portIdx] = createIntArg(6000); // Original should remain unchanged - expect(original.get("host")?.value).toBe("original"); - expect(original.get("port")?.value).toBe(BigInt(5000)); + expect((getByName(original, origKeys, "host") as any)?.value).toBe( + "original", + ); + expect((getByName(original, origKeys, "port") as any)?.value).toBe( + BigInt(5000), + ); // Clone should be modified - expect(clone.get("host")?.value).toBe("cloned"); - expect(clone.get("port")?.value).toBe(BigInt(6000)); + expect((clone.getField(hostIdx) as any).value).toBe("cloned"); + expect((clone.getField(portIdx) as any).value).toBe(BigInt(6000)); }); test("should check equality correctly", () => { - const config1 = CustomArgValue.from({ - host: "localhost", - port: 8080, - ssl: true, - }); - - const config2 = CustomArgValue.from({ - host: "localhost", - port: 8080, - ssl: true, - }); - - const config3 = CustomArgValue.from({ + const cfgShape1 = { host: "localhost", port: 8080, ssl: true }; + const { custom: config1 } = plainToCustom(cfgShape1); + const { custom: config2 } = plainToCustom(cfgShape1); + const { custom: config3 } = plainToCustom({ host: "localhost", port: 3000, ssl: true, }); - expect(config1.equals(config2)).toBe(true); - expect(config1.equals(config3)).toBe(false); + expect(config1.toArray()).toEqual(config2.toArray()); + expect(config1.toArray()).not.toEqual(config3.toArray()); }); test("should handle nested structures correctly", () => { - const user = CustomArgValue.from({ + const userShape = { id: 123, name: "Charlie", active: true, @@ -181,39 +199,36 @@ describe("CustomArgValue Type Safety", () => { notifications: true, }, }, - }); + }; + const { custom: user, keys: userKeys } = plainToCustom(userShape); // Test nested access - const profile = user.get("profile"); - const settings = profile?.get("settings"); + const profile = getByName(user, userKeys, "profile") as CustomArgValue; + const settings = profile.getField(1) as CustomArgValue; - expect(settings?.get("theme")?.value).toBe("auto"); - expect(settings?.get("notifications")?.value).toBe(true); + expect((settings.getField(0) as any)?.value).toBe("auto"); + expect((settings.getField(1) as any)?.value).toBe(true); - // Test nested modification - settings?.set("theme", createStringArg("dark")); - expect(settings?.get("theme")?.value).toBe("dark"); + // Test nested modification (mutate fields) + (settings as any).fields[0] = createStringArg("dark"); + expect((settings.getField(0) as any)?.value).toBe("dark"); // Verify the change is reflected in the full object - const plainObject = user.toPlainObject(); + const plainObject = customToPlain(user, userShape); expect(plainObject.profile.settings.theme).toBe("dark"); }); test("should maintain type information through serialization", () => { - const config = CustomArgValue.from({ - host: "test", - port: 9000, - ssl: false, - }); + const cfgShape = { host: "test", port: 9000, ssl: false }; + const { custom: config } = plainToCustom(cfgShape); - // Convert to plain object and back - const plain = config.toPlainObject(); - const recreated = CustomArgValue.from(plain); + // Convert to plain object and back using test helpers + const plain = customToPlain(config, cfgShape); + const { custom: recreated, keys: recreatedKeys } = plainToCustom(plain); - expect(recreated.equals(config)).toBe(true); - expect(recreated.get("host")?.type).toBe("String"); - expect(recreated.get("port")?.type).toBe("Int"); - expect(recreated.get("ssl")?.type).toBe("Bool"); + expect(recreated.toArray()).toEqual(config.toArray()); + expect(getByName(recreated, recreatedKeys, "host")?.type).toBe("String"); + expect(getByName(recreated, recreatedKeys, "port")?.type).toBe("Int"); + expect(getByName(recreated, recreatedKeys, "ssl")?.type).toBe("Bool"); }); }); - From ef491b69d89cb0956521f777e5dd0e6fa36ce527 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 5 Nov 2025 17:09:38 -0300 Subject: [PATCH 04/10] feat: updated handlebars gen to use custom protocol params --- .trix/client-lib/protocol.ts.hbs | 41 ++++++++++++++++--- bindgen/package.json.hbs | 18 --------- bindgen/protocol.ts.hbs | 67 -------------------------------- bindgen/trix-bindgen.toml | 1 - bindgen/tsconfig.json.hbs | 15 ------- 5 files changed, 35 insertions(+), 107 deletions(-) delete mode 100644 bindgen/package.json.hbs delete mode 100644 bindgen/protocol.ts.hbs delete mode 100644 bindgen/trix-bindgen.toml delete mode 100644 bindgen/tsconfig.json.hbs diff --git a/.trix/client-lib/protocol.ts.hbs b/.trix/client-lib/protocol.ts.hbs index d0dfc12..6e8531d 100644 --- a/.trix/client-lib/protocol.ts.hbs +++ b/.trix/client-lib/protocol.ts.hbs @@ -1,5 +1,3 @@ -// This file is auto-generated by trix bindgen. - import { TRPClient, type ArgValue, @@ -14,6 +12,7 @@ import { type ArgValueUtxoSet, type ArgValueUtxoRef, CustomArgValue, + ArgValue as ArgValueFactory, } from "tx3-sdk/trp"; @@ -31,14 +30,44 @@ export const DEFAULT_ENV_ARGS = { {{/each}} }; -// Custom type definitions {{#each transactions}} {{#each parameters}} {{#if (hasCustomDef this)}} {{#if (getCustomTypeName this)}} -// Custom type definition for {{getCustomTypeName this}} -export type {{getCustomTypeNamePascal this}} = {{generateCustomType this}}; +{{#each custom_def}} +{{#if @first}} +export type {{getCustomTypeNamePascal ../this}} = +{{else}} + | +{{/if}} + CustomArgValue<[ + {{#each fields}} + {{argValueTypeFor type_name "typescript"}}, + {{/each}} + ]> // Constructor {{constructor}}{{#if fields}} with fields: [{{#each fields}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}]{{/if}} +{{/each}}; +// Factory functions to create {{getCustomTypeName this}} instances +{{#each custom_def}} +{{#if fields}} +export function create{{getCustomTypeNamePascal ../this}}Constructor{{constructor}}( + {{#each fields}} + {{camelCase name}}: {{argValueTypeFor type_name "typescript"}}, + {{/each}} +): {{getCustomTypeNamePascal ../this}} { + return new CustomArgValue({{constructor}}, [ + {{#each fields}} + {{camelCase name}}, + {{/each}} + ] as const); +} +{{else}} +export function create{{getCustomTypeNamePascal ../this}}Constructor{{constructor}}(): {{getCustomTypeNamePascal ../this}} { + return new CustomArgValue({{constructor}}, [] as const); +} +{{/if}} + +{{/each}} {{/if}} {{/if}} {{/each}} @@ -84,4 +113,4 @@ export const protocol = new Client({ endpoint: DEFAULT_TRP_ENDPOINT, headers: DEFAULT_HEADERS, envArgs: DEFAULT_ENV_ARGS, -}); \ No newline at end of file +}); diff --git a/bindgen/package.json.hbs b/bindgen/package.json.hbs deleted file mode 100644 index 0d8573d..0000000 --- a/bindgen/package.json.hbs +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "{{protocolName}}", - "version": "{{protocolVersion}}", - "description": "A simple Node.js library for the TX3 protocol", - "main": "dist/protocol.js", - "types": "dist/protocol.d.ts", - "scripts": { - "build": "tsc" - }, - "dependencies": { - "tx3-sdk": "^0.5.0" - }, - "devDependencies": { - "@types/node": "^22.14.1", - "tsx": "^4.19.3", - "typescript": "^5.8.3" - } -} diff --git a/bindgen/protocol.ts.hbs b/bindgen/protocol.ts.hbs deleted file mode 100644 index 07c9856..0000000 --- a/bindgen/protocol.ts.hbs +++ /dev/null @@ -1,67 +0,0 @@ -// This file is auto-generated by trix bindgen. - -import { - TRPClient, - type ArgValue, - type ClientOptions, - type SubmitParams, - type ResolveResponse, -} from "tx3-sdk/trp"; - - -export const DEFAULT_TRP_ENDPOINT = "{{trpEndpoint}}"; - -export const DEFAULT_HEADERS = { -{{#each headers}} - '{{@key}}': '{{this}}', -{{/each}} -}; - -export const DEFAULT_ENV_ARGS = { -{{#each envArgs}} - '{{@key}}': '{{this}}', -{{/each}} -}; - -{{#each transactions}} -export type {{pascalCase params_name}} = { -{{#each parameters}} - {{camelCase name}}: ArgValue; -{{/each}} -} - -export const {{constantCase constant_name}} = { - bytecode: "{{ir_bytes}}", - encoding: "hex", - version: "{{ir_version}}", -}; - -{{/each}} -export class Client { - readonly #client: TRPClient; - - constructor(options: ClientOptions) { - this.#client = new TRPClient(options); - } - - {{#each transactions}} - async {{camelCase function_name}}(args: {{pascalCase params_name}}): Promise { - return await this.#client.resolve({ - tir: {{constantCase constant_name}}, - args, - }); - } - {{/each}} - - - async submit(params: SubmitParams): Promise { - await this.#client.submit(params); - } -} - -// Create a default client instance -export const protocol = new Client({ - endpoint: DEFAULT_TRP_ENDPOINT, - headers: DEFAULT_HEADERS, - envArgs: DEFAULT_ENV_ARGS, -}); diff --git a/bindgen/trix-bindgen.toml b/bindgen/trix-bindgen.toml deleted file mode 100644 index 1698309..0000000 --- a/bindgen/trix-bindgen.toml +++ /dev/null @@ -1 +0,0 @@ -protocol_files = ["protocol.ts.hbs"] \ No newline at end of file diff --git a/bindgen/tsconfig.json.hbs b/bindgen/tsconfig.json.hbs deleted file mode 100644 index 8fe5683..0000000 --- a/bindgen/tsconfig.json.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "node16", - "declaration": true, - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["*.ts"], - "exclude": ["node_modules"] -} \ No newline at end of file From 8600d2fdadd407a04c7fe260531b309a176fac15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Fri, 28 Nov 2025 13:07:42 -0300 Subject: [PATCH 05/10] chore: add missing dependency for testing --- package-lock.json | 17 +++++++++++++++-- packages/tx3-sdk/package.json | 4 ++-- packages/tx3-sdk/src/trp/types.ts | 3 --- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2aa9ff..5fad274 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2780,6 +2781,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2883,6 +2885,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", @@ -3412,6 +3415,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3756,7 +3760,8 @@ "node_modules/bech32": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" }, "node_modules/bl": { "version": "5.1.0", @@ -3809,6 +3814,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -4548,6 +4554,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5787,6 +5794,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7922,6 +7930,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8218,6 +8227,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8881,6 +8891,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8986,6 +8997,7 @@ "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10506,6 +10518,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10532,7 +10545,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -10562,6 +10574,7 @@ "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.12", + "bech32": "^2.0.0", "jest": "^29.7.0", "rollup": "^4.34.9", "ts-jest": "^29.1.2", diff --git a/packages/tx3-sdk/package.json b/packages/tx3-sdk/package.json index f6aa781..d0c0e4c 100644 --- a/packages/tx3-sdk/package.json +++ b/packages/tx3-sdk/package.json @@ -31,6 +31,7 @@ "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.12", + "bech32": "^2.0.0", "jest": "^29.7.0", "rollup": "^4.34.9", "ts-jest": "^29.1.2", @@ -57,6 +58,5 @@ "./dist/trp/index.d.ts" ] } - }, - "dependencies": {} + } } diff --git a/packages/tx3-sdk/src/trp/types.ts b/packages/tx3-sdk/src/trp/types.ts index 1a276e2..d49cc33 100644 --- a/packages/tx3-sdk/src/trp/types.ts +++ b/packages/tx3-sdk/src/trp/types.ts @@ -145,9 +145,6 @@ export class CustomArgValue< this.fields = fields; } - // No `from` helper here: keep the class minimal and let tests build - // CustomArgValue using the constructor or test helpers. - /** * Type guard to check if a value is a CustomArgValue */ From 2598c8b09ed1329be8168961cacaa8e188043730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 2 Dec 2025 00:16:20 -0300 Subject: [PATCH 06/10] feat: web sdk with protocol updated --- .trix/client-lib/protocol.ts.hbs | 74 +++--- packages/tx3-sdk/src/trp/args.ts | 22 +- packages/tx3-sdk/src/trp/client.ts | 9 +- packages/tx3-sdk/src/trp/types.ts | 76 ++---- .../tx3-sdk/tests/custom-argvalue.test.ts | 234 ------------------ 5 files changed, 72 insertions(+), 343 deletions(-) delete mode 100644 packages/tx3-sdk/tests/custom-argvalue.test.ts diff --git a/.trix/client-lib/protocol.ts.hbs b/.trix/client-lib/protocol.ts.hbs index 6e8531d..e358e1b 100644 --- a/.trix/client-lib/protocol.ts.hbs +++ b/.trix/client-lib/protocol.ts.hbs @@ -1,3 +1,5 @@ +// This file is auto-generated by trix bindgen. + import { TRPClient, type ArgValue, @@ -6,13 +8,10 @@ import { type ResolveResponse, type ArgValueInt, type ArgValueBool, - type ArgValueString, type ArgValueBytes, + type ArgValueString, type ArgValueAddress, - type ArgValueUtxoSet, type ArgValueUtxoRef, - CustomArgValue, - ArgValue as ArgValueFactory, } from "tx3-sdk/trp"; @@ -30,53 +29,40 @@ export const DEFAULT_ENV_ARGS = { {{/each}} }; -{{#each transactions}} -{{#each parameters}} -{{#if (hasCustomDef this)}} -{{#if (getCustomTypeName this)}} -{{#each custom_def}} -{{#if @first}} -export type {{getCustomTypeNamePascal ../this}} = -{{else}} - | -{{/if}} - CustomArgValue<[ - {{#each fields}} - {{argValueTypeFor type_name "typescript"}}, - {{/each}} - ]> // Constructor {{constructor}}{{#if fields}} with fields: [{{#each fields}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}]{{/if}} -{{/each}}; - -// Factory functions to create {{getCustomTypeName this}} instances -{{#each custom_def}} -{{#if fields}} -export function create{{getCustomTypeNamePascal ../this}}Constructor{{constructor}}( - {{#each fields}} - {{camelCase name}}: {{argValueTypeFor type_name "typescript"}}, - {{/each}} -): {{getCustomTypeNamePascal ../this}} { - return new CustomArgValue({{constructor}}, [ - {{#each fields}} - {{camelCase name}}, - {{/each}} - ] as const); -} -{{else}} -export function create{{getCustomTypeNamePascal ../this}}Constructor{{constructor}}(): {{getCustomTypeNamePascal ../this}} { - return new CustomArgValue({{constructor}}, [] as const); -} -{{/if}} +// ============================================================================ +// Custom Type Definitions +// ============================================================================ + +{{#each customTypes}} +{{#each variants}} +/** + * {{../name}}{{#unless (eq name "Default")}}.{{name}}{{/unless}} (constructor {{index}}) + {{#each fields}} + * @field {{name}} - {{typeName}} + {{/each}} + */ +export type {{pascalCase ../name}}{{pascalCase name}} = { + constructor: {{index}}; + fields: [{{#each fields}}{{argValueType typeName "typescript"}}{{#unless @last}}, {{/unless}}{{/each}}]; +}; {{/each}} -{{/if}} -{{/if}} -{{/each}} +/** Union of all {{name}} variants */ +export type {{pascalCase name}} = {{#each variants}}{{pascalCase ../name}}{{pascalCase name}}{{#unless @last}} | {{/unless}}{{/each}}; + {{/each}} +// ============================================================================ +// Transaction Definitions +// ============================================================================ {{#each transactions}} export type {{pascalCase params_name}} = { {{#each parameters}} - {{camelCase name}}: {{typeFor type_name "typescript"}}; // {{type_name}} +{{#if isCustom}} + {{camelCase name}}: {{typeFor typeName "typescript"}}; +{{else}} + {{camelCase name}}: {{argValueType typeName "typescript"}}; +{{/if}} {{/each}} } diff --git a/packages/tx3-sdk/src/trp/args.ts b/packages/tx3-sdk/src/trp/args.ts index 2680de6..bd8545b 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -8,6 +8,7 @@ import { ArgValueError, CustomArgValue, ArgValue, + isCustomArgValue, } from "./types.js"; const MIN_I128 = -(BigInt(2) ** BigInt(127)); @@ -246,10 +247,10 @@ function utxoRefToValue(x: UtxoRef): string { } export function toJson(value: PrimitiveArgValue | CustomArgValue): any { - // Handle CustomArgValue with ordered fields - if (value instanceof CustomArgValue) { + // Handle CustomArgValue (plain object with constructor and fields) + if (isCustomArgValue(value)) { return { - constructor: value.constructorIndex, + constructor: value.constructor, fields: value.fields.map((field) => toJson(field)), }; } @@ -329,14 +330,17 @@ export function createUtxoRefArg( /** * Create a CustomArgValue with a constructor index and ordered fields - * @param constructorIndex - The constructor index (positive integer) + * @param constructorIndex - The constructor index (non-negative integer) * @param fields - Ordered array of ArgValue fields */ -export function createCustomArg( +export function createCustomArg( constructorIndex: number, - fields: TFields, -): CustomArgValue { - return new CustomArgValue(constructorIndex, fields); + fields: ArgValue[], +): CustomArgValue { + if (!Number.isInteger(constructorIndex) || constructorIndex < 0) { + throw new ArgValueError("Constructor index must be a non-negative integer"); + } + return { constructor: constructorIndex, fields }; } /** @@ -380,7 +384,7 @@ function valueToCustom(value: any): CustomArgValue { return fromJson(fieldValue, Type.Undefined); }); - return new CustomArgValue(constructorIndex, fields); + return { constructor: constructorIndex, fields }; } export { hexToBytes, bytesToHex, valueToCustom }; diff --git a/packages/tx3-sdk/src/trp/client.ts b/packages/tx3-sdk/src/trp/client.ts index 82f10e0..e81ce6e 100644 --- a/packages/tx3-sdk/src/trp/client.ts +++ b/packages/tx3-sdk/src/trp/client.ts @@ -2,7 +2,7 @@ import { toJson } from "./args.js"; import { ArgValue, ClientOptions, - CustomArgValue, + isCustomArgValue, JsonRpcError, NetworkError, ProtoTxRequest, @@ -111,17 +111,16 @@ export class Client { const newKey = force_snake_case ? key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase() : key; - // Check if it's already an ArgValue, otherwise convert it + // Check if it's already a PrimitiveArgValue if ( value && typeof value === "object" && "type" in value && "value" in value ) { - // It's already an ArgValue (either primitive or custom) result[newKey] = toJson(value); - } else if (value instanceof CustomArgValue) { - // It's a CustomArgValue + } else if (isCustomArgValue(value)) { + // It's a CustomArgValue (plain object with constructor/fields) result[newKey] = toJson(value); } else { // Convert primitive value to ArgValue diff --git a/packages/tx3-sdk/src/trp/types.ts b/packages/tx3-sdk/src/trp/types.ts index d49cc33..18f72ca 100644 --- a/packages/tx3-sdk/src/trp/types.ts +++ b/packages/tx3-sdk/src/trp/types.ts @@ -118,62 +118,36 @@ export const ArgValue = { * Type guard to check if a value is any kind of ArgValue (Primitive or Custom) */ isAny(value: unknown): value is ArgValue { - return this.is(value) || CustomArgValue.is(value); + return this.is(value) || isCustomArgValue(value); }, }; -// Custom argument value that can represent complex nested structures with ordered fields +/** + * Custom argument value - a plain object representing a sum type variant + * with a constructor index and ordered fields. + */ +export interface CustomArgValue { + constructor: number; + fields: ArgValue[]; +} + +/** + * Type guard to check if a value is a CustomArgValue + */ +export function isCustomArgValue(value: unknown): value is CustomArgValue { + return ( + value !== null && + typeof value === "object" && + "constructor" in value && + typeof (value as any).constructor === "number" && + "fields" in value && + Array.isArray((value as any).fields) + ); +} + +// Union of all ArgValue types export type ArgValue = PrimitiveArgValue | CustomArgValue; -export class CustomArgValue< - TFields extends readonly ArgValue[] = readonly ArgValue[], -> { - public readonly type = "Custom" as const; - public readonly constructorIndex: number; - public readonly fields: TFields; - - /** - * Create a new CustomArgValue with a constructor index and ordered fields - * @param constructorIndex - The constructor index (positive integer) - * @param fields - Ordered array of ArgValue fields - */ - constructor(constructorIndex: number, fields: TFields) { - if (!Number.isInteger(constructorIndex) || constructorIndex < 0) { - throw new Error("Constructor index must be a non-negative integer"); - } - this.constructorIndex = constructorIndex; - this.fields = fields; - } - - /** - * Type guard to check if a value is a CustomArgValue - */ - static is(value: unknown): value is CustomArgValue { - return value instanceof CustomArgValue; - } - - /** - * Convert fields to a plain array (for serialization) - */ - toArray(): ArgValue[] { - return [...this.fields]; - } - - /** - * Get a specific field by index - */ - getField(index: number): T | undefined { - return this.fields[index] as T | undefined; - } - - /** - * Get the number of fields - */ - get length(): number { - return this.fields.length; - } -} - export interface BytesEnvelope { content: string; encoding: "base64" | "hex"; diff --git a/packages/tx3-sdk/tests/custom-argvalue.test.ts b/packages/tx3-sdk/tests/custom-argvalue.test.ts deleted file mode 100644 index c8fb765..0000000 --- a/packages/tx3-sdk/tests/custom-argvalue.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { describe, test, expect } from "@jest/globals"; -import { - CustomArgValue, - createIntArg, - createStringArg, - createBoolArg, -} from "../src/trp/index.js"; - -function plainToCustom(obj: Record): { - custom: CustomArgValue; - keys: string[]; -} { - const keys = Object.keys(obj); - const fields = keys.map((k) => { - const v = obj[k]; - if (v && typeof v === "object" && !Array.isArray(v)) { - return plainToCustom(v).custom as any; - } - if (typeof v === "number") return createIntArg(v); - if (typeof v === "bigint") return createIntArg(v as bigint); - if (typeof v === "boolean") return createBoolArg(v); - if (typeof v === "string") return createStringArg(v); - throw new Error(`Unsupported test value type: ${v}`); - }); - return { custom: new CustomArgValue(0, fields as any), keys }; -} - -function getByName(custom: CustomArgValue, keys: string[], name: string): any { - const idx = keys.indexOf(name); - if (idx === -1) return undefined; - return custom.getField(idx as number); -} - -function customToPlain( - custom: CustomArgValue, - shape: Record, -): Record { - const out: Record = {}; - const keys = Object.keys(shape); - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - const expected = shape[k]; - const field = custom.getField(i as number) as any; - if (expected && typeof expected === "object" && !Array.isArray(expected)) { - out[k] = customToPlain(field as CustomArgValue, expected); - } else if (field && typeof field === "object" && "type" in field) { - out[k] = field.value; - } else { - out[k] = field; - } - } - return out; -} - -describe("CustomArgValue Type Safety", () => { - test("should create type-safe CustomArgValue from interface", () => { - const userShape = { - id: 1, - name: "Alice", - active: true, - profile: { - email: "alice@example.com", - settings: { - theme: "dark", - notifications: true, - }, - }, - }; - const { custom: user, keys: userKeys } = plainToCustom(userShape); - - expect(getByName(user, userKeys, "id")?.value).toBe(BigInt(1)); - expect(getByName(user, userKeys, "name")?.value).toBe("Alice"); - expect(getByName(user, userKeys, "active")?.value).toBe(true); - - const profile = getByName(user, userKeys, "profile") as CustomArgValue; - expect(profile).toBeInstanceOf(CustomArgValue); - // nested keys are positional (0=email,1=settings) - expect((profile.getField(0) as any)?.value).toBe("alice@example.com"); - - const settings = profile.getField(1) as CustomArgValue; - expect(settings).toBeInstanceOf(CustomArgValue); - expect((settings.getField(0) as any)?.value).toBe("dark"); - expect((settings.getField(1) as any)?.value).toBe(true); - }); - - test("should maintain type safety on get operations", () => { - const configShape = { host: "localhost", port: 8080, ssl: true }; - const { custom: config, keys: cfgKeys } = plainToCustom(configShape); - - const host = getByName(config, cfgKeys, "host"); // ArgValueString - const port = getByName(config, cfgKeys, "port"); // ArgValueInt - const ssl = getByName(config, cfgKeys, "ssl"); // ArgValueBool - - expect(host?.type).toBe("String"); - expect(port?.type).toBe("Int"); - expect(ssl?.type).toBe("Bool"); - - expect(host?.value).toBe("localhost"); - expect(port?.value).toBe(BigInt(8080)); - expect(ssl?.value).toBe(true); - }); - - test("should support type-safe modifications", () => { - const configShape = { host: "localhost", port: 8080, ssl: false }; - const { custom: config, keys: cfgKeys } = plainToCustom(configShape); - - // Type-safe modifications (mutate underlying fields by index) - const hostIdx = cfgKeys.indexOf("host"); - const portIdx = cfgKeys.indexOf("port"); - const sslIdx = cfgKeys.indexOf("ssl"); - (config as any).fields[hostIdx] = createStringArg("newhost"); - (config as any).fields[portIdx] = createIntArg(3000); - (config as any).fields[sslIdx] = createBoolArg(true); - - expect(getByName(config, cfgKeys, "host")?.value).toBe("newhost"); - expect(getByName(config, cfgKeys, "port")?.value).toBe(BigInt(3000)); - expect(getByName(config, cfgKeys, "ssl")?.value).toBe(true); - }); - - test("should convert to plain object correctly", () => { - const userShape = { - id: 1, - name: "Bob", - active: false, - profile: { - email: "bob@example.com", - settings: { - theme: "light", - notifications: false, - }, - }, - }; - const { custom: user } = plainToCustom(userShape); - const plain = customToPlain(user, userShape); - - expect(plain).toEqual({ - id: BigInt(1), - name: "Bob", - active: false, - profile: { - email: "bob@example.com", - settings: { - theme: "light", - notifications: false, - }, - }, - }); - }); - - test("should clone with type safety", () => { - const origShape = { host: "original", port: 5000, ssl: true }; - const { custom: original, keys: origKeys } = plainToCustom(origShape); - - const clone = new CustomArgValue(original.constructorIndex, [ - ...original.fields, - ]); - - const hostIdx = origKeys.indexOf("host"); - const portIdx = origKeys.indexOf("port"); - (clone as any).fields[hostIdx] = createStringArg("cloned"); - (clone as any).fields[portIdx] = createIntArg(6000); - - // Original should remain unchanged - expect((getByName(original, origKeys, "host") as any)?.value).toBe( - "original", - ); - expect((getByName(original, origKeys, "port") as any)?.value).toBe( - BigInt(5000), - ); - - // Clone should be modified - expect((clone.getField(hostIdx) as any).value).toBe("cloned"); - expect((clone.getField(portIdx) as any).value).toBe(BigInt(6000)); - }); - - test("should check equality correctly", () => { - const cfgShape1 = { host: "localhost", port: 8080, ssl: true }; - const { custom: config1 } = plainToCustom(cfgShape1); - const { custom: config2 } = plainToCustom(cfgShape1); - const { custom: config3 } = plainToCustom({ - host: "localhost", - port: 3000, - ssl: true, - }); - - expect(config1.toArray()).toEqual(config2.toArray()); - expect(config1.toArray()).not.toEqual(config3.toArray()); - }); - - test("should handle nested structures correctly", () => { - const userShape = { - id: 123, - name: "Charlie", - active: true, - profile: { - email: "charlie@example.com", - settings: { - theme: "auto", - notifications: true, - }, - }, - }; - const { custom: user, keys: userKeys } = plainToCustom(userShape); - - // Test nested access - const profile = getByName(user, userKeys, "profile") as CustomArgValue; - const settings = profile.getField(1) as CustomArgValue; - - expect((settings.getField(0) as any)?.value).toBe("auto"); - expect((settings.getField(1) as any)?.value).toBe(true); - - // Test nested modification (mutate fields) - (settings as any).fields[0] = createStringArg("dark"); - expect((settings.getField(0) as any)?.value).toBe("dark"); - - // Verify the change is reflected in the full object - const plainObject = customToPlain(user, userShape); - expect(plainObject.profile.settings.theme).toBe("dark"); - }); - - test("should maintain type information through serialization", () => { - const cfgShape = { host: "test", port: 9000, ssl: false }; - const { custom: config } = plainToCustom(cfgShape); - - // Convert to plain object and back using test helpers - const plain = customToPlain(config, cfgShape); - const { custom: recreated, keys: recreatedKeys } = plainToCustom(plain); - - expect(recreated.toArray()).toEqual(config.toArray()); - expect(getByName(recreated, recreatedKeys, "host")?.type).toBe("String"); - expect(getByName(recreated, recreatedKeys, "port")?.type).toBe("Int"); - expect(getByName(recreated, recreatedKeys, "ssl")?.type).toBe("Bool"); - }); -}); From 430696eccd779b9500d4eaf56e5fa9d3c41564c0 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Tue, 9 Dec 2025 16:42:29 -0300 Subject: [PATCH 07/10] feat: enhance JSON serialization for array handling --- packages/tx3-sdk/src/trp/args.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/tx3-sdk/src/trp/args.ts b/packages/tx3-sdk/src/trp/args.ts index bd8545b..ae34ffb 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -247,7 +247,9 @@ function utxoRefToValue(x: UtxoRef): string { } export function toJson(value: PrimitiveArgValue | CustomArgValue): any { - // Handle CustomArgValue (plain object with constructor and fields) + if (Array.isArray(value)) { + return value.map((v) => toJson(v)); + } if (isCustomArgValue(value)) { return { constructor: value.constructor, @@ -381,6 +383,21 @@ function valueToCustom(value: any): CustomArgValue { return valueToCustom(fieldValue); } + // convert each element to an ArgValue representation + if (Array.isArray(fieldValue)) { + return fieldValue.map((elem: any) => { + if ( + elem && + typeof elem === "object" && + "constructor" in elem && + "fields" in elem + ) { + return valueToCustom(elem); + } + return fromJson(elem, Type.Undefined); + }); + } + return fromJson(fieldValue, Type.Undefined); }); From d71beb4c2a62ddc7ecaff77fba78c0ed27b51770 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Tue, 9 Dec 2025 16:47:52 -0300 Subject: [PATCH 08/10] feat: enhance argument conversion to support arrays and nested objects --- packages/tx3-sdk/src/trp/client.ts | 67 ++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/tx3-sdk/src/trp/client.ts b/packages/tx3-sdk/src/trp/client.ts index e81ce6e..bd248d3 100644 --- a/packages/tx3-sdk/src/trp/client.ts +++ b/packages/tx3-sdk/src/trp/client.ts @@ -106,31 +106,64 @@ export class Client { args: Record, force_snake_case: boolean, ): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(args)) { - const newKey = force_snake_case - ? key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase() - : key; - // Check if it's already a PrimitiveArgValue + // Helper to convert a single value into the JSON shape expected by TRP. + const convertValue = (value: any): any => { + // Already a PrimitiveArgValue if ( value && typeof value === "object" && "type" in value && "value" in value ) { - result[newKey] = toJson(value); - } else if (isCustomArgValue(value)) { - // It's a CustomArgValue (plain object with constructor/fields) - result[newKey] = toJson(value); - } else { - // Convert primitive value to ArgValue - try { - result[newKey] = toJson(ArgValue.from(value)); - } catch { - // If conversion fails, use the value as-is - result[newKey] = value; + return toJson(value); + } + + if (isCustomArgValue(value)) { + return toJson(value); + } + + if (Array.isArray(value)) { + return value.map((el) => { + if (el && typeof el === "object" && "type" in el && "value" in el) { + return toJson(el); + } + + if (isCustomArgValue(el)) { + return toJson(el); + } + + try { + return toJson(ArgValue.from(el)); + } catch { + return convertValue(el); + } + }); + } + + // Try converting primitives + try { + return toJson(ArgValue.from(value)); + } catch { + // If plain object, convert each field recursively + if (value && typeof value === "object") { + const objResult: Record = {}; + for (const [k, v] of Object.entries(value)) { + objResult[k] = convertValue(v); + } + return objResult; } + + return value; } + }; + + const result: Record = {}; + for (const [key, value] of Object.entries(args)) { + const newKey = force_snake_case + ? key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase() + : key; + + result[newKey] = convertValue(value); } return result; } From 4ff8b8452dcba622cd4263d8179a91b845a33da4 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 10 Dec 2025 13:31:22 -0300 Subject: [PATCH 09/10] feat: remove redundant comment in convertArgsToJson method --- package-lock.json | 13 +------------ packages/tx3-sdk/src/trp/client.ts | 1 - 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fad274..1f09e89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,7 +151,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2781,7 +2780,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2885,7 +2883,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", @@ -3415,7 +3412,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3814,7 +3810,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -4554,7 +4549,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5794,7 +5788,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7930,7 +7923,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8227,7 +8219,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8891,7 +8882,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8997,7 +8987,6 @@ "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10518,7 +10507,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10545,6 +10533,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } diff --git a/packages/tx3-sdk/src/trp/client.ts b/packages/tx3-sdk/src/trp/client.ts index bd248d3..f080c68 100644 --- a/packages/tx3-sdk/src/trp/client.ts +++ b/packages/tx3-sdk/src/trp/client.ts @@ -106,7 +106,6 @@ export class Client { args: Record, force_snake_case: boolean, ): Record { - // Helper to convert a single value into the JSON shape expected by TRP. const convertValue = (value: any): any => { // Already a PrimitiveArgValue if ( From a626d95cb0372d3a5ca861fbeb4c9759d129e0ed Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 11 Dec 2025 14:52:05 -0300 Subject: [PATCH 10/10] enhance toJson function to support additional object types and improve error handling --- packages/tx3-sdk/src/trp/args.ts | 65 ++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/tx3-sdk/src/trp/args.ts b/packages/tx3-sdk/src/trp/args.ts index ae34ffb..d152004 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -5,6 +5,7 @@ import { BytesEnvelope, Type, UtxoRef, + Utxo, ArgValueError, CustomArgValue, ArgValue, @@ -246,7 +247,20 @@ function utxoRefToValue(x: UtxoRef): string { return `${bytesToHex(x.txid)}#${x.index}`; } -export function toJson(value: PrimitiveArgValue | CustomArgValue): any { +export function toJson(value: PrimitiveArgValue | CustomArgValue | any): any { + const isPrimitiveArg = (v: any) => + typeof v === "number" || + typeof v === "bigint" || + typeof v === "string" || + typeof v === "boolean" || + v instanceof Uint8Array || + v instanceof Set || + (v && typeof v === "object" && "txid" in v); + + if (isPrimitiveArg(value)) { + return toJson(ArgValue.from(value)); + } + if (Array.isArray(value)) { return value.map((v) => toJson(v)); } @@ -258,30 +272,33 @@ export function toJson(value: PrimitiveArgValue | CustomArgValue): any { } // Handle PrimitiveArgValue - switch (value.type) { - case "Int": - return bigintToValue(value.value); - case "Bool": - return value.value; - case "String": - return value.value; - case "Bytes": - return `0x${bytesToHex(value.value)}`; - case "Address": - return bytesToHex(value.value); - case "UtxoSet": - return Array.from(value.value).map((utxo) => ({ - ref: utxoRefToValue(utxo.ref), - address: bytesToHex(utxo.address), - datum: utxo.datum, - assets: utxo.assets, - script: utxo.script, - })); - case "UtxoRef": - return utxoRefToValue(value.value); - default: - throw new ArgValueError(`Unknown ArgValue type: ${(value as any).type}`); + if (value && typeof value === "object" && "type" in value) { + switch (value.type) { + case "Int": + return bigintToValue(value.value); + case "Bool": + return value.value; + case "String": + return value.value; + case "Bytes": + return `0x${bytesToHex(value.value)}`; + case "Address": + return bytesToHex(value.value); + case "UtxoSet": + return Array.from(value.value as Set).map((utxo: Utxo) => ({ + ref: utxoRefToValue(utxo.ref), + address: bytesToHex(utxo.address), + datum: utxo.datum, + assets: utxo.assets, + script: utxo.script, + })); + case "UtxoRef": + return utxoRefToValue(value.value); + default: + throw new ArgValueError(`Unknown ArgValue type: ${value.type}`); + } } + throw new ArgValueError(`Cannot convert value to JSON: ${value}`); } export function fromJson(value: any, target: Type): PrimitiveArgValue {