@ic-reactor/core 3.0.1-beta.0 → 3.0.1

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.
Files changed (74) hide show
  1. package/README.md +6 -4
  2. package/dist/client.d.ts +3 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +48 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/display/types.d.ts +2 -2
  7. package/dist/display/types.d.ts.map +1 -1
  8. package/dist/display/visitor.d.ts +2 -1
  9. package/dist/display/visitor.d.ts.map +1 -1
  10. package/dist/display/visitor.js +146 -121
  11. package/dist/display/visitor.js.map +1 -1
  12. package/dist/display-reactor.d.ts +9 -92
  13. package/dist/display-reactor.d.ts.map +1 -1
  14. package/dist/display-reactor.js +5 -41
  15. package/dist/display-reactor.js.map +1 -1
  16. package/dist/reactor.d.ts +36 -4
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +101 -61
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types/client.d.ts +11 -0
  21. package/dist/types/client.d.ts.map +1 -1
  22. package/dist/types/display-reactor.d.ts +54 -0
  23. package/dist/types/display-reactor.d.ts.map +1 -0
  24. package/dist/types/display-reactor.js +5 -0
  25. package/dist/types/display-reactor.js.map +1 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/index.js +1 -0
  29. package/dist/types/index.js.map +1 -1
  30. package/dist/types/reactor.d.ts +10 -15
  31. package/dist/types/reactor.d.ts.map +1 -1
  32. package/dist/types/transform.d.ts +1 -1
  33. package/dist/types/transform.d.ts.map +1 -1
  34. package/dist/utils/constants.d.ts +1 -1
  35. package/dist/utils/constants.d.ts.map +1 -1
  36. package/dist/utils/constants.js +1 -1
  37. package/dist/utils/constants.js.map +1 -1
  38. package/dist/utils/helper.d.ts +20 -1
  39. package/dist/utils/helper.d.ts.map +1 -1
  40. package/dist/utils/helper.js +37 -6
  41. package/dist/utils/helper.js.map +1 -1
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/dist/utils/index.js +1 -0
  45. package/dist/utils/index.js.map +1 -1
  46. package/dist/utils/zod.d.ts +34 -0
  47. package/dist/utils/zod.d.ts.map +1 -0
  48. package/dist/utils/zod.js +39 -0
  49. package/dist/utils/zod.js.map +1 -0
  50. package/package.json +7 -6
  51. package/src/client.ts +571 -0
  52. package/src/display/helper.ts +92 -0
  53. package/src/display/index.ts +3 -0
  54. package/src/display/types.ts +91 -0
  55. package/src/display/visitor.ts +415 -0
  56. package/src/display-reactor.ts +361 -0
  57. package/src/errors/index.ts +246 -0
  58. package/src/index.ts +8 -0
  59. package/src/reactor.ts +461 -0
  60. package/src/types/client.ts +110 -0
  61. package/src/types/display-reactor.ts +73 -0
  62. package/src/types/index.ts +6 -0
  63. package/src/types/reactor.ts +188 -0
  64. package/src/types/result.ts +50 -0
  65. package/src/types/transform.ts +29 -0
  66. package/src/types/variant.ts +39 -0
  67. package/src/utils/agent.ts +201 -0
  68. package/src/utils/candid.ts +112 -0
  69. package/src/utils/constants.ts +12 -0
  70. package/src/utils/helper.ts +155 -0
  71. package/src/utils/index.ts +5 -0
  72. package/src/utils/polling.ts +330 -0
  73. package/src/utils/zod.ts +56 -0
  74. package/src/version.ts +4 -0
