@ic-reactor/candid 3.0.11-beta.1 → 3.0.12-beta.0

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 (40) hide show
  1. package/dist/display-reactor.d.ts +1 -2
  2. package/dist/display-reactor.d.ts.map +1 -1
  3. package/dist/display-reactor.js +1 -1
  4. package/dist/display-reactor.js.map +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata-display-reactor.d.ts +3 -3
  9. package/dist/metadata-display-reactor.d.ts.map +1 -1
  10. package/dist/visitor/arguments/index.d.ts +58 -39
  11. package/dist/visitor/arguments/index.d.ts.map +1 -1
  12. package/dist/visitor/arguments/index.js +273 -81
  13. package/dist/visitor/arguments/index.js.map +1 -1
  14. package/dist/visitor/arguments/types.d.ts +228 -45
  15. package/dist/visitor/arguments/types.d.ts.map +1 -1
  16. package/dist/visitor/arguments/types.js +40 -1
  17. package/dist/visitor/arguments/types.js.map +1 -1
  18. package/dist/visitor/helpers.d.ts +1 -1
  19. package/dist/visitor/helpers.d.ts.map +1 -1
  20. package/dist/visitor/returns/index.d.ts +3 -3
  21. package/dist/visitor/returns/index.d.ts.map +1 -1
  22. package/dist/visitor/returns/index.js +54 -15
  23. package/dist/visitor/returns/index.js.map +1 -1
  24. package/dist/visitor/types.d.ts +2 -3
  25. package/dist/visitor/types.d.ts.map +1 -1
  26. package/dist/visitor/types.js +1 -2
  27. package/dist/visitor/types.js.map +1 -1
  28. package/package.json +6 -3
  29. package/src/display-reactor.ts +4 -6
  30. package/src/index.ts +0 -1
  31. package/src/metadata-display-reactor.ts +6 -6
  32. package/src/visitor/arguments/README.md +230 -0
  33. package/src/visitor/arguments/index.test.ts +144 -51
  34. package/src/visitor/arguments/index.ts +351 -146
  35. package/src/visitor/arguments/schema.test.ts +215 -0
  36. package/src/visitor/arguments/types.ts +287 -61
  37. package/src/visitor/helpers.ts +1 -1
  38. package/src/visitor/returns/index.test.ts +163 -1
  39. package/src/visitor/returns/index.ts +62 -16
  40. package/src/visitor/types.ts +2 -3
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect } from "vitest"
2
- import { IDL } from "../types"
3
2
  import { ResultFieldVisitor } from "./index"
4
3
  import type {
5
4
  ResultNode,
@@ -19,6 +18,7 @@ import type {
19
18
  MethodMeta,
20
19
  ServiceMeta,
21
20
  } from "./types"
21
+ import { IDL } from "@icp-sdk/core/candid"
22
22
 
23
23
  describe("ResultFieldVisitor", () => {
24
24
  const visitor = new ResultFieldVisitor()
@@ -1974,3 +1974,165 @@ describe("ResultFieldVisitor", () => {
1974
1974
  })
1975
1975
  })
1976
1976
  })
