@ic-reactor/candid 3.0.7-beta.0 → 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 +5 -1
- package/package.json +4 -3
- package/src/adapter.ts +446 -0
- package/src/constants.ts +11 -0
- package/src/display-reactor.ts +324 -0
- package/src/index.ts +6 -0
- package/src/reactor.ts +199 -0
- package/src/types.ts +107 -0
- package/src/utils.ts +28 -0
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
|
-
| `
|
|
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.
|
|
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.
|
|
45
|
+
"@ic-reactor/core": "^3.0.7-beta.2"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
48
|
"@icp-sdk/core": "^5.0.0"
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@icp-sdk/core": "^5.0.0",
|
|
52
53
|
"@size-limit/preset-small-lib": "^12.0.0",
|
|
53
|
-
"@types/node": "^25.0.
|
|
54
|
+
"@types/node": "^25.0.9",
|
|
54
55
|
"size-limit": "^12.0.0",
|
|
55
56
|
"vitest": "^4.0.17"
|
|
56
57
|
},
|
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 = () => {}
|
package/src/constants.ts
ADDED
|
@@ -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
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
|
+
}
|