@ic-reactor/candid
v3.1.1
Published
IC Reactor Candid Adapter - Fetch and parse Candid definitions from Internet Computer canisters
Maintainers
Readme
@ic-reactor/candid
Lightweight adapter for fetching and parsing Candid definitions from Internet Computer canisters.
Features
- Fetch Candid Definitions: Retrieve Candid interface definitions from any canister
- Multiple Retrieval Methods: Supports both canister metadata and the temporary hack method
- Local Parsing: Use the optional WASM-based parser for fast, offline Candid compilation
- Remote Fallback: Falls back to the didjs canister for Candid-to-JavaScript compilation
- Dynamic Reactor: Includes
CandidReactorfor dynamic IDL fetching and interaction - Dynamic Forms: Generate rich form metadata with validation schemas using
MetadataReactorandCandidFormVisitor - Lightweight: Uses raw
agent.querycalls - no Actor overhead - ClientManager Compatible: Seamlessly integrates with
@ic-reactor/core
Installation
npm install @ic-reactor/candid @icp-sdk/coreOptional: Local Parser
For faster Candid parsing without network requests:
npm install @ic-reactor/parserUsage
With ClientManager (Recommended)
import { CandidAdapter } from "@ic-reactor/candid"
import { ClientManager } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"
// Create and initialize ClientManager
const queryClient = new QueryClient()
const clientManager = new ClientManager({ queryClient })
await clientManager.initialize()
// Create the adapter
const adapter = new CandidAdapter({ clientManager })
// Get the Candid definition for a canister
const { idlFactory } = await adapter.getCandidDefinition(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)With Local Parser (Fastest)
import { CandidAdapter } from "@ic-reactor/candid"
import { ClientManager } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"
const queryClient = new QueryClient()
const clientManager = new ClientManager({ queryClient })
await clientManager.initialize()
const adapter = new CandidAdapter({ clientManager })
// Load the local parser for faster processing
await adapter.loadParser()
// Now parsing happens locally - no network requests
const { idlFactory } = await adapter.getCandidDefinition(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)CandidReactor (Dynamic Interaction)
CandidReactor extends the core Reactor class, allowing you to work with canisters without compile-time IDL. After initialization, all standard Reactor methods work automatically.
import { CandidReactor } from "@ic-reactor/candid"
import { ClientManager } from "@ic-reactor/core"
const clientManager = new ClientManager()
await clientManager.initialize()
// Option 1: Fetch Candid from network
const reactor = new CandidReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
name: "my-canister",
})
await reactor.initialize() // Fetches IDL from network
// Option 2: Provide Candid string directly (no network fetch)
const reactor = new CandidReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
name: "my-canister",
candid: `service : {
icrc1_name : () -> (text) query;
icrc1_balance_of : (record { owner : principal }) -> (nat) query;
}`,
})
await reactor.initialize() // Parses provided Candid string
// Now use standard Reactor methods!
const name = await reactor.callMethod({ functionName: "icrc1_name" })
const balance = await reactor.fetchQuery({
functionName: "icrc1_balance_of",
args: [{ owner }],
})Registering Methods Dynamically
You can also register individual methods on-the-fly:
// Start with just a canister ID
const reactor = new CandidReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
name: "my-canister",
})
// Register a method by its Candid signature
await reactor.registerMethod({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
})
// Now all standard Reactor methods work with this method!
const balance = await reactor.callMethod({
functionName: "icrc1_balance_of",
args: [{ owner }],
})
// With TanStack Query caching
const cachedBalance = await reactor.fetchQuery({
functionName: "icrc1_balance_of",
args: [{ owner }],
})
// Check registered methods
console.log(reactor.getMethodNames())One-Shot Dynamic Calls
For quick one-off calls, use convenience methods that register and call in one step:
// queryDynamic - register + call in one step
const symbol = await reactor.queryDynamic({
functionName: "icrc1_symbol",
candid: "() -> (text) query",
})
// callDynamic - for update calls
const result = await reactor.callDynamic({
functionName: "transfer",
candid:
"(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
args: [{ to: recipient, amount: 100n }],
})
// fetchQueryDynamic - with TanStack Query caching
const cachedBalance = await reactor.fetchQueryDynamic({
functionName: "icrc1_balance_of",
candid: "(record { owner : principal }) -> (nat) query",
args: [{ owner }],
})MetadataReactor (Dynamic Forms)
Use MetadataReactor to build runtime form metadata directly from a canister interface.
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()
const reactor = new MetadataReactor({
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
clientManager,
name: "ledger",
})
await reactor.initialize()
const inputMeta = reactor.getInputMeta("icrc1_transfer")
console.log(inputMeta?.schema) // Zod tuple for full argument validationCandidFormVisitor (Low-Level Form Metadata)
Use CandidFormVisitor when you already have an IDL.ServiceClass and want direct visitor output.
import { CandidFormVisitor } from "@ic-reactor/candid"
const visitor = new CandidFormVisitor()
const serviceMeta = service.accept(visitor, null)
// Access method metadata
const methodMeta = serviceMeta["icrc1_transfer"]
// Argument metadata
console.log(methodMeta.schema) // Zod tuple for all args
console.log(methodMeta.defaults) // Default values for form initialization
// Field-level UI and validation metadata
const arg0 = methodMeta.args[0]
console.log(arg0.component) // e.g. "record-container"
console.log(arg0.renderHint) // { isCompound: true, isPrimitive: false, ... }
console.log(arg0.schema) // Zod schema for this fieldFeatures:
- Zod Validation: Includes method-level and field-level schemas (
schema) for runtime validation. - Component Hints: Includes
componentvalues for renderer selection (variant-select,vector-list,blob-upload, etc.). - Render Hints: Includes
renderHintfor primitive/compound strategy and input type hints. - Form Defaults: Includes ready-to-use
defaultsfor form initialization.
Fetch Raw Candid Source
const candidSource = await adapter.fetchCandidSource(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)
console.log(candidSource)
// Output: service { greet: (text) -> (text) query; }Validate Candid Source
await adapter.loadParser()
const isValid = adapter.validateCandid(`
service {
greet: (text) -> (text) query;
}
`)
console.log(isValid) // trueCompile Candid to JavaScript
// Local compilation (requires parser)
await adapter.loadParser()
const jsCode = adapter.compileLocal("service { greet: (text) -> (text) query }")
// Remote compilation (uses didjs canister)
const jsCode = await adapter.compileRemote(
"service { greet: (text) -> (text) query }"
)API Reference
CandidAdapter
Constructor
new CandidAdapter(params: CandidAdapterParameters)| Parameter | Type | Required | Description |
| ----------------- | --------------------- | -------- | -------------------------------------------------- |
| clientManager | CandidClientManager | Yes | Client manager providing agent and identity access |
| didjsCanisterId | string | No | Custom didjs canister ID |
Properties
| Property | Type | Description |
| ----------------- | --------------------- | -------------------------------------------- |
| clientManager | CandidClientManager | The client manager instance |
| agent | HttpAgent | The HTTP agent from the client manager |
| didjsCanisterId | string | The didjs canister ID for remote compilation |
| hasParser | boolean | Whether the local parser is loaded |
Methods
Main API
| Method | Description |
| --------------------------------- | ------------------------------------------------ |
| getCandidDefinition(canisterId) | Get parsed Candid definition (idlFactory + init) |
| fetchCandidSource(canisterId) | Get raw Candid source string |
| parseCandidSource(candidSource) | Parse Candid source to definition |
Parser Methods
| Method | Description |
| ------------------------------ | ---------------------------------------- |
| loadParser(module?) | Load the local WASM parser |
| compileLocal(candidSource) | Compile Candid locally (requires parser) |
| validateCandid(candidSource) | Validate Candid source (requires parser) |
Fetch Methods
| Method | Description |
| ----------------------------------------------- | --------------------------------- |
| fetchFromMetadata(canisterId) | Get Candid from canister metadata |
| fetchFromTmpHack(canisterId) | Get Candid via tmp hack method |
| compileRemote(candidSource, didjsCanisterId?) | Compile Candid via didjs canister |
Cleanup
| Method | Description |
| --------------- | ------------------------------------ |
| unsubscribe() | Cleanup identity change subscription |
CandidReactor
Extends Reactor from @ic-reactor/core.
Constructor
new CandidReactor(config: CandidReactorParameters)| Parameter | Type | Required | Description |
| --------------- | ------------------ | -------- | ------------------------------------------------ |
| name | string | Yes | Name of the canister/reactor |
| clientManager | ClientManager | Yes | Client manager from @ic-reactor/core |
| canisterId | CanisterId | No | The canister ID (optional if using env vars) |
| candid | string | No | Candid service definition (avoids network fetch) |
| idlFactory | InterfaceFactory | No | IDL factory (if already available) |
| actor | A | No | Existing actor instance |
Methods
| Method | Description |
| ---------------------------- | ----------------------------------------------------------- |
| initialize() | Parse provided Candid or fetch from network, update service |
| registerMethod(options) | Register a method by its Candid signature |
| registerMethods(methods) | Register multiple methods at once |
| hasMethod(functionName) | Check if a method is registered |
| getMethodNames() | Get all registered method names |
| callDynamic(options) | One-shot: register + update call |
| queryDynamic(options) | One-shot: register + query call |
| fetchQueryDynamic(options) | One-shot: register + cached query |
After initialization or registration, all standard Reactor methods work:
callMethod()- Execute a method callfetchQuery()- Fetch with TanStack Query cachinggetQueryOptions()- Get query options for React hooksinvalidateQueries()- Invalidate cached queries- etc.
Types
interface CandidDefinition {
idlFactory: IDL.InterfaceFactory
init?: (args: { IDL: typeof IDL }) => IDL.Type<unknown>[]
}
interface CandidAdapterParameters {
clientManager: CandidClientManager
didjsCanisterId?: string
}
interface CandidClientManager {
agent: HttpAgent
isLocal: boolean
subscribe(callback: (identity: Identity) => void): () => void
}
type CanisterId = string | PrincipalHow It Works
Fetching Candid: The adapter first tries to get the Candid definition from the canister's metadata. If that fails, it falls back to calling the
__get_candid_interface_tmp_hackquery method.Parsing Candid: Once the raw Candid source is retrieved, it needs to be compiled to JavaScript:
- First tries the local WASM parser (if loaded) - instant, no network
- Falls back to the remote didjs canister - requires network request
Evaluation: The compiled JavaScript is dynamically imported to extract the
idlFactoryand optionalinitfunction.Dynamic Execution: For
callandquerymethods, the adapter wraps the provided Candid signature in a temporary service definition, compiles it to anidlFactory, and then uses anActorto encode arguments and execute the call reliably.Identity Changes: The adapter subscribes to identity changes from the ClientManager. When the identity changes, it re-evaluates the default didjs canister ID (unless a custom one was provided).
Standalone Usage
The CandidClientManager interface is simple enough that you can implement it yourself without @ic-reactor/core:
import { HttpAgent } from "@icp-sdk/core/agent"
import { CandidAdapter } from "@ic-reactor/candid"
// Create a minimal client manager implementation
const clientManager = {
agent: await HttpAgent.create({ host: "https://ic0.app" }),
isLocal: false,
subscribe: () => () => {}, // No-op subscription
}
const adapter = new CandidAdapter({ clientManager })License
MIT