1977
+
1978
+ describe("ResultFieldVisitor Reproduction - User reported issue", () => {
1979
+ const visitor = new ResultFieldVisitor()
1980
+
1981
+ it("should handle record data provided as an array (tuple-like) even if IDL has names", () => {
1982
+ const Rule = IDL.Variant({
1983
+ Quorum: IDL.Record({
1984
+ min_approved: IDL.Nat,
1985
+ approvers: IDL.Variant({ Group: IDL.Vec(IDL.Principal) }),
1986
+ }),
1987
+ })
1988
+
1989
+ const NamedRule = IDL.Record({
1990
+ description: IDL.Opt(IDL.Text),
1991
+ id: IDL.Text,
1992
+ name: IDL.Text,
1993
+ rule: Rule,
1994
+ })
1995
+
1996
+ const field = visitor.visitRecord(
1997
+ NamedRule,
1998
+ [
1999
+ ["description", IDL.Opt(IDL.Text)],
2000
+ ["id", IDL.Text],
2001
+ ["name", IDL.Text],
2002
+ ["rule", Rule],
2003
+ ],
2004
+ "NamedRule"
2005
+ )
2006
+
2007
+ // Simulate lib-agent returning an array for a named record
2008
+ const arrayData = [
2009
+ [], // description (empty opt)
2010
+ "1253ec41-ef1d-4317-bb82-2366fc34f37c", // id
2011
+ "Admin approval", // name
2012
+ { Quorum: { min_approved: BigInt(1), approvers: { Group: [] } } }, // rule
2013
+ ]
2014
+
2015
+ // Before the fix, this would throw because arrayData["rule"] is undefined
2016
+ const resolved = field.resolve(arrayData)
2017
+
2018
+ expect(resolved.fields.id.value).toBe(
2019
+ "1253ec41-ef1d-4317-bb82-2366fc34f37c"
2020
+ )
2021
+ expect(resolved.fields.name.value).toBe("Admin approval")
2022
+
2023
+ const ruleNode = resolved.fields.rule as VariantNode
2024
+ expect(ruleNode.selected).toBe("Quorum")
2025
+ })
2026
+
2027
+ it("should handle already transformed variant data with _type", () => {
2028
+ const Rule = IDL.Variant({
2029
+ Quorum: IDL.Text,
2030
+ })
2031
+
2032
+ const field = visitor.visitVariant(Rule, [["Quorum", IDL.Text]], "Rule")
2033
+
2034
+ // Data transformed by DisplayCodecVisitor
2035
+ const transformedData = {
2036
+ _type: "Quorum",
2037
+ Quorum: "some text",
2038
+ }
2039
+
2040
+ // Before the fix, this would throw because transformedData["Quorum"] is there but
2041
+ // Object.keys(transformedData)[0] is "_type"
2042
+ const resolved = field.resolve(transformedData)
2043
+
2044
+ expect(resolved.selected).toBe("Quorum")
2045
+ expect(resolved.selectedOption.value).toBe("some text")
2046
+ })
2047
+
2048
+ it("should handle already transformed optional data (unwrapped)", () => {
2049
+ const OptText = IDL.Opt(IDL.Text)
2050
+ const field = visitor.visitOpt(OptText, IDL.Text, "maybeText")
2051
+
2052
+ // Raw format is [value]
2053
+ expect(field.resolve(["hello"]).value?.value).toBe("hello")
2054
+ expect(field.resolve([]).value).toBeNull()
2055
+
2056
+ // Transformed format is just the value
2057
+ expect(field.resolve("hello").value?.value).toBe("hello")
2058
+ expect(field.resolve(null).value).toBeNull()
2059
+ expect(field.resolve(undefined).value).toBeNull()
2060
+ })
2061
+
2062
+ it("should provide a better error message when an option is missing", () => {
2063
+ const MyVariant = IDL.Variant({ A: IDL.Null, B: IDL.Null })
2064
+ const field = visitor.visitVariant(
2065
+ MyVariant,
2066
+ [
2067
+ ["A", IDL.Null],
2068
+ ["B", IDL.Null],
2069
+ ],
2070
+ "MyVariant"
2071
+ )
2072
+
2073
+ expect(() => field.resolve({ C: null })).toThrow(
2074
+ /Option C not found in variant MyVariant. Available options: A, B/
2075
+ )
2076
+ })
2077
+
2078
+ describe("Recursive types", () => {
2079
+ it("should handle recursive variant with transformed data", () => {
2080
+ const Rule = IDL.Rec()
2081
+ const RuleType = IDL.Variant({
2082
+ Quorum: IDL.Record({
2083
+ min_approved: IDL.Nat,
2084
+ }),
2085
+ Nested: Rule,
2086
+ })
2087
+ Rule.fill(RuleType)
2088
+
2089
+ const field = visitor.visitRec(Rule, RuleType, "rule")
2090
+
2091
+ // Transformed data (using _type)
2092
+ const transformedData = {
2093
+ _type: "Quorum",
2094
+ Quorum: {
2095
+ min_approved: "1",
2096
+ },
2097
+ }
2098
+
2099
+ // This should work because Rule.resolve calls Variant.resolve
2100
+ const resolved = field.resolve(transformedData) as RecursiveNode
2101
+ const variantNode = resolved.inner as VariantNode
2102
+
2103
+ expect(variantNode.selected).toBe("Quorum")
2104
+ expect(variantNode.selectedOption.type).toBe("record")
2105
+ })
2106
+
2107
+ it("should handle deeply nested recursive variant with transformed data", () => {
2108
+ const Rule = IDL.Rec()
2109
+ const RuleType = IDL.Variant({
2110
+ Quorum: IDL.Record({
2111
+ min_approved: IDL.Nat,
2112
+ }),
2113
+ Nested: Rule,
2114
+ })
2115
+ Rule.fill(RuleType)
2116
+
2117
+ const field = visitor.visitRec(Rule, RuleType, "rule")
2118
+
2119
+ const transformedData = {
2120
+ _type: "Nested",
2121
+ Nested: {
2122
+ _type: "Quorum",
2123
+ Quorum: {
2124
+ min_approved: "2",
2125
+ },
2126
+ },
2127
+ }
2128
+
2129
+ const resolved = field.resolve(transformedData) as RecursiveNode
2130
+ const variantNode = resolved.inner as VariantNode
2131
+ expect(variantNode.selected).toBe("Nested")
2132
+
2133
+ const nestedResolved = variantNode.selectedOption as RecursiveNode
2134
+ const innerVariant = nestedResolved.inner as VariantNode
2135
+ expect(innerVariant.selected).toBe("Quorum")
2136
+ })
2137
+ })
2138
+ })
@@ -1,6 +1,5 @@
1
1
  import { isQuery } from "../helpers"
