@ic-reactor/candid 3.0.8-beta.2 → 3.0.10-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.
@@ -2,25 +2,17 @@ import { isQuery } from "../helpers"
2
2
  import { checkTextFormat, checkNumberFormat } from "../constants"
3
3
  import { IDL } from "../types"
4
4
  import type {
5
- ResultField,
6
- ResultFieldWithValue,
7
- RecordResultField,
8
- VariantResultField,
9
- TupleResultField,
10
- OptionalResultField,
11
- VectorResultField,
12
- BlobResultField,
13
- RecursiveResultField,
14
- PrincipalResultField,
15
- NumberResultField,
16
- TextResultField,
17
- BooleanResultField,
18
- NullResultField,
19
- UnknownResultField,
20
- MethodResultMeta,
21
- ServiceResultMeta,
5
+ ResultNode,
6
+ ResolvedNode,
7
+ NodeType,
8
+ MethodMeta,
9
+ ServiceMeta,
22
10
  ResolvedMethodResult,
11
+ NumberFormat,
12
+ TextFormat,
23
13
  } from "./types"
14
+
15
+ export * from "./types"
24
16
  import { DisplayCodecVisitor } from "@ic-reactor/core"
25
17
  import type {
26
18
  ActorMethodReturnType,
@@ -31,523 +23,385 @@ import type {
31
23
 
32
24
  export * from "./types"
33
25
 
26
+ // ════════════════════════════════════════════════════════════════════════════
27
+ // Node Factory - Eliminates Boilerplate
28
+ // ════════════════════════════════════════════════════════════════════════════
29
+
30
+ type Codec = { decode: (v: unknown) => unknown }
31
+
34
32
  /**
35
- * ResultFieldVisitor generates metadata for displaying method results from Candid IDL types.
36
- *
37
- * ## Design Principles
38
- *
39
- * 1. **Works with raw IDL types BEFORE transformation** - generates metadata at initialization
40
- * 2. **No value dependencies** - metadata describes structure, not specific values
41
- * 3. **Describes display format** - includes hints for how values will appear after transformation
42
- * 4. **Efficient** - single traversal, reusable metadata
43
- * 5. **Resolvable** - metadata can be combined with values at runtime via .resolve(val)
44
- *
45
- * ## Key Insight: Metadata vs Values
46
- *
47
- * The visitor generates a "schema" that describes:
48
- * - What type each field is in Candid (nat, Principal, opt, etc.)
49
- * - What it becomes after display transformation (string, null, etc.)
50
- * - How it should be formatted (timestamp, cycle, hex, etc.)
51
- *
52
- * Values are NOT passed during traversal. Instead, the generated schema
53
- * is used at render time to properly display transformed values.
54
- *
55
- * ## Display Transformations (applied by DisplayCodecVisitor)
56
- *
57
- * | Candid Type | Display Type | Notes |
58
- * |-------------|--------------|-------|
59
- * | nat, int, nat64, int64 | string | BigInt → string |
60
- * | Principal | string | Principal.toText() |
61
- * | opt T | T \| null | [value] → value, [] → null |
62
- * | vec nat8 (blob) | string | Uint8Array → hex string |
63
- * | variant { Ok, Err } | unwrapped | { Ok: val } → val (or throws on Err) |
64
- *
65
- * @example
66
- * ```typescript
67
- * const visitor = new ResultFieldVisitor()
68
- * const serviceMeta = service.accept(visitor, null)
69
- *
70
- * // Get method result metadata
71
- * const methodMeta = serviceMeta["icrc1_balance_of"]
72
- * // methodMeta.resultFields[0] = {
73
- * // type: "number",
74
- * // candidType: "nat",
75
- * // displayType: "string",
76
- * // numberFormat: "normal"
77
- * // }
78
- *
79
- * // At render time, apply to transformed value:
80
- * const transformedResult = "1000000000" // Already transformed by DisplayCodec
81
- * renderField(methodMeta.resultFields[0], transformedResult)
82
- * ```
33
+ * Creates a primitive node with automatic resolve implementation.
83
34
  */
35
+ function primitiveNode<T extends NodeType>(
36
+ type: T,
37
+ label: string,
38
+ candidType: string,
39
+ displayType: ResultNode["displayType"],
40
+ codec: Codec,
41
+ extras: object = {}
42
+ ): ResultNode<T> {
43
+ const node: ResultNode<T> = {
44
+ type,
45
+ label,
46
+ candidType,
47
+ displayType,
48
+ ...extras,
49
+ resolve(data: unknown): ResolvedNode<T> {
50
+ return {
51
+ ...node,
52
+ value: codec.decode(data),
53
+ raw: data,
54
+ } as unknown as ResolvedNode<T>
55
+ },
56
+ } as unknown as ResultNode<T>
57
+ return node
58
+ }
59
+
60
+ // ════════════════════════════════════════════════════════════════════════════
61
+ // Simplified Result Field Visitor
62
+ // ════════════════════════════════════════════════════════════════════════════
63
+
84
64
  export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
85
65
  string,
86
- ResultField | MethodResultMeta<A> | ServiceResultMeta<A>
66
+ ResultNode | MethodMeta<A> | ServiceMeta<A>
87
67
  > {
88
- private displayVisitor = new DisplayCodecVisitor()
68
+ private codec = new DisplayCodecVisitor()
89
69
 
90
- // ════════════════════════════════════════════════════════════════════════
91
- // Service & Function Level
92
- // ════════════════════════════════════════════════════════════════════════
70
+ private getCodec(t: IDL.Type): Codec {
71
+ return t.accept(this.codec, null) as Codec
72
+ }
93
73
 
94
- public visitService(t: IDL.ServiceClass): ServiceResultMeta<A> {
95
- const result = {} as ServiceResultMeta<A>
74
+ // ══════════════════════════════════════════════════════════════════════════
75
+ // Service & Function
76
+ // ══════════════════════════════════════════════════════════════════════════
96
77
 
97
- for (const [functionName, func] of t._fields) {
98
- result[functionName as FunctionName<A>] = func.accept(
99
- this,
100
- functionName
101
- ) as MethodResultMeta<A>
78
+ public visitService(t: IDL.ServiceClass): ServiceMeta<A> {
79
+ const result = {} as ServiceMeta<A>
80
+ for (const [name, func] of t._fields) {
81
+ result[name as FunctionName<A>] = func.accept(this, name) as MethodMeta<A>
102
82
  }
103
-
104
83
  return result
105
84
  }
106
85
 
107
86
  public visitFunc(
108
87
  t: IDL.FuncClass,
109
88
  functionName: FunctionName<A>
110
- ): MethodResultMeta<A> {
89
+ ): MethodMeta<A> {
111
90
  const functionType: FunctionType = isQuery(t) ? "query" : "update"
112
-
113
- const resultFields = t.retTypes.map((retType, index) =>
114
- retType.accept(this, `__ret${index}`)
115
- ) as ResultField[]
116
-
117
- const generateMetadata = (
118
- data: ActorMethodReturnType<A[FunctionName<A>]>
119
- ): ResolvedMethodResult<A> => {
120
- const dataArray: unknown[] =
121
- resultFields.length === 0
122
- ? []
123
- : resultFields.length === 1
124
- ? [data]
125
- : (data as unknown[])
126
-
127
- const results = resultFields.map((field, index) =>
128
- field.resolve(dataArray[index])
129
- )
130
-
131
- return {
132
- functionType,
133
- functionName,
134
- results,
135
- raw: data,
136
- }
137
- }
91
+ const returns = t.retTypes.map((ret, i) =>
92
+ ret.accept(this, `__ret${i}`)
93
+ ) as ResultNode[]
138
94
 
139
95
  return {
140
96
  functionType,
141
97
  functionName,
142
- resultFields,
98
+ returns,
143
99
  returnCount: t.retTypes.length,
144
- generateMetadata,
100
+ resolve: (
101
+ data: ActorMethodReturnType<A[FunctionName<A>]>
102
+ ): ResolvedMethodResult<A> => {
103
+ const dataArray = returns.length <= 1 ? [data] : (data as unknown[])
104
+ return {
105
+ functionType,
106
+ functionName,
107
+ results: returns.map((node, i) => node.resolve(dataArray[i])),
108
+ raw: data,
109
+ }
110
+ },
145
111
  }
146
112
  }
147
113
 
148
- // ════════════════════════════════════════════════════════════════════════
114
+ // ══════════════════════════════════════════════════════════════════════════
149
115
  // Compound Types
150
- // ════════════════════════════════════════════════════════════════════════
116
+ // ══════════════════════════════════════════════════════════════════════════
151
117
 
152
118
  public visitRecord(
153
119
  _t: IDL.RecordClass,
154
120
  fields_: Array<[string, IDL.Type]>,
155
121
  label: string
156
- ): RecordResultField {
157
- const fields = fields_.map(
158
- ([key, type]) => type.accept(this, key) as ResultField
159
- )
122
+ ): ResultNode<"record"> {
123
+ const fields: Record<string, ResultNode> = {}
124
+ for (const [key, type] of fields_) {
125
+ fields[key] = type.accept(this, key) as ResultNode
126
+ }
160
127
 
161
- const field: RecordResultField = {
128
+ const node: ResultNode<"record"> = {
162
129
  type: "record",
163
130
  label,
164
131
  candidType: "record",
165
132
  displayType: "object",
166
133
  fields,
167
- resolve(value: unknown): ResultFieldWithValue {
168
- const record = value as Record<string, unknown> | null | undefined
169
- if (record == null) {
170
- return { field, value: null, raw: value }
134
+ resolve(data: unknown): ResolvedNode<"record"> {
135
+ if (data === null || data === undefined) {
136
+ throw new Error(`Expected record for field ${label}, but got ${data}`)
171
137
  }
172
-
173
- const resolvedFields: Record<string, ResultFieldWithValue> = {}
174
- for (const f of fields) {
175
- resolvedFields[f.label] = f.resolve(record[f.label])
138
+ const recordData = data as Record<string, unknown>
139
+ const resolvedFields: Record<string, ResolvedNode> = {}
140
+ for (const [key, field] of Object.entries(fields)) {
141
+ resolvedFields[key] = field.resolve(recordData[key])
176
142
  }
177
-
178
- return { field, value: resolvedFields, raw: value }
143
+ return { ...node, fields: resolvedFields, raw: data }
179
144
  },
180
145
  }
181
- return field
146
+ return node
182
147
  }
183
148
 
184
149
  public visitVariant(
185
150
  _t: IDL.VariantClass,
186
151
  fields_: Array<[string, IDL.Type]>,
187
152
  label: string
188
- ): VariantResultField {
189
- const options: string[] = []
190
- const optionFields: ResultField[] = []
191
-
153
+ ): ResultNode<"variant"> {
154
+ const selectedOption: Record<string, ResultNode> = {}
192
155
  for (const [key, type] of fields_) {
193
- options.push(key)
194
- optionFields.push(type.accept(this, key) as ResultField)
156
+ selectedOption[key] = type.accept(this, key) as ResultNode
195
157
  }
196
-
197
- // Detect if this is a Result type (has Ok and Err options)
198
- const isResult = options.includes("Ok") && options.includes("Err")
199
- const displayType = isResult ? "result" : "variant"
200
-
201
- const field: VariantResultField = {
158
+ const isResult = "Ok" in selectedOption && "Err" in selectedOption
159
+ const node: ResultNode<"variant"> = {
202
160
  type: "variant",
203
161
  label,
204
162
  candidType: "variant",
205
- displayType,
206
- options,
207
- optionFields,
208
- resolve(value: unknown): ResultFieldWithValue {
209
- if (value == null) {
210
- return { field, value: null, raw: value }
163
+ displayType: isResult ? "result" : "variant",
164
+ selectedOption: {} as ResultNode, // placeholder, populated on resolve
165
+ resolve(data: unknown): ResolvedNode<"variant"> {
166
+ if (data === null || data === undefined) {
167
+ throw new Error(
168
+ `Expected variant for field ${label}, but got ${data}`
169
+ )
211
170
  }
212
-
213
- const variant = value as Record<string, unknown>
214
- const optionsInValue = Object.keys(variant)
215
- const activeOption = optionsInValue.find((opt) => options.includes(opt))
216
-
217
- if (!activeOption) {
218
- return { field, value: null, raw: value }
171
+ const variantData = data as Record<string, unknown>
172
+ const selected = Object.keys(variantData)[0]
173
+ const optionNode = selectedOption[selected]
174
+ if (!optionNode) {
175
+ throw new Error(`Option ${selected} not found in variant`)
219
176
  }
220
-
221
- const activeValue = variant[activeOption]
222
- const optionIndex = options.indexOf(activeOption)
223
- const optionField = optionFields[optionIndex]
224
-
225
- const specificField: VariantResultField = {
226
- ...field,
227
- options,
228
- optionFields: [optionField],
229
- }
230
-
231
177
  return {
232
- field: specificField,
233
- value: {
234
- option: activeOption,
235
- value: optionField?.resolve(activeValue) ?? {
236
- field: optionField,
237
- value: activeValue,
238
- raw: activeValue,
239
- },
240
- },
241
- raw: value,
178
+ ...node,
179
+ selected,
180
+ selectedOption: optionNode.resolve(variantData[selected]),
181
+ raw: data,
242
182
  }
243
183
  },
244
184
  }
245
-
246
- return field
185
+ return node
247
186
  }
248
187
 
249
188
  public visitTuple<T extends IDL.Type[]>(
250
189
  _t: IDL.TupleClass<T>,
251
190
  components: IDL.Type[],
252
191
  label: string
253
- ): TupleResultField {
254
- const fields = components.map(
255
- (type, index) => type.accept(this, `_${index}`) as ResultField
192
+ ): ResultNode<"tuple"> {
193
+ const items = components.map(
194
+ (t, i) => t.accept(this, `_${i}`) as ResultNode
256
195
  )
257
196
 
258
- const field: TupleResultField = {
197
+ const node: ResultNode<"tuple"> = {
259
198
  type: "tuple",
260
199
  label,
261
200
  candidType: "tuple",
262
201
  displayType: "array",
263
- fields,
264
- resolve(value: unknown): ResultFieldWithValue {
265
- const tuple = value as unknown[] | null | undefined
266
- if (tuple == null) {
267
- return { field, value: null, raw: value }
202
+ items,
203
+ resolve(data: unknown): ResolvedNode<"tuple"> {
204
+ if (data === null || data === undefined) {
205
+ throw new Error(`Expected tuple for field ${label}, but got ${data}`)
206
+ }
207
+ const tupleData = data as unknown[]
208
+ return {
209
+ ...node,
210
+ items: items.map((item, i) => item.resolve(tupleData[i])),
211
+ raw: data,
268
212
  }
269
-
270
- const resolvedItems = fields.map((f, index) => f.resolve(tuple[index]))
271
- return { field, value: resolvedItems, raw: value }
272
213
  },
273
214
  }
274
-
275
- return field
215
+ return node
276
216
  }
277
217
 
278
218
  public visitOpt<T>(
279
219
  _t: IDL.OptClass<T>,
280
220
  ty: IDL.Type<T>,
281
221
  label: string
282
- ): OptionalResultField {
283
- const innerField = ty.accept(this, label) as ResultField
222
+ ): ResultNode<"optional"> {
223
+ const inner = ty.accept(this, label) as ResultNode
284
224
 
285
- const field: OptionalResultField = {
225
+ const node: ResultNode<"optional"> = {
286
226
  type: "optional",
287
227
  label,
288
228
  candidType: "opt",
289
229
  displayType: "nullable",
290
- innerField,
291
- resolve(value: unknown): ResultFieldWithValue {
292
- const opt = value as [unknown] | [] | null | undefined
293
- if (opt == null || opt.length === 0) {
294
- return { field, value: null, raw: value }
295
- }
296
- return { field, value: innerField.resolve(opt[0]), raw: value }
230
+ value: null, // null until resolved
231
+ resolve(data: unknown): ResolvedNode<"optional"> {
232
+ const opt = data as T[]
233
+ const resolved =
234
+ Array.isArray(opt) && opt.length > 0 ? inner.resolve(opt[0]) : null
235
+ return { ...node, value: resolved, raw: data }
297
236
  },
298
237
  }
299
-
300
- return field
238
+ return node
301
239
  }
302
240
 
303
241
  public visitVec<T>(
304
242
  _t: IDL.VecClass<T>,
305
243
  ty: IDL.Type<T>,
306
244
  label: string
307
- ): VectorResultField | BlobResultField {
308
- // Check if it's blob (vec nat8)
245
+ ): ResultNode<"vector"> | ResultNode<"blob"> {
246
+ // Blob detection (vec nat8)
309
247
  if (ty instanceof IDL.FixedNatClass && ty._bits === 8) {
310
- const codec = _t.accept(this.displayVisitor, null) as any
311
- const blobField: BlobResultField = {
248
+ const codec = this.getCodec(_t)
249
+ const node: ResultNode<"blob"> = {
312
250
  type: "blob",
313
251
  label,
314
252
  candidType: "blob",
315
- displayType: "string", // Transformed to hex string
316
- displayHint: "hex",
317
- resolve(value: unknown): ResultFieldWithValue {
318
- return {
319
- field: blobField,
320
- value: codec.decode(value),
321
- raw: value,
322
- }
253
+ displayType: "string",
254
+ value: "", // empty schema placeholder, populated on resolve
255
+ resolve(data: unknown): ResolvedNode<"blob"> {
256
+ return { ...node, value: codec.decode(data) as string, raw: data }
323
257
  },
324
258
  }
325
- return blobField
259
+ return node
326
260
  }
327
261
 
328
- const itemField = ty.accept(this, "item") as ResultField
262
+ const itemSchema = ty.accept(this, "item") as ResultNode
329
263
 
330
- const field: VectorResultField = {
264
+ const node: ResultNode<"vector"> = {
331
265
  type: "vector",
332
266
  label,
333
267
  candidType: "vec",
334
268
  displayType: "array",
335
- itemField,
336
- resolve(value: unknown): ResultFieldWithValue {
337
- const vec = value as unknown[] | null | undefined
338
- if (vec == null) {
339
- return { field, value: null, raw: value }
269
+ items: [], // empty schema placeholder, populated on resolve
270
+ resolve(data: unknown): ResolvedNode<"vector"> {
271
+ if (data === null || data === undefined) {
272
+ throw new Error(`Expected vector for field ${label}, but got ${data}`)
273
+ }
274
+ const vectorData = data as unknown[]
275
+ return {
276
+ ...node,
277
+ items: vectorData.map((v) => itemSchema.resolve(v)),
278
+ raw: data,
340
279
  }
341
-
342
- const resolvedItems = vec.map((item) => itemField.resolve(item))
343
- return { field, value: resolvedItems, raw: value }
344
280
  },
345
281
  }
346
-
347
- return field
282
+ return node
348
283
  }
349
284
 
350
285
  public visitRec<T>(
351
- t: IDL.RecClass<T>,
286
+ _t: IDL.RecClass<T>,
352
287
  ty: IDL.ConstructType<T>,
353
288
  label: string
354
- ): RecursiveResultField {
355
- // eslint-disable-next-line @typescript-eslint/no-this-alias
289
+ ): ResultNode<"recursive"> {
356
290
  const self = this
291
+ // Lazy extraction to prevent infinite loops
292
+ let innerSchema: ResultNode | null = null
293
+ const getInner = () =>
294
+ (innerSchema ??= ty.accept(self, label) as ResultNode)
357
295
 
358
- const field: RecursiveResultField = {
296
+ const node: ResultNode<"recursive"> = {
359
297
  type: "recursive",
360
298
  label,
361
299
  candidType: "rec",
362
300
  displayType: "recursive",
363
- typeName: t.name,
364
- // Lazy extraction to prevent infinite loops
365
- extract: () => ty.accept(self, label) as ResultField,
366
- resolve(value: unknown): ResultFieldWithValue {
367
- // Extract the inner field and resolve with it
368
- const innerField = field.extract()
369
- return innerField.resolve(value)
301
+ inner: {} as ResultNode, // placeholder, populated on resolve
302
+ resolve(data: unknown): ResolvedNode<"recursive"> {
303
+ return { ...node, inner: getInner().resolve(data), raw: data }
370
304
  },
371
305
  }
372
-
373
- return field
306
+ return node
374
307
  }
375
308
 
376
- // ════════════════════════════════════════════════════════════════════════
377
- // Primitive Types
378
- // ════════════════════════════════════════════════════════════════════════
309
+ // ══════════════════════════════════════════════════════════════════════════
310
+ // Primitives - Using Factory
311
+ // ══════════════════════════════════════════════════════════════════════════
379
312
 
380
313
  public visitPrincipal(
381
314
  t: IDL.PrincipalClass,
382
315
  label: string
383
- ): PrincipalResultField {
384
- const codec = t.accept(this.displayVisitor, null) as any
385
- const field: PrincipalResultField = {
386
- type: "principal",
316
+ ): ResultNode<"principal"> {
317
+ return primitiveNode(
318
+ "principal",
387
319
  label,
388
- candidType: "principal",
389
- displayType: "string", // Principal.toText()
390
- textFormat: checkTextFormat(label),
391
- resolve(value: unknown): ResultFieldWithValue {
392
- return {
393
- field,
394
- value: codec.decode(value),
395
- raw: value,
396
- }
397
- },
398
- }
399
-
400
- return field
320
+ "principal",
321
+ "string",
322
+ this.getCodec(t),
323
+ {
324
+ format: checkTextFormat(label) as TextFormat,
325
+ }
326
+ )
401
327
  }
402
328
 
403
- public visitText(t: IDL.TextClass, label: string): TextResultField {
404
- const codec = t.accept(this.displayVisitor, null) as any
405
- const field: TextResultField = {
406
- type: "text",
407
- label,
408
- candidType: "text",
409
- displayType: "string",
410
- textFormat: checkTextFormat(label),
411
- resolve(value: unknown): ResultFieldWithValue {
412
- return { field, value: codec.decode(value), raw: value }
413
- },
414
- }
415
-
416
- return field
329
+ public visitText(t: IDL.TextClass, label: string): ResultNode<"text"> {
330
+ return primitiveNode("text", label, "text", "string", this.getCodec(t), {
331
+ format: checkTextFormat(label) as TextFormat,
332
+ })
417
333
  }
418
334
 
419
- public visitBool(t: IDL.BoolClass, label: string): BooleanResultField {
420
- const codec = t.accept(this.displayVisitor, null) as any
421
- const field: BooleanResultField = {
422
- type: "boolean",
423
- label,
424
- candidType: "bool",
425
- displayType: "boolean",
426
- resolve(value: unknown): ResultFieldWithValue {
427
- return { field, value: codec.decode(value), raw: value }
428
- },
429
- }
430
-
431
- return field
335
+ public visitBool(t: IDL.BoolClass, label: string): ResultNode<"boolean"> {
336
+ return primitiveNode("boolean", label, "bool", "boolean", this.getCodec(t))
432
337
  }
433
338
 
434
- public visitNull(t: IDL.NullClass, label: string): NullResultField {
435
- const codec = t.accept(this.displayVisitor, null) as any
436
- const field: NullResultField = {
437
- type: "null",
438
- label,
439
- candidType: "null",
440
- displayType: "null",
441
- resolve(value: unknown): ResultFieldWithValue {
442
- return { field, value: codec.decode(value), raw: value }
443
- },
444
- }
445
-
446
- return field
339
+ public visitNull(t: IDL.NullClass, label: string): ResultNode<"null"> {
340
+ return primitiveNode("null", label, "null", "null", this.getCodec(t))
447
341
  }
448
342
 
449
- // Numbers
450
- public visitInt(t: IDL.IntClass, label: string): NumberResultField {
451
- const codec = t.accept(this.displayVisitor, null) as any
452
- const field: NumberResultField = {
453
- type: "number",
454
- label,
455
- candidType: "int",
456
- displayType: "string", // BigInt → string
457
- numberFormat: checkNumberFormat(label),
458
- resolve(value: unknown): ResultFieldWithValue {
459
- return { field, value: codec.decode(value), raw: value }
460
- },
461
- }
462
-
463
- return field
343
+ public visitInt(t: IDL.IntClass, label: string): ResultNode<"number"> {
344
+ return primitiveNode("number", label, "int", "string", this.getCodec(t), {
345
+ format: checkNumberFormat(label) as NumberFormat,
346
+ })
464
347
  }
465
348
 
466
- public visitNat(t: IDL.NatClass, label: string): NumberResultField {
467
- const codec = t.accept(this.displayVisitor, null) as any
468
- const field: NumberResultField = {
469
- type: "number",
470
- label,
471
- candidType: "nat",
472
- displayType: "string", // BigInt → string
473
- numberFormat: checkNumberFormat(label),
474
- resolve(value: unknown): ResultFieldWithValue {
475
- return { field, value: codec.decode(value), raw: value }
476
- },
477
- }
478
-
479
- return field
349
+ public visitNat(t: IDL.NatClass, label: string): ResultNode<"number"> {
350
+ return primitiveNode("number", label, "nat", "string", this.getCodec(t), {
351
+ format: checkNumberFormat(label) as NumberFormat,
352
+ })
480
353
  }
481
354
 
482
- public visitFloat(t: IDL.FloatClass, label: string): NumberResultField {
483
- const codec = t.accept(this.displayVisitor, null) as any
484
- const field: NumberResultField = {
485
- type: "number",
355
+ public visitFloat(t: IDL.FloatClass, label: string): ResultNode<"number"> {
356
+ return primitiveNode(
357
+ "number",
486
358
  label,
487
- candidType: `float${t._bits}`,
488
- displayType: "number", // Floats stay as numbers
489
- numberFormat: checkNumberFormat(label),
490
- resolve(value: unknown): ResultFieldWithValue {
491
- return { field, value: codec.decode(value), raw: value }
492
- },
493
- }
494
-
495
- return field
359
+ `float${t._bits}`,
360
+ "number",
361
+ this.getCodec(t),
362
+ {
363
+ format: checkNumberFormat(label) as NumberFormat,
364
+ }
365
+ )
496
366
  }
497
367
 
498
- public visitFixedInt(t: IDL.FixedIntClass, label: string): NumberResultField {
368
+ public visitFixedInt(
369
+ t: IDL.FixedIntClass,
370
+ label: string
371
+ ): ResultNode<"number"> {
499
372
  const bits = t._bits
500
- const codec = t.accept(this.displayVisitor, null) as any
501
- const field: NumberResultField = {
502
- type: "number",
373
+ return primitiveNode(
374
+ "number",
503
375
  label,
504
- candidType: `int${bits}`,
505
- displayType: bits <= 32 ? "number" : "string", // int64 → string
506
- numberFormat: checkNumberFormat(label),
507
- resolve(value: unknown): ResultFieldWithValue {
508
- return {
509
- field,
510
- value: codec.decode(value),
511
- raw: value,
512
- }
513
- },
514
- }
515
-
516
- return field
376
+ `int${bits}`,
377
+ bits <= 32 ? "number" : "string",
378
+ this.getCodec(t),
379
+ {
380
+ format: checkNumberFormat(label) as NumberFormat,
381
+ }
382
+ )
517
383
  }
518
384
 
519
- public visitFixedNat(t: IDL.FixedNatClass, label: string): NumberResultField {
385
+ public visitFixedNat(
386
+ t: IDL.FixedNatClass,
387
+ label: string
388
+ ): ResultNode<"number"> {
520
389
  const bits = t._bits
521
- const codec = t.accept(this.displayVisitor, null) as any
522
- const field: NumberResultField = {
523
- type: "number",
390
+ return primitiveNode(
391
+ "number",
524
392
  label,
525
- candidType: `nat${bits}`,
526
- displayType: bits <= 32 ? "number" : "string", // nat64 → string
527
- numberFormat: checkNumberFormat(label),
528
- resolve(value: unknown): ResultFieldWithValue {
529
- return {
530
- field,
531
- value: codec.decode(value),
532
- raw: value,
533
- }
534
- },
535
- }
536
-
537
- return field
393
+ `nat${bits}`,
394
+ bits <= 32 ? "number" : "string",
395
+ this.getCodec(t),
396
+ {
397
+ format: checkNumberFormat(label) as NumberFormat,
398
+ }
399
+ )
538
400
  }
539
401
 
540
- public visitType<T>(_t: IDL.Type<T>, label: string): UnknownResultField {
541
- const field: UnknownResultField = {
542
- type: "unknown",
543
- label,
544
- candidType: "unknown",
545
- displayType: "unknown",
546
- resolve(value: unknown): ResultFieldWithValue {
547
- return { field, value, raw: value }
548
- },
549
- }
550
-
551
- return field
402
+ public visitType<T>(_t: IDL.Type<T>, label: string): ResultNode<"unknown"> {
403
+ return primitiveNode("unknown", label, "unknown", "unknown", {
404
+ decode: (v) => v,
405
+ })
552
406
  }
553
407
  }