@ic-reactor/candid 3.0.7-beta.2 → 3.0.8-beta.1
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/dist/display-reactor.d.ts +3 -2
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +6 -0
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +73 -0
- package/dist/metadata-display-reactor.d.ts.map +1 -0
- package/dist/metadata-display-reactor.js +128 -0
- package/dist/metadata-display-reactor.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +69 -0
- package/dist/visitor/arguments/index.d.ts.map +1 -0
- package/dist/visitor/arguments/index.js +277 -0
- package/dist/visitor/arguments/index.js.map +1 -0
- package/dist/visitor/arguments/types.d.ts +92 -0
- package/dist/visitor/arguments/types.d.ts.map +1 -0
- package/dist/visitor/arguments/types.js +2 -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 +61 -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 +200 -0
- package/dist/visitor/helpers.js.map +1 -0
- package/dist/visitor/returns/index.d.ts +76 -0
- package/dist/visitor/returns/index.d.ts.map +1 -0
- package/dist/visitor/returns/index.js +426 -0
- package/dist/visitor/returns/index.js.map +1 -0
- package/dist/visitor/returns/types.d.ts +143 -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 +6 -0
- package/dist/visitor/types.d.ts.map +1 -0
- package/dist/visitor/types.js +3 -0
- package/dist/visitor/types.js.map +1 -0
- package/package.json +3 -2
- package/src/display-reactor.ts +10 -2
- package/src/index.ts +1 -0
- package/src/metadata-display-reactor.ts +184 -0
- package/src/visitor/arguments/index.test.ts +882 -0
- package/src/visitor/arguments/index.ts +405 -0
- package/src/visitor/arguments/types.ts +168 -0
- package/src/visitor/constants.ts +62 -0
- package/src/visitor/helpers.ts +221 -0
- package/src/visitor/returns/index.test.ts +2027 -0
- package/src/visitor/returns/index.ts +553 -0
- package/src/visitor/returns/types.ts +272 -0
- package/src/visitor/types.ts +29 -0
|
@@ -0,0 +1,2027 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
3
|
+
import { ResultFieldVisitor } from "./index"
|
|
4
|
+
import type {
|
|
5
|
+
ResultField,
|
|
6
|
+
RecordResultField,
|
|
7
|
+
VariantResultField,
|
|
8
|
+
TupleResultField,
|
|
9
|
+
OptionalResultField,
|
|
10
|
+
VectorResultField,
|
|
11
|
+
BlobResultField,
|
|
12
|
+
RecursiveResultField,
|
|
13
|
+
NumberResultField,
|
|
14
|
+
TextResultField,
|
|
15
|
+
PrincipalResultField,
|
|
16
|
+
BooleanResultField,
|
|
17
|
+
NullResultField,
|
|
18
|
+
MethodResultMeta,
|
|
19
|
+
ServiceResultMeta,
|
|
20
|
+
} from "./types"
|
|
21
|
+
|
|
22
|
+
describe("ResultFieldVisitor", () => {
|
|
23
|
+
const visitor = new ResultFieldVisitor()
|
|
24
|
+
|
|
25
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
26
|
+
// Primitive Types
|
|
27
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
describe("Primitive Types", () => {
|
|
30
|
+
it("should handle text type", () => {
|
|
31
|
+
const textType = IDL.Text
|
|
32
|
+
const field = visitor.visitText(textType, "message")
|
|
33
|
+
|
|
34
|
+
expect(field.type).toBe("text")
|
|
35
|
+
expect(field.label).toBe("message")
|
|
36
|
+
expect(field.candidType).toBe("text")
|
|
37
|
+
expect(field.displayType).toBe("string")
|
|
38
|
+
expect(field.textFormat).toBe("plain")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should handle text with special label detection", () => {
|
|
42
|
+
const emailField = visitor.visitText(IDL.Text, "email")
|
|
43
|
+
expect(emailField.textFormat).toBe("email")
|
|
44
|
+
|
|
45
|
+
const urlField = visitor.visitText(IDL.Text, "website_url")
|
|
46
|
+
expect(urlField.textFormat).toBe("url")
|
|
47
|
+
|
|
48
|
+
const phoneField = visitor.visitText(IDL.Text, "phone_number")
|
|
49
|
+
expect(phoneField.textFormat).toBe("phone")
|
|
50
|
+
|
|
51
|
+
const uuidField = visitor.visitText(IDL.Text, "transaction_uuid")
|
|
52
|
+
expect(uuidField.textFormat).toBe("uuid")
|
|
53
|
+
|
|
54
|
+
const btcField = visitor.visitText(IDL.Text, "btc_address")
|
|
55
|
+
expect(btcField.textFormat).toBe("btc")
|
|
56
|
+
|
|
57
|
+
const ethField = visitor.visitText(IDL.Text, "ethereum_address")
|
|
58
|
+
expect(ethField.textFormat).toBe("eth")
|
|
59
|
+
|
|
60
|
+
const accountIdField = visitor.visitText(IDL.Text, "account_id")
|
|
61
|
+
expect(accountIdField.textFormat).toBe("account-id")
|
|
62
|
+
|
|
63
|
+
const principalField = visitor.visitText(IDL.Text, "canister_id")
|
|
64
|
+
expect(principalField.textFormat).toBe("principal")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("should handle bool type", () => {
|
|
68
|
+
const boolType = IDL.Bool
|
|
69
|
+
const field = visitor.visitBool(boolType, "isActive")
|
|
70
|
+
|
|
71
|
+
expect(field.type).toBe("boolean")
|
|
72
|
+
expect(field.label).toBe("isActive")
|
|
73
|
+
expect(field.candidType).toBe("bool")
|
|
74
|
+
expect(field.displayType).toBe("boolean")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should handle null type", () => {
|
|
78
|
+
const nullType = IDL.Null
|
|
79
|
+
const field = visitor.visitNull(nullType, "empty")
|
|
80
|
+
|
|
81
|
+
expect(field.type).toBe("null")
|
|
82
|
+
expect(field.candidType).toBe("null")
|
|
83
|
+
expect(field.displayType).toBe("null")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("should handle principal type", () => {
|
|
87
|
+
const principalType = IDL.Principal
|
|
88
|
+
const field = visitor.visitPrincipal(IDL.Principal, "owner")
|
|
89
|
+
|
|
90
|
+
expect(field.type).toBe("principal")
|
|
91
|
+
expect(field.label).toBe("owner")
|
|
92
|
+
expect(field.candidType).toBe("principal")
|
|
93
|
+
expect(field.displayType).toBe("string") // Principal → string after transformation
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
98
|
+
// Number Types - Display Type Mapping
|
|
99
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
describe("Number Types", () => {
|
|
102
|
+
describe("BigInt types (display as string)", () => {
|
|
103
|
+
it("should map nat to string display type", () => {
|
|
104
|
+
const field = visitor.visitNat(IDL.Nat, "amount")
|
|
105
|
+
|
|
106
|
+
expect(field.type).toBe("number")
|
|
107
|
+
expect(field.candidType).toBe("nat")
|
|
108
|
+
expect(field.displayType).toBe("string") // BigInt → string
|
|
109
|
+
expect(field.numberFormat).toBe("normal")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should map int to string display type", () => {
|
|
113
|
+
const field = visitor.visitInt(IDL.Int, "balance")
|
|
114
|
+
|
|
115
|
+
expect(field.type).toBe("number")
|
|
116
|
+
expect(field.candidType).toBe("int")
|
|
117
|
+
expect(field.displayType).toBe("string")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should map nat64 to string display type", () => {
|
|
121
|
+
const field = visitor.visitFixedNat(IDL.Nat64, "timestamp")
|
|
122
|
+
|
|
123
|
+
expect(field.type).toBe("number")
|
|
124
|
+
expect(field.candidType).toBe("nat64")
|
|
125
|
+
expect(field.displayType).toBe("string") // 64-bit → string
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("should map int64 to string display type", () => {
|
|
129
|
+
const field = visitor.visitFixedInt(IDL.Int64, "offset")
|
|
130
|
+
|
|
131
|
+
expect(field.type).toBe("number")
|
|
132
|
+
expect(field.candidType).toBe("int64")
|
|
133
|
+
expect(field.displayType).toBe("string")
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe("Small int types (display as number)", () => {
|
|
138
|
+
it("should map nat8 to number display type", () => {
|
|
139
|
+
const field = visitor.visitFixedNat(IDL.Nat8, "byte")
|
|
140
|
+
|
|
141
|
+
expect(field.type).toBe("number")
|
|
142
|
+
expect(field.candidType).toBe("nat8")
|
|
143
|
+
expect(field.displayType).toBe("number") // Small int stays number
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should map nat16 to number display type", () => {
|
|
147
|
+
const field = visitor.visitFixedNat(IDL.Nat16, "port")
|
|
148
|
+
|
|
149
|
+
expect(field.candidType).toBe("nat16")
|
|
150
|
+
expect(field.displayType).toBe("number")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("should map nat32 to number display type", () => {
|
|
154
|
+
const field = visitor.visitFixedNat(IDL.Nat32, "count")
|
|
155
|
+
|
|
156
|
+
expect(field.candidType).toBe("nat32")
|
|
157
|
+
expect(field.displayType).toBe("number")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should map int8 to number display type", () => {
|
|
161
|
+
const field = visitor.visitFixedInt(IDL.Int8, "temp")
|
|
162
|
+
|
|
163
|
+
expect(field.candidType).toBe("int8")
|
|
164
|
+
expect(field.displayType).toBe("number")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("should map int32 to number display type", () => {
|
|
168
|
+
const field = visitor.visitFixedInt(IDL.Int32, "index")
|
|
169
|
+
|
|
170
|
+
expect(field.candidType).toBe("int32")
|
|
171
|
+
expect(field.displayType).toBe("number")
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe("Float types (display as number)", () => {
|
|
176
|
+
it("should map float32 to number display type", () => {
|
|
177
|
+
const field = visitor.visitFloat(IDL.Float32, "rate")
|
|
178
|
+
|
|
179
|
+
expect(field.candidType).toBe("float32")
|
|
180
|
+
expect(field.displayType).toBe("number")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("should map float64 to number display type", () => {
|
|
184
|
+
const field = visitor.visitFloat(IDL.Float64, "price")
|
|
185
|
+
|
|
186
|
+
expect(field.candidType).toBe("float64")
|
|
187
|
+
expect(field.displayType).toBe("number")
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe("Number format detection", () => {
|
|
192
|
+
it("should detect timestamp format from label", () => {
|
|
193
|
+
const timestampField = visitor.visitFixedNat(IDL.Nat64, "created_at")
|
|
194
|
+
expect(timestampField.numberFormat).toBe("timestamp")
|
|
195
|
+
|
|
196
|
+
const dateField = visitor.visitNat(IDL.Nat, "timestamp_nanos")
|
|
197
|
+
expect(dateField.numberFormat).toBe("timestamp")
|
|
198
|
+
|
|
199
|
+
const deadlineField = visitor.visitNat(IDL.Nat, "deadline")
|
|
200
|
+
expect(deadlineField.numberFormat).toBe("timestamp")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("should detect cycle format from label", () => {
|
|
204
|
+
const cycleField = visitor.visitNat(IDL.Nat, "cycles")
|
|
205
|
+
expect(cycleField.numberFormat).toBe("cycle")
|
|
206
|
+
|
|
207
|
+
// "cycle" as standalone word
|
|
208
|
+
const cycleSingleField = visitor.visitNat(IDL.Nat, "cycle")
|
|
209
|
+
expect(cycleSingleField.numberFormat).toBe("cycle")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("should default to normal format", () => {
|
|
213
|
+
const amountField = visitor.visitNat(IDL.Nat, "amount")
|
|
214
|
+
expect(amountField.numberFormat).toBe("normal")
|
|
215
|
+
|
|
216
|
+
const countField = visitor.visitNat(IDL.Nat, "count")
|
|
217
|
+
expect(countField.numberFormat).toBe("normal")
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// Compound Types
|
|
224
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
describe("Record Types", () => {
|
|
227
|
+
it("should handle simple record", () => {
|
|
228
|
+
const recordType = IDL.Record({
|
|
229
|
+
name: IDL.Text,
|
|
230
|
+
age: IDL.Nat,
|
|
231
|
+
})
|
|
232
|
+
const field = visitor.visitRecord(
|
|
233
|
+
recordType,
|
|
234
|
+
[
|
|
235
|
+
["name", IDL.Text],
|
|
236
|
+
["age", IDL.Nat],
|
|
237
|
+
],
|
|
238
|
+
"person"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
expect(field.type).toBe("record")
|
|
242
|
+
expect(field.label).toBe("person")
|
|
243
|
+
expect(field.candidType).toBe("record")
|
|
244
|
+
expect(field.displayType).toBe("object")
|
|
245
|
+
expect(field.fields).toHaveLength(2)
|
|
246
|
+
|
|
247
|
+
const nameField = field.fields.find((f) => f.label === "name")
|
|
248
|
+
if (!nameField || nameField.type !== "text") {
|
|
249
|
+
throw new Error("Name field not found or not text")
|
|
250
|
+
}
|
|
251
|
+
expect(nameField.displayType).toBe("string")
|
|
252
|
+
|
|
253
|
+
const ageField = field.fields.find((f) => f.label === "age")
|
|
254
|
+
if (!ageField || ageField.type !== "number") {
|
|
255
|
+
throw new Error("Age field not found or not number")
|
|
256
|
+
}
|
|
257
|
+
expect(ageField.displayType).toBe("string") // nat → string
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("should handle nested record", () => {
|
|
261
|
+
const addressType = IDL.Record({
|
|
262
|
+
street: IDL.Text,
|
|
263
|
+
city: IDL.Text,
|
|
264
|
+
})
|
|
265
|
+
const personType = IDL.Record({
|
|
266
|
+
name: IDL.Text,
|
|
267
|
+
address: addressType,
|
|
268
|
+
})
|
|
269
|
+
const field = visitor.visitRecord(
|
|
270
|
+
personType,
|
|
271
|
+
[
|
|
272
|
+
["name", IDL.Text],
|
|
273
|
+
["address", addressType],
|
|
274
|
+
],
|
|
275
|
+
"user"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(field.type).toBe("record")
|
|
279
|
+
|
|
280
|
+
const addressField = field.fields.find((f) => f.label === "address")
|
|
281
|
+
if (!addressField || addressField.type !== "record") {
|
|
282
|
+
throw new Error("Address field not found or not a record")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
expect(addressField.type).toBe("record")
|
|
286
|
+
expect(addressField.displayType).toBe("object")
|
|
287
|
+
expect(addressField.fields).toHaveLength(2)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it("should handle ICRC-1 account record", () => {
|
|
291
|
+
const accountType = IDL.Record({
|
|
292
|
+
owner: IDL.Principal,
|
|
293
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
294
|
+
})
|
|
295
|
+
const field = visitor.visitRecord(
|
|
296
|
+
accountType,
|
|
297
|
+
[
|
|
298
|
+
["owner", IDL.Principal],
|
|
299
|
+
["subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
300
|
+
],
|
|
301
|
+
"account"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
expect(field.type).toBe("record")
|
|
305
|
+
|
|
306
|
+
const ownerField = field.fields.find((f) => f.label === "owner")
|
|
307
|
+
if (!ownerField || ownerField.type !== "principal") {
|
|
308
|
+
throw new Error("Owner field not found or not principal")
|
|
309
|
+
}
|
|
310
|
+
expect(ownerField.type).toBe("principal")
|
|
311
|
+
expect(ownerField.displayType).toBe("string")
|
|
312
|
+
|
|
313
|
+
const subaccountField = field.fields.find((f) => f.label === "subaccount")
|
|
314
|
+
if (!subaccountField || subaccountField.type !== "optional") {
|
|
315
|
+
throw new Error("Subaccount field not found or not optional")
|
|
316
|
+
}
|
|
317
|
+
expect(subaccountField.type).toBe("optional")
|
|
318
|
+
if (subaccountField.innerField.type !== "blob") {
|
|
319
|
+
throw new Error("Subaccount inner field is not blob")
|
|
320
|
+
}
|
|
321
|
+
expect(subaccountField.innerField.type).toBe("blob")
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe("Variant Types", () => {
|
|
326
|
+
it("should handle simple variant", () => {
|
|
327
|
+
const statusType = IDL.Variant({
|
|
328
|
+
Active: IDL.Null,
|
|
329
|
+
Inactive: IDL.Null,
|
|
330
|
+
Pending: IDL.Null,
|
|
331
|
+
})
|
|
332
|
+
const field = visitor.visitVariant(
|
|
333
|
+
statusType,
|
|
334
|
+
[
|
|
335
|
+
["Active", IDL.Null],
|
|
336
|
+
["Inactive", IDL.Null],
|
|
337
|
+
["Pending", IDL.Null],
|
|
338
|
+
],
|
|
339
|
+
"status"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
expect(field.type).toBe("variant")
|
|
343
|
+
expect(field.candidType).toBe("variant")
|
|
344
|
+
expect(field.displayType).toBe("variant")
|
|
345
|
+
expect(field.options).toContain("Active")
|
|
346
|
+
expect(field.options).toContain("Inactive")
|
|
347
|
+
expect(field.options).toContain("Pending")
|
|
348
|
+
expect(field.options).toHaveLength(3)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it("should handle variant with payloads", () => {
|
|
352
|
+
const eventType = IDL.Variant({
|
|
353
|
+
Transfer: IDL.Record({
|
|
354
|
+
from: IDL.Principal,
|
|
355
|
+
to: IDL.Principal,
|
|
356
|
+
amount: IDL.Nat,
|
|
357
|
+
}),
|
|
358
|
+
Approve: IDL.Nat,
|
|
359
|
+
Mint: IDL.Record({
|
|
360
|
+
to: IDL.Principal,
|
|
361
|
+
amount: IDL.Nat,
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
const field = visitor.visitVariant(
|
|
365
|
+
eventType,
|
|
366
|
+
[
|
|
367
|
+
[
|
|
368
|
+
"Transfer",
|
|
369
|
+
IDL.Record({
|
|
370
|
+
from: IDL.Principal,
|
|
371
|
+
to: IDL.Principal,
|
|
372
|
+
amount: IDL.Nat,
|
|
373
|
+
}),
|
|
374
|
+
],
|
|
375
|
+
["Approve", IDL.Nat],
|
|
376
|
+
[
|
|
377
|
+
"Mint",
|
|
378
|
+
IDL.Record({
|
|
379
|
+
to: IDL.Principal,
|
|
380
|
+
amount: IDL.Nat,
|
|
381
|
+
}),
|
|
382
|
+
],
|
|
383
|
+
],
|
|
384
|
+
"event"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
expect(field.type).toBe("variant")
|
|
388
|
+
expect(field.options).toContain("Transfer")
|
|
389
|
+
expect(field.options).toContain("Approve")
|
|
390
|
+
expect(field.options).toContain("Mint")
|
|
391
|
+
expect(field.options).toHaveLength(3)
|
|
392
|
+
|
|
393
|
+
const transferField = field.optionFields.find(
|
|
394
|
+
(f) => f.label === "Transfer"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if (!transferField || transferField.type !== "record") {
|
|
398
|
+
throw new Error("Transfer field not found or not record")
|
|
399
|
+
}
|
|
400
|
+
expect(transferField.type).toBe("record")
|
|
401
|
+
expect(transferField.fields).toHaveLength(3)
|
|
402
|
+
|
|
403
|
+
const approveField = field.optionFields.find((f) => f.label === "Approve")
|
|
404
|
+
if (!approveField || approveField.type !== "number") {
|
|
405
|
+
throw new Error("Approve field not found or not number")
|
|
406
|
+
}
|
|
407
|
+
expect(approveField.type).toBe("number")
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("should detect Result variant (Ok/Err)", () => {
|
|
411
|
+
const resultType = IDL.Variant({
|
|
412
|
+
Ok: IDL.Nat,
|
|
413
|
+
Err: IDL.Text,
|
|
414
|
+
})
|
|
415
|
+
const field = visitor.visitVariant(
|
|
416
|
+
resultType,
|
|
417
|
+
[
|
|
418
|
+
["Ok", IDL.Nat],
|
|
419
|
+
["Err", IDL.Text],
|
|
420
|
+
],
|
|
421
|
+
"result"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
expect(field.type).toBe("variant")
|
|
425
|
+
expect(field.displayType).toBe("result") // Special result type
|
|
426
|
+
expect(field.options).toContain("Ok")
|
|
427
|
+
expect(field.options).toContain("Err")
|
|
428
|
+
|
|
429
|
+
const okField = field.optionFields.find((f) => f.label === "Ok")
|
|
430
|
+
if (!okField || okField.type !== "number") {
|
|
431
|
+
throw new Error("Ok field not found or not number")
|
|
432
|
+
}
|
|
433
|
+
expect(okField.displayType).toBe("string")
|
|
434
|
+
|
|
435
|
+
const errField = field.optionFields.find((f) => f.label === "Err")
|
|
436
|
+
if (!errField || errField.type !== "text") {
|
|
437
|
+
throw new Error("Err field not found or not text")
|
|
438
|
+
}
|
|
439
|
+
expect(errField.displayType).toBe("string")
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it("should detect complex Result variant", () => {
|
|
443
|
+
const resultType = IDL.Variant({
|
|
444
|
+
Ok: IDL.Record({
|
|
445
|
+
id: IDL.Nat,
|
|
446
|
+
data: IDL.Vec(IDL.Nat8),
|
|
447
|
+
}),
|
|
448
|
+
Err: IDL.Variant({
|
|
449
|
+
NotFound: IDL.Null,
|
|
450
|
+
Unauthorized: IDL.Null,
|
|
451
|
+
InvalidInput: IDL.Text,
|
|
452
|
+
}),
|
|
453
|
+
})
|
|
454
|
+
const field = visitor.visitVariant(
|
|
455
|
+
resultType,
|
|
456
|
+
[
|
|
457
|
+
[
|
|
458
|
+
"Ok",
|
|
459
|
+
IDL.Record({
|
|
460
|
+
id: IDL.Nat,
|
|
461
|
+
data: IDL.Vec(IDL.Nat8),
|
|
462
|
+
}),
|
|
463
|
+
],
|
|
464
|
+
[
|
|
465
|
+
"Err",
|
|
466
|
+
IDL.Variant({
|
|
467
|
+
NotFound: IDL.Null,
|
|
468
|
+
Unauthorized: IDL.Null,
|
|
469
|
+
InvalidInput: IDL.Text,
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
],
|
|
473
|
+
"result"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
expect(field.displayType).toBe("result")
|
|
477
|
+
|
|
478
|
+
const okField = field.optionFields.find((f) => f.label === "Ok")
|
|
479
|
+
if (!okField || okField.type !== "record") {
|
|
480
|
+
throw new Error("Ok field not found or not record")
|
|
481
|
+
}
|
|
482
|
+
expect(okField.type).toBe("record")
|
|
483
|
+
|
|
484
|
+
const errField = field.optionFields.find((f) => f.label === "Err")
|
|
485
|
+
if (!errField || errField.type !== "variant") {
|
|
486
|
+
throw new Error("Err field not found or not variant")
|
|
487
|
+
}
|
|
488
|
+
expect(errField.type).toBe("variant")
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it("should not detect non-Result variant with Ok and other options", () => {
|
|
492
|
+
const weirdType = IDL.Variant({
|
|
493
|
+
Ok: IDL.Nat,
|
|
494
|
+
Pending: IDL.Null,
|
|
495
|
+
Processing: IDL.Text,
|
|
496
|
+
})
|
|
497
|
+
const field = visitor.visitVariant(
|
|
498
|
+
weirdType,
|
|
499
|
+
[
|
|
500
|
+
["Ok", IDL.Nat],
|
|
501
|
+
["Pending", IDL.Null],
|
|
502
|
+
["Processing", IDL.Text],
|
|
503
|
+
],
|
|
504
|
+
"status"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
expect(field.displayType).toBe("variant")
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
describe("Tuple Types", () => {
|
|
512
|
+
it("should handle simple tuple", () => {
|
|
513
|
+
const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
|
|
514
|
+
const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
|
|
515
|
+
|
|
516
|
+
expect(field.type).toBe("tuple")
|
|
517
|
+
expect(field.candidType).toBe("tuple")
|
|
518
|
+
expect(field.displayType).toBe("array")
|
|
519
|
+
expect(field.fields).toHaveLength(2)
|
|
520
|
+
expect(field.fields[0].type).toBe("text")
|
|
521
|
+
expect(field.fields[1].type).toBe("number")
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it("should handle tuple with mixed types", () => {
|
|
525
|
+
const tupleType = IDL.Tuple(
|
|
526
|
+
IDL.Principal,
|
|
527
|
+
IDL.Nat64,
|
|
528
|
+
IDL.Bool,
|
|
529
|
+
IDL.Vec(IDL.Nat8)
|
|
530
|
+
)
|
|
531
|
+
const field = visitor.visitTuple(
|
|
532
|
+
tupleType,
|
|
533
|
+
[IDL.Principal, IDL.Nat64, IDL.Bool, IDL.Vec(IDL.Nat8)],
|
|
534
|
+
"data"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
expect(field.fields).toHaveLength(4)
|
|
538
|
+
expect(field.fields[0].type).toBe("principal")
|
|
539
|
+
expect(field.fields[1].type).toBe("number")
|
|
540
|
+
const numField = field.fields[1]
|
|
541
|
+
if (numField.type === "number") {
|
|
542
|
+
expect(numField.displayType).toBe("string") // nat64 → string
|
|
543
|
+
} else {
|
|
544
|
+
throw new Error("Expected number field")
|
|
545
|
+
}
|
|
546
|
+
expect(field.fields[2].type).toBe("boolean")
|
|
547
|
+
expect(field.fields[3].type).toBe("blob")
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
describe("Optional Types", () => {
|
|
552
|
+
it("should handle optional primitive", () => {
|
|
553
|
+
const optType = IDL.Opt(IDL.Text)
|
|
554
|
+
const field = visitor.visitOpt(optType, IDL.Text, "nickname")
|
|
555
|
+
|
|
556
|
+
expect(field.type).toBe("optional")
|
|
557
|
+
expect(field.candidType).toBe("opt")
|
|
558
|
+
expect(field.displayType).toBe("nullable") // opt T → T | null
|
|
559
|
+
expect(field.innerField.type).toBe("text")
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it("should handle optional record", () => {
|
|
563
|
+
const recordInOpt = IDL.Record({
|
|
564
|
+
name: IDL.Text,
|
|
565
|
+
value: IDL.Nat,
|
|
566
|
+
})
|
|
567
|
+
const optType = IDL.Opt(recordInOpt)
|
|
568
|
+
const field = visitor.visitOpt(optType, recordInOpt, "metadata")
|
|
569
|
+
|
|
570
|
+
expect(field.type).toBe("optional")
|
|
571
|
+
expect(field.innerField.type).toBe("record")
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("should handle nested optional", () => {
|
|
575
|
+
const innerOpt = IDL.Opt(IDL.Nat)
|
|
576
|
+
const optType = IDL.Opt(innerOpt)
|
|
577
|
+
const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
|
|
578
|
+
|
|
579
|
+
expect(field.type).toBe("optional")
|
|
580
|
+
expect(field.innerField.type).toBe("optional")
|
|
581
|
+
const inner = field.innerField
|
|
582
|
+
if (inner.type === "optional") {
|
|
583
|
+
expect(inner.innerField.type).toBe("number")
|
|
584
|
+
} else {
|
|
585
|
+
throw new Error("Inner field is not optional")
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
describe("Vector Types", () => {
|
|
591
|
+
it("should handle vector of primitives", () => {
|
|
592
|
+
const vecType = IDL.Vec(IDL.Text)
|
|
593
|
+
const field = visitor.visitVec(vecType, IDL.Text, "tags")
|
|
594
|
+
|
|
595
|
+
expect(field.type).toBe("vector")
|
|
596
|
+
expect(field.candidType).toBe("vec")
|
|
597
|
+
expect(field.displayType).toBe("array")
|
|
598
|
+
if (field.type === "vector") {
|
|
599
|
+
expect(field.itemField.type).toBe("text")
|
|
600
|
+
} else {
|
|
601
|
+
throw new Error("Field is not a vector")
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it("should handle vector of records", () => {
|
|
606
|
+
const recType = IDL.Record({
|
|
607
|
+
id: IDL.Nat,
|
|
608
|
+
name: IDL.Text,
|
|
609
|
+
})
|
|
610
|
+
const vecType = IDL.Vec(recType)
|
|
611
|
+
const field = visitor.visitVec(vecType, recType, "items")
|
|
612
|
+
|
|
613
|
+
expect(field.type).toBe("vector")
|
|
614
|
+
if (field.type === "vector") {
|
|
615
|
+
expect(field.itemField.type).toBe("record")
|
|
616
|
+
} else {
|
|
617
|
+
throw new Error("Field is not a vector")
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it("should handle blob (vec nat8)", () => {
|
|
622
|
+
const blobType = IDL.Vec(IDL.Nat8)
|
|
623
|
+
const field = visitor.visitVec(blobType, IDL.Nat8, "data")
|
|
624
|
+
|
|
625
|
+
expect(field.type).toBe("blob")
|
|
626
|
+
expect(field.candidType).toBe("blob")
|
|
627
|
+
expect(field.displayType).toBe("string") // Blob → hex string
|
|
628
|
+
if (field.type === "blob") {
|
|
629
|
+
expect(field.displayHint).toBe("hex")
|
|
630
|
+
} else {
|
|
631
|
+
throw new Error("Field is not a blob")
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it("should handle nested vectors", () => {
|
|
636
|
+
const innerVec = IDL.Vec(IDL.Nat)
|
|
637
|
+
const nestedVecType = IDL.Vec(innerVec)
|
|
638
|
+
const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
|
|
639
|
+
|
|
640
|
+
expect(field.type).toBe("vector")
|
|
641
|
+
if (field.type === "vector") {
|
|
642
|
+
expect(field.itemField.type).toBe("vector")
|
|
643
|
+
} else {
|
|
644
|
+
throw new Error("Field is not a vector")
|
|
645
|
+
}
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
it("should handle vec of tuples (Map-like)", () => {
|
|
649
|
+
const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
|
|
650
|
+
const mapType = IDL.Vec(tupleType)
|
|
651
|
+
const field = visitor.visitVec(
|
|
652
|
+
mapType,
|
|
653
|
+
tupleType,
|
|
654
|
+
"metadata"
|
|
655
|
+
) as VectorResultField
|
|
656
|
+
|
|
657
|
+
expect(field.type).toBe("vector")
|
|
658
|
+
expect(field.itemField.type).toBe("tuple")
|
|
659
|
+
const itemField = field.itemField
|
|
660
|
+
if (itemField.type === "tuple") {
|
|
661
|
+
expect(itemField.fields).toHaveLength(2)
|
|
662
|
+
} else {
|
|
663
|
+
throw new Error("Item field is not tuple")
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
describe("Recursive Types", () => {
|
|
669
|
+
it("should handle recursive tree type", () => {
|
|
670
|
+
const Tree = IDL.Rec()
|
|
671
|
+
Tree.fill(
|
|
672
|
+
IDL.Variant({
|
|
673
|
+
Leaf: IDL.Nat,
|
|
674
|
+
Node: IDL.Record({
|
|
675
|
+
left: Tree,
|
|
676
|
+
right: Tree,
|
|
677
|
+
}),
|
|
678
|
+
})
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
const field = visitor.visitRec(
|
|
682
|
+
Tree,
|
|
683
|
+
IDL.Variant({
|
|
684
|
+
Leaf: IDL.Nat,
|
|
685
|
+
Node: IDL.Record({
|
|
686
|
+
left: Tree,
|
|
687
|
+
right: Tree,
|
|
688
|
+
}),
|
|
689
|
+
}),
|
|
690
|
+
"tree"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
expect(field.type).toBe("recursive")
|
|
694
|
+
expect(field.candidType).toBe("rec")
|
|
695
|
+
expect(field.displayType).toBe("recursive")
|
|
696
|
+
expect(field.typeName).toBeDefined()
|
|
697
|
+
expect(typeof field.extract).toBe("function")
|
|
698
|
+
|
|
699
|
+
// Extract should return the variant
|
|
700
|
+
const extracted = field.extract()
|
|
701
|
+
if (extracted.type !== "variant") {
|
|
702
|
+
throw new Error("Extracted field is not variant")
|
|
703
|
+
}
|
|
704
|
+
expect(extracted.type).toBe("variant")
|
|
705
|
+
expect(extracted.options).toContain("Leaf")
|
|
706
|
+
expect(extracted.options).toContain("Node")
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it("should handle recursive linked list", () => {
|
|
710
|
+
const List = IDL.Rec()
|
|
711
|
+
List.fill(
|
|
712
|
+
IDL.Variant({
|
|
713
|
+
Nil: IDL.Null,
|
|
714
|
+
Cons: IDL.Record({
|
|
715
|
+
head: IDL.Nat,
|
|
716
|
+
tail: List,
|
|
717
|
+
}),
|
|
718
|
+
})
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
const field = visitor.visitRec(
|
|
722
|
+
List,
|
|
723
|
+
IDL.Variant({
|
|
724
|
+
Nil: IDL.Null,
|
|
725
|
+
Cons: IDL.Record({
|
|
726
|
+
head: IDL.Nat,
|
|
727
|
+
tail: List,
|
|
728
|
+
}),
|
|
729
|
+
}),
|
|
730
|
+
"list"
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
expect(field.type).toBe("recursive")
|
|
734
|
+
|
|
735
|
+
const extracted = field.extract()
|
|
736
|
+
if (extracted.type !== "variant") {
|
|
737
|
+
throw new Error("Extracted field is not variant")
|
|
738
|
+
}
|
|
739
|
+
expect(extracted.options).toEqual(["Nil", "Cons"])
|
|
740
|
+
})
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
744
|
+
// Function Types
|
|
745
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
746
|
+
|
|
747
|
+
describe("Function Types", () => {
|
|
748
|
+
it("should handle query function with single return", () => {
|
|
749
|
+
const funcType = IDL.Func([IDL.Principal], [IDL.Nat], ["query"])
|
|
750
|
+
const meta = visitor.visitFunc(funcType, "get_balance")
|
|
751
|
+
|
|
752
|
+
expect(meta.functionType).toBe("query")
|
|
753
|
+
expect(meta.functionName).toBe("get_balance")
|
|
754
|
+
expect(meta.returnCount).toBe(1)
|
|
755
|
+
expect(meta.resultFields).toHaveLength(1)
|
|
756
|
+
|
|
757
|
+
expect(meta.resultFields).toHaveLength(1)
|
|
758
|
+
|
|
759
|
+
const returnField = meta.resultFields[0]
|
|
760
|
+
if (returnField.type === "number") {
|
|
761
|
+
expect(returnField.candidType).toBe("nat")
|
|
762
|
+
expect(returnField.displayType).toBe("string")
|
|
763
|
+
} else {
|
|
764
|
+
throw new Error("Return field is not number")
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
it("should handle update function with Result return", () => {
|
|
769
|
+
const funcType = IDL.Func(
|
|
770
|
+
[
|
|
771
|
+
IDL.Record({
|
|
772
|
+
to: IDL.Principal,
|
|
773
|
+
amount: IDL.Nat,
|
|
774
|
+
}),
|
|
775
|
+
],
|
|
776
|
+
[
|
|
777
|
+
IDL.Variant({
|
|
778
|
+
Ok: IDL.Nat,
|
|
779
|
+
Err: IDL.Text,
|
|
780
|
+
}),
|
|
781
|
+
],
|
|
782
|
+
[]
|
|
783
|
+
)
|
|
784
|
+
const meta = visitor.visitFunc(funcType, "transfer")
|
|
785
|
+
|
|
786
|
+
expect(meta.functionType).toBe("update")
|
|
787
|
+
expect(meta.returnCount).toBe(1)
|
|
788
|
+
|
|
789
|
+
const resultField = meta.resultFields[0]
|
|
790
|
+
if (resultField.type === "variant") {
|
|
791
|
+
expect(resultField.displayType).toBe("result")
|
|
792
|
+
} else {
|
|
793
|
+
throw new Error("Result field is not variant")
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it("should handle function with multiple returns", () => {
|
|
798
|
+
const funcType = IDL.Func([], [IDL.Text, IDL.Nat, IDL.Bool], ["query"])
|
|
799
|
+
const meta = visitor.visitFunc(funcType, "get_info")
|
|
800
|
+
|
|
801
|
+
expect(meta.returnCount).toBe(3)
|
|
802
|
+
expect(meta.resultFields).toHaveLength(3)
|
|
803
|
+
expect(meta.resultFields[0].type).toBe("text")
|
|
804
|
+
expect(meta.resultFields[1].type).toBe("number")
|
|
805
|
+
expect(meta.resultFields[2].type).toBe("boolean")
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
it("should handle function with no returns", () => {
|
|
809
|
+
const funcType = IDL.Func([IDL.Text], [], [])
|
|
810
|
+
const meta = visitor.visitFunc(funcType, "log")
|
|
811
|
+
|
|
812
|
+
expect(meta.returnCount).toBe(0)
|
|
813
|
+
expect(meta.resultFields).toHaveLength(0)
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
818
|
+
// Service Types
|
|
819
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
820
|
+
|
|
821
|
+
describe("Service Types", () => {
|
|
822
|
+
it("should handle complete service", () => {
|
|
823
|
+
const serviceType = IDL.Service({
|
|
824
|
+
get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
|
|
825
|
+
transfer: IDL.Func(
|
|
826
|
+
[
|
|
827
|
+
IDL.Record({
|
|
828
|
+
to: IDL.Principal,
|
|
829
|
+
amount: IDL.Nat,
|
|
830
|
+
}),
|
|
831
|
+
],
|
|
832
|
+
[
|
|
833
|
+
IDL.Variant({
|
|
834
|
+
Ok: IDL.Nat,
|
|
835
|
+
Err: IDL.Text,
|
|
836
|
+
}),
|
|
837
|
+
],
|
|
838
|
+
[]
|
|
839
|
+
),
|
|
840
|
+
get_metadata: IDL.Func(
|
|
841
|
+
[],
|
|
842
|
+
[IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
|
|
843
|
+
["query"]
|
|
844
|
+
),
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
const serviceMeta = visitor.visitService(serviceType)
|
|
848
|
+
|
|
849
|
+
expect(Object.keys(serviceMeta)).toHaveLength(3)
|
|
850
|
+
|
|
851
|
+
// Check get_balance
|
|
852
|
+
const getBalanceMeta = serviceMeta["get_balance"]
|
|
853
|
+
expect(getBalanceMeta.functionType).toBe("query")
|
|
854
|
+
expect(getBalanceMeta.returnCount).toBe(1)
|
|
855
|
+
expect(getBalanceMeta.resultFields[0].type).toBe("number")
|
|
856
|
+
const balanceField = getBalanceMeta.resultFields[0]
|
|
857
|
+
if (balanceField.type === "number") {
|
|
858
|
+
expect(balanceField.displayType).toBe("string")
|
|
859
|
+
} else {
|
|
860
|
+
throw new Error("Balance field is not number")
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Check transfer
|
|
864
|
+
const transferMeta = serviceMeta["transfer"]
|
|
865
|
+
expect(transferMeta.functionType).toBe("update")
|
|
866
|
+
expect(transferMeta.returnCount).toBe(1)
|
|
867
|
+
// Check get_metadata
|
|
868
|
+
const getMetadataMeta = serviceMeta["get_metadata"]
|
|
869
|
+
expect(getMetadataMeta.returnCount).toBe(1)
|
|
870
|
+
expect(getMetadataMeta.resultFields[0].type).toBe("vector")
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
875
|
+
// Real-World Examples
|
|
876
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
877
|
+
|
|
878
|
+
describe("Real-World Examples", () => {
|
|
879
|
+
it("should handle ICRC-1 balance_of return", () => {
|
|
880
|
+
const funcType = IDL.Func(
|
|
881
|
+
[
|
|
882
|
+
IDL.Record({
|
|
883
|
+
owner: IDL.Principal,
|
|
884
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
885
|
+
}),
|
|
886
|
+
],
|
|
887
|
+
[IDL.Nat],
|
|
888
|
+
["query"]
|
|
889
|
+
)
|
|
890
|
+
const meta = visitor.visitFunc(funcType, "icrc1_balance_of")
|
|
891
|
+
|
|
892
|
+
expect(meta.functionType).toBe("query")
|
|
893
|
+
expect(meta.returnCount).toBe(1)
|
|
894
|
+
|
|
895
|
+
const balanceField = meta.resultFields[0]
|
|
896
|
+
if (balanceField.type === "number") {
|
|
897
|
+
expect(balanceField.candidType).toBe("nat")
|
|
898
|
+
expect(balanceField.displayType).toBe("string")
|
|
899
|
+
} else {
|
|
900
|
+
throw new Error("Balance field is not number")
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it("should handle ICRC-1 transfer return", () => {
|
|
905
|
+
const TransferResult = IDL.Variant({
|
|
906
|
+
Ok: IDL.Nat, // Block index
|
|
907
|
+
Err: IDL.Variant({
|
|
908
|
+
BadFee: IDL.Record({ expected_fee: IDL.Nat }),
|
|
909
|
+
BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
|
|
910
|
+
InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
|
|
911
|
+
TooOld: IDL.Null,
|
|
912
|
+
CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
|
|
913
|
+
Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
|
|
914
|
+
TemporarilyUnavailable: IDL.Null,
|
|
915
|
+
GenericError: IDL.Record({ error_code: IDL.Nat, message: IDL.Text }),
|
|
916
|
+
}),
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
const field = visitor.visitVariant(
|
|
920
|
+
TransferResult,
|
|
921
|
+
[
|
|
922
|
+
["Ok", IDL.Nat], // Block index
|
|
923
|
+
[
|
|
924
|
+
"Err",
|
|
925
|
+
IDL.Variant({
|
|
926
|
+
BadFee: IDL.Record({ expected_fee: IDL.Nat }),
|
|
927
|
+
BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
|
|
928
|
+
InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
|
|
929
|
+
TooOld: IDL.Null,
|
|
930
|
+
CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
|
|
931
|
+
Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
|
|
932
|
+
TemporarilyUnavailable: IDL.Null,
|
|
933
|
+
GenericError: IDL.Record({
|
|
934
|
+
error_code: IDL.Nat,
|
|
935
|
+
message: IDL.Text,
|
|
936
|
+
}),
|
|
937
|
+
}),
|
|
938
|
+
],
|
|
939
|
+
],
|
|
940
|
+
"result"
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
expect(field.displayType).toBe("result")
|
|
944
|
+
|
|
945
|
+
const okField = field.optionFields.find((f) => f.label === "Ok")
|
|
946
|
+
|
|
947
|
+
if (!okField || okField.type !== "number") {
|
|
948
|
+
throw new Error("Ok field is not number")
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
expect(okField.candidType).toBe("nat")
|
|
952
|
+
expect(okField.displayType).toBe("string")
|
|
953
|
+
|
|
954
|
+
const errField = field.optionFields.find((f) => f.label === "Err")
|
|
955
|
+
if (!errField || errField.type !== "variant") {
|
|
956
|
+
throw new Error("Err field is not variant")
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
expect(errField.type).toBe("variant")
|
|
960
|
+
expect(errField.options).toContain("InsufficientFunds")
|
|
961
|
+
expect(errField.options).toContain("GenericError")
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
it("should handle SNS canister status return", () => {
|
|
965
|
+
const CanisterStatusResponse = IDL.Record({
|
|
966
|
+
status: IDL.Variant({
|
|
967
|
+
running: IDL.Null,
|
|
968
|
+
stopping: IDL.Null,
|
|
969
|
+
stopped: IDL.Null,
|
|
970
|
+
}),
|
|
971
|
+
settings: IDL.Record({
|
|
972
|
+
controllers: IDL.Vec(IDL.Principal),
|
|
973
|
+
compute_allocation: IDL.Nat,
|
|
974
|
+
memory_allocation: IDL.Nat,
|
|
975
|
+
freezing_threshold: IDL.Nat,
|
|
976
|
+
}),
|
|
977
|
+
module_hash: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
978
|
+
memory_size: IDL.Nat,
|
|
979
|
+
cycles: IDL.Nat,
|
|
980
|
+
idle_cycles_burned_per_day: IDL.Nat,
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
const field = visitor.visitRecord(
|
|
984
|
+
CanisterStatusResponse,
|
|
985
|
+
[
|
|
986
|
+
[
|
|
987
|
+
"status",
|
|
988
|
+
IDL.Variant({
|
|
989
|
+
running: IDL.Null,
|
|
990
|
+
stopping: IDL.Null,
|
|
991
|
+
stopped: IDL.Null,
|
|
992
|
+
}),
|
|
993
|
+
],
|
|
994
|
+
[
|
|
995
|
+
"settings",
|
|
996
|
+
IDL.Record({
|
|
997
|
+
controllers: IDL.Vec(IDL.Principal),
|
|
998
|
+
compute_allocation: IDL.Nat,
|
|
999
|
+
memory_allocation: IDL.Nat,
|
|
1000
|
+
freezing_threshold: IDL.Nat,
|
|
1001
|
+
}),
|
|
1002
|
+
],
|
|
1003
|
+
["module_hash", IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
1004
|
+
["memory_size", IDL.Nat],
|
|
1005
|
+
["cycles", IDL.Nat],
|
|
1006
|
+
["idle_cycles_burned_per_day", IDL.Nat],
|
|
1007
|
+
],
|
|
1008
|
+
"status"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
expect(field.type).toBe("record")
|
|
1012
|
+
|
|
1013
|
+
// Check status variant
|
|
1014
|
+
const statusField = field.fields.find((f) => f.label === "status")
|
|
1015
|
+
if (!statusField || statusField.type !== "variant") {
|
|
1016
|
+
throw new Error("Status field is not variant")
|
|
1017
|
+
}
|
|
1018
|
+
expect(statusField.type).toBe("variant")
|
|
1019
|
+
expect(statusField.options).toContain("running")
|
|
1020
|
+
expect(statusField.options).toContain("stopping")
|
|
1021
|
+
expect(statusField.options).toContain("stopped")
|
|
1022
|
+
expect(statusField.options).toHaveLength(3)
|
|
1023
|
+
|
|
1024
|
+
// Check settings record
|
|
1025
|
+
const settingsField = field.fields.find((f) => f.label === "settings")
|
|
1026
|
+
if (!settingsField || settingsField.type !== "record") {
|
|
1027
|
+
throw new Error("Settings field is not record")
|
|
1028
|
+
}
|
|
1029
|
+
expect(settingsField.type).toBe("record")
|
|
1030
|
+
|
|
1031
|
+
const controllersField = settingsField.fields.find(
|
|
1032
|
+
(f) => f.label === "controllers"
|
|
1033
|
+
)
|
|
1034
|
+
if (!controllersField || controllersField.type !== "vector") {
|
|
1035
|
+
throw new Error("Controllers field is not vector")
|
|
1036
|
+
}
|
|
1037
|
+
expect(controllersField.type).toBe("vector")
|
|
1038
|
+
expect(controllersField.itemField.type).toBe("principal")
|
|
1039
|
+
|
|
1040
|
+
// Check cycles - should detect special format
|
|
1041
|
+
const cyclesField = field.fields.find((f) => f.label === "cycles")
|
|
1042
|
+
if (!cyclesField || cyclesField.type !== "number") {
|
|
1043
|
+
throw new Error("Cycles field is not number")
|
|
1044
|
+
}
|
|
1045
|
+
expect(cyclesField.numberFormat).toBe("cycle")
|
|
1046
|
+
|
|
1047
|
+
// Check module_hash - optional blob
|
|
1048
|
+
const moduleHashField = field.fields.find(
|
|
1049
|
+
(f) => f.label === "module_hash"
|
|
1050
|
+
) as OptionalResultField
|
|
1051
|
+
expect(moduleHashField.type).toBe("optional")
|
|
1052
|
+
expect(moduleHashField.innerField.type).toBe("blob")
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
it("should handle complex governance proposal types", () => {
|
|
1056
|
+
const ProposalInfo = IDL.Record({
|
|
1057
|
+
id: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
|
|
1058
|
+
status: IDL.Nat32,
|
|
1059
|
+
topic: IDL.Nat32,
|
|
1060
|
+
failure_reason: IDL.Opt(
|
|
1061
|
+
IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
|
|
1062
|
+
),
|
|
1063
|
+
ballots: IDL.Vec(
|
|
1064
|
+
IDL.Tuple(
|
|
1065
|
+
IDL.Nat64,
|
|
1066
|
+
IDL.Record({
|
|
1067
|
+
vote: IDL.Nat32,
|
|
1068
|
+
voting_power: IDL.Nat64,
|
|
1069
|
+
})
|
|
1070
|
+
)
|
|
1071
|
+
),
|
|
1072
|
+
proposal_timestamp_seconds: IDL.Nat64,
|
|
1073
|
+
reward_event_round: IDL.Nat64,
|
|
1074
|
+
deadline_timestamp_seconds: IDL.Opt(IDL.Nat64),
|
|
1075
|
+
executed_timestamp_seconds: IDL.Nat64,
|
|
1076
|
+
reject_cost_e8s: IDL.Nat64,
|
|
1077
|
+
proposer: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
|
|
1078
|
+
reward_status: IDL.Nat32,
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
const field = visitor.visitRecord(
|
|
1082
|
+
ProposalInfo,
|
|
1083
|
+
[
|
|
1084
|
+
["id", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
|
|
1085
|
+
["status", IDL.Nat32],
|
|
1086
|
+
["topic", IDL.Nat32],
|
|
1087
|
+
[
|
|
1088
|
+
"failure_reason",
|
|
1089
|
+
IDL.Opt(
|
|
1090
|
+
IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
|
|
1091
|
+
),
|
|
1092
|
+
],
|
|
1093
|
+
[
|
|
1094
|
+
"ballots",
|
|
1095
|
+
IDL.Vec(
|
|
1096
|
+
IDL.Tuple(
|
|
1097
|
+
IDL.Nat64,
|
|
1098
|
+
IDL.Record({
|
|
1099
|
+
vote: IDL.Nat32,
|
|
1100
|
+
voting_power: IDL.Nat64,
|
|
1101
|
+
})
|
|
1102
|
+
)
|
|
1103
|
+
),
|
|
1104
|
+
],
|
|
1105
|
+
["proposal_timestamp_seconds", IDL.Nat64],
|
|
1106
|
+
["reward_event_round", IDL.Nat64],
|
|
1107
|
+
["deadline_timestamp_seconds", IDL.Opt(IDL.Nat64)],
|
|
1108
|
+
["executed_timestamp_seconds", IDL.Nat64],
|
|
1109
|
+
["reject_cost_e8s: IDL.Nat64", IDL.Nat64], // Fixed label in original
|
|
1110
|
+
["proposer", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
|
|
1111
|
+
["reward_status", IDL.Nat32],
|
|
1112
|
+
],
|
|
1113
|
+
"proposal"
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
expect(field.type).toBe("record")
|
|
1117
|
+
expect(field.fields.length).toBeGreaterThan(8)
|
|
1118
|
+
|
|
1119
|
+
// Check timestamp field (note: the label pattern matching may not match "proposal_timestamp_seconds")
|
|
1120
|
+
const timestampField = field.fields.find(
|
|
1121
|
+
(f) => f.label === "proposal_timestamp_seconds"
|
|
1122
|
+
)
|
|
1123
|
+
if (!timestampField || timestampField.type !== "number") {
|
|
1124
|
+
throw new Error("Timestamp field not found or not number")
|
|
1125
|
+
}
|
|
1126
|
+
expect(timestampField.type).toBe("number")
|
|
1127
|
+
expect(timestampField.displayType).toBe("string") // nat64 → string
|
|
1128
|
+
|
|
1129
|
+
// Check ballots - vec of tuples
|
|
1130
|
+
const ballotsField = field.fields.find((f) => f.label === "ballots")
|
|
1131
|
+
if (!ballotsField || ballotsField.type !== "vector") {
|
|
1132
|
+
throw new Error("Ballots field not found or not vector")
|
|
1133
|
+
}
|
|
1134
|
+
expect(ballotsField.type).toBe("vector")
|
|
1135
|
+
expect(ballotsField.itemField.type).toBe("tuple")
|
|
1136
|
+
|
|
1137
|
+
const ballotTuple = ballotsField.itemField
|
|
1138
|
+
if (ballotTuple.type !== "tuple") {
|
|
1139
|
+
throw new Error("Ballot item is not tuple")
|
|
1140
|
+
}
|
|
1141
|
+
expect(ballotTuple.fields).toHaveLength(2)
|
|
1142
|
+
expect(ballotTuple.fields[0].type).toBe("number") // nat64
|
|
1143
|
+
expect(ballotTuple.fields[1].type).toBe("record") // ballot record
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1148
|
+
// Display Type Verification
|
|
1149
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1150
|
+
|
|
1151
|
+
describe("Display Type Verification", () => {
|
|
1152
|
+
it("should correctly map all types to their display types", () => {
|
|
1153
|
+
const testCases: Array<{ type: IDL.Type; expectedDisplay: string }> = [
|
|
1154
|
+
{ type: IDL.Text, expectedDisplay: "string" },
|
|
1155
|
+
{ type: IDL.Principal, expectedDisplay: "string" },
|
|
1156
|
+
{ type: IDL.Nat, expectedDisplay: "string" },
|
|
1157
|
+
{ type: IDL.Int, expectedDisplay: "string" },
|
|
1158
|
+
{ type: IDL.Nat64, expectedDisplay: "string" },
|
|
1159
|
+
{ type: IDL.Int64, expectedDisplay: "string" },
|
|
1160
|
+
{ type: IDL.Nat8, expectedDisplay: "number" },
|
|
1161
|
+
{ type: IDL.Nat16, expectedDisplay: "number" },
|
|
1162
|
+
{ type: IDL.Nat32, expectedDisplay: "number" },
|
|
1163
|
+
{ type: IDL.Int8, expectedDisplay: "number" },
|
|
1164
|
+
{ type: IDL.Int16, expectedDisplay: "number" },
|
|
1165
|
+
{ type: IDL.Int32, expectedDisplay: "number" },
|
|
1166
|
+
{ type: IDL.Float32, expectedDisplay: "number" },
|
|
1167
|
+
{ type: IDL.Float64, expectedDisplay: "number" },
|
|
1168
|
+
{ type: IDL.Bool, expectedDisplay: "boolean" },
|
|
1169
|
+
{ type: IDL.Null, expectedDisplay: "null" },
|
|
1170
|
+
{ type: IDL.Vec(IDL.Nat8), expectedDisplay: "string" }, // blob → hex string
|
|
1171
|
+
{ type: IDL.Vec(IDL.Text), expectedDisplay: "array" },
|
|
1172
|
+
{ type: IDL.Opt(IDL.Text), expectedDisplay: "nullable" },
|
|
1173
|
+
{ type: IDL.Record({ a: IDL.Text }), expectedDisplay: "object" },
|
|
1174
|
+
{ type: IDL.Tuple(IDL.Text, IDL.Nat), expectedDisplay: "array" },
|
|
1175
|
+
]
|
|
1176
|
+
|
|
1177
|
+
testCases.forEach(({ type, expectedDisplay }) => {
|
|
1178
|
+
const field = type.accept(visitor, "test")
|
|
1179
|
+
if (typeof field === "object" && "displayType" in field) {
|
|
1180
|
+
expect(field.displayType).toBe(expectedDisplay)
|
|
1181
|
+
} else {
|
|
1182
|
+
throw new Error("Expected a ResultField")
|
|
1183
|
+
}
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1189
|
+
// resolve() Method Tests
|
|
1190
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1191
|
+
|
|
1192
|
+
describe("resolve() Method", () => {
|
|
1193
|
+
describe("Primitive Types", () => {
|
|
1194
|
+
it("should resolve text field with value", () => {
|
|
1195
|
+
const field = visitor.visitText(IDL.Text, "message")
|
|
1196
|
+
const resolved = field.resolve("Hello World")
|
|
1197
|
+
|
|
1198
|
+
expect(resolved.field).toBe(field)
|
|
1199
|
+
expect(resolved.value).toBe("Hello World")
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
it("should resolve number field with value", () => {
|
|
1203
|
+
const field = visitor.visitNat(IDL.Nat, "amount")
|
|
1204
|
+
const resolved = field.resolve(BigInt(1000000))
|
|
1205
|
+
|
|
1206
|
+
expect(resolved.field).toBe(field)
|
|
1207
|
+
expect(resolved.value).toBe("1000000")
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
it("should resolve boolean field with value", () => {
|
|
1211
|
+
const field = visitor.visitBool(IDL.Bool, "active")
|
|
1212
|
+
const resolved = field.resolve(true)
|
|
1213
|
+
|
|
1214
|
+
expect(resolved.field).toBe(field)
|
|
1215
|
+
expect(resolved.value).toBe(true)
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
it("should resolve null field", () => {
|
|
1219
|
+
const field = visitor.visitNull(IDL.Null, "empty")
|
|
1220
|
+
const resolved = field.resolve(null)
|
|
1221
|
+
|
|
1222
|
+
expect(resolved.field).toBe(field)
|
|
1223
|
+
expect(resolved.value).toBe(null)
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
it("should resolve principal field with string value", () => {
|
|
1227
|
+
const field = visitor.visitPrincipal(IDL.Principal, "owner")
|
|
1228
|
+
const resolved = field.resolve("aaaaa-aa")
|
|
1229
|
+
|
|
1230
|
+
expect(resolved.field).toBe(field)
|
|
1231
|
+
expect(resolved.value).toBe("aaaaa-aa")
|
|
1232
|
+
})
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
describe("Record Type", () => {
|
|
1236
|
+
it("should resolve record with nested field values", () => {
|
|
1237
|
+
const recordType = IDL.Record({
|
|
1238
|
+
name: IDL.Text,
|
|
1239
|
+
age: IDL.Nat32,
|
|
1240
|
+
active: IDL.Bool,
|
|
1241
|
+
})
|
|
1242
|
+
const field = visitor.visitRecord(
|
|
1243
|
+
recordType,
|
|
1244
|
+
[
|
|
1245
|
+
["name", IDL.Text],
|
|
1246
|
+
["age", IDL.Nat32],
|
|
1247
|
+
["active", IDL.Bool],
|
|
1248
|
+
],
|
|
1249
|
+
"user"
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
const resolved = field.resolve({
|
|
1253
|
+
name: "Alice",
|
|
1254
|
+
age: 30,
|
|
1255
|
+
active: true,
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
expect(resolved.field).toBe(field)
|
|
1259
|
+
const value = resolved.value as Record<
|
|
1260
|
+
string,
|
|
1261
|
+
{ field: ResultField; value: unknown }
|
|
1262
|
+
>
|
|
1263
|
+
expect(value.name.value).toBe("Alice")
|
|
1264
|
+
expect(value.age.value).toBe(30)
|
|
1265
|
+
expect(value.active.value).toBe(true)
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
it("should handle null record value", () => {
|
|
1269
|
+
const recordType = IDL.Record({ name: IDL.Text })
|
|
1270
|
+
const field = visitor.visitRecord(
|
|
1271
|
+
recordType,
|
|
1272
|
+
[["name", IDL.Text]],
|
|
1273
|
+
"user"
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
const resolved = field.resolve(null)
|
|
1277
|
+
expect(resolved.value).toBe(null)
|
|
1278
|
+
})
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
describe("Variant Type", () => {
|
|
1282
|
+
it("should resolve variant with active option", () => {
|
|
1283
|
+
const variantType = IDL.Variant({
|
|
1284
|
+
Ok: IDL.Text,
|
|
1285
|
+
Err: IDL.Text,
|
|
1286
|
+
})
|
|
1287
|
+
const field = visitor.visitVariant(
|
|
1288
|
+
variantType,
|
|
1289
|
+
[
|
|
1290
|
+
["Ok", IDL.Text],
|
|
1291
|
+
["Err", IDL.Text],
|
|
1292
|
+
],
|
|
1293
|
+
"result"
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
const resolved = field.resolve({ Ok: "Success" })
|
|
1297
|
+
|
|
1298
|
+
const value = resolved.value as {
|
|
1299
|
+
option: string
|
|
1300
|
+
value: { field: ResultField; value: unknown }
|
|
1301
|
+
}
|
|
1302
|
+
expect(value.option).toBe("Ok")
|
|
1303
|
+
expect(value.value.value).toBe("Success")
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
it("should resolve variant with Err option", () => {
|
|
1307
|
+
const variantType = IDL.Variant({
|
|
1308
|
+
Ok: IDL.Nat,
|
|
1309
|
+
Err: IDL.Text,
|
|
1310
|
+
})
|
|
1311
|
+
const field = visitor.visitVariant(
|
|
1312
|
+
variantType,
|
|
1313
|
+
[
|
|
1314
|
+
["Ok", IDL.Nat],
|
|
1315
|
+
["Err", IDL.Text],
|
|
1316
|
+
],
|
|
1317
|
+
"result"
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
const resolved = field.resolve({ Err: "Something went wrong" })
|
|
1321
|
+
|
|
1322
|
+
const value = resolved.value as {
|
|
1323
|
+
option: string
|
|
1324
|
+
value: { field: ResultField; value: unknown }
|
|
1325
|
+
}
|
|
1326
|
+
expect(value.option).toBe("Err")
|
|
1327
|
+
expect(value.value.value).toBe("Something went wrong")
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
it("should handle null variant value", () => {
|
|
1331
|
+
const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
|
|
1332
|
+
const field = visitor.visitVariant(
|
|
1333
|
+
variantType,
|
|
1334
|
+
[
|
|
1335
|
+
["A", IDL.Null],
|
|
1336
|
+
["B", IDL.Text],
|
|
1337
|
+
],
|
|
1338
|
+
"choice"
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
const resolved = field.resolve(null)
|
|
1342
|
+
expect(resolved.value).toBe(null)
|
|
1343
|
+
})
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
describe("Tuple Type", () => {
|
|
1347
|
+
it("should resolve tuple with indexed values", () => {
|
|
1348
|
+
const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
|
|
1349
|
+
const field = visitor.visitTuple(
|
|
1350
|
+
tupleType,
|
|
1351
|
+
[IDL.Text, IDL.Nat, IDL.Bool],
|
|
1352
|
+
"data"
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
const resolved = field.resolve(["hello", 123n, true])
|
|
1356
|
+
|
|
1357
|
+
expect(resolved.field).toBe(field)
|
|
1358
|
+
const value = resolved.value as Array<{
|
|
1359
|
+
field: ResultField
|
|
1360
|
+
value: unknown
|
|
1361
|
+
}>
|
|
1362
|
+
expect(value).toHaveLength(3)
|
|
1363
|
+
expect(value[0].value).toBe("hello")
|
|
1364
|
+
expect(value[1].value).toBe("123")
|
|
1365
|
+
expect(value[2].value).toBe(true)
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1368
|
+
it("should handle null tuple value", () => {
|
|
1369
|
+
const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
|
|
1370
|
+
const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
|
|
1371
|
+
|
|
1372
|
+
const resolved = field.resolve(null)
|
|
1373
|
+
expect(resolved.value).toBe(null)
|
|
1374
|
+
})
|
|
1375
|
+
})
|
|
1376
|
+
|
|
1377
|
+
describe("Optional Type", () => {
|
|
1378
|
+
it("should resolve optional with value", () => {
|
|
1379
|
+
const optType = IDL.Opt(IDL.Text)
|
|
1380
|
+
const field = visitor.visitOpt(optType, IDL.Text, "nickname")
|
|
1381
|
+
|
|
1382
|
+
const resolved = field.resolve(["Bob"])
|
|
1383
|
+
|
|
1384
|
+
expect(resolved.field).toBe(field)
|
|
1385
|
+
const inner = resolved.value as { field: ResultField; value: unknown }
|
|
1386
|
+
expect(inner.value).toBe("Bob")
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
it("should resolve optional with null", () => {
|
|
1390
|
+
const optType = IDL.Opt(IDL.Text)
|
|
1391
|
+
const field = visitor.visitOpt(optType, IDL.Text, "nickname")
|
|
1392
|
+
|
|
1393
|
+
const resolved = field.resolve(null)
|
|
1394
|
+
|
|
1395
|
+
expect(resolved.field).toBe(field)
|
|
1396
|
+
expect(resolved.value).toBe(null)
|
|
1397
|
+
})
|
|
1398
|
+
})
|
|
1399
|
+
|
|
1400
|
+
describe("Vector Type", () => {
|
|
1401
|
+
it("should resolve vector with array of values", () => {
|
|
1402
|
+
const vecType = IDL.Vec(IDL.Text)
|
|
1403
|
+
const field = visitor.visitVec(vecType, IDL.Text, "tags")
|
|
1404
|
+
|
|
1405
|
+
const resolved = field.resolve(["a", "b", "c"])
|
|
1406
|
+
|
|
1407
|
+
expect(resolved.field).toBe(field)
|
|
1408
|
+
const value = resolved.value as Array<{
|
|
1409
|
+
field: ResultField
|
|
1410
|
+
value: unknown
|
|
1411
|
+
}>
|
|
1412
|
+
expect(value).toHaveLength(3)
|
|
1413
|
+
expect(value[0].value).toBe("a")
|
|
1414
|
+
expect(value[1].value).toBe("b")
|
|
1415
|
+
expect(value[2].value).toBe("c")
|
|
1416
|
+
})
|
|
1417
|
+
|
|
1418
|
+
it("should handle empty vector", () => {
|
|
1419
|
+
const vecType = IDL.Vec(IDL.Nat)
|
|
1420
|
+
const field = visitor.visitVec(vecType, IDL.Nat, "numbers")
|
|
1421
|
+
|
|
1422
|
+
const resolved = field.resolve([])
|
|
1423
|
+
|
|
1424
|
+
const value = resolved.value as Array<{
|
|
1425
|
+
field: ResultField
|
|
1426
|
+
value: unknown
|
|
1427
|
+
}>
|
|
1428
|
+
expect(value).toHaveLength(0)
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1431
|
+
it("should handle null vector value", () => {
|
|
1432
|
+
const vecType = IDL.Vec(IDL.Text)
|
|
1433
|
+
const field = visitor.visitVec(vecType, IDL.Text, "items")
|
|
1434
|
+
|
|
1435
|
+
const resolved = field.resolve(null)
|
|
1436
|
+
expect(resolved.value).toBe(null)
|
|
1437
|
+
})
|
|
1438
|
+
})
|
|
1439
|
+
|
|
1440
|
+
describe("Blob Type", () => {
|
|
1441
|
+
it("should resolve blob with hex string value", () => {
|
|
1442
|
+
const blobType = IDL.Vec(IDL.Nat8)
|
|
1443
|
+
const field = visitor.visitVec(blobType, IDL.Nat8, "data")
|
|
1444
|
+
|
|
1445
|
+
const resolved = field.resolve(new Uint8Array([0x12, 0x34, 0xab, 0xcd]))
|
|
1446
|
+
|
|
1447
|
+
expect(resolved.field).toBe(field)
|
|
1448
|
+
expect(resolved.value).toBe("1234abcd")
|
|
1449
|
+
})
|
|
1450
|
+
})
|
|
1451
|
+
|
|
1452
|
+
describe("Recursive Type", () => {
|
|
1453
|
+
it("should resolve recursive type", () => {
|
|
1454
|
+
const Node = IDL.Rec()
|
|
1455
|
+
Node.fill(
|
|
1456
|
+
IDL.Record({
|
|
1457
|
+
value: IDL.Nat,
|
|
1458
|
+
children: IDL.Vec(Node),
|
|
1459
|
+
})
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
const field = visitor.visitRec(
|
|
1463
|
+
Node,
|
|
1464
|
+
// We can just pass null or the constructed type if available, but visitRec implementation calls extract which calls accept on the internal type.
|
|
1465
|
+
IDL.Record({
|
|
1466
|
+
value: IDL.Nat,
|
|
1467
|
+
children: IDL.Vec(Node),
|
|
1468
|
+
}),
|
|
1469
|
+
"tree"
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
const resolved = field.resolve({
|
|
1473
|
+
value: BigInt(42),
|
|
1474
|
+
children: [],
|
|
1475
|
+
})
|
|
1476
|
+
|
|
1477
|
+
// The recursive type should delegate to its inner record type
|
|
1478
|
+
expect(resolved.field.type).toBe("record")
|
|
1479
|
+
})
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
describe("Nested Structures", () => {
|
|
1483
|
+
it("should resolve deeply nested structure", () => {
|
|
1484
|
+
const nestedType = IDL.Record({
|
|
1485
|
+
user: IDL.Record({
|
|
1486
|
+
profile: IDL.Record({
|
|
1487
|
+
name: IDL.Text,
|
|
1488
|
+
verified: IDL.Bool,
|
|
1489
|
+
}),
|
|
1490
|
+
}),
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
const field = visitor.visitRecord(
|
|
1494
|
+
nestedType,
|
|
1495
|
+
[
|
|
1496
|
+
[
|
|
1497
|
+
"user",
|
|
1498
|
+
IDL.Record({
|
|
1499
|
+
profile: IDL.Record({
|
|
1500
|
+
name: IDL.Text,
|
|
1501
|
+
verified: IDL.Bool,
|
|
1502
|
+
}),
|
|
1503
|
+
}),
|
|
1504
|
+
],
|
|
1505
|
+
],
|
|
1506
|
+
"data"
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
const resolved = field.resolve({
|
|
1510
|
+
user: {
|
|
1511
|
+
profile: {
|
|
1512
|
+
name: "Alice",
|
|
1513
|
+
verified: true,
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
const value = resolved.value as Record<
|
|
1519
|
+
string,
|
|
1520
|
+
{ field: ResultField; value: unknown }
|
|
1521
|
+
>
|
|
1522
|
+
const userValue = value.user.value as Record<
|
|
1523
|
+
string,
|
|
1524
|
+
{ field: ResultField; value: unknown }
|
|
1525
|
+
>
|
|
1526
|
+
const profileValue = userValue.profile.value as Record<
|
|
1527
|
+
string,
|
|
1528
|
+
{ field: ResultField; value: unknown }
|
|
1529
|
+
>
|
|
1530
|
+
expect(profileValue.name.value).toBe("Alice")
|
|
1531
|
+
expect(profileValue.verified.value).toBe(true)
|
|
1532
|
+
})
|
|
1533
|
+
})
|
|
1534
|
+
})
|
|
1535
|
+
|
|
1536
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1537
|
+
// generateMetadata() Method Tests
|
|
1538
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1539
|
+
|
|
1540
|
+
describe("generateMetadata() Method", () => {
|
|
1541
|
+
it("should generate metadata for single return value", () => {
|
|
1542
|
+
const service = IDL.Service({
|
|
1543
|
+
getName: IDL.Func([], [IDL.Text], ["query"]),
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
const serviceMeta = visitor.visitService(
|
|
1547
|
+
service as unknown as IDL.ServiceClass
|
|
1548
|
+
)
|
|
1549
|
+
const methodMeta = serviceMeta["getName"]
|
|
1550
|
+
|
|
1551
|
+
const result = methodMeta.generateMetadata("Alice")
|
|
1552
|
+
|
|
1553
|
+
expect(result.functionName).toBe("getName")
|
|
1554
|
+
expect(result.functionType).toBe("query")
|
|
1555
|
+
expect(result.results).toHaveLength(1)
|
|
1556
|
+
expect(result.results[0].value).toBe("Alice")
|
|
1557
|
+
expect(result.results[0].field.type).toBe("text")
|
|
1558
|
+
})
|
|
1559
|
+
|
|
1560
|
+
it("should generate metadata for multiple return values", () => {
|
|
1561
|
+
const service = IDL.Service({
|
|
1562
|
+
getStats: IDL.Func([], [IDL.Nat, IDL.Nat, IDL.Text], ["query"]),
|
|
1563
|
+
})
|
|
1564
|
+
|
|
1565
|
+
const serviceMeta = visitor.visitService(
|
|
1566
|
+
service as unknown as IDL.ServiceClass
|
|
1567
|
+
)
|
|
1568
|
+
const methodMeta = serviceMeta["getStats"]
|
|
1569
|
+
|
|
1570
|
+
const result = methodMeta.generateMetadata([
|
|
1571
|
+
BigInt(100),
|
|
1572
|
+
BigInt(200),
|
|
1573
|
+
"active",
|
|
1574
|
+
])
|
|
1575
|
+
|
|
1576
|
+
expect(result.results).toHaveLength(3)
|
|
1577
|
+
expect(result.results[0].value).toBe("100")
|
|
1578
|
+
expect(result.results[0].field.type).toBe("number")
|
|
1579
|
+
expect(result.results[1].value).toBe("200")
|
|
1580
|
+
expect(result.results[2].value).toBe("active")
|
|
1581
|
+
expect(result.results[2].field.type).toBe("text")
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
it("should generate metadata for record return value", () => {
|
|
1585
|
+
const service = IDL.Service({
|
|
1586
|
+
getUser: IDL.Func(
|
|
1587
|
+
[],
|
|
1588
|
+
[IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
|
|
1589
|
+
["query"]
|
|
1590
|
+
),
|
|
1591
|
+
})
|
|
1592
|
+
|
|
1593
|
+
const serviceMeta = visitor.visitService(
|
|
1594
|
+
service as unknown as IDL.ServiceClass
|
|
1595
|
+
)
|
|
1596
|
+
const methodMeta = serviceMeta["getUser"]
|
|
1597
|
+
|
|
1598
|
+
const result = methodMeta.generateMetadata({
|
|
1599
|
+
name: "Bob",
|
|
1600
|
+
balance: BigInt(1000),
|
|
1601
|
+
})
|
|
1602
|
+
|
|
1603
|
+
expect(result.results).toHaveLength(1)
|
|
1604
|
+
expect(result.results[0].field.type).toBe("record")
|
|
1605
|
+
|
|
1606
|
+
const recordValue = result.results[0].value
|
|
1607
|
+
if (typeof recordValue !== "object" || recordValue === null) {
|
|
1608
|
+
throw new Error("Expected record value object")
|
|
1609
|
+
}
|
|
1610
|
+
const val = recordValue as Record<
|
|
1611
|
+
string,
|
|
1612
|
+
{ field: ResultField; value: unknown }
|
|
1613
|
+
>
|
|
1614
|
+
expect(val.name.value).toBe("Bob")
|
|
1615
|
+
expect(val.balance.value).toBe("1000")
|
|
1616
|
+
})
|
|
1617
|
+
|
|
1618
|
+
it("should generate metadata for Result variant", () => {
|
|
1619
|
+
const service = IDL.Service({
|
|
1620
|
+
transfer: IDL.Func(
|
|
1621
|
+
[],
|
|
1622
|
+
[IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
|
|
1623
|
+
[]
|
|
1624
|
+
),
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
const serviceMeta = visitor.visitService(
|
|
1628
|
+
service as unknown as IDL.ServiceClass
|
|
1629
|
+
)
|
|
1630
|
+
const methodMeta = serviceMeta["transfer"]
|
|
1631
|
+
|
|
1632
|
+
// Test Ok case
|
|
1633
|
+
const okResult = methodMeta.generateMetadata({ Ok: BigInt(12345) })
|
|
1634
|
+
expect(okResult.results[0].field.type).toBe("variant")
|
|
1635
|
+
if (okResult.results[0].field.type === "variant") {
|
|
1636
|
+
expect(okResult.results[0].field.displayType).toBe("result")
|
|
1637
|
+
} else {
|
|
1638
|
+
throw new Error("Expected variant field")
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const okValue = okResult.results[0].value as {
|
|
1642
|
+
option: string
|
|
1643
|
+
value: { field: ResultField; value: unknown }
|
|
1644
|
+
}
|
|
1645
|
+
expect(okValue.option).toBe("Ok")
|
|
1646
|
+
expect(okValue.value.value).toBe("12345")
|
|
1647
|
+
|
|
1648
|
+
// Test Err case
|
|
1649
|
+
const errResult = methodMeta.generateMetadata({
|
|
1650
|
+
Err: "Insufficient funds",
|
|
1651
|
+
})
|
|
1652
|
+
const errValue = errResult.results[0].value as {
|
|
1653
|
+
option: string
|
|
1654
|
+
value: { field: ResultField; value: unknown }
|
|
1655
|
+
}
|
|
1656
|
+
expect(errValue.option).toBe("Err")
|
|
1657
|
+
expect(errValue.value.value).toBe("Insufficient funds")
|
|
1658
|
+
})
|
|
1659
|
+
|
|
1660
|
+
it("should generate metadata for optional return value", () => {
|
|
1661
|
+
const service = IDL.Service({
|
|
1662
|
+
findUser: IDL.Func([], [IDL.Opt(IDL.Text)], ["query"]),
|
|
1663
|
+
})
|
|
1664
|
+
|
|
1665
|
+
const serviceMeta = visitor.visitService(
|
|
1666
|
+
service as unknown as IDL.ServiceClass
|
|
1667
|
+
)
|
|
1668
|
+
const methodMeta = serviceMeta["findUser"]
|
|
1669
|
+
|
|
1670
|
+
// Test with value - Optional is [value]
|
|
1671
|
+
const foundResult = methodMeta.generateMetadata(["Alice"])
|
|
1672
|
+
expect(foundResult.results[0].field.type).toBe("optional")
|
|
1673
|
+
const foundInner = foundResult.results[0].value as {
|
|
1674
|
+
field: ResultField
|
|
1675
|
+
value: unknown
|
|
1676
|
+
}
|
|
1677
|
+
expect(foundInner.value).toBe("Alice")
|
|
1678
|
+
|
|
1679
|
+
// Test with null - optional is []
|
|
1680
|
+
const notFoundResult = methodMeta.generateMetadata([])
|
|
1681
|
+
expect(notFoundResult.results[0].value).toBe(null)
|
|
1682
|
+
})
|
|
1683
|
+
|
|
1684
|
+
it("should generate metadata for vector return value", () => {
|
|
1685
|
+
const service = IDL.Service({
|
|
1686
|
+
getItems: IDL.Func([], [IDL.Vec(IDL.Text)], ["query"]),
|
|
1687
|
+
})
|
|
1688
|
+
|
|
1689
|
+
const serviceMeta = visitor.visitService(
|
|
1690
|
+
service as unknown as IDL.ServiceClass
|
|
1691
|
+
)
|
|
1692
|
+
const methodMeta = serviceMeta["getItems"]
|
|
1693
|
+
|
|
1694
|
+
const result = methodMeta.generateMetadata(["item1", "item2", "item3"])
|
|
1695
|
+
|
|
1696
|
+
expect(result.results[0].field.type).toBe("vector")
|
|
1697
|
+
const vecValue = result.results[0].value as Array<{
|
|
1698
|
+
field: ResultField
|
|
1699
|
+
value: unknown
|
|
1700
|
+
}>
|
|
1701
|
+
expect(vecValue).toHaveLength(3)
|
|
1702
|
+
expect(vecValue[0].value).toBe("item1")
|
|
1703
|
+
expect(vecValue[1].value).toBe("item2")
|
|
1704
|
+
expect(vecValue[2].value).toBe("item3")
|
|
1705
|
+
})
|
|
1706
|
+
|
|
1707
|
+
it("should generate metadata for update function", () => {
|
|
1708
|
+
const service = IDL.Service({
|
|
1709
|
+
setName: IDL.Func([IDL.Text], [IDL.Bool], []),
|
|
1710
|
+
})
|
|
1711
|
+
|
|
1712
|
+
const serviceMeta = visitor.visitService(
|
|
1713
|
+
service as unknown as IDL.ServiceClass
|
|
1714
|
+
)
|
|
1715
|
+
const methodMeta = serviceMeta["setName"]
|
|
1716
|
+
|
|
1717
|
+
expect(methodMeta.functionType).toBe("update")
|
|
1718
|
+
|
|
1719
|
+
const rawData = [true]
|
|
1720
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1721
|
+
|
|
1722
|
+
expect(result.functionType).toBe("update")
|
|
1723
|
+
expect(result.functionName).toBe("setName")
|
|
1724
|
+
expect(result.results[0].value).toBe(true)
|
|
1725
|
+
expect(result.results[0].raw).toBe(true)
|
|
1726
|
+
})
|
|
1727
|
+
|
|
1728
|
+
it("should generate metadata for complex ICRC-1 like response", () => {
|
|
1729
|
+
const Account = IDL.Record({
|
|
1730
|
+
owner: IDL.Principal,
|
|
1731
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
const TransferResult = IDL.Variant({
|
|
1735
|
+
Ok: IDL.Nat,
|
|
1736
|
+
Err: IDL.Variant({
|
|
1737
|
+
InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
|
|
1738
|
+
InvalidAccount: IDL.Null,
|
|
1739
|
+
TooOld: IDL.Null,
|
|
1740
|
+
}),
|
|
1741
|
+
})
|
|
1742
|
+
|
|
1743
|
+
const service = IDL.Service({
|
|
1744
|
+
icrc1_transfer: IDL.Func([], [TransferResult], []),
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
const serviceMeta = visitor.visitService(
|
|
1748
|
+
service as unknown as IDL.ServiceClass
|
|
1749
|
+
)
|
|
1750
|
+
const methodMeta = serviceMeta["icrc1_transfer"]
|
|
1751
|
+
|
|
1752
|
+
// Test successful transfer
|
|
1753
|
+
const successResult = methodMeta.generateMetadata({ Ok: BigInt(1000) })
|
|
1754
|
+
console.log(
|
|
1755
|
+
"🚀 ~ result:",
|
|
1756
|
+
JSON.stringify(
|
|
1757
|
+
successResult,
|
|
1758
|
+
(_, v) => (typeof v === "bigint" ? `${v}n` : v),
|
|
1759
|
+
2
|
|
1760
|
+
)
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
const successValue = successResult.results[0].value as {
|
|
1764
|
+
option: string
|
|
1765
|
+
value: { field: ResultField; value: unknown }
|
|
1766
|
+
}
|
|
1767
|
+
expect(successValue.option).toBe("Ok")
|
|
1768
|
+
expect(successValue.value.value).toBe("1000")
|
|
1769
|
+
|
|
1770
|
+
// Test error case
|
|
1771
|
+
const errorResult = methodMeta.generateMetadata({
|
|
1772
|
+
Err: { InsufficientFunds: { balance: BigInt(50) } },
|
|
1773
|
+
})
|
|
1774
|
+
const errorValue = errorResult.results[0].value as {
|
|
1775
|
+
option: string
|
|
1776
|
+
value: { field: ResultField; value: unknown }
|
|
1777
|
+
}
|
|
1778
|
+
expect(errorValue.option).toBe("Err")
|
|
1779
|
+
|
|
1780
|
+
const val = errorValue.value.value
|
|
1781
|
+
if (typeof val !== "object" || val === null || !("option" in val)) {
|
|
1782
|
+
throw new Error("Expected variant value object")
|
|
1783
|
+
}
|
|
1784
|
+
expect(val.option).toBe("InsufficientFunds")
|
|
1785
|
+
})
|
|
1786
|
+
|
|
1787
|
+
it("should handle empty return", () => {
|
|
1788
|
+
const service = IDL.Service({
|
|
1789
|
+
doSomething: IDL.Func([], [], []),
|
|
1790
|
+
})
|
|
1791
|
+
|
|
1792
|
+
const serviceMeta = visitor.visitService(
|
|
1793
|
+
service as unknown as IDL.ServiceClass
|
|
1794
|
+
)
|
|
1795
|
+
const methodMeta = serviceMeta["doSomething"]
|
|
1796
|
+
|
|
1797
|
+
expect(methodMeta.returnCount).toBe(0)
|
|
1798
|
+
|
|
1799
|
+
const result = methodMeta.generateMetadata([])
|
|
1800
|
+
expect(result.results).toHaveLength(0)
|
|
1801
|
+
})
|
|
1802
|
+
})
|
|
1803
|
+
|
|
1804
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1805
|
+
// generateMetadataWithRaw() Method Tests
|
|
1806
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1807
|
+
|
|
1808
|
+
describe("generateMetadataWithRaw() Method", () => {
|
|
1809
|
+
it("should include both raw and display values for single return", () => {
|
|
1810
|
+
const service = IDL.Service({
|
|
1811
|
+
getBalance: IDL.Func([], [IDL.Nat], ["query"]),
|
|
1812
|
+
})
|
|
1813
|
+
|
|
1814
|
+
const serviceMeta = visitor.visitService(
|
|
1815
|
+
service as unknown as IDL.ServiceClass
|
|
1816
|
+
)
|
|
1817
|
+
const methodMeta = serviceMeta["getBalance"]
|
|
1818
|
+
|
|
1819
|
+
// Simulate raw BigInt and display string
|
|
1820
|
+
const rawData = [BigInt(1000000)]
|
|
1821
|
+
|
|
1822
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1823
|
+
|
|
1824
|
+
expect(result.functionName).toBe("getBalance")
|
|
1825
|
+
expect(result.functionType).toBe("query")
|
|
1826
|
+
expect(result.results).toHaveLength(1)
|
|
1827
|
+
expect(result.results[0].raw).toBe(BigInt(1000000))
|
|
1828
|
+
expect(result.results[0].value).toBe("1000000")
|
|
1829
|
+
expect(result.results[0].raw).toBe(BigInt(1000000))
|
|
1830
|
+
expect(result.results[0].field.type).toBe("number")
|
|
1831
|
+
})
|
|
1832
|
+
|
|
1833
|
+
it("should include both raw and display values for multiple returns", () => {
|
|
1834
|
+
const service = IDL.Service({
|
|
1835
|
+
getStats: IDL.Func([], [IDL.Nat64, IDL.Text, IDL.Bool], ["query"]),
|
|
1836
|
+
})
|
|
1837
|
+
|
|
1838
|
+
const serviceMeta = visitor.visitService(
|
|
1839
|
+
service as unknown as IDL.ServiceClass
|
|
1840
|
+
)
|
|
1841
|
+
const methodMeta = serviceMeta["getStats"]
|
|
1842
|
+
|
|
1843
|
+
// Use BigInt with string to safe safe integer
|
|
1844
|
+
const rawData = [BigInt("9007199254740993"), "active", true]
|
|
1845
|
+
|
|
1846
|
+
const result = methodMeta.generateMetadata(rawData)
|
|
1847
|
+
|
|
1848
|
+
expect(result.results).toHaveLength(3)
|
|
1849
|
+
|
|
1850
|
+
// nat64 → string display, BigInt raw
|
|
1851
|
+
expect(result.results[0].value).toBe("9007199254740993")
|
|
1852
|
+
expect(result.results[0].raw).toBe(BigInt("9007199254740993"))
|
|
1853
|
+
expect(result.results[0].field.candidType).toBe("nat64")
|
|
1854
|
+
|
|
1855
|
+
// text → same for both
|
|
1856
|
+
expect(result.results[1].value).toBe("active")
|
|
1857
|
+
expect(result.results[1].raw).toBe("active")
|
|
1858
|
+
|
|
1859
|
+
// bool → same for both
|
|
1860
|
+
expect(result.results[2].value).toBe(true)
|
|
1861
|
+
expect(result.results[2].raw).toBe(true)
|
|
1862
|
+
})
|
|
1863
|
+
|
|
1864
|
+
it("should handle record with raw and display values", () => {
|
|
1865
|
+
const service = IDL.Service({
|
|
1866
|
+
getUser: IDL.Func(
|
|
1867
|
+
[],
|
|
1868
|
+
[IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
|
|
1869
|
+
["query"]
|
|
1870
|
+
),
|
|
1871
|
+
})
|
|
1872
|
+
|
|
1873
|
+
const serviceMeta = visitor.visitService(
|
|
1874
|
+
service as unknown as IDL.ServiceClass
|
|
1875
|
+
)
|
|
1876
|
+
const methodMeta = serviceMeta["getUser"]
|
|
1877
|
+
|
|
1878
|
+
const rawData = [{ name: "Alice", balance: BigInt(500) }]
|
|
1879
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1880
|
+
|
|
1881
|
+
expect(result.results[0].raw).toEqual({
|
|
1882
|
+
name: "Alice",
|
|
1883
|
+
balance: BigInt(500),
|
|
1884
|
+
})
|
|
1885
|
+
|
|
1886
|
+
const recordValue = result.results[0].value
|
|
1887
|
+
if (typeof recordValue !== "object" || recordValue === null) {
|
|
1888
|
+
throw new Error("Expected record value object")
|
|
1889
|
+
}
|
|
1890
|
+
const val = recordValue as Record<
|
|
1891
|
+
string,
|
|
1892
|
+
{ field: ResultField; value: unknown }
|
|
1893
|
+
>
|
|
1894
|
+
expect(val.name.value).toBe("Alice")
|
|
1895
|
+
expect(val.balance.value).toBe("500")
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
it("should handle Result variant with raw and display values", () => {
|
|
1899
|
+
const service = IDL.Service({
|
|
1900
|
+
transfer: IDL.Func(
|
|
1901
|
+
[],
|
|
1902
|
+
[IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
|
|
1903
|
+
[]
|
|
1904
|
+
),
|
|
1905
|
+
})
|
|
1906
|
+
|
|
1907
|
+
const serviceMeta = visitor.visitService(
|
|
1908
|
+
service as unknown as IDL.ServiceClass
|
|
1909
|
+
)
|
|
1910
|
+
const methodMeta = serviceMeta["transfer"]
|
|
1911
|
+
|
|
1912
|
+
// Test Ok case with raw BigInt
|
|
1913
|
+
const rawData = [{ Ok: BigInt(12345) }]
|
|
1914
|
+
|
|
1915
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1916
|
+
|
|
1917
|
+
expect(result.results[0].raw).toEqual({ Ok: BigInt(12345) })
|
|
1918
|
+
|
|
1919
|
+
const variantValue = result.results[0].value as any
|
|
1920
|
+
if (
|
|
1921
|
+
typeof variantValue !== "object" ||
|
|
1922
|
+
variantValue === null ||
|
|
1923
|
+
!("option" in variantValue)
|
|
1924
|
+
) {
|
|
1925
|
+
throw new Error("Expected variant value object")
|
|
1926
|
+
}
|
|
1927
|
+
expect(variantValue.option).toBe("Ok")
|
|
1928
|
+
const innerVal = variantValue.value
|
|
1929
|
+
expect(innerVal.value).toBe("12345")
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
it("should preserve raw Principal object", () => {
|
|
1933
|
+
const { Principal } = require("@icp-sdk/core/principal")
|
|
1934
|
+
|
|
1935
|
+
const service = IDL.Service({
|
|
1936
|
+
getOwner: IDL.Func([], [IDL.Principal], ["query"]),
|
|
1937
|
+
})
|
|
1938
|
+
|
|
1939
|
+
const serviceMeta = visitor.visitService(
|
|
1940
|
+
service as unknown as IDL.ServiceClass
|
|
1941
|
+
)
|
|
1942
|
+
const methodMeta = serviceMeta["getOwner"]
|
|
1943
|
+
|
|
1944
|
+
const principal = Principal.fromText("aaaaa-aa")
|
|
1945
|
+
const rawData = [principal]
|
|
1946
|
+
|
|
1947
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1948
|
+
|
|
1949
|
+
expect(result.results[0].value).toBe("aaaaa-aa")
|
|
1950
|
+
expect(result.results[0].raw).toBe(principal)
|
|
1951
|
+
expect(result.results[0].field.type).toBe("principal")
|
|
1952
|
+
})
|
|
1953
|
+
|
|
1954
|
+
it("should handle vector with raw and display values", () => {
|
|
1955
|
+
const service = IDL.Service({
|
|
1956
|
+
getAmounts: IDL.Func([], [IDL.Vec(IDL.Nat)], ["query"]),
|
|
1957
|
+
})
|
|
1958
|
+
|
|
1959
|
+
const serviceMeta = visitor.visitService(
|
|
1960
|
+
service as unknown as IDL.ServiceClass
|
|
1961
|
+
)
|
|
1962
|
+
const methodMeta = serviceMeta["getAmounts"]
|
|
1963
|
+
|
|
1964
|
+
const rawData = [[BigInt(100), BigInt(200), BigInt(300)]]
|
|
1965
|
+
|
|
1966
|
+
const result = methodMeta.generateMetadata(rawData[0])
|
|
1967
|
+
|
|
1968
|
+
const vecValue = result.results[0].value
|
|
1969
|
+
if (!Array.isArray(vecValue)) {
|
|
1970
|
+
throw new Error("Expected vector value array")
|
|
1971
|
+
}
|
|
1972
|
+
expect(vecValue).toHaveLength(3)
|
|
1973
|
+
expect(vecValue[0].value).toBe("100")
|
|
1974
|
+
expect(vecValue[1].value).toBe("200")
|
|
1975
|
+
expect(vecValue[2].value).toBe("300")
|
|
1976
|
+
})
|
|
1977
|
+
|
|
1978
|
+
it("should handle optional with raw and display values", () => {
|
|
1979
|
+
const service = IDL.Service({
|
|
1980
|
+
findBalance: IDL.Func([], [IDL.Opt(IDL.Nat)], ["query"]),
|
|
1981
|
+
})
|
|
1982
|
+
|
|
1983
|
+
const serviceMeta = visitor.visitService(
|
|
1984
|
+
service as unknown as IDL.ServiceClass
|
|
1985
|
+
)
|
|
1986
|
+
const methodMeta = serviceMeta["findBalance"]
|
|
1987
|
+
|
|
1988
|
+
// Test with value - Optional is [value]
|
|
1989
|
+
const rawDataWithValue = [[BigInt(999)]]
|
|
1990
|
+
|
|
1991
|
+
const resultWithValue = methodMeta.generateMetadata(rawDataWithValue[0])
|
|
1992
|
+
|
|
1993
|
+
expect(resultWithValue.results[0].raw).toEqual([BigInt(999)])
|
|
1994
|
+
const innerValue = resultWithValue.results[0].value
|
|
1995
|
+
if (
|
|
1996
|
+
typeof innerValue !== "object" ||
|
|
1997
|
+
innerValue === null ||
|
|
1998
|
+
!("value" in innerValue)
|
|
1999
|
+
) {
|
|
2000
|
+
throw new Error("Expected optional value object")
|
|
2001
|
+
}
|
|
2002
|
+
expect((innerValue as any).value).toBe("999")
|
|
2003
|
+
|
|
2004
|
+
// Test with null - Optional is []
|
|
2005
|
+
const rawDataNull = [[]]
|
|
2006
|
+
|
|
2007
|
+
const resultNull = methodMeta.generateMetadata(rawDataNull[0])
|
|
2008
|
+
expect(resultNull.results[0].raw).toEqual([])
|
|
2009
|
+
expect(resultNull.results[0].value).toBe(null)
|
|
2010
|
+
})
|
|
2011
|
+
|
|
2012
|
+
it("should handle empty return", () => {
|
|
2013
|
+
const service = IDL.Service({
|
|
2014
|
+
doNothing: IDL.Func([], [], []),
|
|
2015
|
+
})
|
|
2016
|
+
|
|
2017
|
+
const serviceMeta = visitor.visitService(
|
|
2018
|
+
service as unknown as IDL.ServiceClass
|
|
2019
|
+
)
|
|
2020
|
+
const methodMeta = serviceMeta["doNothing"]
|
|
2021
|
+
|
|
2022
|
+
const result = methodMeta.generateMetadata([])
|
|
2023
|
+
|
|
2024
|
+
expect(result.results).toHaveLength(0)
|
|
2025
|
+
})
|
|
2026
|
+
})
|
|
2027
|
+
})
|