2
2
  import { checkTextFormat, checkNumberFormat } from "../constants"
3
- import { IDL } from "../types"
4
3
  import type {
5
4
  ResultNode,
6
5
  ResolvedNode,
@@ -12,8 +11,8 @@ import type {
12
11
  TextFormat,
13
12
  } from "./types"
14
13
 
15
- export * from "./types"
16
- import { sha256 } from "@noble/hashes/sha2"
14
+ import { sha256 } from "@noble/hashes/sha2.js"
15
+ import { IDL } from "@icp-sdk/core/candid"
17
16
  import { DisplayCodecVisitor, uint8ArrayToHex } from "@ic-reactor/core"
18
17
  import type {
19
18
  ActorMethodReturnType,
@@ -48,11 +47,17 @@ function primitiveNode<T extends NodeType>(
48
47
  displayType,
49
48
  ...extras,
50
49
  resolve(data: unknown): ResolvedNode<T> {
51
- return {
52
- ...node,
53
- value: codec.decode(data),
54
- raw: data,
55
- } as unknown as ResolvedNode<T>
50
+ try {
51
+ return {
52
+ ...node,
53
+ value: codec.decode(data),
54
+ raw: data,
55
+ } as unknown as ResolvedNode<T>
56
+ } catch (e) {
57
+ throw new Error(
58
+ `Failed to decode field "${label}" of type ${type} (${candidType}) with data: ${JSON.stringify(data)}. Error: ${e}`
59
+ )
60
+ }
56
61
  },
57
62
  } as unknown as ResultNode<T>
58
63
  return node
@@ -68,8 +73,19 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
68
73
  > {
69
74
  private codec = new DisplayCodecVisitor()
70
75
 
76
+ private recCache = new Map<IDL.RecClass<any>, ResultNode<"recursive">>()
77
+
71
78
  private getCodec(t: IDL.Type): Codec {
72
- return t.accept(this.codec, null) as Codec
79
+ const codec = t.accept(this.codec, null) as any
80
+ return {
81
+ decode: (v: unknown) => {
82
+ try {
83
+ return typeof codec?.decode === "function" ? codec.decode(v) : v
84
+ } catch {
85
+ return v
86
+ }
87
+ },
88
+ }
73
89
  }
74
90
 
75
91
  // ══════════════════════════════════════════════════════════════════════════
@@ -138,8 +154,20 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
138
154
  }
139
155
  const recordData = data as Record<string, unknown>
140
156
  const resolvedFields: Record<string, ResolvedNode> = {}
157
+ let index = 0
141
158
  for (const [key, field] of Object.entries(fields)) {
142
- resolvedFields[key] = field.resolve(recordData[key])
159
+ // Try named key first, then try numeric index (for tuples/indexed records)
160
+ const value =
161
+ recordData[key] !== undefined ? recordData[key] : recordData[index]
162
+
163
+ if (!field || typeof field.resolve !== "function") {
164
+ throw new Error(
165
+ `Field "${key}" in record "${label}" is not a valid ResultNode. Got: ${JSON.stringify(field)}`
166
+ )
167
+ }
168
+
169
+ resolvedFields[key] = field.resolve(value)
170
+ index++
143
171
  }
144
172
  return { ...node, fields: resolvedFields, raw: data }
145
173
  },
@@ -170,10 +198,15 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
170
198
  )
