import { createContext, resolveRouteCall, resolveRouteThen, } from "@cam/core" import { validateCamContractCall, } from "@cam/evm-viem" import { isCamNamespaceNameForType, toInertValue, } from "@cam/protocol" import type { CamDocument } from "@cam/core" import type { CamHost, ResolvedCamContract } from "@cam/evm-viem" import type { InertRecord } from "@cam/protocol" import { CamViewerError } from "./errors.ts" import { assertViewerRouteAccountAvailable } from "./route-preflight.ts" import type { CamViewerAccount, CamViewerPreparedContractCall, } from "./types.ts" export function prepareViewerContractCall({ cam, contracts, host, account, route, inputs, }: { readonly cam: CamDocument readonly contracts: Record readonly host: CamHost readonly account?: CamViewerAccount readonly route: string readonly inputs: InertRecord }): CamViewerPreparedContractCall { const routeDeclaration = cam.routes[route] if (routeDeclaration === undefined) { throw new CamViewerError("CAM_VIEWER_ACTION_UNSUPPORTED", `CAM write route does not exist: ${route}`) } if (routeDeclaration.kind !== "write") { throw new CamViewerError("CAM_VIEWER_ACTION_UNSUPPORTED", `CAM contract action route must be declared as write: ${route}`) } assertViewerRouteAccountAvailable({ cam, route, ...(account === undefined ? {} : { account }), }) const context = createContext({ host, ...(account === undefined ? {} : { account }), inputs, outputs: [], }) const call = resolveRouteCall(cam, route, context) if (!isCamNamespaceNameForType(call.namespace, "contract")) { throw new CamViewerError("CAM_VIEWER_ACTION_UNSUPPORTED", `CAM write route must call a contract namespace: ${route}`) } const contract = contracts[call.namespace] if (contract === undefined) { throw new CamViewerError("CAM_VIEWER_ACTION_UNSUPPORTED", `CAM contract action references unresolved namespace: ${call.namespace}`) } const prepared: CamViewerPreparedContractCall = { route, address: contract.address, abi: cloneContractAbi(contract.abi), function: call.function, args: clonePreparedCallData(call.args, "call.args"), ...(call.value === undefined ? {} : { value: clonePreparedCallData(call.value, "call.value") }), then: clonePreparedCallData(resolveRouteThen(cam, route, context), "call.then"), } validateCamContractCall(prepared) return prepared } function cloneContractAbi(abi: ResolvedCamContract["abi"]): ResolvedCamContract["abi"] { return clonePreparedCallData(abi, "contract.abi") } function clonePreparedCallData(value: T, path: string): T { try { return toInertValue(value) as T } catch (cause) { throw new CamViewerError( "CAM_VIEWER_INVALID_INERT_VALUE", `CAM viewer data is not safely cloneable: ${path}`, cause, ) } }