package/src/client.ts ADDED
@@ -0,0 +1,571 @@
1
+ import type { Identity } from "@icp-sdk/core/agent"
2
+ import type { AuthClient, AuthClientLoginOptions } from "@icp-sdk/auth/client"
3
+ import type {
4
+ ClientManagerParameters,
5
+ AgentState,
6
+ AuthState,
7
+ } from "./types/client"
8
+ import type { Principal } from "@icp-sdk/core/principal"
9
+ import type { QueryClient } from "@tanstack/react-query"
10
+
11
+ import { HttpAgent } from "@icp-sdk/core/agent"
12
+ import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
13
+ import {
14
+ IC_HOST_NETWORK_URI,
15
+ IC_INTERNET_IDENTITY_PROVIDER,
16
+ LOCAL_INTERNET_IDENTITY_PROVIDER,
17
+ } from "./utils/constants"
18
+ import {
19
+ getNetworkByHostname,
20
+ getProcessEnvNetwork,
21
+ isDev,
22
+ } from "./utils/helper"
23
+
24
+ /**
25
+ * ClientManager is a central class for managing the Internet Computer (IC) agent and authentication state.
26
+ *
27
+ * It initializes the agent (connecting to local or mainnet), handles authentication via AuthClient,
28
+ * and integrates with TanStack Query's QueryClient for state management.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { ClientManager } from "@ic-reactor/core";
33
+ * import { QueryClient } from "@tanstack/react-query";
34
+ *
35
+ * const queryClient = new QueryClient();
36
+ * const clientManager = new ClientManager({
37
+ * queryClient,
38
+ * withLocalEnv: true, // Use local replica
39
+ * });
40
+ *
41
+ * await clientManager.initialize();
42
+ * ```
43
+ */
44
+ export class ClientManager {
45
+ #agent: HttpAgent
46
+ #authClient?: AuthClient
47
+ #identitySubscribers: Array<(identity: Identity) => void> = []
48
+ #agentStateSubscribers: Array<(state: AgentState) => void> = []
49
+ #authStateSubscribers: Array<(state: AuthState) => void> = []
50
+ #targetCanisterIds: Set<string> = new Set()
51
+
52
+ /**
53
+ * The TanStack QueryClient used for managing cached canister data and invalidating queries on identity changes.
54
+ */
55
+ public queryClient: QueryClient
56
+ /**
57
+ * Current state of the HttpAgent, including initialization status, network, and error information.
58
+ */
59
+ public agentState: AgentState
60
+ /**
61
+ * Current authentication state, including the active identity, authentication progress, and errors.
62
+ */
63
+ public authState: AuthState
64
+
65
+ private initPromise?: Promise<void>
66
+ private authPromise?: Promise<Identity | undefined>
67
+ private port: number
68
+ private internetIdentityId?: string
69
+
70
+ /**
71
+ * Creates a new instance of ClientManager.
72
+ *
73
+ * @param parameters - Configuration options for the agent and network environment.
74
+ */
75
+ constructor({
76
+ port = 4943,
77
+ withLocalEnv,
78
+ withProcessEnv,
79
+ withCanisterEnv,
80
+ agentOptions = {},
81
+ queryClient,
82
+ authClient,
83
+ }: ClientManagerParameters) {
84
+ this.queryClient = queryClient
85
+
86
+ this.agentState = {
87
+ isInitialized: false,
88
+ isInitializing: false,
89
+ error: undefined,
90
+ network: undefined,
91
+ isLocalhost: false,
92
+ }
93
+
94
+ this.authState = {
95
+ identity: null,
96
+ isAuthenticating: false,
97
+ isAuthenticated: false,
98
+ error: undefined,
99
+ }
100
+
101
+ this.port = port
102
+
103
+ // EXPERIMENTAL: Use canister environment from ic_env cookie when enabled
104
+ // ⚠️ This may cause issues with update calls on localhost development
105
+ if (withCanisterEnv) {
106
+ const canisterEnv =
107
+ typeof window !== "undefined" ? safeGetCanisterEnv() : undefined
108
+
109
+ if (canisterEnv) {
110
+ this.internetIdentityId =
111
+ canisterEnv["internet_identity"] ||
112
+ canisterEnv["PUBLIC_CANISTER_ID:internet_identity"] ||
113
+ canisterEnv["CANISTER_ID_INTERNET_IDENTITY"]
114
+ }
115
+
116
+ if (isDev() && typeof window !== "undefined") {
117
+ agentOptions.host = agentOptions.host ?? window.location.origin
118
+ agentOptions.verifyQuerySignatures =
119
+ agentOptions.verifyQuerySignatures ?? false
120
+ } else {
121
+ agentOptions.verifyQuerySignatures =
122
+ agentOptions.verifyQuerySignatures ?? true
123
+ }
124
+
125
+ if (canisterEnv?.IC_ROOT_KEY) {
126
+ agentOptions.rootKey = agentOptions.rootKey ?? canisterEnv.IC_ROOT_KEY
127
+ }
128
+ }
129
+
130
+ if (withProcessEnv) {
131
+ const processNetwork = getProcessEnvNetwork()
132
+ if (processNetwork === "ic") {
133
+ agentOptions.host = IC_HOST_NETWORK_URI
134
+ } else if (processNetwork === "local") {
135
+ agentOptions.host =
136
+ typeof process !== "undefined" && process.env.IC_HOST
137
+ ? process.env.IC_HOST
138
+ : `http://127.0.0.1:${port}`
139
+ }
140
+ } else if (withLocalEnv) {
141
+ agentOptions.host = `http://127.0.0.1:${port}`
142
+ } else if (!withCanisterEnv) {
143
+ // Only set default host if withCanisterEnv hasn't already configured it
144
+ agentOptions.host = agentOptions.host ?? IC_HOST_NETWORK_URI
145
+ }
146
+
147
+ this.#agent = HttpAgent.createSync(agentOptions)
148
+ this.updateAgentState({
149
+ isLocalhost: this.isLocal,
150
+ network: this.network,
151
+ })
152
+
153
+ if (authClient) {
154
+ this.#authClient = authClient
155
+ const identity = this.#authClient.getIdentity()
156
+ this.updateAgent(identity)
157
+ this.authState = {
158
+ identity,
159
+ isAuthenticated: !identity.getPrincipal().isAnonymous(),
160
+ isAuthenticating: false,
161
+ error: undefined,
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Orchestrates the complete initialization of the ClientManager.
168
+ * This method awaits the agent's core initialization (e.g., fetching root keys)
169
+ * and triggers the authentication (session restoration) in the background.
170
+ *
171
+ * @returns A promise that resolves to the ClientManager instance when core initialization is complete.
172
+ */
173
+ public async initialize() {
174
+ await this.initializeAgent()
175
+ this.authenticate()
176
+ return this
177
+ }
178
+
179
+ /**
180
+ * Specifically initializes the HttpAgent.
181
+ * On local networks, this includes fetching the root key for certificate verification.
182
+ *
183
+ * @returns A promise that resolves when the agent is fully initialized.
184
+ */
185
+ public async initializeAgent() {
186
+ if (this.agentState.isInitialized) {
187
+ return
188
+ }
189
+ if (this.agentState.isInitializing) {
190
+ return this.initPromise
191
+ }
192
+
193
+ this.initPromise = (async () => {
194
+ this.updateAgentState({ isInitializing: true })
195
+ if (typeof window !== "undefined") {
196
+ console.info(
197
+ `%cic-reactor:%c Initializing agent for ${this.network} network`,
198
+ "color: #3b82f6; font-weight: bold",
199
+ "color: inherit",
200
+ {
201
+ host: this.agentHost?.toString(),
202
+ isLocal: this.isLocal,
203
+ }
204
+ )
205
+ }
206
+ try {
207
+ if (this.isLocal) {
208
+ await this.#agent.fetchRootKey()
209
+ }
210
+ this.updateAgentState({ isInitialized: true, isInitializing: false })
211
+ } catch (error) {
212
+ this.updateAgentState({
213
+ error: error as Error,
214
+ isInitializing: false,
215
+ })
216
+ this.initPromise = undefined
217
+ throw error
218
+ }
219
+ })()
220
+
221
+ return this.initPromise
222
+ }
223
+
224
+ private authModuleMissing = false
225
+
226
+ /**
227
+ * Attempts to initialize the authentication client and restore a previous session.
228
+ *
229
+ * If an `AuthClient` is already initialized (passed in constructor or previously created),
230
+ * it uses that instance. Otherwise, it dynamically imports the `@icp-sdk/auth` module
231
+ * and creates a new AuthClient.
232
+ *
233
+ * If the module is missing and no client is provided, it fails gracefully by marking authentication as unavailable.
234
+ *
235
+ * @returns A promise that resolves to the restored Identity, or undefined if auth fails or is unavailable.
236
+ */
237
+ public authenticate = async (): Promise<Identity | undefined> => {
238
+ if (this.authState.isAuthenticated) {
239
+ return this.authState.identity || undefined
240
+ }
241
+ if (this.authPromise) {
242
+ return this.authPromise
243
+ }
244
+
245
+ if (this.authModuleMissing) {
246
+ return undefined
247
+ }
248
+
249
+ this.authPromise = (async () => {
250
+ if (typeof window !== "undefined") {
251
+ console.info(
252
+ `%cic-reactor:%c Authenticating...`,
253
+ "color: #3b82f6; font-weight: bold",
254
+ "color: inherit",
255
+ {
256
+ network: this.network,
257
+ authClient: this.#authClient ? "Shared Instance" : "Dynamic Import",
258
+ }
259
+ )
260
+ }
261
+ this.updateAuthState({ isAuthenticating: true })
262
+ try {
263
+ if (!this.#authClient) {
264
+ const authModule = await import("@icp-sdk/auth/client").catch(() => {
265
+ this.authModuleMissing = true
266
+ return null
267
+ })
268
+
269
+ if (!authModule) {
270
+ this.authModuleMissing = true
271
+ this.updateAuthState({ isAuthenticating: false })
272
+ return undefined
273
+ }
274
+
275
+ const { AuthClient } = authModule
276
+ this.#authClient = await AuthClient.create()
277
+ }
278
+ const identity = this.#authClient.getIdentity()
279
+ this.updateAgent(identity)
280
+ this.updateAuthState({
281
+ identity,
282
+ isAuthenticated: !identity.getPrincipal().isAnonymous(),
283
+ isAuthenticating: false,
284
+ })
285
+ return identity
286
+ } catch (error) {
287
+ this.updateAuthState({ error: error as Error, isAuthenticating: false })
288
+ console.error("Authentication failed:", error)
289
+ throw error
290
+ } finally {
291
+ this.authPromise = undefined
292
+ }
293
+ })()
294
+
295
+ return this.authPromise
296
+ }
297
+
298
+ /**
299
+ * Triggers the login flow using the Internet Identity provider.
300
+ *
301
+ * @param loginOptions - Options for the login flow, including identity provider and callbacks.
302
+ * @throws An error if the authentication module is not installed.
303
+ */
304
+ public login = async (loginOptions?: AuthClientLoginOptions) => {
305
+ try {
306
+ // Ensure agent is initialized before login
307
+ if (!this.agentState.isInitialized) {
308
+ await this.initializeAgent()
309
+ }
310
+
311
+ if (!this.#authClient) {
312
+ await this.authenticate()
313
+ }
314
+
315
+ if (!this.#authClient) {
316
+ throw new Error(
317
+ "Authentication module is missing or failed to initialize. To use login, please install the auth package: npm install @icp-sdk/auth"
318
+ )
319
+ }
320
+
321
+ this.updateAuthState({ isAuthenticating: true, error: undefined })
322
+
323
+ // Auto-detect identity provider based on network if not provided
324
+ const identityProvider =
325
+ loginOptions?.identityProvider || this.getDefaultIdentityProvider()
326
+
327
+ await this.#authClient.login({
328
+ ...loginOptions,
329
+ identityProvider,
330
+ onSuccess: () => {
331
+ const identity = this.#authClient!.getIdentity()
332
+ if (identity) {
333
+ this.updateAgent(identity)
334
+ this.updateAuthState({
335
+ identity,
336
+ isAuthenticated: true,
337
+ isAuthenticating: false,
338
+ })
339
+ }
340
+ ;(loginOptions?.onSuccess as any)?.()
341
+ },
342
+ onError: (error) => {
343
+ this.updateAuthState({
344
+ error: new Error(error),
345
+ isAuthenticating: false,
346
+ })
347
+ loginOptions?.onError?.(error)
348
+ },
349
+ })
350
+ } catch (error) {
351
+ this.updateAuthState({
352
+ error: error as Error,
353
+ isAuthenticating: false,
354
+ })
355
+ throw error
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Logs out the user and reverts the agent to an anonymous identity.
361
+ *
362
+ * @throws An error if the authentication module is not installed.
363
+ */
364
+ public logout = async () => {
365
+ if (!this.#authClient) {
366
+ throw new Error(
367
+ "Authentication module is missing or failed to initialize. To use logout, please install the auth package: npm install @icp-sdk/auth"
368
+ )
369
+ }
370
+ this.updateAuthState({ isAuthenticating: true, error: undefined })
371
+ await this.#authClient.logout()
372
+ const identity = this.#authClient.getIdentity()
373
+ if (identity) {
374
+ this.updateAgent(identity)
375
+ this.updateAuthState({
376
+ identity,
377
+ isAuthenticated: false,
378
+ isAuthenticating: false,
379
+ })
380
+ }
381
+ }
382
+
383
+ /**
384
+ * The underlying HttpAgent managed by this class.
385
+ */
386
+ get agent() {
387
+ return this.#agent
388
+ }
389
+
390
+ /**
391
+ * The host URL of the current IC agent.
392
+ */
393
+ get agentHost(): URL | undefined {
394
+ return this.#agent.host
395
+ }
396
+
397
+ /**
398
+ * The hostname of the current IC agent.
399
+ */
400
+ get agentHostName() {
401
+ return this.agentHost?.hostname || ""
402
+ }
403
+
404
+ /**
405
+ * Returns true if the agent is connecting to a local environment.
406
+ */
407
+ get isLocal() {
408
+ return this.network !== "ic"
409
+ }
410
+
411
+ /**
412
+ * Returns the current network type ('ic' or 'local').
413
+ */
414
+ get network() {
415
+ const hostname = this.agentHostName
416
+ return getNetworkByHostname(hostname)
417
+ }
418
+
419
+ /**
420
+ * Returns the current user's Principal identity.
421
+ */
422
+ public getUserPrincipal() {
423
+ return this.#agent.getPrincipal()
424
+ }
425
+
426
+ /**
427
+ * Registers a canister ID that this agent will interact with.
428
+ * This is used for informational purposes and network detection.
429
+ */
430
+ public registerCanisterId(canisterId: string, name?: string): void {
431
+ if (typeof window !== "undefined") {
432
+ const actorName = name || canisterId
433
+ console.info(
434
+ `%cic-reactor:%c Adding actor ${actorName}`,
435
+ "color: #3b82f6; font-weight: bold",
436
+ "color: inherit",
437
+ {
438
+ network: this.network,
439
+ canisterId,
440
+ ...(name && { name }),
441
+ }
442
+ )
443
+ }
444
+ this.#targetCanisterIds.add(canisterId)
445
+ }
446
+
447
+ /**
448
+ * Returns a list of all canister IDs registered with this agent.
449
+ */
450
+ public connectedCanisterIds(): string[] {
451
+ return Array.from(this.#targetCanisterIds)
452
+ }
453
+
454
+ /**
455
+ * Get the subnet ID for a canister.
456
+ */
457
+ public getSubnetIdFromCanister(canisterId: string) {
458
+ return this.#agent.getSubnetIdFromCanister(canisterId)
459
+ }
460
+
461
+ /**
462
+ * Sync time with a specific subnet.
463
+ */
464
+ public syncTimeWithSubnet(subnetId: Principal) {
465
+ return this.#agent.syncTimeWithSubnet(subnetId)
466
+ }
467
+
468
+ private getDefaultIdentityProvider(): string {
469
+ if (this.isLocal) {
470
+ if (this.internetIdentityId) {
471
+ return `http://${this.internetIdentityId}.localhost:${this.port}`
472
+ }
473
+ return LOCAL_INTERNET_IDENTITY_PROVIDER
474
+ } else {
475
+ return IC_INTERNET_IDENTITY_PROVIDER
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Subscribes to identity changes (e.g., after login/logout).
481
+ * @param callback - Function called with the new identity.
482
+ * @returns An unsubscribe function.
483
+ */
484
+ public subscribe(callback: (identity: Identity) => void) {
485
+ this.#identitySubscribers.push(callback)
486
+ return () => {
487
+ this.#identitySubscribers = this.#identitySubscribers.filter(
488
+ (sub) => sub !== callback
489
+ )
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Subscribes to changes in the agent's initialization state.
495
+ * @param callback - Function called with the updated agent state.
496
+ * @returns An unsubscribe function.
497
+ */
498
+ public subscribeAgentState(callback: (state: AgentState) => void) {
499
+ this.#agentStateSubscribers.push(callback)
500
+ return () => {
501
+ this.#agentStateSubscribers = this.#agentStateSubscribers.filter(
502
+ (sub) => sub !== callback
503
+ )
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Subscribes to changes in the authentication state.
509
+ * @param callback - Function called with the updated authentication state.
510
+ * @returns An unsubscribe function.
511
+ */
512
+ public subscribeAuthState(callback: (state: AuthState) => void) {
513
+ this.#authStateSubscribers.push(callback)
514
+ return () => {
515
+ this.#authStateSubscribers = this.#authStateSubscribers.filter(
516
+ (sub) => sub !== callback
517
+ )
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Replaces the current agent's identity and invalidates TanStack queries.
523
+ * @param identity - The new identity to use.
524
+ */
525
+ public updateAgent(identity: Identity) {
526
+ if (typeof window !== "undefined") {
527
+ console.info(
528
+ `%cic-reactor:%c Updating agent identity`,
529
+ "color: #3b82f6; font-weight: bold",
530
+ "color: inherit",
531
+ {
532
+ principal: identity.getPrincipal().toText(),
533
+ }
534
+ )
535
+ }
536
+ // Cancel all queries for connected canisters to prevent race conditions
537
+ // with the old identity
538
+ this.connectedCanisterIds().forEach((canisterId) => {
539
+ this.queryClient.cancelQueries({
540
+ queryKey: [canisterId],
541
+ })
542
+ })
543
+
544
+ this.#agent.replaceIdentity(identity)
545
+ this.notifySubscribers(identity)
546
+ this.queryClient.invalidateQueries()
547
+ }
548
+
549
+ private notifySubscribers(identity: Identity) {
550
+ this.#identitySubscribers.forEach((sub) => sub(identity))
551
+ }
552
+
553
+ private notifyAgentStateSubscribers(state: AgentState) {
554
+ this.#agentStateSubscribers.forEach((sub) => sub(state))
555
+ }
556
+
557
+ private notifyAuthStateSubscribers(state: AuthState) {
558
+ this.#authStateSubscribers.forEach((sub) => sub(state))
559
+ }
560
+
561
+ private updateAgentState(newState: Partial<AgentState>) {
562
+ this.agentState = { ...this.agentState, ...newState }
563
+ this.notifyAgentStateSubscribers(this.agentState)
564
+ }
565
+
566
+ private updateAuthState(newState: Partial<AuthState>) {
567
+ console.debug("Updating Auth State:", newState)
568
+ this.authState = { ...this.authState, ...newState }
569
+ this.notifyAuthStateSubscribers(this.authState)
570
+ }
571
+ }
@@ -0,0 +1,92 @@
1
+ import { IDL } from "@icp-sdk/core/candid"
2
+ import { ActorDisplayCodec, DisplayOf, DisplayCodec } from "./types"
3
+ import { DisplayCodecVisitor } from "./visitor"
4
+
5
+ export function didToDisplayCodec<
6
+ TCandid = unknown,
7
+ TDisplay = DisplayOf<TCandid>,
8
+ >(didType: IDL.Type): ActorDisplayCodec<TCandid, TDisplay> {
9
+ const visitor = new DisplayCodecVisitor()
10
+ const codec = visitor.visitType(didType, null) as DisplayCodec<
11
+ TCandid,
12
+ TDisplay
13
+ >
14
+ // Return a unified interface with both native methods and convenience aliases
15
+ return {
16
+ codec,
17
+ asDisplay: (val: TCandid) => codec.decode(val) as TDisplay,
18
+ asCandid: (val: TDisplay) => codec.encode(val) as TCandid,
19
+ }
20
+ }
21
+
22
+ export function didToDisplayCodecs<TTypes extends Record<string, IDL.Type>>(
23
+ didTypes: TTypes
24
+ ): {
25
+ [K in keyof TTypes]: TTypes[K] extends IDL.Type<infer TCandid>
26
+ ? ActorDisplayCodec<TCandid>
27
+ : never
28
+ } {
29
+ const result = {} as any
30
+
31
+ for (const [name, idlType] of Object.entries(didTypes)) {
32
+ result[name] = didToDisplayCodec(idlType)
33
+ }
34
+
35
+ return result
36
+ }
37
+
38
+ export function transformArgsWithCodec<T extends unknown[]>(
39
+ argsCodec: ActorDisplayCodec,
40
+ args: unknown[] | undefined
41
+ ): T {
42
+ // Empty args - no transformation needed
43
+ // This handles methods with 0 arguments where codec is IDL.Null
44
+ if (!args || args.length === 0) {
45
+ return (args || []) as T
46
+ }
47
+
48
+ // Single argument - unwrap from array and transform
49
+ if (args.length === 1) {
50
+ try {
51
+ return [argsCodec.asCandid(args[0])] as T
52
+ } catch {
53
+ // Fallback: return as-is if transformation fails
54
+ return args as T
55
+ }
56
+ }
57
+
58
+ // Multiple arguments - transform as tuple
59
+ try {
60
+ return argsCodec.asCandid(args) as T
61
+ } catch {
62
+ // Fallback: return as-is if transformation fails
63
+ return args as T
64
+ }
65
+ }
66
+
67
+ export function transformResultWithCodec<T>(
68
+ resultCodec: ActorDisplayCodec,
69
+ result: unknown
70
+ ): T {
71
+ // Null/undefined results - no transformation needed
72
+ if (result === null || result === undefined) {
73
+ return result as T
74
+ }
75
+
76
+ try {
77
+ return resultCodec.asDisplay(result) as T
78
+ } catch {
79
+ // Fallback: return as-is if transformation fails
80
+ return result as T
81
+ }
82
+ }
83
+
84
+ export function didTypeFromArray(types: IDL.Type[]): IDL.Type {
85
+ if (types.length === 0) {
86
+ return IDL.Null
87
+ }
88
+ if (types.length === 1) {
89
+ return types[0]
90
+ }
91
+ return IDL.Tuple(...types)
92
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./visitor"
2
+ export * from "./helper"
3
+ export * from "./types"