Skip to content
IC Reactor

CandidAdapter

CandidAdapter provides low-level utilities for fetching Candid definitions from canisters and compiling them to IDL factories. Use it when you need fine-grained control over Candid handling.

Fetching the Candid source and compiling it to JavaScript are two separate steps, each with its own fallback chain.

Fetching the source:

  1. Canister metadata (preferred) — reads the canister’s candid metadata path
  2. __get_candid_interface_tmp_hack (fallback) — query method on the canister

Compiling to JavaScript:

  1. Local parser (preferred) — the @ic-reactor/parser WASM module, a dependency of this package, loaded on first use
  2. didjs canister (fallback) — remote did_to_js call

The didjs canister runs upstream candid_parser without the local parser’s fixes. For an interface with a record field or variant tag named like a numeric id, such as _0_, it prints the key _0_, which @icp-sdk/core sends as field 0, where the local parser prints _4735054_ (see Field names that look like numeric ids).

import { CandidAdapter } from "@ic-reactor/candid"
const adapter = new CandidAdapter({ clientManager })
// Fetch and parse in one call
const { idlFactory } = await adapter.getCandidDefinition(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)
import { CandidAdapter } from "@ic-reactor/candid"
new CandidAdapter(config: CandidAdapterParameters)
Parameter Type Required Description
clientManager CandidClientManager Yes Client manager with agent access
didjsCanisterId CanisterId No Custom didjs canister ID

The didjs canister ID is auto-selected based on network:

  • IC Mainnet: a4gq6-oaaaa-aaaab-qaa4q-cai
  • Local: bd3sg-teaaa-aaaaa-qaaba-cai (must be deployed to the local replica)

The default is read from clientManager.isLocal each time the adapter needs it, and an ID passed here or assigned to didjsCanisterId wins. The adapter keeps no subscription on the client manager, so an adapter, or a candid reactor that created one, needs no cleanup when it is dropped. The deprecated unsubscribe() is a no-op.


Fetch and parse Candid for a canister in one call.

import { IDL } from "@icp-sdk/core/candid"
const { idlFactory, init } = await adapter.getCandidDefinition(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)
// Use the idlFactory
const service = idlFactory({ IDL })
console.log(service._fields) // Method definitions
interface CandidDefinition {
idlFactory: IDL.InterfaceFactory
init?: (args: { IDL: typeof IDL }) => IDL.Type<unknown>[]
}

Fetch raw Candid source text from a canister.

const candidSource = await adapter.fetchCandidSource(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)
console.log(candidSource)
// service { icrc1_name : () -> (text) query; ... }

Parse Candid source text into an IDL factory.

const candidSource = `
service : {
greet : (text) -> (text) query;
}
`
const { idlFactory } = await adapter.parseCandidSource(candidSource)

This method:

  1. Tries the local parser first, loading @ic-reactor/parser on first use
  2. Falls back to remote compilation via the didjs canister

It is the only method that returns a CandidDefinition — compileLocal and compileRemote return the compiled JavaScript source instead.


Load the local WASM parser for offline compilation.

import { CandidAdapter } from "@ic-reactor/candid"
const adapter = new CandidAdapter({ clientManager })
// Load the parser (only needed once). It imports @ic-reactor/parser itself:
// the package is a dependency of @ic-reactor/candid, so do not import it
// from your app unless you also install it directly.
await adapter.loadParser()
// Now compileLocal works
const jsSource = adapter.compileLocal(candidSource)

Compile Candid using the local WASM parser (synchronous after loading). Returns the compiled JavaScript source string — not a CandidDefinition. Use parseCandidSource() when you need an idlFactory.

// Requires loadParser() first
const jsSource: string = adapter.compileLocal(candidSource)

Throws if the parser hasn’t been loaded.


Compile Candid using the didjs canister. Also returns the compiled JavaScript source, wrapped in a promise.

const jsSource = await adapter.compileRemote(candidSource) // string | undefined

This is the fallback method when local parsing isn’t available.


Validate Candid syntax without compiling.

await adapter.loadParser()
const isValid = adapter.validateCandid(`
service : {
greet : (text) -> (text) query;
}
`)
console.log(isValid) // true

Requires the parser to be loaded.


Property Type Description
clientManager CandidClientManager The client manager instance
didjsCanisterId CanisterId The didjs canister ID in use: the one given or assigned, otherwise the default for the current network

import { CandidAdapter } from "@ic-reactor/candid"
import { ClientManager } from "@ic-reactor/core"
import { IDL } from "@icp-sdk/core/candid"
import { QueryClient } from "@tanstack/query-core"
const clientManager = new ClientManager({ queryClient: new QueryClient() })
await clientManager.initialize()
const adapter = new CandidAdapter({ clientManager })
// Get full definition
const { idlFactory } = await adapter.getCandidDefinition(
"ryjl3-tyaaa-aaaaa-aaaba-cai"
)
// List methods
const service = idlFactory({ IDL })
for (const [name, func] of service._fields) {
console.log(`${name}: ${func.toString()}`)
}
import { CandidAdapter } from "@ic-reactor/candid"
const adapter = new CandidAdapter({ clientManager })
// Load parser once at startup
await adapter.loadParser()
// All subsequent compilations are local (fast!)
const candidSource = await adapter.fetchCandidSource(canisterId)
const jsSource = adapter.compileLocal(candidSource)
try {
const definition = await adapter.getCandidDefinition(canisterId)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (message.includes("Failed to compile Candid")) {
console.log("Invalid Candid syntax")
} else {
// "Failed to retrieve Candid source by any method: …" comes from a canister
// that exposes no Candid and from failed requests (network, missing local
// root key) alike. The rest of the message says what stopped each attempt,
// so pass it on rather than report "no Candid".
throw error
}
}

For dynamic calls, you can parse individual method signatures:

import { IDL } from "@icp-sdk/core/candid"
// Parse a method signature wrapped in a service
const methodSignature = "(record { owner : principal }) -> (nat) query"
const serviceSource = `service : { my_method : ${methodSignature}; }`
const { idlFactory } = await adapter.parseCandidSource(serviceSource)
const service = idlFactory({ IDL })
// Extract the method's type information
const funcField = service._fields.find(([name]) => name === "my_method")
if (!funcField) throw new Error("my_method is not in the service")
const func = funcField[1]
console.log("Arg types:", func.argTypes)
console.log("Return types:", func.retTypes)
console.log("Is query:", func.annotations.includes("query"))