@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,882 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
3
|
+
import { ArgumentFieldVisitor } from "./index"
|
|
4
|
+
|
|
5
|
+
describe("ArgumentFieldVisitor", () => {
|
|
6
|
+
const visitor = new ArgumentFieldVisitor()
|
|
7
|
+
|
|
8
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
9
|
+
// Primitive Types
|
|
10
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
describe("Primitive Types", () => {
|
|
13
|
+
it("should handle text type", () => {
|
|
14
|
+
const field = visitor.visitText(IDL.Text, "username")
|
|
15
|
+
|
|
16
|
+
expect(field.type).toBe("text")
|
|
17
|
+
expect(field.label).toBe("username")
|
|
18
|
+
expect(field.defaultValue).toBe("")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("should handle bool type", () => {
|
|
22
|
+
const field = visitor.visitBool(IDL.Bool, "isActive")
|
|
23
|
+
|
|
24
|
+
expect(field.type).toBe("boolean")
|
|
25
|
+
expect(field.label).toBe("isActive")
|
|
26
|
+
expect(field.defaultValue).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should handle null type", () => {
|
|
30
|
+
const field = visitor.visitNull(IDL.Null, "empty")
|
|
31
|
+
|
|
32
|
+
expect(field.type).toBe("null")
|
|
33
|
+
expect(field.label).toBe("empty")
|
|
34
|
+
expect(field.defaultValue).toBe(null)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should handle principal type", () => {
|
|
38
|
+
const field = visitor.visitPrincipal(IDL.Principal, "caller")
|
|
39
|
+
|
|
40
|
+
expect(field.type).toBe("principal")
|
|
41
|
+
expect(field.label).toBe("caller")
|
|
42
|
+
expect(field.defaultValue).toBe("")
|
|
43
|
+
expect(field.minLength).toBe(7)
|
|
44
|
+
expect(field.maxLength).toBe(64)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
49
|
+
// Number Types
|
|
50
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
describe("Number Types", () => {
|
|
53
|
+
it("should handle nat type", () => {
|
|
54
|
+
const field = visitor.visitNat(IDL.Nat, "amount")
|
|
55
|
+
|
|
56
|
+
expect(field.type).toBe("number")
|
|
57
|
+
expect(field.label).toBe("amount")
|
|
58
|
+
expect(field.candidType).toBe("nat")
|
|
59
|
+
expect(field.defaultValue).toBe("")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("should handle int type", () => {
|
|
63
|
+
const field = visitor.visitInt(IDL.Int, "balance")
|
|
64
|
+
|
|
65
|
+
expect(field.type).toBe("number")
|
|
66
|
+
expect(field.candidType).toBe("int")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("should handle nat8 type", () => {
|
|
70
|
+
const field = visitor.visitFixedNat(IDL.Nat8 as IDL.FixedNatClass, "byte")
|
|
71
|
+
|
|
72
|
+
expect(field.type).toBe("number")
|
|
73
|
+
expect(field.candidType).toBe("nat8")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("should handle nat64 type", () => {
|
|
77
|
+
const field = visitor.visitFixedNat(
|
|
78
|
+
IDL.Nat64 as IDL.FixedNatClass,
|
|
79
|
+
"timestamp"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
expect(field.type).toBe("number")
|
|
83
|
+
expect(field.candidType).toBe("nat64")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("should handle int32 type", () => {
|
|
87
|
+
const field = visitor.visitFixedInt(
|
|
88
|
+
IDL.Int32 as IDL.FixedIntClass,
|
|
89
|
+
"count"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
expect(field.type).toBe("number")
|
|
93
|
+
expect(field.candidType).toBe("int32")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("should handle float64 type", () => {
|
|
97
|
+
const field = visitor.visitFloat(IDL.Float64 as IDL.FloatClass, "price")
|
|
98
|
+
|
|
99
|
+
expect(field.type).toBe("number")
|
|
100
|
+
expect(field.candidType).toBe("float")
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
105
|
+
// Compound Types
|
|
106
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
describe("Record Types", () => {
|
|
109
|
+
it("should handle simple record", () => {
|
|
110
|
+
const recordType = IDL.Record({
|
|
111
|
+
name: IDL.Text,
|
|
112
|
+
age: IDL.Nat,
|
|
113
|
+
})
|
|
114
|
+
const field = visitor.visitRecord(
|
|
115
|
+
recordType,
|
|
116
|
+
[
|
|
117
|
+
["name", IDL.Text],
|
|
118
|
+
["age", IDL.Nat],
|
|
119
|
+
],
|
|
120
|
+
"person"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(field.type).toBe("record")
|
|
124
|
+
expect(field.label).toBe("person")
|
|
125
|
+
expect(field.fields).toHaveLength(2)
|
|
126
|
+
|
|
127
|
+
const nameField = field.fields.find((f) => f.label === "name")
|
|
128
|
+
if (!nameField || nameField.type !== "text") {
|
|
129
|
+
throw new Error("Name field not found or not text")
|
|
130
|
+
}
|
|
131
|
+
expect(nameField.type).toBe("text")
|
|
132
|
+
expect(nameField.defaultValue).toBe("")
|
|
133
|
+
|
|
134
|
+
const ageField = field.fields.find((f) => f.label === "age")
|
|
135
|
+
if (!ageField || ageField.type !== "number") {
|
|
136
|
+
throw new Error("Age field not found or not number")
|
|
137
|
+
}
|
|
138
|
+
expect(ageField.type).toBe("number")
|
|
139
|
+
expect(ageField.candidType).toBe("nat")
|
|
140
|
+
|
|
141
|
+
expect(field.defaultValues).toEqual({
|
|
142
|
+
name: "",
|
|
143
|
+
age: "",
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("should handle nested record", () => {
|
|
148
|
+
const addressType = IDL.Record({
|
|
149
|
+
street: IDL.Text,
|
|
150
|
+
city: IDL.Text,
|
|
151
|
+
})
|
|
152
|
+
const personType = IDL.Record({
|
|
153
|
+
name: IDL.Text,
|
|
154
|
+
address: addressType,
|
|
155
|
+
})
|
|
156
|
+
const field = visitor.visitRecord(
|
|
157
|
+
personType,
|
|
158
|
+
[
|
|
159
|
+
["name", IDL.Text],
|
|
160
|
+
["address", addressType],
|
|
161
|
+
],
|
|
162
|
+
"user"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
expect(field.type).toBe("record")
|
|
166
|
+
expect(field.fields).toHaveLength(2)
|
|
167
|
+
|
|
168
|
+
const addressField = field.fields.find((f) => f.label === "address")
|
|
169
|
+
if (!addressField || addressField.type !== "record") {
|
|
170
|
+
throw new Error("Address field not found or not record")
|
|
171
|
+
}
|
|
172
|
+
expect(addressField.type).toBe("record")
|
|
173
|
+
expect(addressField.fields).toHaveLength(2)
|
|
174
|
+
|
|
175
|
+
expect(field.defaultValues).toEqual({
|
|
176
|
+
name: "",
|
|
177
|
+
address: {
|
|
178
|
+
street: "",
|
|
179
|
+
city: "",
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("should handle ICRC-1 transfer record", () => {
|
|
185
|
+
const transferType = IDL.Record({
|
|
186
|
+
to: IDL.Record({
|
|
187
|
+
owner: IDL.Principal,
|
|
188
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
189
|
+
}),
|
|
190
|
+
amount: IDL.Nat,
|
|
191
|
+
fee: IDL.Opt(IDL.Nat),
|
|
192
|
+
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
193
|
+
created_at_time: IDL.Opt(IDL.Nat64),
|
|
194
|
+
})
|
|
195
|
+
const field = visitor.visitRecord(
|
|
196
|
+
transferType,
|
|
197
|
+
[
|
|
198
|
+
[
|
|
199
|
+
"to",
|
|
200
|
+
IDL.Record({
|
|
201
|
+
owner: IDL.Principal,
|
|
202
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
["amount", IDL.Nat],
|
|
206
|
+
["fee", IDL.Opt(IDL.Nat)],
|
|
207
|
+
["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
208
|
+
["created_at_time", IDL.Opt(IDL.Nat64)],
|
|
209
|
+
],
|
|
210
|
+
"transfer"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
expect(field.type).toBe("record")
|
|
214
|
+
expect(field.fields).toHaveLength(5)
|
|
215
|
+
|
|
216
|
+
// Check 'to' field
|
|
217
|
+
const toField = field.fields.find((f) => f.label === "to")
|
|
218
|
+
if (!toField || toField.type !== "record") {
|
|
219
|
+
throw new Error("To field not found or not record")
|
|
220
|
+
}
|
|
221
|
+
expect(toField.type).toBe("record")
|
|
222
|
+
expect(toField.fields).toHaveLength(2)
|
|
223
|
+
|
|
224
|
+
// Check 'amount' field
|
|
225
|
+
const amountField = field.fields.find((f) => f.label === "amount")
|
|
226
|
+
if (!amountField || amountField.type !== "number") {
|
|
227
|
+
throw new Error("Amount field not found or not number")
|
|
228
|
+
}
|
|
229
|
+
expect(amountField.type).toBe("number")
|
|
230
|
+
expect(amountField.candidType).toBe("nat")
|
|
231
|
+
|
|
232
|
+
// Check optional 'fee' field
|
|
233
|
+
const feeField = field.fields.find((f) => f.label === "fee")
|
|
234
|
+
if (!feeField || feeField.type !== "optional") {
|
|
235
|
+
throw new Error("Fee field not found or not optional")
|
|
236
|
+
}
|
|
237
|
+
expect(feeField.type).toBe("optional")
|
|
238
|
+
expect(feeField.innerField.type).toBe("number")
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe("Variant Types", () => {
|
|
243
|
+
it("should handle simple variant", () => {
|
|
244
|
+
const statusType = IDL.Variant({
|
|
245
|
+
Active: IDL.Null,
|
|
246
|
+
Inactive: IDL.Null,
|
|
247
|
+
Pending: IDL.Null,
|
|
248
|
+
})
|
|
249
|
+
const field = visitor.visitVariant(
|
|
250
|
+
statusType,
|
|
251
|
+
[
|
|
252
|
+
["Inactive", IDL.Null],
|
|
253
|
+
["Active", IDL.Null],
|
|
254
|
+
["Pending", IDL.Null],
|
|
255
|
+
],
|
|
256
|
+
"status"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
expect(field.type).toBe("variant")
|
|
260
|
+
expect(field.label).toBe("status")
|
|
261
|
+
expect(field.options).toEqual(["Inactive", "Active", "Pending"])
|
|
262
|
+
expect(field.defaultOption).toBe("Inactive")
|
|
263
|
+
expect(field.fields).toHaveLength(3)
|
|
264
|
+
|
|
265
|
+
field.fields.forEach((f) => {
|
|
266
|
+
expect(f.type).toBe("null")
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it("should handle variant with payloads", () => {
|
|
271
|
+
const actionType = IDL.Variant({
|
|
272
|
+
Transfer: IDL.Record({
|
|
273
|
+
to: IDL.Principal,
|
|
274
|
+
amount: IDL.Nat,
|
|
275
|
+
}),
|
|
276
|
+
Approve: IDL.Record({
|
|
277
|
+
spender: IDL.Principal,
|
|
278
|
+
amount: IDL.Nat,
|
|
279
|
+
}),
|
|
280
|
+
Burn: IDL.Nat,
|
|
281
|
+
})
|
|
282
|
+
const field = visitor.visitVariant(
|
|
283
|
+
actionType,
|
|
284
|
+
[
|
|
285
|
+
[
|
|
286
|
+
"Approve",
|
|
287
|
+
IDL.Record({
|
|
288
|
+
spender: IDL.Principal,
|
|
289
|
+
amount: IDL.Nat,
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
["Burn", IDL.Nat],
|
|
293
|
+
[
|
|
294
|
+
"Transfer",
|
|
295
|
+
IDL.Record({
|
|
296
|
+
to: IDL.Principal,
|
|
297
|
+
amount: IDL.Nat,
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
],
|
|
301
|
+
"action"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
expect(field.type).toBe("variant")
|
|
305
|
+
expect(field.options).toEqual(["Approve", "Burn", "Transfer"]) // Sorted order
|
|
306
|
+
|
|
307
|
+
const transferField = field.fields.find((f) => f.label === "Transfer")
|
|
308
|
+
if (!transferField || transferField.type !== "record") {
|
|
309
|
+
throw new Error("Transfer field not found or not record")
|
|
310
|
+
}
|
|
311
|
+
expect(transferField.type).toBe("record")
|
|
312
|
+
expect(transferField.fields).toHaveLength(2)
|
|
313
|
+
|
|
314
|
+
const burnField = field.fields.find((f) => f.label === "Burn")
|
|
315
|
+
if (!burnField || burnField.type !== "number") {
|
|
316
|
+
throw new Error("Burn field not found or not number")
|
|
317
|
+
}
|
|
318
|
+
expect(burnField.type).toBe("number")
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("should handle Result variant (Ok/Err)", () => {
|
|
322
|
+
const resultType = IDL.Variant({
|
|
323
|
+
Ok: IDL.Nat,
|
|
324
|
+
Err: IDL.Text,
|
|
325
|
+
})
|
|
326
|
+
const field = visitor.visitVariant(
|
|
327
|
+
resultType,
|
|
328
|
+
[
|
|
329
|
+
["Ok", IDL.Nat],
|
|
330
|
+
["Err", IDL.Text],
|
|
331
|
+
],
|
|
332
|
+
"result"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
expect(field.type).toBe("variant")
|
|
336
|
+
expect(field.options).toContain("Ok")
|
|
337
|
+
expect(field.options).toContain("Err")
|
|
338
|
+
|
|
339
|
+
const okField = field.fields.find((f) => f.label === "Ok")
|
|
340
|
+
if (!okField || okField.type !== "number") {
|
|
341
|
+
throw new Error("Ok field not found or not number")
|
|
342
|
+
}
|
|
343
|
+
expect(okField.type).toBe("number")
|
|
344
|
+
|
|
345
|
+
const errField = field.fields.find((f) => f.label === "Err")
|
|
346
|
+
if (!errField || errField.type !== "text") {
|
|
347
|
+
throw new Error("Err field not found or not text")
|
|
348
|
+
}
|
|
349
|
+
expect(errField.type).toBe("text")
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe("Tuple Types", () => {
|
|
354
|
+
it("should handle simple tuple", () => {
|
|
355
|
+
const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
|
|
356
|
+
const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
|
|
357
|
+
|
|
358
|
+
expect(field.type).toBe("tuple")
|
|
359
|
+
expect(field.label).toBe("pair")
|
|
360
|
+
expect(field.fields).toHaveLength(2)
|
|
361
|
+
expect(field.fields[0].type).toBe("text")
|
|
362
|
+
expect(field.fields[1].type).toBe("number")
|
|
363
|
+
expect(field.defaultValues).toEqual(["", ""])
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it("triple tuple", () => {
|
|
367
|
+
const tupleType = IDL.Tuple(IDL.Principal, IDL.Nat, IDL.Bool)
|
|
368
|
+
const field = visitor.visitTuple(
|
|
369
|
+
tupleType,
|
|
370
|
+
[IDL.Principal, IDL.Nat, IDL.Bool],
|
|
371
|
+
"triple"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
expect(field.type).toBe("tuple")
|
|
375
|
+
expect(field.fields).toHaveLength(3)
|
|
376
|
+
expect(field.fields[0].type).toBe("principal")
|
|
377
|
+
expect(field.fields[1].type).toBe("number")
|
|
378
|
+
expect(field.fields[2].type).toBe("boolean")
|
|
379
|
+
expect(field.defaultValues).toEqual(["", "", false])
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe("Optional Types", () => {
|
|
384
|
+
it("should handle optional primitive", () => {
|
|
385
|
+
const optType = IDL.Opt(IDL.Text)
|
|
386
|
+
const field = visitor.visitOpt(optType, IDL.Text, "nickname")
|
|
387
|
+
|
|
388
|
+
expect(field.type).toBe("optional")
|
|
389
|
+
expect(field.label).toBe("nickname")
|
|
390
|
+
expect(field.defaultValue).toBe(null)
|
|
391
|
+
expect(field.innerField.type).toBe("text")
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it("should handle optional record", () => {
|
|
395
|
+
const recType = IDL.Record({
|
|
396
|
+
name: IDL.Text,
|
|
397
|
+
value: IDL.Nat,
|
|
398
|
+
})
|
|
399
|
+
const optType = IDL.Opt(recType)
|
|
400
|
+
const field = visitor.visitOpt(optType, recType, "metadata")
|
|
401
|
+
|
|
402
|
+
expect(field.type).toBe("optional")
|
|
403
|
+
expect(field.innerField.type).toBe("record")
|
|
404
|
+
expect(field.innerField.type).toBe("record")
|
|
405
|
+
const inner = field.innerField
|
|
406
|
+
if (inner.type === "record") {
|
|
407
|
+
expect(inner.fields).toHaveLength(2)
|
|
408
|
+
} else {
|
|
409
|
+
throw new Error("Inner field is not record")
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("should handle nested optional", () => {
|
|
414
|
+
const innerOpt = IDL.Opt(IDL.Nat)
|
|
415
|
+
const optType = IDL.Opt(innerOpt)
|
|
416
|
+
const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
|
|
417
|
+
|
|
418
|
+
expect(field.type).toBe("optional")
|
|
419
|
+
expect(field.innerField.type).toBe("optional")
|
|
420
|
+
expect(field.innerField.type).toBe("optional")
|
|
421
|
+
const inner = field.innerField
|
|
422
|
+
if (inner.type === "optional") {
|
|
423
|
+
expect(inner.innerField.type).toBe("number")
|
|
424
|
+
} else {
|
|
425
|
+
throw new Error("Inner field is not optional")
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
describe("Vector Types", () => {
|
|
431
|
+
it("should handle vector of primitives", () => {
|
|
432
|
+
const vecType = IDL.Vec(IDL.Text)
|
|
433
|
+
const field = visitor.visitVec(vecType, IDL.Text, "tags")
|
|
434
|
+
|
|
435
|
+
expect(field.type).toBe("vector")
|
|
436
|
+
expect(field.label).toBe("tags")
|
|
437
|
+
expect(field.defaultValue).toEqual([])
|
|
438
|
+
expect(field.itemField.type).toBe("text")
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it("should handle vector of records", () => {
|
|
442
|
+
const recType = IDL.Record({
|
|
443
|
+
id: IDL.Nat,
|
|
444
|
+
name: IDL.Text,
|
|
445
|
+
})
|
|
446
|
+
const vecType = IDL.Vec(recType)
|
|
447
|
+
const field = visitor.visitVec(vecType, recType, "items")
|
|
448
|
+
|
|
449
|
+
expect(field.type).toBe("vector")
|
|
450
|
+
expect(field.itemField.type).toBe("record")
|
|
451
|
+
const item = field.itemField
|
|
452
|
+
if (item.type === "record") {
|
|
453
|
+
expect(item.fields).toHaveLength(2)
|
|
454
|
+
} else {
|
|
455
|
+
throw new Error("Item field is not record")
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it("blob (vec nat8)", () => {
|
|
460
|
+
const blobType = IDL.Vec(IDL.Nat8)
|
|
461
|
+
const field = visitor.visitVec(blobType, IDL.Nat8, "data")
|
|
462
|
+
|
|
463
|
+
expect(field.type).toBe("blob")
|
|
464
|
+
expect(field.label).toBe("data")
|
|
465
|
+
expect(field.defaultValue).toBe("")
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it("should handle nested vectors", () => {
|
|
469
|
+
const innerVec = IDL.Vec(IDL.Nat)
|
|
470
|
+
const nestedVecType = IDL.Vec(innerVec)
|
|
471
|
+
const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
|
|
472
|
+
|
|
473
|
+
expect(field.type).toBe("vector")
|
|
474
|
+
expect(field.itemField.type).toBe("vector")
|
|
475
|
+
const item = field.itemField
|
|
476
|
+
if (item.type === "vector") {
|
|
477
|
+
expect(item.itemField.type).toBe("number")
|
|
478
|
+
} else {
|
|
479
|
+
throw new Error("Item field is not vector")
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe("Recursive Types", () => {
|
|
485
|
+
it("should handle recursive type (tree)", () => {
|
|
486
|
+
const Tree = IDL.Rec()
|
|
487
|
+
Tree.fill(
|
|
488
|
+
IDL.Variant({
|
|
489
|
+
Leaf: IDL.Nat,
|
|
490
|
+
Node: IDL.Record({
|
|
491
|
+
left: Tree,
|
|
492
|
+
right: Tree,
|
|
493
|
+
}),
|
|
494
|
+
})
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
const field = visitor.visitRec(
|
|
498
|
+
Tree,
|
|
499
|
+
IDL.Variant({
|
|
500
|
+
Leaf: IDL.Nat,
|
|
501
|
+
Node: IDL.Record({
|
|
502
|
+
left: Tree,
|
|
503
|
+
right: Tree,
|
|
504
|
+
}),
|
|
505
|
+
}),
|
|
506
|
+
"tree"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
expect(field.type).toBe("recursive")
|
|
510
|
+
expect(field.label).toBe("tree")
|
|
511
|
+
expect(typeof field.extract).toBe("function")
|
|
512
|
+
|
|
513
|
+
// Extract should return a variant
|
|
514
|
+
const extracted = field.extract()
|
|
515
|
+
if (extracted.type !== "variant") {
|
|
516
|
+
throw new Error("Extracted field is not variant")
|
|
517
|
+
}
|
|
518
|
+
expect(extracted.type).toBe("variant")
|
|
519
|
+
expect(extracted.options).toContain("Leaf")
|
|
520
|
+
expect(extracted.options).toContain("Node")
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it("should handle recursive linked list", () => {
|
|
524
|
+
const List = IDL.Rec()
|
|
525
|
+
List.fill(
|
|
526
|
+
IDL.Variant({
|
|
527
|
+
Nil: IDL.Null,
|
|
528
|
+
Cons: IDL.Record({
|
|
529
|
+
head: IDL.Nat,
|
|
530
|
+
tail: List,
|
|
531
|
+
}),
|
|
532
|
+
})
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
const field = visitor.visitRec(
|
|
536
|
+
List,
|
|
537
|
+
IDL.Variant({
|
|
538
|
+
Nil: IDL.Null,
|
|
539
|
+
Cons: IDL.Record({
|
|
540
|
+
head: IDL.Nat,
|
|
541
|
+
tail: List,
|
|
542
|
+
}),
|
|
543
|
+
}),
|
|
544
|
+
"list"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
expect(field.type).toBe("recursive")
|
|
548
|
+
|
|
549
|
+
const extracted = field.extract()
|
|
550
|
+
if (extracted.type !== "variant") {
|
|
551
|
+
throw new Error("Extracted field is not variant")
|
|
552
|
+
}
|
|
553
|
+
expect(extracted.type).toBe("variant")
|
|
554
|
+
expect(extracted.options).toEqual(["Nil", "Cons"])
|
|
555
|
+
|
|
556
|
+
const consField = extracted.fields.find((f) => f.label === "Cons")
|
|
557
|
+
if (!consField || consField.type !== "record") {
|
|
558
|
+
throw new Error("Cons field not found or not record")
|
|
559
|
+
}
|
|
560
|
+
expect(consField.type).toBe("record")
|
|
561
|
+
expect(consField.fields).toHaveLength(2)
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
566
|
+
// Function Types
|
|
567
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
568
|
+
|
|
569
|
+
describe("Function Types", () => {
|
|
570
|
+
it("should handle query function", () => {
|
|
571
|
+
const funcType = IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ["query"])
|
|
572
|
+
const meta = visitor.visitFunc(funcType, "lookup")
|
|
573
|
+
|
|
574
|
+
expect(meta.functionType).toBe("query")
|
|
575
|
+
expect(meta.functionName).toBe("lookup")
|
|
576
|
+
expect(meta.fields).toHaveLength(1)
|
|
577
|
+
expect(meta.fields[0].type).toBe("text")
|
|
578
|
+
expect(meta.defaultValues).toEqual([""])
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it("should handle update function", () => {
|
|
582
|
+
const funcType = IDL.Func(
|
|
583
|
+
[
|
|
584
|
+
IDL.Record({
|
|
585
|
+
to: IDL.Principal,
|
|
586
|
+
amount: IDL.Nat,
|
|
587
|
+
}),
|
|
588
|
+
],
|
|
589
|
+
[
|
|
590
|
+
IDL.Variant({
|
|
591
|
+
Ok: IDL.Nat,
|
|
592
|
+
Err: IDL.Text,
|
|
593
|
+
}),
|
|
594
|
+
],
|
|
595
|
+
[]
|
|
596
|
+
)
|
|
597
|
+
const meta = visitor.visitFunc(funcType, "transfer")
|
|
598
|
+
|
|
599
|
+
expect(meta.functionType).toBe("update")
|
|
600
|
+
expect(meta.functionName).toBe("transfer")
|
|
601
|
+
expect(meta.fields).toHaveLength(1)
|
|
602
|
+
expect(meta.fields[0].type).toBe("record")
|
|
603
|
+
|
|
604
|
+
const recordField = meta.fields[0]
|
|
605
|
+
if (recordField.type !== "record") {
|
|
606
|
+
throw new Error("Expected record field")
|
|
607
|
+
}
|
|
608
|
+
expect(recordField.fields).toHaveLength(2)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
it("should handle function with multiple arguments", () => {
|
|
612
|
+
const funcType = IDL.Func(
|
|
613
|
+
[IDL.Principal, IDL.Nat, IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
614
|
+
[IDL.Bool],
|
|
615
|
+
[]
|
|
616
|
+
)
|
|
617
|
+
const meta = visitor.visitFunc(funcType, "authorize")
|
|
618
|
+
|
|
619
|
+
expect(meta.fields).toHaveLength(3)
|
|
620
|
+
expect(meta.fields[0].type).toBe("principal")
|
|
621
|
+
expect(meta.fields[1].type).toBe("number")
|
|
622
|
+
expect(meta.fields[2].type).toBe("optional")
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it("should handle function with no arguments", () => {
|
|
626
|
+
const funcType = IDL.Func([], [IDL.Nat], ["query"])
|
|
627
|
+
const meta = visitor.visitFunc(funcType, "getBalance")
|
|
628
|
+
|
|
629
|
+
expect(meta.functionType).toBe("query")
|
|
630
|
+
expect(meta.fields).toHaveLength(0)
|
|
631
|
+
expect(meta.defaultValues).toEqual([])
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
636
|
+
// Service Types
|
|
637
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
638
|
+
|
|
639
|
+
describe("Service Types", () => {
|
|
640
|
+
it("should handle complete service", () => {
|
|
641
|
+
const serviceType = IDL.Service({
|
|
642
|
+
get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
|
|
643
|
+
transfer: IDL.Func(
|
|
644
|
+
[
|
|
645
|
+
IDL.Record({
|
|
646
|
+
to: IDL.Principal,
|
|
647
|
+
amount: IDL.Nat,
|
|
648
|
+
}),
|
|
649
|
+
],
|
|
650
|
+
[
|
|
651
|
+
IDL.Variant({
|
|
652
|
+
Ok: IDL.Nat,
|
|
653
|
+
Err: IDL.Text,
|
|
654
|
+
}),
|
|
655
|
+
],
|
|
656
|
+
[]
|
|
657
|
+
),
|
|
658
|
+
get_metadata: IDL.Func(
|
|
659
|
+
[],
|
|
660
|
+
[IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
|
|
661
|
+
["query"]
|
|
662
|
+
),
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
const serviceMeta = visitor.visitService(serviceType)
|
|
666
|
+
|
|
667
|
+
expect(Object.keys(serviceMeta)).toHaveLength(3)
|
|
668
|
+
expect(serviceMeta).toHaveProperty("get_balance")
|
|
669
|
+
expect(serviceMeta).toHaveProperty("transfer")
|
|
670
|
+
expect(serviceMeta).toHaveProperty("get_metadata")
|
|
671
|
+
|
|
672
|
+
// Check get_balance
|
|
673
|
+
const getBalanceMeta = serviceMeta["get_balance"]
|
|
674
|
+
expect(getBalanceMeta.functionType).toBe("query")
|
|
675
|
+
expect(getBalanceMeta.fields).toHaveLength(1)
|
|
676
|
+
expect(getBalanceMeta.fields[0].type).toBe("principal")
|
|
677
|
+
|
|
678
|
+
// Check transfer
|
|
679
|
+
const transferMeta = serviceMeta["transfer"]
|
|
680
|
+
expect(transferMeta.functionType).toBe("update")
|
|
681
|
+
expect(transferMeta.fields).toHaveLength(1)
|
|
682
|
+
expect(transferMeta.fields[0].type).toBe("record")
|
|
683
|
+
|
|
684
|
+
// Check get_metadata
|
|
685
|
+
const getMetadataMeta = serviceMeta["get_metadata"]
|
|
686
|
+
expect(getMetadataMeta.functionType).toBe("query")
|
|
687
|
+
expect(getMetadataMeta.fields).toHaveLength(0)
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
692
|
+
// Path Generation
|
|
693
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
694
|
+
|
|
695
|
+
describe("Path Generation", () => {
|
|
696
|
+
it("should generate correct paths for nested records", () => {
|
|
697
|
+
const funcType = IDL.Func(
|
|
698
|
+
[
|
|
699
|
+
IDL.Record({
|
|
700
|
+
user: IDL.Record({
|
|
701
|
+
name: IDL.Text,
|
|
702
|
+
age: IDL.Nat,
|
|
703
|
+
}),
|
|
704
|
+
active: IDL.Bool,
|
|
705
|
+
}),
|
|
706
|
+
],
|
|
707
|
+
[],
|
|
708
|
+
[]
|
|
709
|
+
)
|
|
710
|
+
const meta = visitor.visitFunc(funcType, "updateUser")
|
|
711
|
+
|
|
712
|
+
const argRecord = meta.fields[0]
|
|
713
|
+
if (argRecord.type !== "record") {
|
|
714
|
+
throw new Error("Expected record field")
|
|
715
|
+
}
|
|
716
|
+
expect(argRecord.path).toBe("[0]")
|
|
717
|
+
|
|
718
|
+
const userRecord = argRecord.fields.find((f) => f.label === "user")
|
|
719
|
+
if (!userRecord || userRecord.type !== "record") {
|
|
720
|
+
throw new Error("User record not found or not record")
|
|
721
|
+
}
|
|
722
|
+
expect(userRecord.path).toBe("[0].user")
|
|
723
|
+
|
|
724
|
+
const nameField = userRecord.fields.find((f) => f.label === "name")
|
|
725
|
+
if (!nameField || nameField.type !== "text") {
|
|
726
|
+
throw new Error("Name field not found or not text")
|
|
727
|
+
}
|
|
728
|
+
expect(nameField.path).toBe("[0].user.name")
|
|
729
|
+
|
|
730
|
+
const ageField = userRecord.fields.find((f) => f.label === "age")
|
|
731
|
+
if (!ageField || ageField.type !== "number") {
|
|
732
|
+
throw new Error("Age field not found or not number")
|
|
733
|
+
}
|
|
734
|
+
expect(ageField.path).toBe("[0].user.age")
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it("should generate correct paths for vectors", () => {
|
|
738
|
+
const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
|
|
739
|
+
const meta = visitor.visitFunc(funcType, "addTags")
|
|
740
|
+
|
|
741
|
+
const vecField = meta.fields[0]
|
|
742
|
+
if (vecField.type !== "vector") {
|
|
743
|
+
throw new Error("Expected vector field")
|
|
744
|
+
}
|
|
745
|
+
expect(vecField.path).toBe("[0]")
|
|
746
|
+
expect(vecField.itemField.path).toBe("[0][0]")
|
|
747
|
+
})
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
751
|
+
// Complex Real-World Examples
|
|
752
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
753
|
+
|
|
754
|
+
describe("Real-World Examples", () => {
|
|
755
|
+
it("should handle ICRC-2 approve", () => {
|
|
756
|
+
const ApproveArgs = IDL.Record({
|
|
757
|
+
fee: IDL.Opt(IDL.Nat),
|
|
758
|
+
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
759
|
+
from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
760
|
+
created_at_time: IDL.Opt(IDL.Nat64),
|
|
761
|
+
amount: IDL.Nat,
|
|
762
|
+
expected_allowance: IDL.Opt(IDL.Nat),
|
|
763
|
+
expires_at: IDL.Opt(IDL.Nat64),
|
|
764
|
+
spender: IDL.Record({
|
|
765
|
+
owner: IDL.Principal,
|
|
766
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
767
|
+
}),
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
const field = visitor.visitRecord(
|
|
771
|
+
ApproveArgs,
|
|
772
|
+
[
|
|
773
|
+
["fee", IDL.Opt(IDL.Nat)],
|
|
774
|
+
["memo", IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
775
|
+
["from_subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
|
|
776
|
+
["created_at_time", IDL.Opt(IDL.Nat64)],
|
|
777
|
+
["amount", IDL.Nat],
|
|
778
|
+
["expected_allowance", IDL.Opt(IDL.Nat)],
|
|
779
|
+
["expires_at", IDL.Opt(IDL.Nat64)],
|
|
780
|
+
[
|
|
781
|
+
"spender",
|
|
782
|
+
IDL.Record({
|
|
783
|
+
owner: IDL.Principal,
|
|
784
|
+
subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
785
|
+
}),
|
|
786
|
+
],
|
|
787
|
+
],
|
|
788
|
+
"approve"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
expect(field.type).toBe("record")
|
|
792
|
+
expect(field.fields.length).toBeGreaterThan(5)
|
|
793
|
+
|
|
794
|
+
// Check spender field
|
|
795
|
+
const spenderField = field.fields.find((f) => f.label === "spender")
|
|
796
|
+
if (!spenderField || spenderField.type !== "record") {
|
|
797
|
+
throw new Error("Spender field not found or not record")
|
|
798
|
+
}
|
|
799
|
+
expect(spenderField.type).toBe("record")
|
|
800
|
+
expect(spenderField.fields).toHaveLength(2)
|
|
801
|
+
|
|
802
|
+
// Check amount field
|
|
803
|
+
const amountField = field.fields.find((f) => f.label === "amount")
|
|
804
|
+
if (!amountField || amountField.type !== "number") {
|
|
805
|
+
throw new Error("Amount field not found or not number")
|
|
806
|
+
}
|
|
807
|
+
expect(amountField.type).toBe("number")
|
|
808
|
+
expect(amountField.candidType).toBe("nat")
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it("should handle SNS governance proposal", () => {
|
|
812
|
+
const ProposalType = IDL.Variant({
|
|
813
|
+
Motion: IDL.Record({
|
|
814
|
+
motion_text: IDL.Text,
|
|
815
|
+
}),
|
|
816
|
+
TransferSnsTreasuryFunds: IDL.Record({
|
|
817
|
+
from_treasury: IDL.Nat32,
|
|
818
|
+
to_principal: IDL.Opt(IDL.Principal),
|
|
819
|
+
to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
820
|
+
memo: IDL.Opt(IDL.Nat64),
|
|
821
|
+
amount_e8s: IDL.Nat64,
|
|
822
|
+
}),
|
|
823
|
+
UpgradeSnsControlledCanister: IDL.Record({
|
|
824
|
+
new_canister_wasm: IDL.Vec(IDL.Nat8),
|
|
825
|
+
mode: IDL.Opt(IDL.Nat32),
|
|
826
|
+
canister_id: IDL.Opt(IDL.Principal),
|
|
827
|
+
canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
828
|
+
}),
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
const field = visitor.visitVariant(
|
|
832
|
+
ProposalType,
|
|
833
|
+
[
|
|
834
|
+
["Motion", IDL.Record({ motion_text: IDL.Text })],
|
|
835
|
+
[
|
|
836
|
+
"TransferSnsTreasuryFunds",
|
|
837
|
+
IDL.Record({
|
|
838
|
+
from_treasury: IDL.Nat32,
|
|
839
|
+
to_principal: IDL.Opt(IDL.Principal),
|
|
840
|
+
to_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
841
|
+
memo: IDL.Opt(IDL.Nat64),
|
|
842
|
+
amount_e8s: IDL.Nat64,
|
|
843
|
+
}),
|
|
844
|
+
],
|
|
845
|
+
[
|
|
846
|
+
"UpgradeSnsControlledCanister",
|
|
847
|
+
IDL.Record({
|
|
848
|
+
new_canister_wasm: IDL.Vec(IDL.Nat8),
|
|
849
|
+
mode: IDL.Opt(IDL.Nat32),
|
|
850
|
+
canister_id: IDL.Opt(IDL.Principal),
|
|
851
|
+
canister_upgrade_arg: IDL.Opt(IDL.Vec(IDL.Nat8)),
|
|
852
|
+
}),
|
|
853
|
+
],
|
|
854
|
+
],
|
|
855
|
+
"proposal"
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
expect(field.type).toBe("variant")
|
|
859
|
+
expect(field.options).toContain("Motion")
|
|
860
|
+
expect(field.options).toContain("TransferSnsTreasuryFunds")
|
|
861
|
+
expect(field.options).toContain("UpgradeSnsControlledCanister")
|
|
862
|
+
|
|
863
|
+
// Check Motion variant
|
|
864
|
+
const motionField = field.fields.find((f) => f.label === "Motion")
|
|
865
|
+
if (!motionField || motionField.type !== "record") {
|
|
866
|
+
throw new Error("Motion field not found or not record")
|
|
867
|
+
}
|
|
868
|
+
expect(motionField.type).toBe("record")
|
|
869
|
+
expect(motionField.fields).toHaveLength(1)
|
|
870
|
+
|
|
871
|
+
// Check TransferSnsTreasuryFunds variant
|
|
872
|
+
const transferField = field.fields.find(
|
|
873
|
+
(f) => f.label === "TransferSnsTreasuryFunds"
|
|
874
|
+
)
|
|
875
|
+
if (!transferField || transferField.type !== "record") {
|
|
876
|
+
throw new Error("Transfer field not found or not record")
|
|
877
|
+
}
|
|
878
|
+
expect(transferField.type).toBe("record")
|
|
879
|
+
expect(transferField.fields.length).toBeGreaterThan(3)
|
|
880
|
+
})
|
|
881
|
+
})
|
|
882
|
+
})
|