@ic-reactor/candid 3.0.2-beta.0 → 3.0.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.
Files changed (83) hide show
  1. package/README.md +33 -1
  2. package/dist/adapter.js +2 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/display-reactor.d.ts +4 -13
  5. package/dist/display-reactor.d.ts.map +1 -1
  6. package/dist/display-reactor.js +22 -8
  7. package/dist/display-reactor.js.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/metadata-display-reactor.d.ts +108 -0
  13. package/dist/metadata-display-reactor.d.ts.map +1 -0
  14. package/dist/metadata-display-reactor.js +141 -0
  15. package/dist/metadata-display-reactor.js.map +1 -0
  16. package/dist/reactor.d.ts +1 -1
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +10 -6
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types.d.ts +38 -7
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils.d.ts +4 -4
  23. package/dist/utils.d.ts.map +1 -1
  24. package/dist/utils.js +33 -10
  25. package/dist/utils.js.map +1 -1
  26. package/dist/visitor/arguments/helpers.d.ts +55 -0
  27. package/dist/visitor/arguments/helpers.d.ts.map +1 -0
  28. package/dist/visitor/arguments/helpers.js +123 -0
  29. package/dist/visitor/arguments/helpers.js.map +1 -0
  30. package/dist/visitor/arguments/index.d.ts +101 -0
  31. package/dist/visitor/arguments/index.d.ts.map +1 -0
  32. package/dist/visitor/arguments/index.js +780 -0
  33. package/dist/visitor/arguments/index.js.map +1 -0
  34. package/dist/visitor/arguments/types.d.ts +270 -0
  35. package/dist/visitor/arguments/types.d.ts.map +1 -0
  36. package/dist/visitor/arguments/types.js +26 -0
  37. package/dist/visitor/arguments/types.js.map +1 -0
  38. package/dist/visitor/constants.d.ts +4 -0
  39. package/dist/visitor/constants.d.ts.map +1 -0
  40. package/dist/visitor/constants.js +73 -0
  41. package/dist/visitor/constants.js.map +1 -0
  42. package/dist/visitor/helpers.d.ts +30 -0
  43. package/dist/visitor/helpers.d.ts.map +1 -0
  44. package/dist/visitor/helpers.js +204 -0
  45. package/dist/visitor/helpers.js.map +1 -0
  46. package/dist/visitor/index.d.ts +5 -0
  47. package/dist/visitor/index.d.ts.map +1 -0
  48. package/dist/visitor/index.js +5 -0
  49. package/dist/visitor/index.js.map +1 -0
  50. package/dist/visitor/returns/index.d.ts +38 -0
  51. package/dist/visitor/returns/index.d.ts.map +1 -0
  52. package/dist/visitor/returns/index.js +460 -0
  53. package/dist/visitor/returns/index.js.map +1 -0
  54. package/dist/visitor/returns/types.d.ts +202 -0
  55. package/dist/visitor/returns/types.d.ts.map +1 -0
  56. package/dist/visitor/returns/types.js +2 -0
  57. package/dist/visitor/returns/types.js.map +1 -0
  58. package/dist/visitor/types.d.ts +19 -0
  59. package/dist/visitor/types.d.ts.map +1 -0
  60. package/dist/visitor/types.js +2 -0
  61. package/dist/visitor/types.js.map +1 -0
  62. package/package.json +16 -7
  63. package/src/adapter.ts +446 -0
  64. package/src/constants.ts +11 -0
  65. package/src/display-reactor.ts +337 -0
  66. package/src/index.ts +8 -0
  67. package/src/metadata-display-reactor.ts +230 -0
  68. package/src/reactor.ts +199 -0
  69. package/src/types.ts +127 -0
  70. package/src/utils.ts +60 -0
  71. package/src/visitor/arguments/helpers.ts +153 -0
  72. package/src/visitor/arguments/index.test.ts +1439 -0
  73. package/src/visitor/arguments/index.ts +981 -0
  74. package/src/visitor/arguments/schema.test.ts +324 -0
  75. package/src/visitor/arguments/types.ts +387 -0
  76. package/src/visitor/constants.ts +76 -0
  77. package/src/visitor/helpers.test.ts +274 -0
  78. package/src/visitor/helpers.ts +223 -0
  79. package/src/visitor/index.ts +4 -0
  80. package/src/visitor/returns/index.test.ts +2377 -0
  81. package/src/visitor/returns/index.ts +658 -0
  82. package/src/visitor/returns/types.ts +302 -0
  83. package/src/visitor/types.ts +75 -0
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,127 @@
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
+ * An IDL.FuncClass to register as a single-method service.
54
+ * Use this to create a reactor for a funcRecord callback.
55
+ * When provided, the reactor is ready to use immediately (no `initialize()` needed).
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const archiveReactor = new CandidDisplayReactor({
60
+ * canisterId: archived.canisterId,
61
+ * clientManager,
62
+ * funcClass: { methodName: "get_blocks", func: archiveFuncClass },
63
+ * })
64
+ * ```
65
+ */
66
+ funcClass?: {
67
+ /** The method name to register */
68
+ methodName: string
69
+ /** The IDL.FuncClass describing the function signature */
70
+ func: import("@icp-sdk/core/candid").IDL.FuncClass
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Minimal interface for ClientManager that CandidAdapter depends on.
76
+ * This allows the candid package to work with ClientManager without importing the full core package.
77
+ */
78
+ export interface CandidClientManager {
79
+ /** The HTTP agent used for making requests. */
80
+ agent: HttpAgent
81
+ /** Whether the agent is connected to a local network. */
82
+ isLocal: boolean
83
+ /** Subscribe to identity changes. Returns an unsubscribe function. */
84
+ subscribe(callback: (identity: Identity) => void): () => void
85
+ }
86
+
87
+ /**
88
+ * Parameters for initializing the CandidAdapter.
89
+ */
90
+ export interface CandidAdapterParameters {
91
+ /** The client manager that provides agent and identity access. */
92
+ clientManager: CandidClientManager
93
+ /** The canister ID of the didjs canister for compiling Candid to JavaScript. */
94
+ didjsCanisterId?: CanisterId
95
+ }
96
+
97
+ /**
98
+ * Represents a parsed Candid definition with IDL factory and initialization.
99
+ */
100
+ export interface CandidDefinition {
101
+ /** The IDL interface factory. */
102
+ idlFactory: IDL.InterfaceFactory
103
+ /** Optional init function for the canister. */
104
+ init?: (args: { IDL: typeof IDL }) => IDL.Type<unknown>[]
105
+ }
106
+
107
+ /**
108
+ * Interface for the optional parser module (@ic-reactor/parser).
109
+ */
110
+ export interface ReactorParser {
111
+ /**
112
+ * Default function to initialize the WASM module.
113
+ */
114
+ default?: () => Promise<void>
115
+ /**
116
+ * Converts Candid (DID) source to JavaScript code.
117
+ * @param candidSource - The Candid source code.
118
+ * @returns The JavaScript code.
119
+ */
120
+ didToJs(candidSource: string): string
121
+ /**
122
+ * Validates the Candid (IDL) source.
123
+ * @param candidSource - The Candid source code.
124
+ * @returns True if valid, false otherwise.
125
+ */
126
+ validateIDL(candidSource: string): boolean
127
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { CandidDefinition } from "./types"
2
+ import { IDL } from "@icp-sdk/core/candid"
3
+
4
+ /**
5
+ * Imports and evaluates a Candid definition from JavaScript code.
6
+ *
7
+ * This function evaluates JavaScript code in a controlled manner to extract
8
+ * the idlFactory and init exports. The evaluation is done using Function constructor
9
+ * which is safer than dynamic imports with data URLs and more CSP-friendly.
10
+ *
11
+ * @param candidJs - The JavaScript code containing the Candid definition.
12
+ * @returns A promise that resolves to the CandidDefinition.
13
+ * @throws Error if the import fails.
14
+ */
15
+ export async function importCandidDefinition(
16
+ candidJs: string
17
+ ): Promise<CandidDefinition> {
18
+ try {
19
+ // Create a module exports object
20
+ const exports: Record<string, unknown> = {}
21
+
22
+ // Transform ES6 export statements to assignments
23
+ // This is safe because we're only transforming the syntax pattern,
24
+ // not evaluating arbitrary code
25
+ const transformedJs = candidJs
26
+ // Replace 'export const name = value' with 'const name = value; exports.name = name'
27
+ .replace(/export\s+const\s+(\w+)\s*=/g, "const $1 =")
28
+ // Replace 'export function name' with 'function name'
29
+ .replace(/export\s+function\s+(\w+)/g, "function $1")
30
+
31
+ // Create a safe evaluation context with necessary globals
32
+ // We provide IDL from the trusted @icp-sdk/core/candid package
33
+ const evalFunction = new Function(
34
+ "exports",
35
+ "IDL",
36
+ `
37
+ ${transformedJs}
38
+
39
+ // Capture exports
40
+ if (typeof idlFactory !== 'undefined') {
41
+ exports.idlFactory = idlFactory;
42
+ }
43
+ if (typeof init !== 'undefined') {
44
+ exports.init = init;
45
+ }
46
+ return exports;
47
+ `
48
+ )
49
+
50
+ // Execute the function with the exports object and IDL
51
+ const result = evalFunction(exports, IDL)
52
+
53
+ return {
54
+ idlFactory: result.idlFactory as CandidDefinition["idlFactory"],
55
+ init: result.init as CandidDefinition["init"],
56
+ }
57
+ } catch (error) {
58
+ throw new Error(`Failed to import Candid definition: ${error}`)
59
+ }
60
+ }
@@ -0,0 +1,153 @@
1
+ import {
2
+ VisitorDataType,
3
+ CompoundField,
4
+ FieldNode,
5
+ FieldByType,
6
+ PrimitiveField,
7
+ RecordField,
8
+ TupleField,
9
+ VariantField,
10
+ } from "./types"
11
+
12
+ // ════════════════════════════════════════════════════════════════════════════
13
+ // Type Guards
14
+ // ════════════════════════════════════════════════════════════════════════════
15
+
16
+ /**
17
+ * Type guard for checking specific field types.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function FieldInput({ field }: { field: FieldNode }) {
22
+ * if (isFieldType(field, 'record')) {
23
+ * // field is now typed as RecordField
24
+ * return <RecordInput field={field} />
25
+ * }
26
+ * if (isFieldType(field, 'text')) {
27
+ * // field is now typed as TextField
28
+ * return <TextInput field={field} />
29
+ * }
30
+ * // ...
31
+ * }
32
+ * ```
33
+ */
34
+ export function isFieldType<T extends VisitorDataType>(
35
+ field: FieldNode,
36
+ type: T
37
+ ): field is FieldByType<T> {
38
+ return field.type === type
39
+ }
40
+
41
+ /**
42
+ * Check if a field is a compound type (contains other fields).
43
+ * Compound types: record, variant, tuple, optional, vector, recursive
44
+ */
45
+ export function isCompoundField(field: FieldNode): field is CompoundField {
46
+ return [
47
+ "record",
48
+ "variant",
49
+ "tuple",
50
+ "optional",
51
+ "vector",
52
+ "recursive",
53
+ ].includes(field.type)
54
+ }
55
+
56
+ /**
57
+ * Check if a field is a primitive type.
58
+ * Primitive types: principal, number, text, boolean, null
59
+ */
60
+ export function isPrimitiveField(field: FieldNode): field is PrimitiveField {
61
+ return ["principal", "number", "text", "boolean", "null"].includes(field.type)
62
+ }
63
+
64
+ /**
65
+ * Check if a field has child fields array (for iteration).
66
+ * Applies to: record (fields), tuple (fields)
67
+ */
68
+ export function hasChildFields(
69
+ field: FieldNode
70
+ ): field is RecordField | TupleField {
71
+ return (
72
+ (field.type === "record" || field.type === "tuple") &&
73
+ "fields" in field &&
74
+ Array.isArray((field as RecordField).fields)
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Check if a field has variant options (for iteration).
80
+ * Applies to: variant (options)
81
+ */
82
+ export function hasOptions(field: FieldNode): field is VariantField {
83
+ return (
84
+ field.type === "variant" &&
85
+ "options" in field &&
86
+ Array.isArray((field as VariantField).options)
87
+ )
88
+ }
89
+
90
+ // ════════════════════════════════════════════════════════════════════════════
91
+ // Label Formatting Utilities
92
+ // ════════════════════════════════════════════════════════════════════════════
93
+
94
+ /**
95
+ * Format a raw Candid label into a human-readable display label.
96
+ * Handles common patterns like "__arg0", "_0_", "snake_case", etc.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * formatLabel("__arg0") // "Arg 0"
101
+ * formatLabel("_0_") // "Item 0"
102
+ * formatLabel("created_at") // "Created At"
103
+ * formatLabel("userAddress") // "User Address"
104
+ * formatLabel("__ret0") // "Result 0"
105
+ * ```
106
+ */
107
+ export function formatLabel(label: string): string {
108
+ // Handle argument labels: __arg0 -> Arg 0
109
+ if (label.startsWith("__arg")) {
110
+ const num = label.slice(5)
111
+ return `Arg ${num}`
112
+ }
113
+
114
+ // Handle return labels: __ret0 -> Result 0
115
+ if (label.startsWith("__ret")) {
116
+ const num = label.slice(5)
117
+ return `Result ${num}`
118
+ }
119
+
120
+ // Handle tuple index labels: _0_ -> Item 0
121
+ if (/^_\d+_$/.test(label)) {
122
+ const num = label.slice(1, -1)
123
+ return `Item ${num}`
124
+ }
125
+
126
+ // Handle simple index labels: _0 -> Item 0
127
+ if (/^_\d+$/.test(label)) {
128
+ const num = label.slice(1)
129
+ return `Item ${num}`
130
+ }
131
+
132
+ // Handle item labels for vectors: label_item -> Item
133
+ if (label.endsWith("_item")) {
134
+ return "Item"
135
+ }
136
+
137
+ // Convert snake_case or just clean up underscores
138
+ // and capitalize each word
139
+ const trimmed = (() => {
140
+ let start = 0
141
+ let end = label.length
142
+ while (start < end && label[start] === "_") start++
143
+ while (end > start && label[end - 1] === "_") end--
144
+ return label.slice(start, end)
145
+ })()
146
+
147
+ return trimmed
148
+ .replace(/_/g, " ") // Replace underscores with spaces
149
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capitals (camelCase)
150
+ .split(" ")
151
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
152
+ .join(" ")
153
+ }