@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
@@ -0,0 +1,2377 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { ResultFieldVisitor } from "./index"
3
+ import type {
4
+ ResultNode,
5
+ ResolvedNode,
6
+ RecordNode,
7
+ FuncRecordNode,
8
+ VariantNode,
9
+ TupleNode,
10
+ OptionalNode,
11
+ VectorNode,
12
+ BlobNode,
13
+ RecursiveNode,
14
+ NumberNode,
15
+ TextNode,
16
+ PrincipalNode,
17
+ BooleanNode,
18
+ NullNode,
19
+ FuncNode,
20
+ MethodMeta,
21
+ ServiceMeta,
22
+ } from "./types"
23
+ import { IDL } from "@icp-sdk/core/candid"
24
+
25
+ describe("ResultFieldVisitor", () => {
26
+ const visitor = new ResultFieldVisitor()
27
+
28
+ // ════════════════════════════════════════════════════════════════════════
29
+ // Primitive Types
30
+ // ════════════════════════════════════════════════════════════════════════
31
+
32
+ describe("Primitive Types", () => {
33
+ it("should handle text type", () => {
34
+ const textType = IDL.Text
35
+ const field = visitor.visitText(textType, "message")
36
+
37
+ expect(field.type).toBe("text")
38
+ expect(field.label).toBe("message")
39
+ expect(field.candidType).toBe("text")
40
+ expect(field.displayType).toBe("string")
41
+ expect(field.format).toBe("plain")
42
+ })
43
+
44
+ it("should handle text with special label detection", () => {
45
+ const emailField = visitor.visitText(IDL.Text, "email")
46
+ expect(emailField.format).toBe("email")
47
+
48
+ const urlField = visitor.visitText(IDL.Text, "website_url")
49
+ expect(urlField.format).toBe("url")
50
+
51
+ const phoneField = visitor.visitText(IDL.Text, "phone_number")
52
+ expect(phoneField.format).toBe("phone")
53
+
54
+ const uuidField = visitor.visitText(IDL.Text, "transaction_uuid")
55
+ expect(uuidField.format).toBe("uuid")
56
+
57
+ const btcField = visitor.visitText(IDL.Text, "btc_address")
58
+ expect(btcField.format).toBe("btc")
59
+
60
+ const ethField = visitor.visitText(IDL.Text, "ethereum_address")
61
+ expect(ethField.format).toBe("eth")
62
+
63
+ const accountIdField = visitor.visitText(IDL.Text, "account_identifier")
64
+ expect(accountIdField.format).toBe("account-id")
65
+
66
+ const principalField = visitor.visitText(IDL.Text, "canister_id")
67
+ expect(principalField.format).toBe("principal")
68
+ })
69
+
70
+ it("should handle bool type", () => {
71
+ const boolType = IDL.Bool
72
+ const field = visitor.visitBool(boolType, "isActive")
73
+
74
+ expect(field.type).toBe("boolean")
75
+ expect(field.label).toBe("isActive")
76
+ expect(field.candidType).toBe("bool")
77
+ expect(field.displayType).toBe("boolean")
78
+ })
79
+
80
+ it("should handle null type", () => {
81
+ const nullType = IDL.Null
82
+ const field = visitor.visitNull(nullType, "empty")
83
+
84
+ expect(field.type).toBe("null")
85
+ expect(field.candidType).toBe("null")
86
+ expect(field.displayType).toBe("null")
87
+ })
88
+
89
+ it("should handle principal type", () => {
90
+ const principalType = IDL.Principal
91
+ const field = visitor.visitPrincipal(IDL.Principal, "owner")
92
+
93
+ expect(field.type).toBe("principal")
94
+ expect(field.label).toBe("owner")
95
+ expect(field.candidType).toBe("principal")
96
+ expect(field.displayType).toBe("string") // Principal → string after transformation
97
+ })
98
+ })
99
+
100
+ // ════════════════════════════════════════════════════════════════════════
101
+ // Number Types - Display Type Mapping
102
+ // ════════════════════════════════════════════════════════════════════════
103
+
104
+ describe("Number Types", () => {
105
+ describe("BigInt types (display as string)", () => {
106
+ it("should map nat to string display type", () => {
107
+ const field = visitor.visitNat(IDL.Nat, "amount")
108
+
109
+ expect(field.type).toBe("number")
110
+ expect(field.candidType).toBe("nat")
111
+ expect(field.displayType).toBe("string") // BigInt → string
112
+ expect(field.format).toBe("normal")
113
+ })
114
+
115
+ it("should map int to string display type", () => {
116
+ const field = visitor.visitInt(IDL.Int, "balance")
117
+
118
+ expect(field.type).toBe("number")
119
+ expect(field.candidType).toBe("int")
120
+ expect(field.displayType).toBe("string")
121
+ })
122
+
123
+ it("should map nat64 to string display type", () => {
124
+ const field = visitor.visitFixedNat(IDL.Nat64, "timestamp")
125
+
126
+ expect(field.type).toBe("number")
127
+ expect(field.candidType).toBe("nat64")
128
+ expect(field.displayType).toBe("string") // 64-bit → string
129
+ })
130
+
131
+ it("should map int64 to string display type", () => {
132
+ const field = visitor.visitFixedInt(IDL.Int64, "offset")
133
+
134
+ expect(field.type).toBe("number")
135
+ expect(field.candidType).toBe("int64")
136
+ expect(field.displayType).toBe("string")
137
+ })
138
+ })
139
+
140
+ describe("Small int types (display as number)", () => {
141
+ it("should map nat8 to number display type", () => {
142
+ const field = visitor.visitFixedNat(IDL.Nat8, "byte")
143
+
144
+ expect(field.type).toBe("number")
145
+ expect(field.candidType).toBe("nat8")
146
+ expect(field.displayType).toBe("number") // Small int stays number
147
+ })
148
+
149
+ it("should map nat16 to number display type", () => {
150
+ const field = visitor.visitFixedNat(IDL.Nat16, "port")
151
+
152
+ expect(field.candidType).toBe("nat16")
153
+ expect(field.displayType).toBe("number")
154
+ })
155
+
156
+ it("should map nat32 to number display type", () => {
157
+ const field = visitor.visitFixedNat(IDL.Nat32, "count")
158
+
159
+ expect(field.candidType).toBe("nat32")
160
+ expect(field.displayType).toBe("number")
161
+ })
162
+
163
+ it("should map int8 to number display type", () => {
164
+ const field = visitor.visitFixedInt(IDL.Int8, "temp")
165
+
166
+ expect(field.candidType).toBe("int8")
167
+ expect(field.displayType).toBe("number")
168
+ })
169
+
170
+ it("should map int32 to number display type", () => {
171
+ const field = visitor.visitFixedInt(IDL.Int32, "index")
172
+
173
+ expect(field.candidType).toBe("int32")
174
+ expect(field.displayType).toBe("number")
175
+ })
176
+ })
177
+
178
+ describe("Float types (display as number)", () => {
179
+ it("should map float32 to number display type", () => {
180
+ const field = visitor.visitFloat(IDL.Float32, "rate")
181
+
182
+ expect(field.candidType).toBe("float32")
183
+ expect(field.displayType).toBe("number")
184
+ })
185
+
186
+ it("should map float64 to number display type", () => {
187
+ const field = visitor.visitFloat(IDL.Float64, "price")
188
+
189
+ expect(field.candidType).toBe("float64")
190
+ expect(field.displayType).toBe("number")
191
+ })
192
+ })
193
+
194
+ describe("Number format detection", () => {
195
+ it("should detect timestamp format from label", () => {
196
+ const timestampField = visitor.visitFixedNat(IDL.Nat64, "created_at")
197
+ expect(timestampField.format).toBe("timestamp")
198
+
199
+ const dateField = visitor.visitNat(IDL.Nat, "timestamp_nanos")
200
+ expect(dateField.format).toBe("timestamp")
201
+
202
+ const deadlineField = visitor.visitNat(IDL.Nat, "deadline")
203
+ expect(deadlineField.format).toBe("timestamp")
204
+ })
205
+
206
+ it("should detect cycle format from label", () => {
207
+ const cycleField = visitor.visitNat(IDL.Nat, "cycles")
208
+ expect(cycleField.format).toBe("cycle")
209
+
210
+ // "cycle" as standalone word
211
+ const cycleSingleField = visitor.visitNat(IDL.Nat, "cycle")
212
+ expect(cycleSingleField.format).toBe("cycle")
213
+ })
214
+
215
+ it("should default to normal format", () => {
216
+ const amountField = visitor.visitNat(IDL.Nat, "amount")
217
+ expect(amountField.format).toBe("normal")
218
+
219
+ const countField = visitor.visitNat(IDL.Nat, "count")
220
+ expect(countField.format).toBe("normal")
221
+ })
222
+ })
223
+ })
224
+
225
+ // ════════════════════════════════════════════════════════════════════════
226
+ // Compound Types
227
+ // ════════════════════════════════════════════════════════════════════════
228
+
229
+ describe("Record Types", () => {
230
+ it("should handle simple record", () => {
231
+ const recordType = IDL.Record({
232
+ name: IDL.Text,
233
+ age: IDL.Nat,
234
+ })
235
+ const field = visitor.visitRecord(
236
+ recordType,
237
+ [
238
+ ["name", IDL.Text],
239
+ ["age", IDL.Nat],
240
+ ],
241
+ "person"
242
+ )
243
+
244
+ expect(field.type).toBe("record")
245
+ expect(field.label).toBe("person")
246
+ expect(field.candidType).toBe("record")
247
+ expect(field.displayType).toBe("object")
248
+ expect(Object.keys(field.fields)).toHaveLength(2)
249
+
250
+ const nameField = field.fields["name"]
251
+ if (!nameField || nameField.type !== "text") {
252
+ throw new Error("Name field not found or not text")
253
+ }
254
+ expect(nameField.displayType).toBe("string")
255
+
256
+ const ageField = field.fields["age"]
257
+ if (!ageField || ageField.type !== "number") {
258
+ throw new Error("Age field not found or not number")
259
+ }
260
+ expect(ageField.displayType).toBe("string") // nat → string
261
+ })
262
+
263
+ it("should handle nested record", () => {
264
+ const addressType = IDL.Record({
265
+ street: IDL.Text,
266
+ city: IDL.Text,
267
+ })
268
+ const personType = IDL.Record({
269
+ name: IDL.Text,
270
+ address: addressType,
271
+ })
272
+ const field = visitor.visitRecord(
273
+ personType,
274
+ [
275
+ ["name", IDL.Text],
276
+ ["address", addressType],
277
+ ],
278
+ "user"
279
+ )
280
+
281
+ expect(field.type).toBe("record")
282
+
283
+ const addressField = field.fields["address"] as RecordNode
284
+ if (!addressField || addressField.type !== "record") {
285
+ throw new Error("Address field not found or not a record")
286
+ }
287
+
288
+ expect(addressField.type).toBe("record")
289
+ expect(addressField.displayType).toBe("object")
290
+ expect(Object.keys(addressField.fields)).toHaveLength(2)
291
+ })
292
+
293
+ it("should handle ICRC-1 account record", () => {
294
+ const accountType = IDL.Record({
295
+ owner: IDL.Principal,
296
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
297
+ })
298
+ const field = visitor.visitRecord(
299
+ accountType,
300
+ [
301
+ ["owner", IDL.Principal],
302
+ ["subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
303
+ ],
304
+ "account"
305
+ )
306
+
307
+ expect(field.type).toBe("record")
308
+
309
+ const ownerField = field.fields["owner"]
310
+ if (!ownerField || ownerField.type !== "principal") {
311
+ throw new Error("Owner field not found or not principal")
312
+ }
313
+ expect(ownerField.type).toBe("principal")
314
+ expect(ownerField.displayType).toBe("string")
315
+
316
+ const subaccountField = field.fields["subaccount"] as OptionalNode
317
+ if (!subaccountField || subaccountField.type !== "optional") {
318
+ throw new Error("Subaccount field not found or not optional")
319
+ }
320
+ expect(subaccountField.type).toBe("optional")
321
+ // Resolve an example value to validate inner blob type
322
+ const subaccountResolved = subaccountField.resolve([
323
+ new Uint8Array([1, 2, 3]),
324
+ ])
325
+ expect((subaccountResolved.value as ResolvedNode).type).toBe("blob")
326
+ })
327
+ })
328
+
329
+ describe("Variant Types", () => {
330
+ it("should handle simple variant", () => {
331
+ const statusType = IDL.Variant({
332
+ Active: IDL.Null,
333
+ Inactive: IDL.Null,
334
+ Pending: IDL.Null,
335
+ })
336
+ const field = visitor.visitVariant(
337
+ statusType,
338
+ [
339
+ ["Active", IDL.Null],
340
+ ["Inactive", IDL.Null],
341
+ ["Pending", IDL.Null],
342
+ ],
343
+ "status"
344
+ )
345
+
346
+ expect(field.type).toBe("variant")
347
+ expect(field.candidType).toBe("variant")
348
+ expect(field.displayType).toBe("variant-null")
349
+ // Validate options by resolving each option
350
+ const resolvedActive = field.resolve({ Active: null })
351
+ expect(resolvedActive.selected).toBe("Active")
352
+ expect(resolvedActive.selectedValue.type).toBe("null")
353
+ const resolvedInactive = field.resolve({ Inactive: null })
354
+ expect(resolvedInactive.selected).toBe("Inactive")
355
+ const resolvedPending = field.resolve({ Pending: null })
356
+ expect(resolvedPending.selected).toBe("Pending")
357
+ })
358
+
359
+ it("should handle variant with payloads", () => {
360
+ const eventType = IDL.Variant({
361
+ Transfer: IDL.Record({
362
+ from: IDL.Principal,
363
+ to: IDL.Principal,
364
+ amount: IDL.Nat,
365
+ }),
366
+ Approve: IDL.Nat,
367
+ Mint: IDL.Record({
368
+ to: IDL.Principal,
369
+ amount: IDL.Nat,
370
+ }),
371
+ })
372
+ const field = visitor.visitVariant(
373
+ eventType,
374
+ [
375
+ [
376
+ "Transfer",
377
+ IDL.Record({
378
+ from: IDL.Principal,
379
+ to: IDL.Principal,
380
+ amount: IDL.Nat,
381
+ }),
382
+ ],
383
+ ["Approve", IDL.Nat],
384
+ [
385
+ "Mint",
386
+ IDL.Record({
387
+ to: IDL.Principal,
388
+ amount: IDL.Nat,
389
+ }),
390
+ ],
391
+ ],
392
+ "event"
393
+ )
394
+
395
+ expect(field.type).toBe("variant")
396
+ // Validate Transfer payload
397
+ const transferResolved = field.resolve({
398
+ Transfer: { from: "aaaaa-aa", to: "bbbbb-bb", amount: BigInt(1) },
399
+ })
400
+ expect(transferResolved.selected).toBe("Transfer")
401
+ expect(transferResolved.selectedValue.type).toBe("record")
402
+ expect(
403
+ Object.keys((transferResolved.selectedValue as RecordNode).fields)
404
+ ).toHaveLength(3)
405
+
406
+ const approveResolved = field.resolve({ Approve: BigInt(5) })
407
+ expect(approveResolved.selected).toBe("Approve")
408
+ expect(approveResolved.selectedValue.type).toBe("number")
409
+ })
410
+
411
+ it("should detect Result variant (Ok/Err)", () => {
412
+ const resultType = IDL.Variant({
413
+ Ok: IDL.Nat,
414
+ Err: IDL.Text,
415
+ })
416
+ const field = visitor.visitVariant(
417
+ resultType,
418
+ [
419
+ ["Ok", IDL.Nat],
420
+ ["Err", IDL.Text],
421
+ ],
422
+ "result"
423
+ )
424
+
425
+ expect(field.type).toBe("variant")
426
+ expect(field.displayType).toBe("result") // Special result type
427
+
428
+ const okResolved = field.resolve({ Ok: BigInt(1) })
429
+ expect(okResolved.selected).toBe("Ok")
430
+ expect(okResolved.selectedValue.type).toBe("number")
431
+ expect(okResolved.selectedValue.displayType).toBe("string")
432
+
433
+ const errResolved = field.resolve({ Err: "error" })
434
+ expect(errResolved.selected).toBe("Err")
435
+ expect(errResolved.selectedValue.type).toBe("text")
436
+ expect(errResolved.selectedValue.displayType).toBe("string")
437
+ })
438
+
439
+ it("should detect complex Result variant", () => {
440
+ const resultType = IDL.Variant({
441
+ Ok: IDL.Record({
442
+ id: IDL.Nat,
443
+ data: IDL.Vec(IDL.Nat8),
444
+ }),
445
+ Err: IDL.Variant({
446
+ NotFound: IDL.Null,
447
+ Unauthorized: IDL.Null,
448
+ InvalidInput: IDL.Text,
449
+ }),
450
+ })
451
+ const field = visitor.visitVariant(
452
+ resultType,
453
+ [
454
+ [
455
+ "Ok",
456
+ IDL.Record({
457
+ id: IDL.Nat,
458
+ data: IDL.Vec(IDL.Nat8),
459
+ }),
460
+ ],
461
+ [
462
+ "Err",
463
+ IDL.Variant({
464
+ NotFound: IDL.Null,
465
+ Unauthorized: IDL.Null,
466
+ InvalidInput: IDL.Text,
467
+ }),
468
+ ],
469
+ ],
470
+ "result"
471
+ )
472
+
473
+ expect(field.displayType).toBe("result")
474
+
475
+ const okResolved = field.resolve({
476
+ Ok: { id: BigInt(1), data: new Uint8Array([1, 2, 3]) },
477
+ })
478
+ expect(okResolved.selected).toBe("Ok")
479
+ expect(okResolved.selectedValue.type).toBe("record")
480
+
481
+ const errResolved = field.resolve({ Err: { NotFound: null } })
482
+ expect(errResolved.selected).toBe("Err")
483
+ expect(errResolved.selectedValue.type).toBe("variant")
484
+ })
485
+
486
+ it("should not detect non-Result variant with Ok and other options", () => {
487
+ const weirdType = IDL.Variant({
488
+ Ok: IDL.Nat,
489
+ Pending: IDL.Null,
490
+ Processing: IDL.Text,
491
+ })
492
+ const field = visitor.visitVariant(
493
+ weirdType,
494
+ [
495
+ ["Ok", IDL.Nat],
496
+ ["Pending", IDL.Null],
497
+ ["Processing", IDL.Text],
498
+ ],
499
+ "status"
500
+ )
501
+
502
+ expect(field.displayType).toBe("variant")
503
+ })
504
+ })
505
+
506
+ describe("Tuple Types", () => {
507
+ it("should handle simple tuple", () => {
508
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
509
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
510
+
511
+ expect(field.type).toBe("tuple")
512
+ expect(field.candidType).toBe("tuple")
513
+ expect(field.displayType).toBe("array")
514
+ expect(field.items).toHaveLength(2)
515
+ expect(field.items[0].type).toBe("text")
516
+ expect(field.items[1].type).toBe("number")
517
+ })
518
+
519
+ it("should handle tuple with mixed types", () => {
520
+ const tupleType = IDL.Tuple(
521
+ IDL.Principal,
522
+ IDL.Nat64,
523
+ IDL.Bool,
524
+ IDL.Vec(IDL.Nat8)
525
+ )
526
+ const field = visitor.visitTuple(
527
+ tupleType,
528
+ [IDL.Principal, IDL.Nat64, IDL.Bool, IDL.Vec(IDL.Nat8)],
529
+ "data"
530
+ )
531
+
532
+ expect(field.items).toHaveLength(4)
533
+ expect(field.items[0].type).toBe("principal")
534
+ expect(field.items[1].type).toBe("number")
535
+ const numField = field.items[1]
536
+ if (numField.type === "number") {
537
+ expect(numField.displayType).toBe("string") // nat64 → string
538
+ } else {
539
+ throw new Error("Expected number field")
540
+ }
541
+ expect(field.items[2].type).toBe("boolean")
542
+ expect(field.items[3].type).toBe("blob")
543
+ })
544
+ })
545
+
546
+ describe("Optional Types", () => {
547
+ it("should handle optional primitive", () => {
548
+ const optType = IDL.Opt(IDL.Text)
549
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
550
+
551
+ expect(field.type).toBe("optional")
552
+ expect(field.candidType).toBe("opt")
553
+ expect(field.displayType).toBe("nullable") // opt T → T | null
554
+ // Validate inner by resolving a value
555
+ const optResolved = field.resolve(["Bob"])
556
+ expect((optResolved.value as ResolvedNode).type).toBe("text")
557
+ })
558
+
559
+ it("should handle optional record", () => {
560
+ const recordInOpt = IDL.Record({
561
+ name: IDL.Text,
562
+ value: IDL.Nat,
563
+ })
564
+ const optType = IDL.Opt(recordInOpt)
565
+ const field = visitor.visitOpt(optType, recordInOpt, "metadata")
566
+
567
+ expect(field.type).toBe("optional")
568
+ // Validate inner by resolving a record
569
+ const metaResolved = field.resolve([{ name: "x", value: BigInt(1) }])
570
+ expect((metaResolved.value as ResolvedNode).type).toBe("record")
571
+ })
572
+
573
+ it("should handle nested optional", () => {
574
+ const innerOpt = IDL.Opt(IDL.Nat)
575
+ const optType = IDL.Opt(innerOpt)
576
+ const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
577
+
578
+ expect(field.type).toBe("optional")
579
+ // Validate nested optional by resolving a nested array
580
+ const nestedResolved = field.resolve([[BigInt(1)]])
581
+ expect((nestedResolved.value as ResolvedNode).type).toBe("optional")
582
+ const innerResolved = nestedResolved.value as OptionalNode
583
+ expect((innerResolved.value as ResolvedNode).type).toBe("number")
584
+ })
585
+ })
586
+
587
+ describe("Vector Types", () => {
588
+ it("should handle vector of primitives", () => {
589
+ const vecType = IDL.Vec(IDL.Text)
590
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
591
+
592
+ expect(field.type).toBe("vector")
593
+ expect(field.candidType).toBe("vec")
594
+ expect(field.displayType).toBe("array")
595
+ // Validate items by resolving
596
+ const vecResolved = field.resolve(["a", "b", "c"]) as VectorNode
597
+ expect(vecResolved.items).toHaveLength(3)
598
+ expect(vecResolved.items[0].value).toBe("a")
599
+ })
600
+
601
+ it("should handle vector of records", () => {
602
+ const recType = IDL.Record({
603
+ id: IDL.Nat,
604
+ name: IDL.Text,
605
+ })
606
+ const vecType = IDL.Vec(recType)
607
+ const field = visitor.visitVec(vecType, recType, "items")
608
+
609
+ expect(field.type).toBe("vector")
610
+ // Validate by resolving
611
+ const itemsResolved = field.resolve([
612
+ { id: BigInt(1), name: "x" },
613
+ ]) as VectorNode
614
+ expect(itemsResolved.items[0].type).toBe("record")
615
+ })
616
+
617
+ it("should handle blob (vec nat8)", () => {
618
+ const blobType = IDL.Vec(IDL.Nat8)
619
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
620
+
621
+ expect(field.type).toBe("blob")
622
+ expect(field.candidType).toBe("blob")
623
+ expect(field.displayType).toBe("string") // Blob → hex string
624
+ // Validate by resolving a Uint8Array
625
+ const blobResolved = field.resolve(
626
+ new Uint8Array([0x12, 0x34, 0xab, 0xcd])
627
+ ) as ResolvedNode<"blob">
628
+ expect(blobResolved.value).toBe("1234abcd")
629
+ expect(blobResolved.hash).toBeDefined()
630
+ expect(blobResolved.hash).toHaveLength(64)
631
+ })
632
+
633
+ it("should handle large blob (> 512 bytes)", () => {
634
+ const blobType = IDL.Vec(IDL.Nat8)
635
+ const field = visitor.visitVec(blobType, IDL.Nat8, "large_data")
636
+
637
+ expect(field.type).toBe("blob")
638
+ // Create a large Uint8Array
639
+ const largeData = new Uint8Array(513).fill(0xaa)
640
+ const blobResolved = field.resolve(largeData) as ResolvedNode<"blob">
641
+
642
+ expect(blobResolved.value).toBeInstanceOf(Uint8Array)
643
+ expect(blobResolved.displayType).toBe("blob")
644
+ expect(blobResolved.value).toHaveLength(513)
645
+ })
646
+
647
+ it("should handle nested vectors", () => {
648
+ const innerVec = IDL.Vec(IDL.Nat)
649
+ const nestedVecType = IDL.Vec(innerVec)
650
+ const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
651
+
652
+ expect(field.type).toBe("vector")
653
+ // Validate nested items via resolve
654
+ const nestedResolved = field.resolve([[BigInt(1)]]) as VectorNode
655
+ expect(nestedResolved.items[0].type).toBe("vector")
656
+ })
657
+
658
+ it("should handle vec of tuples (Map-like)", () => {
659
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
660
+ const mapType = IDL.Vec(tupleType)
661
+ const field = visitor.visitVec(
662
+ mapType,
663
+ tupleType,
664
+ "metadata"
665
+ ) as VectorNode
666
+
667
+ expect(field.type).toBe("vector")
668
+ // Validate by resolving a tuple item
669
+ const vecTupleResolved = field.resolve([["a", BigInt(1)]])
670
+ expect(vecTupleResolved.items[0].type).toBe("tuple")
671
+ const item = vecTupleResolved.items[0] as TupleNode
672
+ if (item.type === "tuple") {
673
+ expect(item.items).toHaveLength(2)
674
+ } else {
675
+ throw new Error("Item field is not tuple")
676
+ }
677
+ })
678
+ })
679
+
680
+ describe("Recursive Types", () => {
681
+ it("should handle recursive tree type", () => {
682
+ const Tree = IDL.Rec()
683
+ Tree.fill(
684
+ IDL.Variant({
685
+ Leaf: IDL.Nat,
686
+ Node: IDL.Record({
687
+ left: Tree,
688
+ right: Tree,
689
+ }),
690
+ })
691
+ )
692
+
693
+ const field = visitor.visitRec(
694
+ Tree,
695
+ IDL.Variant({
696
+ Leaf: IDL.Nat,
697
+ Node: IDL.Record({
698
+ left: Tree,
699
+ right: Tree,
700
+ }),
701
+ }),
702
+ "tree"
703
+ )
704
+
705
+ expect(field.type).toBe("recursive")
706
+ expect(field.candidType).toBe("rec")
707
+ expect(field.displayType).toBe("recursive")
708
+ // Resolve to inspect inner schema
709
+ const treeResolved = field.resolve({ Leaf: BigInt(1) }) as RecursiveNode
710
+ expect(treeResolved.inner.type).toBe("variant")
711
+ expect((treeResolved.inner as VariantNode).selected).toBe("Leaf")
712
+ })
713
+
714
+ it("should handle recursive linked list", () => {
715
+ const List = IDL.Rec()
716
+ List.fill(
717
+ IDL.Variant({
718
+ Nil: IDL.Null,
719
+ Cons: IDL.Record({
720
+ head: IDL.Nat,
721
+ tail: List,
722
+ }),
723
+ })
724
+ )
725
+
726
+ const field = visitor.visitRec(
727
+ List,
728
+ IDL.Variant({
729
+ Nil: IDL.Null,
730
+ Cons: IDL.Record({
731
+ head: IDL.Nat,
732
+ tail: List,
733
+ }),
734
+ }),
735
+ "list"
736
+ )
737
+
738
+ expect(field.type).toBe("recursive")
739
+
740
+ const listResolved = field.resolve({ Nil: null }) as RecursiveNode
741
+ expect(listResolved.inner.type).toBe("variant")
742
+ expect((listResolved.inner as VariantNode).selected).toBe("Nil")
743
+ })
744
+ })
745
+
746
+ // ════════════════════════════════════════════════════════════════════════
747
+ // Function Types
748
+ // ════════════════════════════════════════════════════════════════════════
749
+
750
+ describe("Function Types", () => {
751
+ it("should handle query function with single return", () => {
752
+ const funcType = IDL.Func([IDL.Principal], [IDL.Nat], ["query"])
753
+ const meta = visitor.visitFuncAsMethod(funcType, "get_balance")
754
+
755
+ expect(meta.functionType).toBe("query")
756
+ expect(meta.functionName).toBe("get_balance")
757
+ expect(meta.returnCount).toBe(1)
758
+ expect(meta.returns).toHaveLength(1)
759
+
760
+ expect(meta.returns).toHaveLength(1)
761
+
762
+ const returnField = meta.returns[0]
763
+ if (returnField.type === "number") {
764
+ expect(returnField.candidType).toBe("nat")
765
+ expect(returnField.displayType).toBe("string")
766
+ } else {
767
+ throw new Error("Return field is not number")
768
+ }
769
+ })
770
+
771
+ it("should handle update function with Result return", () => {
772
+ const funcType = IDL.Func(
773
+ [
774
+ IDL.Record({
775
+ to: IDL.Principal,
776
+ amount: IDL.Nat,
777
+ }),
778
+ ],
779
+ [
780
+ IDL.Variant({
781
+ Ok: IDL.Nat,
782
+ Err: IDL.Text,
783
+ }),
784
+ ],
785
+ []
786
+ )
787
+ const meta = visitor.visitFuncAsMethod(funcType, "transfer")
788
+
789
+ expect(meta.functionType).toBe("update")
790
+ expect(meta.returnCount).toBe(1)
791
+
792
+ const resultField = meta.returns[0]
793
+ if (resultField.type === "variant") {
794
+ expect(resultField.displayType).toBe("result")
795
+ } else {
796
+ throw new Error("Result field is not variant")
797
+ }
798
+ })
799
+
800
+ it("should handle function with multiple returns", () => {
801
+ const funcType = IDL.Func([], [IDL.Text, IDL.Nat, IDL.Bool], ["query"])
802
+ const meta = visitor.visitFuncAsMethod(funcType, "get_info")
803
+
804
+ expect(meta.returnCount).toBe(3)
805
+ expect(meta.returns).toHaveLength(3)
806
+ expect(meta.returns[0].type).toBe("text")
807
+ expect(meta.returns[1].type).toBe("number")
808
+ expect(meta.returns[2].type).toBe("boolean")
809
+ })
810
+
811
+ it("should handle function with no returns", () => {
812
+ const funcType = IDL.Func([IDL.Text], [], [])
813
+ const meta = visitor.visitFuncAsMethod(funcType, "log")
814
+
815
+ expect(meta.returnCount).toBe(0)
816
+ expect(meta.returns).toHaveLength(0)
817
+ })
818
+
819
+ // Tests for func type as data field (not method definition)
820
+ describe("Func as Data Field", () => {
821
+ it("should handle func type in archived_transactions callback", () => {
822
+ // This mimics the ICRC-1 ledger get_transactions response structure
823
+ // where archived_transactions contains a callback func reference
824
+ const GetBlocksRequest = IDL.Record({
825
+ start: IDL.Nat,
826
+ length: IDL.Nat,
827
+ })
828
+ const TransactionRange = IDL.Record({
829
+ transactions: IDL.Vec(IDL.Nat), // simplified
830
+ })
831
+ const ArchivedRange = IDL.Record({
832
+ callback: IDL.Func([GetBlocksRequest], [TransactionRange], ["query"]),
833
+ start: IDL.Nat,
834
+ length: IDL.Nat,
835
+ })
836
+ const GetTransactionsResponse = IDL.Record({
837
+ first_index: IDL.Nat,
838
+ log_length: IDL.Nat,
839
+ transactions: IDL.Vec(IDL.Nat),
840
+ archived_transactions: IDL.Vec(ArchivedRange),
841
+ })
842
+
843
+ // Create the func return type for get_transactions method
844
+ const funcType = IDL.Func(
845
+ [GetBlocksRequest],
846
+ [GetTransactionsResponse],
847
+ ["query"]
848
+ )
849
+ const meta = visitor.visitFuncAsMethod(funcType, "get_transactions")
850
+
851
+ expect(meta.functionType).toBe("query")
852
+ expect(meta.returnCount).toBe(1)
853
+
854
+ // The return should be a record containing archived_transactions
855
+ const returnField = meta.returns[0]
856
+ expect(returnField.type).toBe("record")
857
+
858
+ // Get the archived_transactions field
859
+ const recordField = returnField as RecordNode
860
+ const archivedTxField = recordField.fields["archived_transactions"]
861
+ expect(archivedTxField.type).toBe("vector")
862
+
863
+ // Resolve with actual data that mimics the canister response
864
+ const { Principal } = require("@icp-sdk/core/principal")
865
+ const testPrincipal = Principal.fromText("sa4so-piaaa-aaaar-qacnq-cai")
866
+
867
+ const mockResponse = {
868
+ first_index: BigInt(6000),
869
+ log_length: BigInt(7936),
870
+ transactions: [],
871
+ archived_transactions: [
872
+ {
873
+ callback: [testPrincipal, "get_transactions"],
874
+ start: BigInt(1),
875
+ length: BigInt(2),
876
+ },
877
+ ],
878
+ }
879
+
880
+ // This should NOT throw an error
881
+ const resolved = meta.resolve(mockResponse)
882
+ expect(resolved.results).toHaveLength(1)
883
+
884
+ const resolvedRecord = resolved.results[0] as RecordNode
885
+ const resolvedArchivedTx = resolvedRecord.fields[
886
+ "archived_transactions"
887
+ ] as VectorNode
888
+ expect(resolvedArchivedTx.items).toHaveLength(1)
889
+
890
+ // The archived transaction is a funcRecord (record with a single func field)
891
+ const firstArchivedTx = resolvedArchivedTx.items[0] as FuncRecordNode
892
+ expect(firstArchivedTx.type).toBe("funcRecord")
893
+ expect(firstArchivedTx.displayType).toBe("func-record")
894
+ expect(firstArchivedTx.canisterId).toBe("sa4so-piaaa-aaaar-qacnq-cai")
895
+ expect(firstArchivedTx.methodName).toBe("get_transactions")
896
+ expect(firstArchivedTx.funcType).toBe("query")
897
+ expect(firstArchivedTx.funcFieldKey).toBe("callback")
898
+ expect(firstArchivedTx.fields["callback"]).toBeDefined()
899
+ expect(firstArchivedTx.fields["start"]).toBeDefined()
900
+ expect(firstArchivedTx.fields["length"]).toBeDefined()
901
+ // argFields should exclude the callback func field
902
+ expect(firstArchivedTx.argFields["start"]).toBeDefined()
903
+ expect(firstArchivedTx.argFields["length"]).toBeDefined()
904
+ expect(firstArchivedTx.argFields["callback"]).toBeUndefined()
905
+ })
906
+
907
+ it("should handle standalone func field in record as funcRecord", () => {
908
+ const callbackFunc = IDL.Func([IDL.Text], [IDL.Bool], ["query"])
909
+ const recordType = IDL.Record({
910
+ name: IDL.Text,
911
+ callback: callbackFunc,
912
+ })
913
+
914
+ const field = visitor.visitRecord(
915
+ recordType,
916
+ [
917
+ ["name", IDL.Text],
918
+ ["callback", callbackFunc],
919
+ ],
920
+ "config"
921
+ )
922
+
923
+ // Should be detected as funcRecord since it has exactly one func field
924
+ expect(field.type).toBe("funcRecord")
925
+ expect(field.displayType).toBe("func-record")
926
+ const funcRecordField = field as FuncRecordNode
927
+ expect(funcRecordField.funcFieldKey).toBe("callback")
928
+ expect(funcRecordField.funcType).toBe("query")
929
+ expect(funcRecordField.fields["callback"]).toBeDefined()
930
+ expect(funcRecordField.fields["callback"].type).toBe("func")
931
+ expect(funcRecordField.fields["name"]).toBeDefined()
932
+ // argFields should only have non-func fields
933
+ expect(funcRecordField.argFields["name"]).toBeDefined()
934
+ expect(funcRecordField.argFields["callback"]).toBeUndefined()
935
+
936
+ // Resolve with mock data
937
+ const { Principal } = require("@icp-sdk/core/principal")
938
+ const resolved = funcRecordField.resolve({
939
+ name: "test",
940
+ callback: [Principal.fromText("aaaaa-aa"), "my_method"],
941
+ })
942
+
943
+ expect(resolved.fields["name"].value).toBe("test")
944
+ // funcRecord resolved provides canisterId & methodName at the top level
945
+ expect(resolved.canisterId).toBe("aaaaa-aa")
946
+ expect(resolved.methodName).toBe("my_method")
947
+
948
+ // Verify func field was properly resolved
949
+ const callbackResolved = resolved.fields["callback"] as FuncNode
950
+ expect(callbackResolved.type).toBe("func")
951
+ expect(callbackResolved.canisterId).toBe("aaaaa-aa")
952
+ expect(callbackResolved.methodName).toBe("my_method")
953
+
954
+ // argFields in resolved should contain only the non-func resolved fields
955
+ expect(resolved.argFields["name"]).toBeDefined()
956
+ expect(resolved.argFields["name"].value).toBe("test")
957
+ expect(resolved.argFields["callback"]).toBeUndefined()
958
+ })
959
+
960
+ it("should detect ArchivedBlocksRange pattern as funcRecord", () => {
961
+ // Exact pattern from the Candid:
962
+ // type ArchivedBlocksRange = record {
963
+ // callback : func (GetBlocksArgs) -> (Result_4) query;
964
+ // start : nat64;
965
+ // length : nat64;
966
+ // };
967
+ const GetBlocksArgs = IDL.Record({
968
+ start: IDL.Nat64,
969
+ length: IDL.Nat64,
970
+ })
971
+ const Result_4 = IDL.Variant({
972
+ Ok: IDL.Record({ blocks: IDL.Vec(IDL.Nat8) }),
973
+ Err: IDL.Text,
974
+ })
975
+
976
+ const ArchivedBlocksRange = IDL.Record({
977
+ callback: IDL.Func([GetBlocksArgs], [Result_4], ["query"]),
978
+ start: IDL.Nat64,
979
+ length: IDL.Nat64,
980
+ })
981
+
982
+ const field = visitor.visitRecord(
983
+ ArchivedBlocksRange,
984
+ [
985
+ ["callback", IDL.Func([GetBlocksArgs], [Result_4], ["query"])],
986
+ ["start", IDL.Nat64],
987
+ ["length", IDL.Nat64],
988
+ ],
989
+ "ArchivedBlocksRange"
990
+ )
991
+
992
+ // Schema-level checks
993
+ expect(field.type).toBe("funcRecord")
994
+ expect(field.displayType).toBe("func-record")
995
+ const fr = field as FuncRecordNode
996
+ expect(fr.funcFieldKey).toBe("callback")
997
+ expect(fr.funcType).toBe("query")
998
+ expect(fr.funcField.type).toBe("func")
999
+ expect(Object.keys(fr.argFields)).toEqual(["start", "length"])
1000
+
1001
+ // funcClass carries the raw IDL types for encoding/decoding
1002
+ expect(fr.funcClass.argTypes).toHaveLength(1)
1003
+ expect(fr.funcClass.retTypes).toHaveLength(1)
1004
+
1005
+ // Resolve with real-ish data
1006
+ const { Principal } = require("@icp-sdk/core/principal")
1007
+ const resolved = fr.resolve({
1008
+ callback: [
1009
+ Principal.fromText("qjdve-lqaaa-aaaaa-aaaeq-cai"),
1010
+ "get_blocks",
1011
+ ],
1012
+ start: BigInt(100),
1013
+ length: BigInt(50),
1014
+ })
1015
+ console.log("🚀 ~ resolved:", resolved)
1016
+
1017
+ // Top-level func info for easy consumption
1018
+ expect(resolved.canisterId).toBe("qjdve-lqaaa-aaaaa-aaaeq-cai")
1019
+ expect(resolved.methodName).toBe("get_blocks")
1020
+ expect(resolved.funcType).toBe("query")
1021
+
1022
+ // argFields provide the default call arguments
1023
+ expect(Object.keys(resolved.argFields)).toEqual(["start", "length"])
1024
+ expect(resolved.argFields["start"].raw).toBe(BigInt(100))
1025
+ expect(resolved.argFields["length"].raw).toBe(BigInt(50))
1026
+
1027
+ // defaultArgs: display-type args ready for callMethod
1028
+ expect(resolved.defaultArgs).toEqual([{ start: "100", length: "50" }])
1029
+ })
1030
+
1031
+ it("should keep record with multiple func fields as plain record", () => {
1032
+ const funcA = IDL.Func([IDL.Nat], [IDL.Nat], ["query"])
1033
+ const funcB = IDL.Func([IDL.Text], [IDL.Text], [])
1034
+
1035
+ const field = visitor.visitRecord(
1036
+ IDL.Record({ a: funcA, b: funcB }),
1037
+ [
1038
+ ["a", funcA],
1039
+ ["b", funcB],
1040
+ ],
1041
+ "multiFuncRecord"
1042
+ )
1043
+
1044
+ // Two func fields → stays as a regular record
1045
+ expect(field.type).toBe("record")
1046
+ expect(field.displayType).toBe("object")
1047
+ })
1048
+
1049
+ it("should keep record with zero func fields as plain record", () => {
1050
+ const field = visitor.visitRecord(
1051
+ IDL.Record({ x: IDL.Nat, y: IDL.Nat }),
1052
+ [
1053
+ ["x", IDL.Nat],
1054
+ ["y", IDL.Nat],
1055
+ ],
1056
+ "point"
1057
+ )
1058
+
1059
+ expect(field.type).toBe("record")
1060
+ expect(field.displayType).toBe("object")
1061
+ })
1062
+ })
1063
+ })
1064
+
1065
+ // ════════════════════════════════════════════════════════════════════════
1066
+ // Service Types
1067
+ // ════════════════════════════════════════════════════════════════════════
1068
+
1069
+ describe("Service Types", () => {
1070
+ it("should handle complete service", () => {
1071
+ const serviceType = IDL.Service({
1072
+ get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
1073
+ transfer: IDL.Func(
1074
+ [
1075
+ IDL.Record({
1076
+ to: IDL.Principal,
1077
+ amount: IDL.Nat,
1078
+ }),
1079
+ ],
1080
+ [
1081
+ IDL.Variant({
1082
+ Ok: IDL.Nat,
1083
+ Err: IDL.Text,
1084
+ }),
1085
+ ],
1086
+ []
1087
+ ),
1088
+ get_metadata: IDL.Func(
1089
+ [],
1090
+ [IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
1091
+ ["query"]
1092
+ ),
1093
+ })
1094
+
1095
+ const serviceMeta = visitor.visitService(serviceType)
1096
+
1097
+ expect(Object.keys(serviceMeta)).toHaveLength(3)
1098
+
1099
+ // Check get_balance
1100
+ const getBalanceMeta = serviceMeta["get_balance"]
1101
+ expect(getBalanceMeta.functionType).toBe("query")
1102
+ expect(getBalanceMeta.returnCount).toBe(1)
1103
+ expect(getBalanceMeta.returns[0].type).toBe("number")
1104
+ const balanceField = getBalanceMeta.returns[0]
1105
+ if (balanceField.type === "number") {
1106
+ expect(balanceField.displayType).toBe("string")
1107
+ } else {
1108
+ throw new Error("Balance field is not number")
1109
+ }
1110
+
1111
+ // Check transfer
1112
+ const transferMeta = serviceMeta["transfer"]
1113
+ expect(transferMeta.functionType).toBe("update")
1114
+ expect(transferMeta.returnCount).toBe(1)
1115
+ // Check get_metadata
1116
+ const getMetadataMeta = serviceMeta["get_metadata"]
1117
+ expect(getMetadataMeta.returnCount).toBe(1)
1118
+ expect(getMetadataMeta.returns[0].type).toBe("vector")
1119
+ })
1120
+ })
1121
+
1122
+ // ════════════════════════════════════════════════════════════════════════
1123
+ // Real-World Examples
1124
+ // ════════════════════════════════════════════════════════════════════════
1125
+
1126
+ describe("Real-World Examples", () => {
1127
+ it("should handle ICRC-1 balance_of return", () => {
1128
+ const funcType = IDL.Func(
1129
+ [
1130
+ IDL.Record({
1131
+ owner: IDL.Principal,
1132
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
1133
+ }),
1134
+ ],
1135
+ [IDL.Nat],
1136
+ ["query"]
1137
+ )
1138
+ const meta = visitor.visitFuncAsMethod(funcType, "icrc1_balance_of")
1139
+
1140
+ expect(meta.functionType).toBe("query")
1141
+ expect(meta.returnCount).toBe(1)
1142
+
1143
+ const balanceField = meta.returns[0]
1144
+ if (balanceField.type === "number") {
1145
+ expect(balanceField.candidType).toBe("nat")
1146
+ expect(balanceField.displayType).toBe("string")
1147
+ } else {
1148
+ throw new Error("Balance field is not number")
1149
+ }
1150
+ })
1151
+
1152
+ it("should handle ICRC-1 transfer return", () => {
1153
+ const TransferResult = IDL.Variant({
1154
+ Ok: IDL.Nat, // Block index
1155
+ Err: IDL.Variant({
1156
+ BadFee: IDL.Record({ expected_fee: IDL.Nat }),
1157
+ BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
1158
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
1159
+ TooOld: IDL.Null,
1160
+ CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
1161
+ Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
1162
+ TemporarilyUnavailable: IDL.Null,
1163
+ GenericError: IDL.Record({ error_code: IDL.Nat, message: IDL.Text }),
1164
+ }),
1165
+ })
1166
+
1167
+ const field = visitor.visitVariant(
1168
+ TransferResult,
1169
+ [
1170
+ ["Ok", IDL.Nat], // Block index
1171
+ [
1172
+ "Err",
1173
+ IDL.Variant({
1174
+ BadFee: IDL.Record({ expected_fee: IDL.Nat }),
1175
+ BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
1176
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
1177
+ TooOld: IDL.Null,
1178
+ CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
1179
+ Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
1180
+ TemporarilyUnavailable: IDL.Null,
1181
+ GenericError: IDL.Record({
1182
+ error_code: IDL.Nat,
1183
+ message: IDL.Text,
1184
+ }),
1185
+ }),
1186
+ ],
1187
+ ],
1188
+ "result"
1189
+ )
1190
+
1191
+ expect(field.displayType).toBe("result")
1192
+
1193
+ const okResolved = field.resolve({ Ok: BigInt(123) })
1194
+ if ((okResolved.selectedValue as ResolvedNode).type !== "number") {
1195
+ throw new Error("Ok field is not number")
1196
+ }
1197
+ expect((okResolved.selectedValue as ResolvedNode).candidType).toBe("nat")
1198
+ expect((okResolved.selectedValue as ResolvedNode).displayType).toBe(
1199
+ "string"
1200
+ )
1201
+
1202
+ const errResolved = field.resolve({
1203
+ Err: { InsufficientFunds: { balance: BigInt(0) } },
1204
+ })
1205
+ const innerErr = errResolved.selectedValue as ResolvedNode
1206
+ expect(innerErr.type).toBe("variant")
1207
+ const insufficient = (innerErr as any).resolve({
1208
+ InsufficientFunds: { balance: BigInt(0) },
1209
+ })
1210
+ expect(insufficient.selected).toBe("InsufficientFunds")
1211
+ const generic = (innerErr as any).resolve({
1212
+ GenericError: { error_code: BigInt(1), message: "err" },
1213
+ })
1214
+ expect(generic.selected).toBe("GenericError")
1215
+ })
1216
+
1217
+ it("should handle SNS canister status return", () => {
1218
+ const CanisterStatusResponse = IDL.Record({
1219
+ status: IDL.Variant({
1220
+ running: IDL.Null,
1221
+ stopping: IDL.Null,
1222
+ stopped: IDL.Null,
1223
+ }),
1224
+ settings: IDL.Record({
1225
+ controllers: IDL.Vec(IDL.Principal),
1226
+ compute_allocation: IDL.Nat,
1227
+ memory_allocation: IDL.Nat,
1228
+ freezing_threshold: IDL.Nat,
1229
+ }),
1230
+ module_hash: IDL.Opt(IDL.Vec(IDL.Nat8)),
1231
+ memory_size: IDL.Nat,
1232
+ cycles: IDL.Nat,
1233
+ idle_cycles_burned_per_day: IDL.Nat,
1234
+ })
1235
+
1236
+ const field = visitor.visitRecord(
1237
+ CanisterStatusResponse,
1238
+ [
1239
+ [
1240
+ "status",
1241
+ IDL.Variant({
1242
+ running: IDL.Null,
1243
+ stopping: IDL.Null,
1244
+ stopped: IDL.Null,
1245
+ }),
1246
+ ],
1247
+ [
1248
+ "settings",
1249
+ IDL.Record({
1250
+ controllers: IDL.Vec(IDL.Principal),
1251
+ compute_allocation: IDL.Nat,
1252
+ memory_allocation: IDL.Nat,
1253
+ freezing_threshold: IDL.Nat,
1254
+ }),
1255
+ ],
1256
+ ["module_hash", IDL.Opt(IDL.Vec(IDL.Nat8))],
1257
+ ["memory_size", IDL.Nat],
1258
+ ["cycles", IDL.Nat],
1259
+ ["idle_cycles_burned_per_day", IDL.Nat],
1260
+ ],
1261
+ "status"
1262
+ )
1263
+
1264
+ expect(field.type).toBe("record")
1265
+
1266
+ // Check status variant
1267
+ const statusField = field.fields["status"] as VariantNode
1268
+ if (!statusField || statusField.type !== "variant") {
1269
+ throw new Error("Status field not found or not variant")
1270
+ }
1271
+ expect(statusField.type).toBe("variant")
1272
+ expect(statusField.displayType).toBe("variant-null")
1273
+ // Validate via resolving options
1274
+ const statusResolved = statusField.resolve({ running: null })
1275
+ expect(statusResolved.selected).toBe("running")
1276
+
1277
+ // Check settings record
1278
+ const settingsField = field.fields["settings"] as RecordNode
1279
+ if (!settingsField || settingsField.type !== "record") {
1280
+ throw new Error("Settings field not found or not record")
1281
+ }
1282
+ expect(settingsField.type).toBe("record")
1283
+ expect(settingsField.displayType).toBe("object")
1284
+
1285
+ const controllersField = settingsField.fields["controllers"] as VectorNode
1286
+ if (!controllersField || controllersField.type !== "vector") {
1287
+ throw new Error("Controllers field not found or not vector")
1288
+ }
1289
+ expect(controllersField.type).toBe("vector")
1290
+ // Validate item type via resolve
1291
+ const controllersResolved = controllersField.resolve(["aaaaa-aa"])
1292
+ expect(controllersResolved.items[0].type).toBe("principal")
1293
+
1294
+ // Check cycles - should detect special format
1295
+ const cyclesField = field.fields["cycles"] as NumberNode
1296
+ if (!cyclesField || cyclesField.type !== "number") {
1297
+ throw new Error("Cycles field not found or not number")
1298
+ }
1299
+ expect(cyclesField.format).toBe("cycle")
1300
+
1301
+ // Check module_hash - optional blob
1302
+ const moduleHashField = field.fields["module_hash"] as OptionalNode
1303
+ expect(moduleHashField.type).toBe("optional")
1304
+ // Validate via resolving a sample blob
1305
+ const moduleHashResolved = moduleHashField.resolve([new Uint8Array([1])])
1306
+ expect((moduleHashResolved.value as ResolvedNode).type).toBe("blob")
1307
+ })
1308
+
1309
+ it("should handle complex governance proposal types", () => {
1310
+ const ProposalInfo = IDL.Record({
1311
+ id: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
1312
+ status: IDL.Nat32,
1313
+ topic: IDL.Nat32,
1314
+ failure_reason: IDL.Opt(
1315
+ IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
1316
+ ),
1317
+ ballots: IDL.Vec(
1318
+ IDL.Tuple(
1319
+ IDL.Nat64,
1320
+ IDL.Record({
1321
+ vote: IDL.Nat32,
1322
+ voting_power: IDL.Nat64,
1323
+ })
1324
+ )
1325
+ ),
1326
+ proposal_timestamp_seconds: IDL.Nat64,
1327
+ reward_event_round: IDL.Nat64,
1328
+ deadline_timestamp_seconds: IDL.Opt(IDL.Nat64),
1329
+ executed_timestamp_seconds: IDL.Nat64,
1330
+ reject_cost_e8s: IDL.Nat64,
1331
+ proposer: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
1332
+ reward_status: IDL.Nat32,
1333
+ })
1334
+
1335
+ const field = visitor.visitRecord(
1336
+ ProposalInfo,
1337
+ [
1338
+ ["id", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
1339
+ ["status", IDL.Nat32],
1340
+ ["topic", IDL.Nat32],
1341
+ [
1342
+ "failure_reason",
1343
+ IDL.Opt(
1344
+ IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
1345
+ ),
1346
+ ],
1347
+ [
1348
+ "ballots",
1349
+ IDL.Vec(
1350
+ IDL.Tuple(
1351
+ IDL.Nat64,
1352
+ IDL.Record({
1353
+ vote: IDL.Nat32,
1354
+ voting_power: IDL.Nat64,
1355
+ })
1356
+ )
1357
+ ),
1358
+ ],
1359
+ ["proposal_timestamp_seconds", IDL.Nat64],
1360
+ ["reward_event_round", IDL.Nat64],
1361
+ ["deadline_timestamp_seconds", IDL.Opt(IDL.Nat64)],
1362
+ ["executed_timestamp_seconds", IDL.Nat64],
1363
+ ["reject_cost_e8s: IDL.Nat64", IDL.Nat64], // Fixed label in original
1364
+ ["proposer", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
1365
+ ["reward_status", IDL.Nat32],
1366
+ ],
1367
+ "proposal"
1368
+ )
1369
+
1370
+ expect(field.type).toBe("record")
1371
+ expect(Object.keys(field.fields)).toHaveLength(12)
1372
+
1373
+ // Check timestamp field (note: the label pattern matching may not match "proposal_timestamp_seconds")
1374
+ const timestampField = field.fields["proposal_timestamp_seconds"]
1375
+ if (!timestampField || timestampField.type !== "number") {
1376
+ throw new Error("Timestamp field not found or not number")
1377
+ }
1378
+ expect(timestampField.type).toBe("number")
1379
+ expect(timestampField.displayType).toBe("string") // nat64 → string
1380
+
1381
+ // Check ballots - vec of tuples
1382
+ const ballotsField = field.fields["ballots"]
1383
+ if (!ballotsField || ballotsField.type !== "vector") {
1384
+ throw new Error("Ballots field not found or not vector")
1385
+ }
1386
+ expect(ballotsField.type).toBe("vector")
1387
+ // Validate via resolve
1388
+ const ballotsResolved = ballotsField.resolve([
1389
+ [BigInt(1), { vote: 1, voting_power: BigInt(2) }],
1390
+ ])
1391
+ expect(ballotsResolved.items).toHaveLength(1)
1392
+ const ballotTuple = ballotsResolved.items[0] as TupleNode
1393
+ if (ballotTuple.type !== "tuple") {
1394
+ throw new Error("Ballot item is not tuple")
1395
+ }
1396
+ expect(ballotTuple.items).toHaveLength(2)
1397
+ expect(ballotTuple.items[0].type).toBe("number") // nat64
1398
+ expect(ballotTuple.items[1].type).toBe("record") // ballot record
1399
+ })
1400
+ })
1401
+
1402
+ // ════════════════════════════════════════════════════════════════════════
1403
+ // Display Type Verification
1404
+ // ════════════════════════════════════════════════════════════════════════
1405
+
1406
+ describe("Display Type Verification", () => {
1407
+ it("should correctly map all types to their display types", () => {
1408
+ const testCases: Array<{ type: IDL.Type; expectedDisplay: string }> = [
1409
+ { type: IDL.Text, expectedDisplay: "string" },
1410
+ { type: IDL.Principal, expectedDisplay: "string" },
1411
+ { type: IDL.Nat, expectedDisplay: "string" },
1412
+ { type: IDL.Int, expectedDisplay: "string" },
1413
+ { type: IDL.Nat64, expectedDisplay: "string" },
1414
+ { type: IDL.Int64, expectedDisplay: "string" },
1415
+ { type: IDL.Nat8, expectedDisplay: "number" },
1416
+ { type: IDL.Nat16, expectedDisplay: "number" },
1417
+ { type: IDL.Nat32, expectedDisplay: "number" },
1418
+ { type: IDL.Int8, expectedDisplay: "number" },
1419
+ { type: IDL.Int16, expectedDisplay: "number" },
1420
+ { type: IDL.Int32, expectedDisplay: "number" },
1421
+ { type: IDL.Float32, expectedDisplay: "number" },
1422
+ { type: IDL.Float64, expectedDisplay: "number" },
1423
+ { type: IDL.Bool, expectedDisplay: "boolean" },
1424
+ { type: IDL.Null, expectedDisplay: "null" },
1425
+ { type: IDL.Vec(IDL.Nat8), expectedDisplay: "string" }, // blob → hex string
1426
+ { type: IDL.Vec(IDL.Text), expectedDisplay: "array" },
1427
+ { type: IDL.Opt(IDL.Text), expectedDisplay: "nullable" },
1428
+ { type: IDL.Record({ a: IDL.Text }), expectedDisplay: "object" },
1429
+ { type: IDL.Tuple(IDL.Text, IDL.Nat), expectedDisplay: "array" },
1430
+ ]
1431
+
1432
+ testCases.forEach(({ type, expectedDisplay }) => {
1433
+ const field = type.accept(visitor, "test")
1434
+ if (typeof field === "object" && "displayType" in field) {
1435
+ expect(field.displayType).toBe(expectedDisplay)
1436
+ } else {
1437
+ throw new Error("Expected as ResultNode")
1438
+ }
1439
+ })
1440
+ })
1441
+ })
1442
+
1443
+ // ════════════════════════════════════════════════════════════════════════
1444
+ // resolve() Method Tests
1445
+ // ════════════════════════════════════════════════════════════════════════
1446
+
1447
+ describe("resolve() Method", () => {
1448
+ describe("Primitive Types", () => {
1449
+ it("should resolve text field with value", () => {
1450
+ const field = visitor.visitText(IDL.Text, "message")
1451
+ const resolved = field.resolve("Hello World")
1452
+ expect(resolved.value).toBe("Hello World")
1453
+ })
1454
+
1455
+ it("should resolve number field with value", () => {
1456
+ const field = visitor.visitNat(IDL.Nat, "amount")
1457
+ const resolved = field.resolve(BigInt(1000000))
1458
+ expect(resolved.value).toBe("1000000")
1459
+ })
1460
+
1461
+ it("should resolve boolean field with value", () => {
1462
+ const field = visitor.visitBool(IDL.Bool, "active")
1463
+ const resolved = field.resolve(true)
1464
+ expect(resolved.value).toBe(true)
1465
+ })
1466
+
1467
+ it("should resolve null field", () => {
1468
+ const field = visitor.visitNull(IDL.Null, "empty")
1469
+ const resolved = field.resolve(null)
1470
+ expect(resolved.value).toBe(null)
1471
+ })
1472
+
1473
+ it("should resolve principal field with string value", () => {
1474
+ const field = visitor.visitPrincipal(IDL.Principal, "owner")
1475
+ const resolved = field.resolve("aaaaa-aa")
1476
+ expect(resolved.value).toBe("aaaaa-aa")
1477
+ })
1478
+ })
1479
+
1480
+ describe("Record Type", () => {
1481
+ it("should resolve record with nested field values", () => {
1482
+ const recordType = IDL.Record({
1483
+ name: IDL.Text,
1484
+ age: IDL.Nat32,
1485
+ active: IDL.Bool,
1486
+ })
1487
+ const field = visitor.visitRecord(
1488
+ recordType,
1489
+ [
1490
+ ["name", IDL.Text],
1491
+ ["age", IDL.Nat32],
1492
+ ["active", IDL.Bool],
1493
+ ],
1494
+ "user"
1495
+ )
1496
+
1497
+ const resolved = field.resolve({
1498
+ name: "Alice",
1499
+ age: 30,
1500
+ active: true,
1501
+ })
1502
+
1503
+ expect(resolved.type).toBe(field.type)
1504
+ const value = resolved.fields as Record<string, ResolvedNode>
1505
+ expect(value["name"].value).toBe("Alice")
1506
+ expect(value["age"].value).toBe(30)
1507
+ expect(value["active"].value).toBe(true)
1508
+ })
1509
+
1510
+ it("should handle null record value", () => {
1511
+ const recordType = IDL.Record({ name: IDL.Text })
1512
+ const field = visitor.visitRecord(
1513
+ recordType,
1514
+ [["name", IDL.Text]],
1515
+ "user"
1516
+ )
1517
+
1518
+ expect(() => field.resolve(null)).toThrow(
1519
+ "Expected record, but got null"
1520
+ )
1521
+ })
1522
+ })
1523
+
1524
+ describe("Variant Type", () => {
1525
+ it("should resolve variant with active option", () => {
1526
+ const variantType = IDL.Variant({
1527
+ Ok: IDL.Text,
1528
+ Err: IDL.Text,
1529
+ })
1530
+ const field = visitor.visitVariant(
1531
+ variantType,
1532
+ [
1533
+ ["Ok", IDL.Text],
1534
+ ["Err", IDL.Text],
1535
+ ],
1536
+ "result"
1537
+ )
1538
+
1539
+ const resolved = field.resolve({ Ok: "Success" })
1540
+ expect(resolved.type).toBe(field.type)
1541
+ expect(resolved.selected).toBe("Ok")
1542
+ const data = resolved.selectedValue as ResolvedNode
1543
+ expect(data.value).toBe("Success")
1544
+ })
1545
+
1546
+ it("should resolve variant with Err option", () => {
1547
+ const variantType = IDL.Variant({
1548
+ Ok: IDL.Nat,
1549
+ Err: IDL.Text,
1550
+ })
1551
+ const field = visitor.visitVariant(
1552
+ variantType,
1553
+ [
1554
+ ["Ok", IDL.Nat],
1555
+ ["Err", IDL.Text],
1556
+ ],
1557
+ "result"
1558
+ )
1559
+
1560
+ const resolved = field.resolve({ Err: "Something went wrong" })
1561
+
1562
+ expect(resolved.selected).toBe("Err")
1563
+ const data = resolved.selectedValue as ResolvedNode
1564
+ expect(data.value).toBe("Something went wrong")
1565
+ })
1566
+
1567
+ it("should handle null variant value", () => {
1568
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
1569
+ const field = visitor.visitVariant(
1570
+ variantType,
1571
+ [
1572
+ ["A", IDL.Null],
1573
+ ["B", IDL.Text],
1574
+ ],
1575
+ "choice"
1576
+ )
1577
+
1578
+ expect(() => field.resolve(null)).toThrow(
1579
+ "Expected variant, but got null"
1580
+ )
1581
+ })
1582
+ })
1583
+
1584
+ describe("Tuple Type", () => {
1585
+ it("should resolve tuple with indexed values", () => {
1586
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
1587
+ const field = visitor.visitTuple(
1588
+ tupleType,
1589
+ [IDL.Text, IDL.Nat, IDL.Bool],
1590
+ "data"
1591
+ )
1592
+
1593
+ const resolved = field.resolve(["hello", 123n, true])
1594
+
1595
+ expect(resolved.type).toBe(field.type)
1596
+ const value = resolved.items as ResolvedNode[]
1597
+ expect(value).toHaveLength(3)
1598
+ expect(value[0].value).toBe("hello")
1599
+ expect(value[1].value).toBe("123")
1600
+ expect(value[2].value).toBe(true)
1601
+ })
1602
+
1603
+ it("should handle null tuple value", () => {
1604
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
1605
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
1606
+
1607
+ expect(() => field.resolve(null)).toThrow(
1608
+ "Expected tuple, but got null"
1609
+ )
1610
+ })
1611
+ })
1612
+
1613
+ describe("Optional Type", () => {
1614
+ it("should resolve optional with value", () => {
1615
+ const optType = IDL.Opt(IDL.Text)
1616
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
1617
+
1618
+ const resolved = field.resolve(["Bob"])
1619
+
1620
+ expect(resolved.type).toBe(field.type)
1621
+ const inner = resolved.value as ResolvedNode
1622
+ expect(inner.value).toBe("Bob")
1623
+ })
1624
+
1625
+ it("should resolve optional with null", () => {
1626
+ const optType = IDL.Opt(IDL.Text)
1627
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
1628
+
1629
+ const resolved = field.resolve(null)
1630
+ expect(resolved.value).toBe(null)
1631
+ })
1632
+ })
1633
+
1634
+ describe("Vector Type", () => {
1635
+ it("should resolve vector with array of values", () => {
1636
+ const vecType = IDL.Vec(IDL.Text)
1637
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
1638
+
1639
+ const resolved = field.resolve(["a", "b", "c"]) as VectorNode
1640
+
1641
+ expect(resolved.type).toBe(field.type)
1642
+ const value = resolved.items as ResolvedNode[]
1643
+ expect(value).toHaveLength(3)
1644
+ expect(value[0].value).toBe("a")
1645
+ expect(value[1].value).toBe("b")
1646
+ expect(value[2].value).toBe("c")
1647
+ })
1648
+
1649
+ it("should handle empty vector", () => {
1650
+ const vecType = IDL.Vec(IDL.Nat)
1651
+ const field = visitor.visitVec(vecType, IDL.Nat, "numbers")
1652
+
1653
+ const resolved = field.resolve([]) as VectorNode
1654
+
1655
+ expect(resolved.type).toBe(field.type)
1656
+
1657
+ const value = resolved.items as ResolvedNode[]
1658
+ expect(value).toHaveLength(0)
1659
+ })
1660
+
1661
+ it("should handle null vector value", () => {
1662
+ const vecType = IDL.Vec(IDL.Text)
1663
+ const field = visitor.visitVec(vecType, IDL.Text, "items")
1664
+
1665
+ expect(() => field.resolve(null)).toThrow(
1666
+ "Expected vector, but got null"
1667
+ )
1668
+ })
1669
+ })
1670
+
1671
+ describe("Blob Type", () => {
1672
+ it("should resolve blob with hex string value", () => {
1673
+ const blobType = IDL.Vec(IDL.Nat8)
1674
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1675
+
1676
+ const resolved = field.resolve(new Uint8Array([0x12, 0x34, 0xab, 0xcd]))
1677
+
1678
+ expect(resolved.type).toBe(field.type)
1679
+ expect(resolved.value).toBe("1234abcd")
1680
+ })
1681
+ })
1682
+
1683
+ describe("Recursive Type", () => {
1684
+ it("should resolve recursive type", () => {
1685
+ const Node = IDL.Rec()
1686
+ Node.fill(
1687
+ IDL.Record({
1688
+ value: IDL.Nat,
1689
+ children: IDL.Vec(Node),
1690
+ })
1691
+ )
1692
+
1693
+ const field = visitor.visitRec(
1694
+ Node,
1695
+ // We can just pass null or the constructed type if available, but visitRec implementation calls extract which calls accept on the internal type.
1696
+ IDL.Record({
1697
+ value: IDL.Nat,
1698
+ children: IDL.Vec(Node),
1699
+ }),
1700
+ "tree"
1701
+ )
1702
+
1703
+ const resolved = field.resolve({
1704
+ value: BigInt(42),
1705
+ children: [],
1706
+ })
1707
+
1708
+ // The recursive type should delegate to its inner record type
1709
+ expect(resolved.type).toBe("recursive")
1710
+ })
1711
+ })
1712
+
1713
+ describe("Nested Structures", () => {
1714
+ it("should resolve deeply nested structure", () => {
1715
+ const nestedType = IDL.Record({
1716
+ user: IDL.Record({
1717
+ profile: IDL.Record({
1718
+ name: IDL.Text,
1719
+ verified: IDL.Bool,
1720
+ }),
1721
+ }),
1722
+ })
1723
+
1724
+ const field = visitor.visitRecord(
1725
+ nestedType,
1726
+ [
1727
+ [
1728
+ "user",
1729
+ IDL.Record({
1730
+ profile: IDL.Record({
1731
+ name: IDL.Text,
1732
+ verified: IDL.Bool,
1733
+ }),
1734
+ }),
1735
+ ],
1736
+ ],
1737
+ "data"
1738
+ )
1739
+
1740
+ const resolved = field.resolve({
1741
+ user: {
1742
+ profile: {
1743
+ name: "Alice",
1744
+ verified: true,
1745
+ },
1746
+ },
1747
+ })
1748
+
1749
+ const value = resolved.fields as Record<string, ResolvedNode>
1750
+ const userNode = value.user as RecordNode
1751
+ if (userNode.type !== "record") {
1752
+ throw new Error("User node is not a record")
1753
+ }
1754
+ const profileNode = userNode.fields["profile"] as RecordNode
1755
+ const profileFields = profileNode.fields as Record<string, ResolvedNode>
1756
+ expect(profileFields["name"].value).toBe("Alice")
1757
+ expect(profileFields["verified"].value).toBe(true)
1758
+ })
1759
+ })
1760
+ })
1761
+
1762
+ // ════════════════════════════════════════════════════════════════════════
1763
+ // resolve() Method Tests
1764
+ // ════════════════════════════════════════════════════════════════════════
1765
+
1766
+ describe("resolve() Method", () => {
1767
+ it("should generate metadata for single return value", () => {
1768
+ const service = IDL.Service({
1769
+ getName: IDL.Func([], [IDL.Text], ["query"]),
1770
+ })
1771
+
1772
+ const serviceMeta = visitor.visitService(
1773
+ service as unknown as IDL.ServiceClass
1774
+ )
1775
+ const methodMeta = serviceMeta["getName"]
1776
+
1777
+ const result = methodMeta.resolve("Alice")
1778
+
1779
+ expect(result.functionName).toBe("getName")
1780
+ expect(result.functionType).toBe("query")
1781
+ expect(result.results).toHaveLength(1)
1782
+ expect(result.results[0].value).toBe("Alice")
1783
+ expect(result.results[0].type).toBe("text")
1784
+ })
1785
+
1786
+ it("should generate metadata for multiple return values", () => {
1787
+ const service = IDL.Service({
1788
+ getStats: IDL.Func([], [IDL.Nat, IDL.Nat, IDL.Text], ["query"]),
1789
+ })
1790
+
1791
+ const serviceMeta = visitor.visitService(
1792
+ service as unknown as IDL.ServiceClass
1793
+ )
1794
+ const methodMeta = serviceMeta["getStats"]
1795
+
1796
+ const result = methodMeta.resolve([BigInt(100), BigInt(200), "active"])
1797
+
1798
+ expect(result.results).toHaveLength(3)
1799
+ expect(result.results[0].value).toBe("100")
1800
+ expect(result.results[0].type).toBe("number")
1801
+ expect(result.results[1].value).toBe("200")
1802
+ expect(result.results[2].value).toBe("active")
1803
+ expect(result.results[2].type).toBe("text")
1804
+ })
1805
+
1806
+ it("should generate metadata for record return value", () => {
1807
+ const service = IDL.Service({
1808
+ getUser: IDL.Func(
1809
+ [],
1810
+ [IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
1811
+ ["query"]
1812
+ ),
1813
+ })
1814
+
1815
+ const serviceMeta = visitor.visitService(
1816
+ service as unknown as IDL.ServiceClass
1817
+ )
1818
+ const methodMeta = serviceMeta["getUser"]
1819
+
1820
+ const result = methodMeta.resolve({
1821
+ name: "Bob",
1822
+ balance: BigInt(1000),
1823
+ })
1824
+
1825
+ expect(result.results).toHaveLength(1)
1826
+ expect(result.results[0].type).toBe("record")
1827
+
1828
+ const recordNode = result.results[0] as RecordNode
1829
+ if (recordNode.type !== "record") {
1830
+ throw new Error("Expected record node")
1831
+ }
1832
+ const val = recordNode.fields as Record<string, ResolvedNode>
1833
+ expect(val.name.value).toBe("Bob")
1834
+ expect(val.balance.value).toBe("1000")
1835
+ })
1836
+
1837
+ it("should generate metadata for Result variant", () => {
1838
+ const service = IDL.Service({
1839
+ transfer: IDL.Func(
1840
+ [],
1841
+ [IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
1842
+ []
1843
+ ),
1844
+ })
1845
+
1846
+ const serviceMeta = visitor.visitService(
1847
+ service as unknown as IDL.ServiceClass
1848
+ )
1849
+ const methodMeta = serviceMeta["transfer"]
1850
+
1851
+ // Test Ok case
1852
+ const okResult = methodMeta.resolve({ Ok: BigInt(12345) })
1853
+ expect(okResult.results[0].type).toBe("variant")
1854
+ if (okResult.results[0].type === "variant") {
1855
+ expect(okResult.results[0].displayType).toBe("result")
1856
+ } else {
1857
+ throw new Error("Expected variant field")
1858
+ }
1859
+
1860
+ const okValue = okResult.results[0] as ResolvedNode
1861
+ expect((okValue as any).selected).toBe("Ok")
1862
+ expect(((okValue as any).selectedValue as ResolvedNode).value).toBe(
1863
+ "12345"
1864
+ )
1865
+
1866
+ // Test Err case
1867
+ const errResult = methodMeta.resolve({
1868
+ Err: "Insufficient funds",
1869
+ })
1870
+ const errValue = errResult.results[0] as ResolvedNode
1871
+ expect((errValue as any).selected).toBe("Err")
1872
+ expect(((errValue as any).selectedValue as ResolvedNode).value).toBe(
1873
+ "Insufficient funds"
1874
+ )
1875
+ })
1876
+
1877
+ it("should generate metadata for optional return value", () => {
1878
+ const service = IDL.Service({
1879
+ findUser: IDL.Func([], [IDL.Opt(IDL.Text)], ["query"]),
1880
+ })
1881
+
1882
+ const serviceMeta = visitor.visitService(
1883
+ service as unknown as IDL.ServiceClass
1884
+ )
1885
+ const methodMeta = serviceMeta["findUser"]
1886
+
1887
+ // Test with value - Optional is [value]
1888
+ const foundResult = methodMeta.resolve(["Alice"])
1889
+ expect(foundResult.results[0].type).toBe("optional")
1890
+ const foundInner = foundResult.results[0] as OptionalNode
1891
+ expect(foundInner.value?.value).toBe("Alice")
1892
+
1893
+ // Test with null - optional is []
1894
+ const notFoundResult = methodMeta.resolve([])
1895
+ expect(notFoundResult.results[0].value).toBe(null)
1896
+ })
1897
+
1898
+ it("should generate metadata for vector return value", () => {
1899
+ const service = IDL.Service({
1900
+ getItems: IDL.Func([], [IDL.Vec(IDL.Text)], ["query"]),
1901
+ })
1902
+
1903
+ const serviceMeta = visitor.visitService(
1904
+ service as unknown as IDL.ServiceClass
1905
+ )
1906
+ const methodMeta = serviceMeta["getItems"]
1907
+
1908
+ const result = methodMeta.resolve(["item1", "item2", "item3"])
1909
+
1910
+ expect(result.results[0].type).toBe("vector")
1911
+ const vecNode = result.results[0] as ResolvedNode
1912
+ const vecItems = (vecNode as any).items as ResolvedNode[]
1913
+ expect(vecItems).toHaveLength(3)
1914
+ expect(vecItems[0].value).toBe("item1")
1915
+ expect(vecItems[1].value).toBe("item2")
1916
+ expect(vecItems[2].value).toBe("item3")
1917
+ })
1918
+
1919
+ it("should generate metadata for update function", () => {
1920
+ const service = IDL.Service({
1921
+ setName: IDL.Func([IDL.Text], [IDL.Bool], []),
1922
+ })
1923
+
1924
+ const serviceMeta = visitor.visitService(
1925
+ service as unknown as IDL.ServiceClass
1926
+ )
1927
+ const methodMeta = serviceMeta["setName"]
1928
+
1929
+ expect(methodMeta.functionType).toBe("update")
1930
+
1931
+ const rawData = [true]
1932
+ const result = methodMeta.resolve(rawData[0])
1933
+
1934
+ expect(result.functionType).toBe("update")
1935
+ expect(result.functionName).toBe("setName")
1936
+ expect(result.results[0].value).toBe(true)
1937
+ expect(result.results[0].raw).toBe(true)
1938
+ })
1939
+
1940
+ it("should generate metadata for complex ICRC-1 like response", () => {
1941
+ const Account = IDL.Record({
1942
+ owner: IDL.Principal,
1943
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
1944
+ })
1945
+
1946
+ const TransferResult = IDL.Variant({
1947
+ Ok: IDL.Nat,
1948
+ Err: IDL.Variant({
1949
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
1950
+ InvalidAccount: IDL.Null,
1951
+ TooOld: IDL.Null,
1952
+ }),
1953
+ })
1954
+
1955
+ const service = IDL.Service({
1956
+ icrc1_transfer: IDL.Func([], [TransferResult], []),
1957
+ })
1958
+
1959
+ const serviceMeta = visitor.visitService(
1960
+ service as unknown as IDL.ServiceClass
1961
+ )
1962
+ const methodMeta = serviceMeta["icrc1_transfer"]
1963
+
1964
+ // Test successful transfer
1965
+ const successResult = methodMeta.resolve({ Ok: BigInt(1000) })
1966
+
1967
+ const successValue = successResult.results[0] as ResolvedNode
1968
+ expect((successValue as any).selected).toBe("Ok")
1969
+ expect(((successValue as any).selectedValue as ResolvedNode).value).toBe(
1970
+ "1000"
1971
+ )
1972
+
1973
+ // Test error case
1974
+ const errorResult = methodMeta.resolve({
1975
+ Err: { InsufficientFunds: { balance: BigInt(50) } },
1976
+ })
1977
+ const errorValue = errorResult.results[0] as ResolvedNode
1978
+ expect((errorValue as any).selected).toBe("Err")
1979
+
1980
+ const val = (errorValue as any).selectedValue as ResolvedNode
1981
+ if (typeof val !== "object" || val === null || !("selected" in val)) {
1982
+ throw new Error("Expected variant value object")
1983
+ }
1984
+ expect((val as any).selected).toBe("InsufficientFunds")
1985
+ })
1986
+
1987
+ it("should handle empty return", () => {
1988
+ const service = IDL.Service({
1989
+ doSomething: IDL.Func([], [], []),
1990
+ })
1991
+
1992
+ const serviceMeta = visitor.visitService(
1993
+ service as unknown as IDL.ServiceClass
1994
+ )
1995
+ const methodMeta = serviceMeta["doSomething"]
1996
+
1997
+ expect(methodMeta.returnCount).toBe(0)
1998
+
1999
+ const result = methodMeta.resolve([])
2000
+ expect(result.results).toHaveLength(0)
2001
+ })
2002
+ })
2003
+
2004
+ // ════════════════════════════════════════════════════════════════════════════
2005
+ // resolve() Method Tests
2006
+ // ════════════════════════════════════════════════════════════════════════════
2007
+
2008
+ describe("resolve() Method", () => {
2009
+ it("should include both raw and display values for single return", () => {
2010
+ const service = IDL.Service({
2011
+ getBalance: IDL.Func([], [IDL.Nat], ["query"]),
2012
+ })
2013
+
2014
+ const serviceMeta = visitor.visitService(
2015
+ service as unknown as IDL.ServiceClass
2016
+ )
2017
+ const methodMeta = serviceMeta["getBalance"]
2018
+
2019
+ // Simulate raw BigInt and display string
2020
+ const rawData = [BigInt(1000000)]
2021
+
2022
+ const result = methodMeta.resolve(rawData[0])
2023
+
2024
+ expect(result.functionName).toBe("getBalance")
2025
+ expect(result.functionType).toBe("query")
2026
+ expect(result.results).toHaveLength(1)
2027
+ expect(result.results[0].raw).toBe(BigInt(1000000))
2028
+ expect(result.results[0].value).toBe("1000000")
2029
+ expect(result.results[0].raw).toBe(BigInt(1000000))
2030
+ expect(result.results[0].type).toBe("number")
2031
+ })
2032
+
2033
+ it("should include both raw and display values for multiple returns", () => {
2034
+ const service = IDL.Service({
2035
+ getStats: IDL.Func([], [IDL.Nat64, IDL.Text, IDL.Bool], ["query"]),
2036
+ })
2037
+
2038
+ const serviceMeta = visitor.visitService(
2039
+ service as unknown as IDL.ServiceClass
2040
+ )
2041
+ const methodMeta = serviceMeta["getStats"]
2042
+
2043
+ // Use BigInt with string to safe safe integer
2044
+ const rawData = [BigInt("9007199254740993"), "active", true]
2045
+
2046
+ const result = methodMeta.resolve(rawData)
2047
+
2048
+ expect(result.results).toHaveLength(3)
2049
+
2050
+ // nat64 → string display, BigInt raw
2051
+ expect(result.results[0].value).toBe("9007199254740993")
2052
+ expect(result.results[0].raw).toBe(BigInt("9007199254740993"))
2053
+ expect(result.results[0].candidType).toBe("nat64")
2054
+
2055
+ // text → same for both
2056
+ expect(result.results[1].value).toBe("active")
2057
+ expect(result.results[1].raw).toBe("active")
2058
+
2059
+ // bool → same for both
2060
+ expect(result.results[2].value).toBe(true)
2061
+ expect(result.results[2].raw).toBe(true)
2062
+ })
2063
+
2064
+ it("should handle record with raw and display values", () => {
2065
+ const service = IDL.Service({
2066
+ getUser: IDL.Func(
2067
+ [],
2068
+ [IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
2069
+ ["query"]
2070
+ ),
2071
+ })
2072
+
2073
+ const serviceMeta = visitor.visitService(
2074
+ service as unknown as IDL.ServiceClass
2075
+ )
2076
+ const methodMeta = serviceMeta["getUser"]
2077
+
2078
+ const rawData = [{ name: "Alice", balance: BigInt(500) }]
2079
+ const result = methodMeta.resolve(rawData[0])
2080
+
2081
+ expect(result.results[0].raw).toEqual({
2082
+ name: "Alice",
2083
+ balance: BigInt(500),
2084
+ })
2085
+
2086
+ const recordNode = result.results[0] as RecordNode
2087
+ if (recordNode.type !== "record") {
2088
+ throw new Error("Expected record node")
2089
+ }
2090
+ const val = recordNode.fields as Record<string, ResolvedNode>
2091
+ expect(val.name.value).toBe("Alice")
2092
+ expect(val.balance.value).toBe("500")
2093
+ })
2094
+
2095
+ it("should handle Result variant with raw and display values", () => {
2096
+ const service = IDL.Service({
2097
+ transfer: IDL.Func(
2098
+ [],
2099
+ [IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
2100
+ []
2101
+ ),
2102
+ })
2103
+
2104
+ const serviceMeta = visitor.visitService(
2105
+ service as unknown as IDL.ServiceClass
2106
+ )
2107
+ const methodMeta = serviceMeta["transfer"]
2108
+
2109
+ // Test Ok case with raw BigInt
2110
+ const rawData = [{ Ok: BigInt(12345) }]
2111
+
2112
+ const result = methodMeta.resolve(rawData[0])
2113
+
2114
+ expect(result.results[0].raw).toEqual({ Ok: BigInt(12345) })
2115
+
2116
+ const variantValue = result.results[0] as ResolvedNode
2117
+ expect((variantValue as any).selected).toBe("Ok")
2118
+ const innerVal = (variantValue as any).selectedValue as ResolvedNode
2119
+ expect(innerVal.value).toBe("12345")
2120
+ })
2121
+
2122
+ it("should preserve raw Principal object", () => {
2123
+ const { Principal } = require("@icp-sdk/core/principal")
2124
+
2125
+ const service = IDL.Service({
2126
+ getOwner: IDL.Func([], [IDL.Principal], ["query"]),
2127
+ })
2128
+
2129
+ const serviceMeta = visitor.visitService(
2130
+ service as unknown as IDL.ServiceClass
2131
+ )
2132
+ const methodMeta = serviceMeta["getOwner"]
2133
+
2134
+ const principal = Principal.fromText("aaaaa-aa")
2135
+ const rawData = [principal]
2136
+
2137
+ const result = methodMeta.resolve(rawData[0])
2138
+
2139
+ expect(result.results[0].value).toBe("aaaaa-aa")
2140
+ expect(result.results[0].raw).toBe(principal)
2141
+ expect(result.results[0].type).toBe("principal")
2142
+ })
2143
+
2144
+ it("should handle vector with raw and display values", () => {
2145
+ const service = IDL.Service({
2146
+ getAmounts: IDL.Func([], [IDL.Vec(IDL.Nat)], ["query"]),
2147
+ })
2148
+
2149
+ const serviceMeta = visitor.visitService(
2150
+ service as unknown as IDL.ServiceClass
2151
+ )
2152
+ const methodMeta = serviceMeta["getAmounts"]
2153
+
2154
+ const rawData = [[BigInt(100), BigInt(200), BigInt(300)]]
2155
+
2156
+ const result = methodMeta.resolve(rawData[0])
2157
+
2158
+ const vecNode = result.results[0] as ResolvedNode
2159
+ const vecValue = (vecNode as any).items as ResolvedNode[]
2160
+ expect(vecValue).toHaveLength(3)
2161
+ expect(vecValue[0].value).toBe("100")
2162
+ expect(vecValue[1].value).toBe("200")
2163
+ expect(vecValue[2].value).toBe("300")
2164
+ })
2165
+
2166
+ it("should handle optional with raw and display values", () => {
2167
+ const service = IDL.Service({
2168
+ findBalance: IDL.Func([], [IDL.Opt(IDL.Nat)], ["query"]),
2169
+ })
2170
+
2171
+ const serviceMeta = visitor.visitService(
2172
+ service as unknown as IDL.ServiceClass
2173
+ )
2174
+ const methodMeta = serviceMeta["findBalance"]
2175
+
2176
+ // Test with value - Optional is [value]
2177
+ const rawDataWithValue = [[BigInt(999)]]
2178
+
2179
+ const resultWithValue = methodMeta.resolve(rawDataWithValue[0])
2180
+
2181
+ expect(resultWithValue.results[0].raw).toEqual([BigInt(999)])
2182
+ const innerValue = resultWithValue.results[0].value
2183
+ if (
2184
+ typeof innerValue !== "object" ||
2185
+ innerValue === null ||
2186
+ !("value" in innerValue)
2187
+ ) {
2188
+ throw new Error("Expected optional value object")
2189
+ }
2190
+ expect((innerValue as any).value).toBe("999")
2191
+
2192
+ // Test with null - Optional is []
2193
+ const rawDataNull = [[]]
2194
+
2195
+ const resultNull = methodMeta.resolve(rawDataNull[0])
2196
+ expect(resultNull.results[0].raw).toEqual([])
2197
+ expect(resultNull.results[0].value).toBe(null)
2198
+ })
2199
+
2200
+ it("should handle empty return", () => {
2201
+ const service = IDL.Service({
2202
+ doNothing: IDL.Func([], [], []),
2203
+ })
2204
+
2205
+ const serviceMeta = visitor.visitService(
2206
+ service as unknown as IDL.ServiceClass
2207
+ )
2208
+ const methodMeta = serviceMeta["doNothing"]
2209
+
2210
+ const result = methodMeta.resolve([])
2211
+
2212
+ expect(result.results).toHaveLength(0)
2213
+ })
2214
+ })
2215
+ })
2216
+
2217
+ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
2218
+ const visitor = new ResultFieldVisitor()
2219
+
2220
+ it("should handle record data provided as an array (tuple-like) even if IDL has names", () => {
2221
+ const Rule = IDL.Variant({
2222
+ Quorum: IDL.Record({
2223
+ min_approved: IDL.Nat,
2224
+ approvers: IDL.Variant({ Group: IDL.Vec(IDL.Principal) }),
2225
+ }),
2226
+ })
2227
+
2228
+ const NamedRule = IDL.Record({
2229
+ description: IDL.Opt(IDL.Text),
2230
+ id: IDL.Text,
2231
+ name: IDL.Text,
2232
+ rule: Rule,
2233
+ })
2234
+
2235
+ const field = visitor.visitRecord(
2236
+ NamedRule,
2237
+ [
2238
+ ["description", IDL.Opt(IDL.Text)],
2239
+ ["id", IDL.Text],
2240
+ ["name", IDL.Text],
2241
+ ["rule", Rule],
2242
+ ],
2243
+ "NamedRule"
2244
+ )
2245
+
2246
+ // Simulate lib-agent returning an array for a named record
2247
+ const arrayData = [
2248
+ [], // description (empty opt)
2249
+ "1253ec41-ef1d-4317-bb82-2366fc34f37c", // id
2250
+ "Admin approval", // name
2251
+ { Quorum: { min_approved: BigInt(1), approvers: { Group: [] } } }, // rule
2252
+ ]
2253
+
2254
+ // Before the fix, this would throw because arrayData["rule"] is undefined
2255
+ const resolved = field.resolve(arrayData)
2256
+
2257
+ expect(resolved.fields.id.value).toBe(
2258
+ "1253ec41-ef1d-4317-bb82-2366fc34f37c"
2259
+ )
2260
+ expect(resolved.fields.name.value).toBe("Admin approval")
2261
+
2262
+ const ruleNode = resolved.fields.rule as VariantNode
2263
+ expect(ruleNode.selected).toBe("Quorum")
2264
+ })
2265
+
2266
+ it("should handle already transformed variant data with _type", () => {
2267
+ const Rule = IDL.Variant({
2268
+ Quorum: IDL.Text,
2269
+ })
2270
+
2271
+ const field = visitor.visitVariant(Rule, [["Quorum", IDL.Text]], "Rule")
2272
+
2273
+ // Data transformed by DisplayCodecVisitor
2274
+ const transformedData = {
2275
+ _type: "Quorum",
2276
+ Quorum: "some text",
2277
+ }
2278
+
2279
+ // Before the fix, this would throw because transformedData["Quorum"] is there but
2280
+ // Object.keys(transformedData)[0] is "_type"
2281
+ const resolved = field.resolve(transformedData)
2282
+
2283
+ expect(resolved.selected).toBe("Quorum")
2284
+ expect(resolved.selectedValue.value).toBe("some text")
2285
+ })
2286
+
2287
+ it("should handle already transformed optional data (unwrapped)", () => {
2288
+ const OptText = IDL.Opt(IDL.Text)
2289
+ const field = visitor.visitOpt(OptText, IDL.Text, "maybeText")
2290
+
2291
+ // Raw format is [value]
2292
+ expect(field.resolve(["hello"]).value?.value).toBe("hello")
2293
+ expect(field.resolve([]).value).toBeNull()
2294
+
2295
+ // Transformed format is just the value
2296
+ expect(field.resolve("hello").value?.value).toBe("hello")
2297
+ expect(field.resolve(null).value).toBeNull()
2298
+ expect(field.resolve(undefined).value).toBeNull()
2299
+ })
2300
+
2301
+ it("should provide a better error message when an option is missing", () => {
2302
+ const MyVariant = IDL.Variant({ A: IDL.Null, B: IDL.Null })
2303
+ const field = visitor.visitVariant(
2304
+ MyVariant,
2305
+ [
2306
+ ["A", IDL.Null],
2307
+ ["B", IDL.Null],
2308
+ ],
2309
+ "MyVariant"
2310
+ )
2311
+
2312
+ expect(() => field.resolve({ C: null })).toThrow(
2313
+ /Option "C" not found. Available: A, B/
2314
+ )
2315
+ })
2316
+
2317
+ describe("Recursive types", () => {
2318
+ it("should handle recursive variant with transformed data", () => {
2319
+ const Rule = IDL.Rec()
2320
+ const RuleType = IDL.Variant({
2321
+ Quorum: IDL.Record({
2322
+ min_approved: IDL.Nat,
2323
+ }),
2324
+ Nested: Rule,
2325
+ })
2326
+ Rule.fill(RuleType)
2327
+
2328
+ const field = visitor.visitRec(Rule, RuleType, "rule")
2329
+
2330
+ // Transformed data (using _type)
2331
+ const transformedData = {
2332
+ _type: "Quorum",
2333
+ Quorum: {
2334
+ min_approved: "1",
2335
+ },
2336
+ }
2337
+
2338
+ // This should work because Rule.resolve calls Variant.resolve
2339
+ const resolved = field.resolve(transformedData) as RecursiveNode
2340
+ const variantNode = resolved.inner as VariantNode
2341
+
2342
+ expect(variantNode.selected).toBe("Quorum")
2343
+ expect(variantNode.selectedValue.type).toBe("record")
2344
+ })
2345
+
2346
+ it("should handle deeply nested recursive variant with transformed data", () => {
2347
+ const Rule = IDL.Rec()
2348
+ const RuleType = IDL.Variant({
2349
+ Quorum: IDL.Record({
2350
+ min_approved: IDL.Nat,
2351
+ }),
2352
+ Nested: Rule,
2353
+ })
2354
+ Rule.fill(RuleType)
2355
+
2356
+ const field = visitor.visitRec(Rule, RuleType, "rule")
2357
+
2358
+ const transformedData = {
2359
+ _type: "Nested",
2360
+ Nested: {
2361
+ _type: "Quorum",
2362
+ Quorum: {
2363
+ min_approved: "2",
2364
+ },
2365
+ },
2366
+ }
2367
+
2368
+ const resolved = field.resolve(transformedData) as RecursiveNode
2369
+ const variantNode = resolved.inner as VariantNode
2370
+ expect(variantNode.selected).toBe("Nested")
2371
+
2372
+ const nestedResolved = variantNode.selectedValue as RecursiveNode
2373
+ const innerVariant = nestedResolved.inner as VariantNode
2374
+ expect(innerVariant.selected).toBe("Quorum")
2375
+ })
2376
+ })
2377
+ })