@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.
- package/README.md +33 -1
- package/dist/adapter.js +2 -1
- package/dist/adapter.js.map +1 -1
- package/dist/display-reactor.d.ts +4 -13
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +22 -8
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +108 -0
- package/dist/metadata-display-reactor.d.ts.map +1 -0
- package/dist/metadata-display-reactor.js +141 -0
- package/dist/metadata-display-reactor.js.map +1 -0
- package/dist/reactor.d.ts +1 -1
- package/dist/reactor.d.ts.map +1 -1
- package/dist/reactor.js +10 -6
- package/dist/reactor.js.map +1 -1
- package/dist/types.d.ts +38 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +4 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +33 -10
- package/dist/utils.js.map +1 -1
- package/dist/visitor/arguments/helpers.d.ts +55 -0
- package/dist/visitor/arguments/helpers.d.ts.map +1 -0
- package/dist/visitor/arguments/helpers.js +123 -0
- package/dist/visitor/arguments/helpers.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +101 -0
- package/dist/visitor/arguments/index.d.ts.map +1 -0
- package/dist/visitor/arguments/index.js +780 -0
- package/dist/visitor/arguments/index.js.map +1 -0
- package/dist/visitor/arguments/types.d.ts +270 -0
- package/dist/visitor/arguments/types.d.ts.map +1 -0
- package/dist/visitor/arguments/types.js +26 -0
- package/dist/visitor/arguments/types.js.map +1 -0
- package/dist/visitor/constants.d.ts +4 -0
- package/dist/visitor/constants.d.ts.map +1 -0
- package/dist/visitor/constants.js +73 -0
- package/dist/visitor/constants.js.map +1 -0
- package/dist/visitor/helpers.d.ts +30 -0
- package/dist/visitor/helpers.d.ts.map +1 -0
- package/dist/visitor/helpers.js +204 -0
- package/dist/visitor/helpers.js.map +1 -0
- package/dist/visitor/index.d.ts +5 -0
- package/dist/visitor/index.d.ts.map +1 -0
- package/dist/visitor/index.js +5 -0
- package/dist/visitor/index.js.map +1 -0
- package/dist/visitor/returns/index.d.ts +38 -0
- package/dist/visitor/returns/index.d.ts.map +1 -0
- package/dist/visitor/returns/index.js +460 -0
- package/dist/visitor/returns/index.js.map +1 -0
- package/dist/visitor/returns/types.d.ts +202 -0
- package/dist/visitor/returns/types.d.ts.map +1 -0
- package/dist/visitor/returns/types.js +2 -0
- package/dist/visitor/returns/types.js.map +1 -0
- package/dist/visitor/types.d.ts +19 -0
- package/dist/visitor/types.d.ts.map +1 -0
- package/dist/visitor/types.js +2 -0
- package/dist/visitor/types.js.map +1 -0
- package/package.json +16 -7
- package/src/adapter.ts +446 -0
- package/src/constants.ts +11 -0
- package/src/display-reactor.ts +337 -0
- package/src/index.ts +8 -0
- package/src/metadata-display-reactor.ts +230 -0
- package/src/reactor.ts +199 -0
- package/src/types.ts +127 -0
- package/src/utils.ts +60 -0
- package/src/visitor/arguments/helpers.ts +153 -0
- package/src/visitor/arguments/index.test.ts +1439 -0
- package/src/visitor/arguments/index.ts +981 -0
- package/src/visitor/arguments/schema.test.ts +324 -0
- package/src/visitor/arguments/types.ts +387 -0
- package/src/visitor/constants.ts +76 -0
- package/src/visitor/helpers.test.ts +274 -0
- package/src/visitor/helpers.ts +223 -0
- package/src/visitor/index.ts +4 -0
- package/src/visitor/returns/index.test.ts +2377 -0
- package/src/visitor/returns/index.ts +658 -0
- package/src/visitor/returns/types.ts +302 -0
- 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
|
+
})
|