From 750bed2fb138bc4655d1804930844fadd52df9e0 Mon Sep 17 00:00:00 2001 From: bakarezainab Date: Mon, 31 Aug 2026 08:34:51 +0100 Subject: [PATCH] Auto-Generate --- src/client.ts | 25 ++- src/contract/abstract.ts | 130 +++++++++++++++ src/contract/bindings.ts | 332 ++++++++++++++++++++++++++++++++++++ src/contract/index.ts | 18 ++ src/contract/spec.ts | 352 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + 6 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 src/contract/abstract.ts create mode 100644 src/contract/bindings.ts create mode 100644 src/contract/spec.ts diff --git a/src/client.ts b/src/client.ts index 5ca697d..bffa830 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,8 +1,10 @@ -import { Horizon } from '@stellar/stellar-sdk'; +import { Horizon, xdr } from '@stellar/stellar-sdk'; import { HORIZON_URLS, SOROBAN_RPC_URLS, DEFAULT_NETWORK, SDK_VERSION } from './constants'; import { TrustFlowError } from './errors'; import type { Network, ClientConfig } from './types'; import { IPFSStorage } from './storage'; +import { createContractBinding, SorobanContractClient } from './contract'; + /** * TrustFlowClient is the main entry point for interacting with the TrustFlow Protocol. @@ -174,6 +176,26 @@ export class TrustFlowClient { } } + /** + * Generates auto-bound, type-safe contract client methods from Soroban spec entries. + * + * @param specEntries - Array of Soroban spec entries (XDR base64 strings, ScSpecEntry objects, or Buffers) + * @param overrideContractId - Optional contract ID override (defaults to client's contractId) + * @returns SorobanContractClient instance with bound spec methods + * + * @example + * ```typescript + * const binding = client.createContractBinding(specXdrEntries); + * const res = await binding.methods.create_escrow({ depositor, beneficiary, amount, duration }, caller); + * ``` + */ + createContractBinding = Record>( + specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + overrideContractId?: string, + ): SorobanContractClient & T & { methods: T } { + return createContractBinding(this, specEntries, overrideContractId); + } + /** * Returns a summary of the client configuration. * @@ -195,3 +217,4 @@ export class TrustFlowClient { }; } } + diff --git a/src/contract/abstract.ts b/src/contract/abstract.ts new file mode 100644 index 0000000..2868868 --- /dev/null +++ b/src/contract/abstract.ts @@ -0,0 +1,130 @@ +import { xdr } from '@stellar/stellar-sdk'; +import type { TrustFlowClient } from '../client'; +import type { ContractCallResult } from '../types/contract'; +import type { SimulationResult } from './simulate'; +import type { SignAndSubmitFn } from './invoke'; +import { SorobanSpec } from './spec'; + +/** + * Base abstract class for Soroban type-safe contract clients and generated bindings. + * + * Implementations provide concrete or auto-generated methods matching contract + * Soroban specs/XDR, ensuring all contract calls are compile-time checked and type-safe. + * + * @example + * ```typescript + * class MyContractClient extends AbstractContractClient { + * async createEscrow(params: CreateParams, caller: string) { + * return this.invoke('create_escrow', params, caller); + * } + * } + * ``` + */ +export abstract class AbstractContractClient { + readonly client: TrustFlowClient; + readonly contractId: string; + readonly spec: SorobanSpec; + + /** + * Creates an instance of AbstractContractClient. + * + * @param client - TrustFlowClient instance + * @param specEntries - Array of Soroban spec entries (XDR base64 strings, ScSpecEntry objects, or Buffers) + * @param contractId - Optional contract ID override; defaults to client.contractId + */ + constructor( + client: TrustFlowClient, + specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + contractId?: string, + ) { + this.client = client; + this.contractId = contractId ?? client.contractId; + this.spec = new SorobanSpec(specEntries); + } + + /** + * Encodes JS function parameters into Soroban ScVal array matching the spec for `methodName`. + * + * @param methodName - Name of the contract function + * @param args - Positional array or object map of arguments + * @returns Array of encoded ScVal objects + */ + encodeArgs(methodName: string, args: Record | unknown[]): xdr.ScVal[] { + return this.spec.encodeArgs(methodName, args); + } + + /** + * Decodes a returned ScVal object into a native JS value based on the function output spec. + * + * @param methodName - Name of the contract function + * @param scVal - ScVal returned from contract simulation or invocation + * @returns Native JS representation of the return value + */ + decodeReturnValue(methodName: string, scVal: xdr.ScVal): unknown { + return this.spec.decodeReturnValue(methodName, scVal); + } + + /** + * Parses and constructs the full XDR payload details for a contract call. + * + * @param methodName - Name of the contract method + * @param args - Positional array or object map of arguments + * @returns Metadata object containing method name, encoded ScVals, and base64 XDR array + */ + parseXDRPayload( + methodName: string, + args: Record | unknown[], + ): { + method: string; + scVals: xdr.ScVal[]; + xdrBase64: string[]; + } { + const scVals = this.encodeArgs(methodName, args); + const xdrBase64 = scVals.map((val) => val.toXDR('base64')); + return { + method: methodName, + scVals, + xdrBase64, + }; + } + + /** + * Invokes a contract method using typed spec encoding. + * + * @param methodName - Function name defined in the contract spec + * @param args - Arguments as an array or name-value object + * @param caller - Address of the caller initiating the transaction + * @param signAndSubmit - Optional callback to sign and submit the transaction XDR + * @returns Promise resolving to ContractCallResult with decoded result + */ + abstract invoke( + methodName: string, + args: Record | unknown[], + caller: string, + signAndSubmit?: SignAndSubmitFn, + ): Promise; + + /** + * Reads contract state by simulating a read-only contract function. + * + * @param methodName - Function name defined in the contract spec + * @param args - Arguments as an array or name-value object + * @returns Promise resolving to decoded return value + */ + abstract read( + methodName: string, + args?: Record | unknown[], + ): Promise; + + /** + * Simulates a contract method call to estimate gas and execution outcome. + * + * @param methodName - Function name defined in the contract spec + * @param args - Arguments as an array or name-value object + * @returns Promise resolving to SimulationResult + */ + abstract simulate( + methodName: string, + args?: Record | unknown[], + ): Promise; +} diff --git a/src/contract/bindings.ts b/src/contract/bindings.ts new file mode 100644 index 0000000..d3e61e8 --- /dev/null +++ b/src/contract/bindings.ts @@ -0,0 +1,332 @@ +import { xdr } from '@stellar/stellar-sdk'; +import type { TrustFlowClient } from '../client'; +import type { ContractCallResult } from '../types/contract'; +import type { SimulationResult } from './simulate'; +import type { SignAndSubmitFn } from './invoke'; +import { invokeContract } from './invoke'; +import { readContractState } from './read'; +import { simulateContractCall } from './simulate'; +import { AbstractContractClient } from './abstract'; +import { SorobanSpec } from './spec'; +import { TrustFlowError } from '../errors'; + +/** + * Concrete implementation of `AbstractContractClient` created from Soroban Spec entries. + * Executes spec-validated contract calls, reads, and simulations. + */ +export class SorobanContractClient extends AbstractContractClient { + /** Map of dynamically generated contract methods bound to this client instance */ + readonly methods: Record = {}; + + constructor( + client: TrustFlowClient, + specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + contractId?: string, + ) { + super(client, specEntries, contractId); + this.bindMethods(); + } + + private bindMethods(): void { + for (const [fnName, fnSpec] of this.spec.functions.entries()) { + const invokeFn = async ( + args: Record | unknown[], + caller: string, + signAndSubmit?: SignAndSubmitFn, + ) => { + return this.invoke(fnName, args, caller, signAndSubmit); + }; + + const readFn = async (args?: Record | unknown[]) => { + return this.read(fnName, args); + }; + + const simulateFn = async (args?: Record | unknown[]) => { + return this.simulate(fnName, args); + }; + + this.methods[fnName] = invokeFn; + (this.methods as Record)[`read_${fnName}`] = readFn; + (this.methods as Record)[`simulate_${fnName}`] = simulateFn; + + // CamelCase alias + const camelName = toCamelCase(fnName); + if (camelName !== fnName) { + this.methods[camelName] = invokeFn; + const camelCap = camelName.charAt(0).toUpperCase() + camelName.slice(1); + (this.methods as Record)[`read${camelCap}`] = readFn; + (this.methods as Record)[`simulate${camelCap}`] = simulateFn; + } + + // Also attach directly onto this instance if not colliding with existing properties + if (!(fnName in this)) { + (this as Record)[fnName] = invokeFn; + } + if (!(camelName in this)) { + (this as Record)[camelName] = invokeFn; + } + } + } + + /** + * Invokes a contract method using typed Soroban spec encoding. + * + * @param methodName - Function name defined in contract spec + * @param args - Positional arguments array or named parameter object map + * @param caller - Address of the caller initiating the transaction + * @param signAndSubmit - Optional callback to sign and submit transaction XDR + */ + async invoke( + methodName: string, + args: Record | unknown[], + caller: string, + signAndSubmit?: SignAndSubmitFn, + ): Promise { + const scVals = this.encodeArgs(methodName, args); + const result = await invokeContract(this.client, methodName, scVals, caller, signAndSubmit); + let decoded: T | undefined = undefined; + if (result.success && result.returnValue) { + decoded = this.decodeReturnValue(methodName, result.returnValue as xdr.ScVal) as T; + } + return { + ...result, + result: decoded, + }; + } + + /** + * Reads contract state by simulating a read-only contract method. + * + * @param methodName - Function name defined in contract spec + * @param args - Arguments array or object map + */ + async read( + methodName: string, + args: Record | unknown[] = [], + ): Promise { + const scVals = this.encodeArgs(methodName, args); + const rawResult = await readContractState(this.client, methodName, scVals); + if (rawResult && typeof rawResult === 'object' && 'result' in (rawResult as any)) { + const retval = (rawResult as any).result?.retval; + if (retval) { + return this.decodeReturnValue(methodName, retval as xdr.ScVal) as T; + } + } + return rawResult as T; + } + + /** + * Simulates execution of a contract method for dry-run validation and fee estimation. + * + * @param methodName - Function name defined in contract spec + * @param args - Arguments array or object map + */ + async simulate( + methodName: string, + args: Record | unknown[] = [], + ): Promise { + const payload = this.parseXDRPayload(methodName, args); + const combinedXdr = payload.xdrBase64.join(''); + return simulateContractCall(this.client, combinedXdr); + } +} + +/** + * Creates dynamic type-safe contract client bindings from Soroban spec XDR entries. + * + * @param client - TrustFlowClient instance + * @param specEntries - Array of Soroban spec entries (XDR base64 strings, ScSpecEntry objects, or Buffers) + * @param contractId - Optional contract ID override + * @returns SorobanContractClient instance with bound spec methods + * + * @example + * ```typescript + * const binding = createContractBinding(client, specXdrEntries); + * const result = await binding.methods.create_escrow({ depositor, beneficiary, amount, duration }, caller); + * ``` + */ +export function createContractBinding = Record>( + client: TrustFlowClient, + specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + contractId?: string, +): SorobanContractClient & T & { methods: T } { + const binding = new SorobanContractClient(client, specEntries, contractId); + return binding as SorobanContractClient & T & { methods: T }; +} + +/** + * Alias for `createContractBinding`. + * Auto-generates contract bindings from Soroban spec XDR. + */ +export const generateContractBindings = createContractBinding; + +/** + * Generates strongly-typed TypeScript client source code from Soroban contract spec entries. + * The generated code provides compile-time checked contract bindings that mirror the contract ABI. + * + * @param specEntries - Array of Soroban spec entries (XDR base64 strings, ScSpecEntry objects, or Buffers) + * @param options - Code generation options, including target class name + * @returns TypeScript source code string + * + * @example + * ```typescript + * const tsCode = generateTypeScriptBindings(specEntries, { className: 'EscrowContractClient' }); + * fs.writeFileSync('src/contracts/EscrowContractClient.ts', tsCode); + * ``` + */ +export function generateTypeScriptBindings( + specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + options: { className?: string } = {}, +): string { + const spec = new SorobanSpec(specEntries); + const className = options.className || 'GeneratedContractClient'; + + const lines: string[] = [ + '// Auto-generated by @trustflow/sdk Soroban Spec Binding Generator', + '// DO NOT EDIT MANUALLY - re-generate using generateTypeScriptBindings()', + '', + "import { AbstractContractClient, TrustFlowClient, SignAndSubmitFn, ContractCallResult, SimulationResult } from '@trustflow/sdk';", + '', + ]; + + // Generate struct interfaces + for (const [stName, stSpec] of spec.structs.entries()) { + if (stSpec.doc) { + lines.push(`/** ${stSpec.doc} */`); + } + lines.push(`export interface ${stName} {`); + for (const field of stSpec.fields) { + const fieldType = mapScSpecTypeToTs(field.type); + const docComment = field.doc ? ` /** ${field.doc} */\n` : ''; + lines.push(`${docComment} ${field.name}: ${fieldType};`); + } + lines.push('}'); + lines.push(''); + } + + // Generate enum types + for (const [enName, enSpec] of spec.enums.entries()) { + if (enSpec.doc) { + lines.push(`/** ${enSpec.doc} */`); + } + lines.push(`export enum ${enName} {`); + for (const c of enSpec.cases) { + const docComment = c.doc ? ` /** ${c.doc} */\n` : ''; + lines.push(`${docComment} ${c.name} = ${c.value},`); + } + lines.push('}'); + lines.push(''); + } + + // Generate union types + for (const [unName, unSpec] of spec.unions.entries()) { + if (unSpec.doc) { + lines.push(`/** ${unSpec.doc} */`); + } + const unionCases = unSpec.cases.map((c) => `'${c.name}'`).join(' | '); + lines.push(`export type ${unName} = ${unionCases || 'string'};`); + lines.push(''); + } + + // Generate Contract Client Class + lines.push(`/**`); + lines.push(` * Auto-generated type-safe contract client for Soroban spec.`); + lines.push(` */`); + lines.push(`export class ${className} extends AbstractContractClient {`); + lines.push(` constructor(client: TrustFlowClient, specEntries: any[], contractId?: string) {`); + lines.push(` super(client, specEntries, contractId);`); + lines.push(` }`); + lines.push(''); + + for (const [fnName, fnSpec] of spec.functions.entries()) { + const camelName = toCamelCase(fnName); + const returnType = + fnSpec.outputs.length > 0 ? mapScSpecTypeToTs(fnSpec.outputs[0]) : 'unknown'; + + // Build args interface + const argsTypeFields = fnSpec.inputs + .map((inp) => `${inp.name}: ${mapScSpecTypeToTs(inp.type)}`) + .join('; '); + const argsParamType = argsTypeFields ? `{ ${argsTypeFields} }` : 'Record'; + + if (fnSpec.doc) { + lines.push(` /** ${fnSpec.doc} */`); + } + lines.push(` async ${camelName}(`); + lines.push(` args: ${argsParamType},`); + lines.push(` caller: string,`); + lines.push(` signAndSubmit?: SignAndSubmitFn,`); + lines.push(` ): Promise {`); + lines.push( + ` return this.invoke<${returnType}>('${fnName}', args, caller, signAndSubmit);`, + ); + lines.push(` }`); + lines.push(''); + + // Read method + lines.push(` async read${capitalize(camelName)}(args: ${argsParamType}): Promise<${returnType}> {`); + lines.push(` return this.read<${returnType}>('${fnName}', args);`); + lines.push(` }`); + lines.push(''); + + // Simulate method + lines.push( + ` async simulate${capitalize(camelName)}(args: ${argsParamType}): Promise {`, + ); + lines.push(` return this.simulate('${fnName}', args);`); + lines.push(` }`); + lines.push(''); + } + + lines.push('}'); + lines.push(''); + + return lines.join('\n'); +} + +function mapScSpecTypeToTs(typeDef: xdr.ScSpecTypeDef): string { + const kind = typeDef.switch().name; + switch (kind) { + case 'scSpecTypeBool': + return 'boolean'; + case 'scSpecTypeVoid': + return 'void'; + case 'scSpecTypeU32': + case 'scSpecTypeI32': + return 'number'; + case 'scSpecTypeU64': + case 'scSpecTypeI64': + case 'scSpecTypeU128': + case 'scSpecTypeI128': + case 'scSpecTypeU256': + case 'scSpecTypeI256': + case 'scSpecTypeTime': + case 'scSpecTypeDuration': + return 'bigint'; + case 'scSpecTypeString': + case 'scSpecTypeSymbol': + case 'scSpecTypeAddress': + return 'string'; + case 'scSpecTypeBytes': + case 'scSpecTypeBytesN': + return 'Uint8Array | string'; + case 'scSpecTypeOption': + return `${mapScSpecTypeToTs(typeDef.option().valueType())} | null`; + case 'scSpecTypeVec': + return `${mapScSpecTypeToTs(typeDef.vec().elementType())}[]`; + case 'scSpecTypeMap': + return `Record<${mapScSpecTypeToTs(typeDef.map().keyType())}, ${mapScSpecTypeToTs(typeDef.map().valueType())}>`; + case 'scSpecTypeUdt': + return typeDef.udt().name().toString(); + default: + return 'unknown'; + } +} + +function toCamelCase(str: string): string { + return str.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase()); +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/src/contract/index.ts b/src/contract/index.ts index e661ba2..152d770 100644 --- a/src/contract/index.ts +++ b/src/contract/index.ts @@ -1,3 +1,21 @@ +export { AbstractContractClient } from './abstract'; +export { + SorobanSpec, + type SpecFunction, + type SpecFunctionInput, + type SpecStruct, + type SpecStructField, + type SpecEnum, + type SpecEnumCase, + type SpecUnion, + type SpecUnionCase, +} from './spec'; +export { + SorobanContractClient, + createContractBinding, + generateContractBindings, + generateTypeScriptBindings, +} from './bindings'; export { invokeContract, type SignAndSubmitFn } from './invoke'; export { readContractState } from './read'; export { simulateContractCall } from './simulate'; diff --git a/src/contract/spec.ts b/src/contract/spec.ts new file mode 100644 index 0000000..98f6817 --- /dev/null +++ b/src/contract/spec.ts @@ -0,0 +1,352 @@ +import { Address, nativeToScVal, scValToNative, xdr } from '@stellar/stellar-sdk'; +import { TrustFlowError } from '../errors'; + +/** Represents an input parameter in a Soroban function spec */ +export interface SpecFunctionInput { + name: string; + doc: string; + type: xdr.ScSpecTypeDef; +} + +/** Represents a function spec entry in a Soroban contract ABI */ +export interface SpecFunction { + name: string; + doc: string; + inputs: SpecFunctionInput[]; + outputs: xdr.ScSpecTypeDef[]; +} + +/** Represents a field in a Soroban struct UDT spec */ +export interface SpecStructField { + name: string; + doc: string; + type: xdr.ScSpecTypeDef; +} + +/** Represents a user-defined struct spec entry */ +export interface SpecStruct { + name: string; + doc: string; + lib: string; + fields: SpecStructField[]; +} + +/** Represents an enum case in a Soroban enum UDT spec */ +export interface SpecEnumCase { + name: string; + doc: string; + value: number; +} + +/** Represents a user-defined enum spec entry */ +export interface SpecEnum { + name: string; + doc: string; + lib: string; + cases: SpecEnumCase[]; +} + +/** Represents a case in a Soroban union UDT spec */ +export interface SpecUnionCase { + name: string; + doc: string; + typeList?: xdr.ScSpecTypeDef[]; +} + +/** Represents a user-defined union spec entry */ +export interface SpecUnion { + name: string; + doc: string; + lib: string; + cases: SpecUnionCase[]; +} + +/** + * Parser and validator for Soroban Contract Specification (XDR spec entries). + * Converts JavaScript values to/from Soroban `xdr.ScVal` types according to contract ABIs. + */ +export class SorobanSpec { + readonly entries: xdr.ScSpecEntry[]; + readonly functions: Map = new Map(); + readonly structs: Map = new Map(); + readonly enums: Map = new Map(); + readonly unions: Map = new Map(); + + /** + * Constructs a new SorobanSpec parser. + * + * @param specEntries - Array of Soroban spec entries (XDR base64 strings, ScSpecEntry objects, or Buffers) + */ + constructor(specEntries: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[]) { + this.entries = this.parseEntries(specEntries); + this.indexEntries(); + } + + private parseEntries( + inputList: (xdr.ScSpecEntry | string | Uint8Array | Buffer)[], + ): xdr.ScSpecEntry[] { + const result: xdr.ScSpecEntry[] = []; + for (const item of inputList) { + if (item instanceof xdr.ScSpecEntry) { + result.push(item); + } else if (typeof item === 'string') { + try { + result.push(xdr.ScSpecEntry.fromXDR(item, 'base64')); + } catch { + result.push(xdr.ScSpecEntry.fromXDR(item, 'hex')); + } + } else if (item instanceof Uint8Array || Buffer.isBuffer(item)) { + result.push(xdr.ScSpecEntry.fromXDR(Buffer.from(item))); + } + } + return result; + } + + private indexEntries(): void { + for (const entry of this.entries) { + const kind = entry.switch().name; + if (kind === 'scSpecEntryFunctionV0') { + const fn = entry.functionV0(); + const fnName = fn.name().toString(); + const specFn: SpecFunction = { + name: fnName, + doc: fn.doc().toString(), + inputs: fn.inputs().map((i) => ({ + name: i.name().toString(), + doc: i.doc().toString(), + type: i.type(), + })), + outputs: fn.outputs(), + }; + this.functions.set(fnName, specFn); + } else if (kind === 'scSpecEntryUdtStructV0') { + const st = entry.udtStructV0(); + const stName = st.name().toString(); + const specSt: SpecStruct = { + name: stName, + doc: st.doc().toString(), + lib: st.lib().toString(), + fields: st.fields().map((f) => ({ + name: f.name().toString(), + doc: f.doc().toString(), + type: f.type(), + })), + }; + this.structs.set(stName, specSt); + } else if (kind === 'scSpecEntryUdtEnumV0') { + const en = entry.udtEnumV0(); + const enName = en.name().toString(); + const specEn: SpecEnum = { + name: enName, + doc: en.doc().toString(), + lib: en.lib().toString(), + cases: en.cases().map((c) => ({ + name: c.name().toString(), + doc: c.doc().toString(), + value: c.value(), + })), + }; + this.enums.set(enName, specEn); + } else if (kind === 'scSpecEntryUdtUnionV0') { + const un = entry.udtUnionV0(); + const unName = un.name().toString(); + const specUn: SpecUnion = { + name: unName, + doc: un.doc().toString(), + lib: un.lib().toString(), + cases: un.cases().map((c) => { + const caseKind = c.switch().name; + if (caseKind === 'scSpecUdtUnionCaseVoidV0') { + const v = c.voidV0(); + return { name: v.name().toString(), doc: v.doc().toString() }; + } else { + const t = c.tupleV0(); + return { name: t.name().toString(), doc: t.doc().toString(), typeList: t.typeList() }; + } + }), + }; + this.unions.set(unName, specUn); + } + } + } + + /** + * Retrieves function spec for a given function name. + * + * @param name - Method name + */ + getFunction(name: string): SpecFunction | undefined { + return this.functions.get(name); + } + + /** + * Encodes JS function parameters into an array of Soroban `xdr.ScVal` objects. + * + * @param methodName - Method name defined in contract spec + * @param args - Positional arguments array or object map of named parameters + */ + encodeArgs(methodName: string, args: Record | unknown[]): xdr.ScVal[] { + const fnSpec = this.getFunction(methodName); + if (!fnSpec) { + throw new TrustFlowError( + `Method '${methodName}' not found in Soroban contract spec`, + 'INVALID_CONTRACT_CALL', + ); + } + + let argsArray: unknown[]; + if (Array.isArray(args)) { + argsArray = args; + } else if (typeof args === 'object' && args !== null) { + argsArray = fnSpec.inputs.map((inp) => (args as Record)[inp.name]); + } else { + throw new TrustFlowError( + `Invalid arguments for method '${methodName}': expected array or object`, + 'INVALID_CONTRACT_CALL', + ); + } + + if (argsArray.length !== fnSpec.inputs.length) { + throw new TrustFlowError( + `Method '${methodName}' expects ${fnSpec.inputs.length} arguments, got ${argsArray.length}`, + 'INVALID_CONTRACT_CALL', + ); + } + + return fnSpec.inputs.map((inp, idx) => this.valToScVal(argsArray[idx], inp.type)); + } + + /** + * Converts a single JavaScript value into an `xdr.ScVal` matching the spec type definition. + * + * @param val - JavaScript value to encode + * @param typeDef - Soroban spec type definition + */ + valToScVal(val: unknown, typeDef: xdr.ScSpecTypeDef): xdr.ScVal { + const kind = typeDef.switch().name; + + switch (kind) { + case 'scSpecTypeVal': + return nativeToScVal(val); + case 'scSpecTypeBool': + return nativeToScVal(Boolean(val), { type: 'bool' }); + case 'scSpecTypeVoid': + return xdr.ScVal.scvVoid(); + case 'scSpecTypeU32': + return nativeToScVal(Number(val), { type: 'u32' }); + case 'scSpecTypeI32': + return nativeToScVal(Number(val), { type: 'i32' }); + case 'scSpecTypeU64': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'u64' }); + case 'scSpecTypeI64': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'i64' }); + case 'scSpecTypeTime': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'u64' }); + case 'scSpecTypeDuration': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'u64' }); + case 'scSpecTypeU128': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'i128' }); + case 'scSpecTypeI128': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'i128' }); + case 'scSpecTypeU256': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'u256' }); + case 'scSpecTypeI256': + return nativeToScVal(BigInt(val as string | number | bigint), { type: 'i256' }); + case 'scSpecTypeBytes': + case 'scSpecTypeBytesN': + if (typeof val === 'string') { + return nativeToScVal(Buffer.from(val, 'hex'), { type: 'bytes' }); + } + return nativeToScVal(val, { type: 'bytes' }); + case 'scSpecTypeString': + return nativeToScVal(String(val), { type: 'string' }); + case 'scSpecTypeSymbol': + return nativeToScVal(String(val), { type: 'symbol' }); + case 'scSpecTypeAddress': + return new Address(String(val)).toScVal(); + case 'scSpecTypeOption': { + if (val === null || val === undefined) { + return xdr.ScVal.scvVoid(); + } + const innerType = typeDef.option().valueType(); + return this.valToScVal(val, innerType); + } + case 'scSpecTypeVec': { + if (!Array.isArray(val)) { + throw new TrustFlowError(`Expected array for vector argument`, 'INVALID_CONTRACT_CALL'); + } + const elemType = typeDef.vec().elementType(); + const converted = val.map((v) => this.valToScVal(v, elemType)); + return xdr.ScVal.scvVec(converted); + } + case 'scSpecTypeMap': { + const keyType = typeDef.map().keyType(); + const valType = typeDef.map().valueType(); + const entries: xdr.ScMapEntry[] = []; + if (val instanceof Map) { + for (const [k, v] of val.entries()) { + entries.push( + new xdr.ScMapEntry({ + key: this.valToScVal(k, keyType), + val: this.valToScVal(v, valType), + }), + ); + } + } else if (typeof val === 'object' && val !== null) { + for (const [k, v] of Object.entries(val)) { + entries.push( + new xdr.ScMapEntry({ + key: this.valToScVal(k, keyType), + val: this.valToScVal(v, valType), + }), + ); + } + } + return xdr.ScVal.scvMap(entries); + } + case 'scSpecTypeTuple': { + if (!Array.isArray(val)) { + throw new TrustFlowError(`Expected array for tuple argument`, 'INVALID_CONTRACT_CALL'); + } + const types = typeDef.tuple().valueTypes(); + const converted = val.map((v, i) => this.valToScVal(v, types[i])); + return xdr.ScVal.scvVec(converted); + } + case 'scSpecTypeUdt': { + const udtName = typeDef.udt().name().toString(); + const structSpec = this.structs.get(udtName); + if (structSpec && typeof val === 'object' && val !== null) { + const mapEntries: xdr.ScMapEntry[] = []; + for (const field of structSpec.fields) { + const fieldValue = (val as Record)[field.name]; + mapEntries.push( + new xdr.ScMapEntry({ + key: nativeToScVal(field.name, { type: 'symbol' }), + val: this.valToScVal(fieldValue, field.type), + }), + ); + } + return xdr.ScVal.scvMap(mapEntries); + } + return nativeToScVal(val); + } + default: + return nativeToScVal(val); + } + } + + /** + * Decodes a returned `xdr.ScVal` into native JavaScript value. + * + * @param methodName - Function name defined in contract spec + * @param scVal - ScVal returned from contract simulation or execution + */ + decodeReturnValue(methodName: string, scVal: xdr.ScVal): unknown { + if (!scVal) return undefined; + try { + return scValToNative(scVal); + } catch { + return scVal; + } + } +} diff --git a/src/index.ts b/src/index.ts index b9aa280..5c58a5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ export * from './utils/validation'; export * from './utils/format'; export * from './utils/i128'; export * from './tx-pipeline'; +export * from './contract'; export { TrustFlowClient } from './client'; export * from './errors';