@ic-reactor/core 3.0.3-beta.4 → 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 +8 -9
- 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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { Reactor } from "./reactor"
|
|
2
|
+
import {
|
|
3
|
+
didToDisplayCodec,
|
|
4
|
+
transformArgsWithCodec,
|
|
5
|
+
transformResultWithCodec,
|
|
6
|
+
didTypeFromArray,
|
|
7
|
+
ActorDisplayCodec,
|
|
8
|
+
} from "./display"
|
|
9
|
+
import {
|
|
10
|
+
ActorMethodParameters,
|
|
11
|
+
ActorMethodReturnType,
|
|
12
|
+
FunctionName,
|
|
13
|
+
ReactorArgs,
|
|
14
|
+
ReactorReturnOk,
|
|
15
|
+
ActorMethodCodecs,
|
|
16
|
+
BaseActor,
|
|
17
|
+
TransformKey,
|
|
18
|
+
} from "./types/reactor"
|
|
19
|
+
import { extractOkResult } from "./utils/helper"
|
|
20
|
+
import { ValidationError } from "./errors"
|
|
21
|
+
import {
|
|
22
|
+
DisplayReactorParameters,
|
|
23
|
+
DisplayValidator,
|
|
24
|
+
ValidationResult,
|
|
25
|
+
Validator,
|
|
26
|
+
} from "./types/display-reactor"
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// DisplayReactor
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* DisplayReactor provides automatic type transformations between Candid and
|
|
34
|
+
* display-friendly types, plus optional argument validation.
|
|
35
|
+
*
|
|
36
|
+
* ### Type Transformations
|
|
37
|
+
* - `bigint` → `string` (for JSON/UI display)
|
|
38
|
+
* - `Principal` → `string` (text representation)
|
|
39
|
+
* - `[T] | []` → `T | null` (optional unwrapping)
|
|
40
|
+
* - Small blobs → hex strings
|
|
41
|
+
*
|
|
42
|
+
* ### Validation (Optional)
|
|
43
|
+
* Register validators to check arguments before canister calls.
|
|
44
|
+
* Validators receive **display types** (strings), making them perfect for
|
|
45
|
+
* form validation.
|
|
46
|
+
*
|
|
47
|
+
* @typeParam A - The actor service type
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { DisplayReactor } from "@ic-reactor/core"
|
|
52
|
+
*
|
|
53
|
+
* const reactor = new DisplayReactor<_SERVICE>({
|
|
54
|
+
* clientManager,
|
|
55
|
+
* canisterId: "...",
|
|
56
|
+
* idlFactory,
|
|
57
|
+
* })
|
|
58
|
+
*
|
|
59
|
+
* // Optional: Add validation
|
|
60
|
+
* reactor.registerValidator("transfer", ([input]) => {
|
|
61
|
+
* if (!input.to) {
|
|
62
|
+
* return {
|
|
63
|
+
* success: false,
|
|
64
|
+
* issues: [{ path: ["to"], message: "Recipient is required" }]
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* return { success: true }
|
|
68
|
+
* })
|
|
69
|
+
*
|
|
70
|
+
* // Call with display types
|
|
71
|
+
* await reactor.callMethod({
|
|
72
|
+
* functionName: "transfer",
|
|
73
|
+
* args: [{ to: "aaaaa-aa", amount: "100" }], // strings!
|
|
74
|
+
* })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export class DisplayReactor<
|
|
78
|
+
A = BaseActor,
|
|
79
|
+
T extends TransformKey = "display",
|
|
80
|
+
> extends Reactor<A, T> {
|
|
81
|
+
public readonly transform = "display" as T
|
|
82
|
+
private codecs: Map<
|
|
83
|
+
string,
|
|
84
|
+
{ args: ActorDisplayCodec; result: ActorDisplayCodec }
|
|
85
|
+
> = new Map()
|
|
86
|
+
private validators: Map<string, Validator<any>> = new Map()
|
|
87
|
+
|
|
88
|
+
constructor(config: DisplayReactorParameters<A>) {
|
|
89
|
+
super(config)
|
|
90
|
+
this.initializeCodecs()
|
|
91
|
+
|
|
92
|
+
// Register initial validators if provided
|
|
93
|
+
if (config.validators) {
|
|
94
|
+
for (const [methodName, validator] of Object.entries(config.validators)) {
|
|
95
|
+
if (validator) {
|
|
96
|
+
this.validators.set(methodName, validator as Validator)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Initialize codecs from IDL factory for automatic type transformations
|
|
104
|
+
*/
|
|
105
|
+
private initializeCodecs() {
|
|
106
|
+
try {
|
|
107
|
+
const fields = this.getServiceInterface()?._fields
|
|
108
|
+
if (!fields) {
|
|
109
|
+
throw new Error("No fields found")
|
|
110
|
+
}
|
|
111
|
+
for (const [methodName, funcType] of fields) {
|
|
112
|
+
// Generate args codec
|
|
113
|
+
const argsIdlType = didTypeFromArray(funcType.argTypes)
|
|
114
|
+
// Generate result codec
|
|
115
|
+
const retIdlType = didTypeFromArray(funcType.retTypes)
|
|
116
|
+
// Set codec in map
|
|
117
|
+
this.codecs.set(methodName, {
|
|
118
|
+
args: didToDisplayCodec(argsIdlType),
|
|
119
|
+
result: didToDisplayCodec(retIdlType),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error("Failed to initialize codecs:", error)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Codec Methods
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get a codec for a specific method.
|
|
133
|
+
* Returns the args and result codecs for bidirectional transformation.
|
|
134
|
+
* @param methodName - The name of the method
|
|
135
|
+
* @returns Object with args and result codecs, or null if not found
|
|
136
|
+
*/
|
|
137
|
+
public getCodec<M extends FunctionName<A>>(
|
|
138
|
+
methodName: M
|
|
139
|
+
): ActorMethodCodecs<A, M> | null {
|
|
140
|
+
const cached = this.codecs.get(methodName)
|
|
141
|
+
if (cached) {
|
|
142
|
+
return cached as ActorMethodCodecs<A, M>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Validation Methods
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Register a validator for a specific method.
|
|
154
|
+
* Validators receive display types (strings for Principal/bigint).
|
|
155
|
+
*
|
|
156
|
+
* @param methodName - The name of the method to validate
|
|
157
|
+
* @param validator - The validator function receiving display types
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* // input.to is string, input.amount is string
|
|
162
|
+
* reactor.registerValidator("transfer", ([input]) => {
|
|
163
|
+
* if (!/^\d+$/.test(input.amount)) {
|
|
164
|
+
* return {
|
|
165
|
+
* success: false,
|
|
166
|
+
* issues: [{ path: ["amount"], message: "Must be a valid number" }]
|
|
167
|
+
* }
|
|
168
|
+
* }
|
|
169
|
+
* return { success: true }
|
|
170
|
+
* })
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
registerValidator<M extends FunctionName<A>>(
|
|
174
|
+
methodName: M,
|
|
175
|
+
validator: DisplayValidator<A, M>
|
|
176
|
+
): void {
|
|
177
|
+
this.validators.set(methodName, validator)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Unregister a validator for a specific method.
|
|
182
|
+
*/
|
|
183
|
+
unregisterValidator<M extends FunctionName<A>>(methodName: M): void {
|
|
184
|
+
this.validators.delete(methodName)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a method has a registered validator.
|
|
189
|
+
*/
|
|
190
|
+
hasValidator<M extends FunctionName<A>>(methodName: M): boolean {
|
|
191
|
+
return this.validators.has(methodName)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validate arguments without calling the canister.
|
|
196
|
+
* Arguments are in display format (strings for Principal/bigint).
|
|
197
|
+
* Useful for form validation before submission.
|
|
198
|
+
*
|
|
199
|
+
* @param methodName - The name of the method
|
|
200
|
+
* @param args - The display-type arguments to validate
|
|
201
|
+
* @returns ValidationResult indicating success or failure
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // Validate form data before submission
|
|
206
|
+
* const result = await reactor.validate("transfer", [{
|
|
207
|
+
* to: formData.recipient, // string
|
|
208
|
+
* amount: formData.amount, // string
|
|
209
|
+
* }])
|
|
210
|
+
*
|
|
211
|
+
* if (!result.success) {
|
|
212
|
+
* result.issues.forEach(issue => {
|
|
213
|
+
* form.setError(issue.path[0], issue.message)
|
|
214
|
+
* })
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async validate<M extends FunctionName<A>>(
|
|
219
|
+
methodName: M,
|
|
220
|
+
args: ReactorArgs<A, M, T>
|
|
221
|
+
): Promise<ValidationResult> {
|
|
222
|
+
const validator = this.validators.get(methodName)
|
|
223
|
+
if (!validator) {
|
|
224
|
+
return { success: true }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return validator(args)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Call a method with async validation support.
|
|
232
|
+
* Use this instead of callMethod() when you have async validators.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* // Async validator (e.g., check if address is blocked)
|
|
237
|
+
* reactor.registerValidator("transfer", async ([input]) => {
|
|
238
|
+
* const isBlocked = await checkBlocklist(input.to)
|
|
239
|
+
* if (isBlocked) {
|
|
240
|
+
* return {
|
|
241
|
+
* success: false,
|
|
242
|
+
* issues: [{ path: ["to"], message: "Address is blocked" }]
|
|
243
|
+
* }
|
|
244
|
+
* }
|
|
245
|
+
* return { success: true }
|
|
246
|
+
* })
|
|
247
|
+
*
|
|
248
|
+
* await reactor.callMethodWithValidation({
|
|
249
|
+
* functionName: "transfer",
|
|
250
|
+
* args: [{ to: "...", amount: "100" }],
|
|
251
|
+
* })
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
async callMethodWithValidation<M extends FunctionName<A>>(params: {
|
|
255
|
+
functionName: M
|
|
256
|
+
args?: ReactorArgs<A, M, T>
|
|
257
|
+
callConfig?: Parameters<
|
|
258
|
+
Reactor<A, "display">["callMethod"]
|
|
259
|
+
>[0]["callConfig"]
|
|
260
|
+
}): Promise<ReactorReturnOk<A, M, T>> {
|
|
261
|
+
// Run async validation first (on display types)
|
|
262
|
+
if (params.args) {
|
|
263
|
+
const result = await this.validate(params.functionName, params.args)
|
|
264
|
+
if (!result.success) {
|
|
265
|
+
throw new ValidationError(String(params.functionName), result.issues)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Skip synchronous validation in transformArgs by temporarily removing validator
|
|
270
|
+
const validator = this.validators.get(params.functionName)
|
|
271
|
+
if (validator) {
|
|
272
|
+
this.validators.delete(params.functionName)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// @ts-ignore
|
|
277
|
+
return await this.callMethod(params)
|
|
278
|
+
} finally {
|
|
279
|
+
// Restore validator
|
|
280
|
+
if (validator) {
|
|
281
|
+
this.validators.set(params.functionName, validator)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Transform Methods
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Transform arguments before calling the actor method.
|
|
292
|
+
* 1. Validates display-type args (if validator registered)
|
|
293
|
+
* 2. Converts Display → Candid
|
|
294
|
+
*/
|
|
295
|
+
protected transformArgs<M extends FunctionName<A>>(
|
|
296
|
+
methodName: M,
|
|
297
|
+
args?: ReactorArgs<A, M, T>
|
|
298
|
+
): ActorMethodParameters<A[M]> {
|
|
299
|
+
// 1. Validate FIRST (on display types)
|
|
300
|
+
const validator = this.validators.get(methodName)
|
|
301
|
+
const displayArgs = args as unknown as ReactorArgs<A, M, "display">
|
|
302
|
+
|
|
303
|
+
if (validator && displayArgs) {
|
|
304
|
+
const result = validator(displayArgs)
|
|
305
|
+
|
|
306
|
+
// Handle Promise (async validator)
|
|
307
|
+
if (
|
|
308
|
+
result &&
|
|
309
|
+
typeof (result as Promise<ValidationResult>).then === "function"
|
|
310
|
+
) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Async validators are not supported in callMethod(). ` +
|
|
313
|
+
`Use reactor.callMethodWithValidation() for async validation.`
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const syncResult = result as ValidationResult
|
|
318
|
+
if (!syncResult.success) {
|
|
319
|
+
throw new ValidationError(String(methodName), syncResult.issues)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 2. THEN transform: Display → Candid
|
|
324
|
+
if (this.codecs.has(methodName)) {
|
|
325
|
+
const codec = this.codecs.get(methodName)!
|
|
326
|
+
return transformArgsWithCodec<ActorMethodParameters<A[M]>>(
|
|
327
|
+
codec.args,
|
|
328
|
+
displayArgs
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
if (!args) {
|
|
332
|
+
return [] as unknown as ActorMethodParameters<A[M]>
|
|
333
|
+
}
|
|
334
|
+
return args as ActorMethodParameters<A[M]>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Transform the result after calling the actor method.
|
|
339
|
+
* Always extracts the Ok value from Result types (throws CanisterError on Err).
|
|
340
|
+
* Also converts Candid → Display format.
|
|
341
|
+
*/
|
|
342
|
+
protected transformResult<M extends FunctionName<A>>(
|
|
343
|
+
methodName: M,
|
|
344
|
+
result: ActorMethodReturnType<A[M]>
|
|
345
|
+
): ReactorReturnOk<A, M, T> {
|
|
346
|
+
let transformedResult = result
|
|
347
|
+
// 1. Apply display transformation to the FULL result
|
|
348
|
+
if (this.codecs.has(methodName)) {
|
|
349
|
+
const codec = this.codecs.get(methodName)!
|
|
350
|
+
transformedResult = transformResultWithCodec(codec.result, result)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2. Extract Ok value from the TRANSFORMED (or raw) result
|
|
354
|
+
// This handles { ok: T } / { err: E } from Motoko/Rust canisters
|
|
355
|
+
return extractOkResult(transformedResult) as unknown as ReactorReturnOk<
|
|
356
|
+
A,
|
|
357
|
+
M,
|
|
358
|
+
T
|
|
359
|
+
>
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { NullishType } from "../display/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface representing the generic shape of an API error.
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiError {
|
|
7
|
+
code: string
|
|
8
|
+
message: NullishType<string>
|
|
9
|
+
details: NullishType<Map<string, string>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error thrown when there's an issue calling the canister.
|
|
14
|
+
* This includes network errors, agent errors, canister not found, etc.
|
|
15
|
+
*/
|
|
16
|
+
export class CallError extends Error {
|
|
17
|
+
public readonly cause?: unknown
|
|
18
|
+
|
|
19
|
+
constructor(message: string, cause?: unknown) {
|
|
20
|
+
super(message)
|
|
21
|
+
this.name = "CallError"
|
|
22
|
+
this.cause = cause
|
|
23
|
+
|
|
24
|
+
// Maintains proper stack trace for where our error was thrown
|
|
25
|
+
if (Error.captureStackTrace) {
|
|
26
|
+
Error.captureStackTrace(this, CallError)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Error thrown when the canister returns an Err result.
|
|
33
|
+
* The `err` property contains the typed error value from the canister.
|
|
34
|
+
*
|
|
35
|
+
* It also supports accessing `code`, `message`, and `details` directly
|
|
36
|
+
* if the error object follows the common API error format or is a variant.
|
|
37
|
+
*
|
|
38
|
+
* @typeParam E - The type of the error value from the canister
|
|
39
|
+
*/
|
|
40
|
+
export class CanisterError<E = unknown> extends Error {
|
|
41
|
+
/** The raw error value from the canister */
|
|
42
|
+
public readonly err: E
|
|
43
|
+
/** The error code, extracted from the error object or variant key */
|
|
44
|
+
public readonly code: string
|
|
45
|
+
/** Optional error details Map */
|
|
46
|
+
public readonly details: NullishType<Map<string, string>>
|
|
47
|
+
|
|
48
|
+
constructor(err: E) {
|
|
49
|
+
let code: string | undefined
|
|
50
|
+
let message: string | undefined
|
|
51
|
+
let details: NullishType<Map<string, string>> = undefined
|
|
52
|
+
let isApiShape = false
|
|
53
|
+
|
|
54
|
+
if (typeof err === "object" && err !== null) {
|
|
55
|
+
// 1. Check for structured ApiError shape (has code)
|
|
56
|
+
if ("code" in err && typeof err.code === "string") {
|
|
57
|
+
code = err.code
|
|
58
|
+
isApiShape = true
|
|
59
|
+
if ("message" in err && typeof err.message === "string") {
|
|
60
|
+
message = err.message
|
|
61
|
+
}
|
|
62
|
+
if ("details" in err) {
|
|
63
|
+
details = err.details as any
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 2. Check for ic-reactor transformed variant shape (_type)
|
|
67
|
+
else if ("_type" in err && typeof err._type === "string") {
|
|
68
|
+
code = err._type
|
|
69
|
+
}
|
|
70
|
+
// 3. Simple variant check (single key object)
|
|
71
|
+
else {
|
|
72
|
+
const keys = Object.keys(err)
|
|
73
|
+
if (keys.length === 1) {
|
|
74
|
+
code = keys[0]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const finalCode = code ?? "UNKNOWN_ERROR"
|
|
80
|
+
const finalMessage =
|
|
81
|
+
message ??
|
|
82
|
+
(typeof err === "object" && err !== null
|
|
83
|
+
? JSON.stringify(
|
|
84
|
+
err,
|
|
85
|
+
(_, v) => (typeof v === "bigint" ? v.toString() : v),
|
|
86
|
+
2
|
|
87
|
+
)
|
|
88
|
+
: String(err))
|
|
89
|
+
|
|
90
|
+
super(isApiShape ? finalMessage : `Canister Error: ${finalMessage}`)
|
|
91
|
+
this.name = "CanisterError"
|
|
92
|
+
this.err = err
|
|
93
|
+
this.code = finalCode
|
|
94
|
+
this.details = details
|
|
95
|
+
|
|
96
|
+
// Maintains proper stack trace for where our error was thrown
|
|
97
|
+
if (Error.captureStackTrace) {
|
|
98
|
+
Error.captureStackTrace(this, CanisterError)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Type guard to check if an error object follows the API error format.
|
|
104
|
+
*/
|
|
105
|
+
static isApiError(error: unknown): error is ApiError {
|
|
106
|
+
if (typeof error !== "object" || error === null) {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return "code" in error && "message" in error && "details" in error
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Factory method to create a CanisterError from any error.
|
|
115
|
+
* If the input is already a CanisterError, it returns it.
|
|
116
|
+
* If it's an API error shape, it wraps it.
|
|
117
|
+
* Otherwise, it creates a new CanisterError with an "UNKNOWN_ERROR" code.
|
|
118
|
+
*/
|
|
119
|
+
static create(error: unknown, message?: string): CanisterError {
|
|
120
|
+
if (error instanceof CanisterError) {
|
|
121
|
+
return error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (CanisterError.isApiError(error)) {
|
|
125
|
+
return new CanisterError(error)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new CanisterError({
|
|
129
|
+
code: "UNKNOWN_ERROR",
|
|
130
|
+
message:
|
|
131
|
+
error instanceof Error
|
|
132
|
+
? error.message
|
|
133
|
+
: message || "An unknown error occurred",
|
|
134
|
+
details: undefined,
|
|
135
|
+
} as any)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Type guard to check if an error is a CanisterError.
|
|
141
|
+
* Preserves the generic type E from the input when used in type narrowing.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* // err is typed as CanisterError<TransferError> | CallError
|
|
146
|
+
* if (isCanisterError(err)) {
|
|
147
|
+
* // err.err is typed as TransferError (preserved!)
|
|
148
|
+
* console.log(err.err)
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function isCanisterError<E>(
|
|
153
|
+
error: CanisterError<E> | CallError
|
|
154
|
+
): error is CanisterError<E>
|
|
155
|
+
export function isCanisterError(error: unknown): error is CanisterError<unknown>
|
|
156
|
+
export function isCanisterError(
|
|
157
|
+
error: unknown
|
|
158
|
+
): error is CanisterError<unknown> {
|
|
159
|
+
return error instanceof CanisterError
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Type guard to check if an error is a CallError
|
|
164
|
+
*/
|
|
165
|
+
export function isCallError(error: unknown): error is CallError {
|
|
166
|
+
return error instanceof CallError
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Validation Errors
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Represents a single validation issue
|
|
175
|
+
*/
|
|
176
|
+
export interface ValidationIssue {
|
|
177
|
+
/** Path to the invalid field (e.g., ["to", "amount"]) */
|
|
178
|
+
path: (string | number)[]
|
|
179
|
+
/** Human-readable error message */
|
|
180
|
+
message: string
|
|
181
|
+
/** Validation code (e.g., "required", "min_length") */
|
|
182
|
+
code?: string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Error thrown when argument validation fails before calling the canister.
|
|
187
|
+
* Contains detailed information about which fields failed validation.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* try {
|
|
192
|
+
* await reactor.callMethod({
|
|
193
|
+
* functionName: "transfer",
|
|
194
|
+
* args: [{ to: "", amount: -100 }],
|
|
195
|
+
* })
|
|
196
|
+
* } catch (error) {
|
|
197
|
+
* if (isValidationError(error)) {
|
|
198
|
+
* console.log(error.issues)
|
|
199
|
+
* // [
|
|
200
|
+
* // { path: ["to"], message: "Recipient is required" },
|
|
201
|
+
* // { path: ["amount"], message: "Amount must be positive" }
|
|
202
|
+
* // ]
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export class ValidationError extends Error {
|
|
208
|
+
/** Array of validation issues */
|
|
209
|
+
public readonly issues: ValidationIssue[]
|
|
210
|
+
/** The method name that failed validation */
|
|
211
|
+
public readonly methodName: string
|
|
212
|
+
|
|
213
|
+
constructor(methodName: string, issues: ValidationIssue[]) {
|
|
214
|
+
const messages = issues.map((i) => i.message).join(", ")
|
|
215
|
+
super(`Validation failed for "${methodName}": ${messages}`)
|
|
216
|
+
this.name = "ValidationError"
|
|
217
|
+
this.methodName = methodName
|
|
218
|
+
this.issues = issues
|
|
219
|
+
|
|
220
|
+
// Maintains proper stack trace for where our error was thrown
|
|
221
|
+
if (Error.captureStackTrace) {
|
|
222
|
+
Error.captureStackTrace(this, ValidationError)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get issues for a specific field path
|
|
228
|
+
*/
|
|
229
|
+
getIssuesForPath(path: string): ValidationIssue[] {
|
|
230
|
+
return this.issues.filter((issue) => issue.path.includes(path))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if a specific field has errors
|
|
235
|
+
*/
|
|
236
|
+
hasErrorForPath(path: string): boolean {
|
|
237
|
+
return this.issues.some((issue) => issue.path.includes(path))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Type guard to check if an error is a ValidationError
|
|
243
|
+
*/
|
|
244
|
+
export function isValidationError(error: unknown): error is ValidationError {
|
|
245
|
+
return error instanceof ValidationError
|
|
246
|
+
}
|