171
199
  }
172
200
  const variantData = data as Record<string, unknown>
173
- const selected = Object.keys(variantData)[0]
201
+ // Support both raw { Selected: value } and transformed { _type: 'Selected', Selected: value }
202
+ const selected =
203
+ (variantData._type as string) || Object.keys(variantData)[0]
174
204
  const optionNode = selectedOption[selected]
205
+
175
206
  if (!optionNode) {
176
- throw new Error(`Option ${selected} not found in variant`)
207
+ throw new Error(
208
+ `Option ${selected} not found in variant ${label}. Available options: ${Object.keys(selectedOption).join(", ")}`
209
+ )
177
210
  }
178
211
  return {
179
212
  ...node,
@@ -230,9 +263,16 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
230
263
  displayType: "nullable",
231
264
  value: null, // null until resolved
232
265
  resolve(data: unknown): ResolvedNode<"optional"> {
233
- const opt = data as T[]
234
- const resolved =
235
- Array.isArray(opt) && opt.length > 0 ? inner.resolve(opt[0]) : null
266
+ // If data is an array (raw format [T] or []), unwrap it.
267
+ // Otherwise, use data directly (already transformed or null/undefined).
268
+ const resolved = Array.isArray(data)
269
+ ? data.length > 0
270
+ ? inner.resolve(data[0])
271
+ : null
272
+ : data !== null && data !== undefined
273
+ ? inner.resolve(data)
274
+ : null
275
+
236
276
  return { ...node, value: resolved, raw: data }
237
277
  },
238
278
  }
@@ -300,10 +340,14 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
300
340
  }
301
341
 
302
342
  public visitRec<T>(
303
- _t: IDL.RecClass<T>,
343
+ t: IDL.RecClass<T>,
304
344
  ty: IDL.ConstructType<T>,
305
345
  label: string
306
346
  ): ResultNode<"recursive"> {
347
+ if (this.recCache.has(t)) {
348
+ return this.recCache.get(t)! as ResultNode<"recursive">
349
+ }
350
+
307
351
  const self = this
308
352
  // Lazy extraction to prevent infinite loops
309
353
  let innerSchema: ResultNode | null = null
@@ -320,6 +364,8 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
320
364
  return { ...node, inner: getInner().resolve(data), raw: data }
321
365
  },
322
366
  }
367
+
368
+ this.recCache.set(t, node)
323
369
  return node
324
370
  }
325
371
 
@@ -17,10 +17,9 @@ export type FieldType =
17
17
  | "boolean"
18
18
  | "null"
19
19
 
20
- export { IDL } from "@icp-sdk/core/candid"
21
- export { Principal } from "@icp-sdk/core/principal"
20
+ export type { Principal } from "@icp-sdk/core/principal"
22
21
 
23
- import { IDL } from "@icp-sdk/core/candid"
22
+ import type { IDL } from "@icp-sdk/core/candid"
24
23
  export type AllNumberTypes =
25
24
  | IDL.NatClass
26
25
  | IDL.IntClass