@ic-reactor/core 3.0.3-beta.5 → 3.0.3
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 +6 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -3
- package/dist/client.js.map +1 -1
- package/dist/display/types.d.ts +4 -2
- package/dist/display/types.d.ts.map +1 -1
- package/dist/display/visitor.d.ts +2 -1
- package/dist/display/visitor.d.ts.map +1 -1
- package/dist/display/visitor.js +146 -121
- package/dist/display/visitor.js.map +1 -1
- package/dist/display-reactor.d.ts +9 -41
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +5 -41
- package/dist/display-reactor.js.map +1 -1
- package/dist/reactor.d.ts +17 -1
- package/dist/reactor.d.ts.map +1 -1
- package/dist/reactor.js +60 -44
- package/dist/reactor.js.map +1 -1
- package/dist/types/display-reactor.d.ts +2 -2
- package/dist/types/display-reactor.d.ts.map +1 -1
- package/dist/types/reactor.d.ts +7 -7
- package/dist/types/reactor.d.ts.map +1 -1
- package/dist/types/transform.d.ts +1 -1
- package/dist/types/transform.d.ts.map +1 -1
- package/dist/utils/helper.d.ts +20 -1
- package/dist/utils/helper.d.ts.map +1 -1
- package/dist/utils/helper.js +37 -6
- package/dist/utils/helper.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/zod.d.ts +34 -0
- package/dist/utils/zod.d.ts.map +1 -0
- package/dist/utils/zod.js +39 -0
- package/dist/utils/zod.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/src/client.ts +571 -0
- package/src/display/helper.ts +92 -0
- package/src/display/index.ts +3 -0
- package/src/display/types.ts +91 -0
- package/src/display/visitor.ts +415 -0
- package/src/display-reactor.ts +361 -0
- package/src/errors/index.ts +246 -0
- package/src/index.ts +8 -0
- package/src/reactor.ts +461 -0
- package/src/types/client.ts +110 -0
- package/src/types/display-reactor.ts +73 -0
- package/src/types/index.ts +6 -0
- package/src/types/reactor.ts +188 -0
- package/src/types/result.ts +50 -0
- package/src/types/transform.ts +29 -0
- package/src/types/variant.ts +39 -0
- package/src/utils/agent.ts +201 -0
- package/src/utils/candid.ts +112 -0
- package/src/utils/constants.ts +12 -0
- package/src/utils/helper.ts +155 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/polling.ts +330 -0
- package/src/utils/zod.ts +56 -0
- package/src/version.ts +5 -0
package/src/reactor.ts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallConfig,
|
|
3
|
+
PollingOptions,
|
|
4
|
+
ReadStateOptions,
|
|
5
|
+
} from "@icp-sdk/core/agent"
|
|
6
|
+
import type { ClientManager } from "./client"
|
|
7
|
+
import type { QueryKey, FetchQueryOptions } from "@tanstack/query-core"
|
|
8
|
+
import type {
|
|
9
|
+
ReactorParameters,
|
|
10
|
+
BaseActor,
|
|
11
|
+
ActorMethodParameters,
|
|
12
|
+
ActorMethodReturnType,
|
|
13
|
+
FunctionName,
|
|
14
|
+
TransformKey,
|
|
15
|
+
ReactorArgs,
|
|
16
|
+
ReactorReturnOk,
|
|
17
|
+
ReactorQueryParams,
|
|
18
|
+
ReactorCallParams,
|
|
19
|
+
CanisterId,
|
|
20
|
+
} from "./types/reactor"
|
|
21
|
+
|
|
22
|
+
import { DEFAULT_POLLING_OPTIONS } from "@icp-sdk/core/agent"
|
|
23
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
24
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
25
|
+
import { generateKey, extractOkResult } from "./utils/helper"
|
|
26
|
+
import {
|
|
27
|
+
processQueryCallResponse,
|
|
28
|
+
processUpdateCallResponse,
|
|
29
|
+
} from "./utils/agent"
|
|
30
|
+
import { CallError, CanisterError, ValidationError } from "./errors"
|
|
31
|
+
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reactor class for interacting with IC canisters.
|
|
35
|
+
*
|
|
36
|
+
* This class provides core functionality for:
|
|
37
|
+
* - Direct agent calls using agent.call() and agent.query()
|
|
38
|
+
* - Query caching with TanStack Query integration
|
|
39
|
+
* - Method calls with result unwrapping
|
|
40
|
+
*
|
|
41
|
+
* @typeParam A - The actor service type
|
|
42
|
+
* @typeParam T - The type transformation to apply (default: candid = raw Candid types)
|
|
43
|
+
*/
|
|
44
|
+
export class Reactor<A = BaseActor, T extends TransformKey = "candid"> {
|
|
45
|
+
/** Phantom type brand for inference - never assigned at runtime */
|
|
46
|
+
declare readonly _actor: A
|
|
47
|
+
public readonly transform: TransformKey = "candid"
|
|
48
|
+
public clientManager: ClientManager
|
|
49
|
+
public name: string
|
|
50
|
+
public canisterId: Principal
|
|
51
|
+
public service: IDL.ServiceClass
|
|
52
|
+
public pollingOptions: PollingOptions
|
|
53
|
+
|
|
54
|
+
constructor(config: ReactorParameters) {
|
|
55
|
+
this.clientManager = config.clientManager
|
|
56
|
+
this.name = config.name
|
|
57
|
+
this.pollingOptions =
|
|
58
|
+
"pollingOptions" in config && config.pollingOptions
|
|
59
|
+
? config.pollingOptions
|
|
60
|
+
: DEFAULT_POLLING_OPTIONS
|
|
61
|
+
|
|
62
|
+
const { idlFactory } = config
|
|
63
|
+
if (!idlFactory) {
|
|
64
|
+
throw new Error(`[ic-reactor] idlFactory is missing for ${this.name}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let canisterId = config.canisterId
|
|
68
|
+
|
|
69
|
+
if (!canisterId) {
|
|
70
|
+
const env = safeGetCanisterEnv()
|
|
71
|
+
const key = `PUBLIC_CANISTER_ID:${this.name}`
|
|
72
|
+
canisterId = env?.[key]
|
|
73
|
+
|
|
74
|
+
if (!canisterId) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`[ic-reactor] ${this.name} canister ID not found in ic_env cookie`
|
|
77
|
+
)
|
|
78
|
+
canisterId = "aaaaa-aa" // Fallback
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.canisterId = Principal.from(canisterId)
|
|
83
|
+
this.service = idlFactory({ IDL })
|
|
84
|
+
|
|
85
|
+
// Register this canister ID for delegation during login
|
|
86
|
+
this.clientManager.registerCanisterId(this.canisterId.toString(), this.name)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set the canister ID for this reactor.
|
|
91
|
+
* Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).
|
|
92
|
+
*
|
|
93
|
+
* @param canisterId - The new canister ID (as string or Principal)
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* // Switch to a different ledger canister
|
|
98
|
+
* ledgerReactor.setCanisterId("ryjl3-tyaaa-aaaaa-aaaba-cai")
|
|
99
|
+
*
|
|
100
|
+
* // Then use queries/mutations as normal
|
|
101
|
+
* const { data } = icrc1NameQuery.useQuery()
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
public setCanisterId(canisterId: CanisterId): void {
|
|
105
|
+
this.canisterId = Principal.from(canisterId)
|
|
106
|
+
// Register the new canister ID for delegation
|
|
107
|
+
this.clientManager.registerCanisterId(this.canisterId.toString(), this.name)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set the canister name for this reactor.
|
|
112
|
+
* Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).
|
|
113
|
+
*
|
|
114
|
+
* @param name - The new canister name
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* // Switch to a different ledger canister
|
|
119
|
+
* ledgerReactor.setCanisterName("icrc1")
|
|
120
|
+
*
|
|
121
|
+
* // Then use queries/mutations as normal
|
|
122
|
+
* const { data } = icrc1NameQuery.useQuery()
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
public setCanisterName(name: string): void {
|
|
126
|
+
this.name = name
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected verifyCanister() {
|
|
130
|
+
// Optional: add any verification logic here
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the service interface (IDL.ServiceClass) for this reactor.
|
|
135
|
+
* Useful for introspection and codec generation.
|
|
136
|
+
* @returns The service interface
|
|
137
|
+
*/
|
|
138
|
+
public getServiceInterface(): IDL.ServiceClass {
|
|
139
|
+
return this.service
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the function class for a specific method.
|
|
144
|
+
* @param methodName - The name of the method
|
|
145
|
+
* @returns The function class or null if not found
|
|
146
|
+
*/
|
|
147
|
+
protected getFuncClass<M extends FunctionName<A>>(
|
|
148
|
+
methodName: M
|
|
149
|
+
): IDL.FuncClass | null {
|
|
150
|
+
const field = this.service._fields.find(([name]) => name === methodName)
|
|
151
|
+
return field ? field[1] : null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a method is a query method (query or composite_query).
|
|
156
|
+
*/
|
|
157
|
+
public isQueryMethod<M extends FunctionName<A>>(methodName: M): boolean {
|
|
158
|
+
const func = this.getFuncClass(methodName)
|
|
159
|
+
if (!func) return false
|
|
160
|
+
return (
|
|
161
|
+
func.annotations.includes("query") ||
|
|
162
|
+
func.annotations.includes("composite_query")
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
167
|
+
// TRANSFORMATION METHODS
|
|
168
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Transform arguments before calling the method.
|
|
172
|
+
* Default implementation returns arguments as-is.
|
|
173
|
+
*/
|
|
174
|
+
protected transformArgs<M extends FunctionName<A>>(
|
|
175
|
+
_methodName: M,
|
|
176
|
+
args?: ReactorArgs<A, M, T>
|
|
177
|
+
): ActorMethodParameters<A[M]> {
|
|
178
|
+
if (!args) {
|
|
179
|
+
return [] as unknown as ActorMethodParameters<A[M]>
|
|
180
|
+
}
|
|
181
|
+
return args as ActorMethodParameters<A[M]>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Transform the result after calling the method.
|
|
186
|
+
* Default implementation extracts Ok value from Result types.
|
|
187
|
+
*/
|
|
188
|
+
protected transformResult<M extends FunctionName<A>>(
|
|
189
|
+
_methodName: M,
|
|
190
|
+
result: ActorMethodReturnType<A[M]>
|
|
191
|
+
): ReactorReturnOk<A, M, T> {
|
|
192
|
+
return extractOkResult(result) as ReactorReturnOk<A, M, T>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
196
|
+
// QUERY KEY GENERATION
|
|
197
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
198
|
+
|
|
199
|
+
public generateQueryKey<M extends FunctionName<A>>(
|
|
200
|
+
params: ReactorQueryParams<A, M, T>
|
|
201
|
+
): QueryKey {
|
|
202
|
+
const queryKeys: any[] = [this.canisterId.toString(), params.functionName]
|
|
203
|
+
|
|
204
|
+
if (params.args) {
|
|
205
|
+
const argKey = generateKey(params.args)
|
|
206
|
+
queryKeys.push(argKey)
|
|
207
|
+
}
|
|
208
|
+
if (params.queryKey) {
|
|
209
|
+
queryKeys.push(...params.queryKey)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return queryKeys
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
216
|
+
// QUERY OPTIONS
|
|
217
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
218
|
+
|
|
219
|
+
public getQueryOptions<M extends FunctionName<A>>(
|
|
220
|
+
params: ReactorCallParams<A, M, T>
|
|
221
|
+
): FetchQueryOptions<ReactorReturnOk<A, M, T>> {
|
|
222
|
+
return {
|
|
223
|
+
queryKey: this.generateQueryKey(params),
|
|
224
|
+
queryFn: () => this.callMethod(params),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Invalidate cached queries for this canister.
|
|
230
|
+
* This will mark matching queries as stale and trigger a refetch for any active queries.
|
|
231
|
+
*
|
|
232
|
+
* @param params - Optional parameters to filter the invalidation
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* // Invalidate all queries for this canister
|
|
237
|
+
* reactor.invalidateQueries()
|
|
238
|
+
*
|
|
239
|
+
* // Invalidate only 'getUser' queries
|
|
240
|
+
* reactor.invalidateQueries({ functionName: 'getUser' })
|
|
241
|
+
*
|
|
242
|
+
* // Invalidate 'getUser' query for specific user
|
|
243
|
+
* reactor.invalidateQueries({ functionName: 'getUser', args: ['user-1'] })
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
public invalidateQueries<M extends FunctionName<A>>(
|
|
247
|
+
params?: Partial<ReactorQueryParams<A, M, T>>
|
|
248
|
+
) {
|
|
249
|
+
const queryKey = params
|
|
250
|
+
? this.generateQueryKey({
|
|
251
|
+
functionName: params.functionName as M,
|
|
252
|
+
args: params.args,
|
|
253
|
+
queryKey: params.queryKey,
|
|
254
|
+
})
|
|
255
|
+
: [this.canisterId.toString()]
|
|
256
|
+
|
|
257
|
+
this.queryClient.invalidateQueries({
|
|
258
|
+
queryKey,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
263
|
+
// METHOD CALLS - Using agent.call() and agent.query() directly
|
|
264
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Call a canister method directly using agent.call() or agent.query().
|
|
268
|
+
* This is the recommended approach for interacting with canisters.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* // Query method
|
|
273
|
+
* const result = await reactor.callMethod({
|
|
274
|
+
* functionName: 'greet',
|
|
275
|
+
* args: ['world'],
|
|
276
|
+
* });
|
|
277
|
+
*
|
|
278
|
+
* // Update method with options
|
|
279
|
+
* const result = await reactor.callMethod({
|
|
280
|
+
* functionName: 'transfer',
|
|
281
|
+
* args: [{ to: principal, amount: 100n }],
|
|
282
|
+
* callConfig: { effectiveCanisterId: principal },
|
|
283
|
+
* });
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
public async callMethod<M extends FunctionName<A>>(
|
|
287
|
+
params: Omit<ReactorCallParams<A, M, T>, "queryKey">
|
|
288
|
+
): Promise<ReactorReturnOk<A, M, T>> {
|
|
289
|
+
try {
|
|
290
|
+
const func = this.getFuncClass(params.functionName)
|
|
291
|
+
if (!func) {
|
|
292
|
+
throw new Error(`Method ${String(params.functionName)} not found`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Transform args
|
|
296
|
+
const transformedArgs = this.transformArgs(
|
|
297
|
+
params.functionName,
|
|
298
|
+
params.args
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Encode arguments using Candid
|
|
302
|
+
const arg = IDL.encode(func.argTypes, transformedArgs)
|
|
303
|
+
|
|
304
|
+
// Determine if this is a query or update call
|
|
305
|
+
const isQuery =
|
|
306
|
+
func.annotations.includes("query") ||
|
|
307
|
+
func.annotations.includes("composite_query")
|
|
308
|
+
|
|
309
|
+
// Execute the call
|
|
310
|
+
let rawResponse: Uint8Array
|
|
311
|
+
if (isQuery) {
|
|
312
|
+
rawResponse = await this.executeQuery(
|
|
313
|
+
String(params.functionName),
|
|
314
|
+
arg,
|
|
315
|
+
params.callConfig
|
|
316
|
+
)
|
|
317
|
+
} else {
|
|
318
|
+
rawResponse = await this.executeCall(
|
|
319
|
+
String(params.functionName),
|
|
320
|
+
arg,
|
|
321
|
+
params.callConfig
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Decode the result
|
|
326
|
+
const decoded = IDL.decode(func.retTypes, rawResponse)
|
|
327
|
+
|
|
328
|
+
// Handle single, zero, and multiple return values appropriately
|
|
329
|
+
const response = (
|
|
330
|
+
decoded.length === 0
|
|
331
|
+
? undefined
|
|
332
|
+
: decoded.length === 1
|
|
333
|
+
? decoded[0]
|
|
334
|
+
: decoded
|
|
335
|
+
) as ActorMethodReturnType<A[M]>
|
|
336
|
+
|
|
337
|
+
return this.transformResult(params.functionName, response)
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Re-throw CanisterError as-is (business logic error from canister)
|
|
340
|
+
if (error instanceof CanisterError || error instanceof ValidationError) {
|
|
341
|
+
throw error
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const message = `Failed to call method "${String(params.functionName)}": `
|
|
345
|
+
|
|
346
|
+
// Wrap other errors in CallError (network/agent issues)
|
|
347
|
+
if (error instanceof Error) {
|
|
348
|
+
throw new CallError(message + error.message, error)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
throw new CallError(message + String(error), error)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Fetch data from the canister and cache it using React Query.
|
|
357
|
+
* This method ensures the data is in the cache and returns it.
|
|
358
|
+
*/
|
|
359
|
+
public async fetchQuery<M extends FunctionName<A>>(
|
|
360
|
+
params: ReactorCallParams<A, M, T>
|
|
361
|
+
): Promise<ReactorReturnOk<A, M, T>> {
|
|
362
|
+
const options = this.getQueryOptions(params)
|
|
363
|
+
return this.queryClient.ensureQueryData<ReactorReturnOk<A, M, T>>(options)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get the current data from the cache without fetching.
|
|
368
|
+
*/
|
|
369
|
+
public getQueryData<M extends FunctionName<A>>(
|
|
370
|
+
params: ReactorQueryParams<A, M, T>
|
|
371
|
+
): ReactorReturnOk<A, M, T> | undefined {
|
|
372
|
+
const queryKey = this.generateQueryKey(params)
|
|
373
|
+
return this.queryClient.getQueryData<ReactorReturnOk<A, M, T>>(queryKey)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Execute a query call using agent.query()
|
|
378
|
+
*/
|
|
379
|
+
protected async executeQuery(
|
|
380
|
+
methodName: string,
|
|
381
|
+
arg: Uint8Array,
|
|
382
|
+
callConfig?: CallConfig
|
|
383
|
+
): Promise<Uint8Array> {
|
|
384
|
+
const agent = this.clientManager.agent
|
|
385
|
+
const effectiveCanisterId =
|
|
386
|
+
callConfig?.effectiveCanisterId ?? this.canisterId
|
|
387
|
+
|
|
388
|
+
const response = await agent.query(this.canisterId, {
|
|
389
|
+
methodName,
|
|
390
|
+
arg,
|
|
391
|
+
effectiveCanisterId,
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
return processQueryCallResponse(response, this.canisterId, methodName)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Execute an update call using agent.call()
|
|
399
|
+
*/
|
|
400
|
+
protected async executeCall(
|
|
401
|
+
methodName: string,
|
|
402
|
+
arg: Uint8Array,
|
|
403
|
+
callConfig?: CallConfig
|
|
404
|
+
): Promise<Uint8Array> {
|
|
405
|
+
const agent = this.clientManager.agent
|
|
406
|
+
|
|
407
|
+
const response = await agent.call(this.canisterId, {
|
|
408
|
+
methodName,
|
|
409
|
+
arg,
|
|
410
|
+
effectiveCanisterId: callConfig?.effectiveCanisterId,
|
|
411
|
+
nonce: callConfig?.nonce,
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
return await processUpdateCallResponse(
|
|
415
|
+
response,
|
|
416
|
+
this.canisterId,
|
|
417
|
+
methodName,
|
|
418
|
+
agent,
|
|
419
|
+
this.pollingOptions
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
424
|
+
// SUBNET
|
|
425
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the subnet ID for this canister.
|
|
429
|
+
*/
|
|
430
|
+
public async subnetId() {
|
|
431
|
+
return this.clientManager.agent.getSubnetIdFromCanister(this.canisterId)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get the subnet state for this canister.
|
|
436
|
+
*/
|
|
437
|
+
public async subnetState(options: ReadStateOptions) {
|
|
438
|
+
const subnetId = await this.subnetId()
|
|
439
|
+
return this.clientManager.agent.readSubnetState(subnetId, options)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
443
|
+
// GETTERS
|
|
444
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get the query client from clientManager.
|
|
448
|
+
* This is the recommended way to access the query client for direct queries.
|
|
449
|
+
*/
|
|
450
|
+
get queryClient() {
|
|
451
|
+
return this.clientManager.queryClient
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get the agent from clientManager.
|
|
456
|
+
* This is the recommended way to access the agent for direct calls.
|
|
457
|
+
*/
|
|
458
|
+
get agent() {
|
|
459
|
+
return this.clientManager.agent
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { HttpAgent, HttpAgentOptions, Identity } from "@icp-sdk/core/agent"
|
|
2
|
+
import type { AuthClient } from "@icp-sdk/auth/client"
|
|
3
|
+
import type { QueryClient } from "@tanstack/query-core"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parameters for configuring a ClientManager instance.
|
|
7
|
+
*
|
|
8
|
+
* @property {QueryClient} queryClient - The TanStack QueryClient used for caching and state management.
|
|
9
|
+
* @property {number} [port] - The port used for the local IC replica (default is 4943).
|
|
10
|
+
* @property {HttpAgentOptions} [agentOptions] - Optional configuration for the underlying HttpAgent.
|
|
11
|
+
* @property {boolean} [withLocalEnv] - If true, configures the agent for a local environment.
|
|
12
|
+
* @property {boolean} [withProcessEnv] - If true, auto-configures the agent based on process.env settings.
|
|
13
|
+
*/
|
|
14
|
+
export interface ClientManagerParameters {
|
|
15
|
+
/**
|
|
16
|
+
* The TanStack QueryClient used for caching and state management.
|
|
17
|
+
*/
|
|
18
|
+
queryClient: QueryClient
|
|
19
|
+
/**
|
|
20
|
+
* Optional configuration for the underlying HttpAgent.
|
|
21
|
+
*/
|
|
22
|
+
agentOptions?: HttpAgentOptions
|
|
23
|
+
/**
|
|
24
|
+
* The port used for the local IC replica (default is 4943).
|
|
25
|
+
*/
|
|
26
|
+
port?: number
|
|
27
|
+
/**
|
|
28
|
+
* If true, configures the agent for a local environment.
|
|
29
|
+
*/
|
|
30
|
+
withLocalEnv?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* If true, auto-configures the agent based on process.env settings.
|
|
33
|
+
*/
|
|
34
|
+
withProcessEnv?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Optional pre-initialized AuthClient instance.
|
|
37
|
+
* If provided, the manager will use this instance instead of dynamically importing
|
|
38
|
+
* and creating a new one from `@icp-sdk/auth`.
|
|
39
|
+
* This is useful for environments where dynamic imports are not supported or
|
|
40
|
+
* when you want to share an AuthClient instance across multiple managers.
|
|
41
|
+
*/
|
|
42
|
+
authClient?: AuthClient
|
|
43
|
+
/**
|
|
44
|
+
* **EXPERIMENTAL** - If true, uses the canister environment from `@icp-sdk/core/agent/canister-env`
|
|
45
|
+
* to automatically configure the agent host and root key based on the `ic_env` cookie.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ This feature is experimental and may cause issues with update calls on localhost development.
|
|
48
|
+
* Use with caution and only when you need automatic environment detection from the IC SDK.
|
|
49
|
+
*
|
|
50
|
+
* @experimental
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
withCanisterEnv?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Represents the state of an agent.
|
|
58
|
+
*/
|
|
59
|
+
export interface AgentState {
|
|
60
|
+
/**
|
|
61
|
+
* Indicates whether the agent has been initialized.
|
|
62
|
+
*/
|
|
63
|
+
isInitialized: boolean
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Indicates whether the agent is in the process of initializing.
|
|
67
|
+
*/
|
|
68
|
+
isInitializing: boolean
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Represents an error associated with the agent, if any.
|
|
72
|
+
*/
|
|
73
|
+
error: Error | undefined
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Represents the network associated with the agent, if any.
|
|
77
|
+
*/
|
|
78
|
+
network: string | undefined
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Indicates whether the agent is connected to a local network.
|
|
82
|
+
*/
|
|
83
|
+
isLocalhost: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Represents the authentication state of an agent.
|
|
88
|
+
*/
|
|
89
|
+
export interface AuthState {
|
|
90
|
+
identity: Identity | null
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Indicates whether the authentication process is ongoing.
|
|
94
|
+
*/
|
|
95
|
+
isAuthenticating: boolean
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Indicates whether the agent is authenticated.
|
|
99
|
+
*/
|
|
100
|
+
isAuthenticated: boolean
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Represents any error that occurred during authentication.
|
|
104
|
+
*/
|
|
105
|
+
error: Error | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface UpdateAgentParameters extends HttpAgentOptions {
|
|
109
|
+
agent?: HttpAgent
|
|
110
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Validation Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { ValidationIssue } from "../errors"
|
|
6
|
+
import {
|
|
7
|
+
BaseActor,
|
|
8
|
+
FunctionName,
|
|
9
|
+
ReactorArgs,
|
|
10
|
+
ReactorParameters,
|
|
11
|
+
} from "./reactor"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validation result returned by a validator function.
|
|
15
|
+
* Either success (true) or failure with issues.
|
|
16
|
+
*/
|
|
17
|
+
export type ValidationResult =
|
|
18
|
+
| { success: true }
|
|
19
|
+
| { success: false; issues: ValidationIssue[] }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A validator function that validates method arguments.
|
|
23
|
+
* Receives display types (strings for Principal, bigint, etc.).
|
|
24
|
+
*
|
|
25
|
+
* @param args - The display-type arguments to validate
|
|
26
|
+
* @returns ValidationResult indicating success or failure with issues
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // Validator receives display types
|
|
31
|
+
* reactor.registerValidator("transfer", ([input]) => {
|
|
32
|
+
* const issues = []
|
|
33
|
+
*
|
|
34
|
+
* // input.to is string (not Principal)
|
|
35
|
+
* if (!input.to) {
|
|
36
|
+
* issues.push({ path: ["to"], message: "Recipient is required" })
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // input.amount is string (not bigint)
|
|
40
|
+
* if (!/^\d+$/.test(input.amount)) {
|
|
41
|
+
* issues.push({ path: ["amount"], message: "Must be a valid number" })
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* return issues.length > 0 ? { success: false, issues } : { success: true }
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export type Validator<Args = unknown[]> = (
|
|
49
|
+
args: Args
|
|
50
|
+
) => ValidationResult | Promise<ValidationResult>
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validator that receives display types for a specific method.
|
|
54
|
+
*/
|
|
55
|
+
export type DisplayValidator<A, M extends FunctionName<A>> = Validator<
|
|
56
|
+
ReactorArgs<A, M, "display">
|
|
57
|
+
>
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// DisplayReactor Parameters
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export interface DisplayReactorParameters<
|
|
64
|
+
A = BaseActor,
|
|
65
|
+
> extends ReactorParameters {
|
|
66
|
+
/**
|
|
67
|
+
* Optional initial validators to register.
|
|
68
|
+
* Validators receive display types (strings for Principal, bigint, etc.)
|
|
69
|
+
*/
|
|
70
|
+
validators?: Partial<{
|
|
71
|
+
[M in FunctionName<A>]: DisplayValidator<A, M>
|
|
72
|
+
}>
|
|
73
|
+
}
|