@ic-reactor/candid 3.0.7-beta.1 → 3.0.7-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,7 @@ await clientManager.initialize()
86
86
  const reactor = new CandidReactor({
87
87
  canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
88
88
  clientManager,
89
+ name: "my-canister",
89
90
  })
90
91
  await reactor.initialize() // Fetches IDL from network
91
92
 
@@ -93,6 +94,7 @@ await reactor.initialize() // Fetches IDL from network
93
94
  const reactor = new CandidReactor({
94
95
  canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
95
96
  clientManager,
97
+ name: "my-canister",
96
98
  candid: `service : {
97
99
  icrc1_name : () -> (text) query;
98
100
  icrc1_balance_of : (record { owner : principal }) -> (nat) query;
@@ -117,6 +119,7 @@ You can also register individual methods on-the-fly:
117
119
  const reactor = new CandidReactor({
118
120
  canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
119
121
  clientManager,
122
+ name: "my-canister",
120
123
  })
121
124
 
122
125
  // Register a method by its Candid signature
@@ -272,8 +275,9 @@ new CandidReactor(config: CandidReactorParameters)
272
275
 
273
276
  | Parameter | Type | Required | Description |
274
277
  | --------------- | ------------------ | -------- | ------------------------------------------------ |
275
- | `canisterId` | `CanisterId` | Yes | The canister ID to interact with |
278
+ | `name` | `string` | Yes | Name of the canister/reactor |
276
279
  | `clientManager` | `ClientManager` | Yes | Client manager from `@ic-reactor/core` |
280
+ | `canisterId` | `CanisterId` | No | The canister ID (optional if using env vars) |
277
281
  | `candid` | `string` | No | Candid service definition (avoids network fetch) |
278
282
  | `idlFactory` | `InterfaceFactory` | No | IDL factory (if already available) |
279
283
  | `actor` | `A` | No | Existing actor instance |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/candid",
3
- "version": "3.0.7-beta.1",
3
+ "version": "3.0.7-beta.2",
4
4
  "description": "IC Reactor Candid Adapter - Fetch and parse Candid definitions from Internet Computer canisters",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
+ "src",
19
20
  "README.md"
20
21
  ],
21
22
  "repository": {
@@ -41,7 +42,7 @@
41
42
  "author": "Behrad Deylami",
42
43
  "license": "MIT",
43
44
  "dependencies": {
44
- "@ic-reactor/core": "^3.0.7-beta.1"
45
+ "@ic-reactor/core": "^3.0.7-beta.2"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "@icp-sdk/core": "^5.0.0"
package/src/adapter.ts ADDED
@@ -0,0 +1,446 @@
1
+ import type { HttpAgent } from "@icp-sdk/core/agent"
2
+ import type { Principal } from "@icp-sdk/core/principal"
3
+ import type {
4
+ CandidAdapterParameters,
5
+ CandidDefinition,
6
+ CandidClientManager,
7
+ ReactorParser,
8
+ } from "./types"
9
+
10
+ import { CanisterStatus } from "@icp-sdk/core/agent"
11
+ import { IDL } from "@icp-sdk/core/candid"
12
+ import { DEFAULT_IC_DIDJS_ID, DEFAULT_LOCAL_DIDJS_ID } from "./constants"
13
+ import { importCandidDefinition } from "./utils"
14
+ import { CanisterId } from "@ic-reactor/core"
15
+
16
+ /**
17
+ * CandidAdapter provides functionality to fetch and parse Candid definitions
18
+ * from Internet Computer canisters.
19
+ *
20
+ * It supports multiple methods for retrieving Candid definitions:
21
+ * 1. From canister metadata (preferred)
22
+ * 2. From the `__get_candid_interface_tmp_hack` method (fallback)
23
+ *
24
+ * It also supports parsing Candid to JavaScript using:
25
+ * 1. Local WASM parser (@ic-reactor/parser) - faster, no network request
26
+ * 2. Remote didjs canister - always available fallback
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { CandidAdapter } from "@ic-reactor/candid"
31
+ * import { ClientManager } from "@ic-reactor/core"
32
+ * import { QueryClient } from "@tanstack/query-core"
33
+ *
34
+ * const queryClient = new QueryClient()
35
+ * const clientManager = new ClientManager({ queryClient })
36
+ * await clientManager.initialize()
37
+ *
38
+ * const adapter = new CandidAdapter({ clientManager })
39
+ *
40
+ * // Optionally load the local parser for faster processing
41
+ * await adapter.loadParser()
42
+ *
43
+ * // Get the Candid definition for a canister
44
+ * const { idlFactory } = await adapter.getCandidDefinition("ryjl3-tyaaa-aaaaa-aaaba-cai")
45
+ * ```
46
+ */
47
+ export class CandidAdapter {
48
+ /** The client manager providing agent and identity access. */
49
+ public clientManager: CandidClientManager
50
+
51
+ /** The canister ID of the didjs canister for remote Candid compilation. */
52
+ public didjsCanisterId: CanisterId
53
+
54
+ /** The optional local parser module. */
55
+ private parserModule?: ReactorParser
56
+
57
+ /** Whether parser auto-loading has been attempted. */
58
+ private parserLoadAttempted = false
59
+
60
+ /** Function to unsubscribe from identity updates. */
61
+ public unsubscribe: () => void = noop
62
+
63
+ /**
64
+ * Creates a new CandidAdapter instance.
65
+ *
66
+ * @param params - The adapter parameters.
67
+ */
68
+ constructor({ clientManager, didjsCanisterId }: CandidAdapterParameters) {
69
+ this.clientManager = clientManager
70
+ this.didjsCanisterId = didjsCanisterId || this.getDefaultDidJsId()
71
+
72
+ // Subscribe to identity changes to update didjs canister ID if needed
73
+ this.unsubscribe = clientManager.subscribe(() => {
74
+ if (!didjsCanisterId) {
75
+ this.didjsCanisterId = this.getDefaultDidJsId()
76
+ }
77
+ })
78
+ }
79
+
80
+ /**
81
+ * The HTTP agent from the client manager.
82
+ */
83
+ get agent(): HttpAgent {
84
+ return this.clientManager.agent
85
+ }
86
+
87
+ /**
88
+ * Whether the local parser is available.
89
+ */
90
+ get hasParser(): boolean {
91
+ return this.parserModule !== undefined
92
+ }
93
+
94
+ /**
95
+ * Loads the local parser module for converting Candid to JavaScript.
96
+ * If no module is provided, attempts to dynamically load @ic-reactor/parser.
97
+ *
98
+ * @param module - Optional parser module to use.
99
+ * @throws Error if the parser loading fails.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Load the default parser
104
+ * await adapter.loadParser()
105
+ *
106
+ * // Or provide a custom parser
107
+ * import * as parser from "@ic-reactor/parser"
108
+ * await adapter.loadParser(parser)
109
+ * ```
110
+ */
111
+ public async loadParser(module?: ReactorParser): Promise<void> {
112
+ if (module !== undefined) {
113
+ this.parserModule = module
114
+ this.parserLoadAttempted = true
115
+ return
116
+ }
117
+
118
+ if (this.parserLoadAttempted) {
119
+ return // Already tried loading
120
+ }
121
+
122
+ this.parserLoadAttempted = true
123
+
124
+ try {
125
+ this.parserModule = require("@ic-reactor/parser")
126
+ if (this.parserModule?.default) {
127
+ await this.parserModule.default()
128
+ }
129
+ } catch (error) {
130
+ throw new Error(`Error loading parser: ${error}`)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Attempts to load the parser silently (no error if not available).
136
+ * Useful for optional parser initialization.
137
+ */
138
+ private async tryLoadParser(): Promise<void> {
139
+ if (this.parserModule || this.parserLoadAttempted) {
140
+ return
141
+ }
142
+
143
+ this.parserLoadAttempted = true
144
+
145
+ try {
146
+ this.parserModule = require("@ic-reactor/parser")
147
+ if (this.parserModule?.default) {
148
+ await this.parserModule.default()
149
+ }
150
+ } catch {
151
+ // Silently fail - parser is optional
152
+ this.parserModule = undefined
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Gets the default didjs canister ID based on whether the agent is local or not.
158
+ */
159
+ private getDefaultDidJsId(): string {
160
+ return this.clientManager.isLocal
161
+ ? DEFAULT_LOCAL_DIDJS_ID
162
+ : DEFAULT_IC_DIDJS_ID
163
+ }
164
+
165
+ // ═══════════════════════════════════════════════════════════════════════════
166
+ // MAIN API - High-level methods for fetching Candid definitions
167
+ // ═══════════════════════════════════════════════════════════════════════════
168
+
169
+ /**
170
+ * Gets the parsed Candid definition for a canister, ready for use with Actor.createActor.
171
+ * This is the main entry point for fetching a canister's interface.
172
+ *
173
+ * @param canisterId - The canister ID to get the Candid definition for.
174
+ * @returns The parsed Candid definition with idlFactory and optional init.
175
+ * @throws Error if fetching or parsing fails.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const { idlFactory } = await adapter.getCandidDefinition("ryjl3-tyaaa-aaaaa-aaaba-cai")
180
+ * ```
181
+ */
182
+ public async getCandidDefinition(
183
+ canisterId: CanisterId
184
+ ): Promise<CandidDefinition> {
185
+ try {
186
+ const candidSource = await this.fetchCandidSource(canisterId)
187
+ return await this.parseCandidSource(candidSource)
188
+ } catch (error) {
189
+ throw new Error(`Error fetching canister ${canisterId}: ${error}`)
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Fetches the raw Candid source string for a canister.
195
+ * First attempts to get it from metadata, then falls back to the tmp hack method.
196
+ *
197
+ * @param canisterId - The canister ID to fetch the Candid source for.
198
+ * @returns The raw Candid source string (.did file contents).
199
+ * @throws Error if both methods fail.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const candidSource = await adapter.fetchCandidSource("ryjl3-tyaaa-aaaaa-aaaba-cai")
204
+ * console.log(candidSource) // service { greet: (text) -> (text) query; }
205
+ * ```
206
+ */
207
+ public async fetchCandidSource(canisterId: CanisterId): Promise<string> {
208
+ // First attempt: Try getting Candid from metadata
209
+ const fromMetadata = await this.fetchFromMetadata(canisterId).catch(
210
+ () => undefined
211
+ )
212
+
213
+ if (fromMetadata) {
214
+ return fromMetadata
215
+ }
216
+
217
+ // Second attempt: Try the temporary hack method
218
+ const fromTmpHack = await this.fetchFromTmpHack(canisterId).catch(
219
+ () => undefined
220
+ )
221
+
222
+ if (fromTmpHack) {
223
+ return fromTmpHack
224
+ }
225
+
226
+ throw new Error("Failed to retrieve Candid source by any method.")
227
+ }
228
+
229
+ /**
230
+ * Parses Candid source string and returns the definition with idlFactory.
231
+ * First attempts to use the local parser, then falls back to the remote didjs canister.
232
+ *
233
+ * @param candidSource - The raw Candid source string.
234
+ * @returns The parsed Candid definition.
235
+ * @throws Error if parsing fails.
236
+ */
237
+ public async parseCandidSource(
238
+ candidSource: string
239
+ ): Promise<CandidDefinition> {
240
+ // Try to auto-load parser if not already loaded
241
+ await this.tryLoadParser()
242
+
243
+ let compiledJs: string | undefined
244
+
245
+ // First attempt: Try local parser (faster, no network)
246
+ if (this.parserModule) {
247
+ try {
248
+ compiledJs = this.compileLocal(candidSource)
249
+ } catch {
250
+ // Fall through to remote compilation
251
+ }
252
+ }
253
+
254
+ // Second attempt: Try remote didjs canister
255
+ if (!compiledJs) {
256
+ compiledJs = await this.compileRemote(candidSource)
257
+ }
258
+
259
+ if (!compiledJs) {
260
+ throw new Error("Failed to compile Candid to JavaScript")
261
+ }
262
+
263
+ return importCandidDefinition(compiledJs)
264
+ }
265
+
266
+ // ═══════════════════════════════════════════════════════════════════════════
267
+ // FETCH METHODS - Low-level methods for fetching Candid source
268
+ // ═══════════════════════════════════════════════════════════════════════════
269
+
270
+ /**
271
+ * Fetches Candid source from the canister's metadata.
272
+ *
273
+ * @param canisterId - The canister ID to query.
274
+ * @returns The Candid source string, or undefined if not available.
275
+ */
276
+ public async fetchFromMetadata(
277
+ canisterId: CanisterId
278
+ ): Promise<string | undefined> {
279
+ const status = await CanisterStatus.request({
280
+ agent: this.agent,
281
+ canisterId: canisterId as Principal,
282
+ paths: ["candid"],
283
+ })
284
+
285
+ return status.get("candid") as string | undefined
286
+ }
287
+
288
+ /**
289
+ * Fetches Candid source using the temporary hack method.
290
+ * This calls the `__get_candid_interface_tmp_hack` query method on the canister.
291
+ *
292
+ * @param canisterId - The canister ID to query.
293
+ * @returns The Candid source string.
294
+ */
295
+ public async fetchFromTmpHack(canisterId: CanisterId): Promise<string> {
296
+ const canisterIdStr =
297
+ typeof canisterId === "string" ? canisterId : canisterId.toString()
298
+
299
+ // Use raw agent.query instead of Actor.createActor
300
+ const response = await this.agent.query(canisterIdStr, {
301
+ methodName: "__get_candid_interface_tmp_hack",
302
+ arg: IDL.encode([], []),
303
+ })
304
+
305
+ if ("reply" in response && response.reply) {
306
+ const [candidSource] = IDL.decode([IDL.Text], response.reply.arg) as [
307
+ string,
308
+ ]
309
+ return candidSource
310
+ }
311
+
312
+ throw new Error(`Query failed: ${JSON.stringify(response)}`)
313
+ }
314
+
315
+ // ═══════════════════════════════════════════════════════════════════════════
316
+ // COMPILE METHODS - Methods for compiling Candid to JavaScript
317
+ // ═══════════════════════════════════════════════════════════════════════════
318
+
319
+ /**
320
+ * Compiles Candid source to JavaScript using the local WASM parser.
321
+ *
322
+ * @param candidSource - The Candid source to compile.
323
+ * @returns The compiled JavaScript code.
324
+ * @throws Error if the parser is not loaded.
325
+ */
326
+ public compileLocal(candidSource: string): string {
327
+ if (!this.parserModule) {
328
+ throw new Error("Parser not loaded. Call loadParser() first.")
329
+ }
330
+
331
+ return this.parserModule.didToJs(candidSource)
332
+ }
333
+
334
+ /**
335
+ * Compiles Candid source to JavaScript using the remote didjs canister.
336
+ *
337
+ * @param candidSource - The Candid source to compile.
338
+ * @param didjsCanisterId - Optional custom didjs canister ID.
339
+ * @returns The compiled JavaScript code, or undefined if compilation fails.
340
+ */
341
+ public async compileRemote(
342
+ candidSource: string,
343
+ didjsCanisterId?: string
344
+ ): Promise<string | undefined> {
345
+ const canisterId = didjsCanisterId || this.didjsCanisterId
346
+
347
+ // Use raw agent.query instead of Actor.createActor
348
+ const response = await this.agent.query(canisterId, {
349
+ methodName: "did_to_js",
350
+ arg: IDL.encode([IDL.Text], [candidSource]),
351
+ })
352
+
353
+ if ("reply" in response && response.reply) {
354
+ const [result] = IDL.decode([IDL.Opt(IDL.Text)], response.reply.arg) as [
355
+ [string] | [],
356
+ ]
357
+ return result[0]
358
+ }
359
+
360
+ throw new Error(`Query failed: ${JSON.stringify(response)}`)
361
+ }
362
+
363
+ /**
364
+ * Validates Candid source using the local parser.
365
+ *
366
+ * @param candidSource - The Candid source to validate.
367
+ * @returns True if the source is valid, false otherwise.
368
+ * @throws Error if the parser is not loaded.
369
+ */
370
+ public validateCandid(candidSource: string): boolean {
371
+ if (!this.parserModule) {
372
+ throw new Error("Parser not loaded. Call loadParser() first.")
373
+ }
374
+
375
+ return this.parserModule.validateIDL(candidSource)
376
+ }
377
+
378
+ // ═══════════════════════════════════════════════════════════════════════════
379
+ // DEPRECATED ALIASES - For backwards compatibility
380
+ // ═══════════════════════════════════════════════════════════════════════════
381
+
382
+ /**
383
+ * @deprecated Use `loadParser()` instead.
384
+ */
385
+ public async initializeParser(module?: ReactorParser): Promise<void> {
386
+ return this.loadParser(module)
387
+ }
388
+
389
+ /**
390
+ * @deprecated Use `fetchCandidSource()` instead.
391
+ */
392
+ public async fetchCandidDefinition(canisterId: CanisterId): Promise<string> {
393
+ return this.fetchCandidSource(canisterId)
394
+ }
395
+
396
+ /**
397
+ * @deprecated Use `fetchFromMetadata()` instead.
398
+ */
399
+ public async getFromMetadata(
400
+ canisterId: CanisterId
401
+ ): Promise<string | undefined> {
402
+ return this.fetchFromMetadata(canisterId)
403
+ }
404
+
405
+ /**
406
+ * @deprecated Use `fetchFromTmpHack()` instead.
407
+ */
408
+ public async getFromTmpHack(canisterId: CanisterId): Promise<string> {
409
+ return this.fetchFromTmpHack(canisterId)
410
+ }
411
+
412
+ /**
413
+ * @deprecated Use `parseCandidSource()` instead.
414
+ */
415
+ public async evaluateCandidDefinition(
416
+ data: string
417
+ ): Promise<CandidDefinition> {
418
+ return this.parseCandidSource(data)
419
+ }
420
+
421
+ /**
422
+ * @deprecated Use `compileRemote()` instead.
423
+ */
424
+ public async fetchDidTojs(
425
+ candidSource: string,
426
+ didjsCanisterId?: string
427
+ ): Promise<string | undefined> {
428
+ return this.compileRemote(candidSource, didjsCanisterId)
429
+ }
430
+
431
+ /**
432
+ * @deprecated Use `compileLocal()` instead.
433
+ */
434
+ public parseDidToJs(candidSource: string): string {
435
+ return this.compileLocal(candidSource)
436
+ }
437
+
438
+ /**
439
+ * @deprecated Use `validateCandid()` instead.
440
+ */
441
+ public validateIDL(candidSource: string): boolean {
442
+ return this.validateCandid(candidSource)
443
+ }
444
+ }
445
+
446
+ const noop = () => {}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Default didjs canister ID for the IC mainnet.
3
+ * This canister is used to compile Candid to JavaScript.
4
+ */
5
+ export const DEFAULT_IC_DIDJS_ID = "a4gq6-oaaaa-aaaab-qaa4q-cai"
6
+
7
+ /**
8
+ * Default didjs canister ID for local development.
9
+ * This canister is used to compile Candid to JavaScript locally.
10
+ */
11
+ export const DEFAULT_LOCAL_DIDJS_ID = "bd3sg-teaaa-aaaaa-qaaba-cai"
@@ -0,0 +1,324 @@
1
+ import type { BaseActor, DisplayReactorParameters } from "@ic-reactor/core"
2
+ import type {
3
+ CandidDisplayReactorParameters,
4
+ DynamicMethodOptions,
5
+ } from "./types"
6
+
7
+ import {
8
+ DisplayReactor,
9
+ didToDisplayCodec,
10
+ didTypeFromArray,
11
+ } from "@ic-reactor/core"
12
+ import { CandidAdapter } from "./adapter"
13
+ import { IDL } from "@icp-sdk/core/candid"
14
+
15
+ // ============================================================================
16
+ // CandidDisplayReactor
17
+ // ============================================================================
18
+
19
+ /**
20
+ * CandidDisplayReactor combines the display transformation capabilities of
21
+ * DisplayReactor with dynamic Candid parsing from CandidReactor.
22
+ *
23
+ * This class provides:
24
+ * - **Display transformations**: Automatic type conversion between Candid and
25
+ * display-friendly types (bigint ↔ string, Principal ↔ string, etc.)
26
+ * - **Validation**: Optional argument validation with display types
27
+ * - **Dynamic Candid parsing**: Initialize from Candid source or fetch from network
28
+ * - **Dynamic method registration**: Register methods at runtime with Candid signatures
29
+ *
30
+ * @typeParam A - The actor service type
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { CandidDisplayReactor } from "@ic-reactor/candid"
35
+ *
36
+ * const reactor = new CandidDisplayReactor({
37
+ * clientManager,
38
+ * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
39
+ * })
40
+ *
41
+ * // Initialize from network (fetches Candid from canister)
42
+ * await reactor.initialize()
43
+ *
44
+ * // Or provide Candid source directly
45
+ * const reactor2 = new CandidDisplayReactor({
46
+ * clientManager,
47
+ * canisterId: "...",
48
+ * candid: `service : { greet : (text) -> (text) query }`
49
+ * })
50
+ * await reactor2.initialize()
51
+ *
52
+ * // Call methods with display types (strings instead of bigint/Principal)
53
+ * const result = await reactor.callMethod({
54
+ * functionName: "transfer",
55
+ * args: [{ to: "aaaaa-aa", amount: "1000000" }] // strings!
56
+ * })
57
+ *
58
+ * // Add validation
59
+ * reactor.registerValidator("transfer", ([input]) => {
60
+ * if (!input.to) {
61
+ * return { success: false, issues: [{ path: ["to"], message: "Required" }] }
62
+ * }
63
+ * return { success: true }
64
+ * })
65
+ * ```
66
+ */
67
+ export class CandidDisplayReactor<A = BaseActor> extends DisplayReactor<A> {
68
+ public adapter: CandidAdapter
69
+ private candidSource?: string
70
+
71
+ constructor(config: CandidDisplayReactorParameters<A>) {
72
+ const superConfig = { ...config }
73
+
74
+ if (!superConfig.idlFactory) {
75
+ superConfig.idlFactory = ({ IDL }) => IDL.Service({})
76
+ }
77
+
78
+ super(superConfig as DisplayReactorParameters<A>)
79
+
80
+ this.candidSource = config.candid
81
+
82
+ if (config.adapter) {
83
+ this.adapter = config.adapter
84
+ } else {
85
+ this.adapter = new CandidAdapter({
86
+ clientManager: this.clientManager,
87
+ })
88
+ }
89
+ }
90
+
91
+ // ══════════════════════════════════════════════════════════════════════════
92
+ // INITIALIZATION
93
+ // ══════════════════════════════════════════════════════════════════════════
94
+
95
+ /**
96
+ * Initializes the reactor by parsing the provided Candid string or fetching it from the network.
97
+ * This updates the internal service definition with the actual canister interface.
98
+ *
99
+ * After initialization, all DisplayReactor methods work with display type transformations.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const reactor = new CandidDisplayReactor({
104
+ * clientManager,
105
+ * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
106
+ * })
107
+ *
108
+ * // Fetches Candid from the canister and initializes
109
+ * await reactor.initialize()
110
+ *
111
+ * // Now you can call methods with display types
112
+ * const balance = await reactor.callMethod({
113
+ * functionName: "icrc1_balance_of",
114
+ * args: [{ owner: "aaaaa-aa" }] // Principal as string!
115
+ * })
116
+ * ```
117
+ */
118
+ public async initialize(): Promise<void> {
119
+ let idlFactory: IDL.InterfaceFactory
120
+
121
+ if (this.candidSource) {
122
+ const definition = await this.adapter.parseCandidSource(this.candidSource)
123
+ idlFactory = definition.idlFactory
124
+ } else {
125
+ const definition = await this.adapter.getCandidDefinition(this.canisterId)
126
+ idlFactory = definition.idlFactory
127
+ }
128
+
129
+ this.service = idlFactory({ IDL })
130
+
131
+ // Re-initialize codecs after service is updated
132
+ this.reinitializeCodecs()
133
+ }
134
+
135
+ /**
136
+ * Re-initialize the display codecs after the service has been updated.
137
+ * This is called automatically after initialize() or registerMethod().
138
+ */
139
+ private reinitializeCodecs(): void {
140
+ const fields = this.getServiceInterface()?._fields
141
+ if (!fields) return
142
+
143
+ // Access the private codecs map from DisplayReactor
144
+ const codecs = (this as any).codecs as Map<
145
+ string,
146
+ { args: any; result: any }
147
+ >
148
+
149
+ for (const [methodName, funcType] of fields) {
150
+ // Skip if already exists
151
+ if (codecs.has(methodName)) continue
152
+
153
+ const argsIdlType = didTypeFromArray(funcType.argTypes)
154
+ const retIdlType = didTypeFromArray(funcType.retTypes)
155
+
156
+ codecs.set(methodName, {
157
+ args: didToDisplayCodec(argsIdlType),
158
+ result: didToDisplayCodec(retIdlType),
159
+ })
160
+ }
161
+ }
162
+
163
+ // ══════════════════════════════════════════════════════════════════════════
164
+ // DYNAMIC METHOD REGISTRATION
165
+ // ══════════════════════════════════════════════════════════════════════════
166
+
167
+ /**
168
+ * Register a dynamic method by its Candid signature.
169
+ * After registration, all DisplayReactor methods work with display type transformations.
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * // Register a method
174
+ * await reactor.registerMethod({
175
+ * functionName: "icrc1_balance_of",
176
+ * candid: "(record { owner : principal }) -> (nat) query"
177
+ * })
178
+ *
179
+ * // Now use with display types!
180
+ * const balance = await reactor.callMethod({
181
+ * functionName: "icrc1_balance_of",
182
+ * args: [{ owner: "aaaaa-aa" }] // Principal as string
183
+ * })
184
+ * // balance is string (not bigint) due to display transformation
185
+ * ```
186
+ */
187
+ public async registerMethod(options: DynamicMethodOptions): Promise<void> {
188
+ const { functionName, candid } = options
189
+
190
+ // Check if method already registered
191
+ const existing = this.service._fields.find(
192
+ ([name]) => name === functionName
193
+ )
194
+ if (existing) return
195
+
196
+ // Parse the Candid signature
197
+ const serviceSource = candid.includes("service :")
198
+ ? candid
199
+ : `service : { ${functionName} : ${candid}; }`
200
+
201
+ const { idlFactory } = await this.adapter.parseCandidSource(serviceSource)
202
+ const parsedService = idlFactory({ IDL })
203
+
204
+ const funcField = parsedService._fields.find(
205
+ ([name]) => name === functionName
206
+ )
207
+ if (!funcField) {
208
+ throw new Error(
209
+ `Method "${functionName}" not found in the provided Candid signature`
210
+ )
211
+ }
212
+
213
+ // Inject into our service
214
+ this.service._fields.push(funcField)
215
+
216
+ // Re-initialize codecs for the new method
217
+ this.reinitializeCodecs()
218
+ }
219
+
220
+ /**
221
+ * Register multiple methods at once.
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * await reactor.registerMethods([
226
+ * { functionName: "icrc1_balance_of", candid: "(record { owner : principal }) -> (nat) query" },
227
+ * { functionName: "icrc1_transfer", candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })" }
228
+ * ])
229
+ * ```
230
+ */
231
+ public async registerMethods(methods: DynamicMethodOptions[]): Promise<void> {
232
+ await Promise.all(methods.map((m) => this.registerMethod(m)))
233
+ }
234
+
235
+ /**
236
+ * Check if a method is registered (either from initialize or registerMethod).
237
+ */
238
+ public hasMethod(functionName: string): boolean {
239
+ return this.service._fields.some(([name]) => name === functionName)
240
+ }
241
+
242
+ /**
243
+ * Get all registered method names.
244
+ */
245
+ public getMethodNames(): string[] {
246
+ return this.service._fields.map(([name]) => name)
247
+ }
248
+
249
+ // ══════════════════════════════════════════════════════════════════════════
250
+ // DYNAMIC CALL SHORTCUTS
251
+ // ══════════════════════════════════════════════════════════════════════════
252
+
253
+ /**
254
+ * Perform a dynamic update call in one step with display type transformations.
255
+ * Registers the method if not already registered, then calls it.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const result = await reactor.callDynamic({
260
+ * functionName: "transfer",
261
+ * candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
262
+ * args: [{ to: "aaaaa-aa", amount: "100" }] // Display types!
263
+ * })
264
+ * ```
265
+ */
266
+ public async callDynamic<T = unknown>(
267
+ options: DynamicMethodOptions & { args?: unknown[] }
268
+ ): Promise<T> {
269
+ await this.registerMethod(options)
270
+ return this.callMethod({
271
+ functionName: options.functionName as any,
272
+ args: options.args as any,
273
+ }) as T
274
+ }
275
+
276
+ /**
277
+ * Perform a dynamic query call in one step with display type transformations.
278
+ * Registers the method if not already registered, then calls it.
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * const balance = await reactor.queryDynamic({
283
+ * functionName: "icrc1_balance_of",
284
+ * candid: "(record { owner : principal }) -> (nat) query",
285
+ * args: [{ owner: "aaaaa-aa" }] // Display types!
286
+ * })
287
+ * // balance is string (not BigInt)
288
+ * ```
289
+ */
290
+ public async queryDynamic<T = unknown>(
291
+ options: DynamicMethodOptions & { args?: unknown[] }
292
+ ): Promise<T> {
293
+ await this.registerMethod(options)
294
+ return this.callMethod({
295
+ functionName: options.functionName as any,
296
+ args: options.args as any,
297
+ }) as T
298
+ }
299
+
300
+ /**
301
+ * Fetch with dynamic Candid and TanStack Query caching.
302
+ * Registers the method if not already registered, then fetches with caching.
303
+ * Results are transformed to display types.
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const balance = await reactor.fetchQueryDynamic({
308
+ * functionName: "icrc1_balance_of",
309
+ * candid: "(record { owner : principal }) -> (nat) query",
310
+ * args: [{ owner: "aaaaa-aa" }]
311
+ * })
312
+ * // Subsequent calls with same args return cached result
313
+ * ```
314
+ */
315
+ public async fetchQueryDynamic<T = unknown>(
316
+ options: DynamicMethodOptions & { args?: unknown[] }
317
+ ): Promise<T> {
318
+ await this.registerMethod(options)
319
+ return this.fetchQuery({
320
+ functionName: options.functionName as any,
321
+ args: options.args as any,
322
+ }) as T
323
+ }
324
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { CandidAdapter } from "./adapter"
2
+ export * from "./types"
3
+ export * from "./constants"
4
+ export * from "./utils"
5
+ export * from "./reactor"
6
+ export * from "./display-reactor"
package/src/reactor.ts ADDED
@@ -0,0 +1,199 @@
1
+ import type { BaseActor, ReactorParameters } from "@ic-reactor/core"
2
+ import type { CandidReactorParameters, DynamicMethodOptions } from "./types"
3
+
4
+ import { Reactor } from "@ic-reactor/core"
5
+ import { CandidAdapter } from "./adapter"
6
+ import { IDL } from "@icp-sdk/core/candid"
7
+
8
+ export class CandidReactor<A = BaseActor> extends Reactor<A> {
9
+ public adapter: CandidAdapter
10
+ private candidSource?: string
11
+
12
+ constructor(config: CandidReactorParameters) {
13
+ const superConfig = { ...config }
14
+
15
+ if (!superConfig.idlFactory) {
16
+ superConfig.idlFactory = ({ IDL }) => IDL.Service({})
17
+ }
18
+
19
+ super(superConfig as ReactorParameters)
20
+
21
+ this.candidSource = config.candid
22
+ if (config.adapter) {
23
+ this.adapter = config.adapter
24
+ } else {
25
+ this.adapter = new CandidAdapter({
26
+ clientManager: this.clientManager,
27
+ })
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Initializes the reactor by parsing the provided Candid string or fetching it from the network.
33
+ * This updates the internal service definition with the actual canister interface.
34
+ *
35
+ * After initialization, all standard Reactor methods (callMethod, fetchQuery, etc.) work.
36
+ */
37
+ public async initialize(): Promise<void> {
38
+ let idlFactory: IDL.InterfaceFactory
39
+
40
+ if (this.candidSource) {
41
+ const definition = await this.adapter.parseCandidSource(this.candidSource)
42
+ idlFactory = definition.idlFactory
43
+ } else {
44
+ const definition = await this.adapter.getCandidDefinition(this.canisterId)
45
+ idlFactory = definition.idlFactory
46
+ }
47
+
48
+ this.service = idlFactory({ IDL })
49
+ }
50
+
51
+ /**
52
+ * Register a dynamic method by its Candid signature.
53
+ * After registration, all standard Reactor methods work with this method name.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Register a method
58
+ * await reactor.registerMethod({
59
+ * functionName: "icrc1_balance_of",
60
+ * candid: "(record { owner : principal }) -> (nat) query"
61
+ * })
62
+ *
63
+ * // Now use standard Reactor methods!
64
+ * const balance = await reactor.callMethod({
65
+ * functionName: "icrc1_balance_of",
66
+ * args: [{ owner }]
67
+ * })
68
+ *
69
+ * // Or with caching
70
+ * const cachedBalance = await reactor.fetchQuery({
71
+ * functionName: "icrc1_balance_of",
72
+ * args: [{ owner }]
73
+ * })
74
+ * ```
75
+ */
76
+ public async registerMethod(options: DynamicMethodOptions): Promise<void> {
77
+ const { functionName, candid } = options
78
+
79
+ // Check if method already registered
80
+ const existing = this.service._fields.find(
81
+ ([name]) => name === functionName
82
+ )
83
+ if (existing) return
84
+
85
+ // Parse the Candid signature
86
+ const serviceSource = candid.includes("service :")
87
+ ? candid
88
+ : `service : { ${functionName} : ${candid}; }`
89
+
90
+ const { idlFactory } = await this.adapter.parseCandidSource(serviceSource)
91
+ const parsedService = idlFactory({ IDL })
92
+
93
+ const funcField = parsedService._fields.find(
94
+ ([name]) => name === functionName
95
+ )
96
+ if (!funcField) {
97
+ throw new Error(
98
+ `Method "${functionName}" not found in the provided Candid signature`
99
+ )
100
+ }
101
+
102
+ // Inject into our service
103
+ this.service._fields.push(funcField)
104
+ }
105
+
106
+ /**
107
+ * Register multiple methods at once.
108
+ */
109
+ public async registerMethods(methods: DynamicMethodOptions[]): Promise<void> {
110
+ await Promise.all(methods.map((m) => this.registerMethod(m)))
111
+ }
112
+
113
+ /**
114
+ * Check if a method is registered (either from initialize or registerMethod).
115
+ */
116
+ public hasMethod(functionName: string): boolean {
117
+ return this.service._fields.some(([name]) => name === functionName)
118
+ }
119
+
120
+ /**
121
+ * Get all registered method names.
122
+ */
123
+ public getMethodNames(): string[] {
124
+ return this.service._fields.map(([name]) => name)
125
+ }
126
+
127
+ // ══════════════════════════════════════════════════════════════════════
128
+ // DYNAMIC CALL SHORTCUTS
129
+ // ══════════════════════════════════════════════════════════════════════
130
+
131
+ /**
132
+ * Perform a dynamic update call in one step.
133
+ * Registers the method if not already registered, then calls it.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const result = await reactor.callDynamic({
138
+ * functionName: "transfer",
139
+ * candid: "(record { to : principal; amount : nat }) -> (variant { Ok : nat; Err : text })",
140
+ * args: [{ to: Principal.fromText("..."), amount: 100n }]
141
+ * })
142
+ * ```
143
+ */
144
+ public async callDynamic<T = unknown>(
145
+ options: DynamicMethodOptions & { args?: unknown[] }
146
+ ): Promise<T> {
147
+ await this.registerMethod(options)
148
+ return this.callMethod({
149
+ functionName: options.functionName as any,
150
+ args: options.args as any,
151
+ }) as T
152
+ }
153
+
154
+ /**
155
+ * Perform a dynamic query call in one step.
156
+ * Registers the method if not already registered, then calls it.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const balance = await reactor.queryDynamic({
161
+ * functionName: "icrc1_balance_of",
162
+ * candid: "(record { owner : principal }) -> (nat) query",
163
+ * args: [{ owner: Principal.fromText("...") }]
164
+ * })
165
+ * ```
166
+ */
167
+ public async queryDynamic<T = unknown>(
168
+ options: DynamicMethodOptions & { args?: unknown[] }
169
+ ): Promise<T> {
170
+ await this.registerMethod(options)
171
+ return this.callMethod({
172
+ functionName: options.functionName as any,
173
+ args: options.args as any,
174
+ }) as T
175
+ }
176
+
177
+ /**
178
+ * Fetch with dynamic Candid and TanStack Query caching.
179
+ * Registers the method if not already registered, then fetches with caching.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const balance = await reactor.fetchQueryDynamic({
184
+ * functionName: "icrc1_balance_of",
185
+ * candid: "(record { owner : principal }) -> (nat) query",
186
+ * args: [{ owner }]
187
+ * })
188
+ * ```
189
+ */
190
+ public async fetchQueryDynamic<T = unknown>(
191
+ options: DynamicMethodOptions & { args?: unknown[] }
192
+ ): Promise<T> {
193
+ await this.registerMethod(options)
194
+ return this.fetchQuery({
195
+ functionName: options.functionName as any,
196
+ args: options.args as any,
197
+ }) as T
198
+ }
199
+ }
package/src/types.ts ADDED
@@ -0,0 +1,107 @@
1
+ import type { HttpAgent, Identity } from "@icp-sdk/core/agent"
2
+ import type { IDL } from "@icp-sdk/core/candid"
3
+ import type {
4
+ BaseActor,
5
+ CanisterId,
6
+ DisplayReactorParameters,
7
+ ReactorParameters,
8
+ } from "@ic-reactor/core"
9
+ import type { CandidAdapter } from "./adapter"
10
+
11
+ export interface DynamicMethodOptions {
12
+ /** The method name to register. */
13
+ functionName: string
14
+ /**
15
+ * The Candid signature for the method.
16
+ * Can be either a method signature like "(text) -> (text) query"
17
+ * or a full service definition like "service : { greet: (text) -> (text) query }".
18
+ */
19
+ candid: string
20
+ }
21
+
22
+ export interface CandidReactorParameters extends Omit<
23
+ ReactorParameters,
24
+ "idlFactory"
25
+ > {
26
+ /** The canister ID. */
27
+ canisterId?: CanisterId
28
+ /** The Candid source code. If not provided, the canister's Candid will be fetched. */
29
+ candid?: string
30
+ /** The IDL interface factory. */
31
+ idlFactory?: (IDL: any) => any
32
+ /** The Candid adapter. */
33
+ adapter?: CandidAdapter
34
+ }
35
+
36
+ // ============================================================================
37
+ // CandidDisplayReactor Parameters
38
+ // ============================================================================
39
+
40
+ export interface CandidDisplayReactorParameters<A = BaseActor> extends Omit<
41
+ DisplayReactorParameters<A>,
42
+ "idlFactory"
43
+ > {
44
+ /** The canister ID. */
45
+ canisterId?: CanisterId
46
+ /** The Candid source code. If not provided, the canister's Candid will be fetched. */
47
+ candid?: string
48
+ /** The IDL interface factory. */
49
+ idlFactory?: (IDL: any) => any
50
+ /** The Candid adapter. */
51
+ adapter?: CandidAdapter
52
+ }
53
+
54
+ /**
55
+ * Minimal interface for ClientManager that CandidAdapter depends on.
56
+ * This allows the candid package to work with ClientManager without importing the full core package.
57
+ */
58
+ export interface CandidClientManager {
59
+ /** The HTTP agent used for making requests. */
60
+ agent: HttpAgent
61
+ /** Whether the agent is connected to a local network. */
62
+ isLocal: boolean
63
+ /** Subscribe to identity changes. Returns an unsubscribe function. */
64
+ subscribe(callback: (identity: Identity) => void): () => void
65
+ }
66
+
67
+ /**
68
+ * Parameters for initializing the CandidAdapter.
69
+ */
70
+ export interface CandidAdapterParameters {
71
+ /** The client manager that provides agent and identity access. */
72
+ clientManager: CandidClientManager
73
+ /** The canister ID of the didjs canister for compiling Candid to JavaScript. */
74
+ didjsCanisterId?: CanisterId
75
+ }
76
+
77
+ /**
78
+ * Represents a parsed Candid definition with IDL factory and initialization.
79
+ */
80
+ export interface CandidDefinition {
81
+ /** The IDL interface factory. */
82
+ idlFactory: IDL.InterfaceFactory
83
+ /** Optional init function for the canister. */
84
+ init?: (args: { IDL: typeof IDL }) => IDL.Type<unknown>[]
85
+ }
86
+
87
+ /**
88
+ * Interface for the optional parser module (@ic-reactor/parser).
89
+ */
90
+ export interface ReactorParser {
91
+ /**
92
+ * Default function to initialize the WASM module.
93
+ */
94
+ default?: () => Promise<void>
95
+ /**
96
+ * Converts Candid (DID) source to JavaScript code.
97
+ * @param candidSource - The Candid source code.
98
+ * @returns The JavaScript code.
99
+ */
100
+ didToJs(candidSource: string): string
101
+ /**
102
+ * Validates the Candid (IDL) source.
103
+ * @param candidSource - The Candid source code.
104
+ * @returns True if valid, false otherwise.
105
+ */
106
+ validateIDL(candidSource: string): boolean
107
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { CandidDefinition } from "./types"
2
+
3
+ /**
4
+ * Imports and evaluates a Candid definition from JavaScript code.
5
+ *
6
+ * @param candidJs - The JavaScript code containing the Candid definition.
7
+ * @returns A promise that resolves to the CandidDefinition.
8
+ * @throws Error if the import fails.
9
+ */
10
+ export async function importCandidDefinition(
11
+ candidJs: string
12
+ ): Promise<CandidDefinition> {
13
+ try {
14
+ // Create a data URL with the JavaScript code
15
+ const dataUri =
16
+ "data:text/javascript;charset=utf-8," + encodeURIComponent(candidJs)
17
+
18
+ // Dynamically import the module
19
+ const module = await import(/* webpackIgnore: true */ dataUri)
20
+
21
+ return {
22
+ idlFactory: module.idlFactory,
23
+ init: module.init,
24
+ }
25
+ } catch (error) {
26
+ throw new Error(`Failed to import Candid definition: ${error}`)
27
+ }
28
+ }