Form Schema Generation
Generate field trees, defaults, component hints, and zod schemas directly from Candid.
MetadataReactor is the candid-native metadata reactor.
It extends CandidReactor and adds runtime metadata for:
getInputMeta, getAllInputMeta)getOutputMeta, getAllOutputMeta)buildForMethod)buildMethodVariableCandidates)Unlike MetadataDisplayReactor, this class keeps argument handling candid-native.
import { MetadataReactor } from "@ic-reactor/candid"Use MetadataReactor when you want:
Use MetadataDisplayReactor if you specifically want display-transform behavior for arguments in your app flow.
Form Schema Generation
Generate field trees, defaults, component hints, and zod schemas directly from Candid.
Output Resolution
Resolve canister return values with method-aware output metadata.
Hydration from Hex
Convert candid-encoded argument hex into UI-friendly form values.
Runtime Method Expansion
Register new methods at runtime and regenerate metadata immediately.
import { MetadataReactor } from "@ic-reactor/candid"import { ClientManager } from "@ic-reactor/core"import { QueryClient } from "@tanstack/query-core"
const clientManager = new ClientManager({ queryClient: new QueryClient() })await clientManager.initialize()
export const reactor = new MetadataReactor({ name: "ledger", canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", clientManager,})
await reactor.initialize()
const method = "icrc1_transfer"const inputMeta = reactor.getInputMeta(method)const outputMeta = reactor.getOutputMeta(method)getInputMeta(methodName) returns FormArgumentsMeta for a single method.
args: structured field nodesdefaults: default form stateschema: zod tuple for method argsargCount, isEmpty, functionTypegetAllInputMeta() returns the method-to-metadata map for the full service.
import { reactor } from "./reactor"
const transferInput = reactor.getInputMeta("icrc1_transfer")console.log(transferInput?.defaults)console.log(transferInput?.schema)getOutputMeta(methodName) returns MethodMeta, including a resolve(...) function.
getAllOutputMeta() returns metadata for all methods.
callMethod already resolves the reply through this metadata, so it returns a
MethodResult, not the raw Candid value. The raw value is in its raw field,
and that is what resolve(...) takes.
import { reactor } from "./reactor"
const transferOutput = reactor.getOutputMeta("icrc1_transfer")const result = await reactor.callMethod({ functionName: "icrc1_transfer", args: [/* candid args */],})
console.log(result.results) // resolved nodes, one per return value
// resolve() takes the raw Candid value, such as result.rawconst resolved = transferOutput?.resolve(result.raw)buildForMethod(methodName, { candidArgsHex }) combines:
Hydration status can be:
emptyhydratedskippederrorimport { reactor } from "./reactor"
// icrc1_balance_of's argument, { owner = "aaaaa-aa"; subaccount = null },// Candid-encodedconst built = await reactor.buildForMethod("icrc1_balance_of", { candidArgsHex: "4449444c036d7b6e006c02b3b0dac30368ad86ca8305010102010000",})
console.log(built.meta.defaults) // [{ owner: "", subaccount: null }]console.log(built.hydration.status) // "hydrated"if (built.hydration.status === "hydrated") { console.log(built.hydration.values) // [{ owner: "aaaaa-aa", subaccount: null }]}You can skip hydration if payloads include unsupported placeholders:
import { reactor } from "./reactor"
const built = await reactor.buildForMethod("icrc1_transfer", { candidArgsHex, skipHydrationIfContains: "{{",})Use buildForValueType(...) when you want field/schema metadata for one type expression.
import { reactor } from "./reactor"
const valueMeta = await reactor.buildForValueType( "record { owner : principal; amount : nat }")console.log(valueMeta.meta.defaults)buildMethodVariableCandidates(methodName) helps build mapping UIs for workflow tools.
It lists the paths into the method’s return value that a later step can
reference. Arguments are not included.
import { reactor } from "./reactor"
const candidates = reactor.buildMethodVariableCandidates("icrc1_transfer")// e.g. "$icrc1_transfer.Ok", "$icrc1_transfer.Err.InsufficientFunds.balance"When methods are discovered at runtime, register them and metadata is regenerated automatically.
import { reactor } from "./reactor"
await reactor.registerMethod({ functionName: "get_metadata", candid: "() -> (record { name : text; version : nat }) query",})
const meta = reactor.getInputMeta("get_metadata")MetadataReactor.getInputMeta / getOutputMeta.defaults + schema.candidArgsHex for replay/edit flows.MethodMeta.resolve.The form metadata holds display types: text for principals and numbers, null
for an empty opt, { _type } for a variant. MetadataReactor is
candid-native, so the values the user submits are converted to Candid before
callMethod.
import { didToDisplayCodec } from "@ic-reactor/core"import { IDL } from "@icp-sdk/core/candid"import { reactor } from "./reactor"
const methodName = "icrc1_transfer"const inputMeta = reactor.getInputMeta(methodName)if (!inputMeta) throw new Error("Method metadata missing")
// 1) initialize the form with inputMeta.defaults: "" for principals, text// and numbers. "" is a valid text, so a text field left empty is sent// as ""; principals and numbers fail the schema until the user fills// them in. Say the user entered:const formValues = [ { to: { owner: "aaaaa-aa", subaccount: null }, amount: "100000", fee: null, memo: null, from_subaccount: null, created_at_time: null, },]
// 2) validate before submitconst values = inputMeta.schema.parse(formValues)
// 3) convert the display types to Candid (Principal, bigint, [] or [x])const func = reactor.getServiceInterface().fieldsAsObject()[methodName]const args = didToDisplayCodec(IDL.Tuple(...func.argTypes)).asCandid(values)
const result = await reactor.callMethod({ functionName: methodName, args })
// 4) render: callMethod returns the resolved result, one node per valueconst rendered = result.resultsMetadataDisplayReactor takes display types in callMethod, so it sends the
form values without step 3.
A record with exactly one func field, such as an archived block range in a
ledger’s reply, resolves as a funcRecord node. Its defaultArgs are the
values beside the callback, arranged as the callback’s arguments. On
MetadataReactor they are Candid values (bigints, a Principal, [] or [x]
for an opt), which is what its callMethod takes, so a reactor built from
the node’s funcClass can call the callback with them as they are:
import { MetadataReactor } from "@ic-reactor/candid"
// `archived` is the resolved funcRecord node, for example// result.results[0].fields.archived_blocks.items[0]console.log(archived.defaultArgs) // [{ start: 0n, length: 100n }]
const archive = new MetadataReactor({ name: "archive", canisterId: archived.canisterId, clientManager, funcClass: { methodName: archived.methodName, func: archived.funcClass },})
const blocks = await archive.callMethod({ functionName: archived.methodName, args: archived.defaultArgs,})MetadataDisplayReactor gives display-typed defaultArgs ("0" for that
nat64) for its own display-typed callMethod. Call the callback through the
same kind of reactor that resolved the record.
metadata not found for a methodawait reactor.initialize() completed.reactor.getMethodNames().registerMethod(...) first.errorcandidArgsHex matches the method argument types exactly.0x prefix).skippedskipHydrationIfContains and input matched that token.