diff --git a/.trix/client-lib/protocol.ts.hbs b/.trix/client-lib/protocol.ts.hbs index cc580a7..e358e1b 100644 --- a/.trix/client-lib/protocol.ts.hbs +++ b/.trix/client-lib/protocol.ts.hbs @@ -6,6 +6,12 @@ import { type ClientOptions, type SubmitParams, type ResolveResponse, + type ArgValueInt, + type ArgValueBool, + type ArgValueBytes, + type ArgValueString, + type ArgValueAddress, + type ArgValueUtxoRef, } from "tx3-sdk/trp"; @@ -23,10 +29,40 @@ export const DEFAULT_ENV_ARGS = { {{/each}} }; +// ============================================================================ +// 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}} +/** 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}}: ArgValue | {{typeFor type_name "typescript"}}; // {{type_name}} +{{#if isCustom}} + {{camelCase name}}: {{typeFor typeName "typescript"}}; +{{else}} + {{camelCase name}}: {{argValueType typeName "typescript"}}; +{{/if}} {{/each}} } 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 diff --git a/package-lock.json b/package-lock.json index d19daa9..1f09e89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3756,7 +3756,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", @@ -10555,13 +10556,14 @@ } }, "packages/tx3-sdk": { - "version": "0.4.0", + "version": "0.6.0", "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-commonjs": "^28.0.2", "@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/args.ts b/packages/tx3-sdk/src/trp/args.ts index df38e41..d152004 100644 --- a/packages/tx3-sdk/src/trp/args.ts +++ b/packages/tx3-sdk/src/trp/args.ts @@ -1,27 +1,37 @@ -import { bech32 } from 'bech32'; +import { bech32 } from "bech32"; -import { ArgValue, BytesEnvelope, Type, UtxoRef, ArgValueError } from './types.js'; +import { + PrimitiveArgValue, + BytesEnvelope, + Type, + UtxoRef, + Utxo, + ArgValueError, + CustomArgValue, + ArgValue, + isCustomArgValue, +} 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); - 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; @@ -31,8 +41,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 { @@ -43,24 +53,27 @@ function base64ToBytes(s: string): Uint8Array { bytes[i] = binary.charCodeAt(i); } return bytes; - } catch (error) { + } catch { throw new ArgValueError(`Invalid base64: ${s}`); } } 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 +86,7 @@ function bigintToValue(i: bigint): any { carry = sum >> 8; } } - + return `0x${bytesToHex(bytes)}`; } } @@ -92,9 +105,9 @@ function numberToBigint(x: number): bigint { } const bigintValue = BigInt(x); - + checkBigintInRange(bigintValue); - + return bigintValue; } @@ -103,47 +116,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 +166,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 +195,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 +206,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)) { + const index = Number.parseInt(indexStr, 10); + + if (Number.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 +247,72 @@ function utxoRefToValue(x: UtxoRef): string { return `${bytesToHex(x.txid)}#${x.index}`; } -export function toJson(value: ArgValue): any { - 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}`); +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)); + } + if (isCustomArgValue(value)) { + return { + constructor: value.constructor, + fields: value.fields.map((field) => toJson(field)), + }; + } + + // Handle PrimitiveArgValue + 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): 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: @@ -275,30 +320,105 @@ 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 utility functions -export { hexToBytes, bytesToHex }; +/** + * Create a CustomArgValue with a constructor index and ordered fields + * @param constructorIndex - The constructor index (non-negative integer) + * @param fields - Ordered array of ArgValue fields + */ +export function createCustomArg( + constructorIndex: number, + fields: ArgValue[], +): CustomArgValue { + if (!Number.isInteger(constructorIndex) || constructorIndex < 0) { + throw new ArgValueError("Constructor index must be a non-negative integer"); + } + return { constructor: 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); + } + + // 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); + }); + + 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 1705a1d..f080c68 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, + isCustomArgValue, 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,67 @@ 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 convertValue = (value: any): any => { + // Already a PrimitiveArgValue + if ( + value && + typeof value === "object" && + "type" in value && + "value" in 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] = toJson(ArgValue.is(value) ? value : ArgValue.from(value)); + + result[newKey] = convertValue(value); } return result; } @@ -120,7 +172,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 +187,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..18f72ca 100644 --- a/packages/tx3-sdk/src/trp/types.ts +++ b/packages/tx3-sdk/src/trp/types.ts @@ -19,97 +19,147 @@ 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 (value instanceof Set) return this.fromUtxoSet(value); + if (typeof value === "object" && value !== null && "txid" in value) + return this.fromUtxoRef(value); + 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) || isCustomArgValue(value); + }, }; +/** + * 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 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 +174,7 @@ export interface ResolveResponse { } export interface VKeyWitness { - type: 'vkey'; + type: "vkey"; key: BytesEnvelope; signature: BytesEnvelope; } @@ -150,35 +200,35 @@ 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) { 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) { super(`HTTP error ${statusCode}: ${message}`); - this.name = 'StatusCodeError'; + this.name = "StatusCodeError"; } } export class JsonRpcError extends TrpError { 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/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"); }); }); });