import type {
CandidDisplayReactorParameters,
DynamicMethodOptions,
} from "./types.js"
import { CandidAdapter } from "./adapter.js"
import {
BaseActor,
DisplayReactorParameters,
TransformKey,
DisplayReactor,
didToDisplayCodec,
didTypeFromArray,
} from "@ic-reactor/core"
import { IDL } from "@icp-sdk/core/candid"
// ============================================================================
// CandidDisplayReactor
// ============================================================================
/**
* CandidDisplayReactor combines the display transformation capabilities of
* DisplayReactor with dynamic Candid parsing from CandidReactor.
*
* This class provides:
* - **Display transformations**: Automatic type conversion between Candid and
* display-friendly types (bigint ↔ string, Principal ↔ string, etc.)
* - **Validation**: Optional argument validation with display types
* - **Dynamic Candid parsing**: Initialize from Candid source or fetch from network
* - **Dynamic method registration**: Register methods at runtime with Candid signatures
*
* @typeParam A - The actor service type
*
* @example
* ```typescript
* import { CandidDisplayReactor } from "@ic-reactor/candid"
*
* const reactor = new CandidDisplayReactor({
* clientManager,
* canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
* })
*
* // Initialize from network (fetches Candid from canister)
* await reactor.initialize()
*
* // Or provide Candid source directly
* const reactor2 = new CandidDisplayReactor({
* clientManager,
* canisterId: "...",
* candid: `service : { greet : (text) -> (text) query }`
* })
* await reactor2.initialize()
*
* // Call methods with display types (strings instead of bigint/Principal)
* const result = await reactor.callMethod({
* functionName: "transfer",
* args: [{ to: "aaaaa-aa", amount: "1000000" }] // strings!
* })
*
* // Add validation
* reactor.registerValidator("transfer", ([input]) => {
* if (!input.to) {
* return { success: false, issues: [{ path: ["to"], message: "Required" }] }
* }
* return { success: true }
* })
* ```
*/
export class CandidDisplayReactor<
A = BaseActor,
T extends TransformKey = "display",
> extends DisplayReactor {
public readonly transform = "display" as T
public adapter: CandidAdapter
private candidSource?: string
constructor(config: CandidDisplayReactorParameters) {
const superConfig = { ...config }
// If funcClass is provided, build an idlFactory from it
if (config.funcClass && !superConfig.idlFactory) {
const { methodName, func } = config.funcClass
superConfig.idlFactory = ({ IDL }) => IDL.Service({ [methodName]: func })
}
if (!superConfig.idlFactory) {
superConfig.idlFactory = ({ IDL }) => IDL.Service({})
}
super(superConfig as DisplayReactorParameters)
this.candidSource = config.candid
if (config.adapter) {
this.adapter = config.adapter
} else {
this.adapter = new CandidAdapter({
clientManager: this.clientManager,
})
}
}
// ══════════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ══════════════════════════════════════════════════════════════════════════
/**
* Initializes the reactor by parsing the provided Candid string or fetching it from the network.
* This updates the internal service definition with the actual canister interface.
*
* After initialization, all DisplayReactor methods work with display type transformations.
*
* @example
* ```typescript
* const reactor = new CandidDisplayReactor({
* clientManager,
* canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
* })
*
* // Fetches Candid from the canister and initializes
* await reactor.initialize()
*
* // Now you can call methods with display types
* const balance = await reactor.callMethod({
* functionName: "icrc1_balance_of",
* args: [{ owner: "aaaaa-aa" }] // Principal as string!
* })
* ```
*/
public async initialize(): Promise {
let idlFactory: IDL.InterfaceFactory
if (this.candidSource) {
const definition = await this.adapter.parseCandidSource(this.candidSource)
idlFactory = definition.idlFactory
} else {
const definition = await this.adapter.getCandidDefinition(this.canisterId)
idlFactory = definition.idlFactory
}
this.service = idlFactory({ IDL })
// Re-initialize codecs after service is updated
this.reinitializeCodecs()
}
/**
* Re-initialize the display codecs after the service has been updated.
* This is called automatically after initialize() or registerMethod().
*/
private reinitializeCodecs(): void {
const fields = this.getServiceInterface()?._fields
if (!fields) return
// Access the private codecs map from DisplayReactor
const codecs = (this as any).codecs as Map<
string,
{ args: any; result: any }
>
for (const [methodName, funcType] of fields) {
// Skip if already exists
if (codecs.has(methodName)) continue
const argsIdlType = didTypeFromArray(funcType.argTypes)
const retIdlType = didTypeFromArray(funcType.retTypes)
codecs.set(methodName, {
args: didToDisplayCodec(argsIdlType),
result: didToDisplayCodec(retIdlType),
})
}
}
// ══════════════════════════════════════════════════════════════════════════
// DYNAMIC METHOD REGISTRATION
// ══════════════════════════════════════════════════════════════════════════
/**
* Register a dynamic method by its Candid signature.
* After registration, all DisplayReactor methods work with display type transformations.
*
* @example
* ```typescript
* // Register a method
* await reactor.registerMethod({
* functionName: "icrc1_balance_of",
* candid: "(record { owner : principal }) -> (nat) query"
* })
*
* // Now use with display types!
* const balance = await reactor.callMethod({
* functionName: "icrc1_balance_of",
* args: [{ owner: "aaaaa-aa" }] // Principal as string
* })
* // balance is string (not bigint) due to display transformation
* ```
*/
public async registerMethod(options: DynamicMethodOptions): Promise {
const { functionName, candid } = options
// Check if method already registered
const existing = this.service._fields.find(
([name]) => name === functionName
)
if (existing) return
// Parse the Candid signature
const serviceSource = candid.includes("service :")
? candid
: `service : { ${functionName} : ${candid}; }`
const { idlFactory } = await this.adapter.parseCandidSource(serviceSource)
const parsedService = idlFactory({ IDL })
const funcField = parsedService._fields.find(
([name]) => name === functionName
)
if (!funcField) {
throw new Error(
`Method "${functionName}" not found in the provided Candid signature`
)
}
// Inject into our service
this.service._fields.push(funcField)
// Re-initialize codecs for the new method
this.reinitializeCodecs()
}
/**
* Register multiple methods at once.
*
* @example
* ```typescript
* await reactor.registerMethods([
* { functionName: "icrc1_balance_of", candid: "(record { owner : principal }) -> (nat) query" },
* { functionName: "icrc1_transfer", candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })" }
* ])
* ```
*/
public async registerMethods(methods: DynamicMethodOptions[]): Promise {
await Promise.all(methods.map((m) => this.registerMethod(m)))
}
/**
* Check if a method is registered (either from initialize or registerMethod).
*/
public hasMethod(functionName: string): boolean {
return this.service._fields.some(([name]) => name === functionName)
}
/**
* Get all registered method names.
*/
public getMethodNames(): string[] {
return this.service._fields.map(([name]) => name)
}
// ══════════════════════════════════════════════════════════════════════════
// DYNAMIC CALL SHORTCUTS
// ══════════════════════════════════════════════════════════════════════════
/**
* Perform a dynamic update call in one step with display type transformations.
* Registers the method if not already registered, then calls it.
*
* @example
* ```typescript
* const result = await reactor.callDynamic({
* functionName: "transfer",
* candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
* args: [{ to: "aaaaa-aa", amount: "100" }] // Display types!
* })
* ```
*/
public async callDynamic(
options: DynamicMethodOptions & { args?: unknown[] }
): Promise {
await this.registerMethod(options)
return this.callMethod({
functionName: options.functionName as any,
args: options.args as any,
}) as T
}
/**
* Perform a dynamic query call in one step with display type transformations.
* Registers the method if not already registered, then calls it.
*
* @example
* ```typescript
* const balance = await reactor.queryDynamic({
* functionName: "icrc1_balance_of",
* candid: "(record { owner : principal }) -> (nat) query",
* args: [{ owner: "aaaaa-aa" }] // Display types!
* })
* // balance is string (not BigInt)
* ```
*/
public async queryDynamic(
options: DynamicMethodOptions & { args?: unknown[] }
): Promise {
await this.registerMethod(options)
return this.callMethod({
functionName: options.functionName as any,
args: options.args as any,
}) as T
}
/**
* Fetch with dynamic Candid and TanStack Query caching.
* Registers the method if not already registered, then fetches with caching.
* Results are transformed to display types.
*
* @example
* ```typescript
* const balance = await reactor.fetchQueryDynamic({
* functionName: "icrc1_balance_of",
* candid: "(record { owner : principal }) -> (nat) query",
* args: [{ owner: "aaaaa-aa" }]
* })
* // Subsequent calls with same args return cached result
* ```
*/
public async fetchQueryDynamic(
options: DynamicMethodOptions & { args?: unknown[] }
): Promise {
await this.registerMethod(options)
return this.fetchQuery({
functionName: options.functionName as any,
args: options.args as any,
}) as T
}
}