@callcium/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3216 @@
1
+ import { keccak_256 } from "@noble/hashes/sha3";
2
+ //#region src/errors.ts
3
+ /**
4
+ * Thrown when a policy or descriptor blob is structurally malformed.
5
+ * The optional `offset` indicates the byte position in the source blob where the error was detected.
6
+ */
7
+ var CallciumError = class CallciumError extends Error {
8
+ code;
9
+ /** Byte position in the source blob where the error was detected, if applicable. */
10
+ offset;
11
+ constructor(code, message, offset) {
12
+ const prefix = offset !== void 0 ? `[offset ${offset}] ` : "";
13
+ super(`${prefix}${message}`);
14
+ this.name = "CallciumError";
15
+ this.code = code;
16
+ Object.setPrototypeOf(this, CallciumError.prototype);
17
+ }
18
+ };
19
+ /**
20
+ * Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement.
21
+ * Distinct from `CallciumError("VALIDATION_ERROR")`, which signals static
22
+ * issues in the policy itself.
23
+ * Carries the full list of violations (one per failed group).
24
+ */
25
+ var PolicyViolationError = class PolicyViolationError extends Error {
26
+ violations;
27
+ constructor(violations) {
28
+ const first = violations[0];
29
+ const msg = first ? `Policy violation: ${first.message}` : "Policy violation";
30
+ super(msg);
31
+ this.name = "PolicyViolationError";
32
+ this.violations = violations;
33
+ Object.setPrototypeOf(this, PolicyViolationError.prototype);
34
+ }
35
+ };
36
+ //#endregion
37
+ //#region src/bytes.ts
38
+ /** Regex that matches a valid hex body (even number of hex chars). */
39
+ const HEX_BODY_RE = /^[0-9a-fA-F]*$/;
40
+ /** Convert a hex string to a byte array, stripping the 0x prefix if present. */
41
+ function hexToBytes(hex) {
42
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
43
+ if (clean.length % 2 !== 0) throw new CallciumError("INVALID_HEX", "Odd-length hex string");
44
+ if (!HEX_BODY_RE.test(clean)) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
45
+ const bytes = new Uint8Array(clean.length / 2);
46
+ for (let i = 0; i < clean.length; i += 2) bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
47
+ return bytes;
48
+ }
49
+ const HEX_LUT = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
50
+ /** Convert a byte array to a 0x-prefixed lowercase hex string. */
51
+ function bytesToHex(bytes) {
52
+ let body = "";
53
+ for (let i = 0; i < bytes.length; i++) body += HEX_LUT[bytes[i]];
54
+ return `0x${body}`;
55
+ }
56
+ /** Extract a subarray and return it as a 0x-prefixed hex string. */
57
+ function toHex(data, start, end) {
58
+ return bytesToHex(data.subarray(start, end));
59
+ }
60
+ /** Read a big-endian uint16 from a byte array at the given offset. */
61
+ function readU16(data, offset) {
62
+ return data[offset] << 8 | data[offset + 1];
63
+ }
64
+ /** Read a big-endian uint24 from a byte array at the given offset. */
65
+ function readU24(data, offset) {
66
+ return data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
67
+ }
68
+ /** Read a big-endian uint32 from a byte array at the given offset. */
69
+ function readU32(data, offset) {
70
+ return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
71
+ }
72
+ /** Write a 16-bit big-endian value into a Uint8Array at offset. */
73
+ function writeBE16(buffer, offset, value) {
74
+ buffer[offset] = value >>> 8 & 255;
75
+ buffer[offset + 1] = value & 255;
76
+ }
77
+ /** Write a 24-bit big-endian value into a Uint8Array at offset. */
78
+ function writeBE24(buffer, offset, value) {
79
+ buffer[offset] = value >>> 16 & 255;
80
+ buffer[offset + 1] = value >>> 8 & 255;
81
+ buffer[offset + 2] = value & 255;
82
+ }
83
+ /** Format a bigint as a zero-padded 32-byte hex string. */
84
+ function bigintToHex(value) {
85
+ return `0x${value.toString(16).padStart(64, "0")}`;
86
+ }
87
+ /** Validate and brand a hex string as an address. Throws if the runtime length is not exactly 20 bytes. */
88
+ function toAddress(hex) {
89
+ const body = hex.startsWith("0x") ? hex.slice(2) : hex;
90
+ if (body.length !== 40) throw new CallciumError("INVALID_HEX", `Invalid address length: expected 40 hex chars, got ${body.length}`);
91
+ if (!HEX_BODY_RE.test(body)) throw new CallciumError("INVALID_HEX", "Invalid hex characters in address");
92
+ return `0x${body}`;
93
+ }
94
+ //#endregion
95
+ //#region src/constants.ts
96
+ /** Binary layout constants for the Callcium descriptor format. */
97
+ const DescriptorFormat = {
98
+ VERSION: 1,
99
+ HEADER_SIZE: 2,
100
+ TYPECODE_SIZE: 1,
101
+ COMPOSITE_META_SIZE: 3,
102
+ TUPLE_HEADER_SIZE: 6,
103
+ ARRAY_HEADER_SIZE: 4,
104
+ ARRAY_LENGTH_SIZE: 2,
105
+ TUPLE_FIELDCOUNT_SIZE: 2,
106
+ META_STATIC_WORDS_SHIFT: 12,
107
+ META_NODE_LENGTH_MASK: 4095,
108
+ MAX_NODE_LENGTH: 4095,
109
+ MAX_STATIC_ARRAY_LENGTH: 4095,
110
+ MAX_TUPLE_FIELDS: 4089,
111
+ MAX_PARAMS: 255
112
+ };
113
+ /** Binary layout constants for the Callcium policy format. */
114
+ const PolicyFormat = {
115
+ VERSION: 1,
116
+ VERSION_MASK: 15,
117
+ FLAG_NO_SELECTOR: 16,
118
+ RESERVED_MASK: 224,
119
+ HEADER_SIZE: 1,
120
+ SELECTOR_OFFSET: 1,
121
+ SELECTOR_SIZE: 4,
122
+ DESC_LENGTH_OFFSET: 5,
123
+ DESC_LENGTH_SIZE: 2,
124
+ DESC_OFFSET: 7,
125
+ GROUP_COUNT_SIZE: 1,
126
+ GROUP_RULECOUNT_SIZE: 2,
127
+ GROUP_SIZE_SIZE: 4,
128
+ GROUP_HEADER_SIZE: 6,
129
+ RULE_SIZE_SIZE: 2,
130
+ RULE_SCOPE_OFFSET: 2,
131
+ RULE_DEPTH_OFFSET: 3,
132
+ RULE_PATH_OFFSET: 4,
133
+ PATH_STEP_SIZE: 2,
134
+ RULE_OPCODE_SIZE: 1,
135
+ RULE_DATALENGTH_SIZE: 2,
136
+ RULE_FIXED_OVERHEAD: 7,
137
+ RULE_MIN_SIZE: 9
138
+ };
139
+ /** Build a plain `{ KEY: code }` object from a table at runtime. */
140
+ function buildCodeMap(table) {
141
+ return Object.fromEntries(table.map((e) => [e.key, e.code]));
142
+ }
143
+ const SCOPE_TABLE = [{
144
+ key: "CONTEXT",
145
+ code: 0,
146
+ label: "context"
147
+ }, {
148
+ key: "CALLDATA",
149
+ code: 1,
150
+ label: "calldata"
151
+ }];
152
+ /** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
153
+ const Scope = buildCodeMap(SCOPE_TABLE);
154
+ const scopeByCode = new Map(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
155
+ /**
156
+ * Map a scope code to its display label.
157
+ * @param code - Scope byte value.
158
+ * @returns Display metadata for the scope.
159
+ * @throws {CallciumError} If the code is not a recognised scope.
160
+ */
161
+ function lookupScope(code) {
162
+ const info = scopeByCode.get(code);
163
+ if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
164
+ return info;
165
+ }
166
+ const CTX_PROP_TABLE = [
167
+ {
168
+ key: "MSG_SENDER",
169
+ code: 0,
170
+ label: "msg.sender",
171
+ typeCode: 64
172
+ },
173
+ {
174
+ key: "MSG_VALUE",
175
+ code: 1,
176
+ label: "msg.value",
177
+ typeCode: 31
178
+ },
179
+ {
180
+ key: "BLOCK_TIMESTAMP",
181
+ code: 2,
182
+ label: "block.timestamp",
183
+ typeCode: 31
184
+ },
185
+ {
186
+ key: "BLOCK_NUMBER",
187
+ code: 3,
188
+ label: "block.number",
189
+ typeCode: 31
190
+ },
191
+ {
192
+ key: "CHAIN_ID",
193
+ code: 4,
194
+ label: "block.chainid",
195
+ typeCode: 31
196
+ },
197
+ {
198
+ key: "TX_ORIGIN",
199
+ code: 5,
200
+ label: "tx.origin",
201
+ typeCode: 64
202
+ }
203
+ ];
204
+ /** Well-known context property IDs for context-scope rules. */
205
+ const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
206
+ /** Maximum valid context property ID. */
207
+ const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
208
+ /** Maps each context property table key to its camelCase Context key. */
209
+ const CTX_KEY_MAP = {
210
+ MSG_SENDER: "msgSender",
211
+ MSG_VALUE: "msgValue",
212
+ BLOCK_TIMESTAMP: "blockTimestamp",
213
+ BLOCK_NUMBER: "blockNumber",
214
+ CHAIN_ID: "chainId",
215
+ TX_ORIGIN: "txOrigin"
216
+ };
217
+ const ctxPropByCode = new Map(CTX_PROP_TABLE.map((e) => [e.code, {
218
+ label: e.label,
219
+ contextKey: CTX_KEY_MAP[e.key],
220
+ typeCode: e.typeCode
221
+ }]));
222
+ /**
223
+ * Map a context property code to its display label and ABI type code.
224
+ * @param code - Context property ID.
225
+ * @returns Display metadata including the Solidity type code for the property value.
226
+ * @throws {CallciumError} If the code is not a recognised context property.
227
+ */
228
+ function lookupContextProperty(code) {
229
+ const info = ctxPropByCode.get(code);
230
+ if (!info) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Unknown context property ${code}`);
231
+ return info;
232
+ }
233
+ /** Protocol-imposed safety limits for path depth and quantifier array size. */
234
+ const Limits = {
235
+ MAX_PATH_DEPTH: 32,
236
+ MAX_QUANTIFIED_ARRAY_LENGTH: 256
237
+ };
238
+ /** @internal */
239
+ const OP_TABLE = [
240
+ {
241
+ key: "EQ",
242
+ code: 1,
243
+ label: "==",
244
+ operands: "single"
245
+ },
246
+ {
247
+ key: "GT",
248
+ code: 2,
249
+ label: ">",
250
+ operands: "single"
251
+ },
252
+ {
253
+ key: "LT",
254
+ code: 3,
255
+ label: "<",
256
+ operands: "single"
257
+ },
258
+ {
259
+ key: "GTE",
260
+ code: 4,
261
+ label: ">=",
262
+ operands: "single"
263
+ },
264
+ {
265
+ key: "LTE",
266
+ code: 5,
267
+ label: "<=",
268
+ operands: "single"
269
+ },
270
+ {
271
+ key: "BETWEEN",
272
+ code: 6,
273
+ label: "between",
274
+ operands: "range"
275
+ },
276
+ {
277
+ key: "IN",
278
+ code: 7,
279
+ label: "in",
280
+ operands: "variadic"
281
+ },
282
+ {
283
+ key: "BITMASK_ALL",
284
+ code: 16,
285
+ label: "bitmask all",
286
+ operands: "single"
287
+ },
288
+ {
289
+ key: "BITMASK_ANY",
290
+ code: 17,
291
+ label: "bitmask any",
292
+ operands: "single"
293
+ },
294
+ {
295
+ key: "BITMASK_NONE",
296
+ code: 18,
297
+ label: "bitmask none",
298
+ operands: "single"
299
+ },
300
+ {
301
+ key: "LENGTH_EQ",
302
+ code: 32,
303
+ label: "length ==",
304
+ operands: "single"
305
+ },
306
+ {
307
+ key: "LENGTH_GT",
308
+ code: 33,
309
+ label: "length >",
310
+ operands: "single"
311
+ },
312
+ {
313
+ key: "LENGTH_LT",
314
+ code: 34,
315
+ label: "length <",
316
+ operands: "single"
317
+ },
318
+ {
319
+ key: "LENGTH_GTE",
320
+ code: 35,
321
+ label: "length >=",
322
+ operands: "single"
323
+ },
324
+ {
325
+ key: "LENGTH_LTE",
326
+ code: 36,
327
+ label: "length <=",
328
+ operands: "single"
329
+ },
330
+ {
331
+ key: "LENGTH_BETWEEN",
332
+ code: 37,
333
+ label: "length between",
334
+ operands: "range"
335
+ }
336
+ ];
337
+ /** Callcium policy operator codes. */
338
+ const Op = {
339
+ ...buildCodeMap(OP_TABLE),
340
+ NOT: 128
341
+ };
342
+ const opByCode = new Map(OP_TABLE.map((e) => [e.code, {
343
+ label: e.label,
344
+ operands: e.operands
345
+ }]));
346
+ /**
347
+ * Map an operator code to its display label. Strips the NOT flag automatically.
348
+ * @param code - Operator byte value (may include the NOT flag).
349
+ * @returns Display metadata for the base operator.
350
+ * @throws {CallciumError} If the base code is not a recognised operator.
351
+ */
352
+ function lookupOp(code) {
353
+ const base = code & ~Op.NOT;
354
+ const info = opByCode.get(base);
355
+ if (!info) throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
356
+ return info;
357
+ }
358
+ /**
359
+ * Check whether an operator's data payload has the correct length.
360
+ * @param opBase - Base operator code with the NOT flag stripped.
361
+ * @param dataLength - Byte length of the operator's data payload.
362
+ * @returns True if the data length is valid for the given operator.
363
+ */
364
+ function isValidOperatorData(opBase, dataLength) {
365
+ const operands = opByCode.get(opBase)?.operands;
366
+ if (operands === "single") return dataLength === 32;
367
+ if (operands === "range") return dataLength === 64;
368
+ if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
369
+ return false;
370
+ }
371
+ const QUANTIFIER_TABLE = [
372
+ {
373
+ key: "ALL_OR_EMPTY",
374
+ code: 65535,
375
+ label: "all or empty"
376
+ },
377
+ {
378
+ key: "ALL",
379
+ code: 65534,
380
+ label: "all"
381
+ },
382
+ {
383
+ key: "ANY",
384
+ code: 65533,
385
+ label: "any"
386
+ }
387
+ ];
388
+ /** Reserved path step values that trigger quantified evaluation over array elements. */
389
+ const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
390
+ const quantifierByCode = new Map(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
391
+ /** Check whether a path step is a quantifier (ALL_OR_EMPTY, ALL, or ANY). */
392
+ function isQuantifier(step) {
393
+ return step >= Quantifier.ANY;
394
+ }
395
+ /**
396
+ * Map a quantifier path step to its display label.
397
+ * @param code - Quantifier step value.
398
+ * @returns Display metadata for the quantifier.
399
+ * @throws {CallciumError} If the code is not a recognised quantifier.
400
+ */
401
+ function lookupQuantifier(code) {
402
+ const info = quantifierByCode.get(code);
403
+ if (!info) throw new CallciumError("INVALID_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
404
+ return info;
405
+ }
406
+ /** ABI type code ranges and sentinel values for the descriptor format. */
407
+ const TypeCode = {
408
+ UINT_MIN: 0,
409
+ UINT_MAX: 31,
410
+ INT_MIN: 32,
411
+ INT_MAX: 63,
412
+ ADDRESS: 64,
413
+ BOOL: 65,
414
+ FUNCTION: 66,
415
+ FIXED_BYTES_MIN: 80,
416
+ FIXED_BYTES_MAX: 111,
417
+ BYTES: 112,
418
+ STRING: 113,
419
+ STATIC_ARRAY: 128,
420
+ DYNAMIC_ARRAY: 129,
421
+ TUPLE: 144
422
+ };
423
+ /** Throw an UNKNOWN_TYPE_CODE error. */
424
+ function unknownTypeCode(code) {
425
+ throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
426
+ }
427
+ const ELEMENTARY = {
428
+ typeClass: "elementary",
429
+ isDynamic: false
430
+ };
431
+ const ELEMENTARY_DYN = {
432
+ typeClass: "elementary",
433
+ isDynamic: true
434
+ };
435
+ const STATIC_ARR = {
436
+ typeClass: "staticArray",
437
+ isDynamic: false
438
+ };
439
+ const DYNAMIC_ARR = {
440
+ typeClass: "dynamicArray",
441
+ isDynamic: true
442
+ };
443
+ const TUPLE = {
444
+ typeClass: "tuple",
445
+ isDynamic: false
446
+ };
447
+ /**
448
+ * Classify a type code into its structural category and dynamism.
449
+ * Lightweight variant of `lookupTypeCode` that skips label computation.
450
+ * @param code - A single-byte descriptor type code.
451
+ * @returns Type class and whether the type is ABI-dynamic.
452
+ * @throws {CallciumError} If the code is not a recognised type code.
453
+ */
454
+ function classifyTypeCode(code) {
455
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
456
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
457
+ if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
458
+ if (code >= 67 && code <= 79) unknownTypeCode(code);
459
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
460
+ if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
461
+ if (code >= 114 && code <= 127) unknownTypeCode(code);
462
+ if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
463
+ if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
464
+ if (code >= 130 && code <= 143) unknownTypeCode(code);
465
+ if (code === TypeCode.TUPLE) return TUPLE;
466
+ unknownTypeCode(code);
467
+ }
468
+ /** Compute the Solidity type label for a type code. */
469
+ function typeCodeLabel(code) {
470
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
471
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
472
+ if (code === TypeCode.ADDRESS) return "address";
473
+ if (code === TypeCode.BOOL) return "bool";
474
+ if (code === TypeCode.FUNCTION) return "function";
475
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
476
+ if (code === TypeCode.BYTES) return "bytes";
477
+ if (code === TypeCode.STRING) return "string";
478
+ if (code === TypeCode.STATIC_ARRAY) return "T[k]";
479
+ if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
480
+ if (code === TypeCode.TUPLE) return "tuple";
481
+ return `0x${code.toString(16).padStart(2, "0")}`;
482
+ }
483
+ /**
484
+ * Map a raw type code byte to its Solidity label, structural category, and dynamism.
485
+ * @param code - A single-byte descriptor type code.
486
+ * @returns Label, type class, and whether the type is ABI-dynamic.
487
+ * @throws {CallciumError} If the code is not a recognised type code.
488
+ */
489
+ function lookupTypeCode(code) {
490
+ return {
491
+ label: typeCodeLabel(code),
492
+ ...classifyTypeCode(code)
493
+ };
494
+ }
495
+ //#endregion
496
+ //#region src/descriptor.ts
497
+ /**
498
+ * Return the byte length of the node at `offset`.
499
+ * Elementary nodes occupy exactly one byte; composite nodes encode their
500
+ * length in the lower 12 bits of the 24-bit meta field.
501
+ */
502
+ function nodeLength(desc, offset) {
503
+ if (desc[offset] < TypeCode.STATIC_ARRAY) return DescriptorFormat.TYPECODE_SIZE;
504
+ return readU24(desc, offset + 1) & DescriptorFormat.META_NODE_LENGTH_MASK;
505
+ }
506
+ /**
507
+ * Return the byte offset of the `fieldIndex`-th field inside a tuple node.
508
+ * Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
509
+ */
510
+ function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
511
+ let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
512
+ for (let i = 0; i < fieldIndex; i++) cursor += nodeLength(desc, cursor);
513
+ return cursor;
514
+ }
515
+ /** Return paramCount from header byte 1. */
516
+ function paramCount(desc) {
517
+ return desc[1];
518
+ }
519
+ /** Return byte offset of the N-th top-level param (0-indexed). */
520
+ function paramOffset(desc, index) {
521
+ const count = desc[1];
522
+ if (index >= count) throw new CallciumError("INVALID_PATH", `Param index ${index} out of range (paramCount=${count}).`);
523
+ let cursor = DescriptorFormat.HEADER_SIZE;
524
+ for (let i = 0; i < index; i++) cursor += nodeLength(desc, cursor);
525
+ return cursor;
526
+ }
527
+ /**
528
+ * Inspect the type node at a given byte offset.
529
+ * @param desc - Raw descriptor bytes.
530
+ * @param offset - Byte position of the node.
531
+ * @returns typeCode, isDynamic, and staticSize in bytes.
532
+ */
533
+ function inspect$1(desc, offset) {
534
+ const typeCode = desc[offset];
535
+ let staticWords;
536
+ if (typeCode < TypeCode.STATIC_ARRAY) staticWords = typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
537
+ else staticWords = readU24(desc, offset + 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
538
+ return {
539
+ typeCode,
540
+ isDynamic: staticWords === 0,
541
+ staticSize: staticWords * 32
542
+ };
543
+ }
544
+ /**
545
+ * Resolve the type at a calldata path (array of step indices).
546
+ *
547
+ * The first step selects a top-level param. Each subsequent step descends
548
+ * into the current node: for tuples it selects a field, for arrays it
549
+ * advances to the element descriptor.
550
+ *
551
+ * @param desc - Raw descriptor bytes.
552
+ * @param steps - Path steps, length >= 1.
553
+ * @returns TypeInfo for the node at the resolved path.
554
+ * @throws {CallciumError} On empty steps, out-of-bounds param, or descent into elementary type.
555
+ */
556
+ function typeAt(desc, steps) {
557
+ if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Path must have at least one step.");
558
+ const paramIndex = steps[0];
559
+ const count = desc[1];
560
+ if (paramIndex >= count) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (paramCount=${count}).`);
561
+ let cursor = paramOffset(desc, paramIndex);
562
+ for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
563
+ const typeCode = desc[cursor];
564
+ const step = steps[stepIndex];
565
+ if (typeCode === TypeCode.TUPLE) {
566
+ const fields = tupleFieldCount(desc, cursor);
567
+ if (step >= fields) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
568
+ cursor = tupleFieldOffset(desc, cursor, step);
569
+ } else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) cursor = arrayElementOffset(cursor);
570
+ else throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type at offset ${cursor}.`);
571
+ }
572
+ return inspect$1(desc, cursor);
573
+ }
574
+ /**
575
+ * Return tuple field count at a tuple node offset.
576
+ * @param desc - Raw descriptor bytes.
577
+ * @param offset - Byte position of the tuple node.
578
+ */
579
+ function tupleFieldCount(desc, offset) {
580
+ return readU16(desc, offset + 4);
581
+ }
582
+ /**
583
+ * Return static array length at a static array node offset.
584
+ * @param desc - Raw descriptor bytes.
585
+ * @param offset - Byte position of the static array node.
586
+ */
587
+ function staticArrayLength(desc, offset) {
588
+ const elemOffset = arrayElementOffset(offset);
589
+ return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
590
+ }
591
+ /**
592
+ * Return the byte offset of the array element descriptor.
593
+ * @param offset - Byte position of the array node (static or dynamic).
594
+ */
595
+ function arrayElementOffset(offset) {
596
+ return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
597
+ }
598
+ /** Inspect and navigate raw descriptor bytes. */
599
+ const Descriptor = {
600
+ paramCount,
601
+ paramOffset,
602
+ inspect: inspect$1,
603
+ typeAt,
604
+ tupleFieldOffset,
605
+ tupleFieldCount,
606
+ staticArrayLength,
607
+ nodeLength,
608
+ arrayElementOffset
609
+ };
610
+ //#endregion
611
+ //#region src/type-desc.ts
612
+ /**
613
+ * Extract the staticWords count from a descriptor node.
614
+ *
615
+ * For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
616
+ * BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
617
+ * reads the 24-bit meta field at offset 1 and shifts right by
618
+ * META_STATIC_WORDS_SHIFT.
619
+ */
620
+ function extractStaticWords(desc) {
621
+ const typeCode = desc[0];
622
+ if (typeCode < TypeCode.STATIC_ARRAY) return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
623
+ return readU24(desc, 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
624
+ }
625
+ /** Build a descriptor node for `address`. */
626
+ function address() {
627
+ return new Uint8Array([TypeCode.ADDRESS]);
628
+ }
629
+ /** Build a descriptor node for `bool`. */
630
+ function bool() {
631
+ return new Uint8Array([TypeCode.BOOL]);
632
+ }
633
+ /** Build a descriptor node for `function`. */
634
+ function function_() {
635
+ return new Uint8Array([TypeCode.FUNCTION]);
636
+ }
637
+ /** Build a descriptor node for `uint256`. */
638
+ function uint256() {
639
+ return new Uint8Array([TypeCode.UINT_MAX]);
640
+ }
641
+ /**
642
+ * Build a descriptor node for `uintN`.
643
+ * @param bits - Bit width, 8–256 in steps of 8.
644
+ * @throws {CallciumError} If bits is out of range or not a multiple of 8.
645
+ */
646
+ function uintN(bits) {
647
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("INVALID_TYPE_STRING", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8.`);
648
+ return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
649
+ }
650
+ /**
651
+ * Build a descriptor node for `intN`.
652
+ * @param bits - Bit width, 8–256 in steps of 8.
653
+ * @throws {CallciumError} If bits is out of range or not a multiple of 8.
654
+ */
655
+ function intN(bits) {
656
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("INVALID_TYPE_STRING", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8.`);
657
+ return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
658
+ }
659
+ /** Build a descriptor node for `bytes` (dynamic). */
660
+ function bytes() {
661
+ return new Uint8Array([TypeCode.BYTES]);
662
+ }
663
+ /** Build a descriptor node for `string`. */
664
+ function string_() {
665
+ return new Uint8Array([TypeCode.STRING]);
666
+ }
667
+ /**
668
+ * Build a descriptor node for `bytesN`.
669
+ * @param n - Byte width, 1–32.
670
+ * @throws {CallciumError} If n is out of range.
671
+ */
672
+ function bytesN(n) {
673
+ if (n < 1 || n > 32) throw new CallciumError("INVALID_TYPE_STRING", `Invalid bytesN size: ${n}. Must be 1–32.`);
674
+ return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
675
+ }
676
+ /**
677
+ * Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
678
+ *
679
+ * Dynamic form: `[0x81][meta:3][elemDesc]`
680
+ * Static form: `[0x80][meta:3][elemDesc][length:be16]`
681
+ *
682
+ * @param elemDesc - Descriptor bytes for the element type.
683
+ * @param length - Fixed array length. When omitted, produces a dynamic array.
684
+ * @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
685
+ */
686
+ function array(elemDesc, length) {
687
+ if (length === void 0) {
688
+ const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
689
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
690
+ const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
691
+ const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
692
+ buf[0] = TypeCode.DYNAMIC_ARRAY;
693
+ writeBE24(buf, 1, meta24);
694
+ buf.set(elemDesc, 4);
695
+ return buf;
696
+ }
697
+ if (length <= 0 || length > DescriptorFormat.MAX_STATIC_ARRAY_LENGTH) throw new CallciumError("INVALID_ARRAY_LENGTH", `Static array length ${length} out of range. Must be 1–${DescriptorFormat.MAX_STATIC_ARRAY_LENGTH}.`);
698
+ const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
699
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
700
+ const elemStaticWords = extractStaticWords(elemDesc);
701
+ const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
702
+ if (staticWords > DescriptorFormat.META_NODE_LENGTH_MASK) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Static array static words ${staticWords} exceeds 12-bit maximum ${DescriptorFormat.META_NODE_LENGTH_MASK}.`);
703
+ const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
704
+ const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
705
+ buf[0] = TypeCode.STATIC_ARRAY;
706
+ writeBE24(buf, 1, meta24);
707
+ buf.set(elemDesc, 4);
708
+ writeBE16(buf, 4 + elemDesc.length, length);
709
+ return buf;
710
+ }
711
+ /**
712
+ * Build a descriptor node for a tuple.
713
+ *
714
+ * Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
715
+ *
716
+ * @param fieldDescs - Descriptor bytes for each field, in order.
717
+ * @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
718
+ */
719
+ function tuple(fieldDescs) {
720
+ if (fieldDescs.length === 0 || fieldDescs.length > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DescriptorFormat.MAX_TUPLE_FIELDS}.`);
721
+ const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
722
+ const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
723
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
724
+ let anyDynamic = false;
725
+ let staticWordsSum = 0;
726
+ for (const field of fieldDescs) {
727
+ const staticWords = extractStaticWords(field);
728
+ if (staticWords === 0) anyDynamic = true;
729
+ staticWordsSum += staticWords;
730
+ }
731
+ const staticWords = anyDynamic ? 0 : staticWordsSum;
732
+ if (staticWords > DescriptorFormat.META_NODE_LENGTH_MASK) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Tuple static words ${staticWords} exceeds 12-bit maximum ${DescriptorFormat.META_NODE_LENGTH_MASK}.`);
733
+ const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
734
+ const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
735
+ buf[0] = TypeCode.TUPLE;
736
+ writeBE24(buf, 1, meta24);
737
+ writeBE16(buf, 4, fieldDescs.length);
738
+ let offset = 6;
739
+ for (const field of fieldDescs) {
740
+ buf.set(field, offset);
741
+ offset += field.length;
742
+ }
743
+ return buf;
744
+ }
745
+ //#endregion
746
+ //#region src/descriptor-coder.ts
747
+ /**
748
+ * Collect the positions of depth-0 commas in input[start..end).
749
+ * Commas inside `()` or `[]` are skipped.
750
+ */
751
+ function commaPositions(input, start, end) {
752
+ const positions = [];
753
+ let depth = 0;
754
+ for (let i = start; i < end; i++) {
755
+ const char = input[i];
756
+ if (char === "(" || char === "[") depth++;
757
+ else if (char === ")" || char === "]") depth--;
758
+ else if (char === "," && depth === 0) positions.push(i);
759
+ }
760
+ return positions;
761
+ }
762
+ /**
763
+ * Parse a decimal integer from input[start..end).
764
+ * Returns -1 if the substring is empty or contains non-digit characters.
765
+ */
766
+ function parseUint(input, start, end) {
767
+ if (start >= end) return -1;
768
+ let value = 0;
769
+ for (let i = start; i < end; i++) {
770
+ const code = input.charCodeAt(i);
771
+ if (code < 48 || code > 57) return -1;
772
+ value = value * 10 + (code - 48);
773
+ }
774
+ return value;
775
+ }
776
+ /**
777
+ * Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
778
+ * `start` and the substring to parse ends before `end`.
779
+ */
780
+ function parseTuple(input, start, end) {
781
+ if (input[start] !== "(") throw new CallciumError("INVALID_TYPE_STRING", `Expected '(' at position ${start}`);
782
+ let depth = 0;
783
+ let closePos = -1;
784
+ for (let i = start; i < end; i++) if (input[i] === "(") depth++;
785
+ else if (input[i] === ")") {
786
+ depth--;
787
+ if (depth === 0) {
788
+ closePos = i;
789
+ break;
790
+ }
791
+ }
792
+ if (closePos === -1) throw new CallciumError("INVALID_TYPE_STRING", `Unmatched '(' at position ${start}`);
793
+ const innerStart = start + 1;
794
+ const innerEnd = closePos;
795
+ const commas = commaPositions(input, innerStart, innerEnd);
796
+ const segments = [];
797
+ let segStart = innerStart;
798
+ for (const comma of commas) {
799
+ segments.push([segStart, comma]);
800
+ segStart = comma + 1;
801
+ }
802
+ segments.push([segStart, innerEnd]);
803
+ if (segments.length === 1 && segments[0][0] === segments[0][1]) throw new CallciumError("INVALID_TYPE_STRING", "Empty tuple is not allowed");
804
+ return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
805
+ }
806
+ /** Parse the base type (no array suffixes) from input[start..end). */
807
+ function parseBaseType(input, start, end) {
808
+ const segment = input.slice(start, end);
809
+ if (segment === "address") return address();
810
+ if (segment === "bool") return bool();
811
+ if (segment === "function") return function_();
812
+ if (segment === "bytes") return bytes();
813
+ if (segment === "string") return string_();
814
+ if (segment === "uint256") return uint256();
815
+ if (segment === "int256") return intN(256);
816
+ if (segment.startsWith("(")) return parseTuple(input, start, end);
817
+ if (segment.startsWith("uint")) {
818
+ const bits = parseUint(input, start + 4, end);
819
+ if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
820
+ return uintN(bits);
821
+ }
822
+ if (segment.startsWith("int")) {
823
+ const bits = parseUint(input, start + 3, end);
824
+ if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
825
+ return intN(bits);
826
+ }
827
+ if (segment.startsWith("bytes")) {
828
+ const n = parseUint(input, start + 5, end);
829
+ if (n === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
830
+ return bytesN(n);
831
+ }
832
+ throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
833
+ }
834
+ /**
835
+ * Parse a type segment from input[start..end).
836
+ *
837
+ * Scans backward from end to collect all `[...]` suffixes, identifies the
838
+ * base type extent, parses the base, then applies suffixes left-to-right.
839
+ */
840
+ function parseType(input, start, end) {
841
+ if (start >= end) throw new CallciumError("INVALID_TYPE_STRING", "Empty type segment");
842
+ const suffixes = [];
843
+ let baseEnd = end;
844
+ while (baseEnd > start && input[baseEnd - 1] === "]") {
845
+ const closePos = baseEnd - 1;
846
+ let openPos = closePos - 1;
847
+ while (openPos > start && input[openPos] !== "[") openPos--;
848
+ if (input[openPos] !== "[") throw new CallciumError("INVALID_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
849
+ const innerStart = openPos + 1;
850
+ const innerEnd = closePos;
851
+ if (innerStart === innerEnd) suffixes.unshift(void 0);
852
+ else {
853
+ const length = parseUint(input, innerStart, innerEnd);
854
+ if (length === -1) throw new CallciumError("INVALID_TYPE_STRING", `Invalid array length at position ${innerStart}`);
855
+ suffixes.unshift(length);
856
+ }
857
+ baseEnd = openPos;
858
+ }
859
+ let desc = parseBaseType(input, start, baseEnd);
860
+ for (const length of suffixes) desc = array(desc, length);
861
+ return desc;
862
+ }
863
+ /**
864
+ * Encode a comma-separated list of ABI type strings into a binary descriptor.
865
+ *
866
+ * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
867
+ * @returns Binary descriptor bytes starting with the version+paramCount header.
868
+ * @throws {CallciumError} With code `INVALID_TYPE_STRING` for malformed input.
869
+ * @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
870
+ */
871
+ function fromTypes(typesCsv) {
872
+ if (typesCsv === "") return new Uint8Array([DescriptorFormat.VERSION, 0]);
873
+ const commas = commaPositions(typesCsv, 0, typesCsv.length);
874
+ const segments = [];
875
+ let segStart = 0;
876
+ for (const comma of commas) {
877
+ segments.push([segStart, comma]);
878
+ segStart = comma + 1;
879
+ }
880
+ segments.push([segStart, typesCsv.length]);
881
+ const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
882
+ if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}.`);
883
+ const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
884
+ const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
885
+ result[0] = DescriptorFormat.VERSION;
886
+ result[1] = paramDescs.length;
887
+ let offset = DescriptorFormat.HEADER_SIZE;
888
+ for (const desc of paramDescs) {
889
+ result.set(desc, offset);
890
+ offset += desc.length;
891
+ }
892
+ return result;
893
+ }
894
+ /** Reconstruct a Solidity type string from a descriptor node at the given offset. */
895
+ function nodeToTypeString(desc, offset) {
896
+ const typeCode = desc[offset];
897
+ if (typeCode === TypeCode.TUPLE) {
898
+ const fieldCount = Descriptor.tupleFieldCount(desc, offset);
899
+ const fieldTypes = [];
900
+ let fieldOffset = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
901
+ for (let i = 0; i < fieldCount; i++) {
902
+ fieldTypes.push(nodeToTypeString(desc, fieldOffset));
903
+ fieldOffset += Descriptor.nodeLength(desc, fieldOffset);
904
+ }
905
+ return `(${fieldTypes.join(",")})`;
906
+ }
907
+ if (typeCode === TypeCode.STATIC_ARRAY) {
908
+ const length = Descriptor.staticArrayLength(desc, offset);
909
+ return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[${length}]`;
910
+ }
911
+ if (typeCode === TypeCode.DYNAMIC_ARRAY) return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[]`;
912
+ return lookupTypeCode(typeCode).label;
913
+ }
914
+ /**
915
+ * Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
916
+ *
917
+ * Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
918
+ *
919
+ * @param desc - Binary descriptor bytes (with version+paramCount header).
920
+ * @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
921
+ * @throws {CallciumError} If the descriptor is malformed.
922
+ */
923
+ function toTypes(desc) {
924
+ const count = Descriptor.paramCount(desc);
925
+ const types = [];
926
+ let offset = DescriptorFormat.HEADER_SIZE;
927
+ for (let i = 0; i < count; i++) {
928
+ types.push(nodeToTypeString(desc, offset));
929
+ offset += Descriptor.nodeLength(desc, offset);
930
+ }
931
+ return types.join(",");
932
+ }
933
+ /** Recursively parse a single descriptor node starting at offset. */
934
+ function parseNode(data, offset) {
935
+ if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
936
+ const code = data[offset];
937
+ const info = classifyTypeCode(code);
938
+ const metaOffset = offset + DescriptorFormat.TYPECODE_SIZE;
939
+ if (info.typeClass === "elementary") return {
940
+ node: {
941
+ type: "elementary",
942
+ typeCode: code,
943
+ isDynamic: info.isDynamic,
944
+ staticSize: info.isDynamic ? 0 : 32,
945
+ span: {
946
+ start: offset,
947
+ end: metaOffset
948
+ }
949
+ },
950
+ next: metaOffset
951
+ };
952
+ const metaEnd = metaOffset + DescriptorFormat.COMPOSITE_META_SIZE;
953
+ if (metaEnd > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
954
+ const meta = readU24(data, metaOffset);
955
+ const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
956
+ const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
957
+ const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
958
+ if (nodeLength < minHeader) throw new CallciumError("MALFORMED_HEADER", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
959
+ if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
960
+ const isDynamic = staticWords === 0;
961
+ const staticSize = isDynamic ? 0 : staticWords * 32;
962
+ const nodeEnd = offset + nodeLength;
963
+ if (info.typeClass === "tuple") {
964
+ const fieldCountOffset = metaEnd;
965
+ if (fieldCountOffset + 2 > data.length) throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
966
+ const fieldCount = readU16(data, fieldCountOffset);
967
+ if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
968
+ if (fieldCount > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldCount} exceeds maximum ${DescriptorFormat.MAX_TUPLE_FIELDS}`, offset);
969
+ const fields = [];
970
+ let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
971
+ for (let i = 0; i < fieldCount; i++) {
972
+ const result = parseNode(data, cursor);
973
+ fields.push(result.node);
974
+ cursor = result.next;
975
+ }
976
+ return {
977
+ node: {
978
+ type: "tuple",
979
+ typeCode: code,
980
+ isDynamic,
981
+ staticSize,
982
+ fields,
983
+ span: {
984
+ start: offset,
985
+ end: nodeEnd
986
+ }
987
+ },
988
+ next: nodeEnd
989
+ };
990
+ }
991
+ if (info.typeClass === "staticArray") {
992
+ const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset));
993
+ const lengthOffset = elemResult.next;
994
+ if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
995
+ const length = readU16(data, lengthOffset);
996
+ if (length === 0) throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
997
+ if (length > DescriptorFormat.MAX_STATIC_ARRAY_LENGTH) throw new CallciumError("INVALID_ARRAY_LENGTH", `Static array length ${length} exceeds maximum ${DescriptorFormat.MAX_STATIC_ARRAY_LENGTH}`, offset);
998
+ return {
999
+ node: {
1000
+ type: "staticArray",
1001
+ typeCode: code,
1002
+ isDynamic,
1003
+ staticSize,
1004
+ element: elemResult.node,
1005
+ length,
1006
+ span: {
1007
+ start: offset,
1008
+ end: nodeEnd
1009
+ }
1010
+ },
1011
+ next: nodeEnd
1012
+ };
1013
+ }
1014
+ return {
1015
+ node: {
1016
+ type: "dynamicArray",
1017
+ typeCode: code,
1018
+ isDynamic: true,
1019
+ staticSize: 0,
1020
+ element: parseNode(data, Descriptor.arrayElementOffset(offset)).node,
1021
+ span: {
1022
+ start: offset,
1023
+ end: nodeEnd
1024
+ }
1025
+ },
1026
+ next: nodeEnd
1027
+ };
1028
+ }
1029
+ /** Convert a zero-based param index to a BE16 hex path step. */
1030
+ function indexToPath(index) {
1031
+ return `0x${index.toString(16).padStart(4, "0")}`;
1032
+ }
1033
+ /** Decode a binary descriptor blob, returning the structural representation and AST. */
1034
+ function decodeDescriptor(data) {
1035
+ if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
1036
+ const version = data[0];
1037
+ if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
1038
+ if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
1039
+ const declaredCount = data[1];
1040
+ const params = [];
1041
+ const tree = [];
1042
+ let cursor = DescriptorFormat.HEADER_SIZE;
1043
+ while (cursor < data.length) {
1044
+ if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("PARAM_COUNT_MISMATCH", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
1045
+ const { node, next } = parseNode(data, cursor);
1046
+ tree.push(node);
1047
+ params.push({
1048
+ index: params.length,
1049
+ typeCode: node.typeCode,
1050
+ isDynamic: node.isDynamic,
1051
+ staticSize: node.staticSize,
1052
+ path: indexToPath(params.length),
1053
+ span: {
1054
+ start: cursor,
1055
+ end: next
1056
+ }
1057
+ });
1058
+ cursor = next;
1059
+ }
1060
+ if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
1061
+ return {
1062
+ descriptor: {
1063
+ version,
1064
+ params
1065
+ },
1066
+ tree
1067
+ };
1068
+ }
1069
+ /** Encode and decode descriptors. */
1070
+ const DescriptorCoder = {
1071
+ fromTypes,
1072
+ toTypes
1073
+ };
1074
+ //#endregion
1075
+ //#region src/policy-coder.ts
1076
+ /**
1077
+ * Parse a BE16-encoded hex path into an array of step values.
1078
+ * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
1079
+ * @returns Array of numeric step values.
1080
+ */
1081
+ function parsePathSteps(path) {
1082
+ const body = path.slice(2);
1083
+ const steps = [];
1084
+ for (let i = 0; i < body.length; i += 4) steps.push(parseInt(body.slice(i, i + 4), 16));
1085
+ return steps;
1086
+ }
1087
+ /** Wrap a value with its byte span for positional tracking. */
1088
+ function field(value, start, end) {
1089
+ return {
1090
+ value,
1091
+ span: {
1092
+ start,
1093
+ end
1094
+ }
1095
+ };
1096
+ }
1097
+ /** Decode a policy blob, returning the structural representation and descriptor AST. */
1098
+ function decodePolicy(blob) {
1099
+ const data = hexToBytes(blob);
1100
+ if (data.length < PolicyFormat.DESC_OFFSET + 1) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
1101
+ const headerByte = data[0];
1102
+ const version = headerByte & PolicyFormat.VERSION_MASK;
1103
+ if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
1104
+ if ((headerByte & PolicyFormat.RESERVED_MASK) !== 0) throw new CallciumError("MALFORMED_HEADER", "Reserved header bits must be zero");
1105
+ const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
1106
+ const selectorStart = PolicyFormat.SELECTOR_OFFSET;
1107
+ const selectorEnd = selectorStart + PolicyFormat.SELECTOR_SIZE;
1108
+ const selectorHex = toHex(data, selectorStart, selectorEnd);
1109
+ if (isSelectorless && selectorHex !== "0x00000000") throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
1110
+ const descLengthStart = PolicyFormat.DESC_LENGTH_OFFSET;
1111
+ const descLengthValue = readU16(data, descLengthStart);
1112
+ if (descLengthValue < 2) throw new CallciumError("MALFORMED_HEADER", "Descriptor length must be at least 2");
1113
+ const descStart = PolicyFormat.DESC_OFFSET;
1114
+ const descEnd = descStart + descLengthValue;
1115
+ const groupCountOffset = descEnd;
1116
+ if (data.length < groupCountOffset + PolicyFormat.GROUP_COUNT_SIZE) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
1117
+ const { descriptor: desc, tree } = decodeDescriptor(data.subarray(descStart, descEnd));
1118
+ const params = desc.params.map((param) => ({
1119
+ ...param,
1120
+ span: {
1121
+ start: descStart + param.span.start,
1122
+ end: descStart + param.span.end
1123
+ }
1124
+ }));
1125
+ const descriptorRaw = toHex(data, descStart, descEnd);
1126
+ const groupCountStart = groupCountOffset;
1127
+ const groupCountEnd = groupCountStart + PolicyFormat.GROUP_COUNT_SIZE;
1128
+ const groupCountValue = data[groupCountOffset];
1129
+ if (groupCountValue === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
1130
+ let offset = groupCountEnd;
1131
+ const groups = [];
1132
+ for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
1133
+ if (offset + PolicyFormat.GROUP_HEADER_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
1134
+ const ruleCountStart = offset;
1135
+ const ruleCountValue = readU16(data, ruleCountStart);
1136
+ const groupSizeStart = offset + PolicyFormat.GROUP_RULECOUNT_SIZE;
1137
+ const groupSizeValue = readU32(data, groupSizeStart);
1138
+ const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
1139
+ const groupEnd = groupBodyStart + groupSizeValue;
1140
+ if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
1141
+ if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("GROUP_SIZE_MISMATCH", "Declared group size is too small for its rule count", offset);
1142
+ if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
1143
+ const rules = [];
1144
+ let ruleOffset = groupBodyStart;
1145
+ for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
1146
+ if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
1147
+ const ruleSizeValue = readU16(data, ruleOffset);
1148
+ if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_SIZE_MISMATCH", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
1149
+ if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
1150
+ const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
1151
+ const scopeValue = data[scopeOffset];
1152
+ if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
1153
+ const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
1154
+ const depthValue = data[depthOffset];
1155
+ if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
1156
+ if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
1157
+ const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
1158
+ const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
1159
+ const pathHex = toHex(data, pathStart, pathStart + pathLength);
1160
+ const opCodeOffset = pathStart + pathLength;
1161
+ const opCodeValue = data[opCodeOffset];
1162
+ const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
1163
+ const dataLengthValue = readU16(data, dataLengthOffset);
1164
+ const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
1165
+ const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + dataLengthValue;
1166
+ if (ruleSizeValue !== expectedRuleSize) throw new CallciumError("RULE_SIZE_MISMATCH", `Declared rule size ${ruleSizeValue} does not match computed size ${expectedRuleSize}`, ruleOffset);
1167
+ const opBase = opCodeValue & ~Op.NOT;
1168
+ if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("INVALID_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
1169
+ rules.push({
1170
+ ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
1171
+ scope: field(scopeValue, scopeOffset, scopeOffset + 1),
1172
+ pathDepth: field(depthValue, depthOffset, depthOffset + 1),
1173
+ path: field(pathHex, pathStart, pathStart + pathLength),
1174
+ opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
1175
+ dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
1176
+ data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
1177
+ span: {
1178
+ start: ruleOffset,
1179
+ end: ruleOffset + ruleSizeValue
1180
+ }
1181
+ });
1182
+ ruleOffset += ruleSizeValue;
1183
+ }
1184
+ if (ruleOffset !== groupEnd) throw new CallciumError("GROUP_SIZE_MISMATCH", "Rules do not exactly fill the declared group size", offset);
1185
+ groups.push({
1186
+ ruleCount: field(ruleCountValue, ruleCountStart, ruleCountStart + PolicyFormat.GROUP_RULECOUNT_SIZE),
1187
+ groupSize: field(groupSizeValue, groupSizeStart, groupSizeStart + PolicyFormat.GROUP_SIZE_SIZE),
1188
+ rules,
1189
+ span: {
1190
+ start: offset,
1191
+ end: groupEnd
1192
+ }
1193
+ });
1194
+ offset = groupEnd;
1195
+ }
1196
+ if (offset !== data.length) throw new CallciumError("TRAILING_BYTES", "Trailing bytes after last group", offset);
1197
+ return {
1198
+ policy: {
1199
+ header: field(headerByte, 0, PolicyFormat.HEADER_SIZE),
1200
+ selector: field(selectorHex, selectorStart, selectorEnd),
1201
+ descLength: field(descLengthValue, descLengthStart, descLengthStart + PolicyFormat.DESC_LENGTH_SIZE),
1202
+ descriptor: {
1203
+ raw: descriptorRaw,
1204
+ header: field({
1205
+ version: desc.version,
1206
+ paramCount: desc.params.length
1207
+ }, descStart, descStart + DescriptorFormat.HEADER_SIZE),
1208
+ params,
1209
+ span: {
1210
+ start: descStart,
1211
+ end: descEnd
1212
+ }
1213
+ },
1214
+ groupCount: field(groupCountValue, groupCountStart, groupCountEnd),
1215
+ groups,
1216
+ span: {
1217
+ start: 0,
1218
+ end: data.length
1219
+ },
1220
+ version,
1221
+ isSelectorless
1222
+ },
1223
+ tree
1224
+ };
1225
+ }
1226
+ /** Flatten a Constraint into one Rule per operator. */
1227
+ function flattenConstraint(constraint) {
1228
+ const path = hexToBytes(constraint.path);
1229
+ return constraint.operators.map((op) => ({
1230
+ scope: constraint.scope,
1231
+ path,
1232
+ operator: hexToBytes(op)
1233
+ }));
1234
+ }
1235
+ /** Compare two byte arrays lexicographically. */
1236
+ function compareBytes(a, b) {
1237
+ const length = Math.min(a.length, b.length);
1238
+ for (let i = 0; i < length; i++) if (a[i] !== b[i]) return a[i] - b[i];
1239
+ return a.length - b.length;
1240
+ }
1241
+ /** Sort rules by (scope, pathDepth, pathBytes, operatorBytes). */
1242
+ function sortRules(rules) {
1243
+ rules.sort((a, b) => {
1244
+ if (a.scope !== b.scope) return a.scope - b.scope;
1245
+ const depthA = a.path.length / 2;
1246
+ const depthB = b.path.length / 2;
1247
+ if (depthA !== depthB) return depthA - depthB;
1248
+ const pathCmp = compareBytes(a.path, b.path);
1249
+ if (pathCmp !== 0) return pathCmp;
1250
+ return compareBytes(a.operator, b.operator);
1251
+ });
1252
+ }
1253
+ /** Serialize a single rule to its wire format bytes. */
1254
+ function encodeRule(rule) {
1255
+ if (rule.path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
1256
+ if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
1257
+ if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
1258
+ const depth = rule.path.length / 2;
1259
+ if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("INVALID_PATH", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
1260
+ if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
1261
+ const opCode = rule.operator[0];
1262
+ const data = rule.operator.subarray(1);
1263
+ const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + data.length;
1264
+ if (ruleSize > 65535) throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
1265
+ const buf = new Uint8Array(ruleSize);
1266
+ writeBE16(buf, 0, ruleSize);
1267
+ buf[2] = rule.scope;
1268
+ buf[3] = depth;
1269
+ buf.set(rule.path, 4);
1270
+ const opOffset = 4 + rule.path.length;
1271
+ buf[opOffset] = opCode;
1272
+ writeBE16(buf, opOffset + 1, data.length);
1273
+ buf.set(data, opOffset + 3);
1274
+ return buf;
1275
+ }
1276
+ /** Serialize all rules in a group to a single byte array. */
1277
+ function encodeGroupRules(rules) {
1278
+ const parts = rules.map(encodeRule);
1279
+ const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
1280
+ const buf = new Uint8Array(totalSize);
1281
+ let offset = 0;
1282
+ for (const part of parts) {
1283
+ buf.set(part, offset);
1284
+ offset += part.length;
1285
+ }
1286
+ return buf;
1287
+ }
1288
+ /** Build a single operator hex string from a decoded rule's opCode and data. */
1289
+ function buildOperatorHex(rule) {
1290
+ return `0x${rule.opCode.value.toString(16).padStart(2, "0")}${rule.data.value.slice(2)}`;
1291
+ }
1292
+ /**
1293
+ * Encode a PolicyData structure into the canonical binary format.
1294
+ * @param data - The policy data to encode.
1295
+ * @returns The encoded policy as a 0x-prefixed hex string.
1296
+ */
1297
+ function encode(data) {
1298
+ const sortedGroups = data.groups.map((group) => {
1299
+ const rules = group.flatMap(flattenConstraint);
1300
+ sortRules(rules);
1301
+ return rules;
1302
+ });
1303
+ if (sortedGroups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
1304
+ if (sortedGroups.length > 255) throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${sortedGroups.length} exceeds maximum 255`);
1305
+ const groupsWithHash = sortedGroups.map((rules, groupIndex) => {
1306
+ if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
1307
+ if (rules.length > 65535) throw new CallciumError("RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`);
1308
+ const wireBytes = encodeGroupRules(rules);
1309
+ return {
1310
+ wireBytes,
1311
+ hash: keccak_256(wireBytes),
1312
+ ruleCount: rules.length
1313
+ };
1314
+ });
1315
+ groupsWithHash.sort((a, b) => compareBytes(a.hash, b.hash));
1316
+ const descBytes = hexToBytes(data.descriptor);
1317
+ if (descBytes.length > 65535) throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
1318
+ const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
1319
+ const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
1320
+ let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
1321
+ for (const g of groupsWithHash) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
1322
+ const out = new Uint8Array(totalSize);
1323
+ let offset = 0;
1324
+ out[offset++] = headerByte;
1325
+ out.set(selectorBytes, offset);
1326
+ offset += PolicyFormat.SELECTOR_SIZE;
1327
+ writeBE16(out, offset, descBytes.length);
1328
+ offset += PolicyFormat.DESC_LENGTH_SIZE;
1329
+ out.set(descBytes, offset);
1330
+ offset += descBytes.length;
1331
+ out[offset++] = groupsWithHash.length;
1332
+ for (const g of groupsWithHash) {
1333
+ writeBE16(out, offset, g.ruleCount);
1334
+ offset += PolicyFormat.GROUP_RULECOUNT_SIZE;
1335
+ out[offset++] = g.wireBytes.length >>> 24 & 255;
1336
+ out[offset++] = g.wireBytes.length >>> 16 & 255;
1337
+ out[offset++] = g.wireBytes.length >>> 8 & 255;
1338
+ out[offset++] = g.wireBytes.length & 255;
1339
+ out.set(g.wireBytes, offset);
1340
+ offset += g.wireBytes.length;
1341
+ }
1342
+ return bytesToHex(out);
1343
+ }
1344
+ /**
1345
+ * Decode a binary policy blob into a PolicyData structure.
1346
+ * @param blob - Binary policy as 0x-prefixed hex string.
1347
+ * @returns The decoded policy data with constraints grouped by scope and path.
1348
+ * @throws {CallciumError} If the blob is structurally malformed.
1349
+ */
1350
+ function decode(blob) {
1351
+ const { policy } = decodePolicy(blob);
1352
+ const groups = policy.groups.map((group) => {
1353
+ const constraintMap = /* @__PURE__ */ new Map();
1354
+ const constraintOrder = [];
1355
+ for (const rule of group.rules) {
1356
+ const key = `${rule.scope.value}:${rule.path.value}`;
1357
+ const opHex = buildOperatorHex(rule);
1358
+ const existing = constraintMap.get(key);
1359
+ if (existing !== void 0) existing.operators.push(opHex);
1360
+ else {
1361
+ const constraint = {
1362
+ scope: rule.scope.value,
1363
+ path: rule.path.value,
1364
+ operators: [opHex],
1365
+ span: rule.span
1366
+ };
1367
+ constraintMap.set(key, constraint);
1368
+ constraintOrder.push(key);
1369
+ }
1370
+ }
1371
+ return constraintOrder.map((k) => constraintMap.get(k));
1372
+ });
1373
+ return {
1374
+ isSelectorless: policy.isSelectorless,
1375
+ selector: policy.selector.value,
1376
+ descriptor: policy.descriptor.raw,
1377
+ groups,
1378
+ span: policy.span
1379
+ };
1380
+ }
1381
+ /**
1382
+ * Inspect a binary policy blob, returning the structural representation with full byte-level spans.
1383
+ * @param blob - Binary policy as 0x-prefixed hex string.
1384
+ * @returns The inspected policy with per-field spans for every structural element.
1385
+ * @throws {CallciumError} If the blob is structurally malformed.
1386
+ */
1387
+ function inspect(blob) {
1388
+ return decodePolicy(blob).policy;
1389
+ }
1390
+ /** Encode, decode, and inspect policies in the canonical binary format. */
1391
+ const PolicyCoder = {
1392
+ encode,
1393
+ decode,
1394
+ inspect
1395
+ };
1396
+ //#endregion
1397
+ //#region src/operators.ts
1398
+ /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
1399
+ function toBigInt(bytes, offset = 0) {
1400
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
1401
+ return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
1402
+ }
1403
+ /** Return true when the type code represents a signed integer (int8 through int256). */
1404
+ function isSigned(typeCode) {
1405
+ return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1406
+ }
1407
+ /** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
1408
+ function isLengthOp(opCode) {
1409
+ const base = opCode & ~Op.NOT;
1410
+ return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
1411
+ }
1412
+ /** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
1413
+ function isLengthValidType(typeCode) {
1414
+ return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
1415
+ }
1416
+ /** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
1417
+ function compareTyped(a, b, typeCode) {
1418
+ if (isSigned(typeCode)) {
1419
+ const sa = BigInt.asIntN(256, a);
1420
+ const sb = BigInt.asIntN(256, b);
1421
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
1422
+ }
1423
+ return a < b ? -1 : a > b ? 1 : 0;
1424
+ }
1425
+ /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
1426
+ function isIn(value, data) {
1427
+ const count = data.length / 32;
1428
+ let lo = 0;
1429
+ let hi = count - 1;
1430
+ while (lo <= hi) {
1431
+ const mid = lo + hi >>> 1;
1432
+ const elem = toBigInt(data, mid * 32);
1433
+ if (value === elem) return true;
1434
+ if (value < elem) hi = mid - 1;
1435
+ else lo = mid + 1;
1436
+ }
1437
+ return false;
1438
+ }
1439
+ /**
1440
+ * Apply a single operator against a value and operand data.
1441
+ * Handles the NOT flag internally by inverting the base operator result.
1442
+ * @param opCode - Operator code, potentially OR'd with the NOT flag.
1443
+ * @param value - The 256-bit value to test (ignored for LENGTH_* operators).
1444
+ * @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
1445
+ * @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
1446
+ * @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
1447
+ * @returns Whether the value satisfies the operator.
1448
+ */
1449
+ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1450
+ const negate = (opCode & Op.NOT) !== 0;
1451
+ const base = opCode & ~Op.NOT;
1452
+ let result;
1453
+ switch (base) {
1454
+ case Op.EQ:
1455
+ result = value === toBigInt(operandData, 0);
1456
+ break;
1457
+ case Op.GT:
1458
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
1459
+ break;
1460
+ case Op.LT:
1461
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
1462
+ break;
1463
+ case Op.GTE:
1464
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
1465
+ break;
1466
+ case Op.LTE:
1467
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
1468
+ break;
1469
+ case Op.BETWEEN: {
1470
+ const min = toBigInt(operandData, 0);
1471
+ const max = toBigInt(operandData, 32);
1472
+ result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
1473
+ break;
1474
+ }
1475
+ case Op.IN:
1476
+ result = isIn(value, operandData);
1477
+ break;
1478
+ case Op.BITMASK_ALL: {
1479
+ const mask = toBigInt(operandData, 0);
1480
+ result = (value & mask) === mask;
1481
+ break;
1482
+ }
1483
+ case Op.BITMASK_ANY:
1484
+ result = (value & toBigInt(operandData, 0)) !== 0n;
1485
+ break;
1486
+ case Op.BITMASK_NONE:
1487
+ result = (value & toBigInt(operandData, 0)) === 0n;
1488
+ break;
1489
+ case Op.LENGTH_EQ:
1490
+ result = BigInt(valueLength) === toBigInt(operandData, 0);
1491
+ break;
1492
+ case Op.LENGTH_GT:
1493
+ result = BigInt(valueLength) > toBigInt(operandData, 0);
1494
+ break;
1495
+ case Op.LENGTH_LT:
1496
+ result = BigInt(valueLength) < toBigInt(operandData, 0);
1497
+ break;
1498
+ case Op.LENGTH_GTE:
1499
+ result = BigInt(valueLength) >= toBigInt(operandData, 0);
1500
+ break;
1501
+ case Op.LENGTH_LTE:
1502
+ result = BigInt(valueLength) <= toBigInt(operandData, 0);
1503
+ break;
1504
+ case Op.LENGTH_BETWEEN: {
1505
+ const length = BigInt(valueLength);
1506
+ const min = toBigInt(operandData, 0);
1507
+ const max = toBigInt(operandData, 32);
1508
+ result = length >= min && length <= max;
1509
+ break;
1510
+ }
1511
+ default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
1512
+ }
1513
+ return negate ? !result : result;
1514
+ }
1515
+ //#endregion
1516
+ //#region src/validation-issue.ts
1517
+ const ZERO = `0x${"0".repeat(64)}`;
1518
+ /** Construct a fully populated Issue. */
1519
+ function makeIssue(severity, category, groupIndex, constraintIndex, code, value1, value2, message) {
1520
+ return {
1521
+ severity,
1522
+ category,
1523
+ groupIndex,
1524
+ constraintIndex,
1525
+ code,
1526
+ value1,
1527
+ value2,
1528
+ message
1529
+ };
1530
+ }
1531
+ /** Create an issue from operator compatibility results. */
1532
+ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1533
+ return makeIssue("error", "typeMismatch", groupIndex, constraintIndex, code, opCode, ZERO, message);
1534
+ }
1535
+ /** eq(v) and neq(v) on same path. */
1536
+ function eqNeqContradiction(groupIndex, constraintIndex, value) {
1537
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "EQ_NEQ_CONTRADICTION", value, ZERO, "eq(v) and neq(v) on same path");
1538
+ }
1539
+ /** Multiple eq() operators with different values. */
1540
+ function conflictingEquality(groupIndex, constraintIndex, existing, newValue) {
1541
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_EQUALITY", existing, newValue, "Multiple eq() operators with different values");
1542
+ }
1543
+ /** Value is outside the physical range of the type. */
1544
+ function outOfPhysicalBounds(groupIndex, constraintIndex, value) {
1545
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_BOUNDS", value, ZERO, "Value is outside the physical range of the type");
1546
+ }
1547
+ /** gt() on type maximum is impossible. */
1548
+ function impossibleGt(groupIndex, constraintIndex, value) {
1549
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_GT", value, ZERO, "gt() on type maximum is impossible");
1550
+ }
1551
+ /** lt() on type minimum is impossible. */
1552
+ function impossibleLt(groupIndex, constraintIndex, value) {
1553
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LT", value, ZERO, "lt() on type minimum is impossible");
1554
+ }
1555
+ /** eq() value is excluded by bound. */
1556
+ function boundsExcludeEquality(groupIndex, constraintIndex, eq, bound) {
1557
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_EQUALITY", eq, bound, "eq() value is excluded by bound");
1558
+ }
1559
+ /** Lower bound exceeds upper bound. */
1560
+ function impossibleRange(groupIndex, constraintIndex, lower, upper) {
1561
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_RANGE", lower, upper, "Lower bound exceeds upper bound");
1562
+ }
1563
+ /** Set operation excludes existing eq() value. */
1564
+ function setExcludesEquality(groupIndex, constraintIndex, value) {
1565
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_EXCLUDES_EQUALITY", value, ZERO, "Set operation excludes existing eq() value");
1566
+ }
1567
+ /** Multiple isIn() sets have no intersection. */
1568
+ function emptySetIntersection(groupIndex, constraintIndex) {
1569
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "EMPTY_SET_INTERSECTION", ZERO, ZERO, "Multiple isIn() sets have no intersection");
1570
+ }
1571
+ /** All values in isIn() set are excluded by neq/notIn. */
1572
+ function setFullyExcluded(groupIndex, constraintIndex) {
1573
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "SET_FULLY_EXCLUDED", ZERO, ZERO, "All values in isIn() set are excluded by neq/notIn");
1574
+ }
1575
+ /** lengthEq(v) and lengthNeq(v) on same path. */
1576
+ function lengthEqNeqContradiction(groupIndex, constraintIndex, value) {
1577
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "LENGTH_EQ_NEQ_CONTRADICTION", value, ZERO, "lengthEq(v) and lengthNeq(v) on same path");
1578
+ }
1579
+ /** Multiple lengthEq() operators with different values. */
1580
+ function conflictingLength(groupIndex, constraintIndex, existing, newValue) {
1581
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "CONFLICTING_LENGTH", existing, newValue, "Multiple lengthEq() operators with different values");
1582
+ }
1583
+ /** lengthEq() value is excluded by bound. */
1584
+ function boundsExcludeLength(groupIndex, constraintIndex, eq, bound) {
1585
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BOUNDS_EXCLUDE_LENGTH", eq, bound, "lengthEq() value is excluded by bound");
1586
+ }
1587
+ /** Lower length bound exceeds upper bound. */
1588
+ function impossibleLengthRange(groupIndex, constraintIndex, lower, upper) {
1589
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_RANGE", lower, upper, "Lower length bound exceeds upper bound");
1590
+ }
1591
+ /** Length value is outside the physical range. */
1592
+ function outOfPhysicalLengthBounds(groupIndex, constraintIndex, value) {
1593
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "OUT_OF_PHYSICAL_LENGTH_BOUNDS", value, ZERO, "Length value is outside the physical range");
1594
+ }
1595
+ /** lengthGt() on maximum length is impossible. */
1596
+ function impossibleLengthGt(groupIndex, constraintIndex, value) {
1597
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_GT", value, ZERO, "lengthGt() on maximum length is impossible");
1598
+ }
1599
+ /** lengthLt() on minimum length is impossible. */
1600
+ function impossibleLengthLt(groupIndex, constraintIndex, value) {
1601
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "IMPOSSIBLE_LENGTH_LT", value, ZERO, "lengthLt() on minimum length is impossible");
1602
+ }
1603
+ /** Bitmask operators conflict. */
1604
+ function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1605
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_CONTRADICTION", mask, conflicting, "Bitmask operators conflict");
1606
+ }
1607
+ /** bitmaskAny is impossible because all bits are forbidden. */
1608
+ function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1609
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "BITMASK_ANY_IMPOSSIBLE", mask, mustBeZero, "bitmaskAny is impossible because all bits are forbidden");
1610
+ }
1611
+ /** isIn/notIn set is not strictly sorted and deduplicated. */
1612
+ function unsortedInSet(groupIndex, constraintIndex) {
1613
+ return makeIssue("error", "contradiction", groupIndex, constraintIndex, "UNSORTED_IN_SET", ZERO, ZERO, "isIn/notIn set is not strictly sorted and deduplicated.");
1614
+ }
1615
+ /** Group contains zero constraints. */
1616
+ function emptyGroup(groupIndex) {
1617
+ return makeIssue("error", "vacuity", groupIndex, 0, "EMPTY_GROUP", ZERO, ZERO, "Group contains zero constraints.");
1618
+ }
1619
+ /** Numeric bound is dominated by a stricter bound. */
1620
+ function dominatedBound(groupIndex, constraintIndex, value) {
1621
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_BOUND", value, ZERO, "Numeric bound is dominated by a stricter bound");
1622
+ }
1623
+ /** Bound is redundant because eq() is set. */
1624
+ function redundantBound(groupIndex, constraintIndex, bound, eq) {
1625
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BOUND", bound, eq, "Bound is redundant because eq() is set");
1626
+ }
1627
+ /** notIn() value was present in isIn() set. */
1628
+ function setReduction(groupIndex, constraintIndex, value) {
1629
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUCTION", value, ZERO, "notIn() value was present in isIn() set");
1630
+ }
1631
+ /** isIn() sets partially overlap. */
1632
+ function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1633
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_REDUNDANCY", intersectionCount, ZERO, "isIn() sets partially overlap");
1634
+ }
1635
+ /** Some values in isIn() set are excluded by neq/notIn. */
1636
+ function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1637
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "SET_PARTIALLY_EXCLUDED", excludedCount, ZERO, "Some values in isIn() set are excluded by neq/notIn");
1638
+ }
1639
+ /** Length bound is dominated by a stricter bound. */
1640
+ function dominatedLengthBound(groupIndex, constraintIndex, value) {
1641
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DOMINATED_LENGTH_BOUND", value, ZERO, "Length bound is dominated by a stricter bound");
1642
+ }
1643
+ /** Length bound is redundant because lengthEq() is set. */
1644
+ function redundantLengthBound(groupIndex, constraintIndex, bound, eq) {
1645
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_LENGTH_BOUND", bound, eq, "Length bound is redundant because lengthEq() is set");
1646
+ }
1647
+ /** Bitmask operation is redundant. */
1648
+ function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1649
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "REDUNDANT_BITMASK", mask, existing, "Bitmask operation is redundant");
1650
+ }
1651
+ /** Duplicate operator in constraint. */
1652
+ function duplicateConstraint(groupIndex, constraintIndex) {
1653
+ return makeIssue("warning", "redundancy", groupIndex, constraintIndex, "DUPLICATE_CONSTRAINT", ZERO, ZERO, "Duplicate operator in constraint");
1654
+ }
1655
+ /** gte() bound equals type minimum (always true). */
1656
+ function vacuousGte(groupIndex, constraintIndex, value) {
1657
+ return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_GTE", value, ZERO, "gte() bound equals type minimum (always true)");
1658
+ }
1659
+ /** lte() bound equals type maximum (always true). */
1660
+ function vacuousLte(groupIndex, constraintIndex, value) {
1661
+ return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LTE", value, ZERO, "lte() bound equals type maximum (always true)");
1662
+ }
1663
+ /** lengthGte(0) is always true. */
1664
+ function vacuousLengthGte(groupIndex, constraintIndex, value) {
1665
+ return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_GTE", value, ZERO, "lengthGte(0) is always true");
1666
+ }
1667
+ /** lengthLte() bound equals maximum (always true). */
1668
+ function vacuousLengthLte(groupIndex, constraintIndex, value) {
1669
+ return makeIssue("info", "vacuity", groupIndex, constraintIndex, "VACUOUS_LENGTH_LTE", value, ZERO, "lengthLte() bound equals maximum (always true)");
1670
+ }
1671
+ /** Pre-select value or length issue factories for a bound domain. */
1672
+ function boundIssues(isLength) {
1673
+ return {
1674
+ eqNeqContradiction: isLength ? lengthEqNeqContradiction : eqNeqContradiction,
1675
+ conflicting: isLength ? conflictingLength : conflictingEquality,
1676
+ outOfPhysicalBounds: isLength ? outOfPhysicalLengthBounds : outOfPhysicalBounds,
1677
+ impossibleGt: isLength ? impossibleLengthGt : impossibleGt,
1678
+ impossibleLt: isLength ? impossibleLengthLt : impossibleLt,
1679
+ boundsExcludeEquality: isLength ? boundsExcludeLength : boundsExcludeEquality,
1680
+ impossibleRange: isLength ? impossibleLengthRange : impossibleRange,
1681
+ dominated: isLength ? dominatedLengthBound : dominatedBound,
1682
+ redundant: isLength ? redundantLengthBound : redundantBound,
1683
+ vacuousGte: isLength ? vacuousLengthGte : vacuousGte,
1684
+ vacuousLte: isLength ? vacuousLengthLte : vacuousLte
1685
+ };
1686
+ }
1687
+ //#endregion
1688
+ //#region src/policy-validator.ts
1689
+ /**
1690
+ * Maximum number of tracked "holes" (neq exclusions) and notIn values per
1691
+ * domain. A hole is a single value excluded via a negated equality (neq).
1692
+ * During cross-operator analysis, holes punch values out of isIn sets — if
1693
+ * every member of an isIn set is excluded by holes or notIn, the constraint
1694
+ * is unsatisfiable (SET_FULLY_EXCLUDED). When only some are excluded, the
1695
+ * validator emits SET_PARTIALLY_EXCLUDED.
1696
+ *
1697
+ * Both caps are protocol-defined limits. Beyond these limits,
1698
+ * additional exclusions are silently ignored — the validator becomes
1699
+ * best-effort for set-reduction diagnostics but never produces false
1700
+ * positives, only potential false negatives (missed redundancy warnings).
1701
+ */
1702
+ const MAX_HOLES = 8;
1703
+ const MAX_NOT_IN = 8;
1704
+ const LENGTH_MAX = 4294967295n;
1705
+ const UINT256_MAX = (1n << 256n) - 1n;
1706
+ const INT256_SIGN_BIT = 1n << 255n;
1707
+ /** Reinterpret a uint256 bigint as a signed int256. */
1708
+ function toSigned(v) {
1709
+ return v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
1710
+ }
1711
+ /** a > b. */
1712
+ function isGt(a, b, signed) {
1713
+ return signed ? toSigned(a) > toSigned(b) : a > b;
1714
+ }
1715
+ /** a >= b. */
1716
+ function isGte(a, b, signed) {
1717
+ return signed ? toSigned(a) >= toSigned(b) : a >= b;
1718
+ }
1719
+ /** a < b. */
1720
+ function isLt(a, b, signed) {
1721
+ return signed ? toSigned(a) < toSigned(b) : a < b;
1722
+ }
1723
+ /** a <= b. */
1724
+ function isLte(a, b, signed) {
1725
+ return signed ? toSigned(a) <= toSigned(b) : a <= b;
1726
+ }
1727
+ /** True for uint and int type codes. */
1728
+ function isNumericType(typeCode) {
1729
+ return typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1730
+ }
1731
+ /** True for uint and bytes32 type codes. */
1732
+ function isBitmaskCompatible(typeCode) {
1733
+ return typeCode <= TypeCode.UINT_MAX || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
1734
+ }
1735
+ /** Physical domain limits for a type code (returned as raw uint256 bigints). */
1736
+ function getDomainLimits(typeCode) {
1737
+ if (typeCode <= TypeCode.UINT_MAX) {
1738
+ const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
1739
+ return {
1740
+ min: 0n,
1741
+ max: bits === 256n ? UINT256_MAX : (1n << bits) - 1n
1742
+ };
1743
+ }
1744
+ if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
1745
+ const bits = BigInt(typeCode - TypeCode.INT_MIN + 1) * 8n;
1746
+ if (bits === 256n) return {
1747
+ min: INT256_SIGN_BIT,
1748
+ max: INT256_SIGN_BIT - 1n
1749
+ };
1750
+ const half = 1n << bits - 1n;
1751
+ const max = half - 1n;
1752
+ return {
1753
+ min: UINT256_MAX - half + 1n & UINT256_MAX,
1754
+ max
1755
+ };
1756
+ }
1757
+ if (typeCode === TypeCode.BOOL) return {
1758
+ min: 0n,
1759
+ max: 1n
1760
+ };
1761
+ if (typeCode === TypeCode.ADDRESS) return {
1762
+ min: 0n,
1763
+ max: (1n << 160n) - 1n
1764
+ };
1765
+ return {
1766
+ min: 0n,
1767
+ max: UINT256_MAX
1768
+ };
1769
+ }
1770
+ /** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
1771
+ function isValueOp(opBase) {
1772
+ return opBase <= Op.BITMASK_NONE;
1773
+ }
1774
+ /** True for GT, LT, GTE, LTE, BETWEEN. */
1775
+ function isComparisonOp(opBase) {
1776
+ return opBase >= Op.GT && opBase <= Op.BETWEEN;
1777
+ }
1778
+ /** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
1779
+ function isBitmaskOp(opBase) {
1780
+ return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
1781
+ }
1782
+ /** Return the incompatibility reason, or null if the operator is allowed. */
1783
+ function getIncompat(opBase, typeInfo) {
1784
+ const { typeCode, isDynamic, staticSize } = typeInfo;
1785
+ if (isValueOp(opBase)) {
1786
+ if (isDynamic || staticSize !== 32) return {
1787
+ code: "VALUE_OP_ON_DYNAMIC",
1788
+ message: "Value operator used on dynamic type"
1789
+ };
1790
+ if (isComparisonOp(opBase) && !isNumericType(typeCode)) return {
1791
+ code: "NUMERIC_OP_ON_NON_NUMERIC",
1792
+ message: "Comparison operator used on non-numeric type"
1793
+ };
1794
+ if (isBitmaskOp(opBase) && !isBitmaskCompatible(typeCode)) return {
1795
+ code: "BITMASK_ON_INVALID",
1796
+ message: "Bitmask operator used on incompatible type"
1797
+ };
1798
+ return null;
1799
+ }
1800
+ if (isLengthOp(opBase)) {
1801
+ if (!isLengthValidType(typeCode)) return {
1802
+ code: "LENGTH_ON_STATIC",
1803
+ message: "Length operator used on non-dynamic type"
1804
+ };
1805
+ return null;
1806
+ }
1807
+ return {
1808
+ code: "UNKNOWN_OPERATOR",
1809
+ message: "Unknown operator code"
1810
+ };
1811
+ }
1812
+ /** Check operator–type compatibility and return an issue descriptor on mismatch. */
1813
+ function checkCompatibility(opBase, typeCode, isDynamic, staticSize) {
1814
+ const incompat = getIncompat(opBase, {
1815
+ typeCode,
1816
+ isDynamic,
1817
+ staticSize
1818
+ });
1819
+ return incompat ? {
1820
+ compatible: false,
1821
+ ...incompat
1822
+ } : {
1823
+ compatible: true,
1824
+ code: "",
1825
+ message: ""
1826
+ };
1827
+ }
1828
+ /**
1829
+ * True if operator `opCode` is allowed on a target with the given type info.
1830
+ * Tolerates the NOT flag on `opCode`.
1831
+ */
1832
+ function isOpAllowed(opCode, typeInfo) {
1833
+ return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
1834
+ }
1835
+ /** Read a single uint256 from operator hex (bytes after the opcode byte). */
1836
+ function readValue(opHex) {
1837
+ return BigInt(`0x${opHex.slice(4)}`);
1838
+ }
1839
+ /** Read a pair of uint256 values from a BETWEEN operator. */
1840
+ function readPair(opHex) {
1841
+ const body = opHex.slice(4);
1842
+ return {
1843
+ low: BigInt(`0x${body.slice(0, 64)}`),
1844
+ high: BigInt(`0x${body.slice(64, 128)}`)
1845
+ };
1846
+ }
1847
+ /** Unpack an IN operator's payload into an array of bigint values. */
1848
+ function unpackSet(opHex) {
1849
+ const body = opHex.slice(4);
1850
+ const count = body.length / 64;
1851
+ const values = [];
1852
+ for (let i = 0; i < count; i++) values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
1853
+ return values;
1854
+ }
1855
+ /** True if every element is strictly greater than the previous. */
1856
+ function isStrictlyAscending(values) {
1857
+ for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return false;
1858
+ return true;
1859
+ }
1860
+ /** Convert LENGTH_* opcode to its core comparison equivalent. */
1861
+ function normalizeLengthOp(base) {
1862
+ if (base === Op.LENGTH_EQ) return Op.EQ;
1863
+ if (base === Op.LENGTH_GT) return Op.GT;
1864
+ if (base === Op.LENGTH_LT) return Op.LT;
1865
+ if (base === Op.LENGTH_GTE) return Op.GTE;
1866
+ if (base === Op.LENGTH_LTE) return Op.LTE;
1867
+ return base;
1868
+ }
1869
+ /** Convert a negated bound opcode to its positive equivalent. */
1870
+ function negateBoundOp(base) {
1871
+ if (base === Op.GT) return Op.LTE;
1872
+ if (base === Op.GTE) return Op.LT;
1873
+ if (base === Op.LT) return Op.GTE;
1874
+ if (base === Op.LTE) return Op.GT;
1875
+ return base;
1876
+ }
1877
+ /** Create a fresh constraint context with domain limits from the type. */
1878
+ function initContext(scope, path, typeInfo) {
1879
+ const { min, max } = getDomainLimits(typeInfo.typeCode);
1880
+ return {
1881
+ scope,
1882
+ path,
1883
+ typeInfo,
1884
+ numeric: {
1885
+ isSigned: isSigned(typeInfo.typeCode),
1886
+ min,
1887
+ max,
1888
+ hasEq: false,
1889
+ eq: 0n,
1890
+ hasLower: false,
1891
+ lower: 0n,
1892
+ lowerInclusive: false,
1893
+ hasUpper: false,
1894
+ upper: 0n,
1895
+ upperInclusive: false,
1896
+ holes: []
1897
+ },
1898
+ bitmask: {
1899
+ mustBeOne: 0n,
1900
+ mustBeZero: 0n
1901
+ },
1902
+ length: {
1903
+ isSigned: false,
1904
+ min: 0n,
1905
+ max: LENGTH_MAX,
1906
+ hasEq: false,
1907
+ eq: 0n,
1908
+ hasLower: false,
1909
+ lower: 0n,
1910
+ lowerInclusive: false,
1911
+ hasUpper: false,
1912
+ upper: 0n,
1913
+ upperInclusive: false,
1914
+ holes: []
1915
+ },
1916
+ set: {
1917
+ hasIn: false,
1918
+ inValues: [],
1919
+ notInValues: []
1920
+ }
1921
+ };
1922
+ }
1923
+ /** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
1924
+ function updateBound(domain, base, isNegated, value, emit, groupIndex, constraintIndex, issues) {
1925
+ let changedEq = false;
1926
+ let changedLower = false;
1927
+ let changedUpper = false;
1928
+ if (isNegated) {
1929
+ if (base === Op.EQ) {
1930
+ if (domain.hasEq && domain.eq === value) issues.push(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
1931
+ let alreadyHole = false;
1932
+ for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) {
1933
+ alreadyHole = true;
1934
+ break;
1935
+ }
1936
+ if (!alreadyHole && domain.holes.length < MAX_HOLES) domain.holes.push(value);
1937
+ } else updateBound(domain, negateBoundOp(base), false, value, emit, groupIndex, constraintIndex, issues);
1938
+ return;
1939
+ }
1940
+ if (base === Op.GTE && value === domain.min) issues.push(emit.vacuousGte(groupIndex, constraintIndex, bigintToHex(value)));
1941
+ else if (base === Op.LTE && value === domain.max) issues.push(emit.vacuousLte(groupIndex, constraintIndex, bigintToHex(value)));
1942
+ if (isLt(value, domain.min, domain.isSigned) || isGt(value, domain.max, domain.isSigned)) issues.push(emit.outOfPhysicalBounds(groupIndex, constraintIndex, bigintToHex(value)));
1943
+ else if (base === Op.GT && value === domain.max) issues.push(emit.impossibleGt(groupIndex, constraintIndex, bigintToHex(value)));
1944
+ else if (base === Op.LT && value === domain.min) issues.push(emit.impossibleLt(groupIndex, constraintIndex, bigintToHex(value)));
1945
+ if (base === Op.EQ) {
1946
+ if (!domain.hasEq || domain.eq !== value) changedEq = true;
1947
+ if (domain.hasEq) {
1948
+ if (domain.eq !== value) issues.push(emit.conflicting(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(value)));
1949
+ }
1950
+ for (let j = 0; j < domain.holes.length; j++) if (domain.holes[j] === value) issues.push(emit.eqNeqContradiction(groupIndex, constraintIndex, bigintToHex(value)));
1951
+ domain.hasEq = true;
1952
+ domain.eq = value;
1953
+ } else if (base === Op.GT || base === Op.GTE) {
1954
+ const inclusive = base === Op.GTE;
1955
+ if (domain.hasLower) {
1956
+ let redundant = false;
1957
+ let strictlyBetter = false;
1958
+ if (isLt(value, domain.lower, domain.isSigned)) redundant = true;
1959
+ else if (value === domain.lower) {
1960
+ if (domain.lowerInclusive) if (!inclusive) strictlyBetter = true;
1961
+ else redundant = true;
1962
+ else if (!inclusive) redundant = true;
1963
+ } else strictlyBetter = true;
1964
+ if (redundant) issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
1965
+ if (strictlyBetter) {
1966
+ issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.lower)));
1967
+ domain.lower = value;
1968
+ domain.lowerInclusive = inclusive;
1969
+ changedLower = true;
1970
+ }
1971
+ } else {
1972
+ domain.hasLower = true;
1973
+ domain.lower = value;
1974
+ domain.lowerInclusive = inclusive;
1975
+ changedLower = true;
1976
+ }
1977
+ } else if (base === Op.LT || base === Op.LTE) {
1978
+ const inclusive = base === Op.LTE;
1979
+ if (domain.hasUpper) {
1980
+ let redundant = false;
1981
+ let strictlyBetter = false;
1982
+ if (isGt(value, domain.upper, domain.isSigned)) redundant = true;
1983
+ else if (value === domain.upper) {
1984
+ if (domain.upperInclusive) if (!inclusive) strictlyBetter = true;
1985
+ else redundant = true;
1986
+ else if (!inclusive) redundant = true;
1987
+ } else strictlyBetter = true;
1988
+ if (redundant) issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(value)));
1989
+ if (strictlyBetter) {
1990
+ issues.push(emit.dominated(groupIndex, constraintIndex, bigintToHex(domain.upper)));
1991
+ domain.upper = value;
1992
+ domain.upperInclusive = inclusive;
1993
+ changedUpper = true;
1994
+ }
1995
+ } else {
1996
+ domain.hasUpper = true;
1997
+ domain.upper = value;
1998
+ domain.upperInclusive = inclusive;
1999
+ changedUpper = true;
2000
+ }
2001
+ }
2002
+ if (changedEq || changedLower) {
2003
+ if (domain.hasEq && domain.hasLower) if (domain.lowerInclusive ? isLt(domain.eq, domain.lower, domain.isSigned) : isLte(domain.eq, domain.lower, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.lower)));
2004
+ else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.eq)));
2005
+ }
2006
+ if (changedEq || changedUpper) {
2007
+ if (domain.hasEq && domain.hasUpper) if (domain.upperInclusive ? isGt(domain.eq, domain.upper, domain.isSigned) : isGte(domain.eq, domain.upper, domain.isSigned)) issues.push(emit.boundsExcludeEquality(groupIndex, constraintIndex, bigintToHex(domain.eq), bigintToHex(domain.upper)));
2008
+ else issues.push(emit.redundant(groupIndex, constraintIndex, bigintToHex(domain.upper), bigintToHex(domain.eq)));
2009
+ }
2010
+ if (changedLower || changedUpper) {
2011
+ if (domain.hasLower && domain.hasUpper) {
2012
+ if (isGt(domain.lower, domain.upper, domain.isSigned) || domain.lower === domain.upper && (!domain.lowerInclusive || !domain.upperInclusive)) issues.push(emit.impossibleRange(groupIndex, constraintIndex, bigintToHex(domain.lower), bigintToHex(domain.upper)));
2013
+ }
2014
+ }
2015
+ }
2016
+ /** Update the bitmask domain and emit issues on contradiction or redundancy. */
2017
+ function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues) {
2018
+ if (isNegated) return;
2019
+ if (base === Op.BITMASK_ALL) {
2020
+ if ((ctx.bitmask.mustBeZero & mask) !== 0n) issues.push(bitmaskContradiction(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeZero)));
2021
+ if ((ctx.bitmask.mustBeOne & mask) === mask) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
2022
+ ctx.bitmask.mustBeOne |= mask;
2023
+ } else if (base === Op.BITMASK_NONE) {
2024
+ if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(bitmaskContradiction(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
2025
+ if ((ctx.bitmask.mustBeZero & mask) === mask) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeZero)));
2026
+ ctx.bitmask.mustBeZero |= mask;
2027
+ } else if (base === Op.BITMASK_ANY) {
2028
+ if (mask !== 0n && (ctx.bitmask.mustBeZero & mask) === mask) issues.push(bitmaskAnyImpossible(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeZero)));
2029
+ if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
2030
+ }
2031
+ }
2032
+ /** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
2033
+ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
2034
+ if (!ctx.set.hasIn) return;
2035
+ let possibleCount = 0;
2036
+ const inCount = ctx.set.inValues.length;
2037
+ for (let i = 0; i < inCount; i++) {
2038
+ const value = ctx.set.inValues[i];
2039
+ let forbidden = false;
2040
+ for (let k = 0; k < ctx.numeric.holes.length; k++) if (ctx.numeric.holes[k] === value) {
2041
+ forbidden = true;
2042
+ break;
2043
+ }
2044
+ if (!forbidden) {
2045
+ for (let k = 0; k < ctx.set.notInValues.length; k++) if (ctx.set.notInValues[k] === value) {
2046
+ forbidden = true;
2047
+ break;
2048
+ }
2049
+ }
2050
+ if (!forbidden) possibleCount++;
2051
+ }
2052
+ if (possibleCount === 0) issues.push(setFullyExcluded(groupIndex, constraintIndex));
2053
+ else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
2054
+ }
2055
+ /** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
2056
+ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
2057
+ if (isNegated) {
2058
+ for (const value of values) {
2059
+ if (ctx.numeric.hasEq && ctx.numeric.eq === value) issues.push(setExcludesEquality(groupIndex, constraintIndex, bigintToHex(value)));
2060
+ if (ctx.set.hasIn) {
2061
+ let inSet = false;
2062
+ for (const existing of ctx.set.inValues) if (existing === value) {
2063
+ inSet = true;
2064
+ break;
2065
+ }
2066
+ if (inSet) issues.push(setReduction(groupIndex, constraintIndex, bigintToHex(value)));
2067
+ }
2068
+ if (ctx.set.notInValues.length < MAX_NOT_IN) ctx.set.notInValues.push(value);
2069
+ }
2070
+ checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2071
+ } else {
2072
+ if (ctx.numeric.hasEq) {
2073
+ let found = false;
2074
+ for (const candidate of values) if (candidate === ctx.numeric.eq) {
2075
+ found = true;
2076
+ break;
2077
+ }
2078
+ if (!found) issues.push(setExcludesEquality(groupIndex, constraintIndex, bigintToHex(ctx.numeric.eq)));
2079
+ }
2080
+ if (ctx.set.hasIn) {
2081
+ const intersection = [];
2082
+ for (const existing of ctx.set.inValues) for (const candidate of values) if (existing === candidate) {
2083
+ intersection.push(existing);
2084
+ break;
2085
+ }
2086
+ if (intersection.length === 0) issues.push(emptySetIntersection(groupIndex, constraintIndex));
2087
+ else if (intersection.length < values.length || intersection.length < ctx.set.inValues.length) issues.push(setRedundancy(groupIndex, constraintIndex, bigintToHex(BigInt(intersection.length))));
2088
+ ctx.set.inValues = intersection;
2089
+ } else {
2090
+ ctx.set.hasIn = true;
2091
+ ctx.set.inValues = values;
2092
+ }
2093
+ checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2094
+ }
2095
+ }
2096
+ /** Emit a warning if the constraint contains any identical operators. */
2097
+ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2098
+ const seen = /* @__PURE__ */ new Set();
2099
+ for (const op of operators) {
2100
+ const key = op.toLowerCase();
2101
+ if (seen.has(key)) {
2102
+ issues.push(duplicateConstraint(groupIndex, constraintIndex));
2103
+ return;
2104
+ }
2105
+ seen.add(key);
2106
+ }
2107
+ }
2108
+ /** Validate all operators on a single constraint against its domain context. */
2109
+ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
2110
+ const operators = constraint.operators;
2111
+ for (const opHex of operators) {
2112
+ const opCode = parseInt(opHex.slice(2, 4), 16);
2113
+ const base = opCode & ~Op.NOT;
2114
+ const isNegated = (opCode & Op.NOT) !== 0;
2115
+ const compat = checkCompatibility(base, ctx.typeInfo.typeCode, ctx.typeInfo.isDynamic, ctx.typeInfo.staticSize);
2116
+ if (!compat.compatible) {
2117
+ issues.push(fromOpRule(compat.code, compat.message, groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2118
+ continue;
2119
+ }
2120
+ if (isValueOp(base)) {
2121
+ if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
2122
+ const { low, high } = readPair(opHex);
2123
+ updateBound(ctx.numeric, Op.GTE, isNegated, low, boundIssues(false), groupIndex, constraintIndex, issues);
2124
+ updateBound(ctx.numeric, Op.LTE, isNegated, high, boundIssues(false), groupIndex, constraintIndex, issues);
2125
+ } else {
2126
+ const value = readValue(opHex);
2127
+ const holesBefore = ctx.numeric.holes.length;
2128
+ updateBound(ctx.numeric, base, isNegated, value, boundIssues(false), groupIndex, constraintIndex, issues);
2129
+ if (ctx.numeric.holes.length > holesBefore) checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2130
+ }
2131
+ else if (isBitmaskOp(base)) updateBitmask(ctx, base, isNegated, readValue(opHex), groupIndex, constraintIndex, issues);
2132
+ else if (base === Op.IN) {
2133
+ const values = unpackSet(opHex);
2134
+ if (!isStrictlyAscending(values)) issues.push(unsortedInSet(groupIndex, constraintIndex));
2135
+ else updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
2136
+ }
2137
+ } else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
2138
+ const { low, high } = readPair(opHex);
2139
+ updateBound(ctx.length, Op.GTE, isNegated, low, boundIssues(true), groupIndex, constraintIndex, issues);
2140
+ updateBound(ctx.length, Op.LTE, isNegated, high, boundIssues(true), groupIndex, constraintIndex, issues);
2141
+ } else {
2142
+ const value = readValue(opHex);
2143
+ updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, boundIssues(true), groupIndex, constraintIndex, issues);
2144
+ }
2145
+ }
2146
+ checkDuplicates(issues, operators, groupIndex, constraintIndex);
2147
+ }
2148
+ /** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
2149
+ function validateGroup(data, descBytes, groupIndex, issues) {
2150
+ const constraints = data.groups[groupIndex];
2151
+ const contexts = [];
2152
+ for (let constraintIndex = 0; constraintIndex < constraints.length; constraintIndex++) {
2153
+ const constraint = constraints[constraintIndex];
2154
+ const normalizedPath = `0x${constraint.path.slice(2).toLowerCase()}`;
2155
+ let ctx;
2156
+ for (const existing of contexts) if (existing.scope === constraint.scope && existing.path === normalizedPath) {
2157
+ ctx = existing;
2158
+ break;
2159
+ }
2160
+ if (!ctx) {
2161
+ let typeInfo;
2162
+ if (constraint.scope === Scope.CALLDATA) {
2163
+ const steps = parsePathSteps(constraint.path);
2164
+ typeInfo = Descriptor.typeAt(descBytes, steps);
2165
+ } else {
2166
+ const ctxId = parsePathSteps(constraint.path)[0];
2167
+ typeInfo = {
2168
+ typeCode: lookupContextProperty(ctxId).typeCode,
2169
+ isDynamic: false,
2170
+ staticSize: 32
2171
+ };
2172
+ }
2173
+ ctx = initContext(constraint.scope, normalizedPath, typeInfo);
2174
+ contexts.push(ctx);
2175
+ }
2176
+ validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
2177
+ }
2178
+ }
2179
+ /**
2180
+ * Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
2181
+ * @param data - The canonical policy data to validate.
2182
+ * @returns All validation issues found, ordered by group and constraint index.
2183
+ */
2184
+ function validate(data) {
2185
+ const issues = [];
2186
+ const descBytes = hexToBytes(data.descriptor);
2187
+ for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
2188
+ if (data.groups[groupIndex].length === 0) {
2189
+ issues.push(emptyGroup(groupIndex));
2190
+ continue;
2191
+ }
2192
+ validateGroup(data, descBytes, groupIndex, issues);
2193
+ }
2194
+ return issues;
2195
+ }
2196
+ /** Semantic validation for policies. */
2197
+ const PolicyValidator = { validate };
2198
+ //#endregion
2199
+ //#region src/signature.ts
2200
+ /** Return true if the char code is an ASCII letter. */
2201
+ function isAlpha(c) {
2202
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122;
2203
+ }
2204
+ /** Return true if the char code is an ASCII alphanumeric character. */
2205
+ function isAlphanum(c) {
2206
+ return isAlpha(c) || c >= 48 && c <= 57;
2207
+ }
2208
+ /**
2209
+ * Parse an ABI function signature into its selector and types.
2210
+ *
2211
+ * Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
2212
+ * structure must be `name(types)` with the final `)` as the last character.
2213
+ *
2214
+ * @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
2215
+ * @returns The 4-byte selector and the comma-separated types string.
2216
+ * @throws {CallciumError} with code `"INVALID_SIGNATURE"` if the signature is malformed.
2217
+ */
2218
+ function parse(signature) {
2219
+ if (signature.length < 3) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
2220
+ let openParen = -1;
2221
+ for (let i = 0; i < signature.length; i++) {
2222
+ const c = signature.charCodeAt(i);
2223
+ if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("INVALID_SIGNATURE", "Signature must not contain whitespace");
2224
+ if (c > 126) throw new CallciumError("INVALID_SIGNATURE", "Signature must contain only ASCII characters");
2225
+ if (openParen === -1 && c === 40) openParen = i;
2226
+ }
2227
+ if (openParen < 1) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
2228
+ if (!signature.endsWith(")")) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
2229
+ const firstChar = signature.charCodeAt(0);
2230
+ if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_SIGNATURE", "Function name must start with a letter or underscore");
2231
+ for (let i = 1; i < openParen; i++) {
2232
+ const c = signature.charCodeAt(i);
2233
+ if (!isAlphanum(c) && c !== 95) throw new CallciumError("INVALID_SIGNATURE", "Function name must contain only alphanumeric characters or underscores");
2234
+ }
2235
+ const types = signature.slice(openParen + 1, signature.length - 1);
2236
+ const encoded = new Uint8Array(signature.length);
2237
+ for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
2238
+ return {
2239
+ selector: bytesToHex(keccak_256(encoded).subarray(0, 4)),
2240
+ types
2241
+ };
2242
+ }
2243
+ /** ABI function signature parser. */
2244
+ const SignatureParser = { parse };
2245
+ //#endregion
2246
+ //#region src/policy-builder.ts
2247
+ /** Validate a context-scope path. */
2248
+ function validateContextPath(pathBytes) {
2249
+ if (pathBytes.length !== 2) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step (2 bytes).");
2250
+ const step = readU16(pathBytes, 0);
2251
+ if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
2252
+ }
2253
+ /** Validate a calldata-scope path against the descriptor. */
2254
+ function validateCalldataPath(path, desc) {
2255
+ const steps = parsePathSteps(path);
2256
+ if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Calldata path must have at least one step.");
2257
+ const argIndex = steps[0];
2258
+ const paramCount = Descriptor.paramCount(desc);
2259
+ if (argIndex >= paramCount) throw new CallciumError("INVALID_PATH", `Argument index ${argIndex} out of range (descriptor has ${paramCount} params).`);
2260
+ let offset = Descriptor.paramOffset(desc, argIndex);
2261
+ let hasQuantifier = false;
2262
+ for (let i = 1; i < steps.length; i++) {
2263
+ const step = steps[i];
2264
+ const info = Descriptor.inspect(desc, offset);
2265
+ if (info.typeCode === TypeCode.TUPLE) {
2266
+ if (isQuantifier(step)) throw new CallciumError("INVALID_PATH", "Quantifier step is not valid on a tuple node.");
2267
+ const fieldCount = Descriptor.tupleFieldCount(desc, offset);
2268
+ if (step >= fieldCount) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`);
2269
+ offset = Descriptor.tupleFieldOffset(desc, offset, step);
2270
+ } else if (info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY) {
2271
+ if (isQuantifier(step)) {
2272
+ if (hasQuantifier) throw new CallciumError("INVALID_PATH", "Nested quantifiers are not allowed.");
2273
+ hasQuantifier = true;
2274
+ } else if (info.typeCode === TypeCode.STATIC_ARRAY) {
2275
+ const arrayLength = Descriptor.staticArrayLength(desc, offset);
2276
+ if (step >= arrayLength) throw new CallciumError("INVALID_PATH", `Array index ${step} out of range (static array has ${arrayLength} elements).`);
2277
+ }
2278
+ offset = Descriptor.arrayElementOffset(offset);
2279
+ } else throw new CallciumError("INVALID_PATH", "Cannot descend into an elementary type.");
2280
+ }
2281
+ }
2282
+ /** Fluent builder for constructing Callcium policies. */
2283
+ var PolicyBuilder = class PolicyBuilder {
2284
+ draft;
2285
+ constructor(draft) {
2286
+ this.draft = draft;
2287
+ }
2288
+ /**
2289
+ * Create a builder from a function signature.
2290
+ * @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
2291
+ */
2292
+ static create(signature) {
2293
+ const parsed = SignatureParser.parse(signature);
2294
+ const descriptor = DescriptorCoder.fromTypes(parsed.types);
2295
+ return new PolicyBuilder({
2296
+ isSelectorless: false,
2297
+ selector: parsed.selector,
2298
+ descriptor,
2299
+ groups: [[]],
2300
+ pathHashes: [/* @__PURE__ */ new Set()]
2301
+ });
2302
+ }
2303
+ /**
2304
+ * Create a selectorless builder from a raw type string.
2305
+ * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
2306
+ */
2307
+ static createRaw(typesCsv) {
2308
+ return new PolicyBuilder({
2309
+ isSelectorless: true,
2310
+ selector: "0x00000000",
2311
+ descriptor: DescriptorCoder.fromTypes(typesCsv),
2312
+ groups: [[]],
2313
+ pathHashes: [/* @__PURE__ */ new Set()]
2314
+ });
2315
+ }
2316
+ /**
2317
+ * Add a constraint to the current group.
2318
+ * @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
2319
+ */
2320
+ add(constraint) {
2321
+ const c = {
2322
+ scope: constraint.scope,
2323
+ path: constraint.path,
2324
+ operators: [...constraint.operators]
2325
+ };
2326
+ if (c.operators.length === 0) throw new CallciumError("INVALID_CONSTRAINT", "Constraint must have at least one operator.");
2327
+ if (c.scope === Scope.CONTEXT) validateContextPath(hexToBytes(c.path));
2328
+ else if (c.scope === Scope.CALLDATA) validateCalldataPath(c.path, this.draft.descriptor);
2329
+ else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
2330
+ const key = `${c.scope}:${c.path.toLowerCase()}`;
2331
+ const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1];
2332
+ if (currentHashes.has(key)) throw new CallciumError("DUPLICATE_PATH", `Duplicate path ${c.path} in the same group.`);
2333
+ currentHashes.add(key);
2334
+ this.draft.groups[this.draft.groups.length - 1].push(c);
2335
+ return this;
2336
+ }
2337
+ /** Start a new constraint group (OR branch). */
2338
+ or() {
2339
+ if (this.draft.groups[this.draft.groups.length - 1].length === 0) throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty.");
2340
+ this.draft.groups.push([]);
2341
+ this.draft.pathHashes.push(/* @__PURE__ */ new Set());
2342
+ return this;
2343
+ }
2344
+ /**
2345
+ * Build the policy into an encoded binary blob.
2346
+ * @returns The policy as a 0x-prefixed hex string.
2347
+ * @throws {CallciumError} If any group is empty or validation finds errors.
2348
+ */
2349
+ build() {
2350
+ this.checkGroups();
2351
+ const policyData = this.toPolicyData();
2352
+ const firstError = PolicyValidator.validate(policyData).find((issue) => issue.severity === "error");
2353
+ if (firstError) throw new CallciumError("VALIDATION_ERROR", firstError.message);
2354
+ return PolicyCoder.encode(policyData);
2355
+ }
2356
+ /**
2357
+ * Validate the policy without encoding.
2358
+ * @returns All validation issues found.
2359
+ */
2360
+ validate() {
2361
+ this.checkGroups();
2362
+ const policyData = this.toPolicyData();
2363
+ return PolicyValidator.validate(policyData);
2364
+ }
2365
+ /** Throw if any group is empty. */
2366
+ checkGroups() {
2367
+ for (let i = 0; i < this.draft.groups.length; i++) if (this.draft.groups[i].length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty.`);
2368
+ }
2369
+ /** Convert the draft to a PolicyData structure. */
2370
+ toPolicyData() {
2371
+ return {
2372
+ isSelectorless: this.draft.isSelectorless,
2373
+ selector: this.draft.selector,
2374
+ descriptor: bytesToHex(this.draft.descriptor),
2375
+ groups: this.draft.groups
2376
+ };
2377
+ }
2378
+ };
2379
+ //#endregion
2380
+ //#region src/constraint.ts
2381
+ /** Convert a scalar value to a 32-byte big-endian word. */
2382
+ function encodeWord(value) {
2383
+ const word = new Uint8Array(32);
2384
+ if (typeof value === "boolean") {
2385
+ word[31] = value ? 1 : 0;
2386
+ return word;
2387
+ }
2388
+ if (typeof value === "string") {
2389
+ const body = value.startsWith("0x") ? value.slice(2) : value;
2390
+ if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
2391
+ for (let i = 0; i < 20; i++) {
2392
+ const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2393
+ if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
2394
+ word[12 + i] = byte;
2395
+ }
2396
+ return word;
2397
+ }
2398
+ let bigValue = typeof value === "number" ? BigInt(value) : value;
2399
+ for (let i = 31; i >= 0; i--) {
2400
+ word[i] = Number(bigValue & 255n);
2401
+ bigValue >>= 8n;
2402
+ }
2403
+ return word;
2404
+ }
2405
+ /** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
2406
+ function singleOp(opCode, value) {
2407
+ const buffer = new Uint8Array(33);
2408
+ buffer[0] = opCode;
2409
+ buffer.set(encodeWord(value), 1);
2410
+ return bytesToHex(buffer);
2411
+ }
2412
+ /** Pack a range operator (opCode byte + min word + max word). */
2413
+ function rangeOp(opCode, min, max) {
2414
+ if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
2415
+ const buffer = new Uint8Array(65);
2416
+ buffer[0] = opCode;
2417
+ buffer.set(encodeWord(min), 1);
2418
+ buffer.set(encodeWord(max), 33);
2419
+ return bytesToHex(buffer);
2420
+ }
2421
+ /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
2422
+ function setOp(opCode, values) {
2423
+ const bigs = values.map((v) => {
2424
+ if (typeof v === "bigint") return v;
2425
+ if (typeof v === "number") return BigInt(v);
2426
+ if (typeof v === "boolean") return v ? 1n : 0n;
2427
+ const body = v.startsWith("0x") ? v.slice(2) : v;
2428
+ return BigInt("0x" + (body || "0"));
2429
+ });
2430
+ bigs.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2431
+ const deduped = [];
2432
+ for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
2433
+ if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
2434
+ const buffer = new Uint8Array(1 + deduped.length * 32);
2435
+ buffer[0] = opCode;
2436
+ for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
2437
+ return bytesToHex(buffer);
2438
+ }
2439
+ /** Encode a sequence of uint16 path steps as a big-endian hex string. */
2440
+ function encodePath(steps) {
2441
+ const buffer = new Uint8Array(steps.length * 2);
2442
+ for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
2443
+ return bytesToHex(buffer);
2444
+ }
2445
+ /**
2446
+ * Mutable builder that accumulates operators targeting a single path.
2447
+ * Implements the `Constraint` interface so it can be passed directly to policy builders.
2448
+ */
2449
+ var ConstraintBuilder = class {
2450
+ scope;
2451
+ path;
2452
+ operators;
2453
+ /** @internal */
2454
+ constructor(scope, path) {
2455
+ this.scope = scope;
2456
+ this.path = path;
2457
+ this.operators = [];
2458
+ }
2459
+ /** Push a pre-encoded operator hex string and return this for chaining. */
2460
+ push(opHex) {
2461
+ this.operators.push(opHex);
2462
+ return this;
2463
+ }
2464
+ /** Assert the value equals `value`. */
2465
+ eq(value) {
2466
+ return this.push(singleOp(Op.EQ, value));
2467
+ }
2468
+ /** Assert the value does not equal `value`. */
2469
+ neq(value) {
2470
+ return this.push(singleOp(Op.EQ | Op.NOT, value));
2471
+ }
2472
+ /** Assert the value is greater than `bound`. */
2473
+ gt(bound) {
2474
+ return this.push(singleOp(Op.GT, bound));
2475
+ }
2476
+ /** Assert the value is less than `bound`. */
2477
+ lt(bound) {
2478
+ return this.push(singleOp(Op.LT, bound));
2479
+ }
2480
+ /** Assert the value is greater than or equal to `bound`. */
2481
+ gte(bound) {
2482
+ return this.push(singleOp(Op.GTE, bound));
2483
+ }
2484
+ /** Assert the value is less than or equal to `bound`. */
2485
+ lte(bound) {
2486
+ return this.push(singleOp(Op.LTE, bound));
2487
+ }
2488
+ /**
2489
+ * Assert the value is within [min, max] inclusive.
2490
+ * @throws {CallciumError} If min > max.
2491
+ */
2492
+ between(min, max) {
2493
+ return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
2494
+ }
2495
+ /**
2496
+ * Assert the value is a member of the set.
2497
+ * Values are sorted and deduplicated before encoding.
2498
+ * @throws {CallciumError} If the set is empty after deduplication.
2499
+ */
2500
+ isIn(values) {
2501
+ return this.push(setOp(Op.IN, values));
2502
+ }
2503
+ /**
2504
+ * Assert the value is not a member of the set.
2505
+ * @throws {CallciumError} If the set is empty after deduplication.
2506
+ */
2507
+ notIn(values) {
2508
+ return this.push(setOp(Op.IN | Op.NOT, values));
2509
+ }
2510
+ /** Assert all bits in `mask` are set. */
2511
+ bitmaskAll(mask) {
2512
+ return this.push(singleOp(Op.BITMASK_ALL, mask));
2513
+ }
2514
+ /** Assert at least one bit in `mask` is set. */
2515
+ bitmaskAny(mask) {
2516
+ return this.push(singleOp(Op.BITMASK_ANY, mask));
2517
+ }
2518
+ /** Assert no bit in `mask` is set. */
2519
+ bitmaskNone(mask) {
2520
+ return this.push(singleOp(Op.BITMASK_NONE, mask));
2521
+ }
2522
+ /** Assert the runtime length equals `n`. */
2523
+ lengthEq(n) {
2524
+ return this.push(singleOp(Op.LENGTH_EQ, n));
2525
+ }
2526
+ /** Assert the runtime length is greater than `n`. */
2527
+ lengthGt(n) {
2528
+ return this.push(singleOp(Op.LENGTH_GT, n));
2529
+ }
2530
+ /** Assert the runtime length is less than `n`. */
2531
+ lengthLt(n) {
2532
+ return this.push(singleOp(Op.LENGTH_LT, n));
2533
+ }
2534
+ /** Assert the runtime length is greater than or equal to `n`. */
2535
+ lengthGte(n) {
2536
+ return this.push(singleOp(Op.LENGTH_GTE, n));
2537
+ }
2538
+ /** Assert the runtime length is less than or equal to `n`. */
2539
+ lengthLte(n) {
2540
+ return this.push(singleOp(Op.LENGTH_LTE, n));
2541
+ }
2542
+ /**
2543
+ * Assert the runtime length is within [min, max] inclusive.
2544
+ * @throws {CallciumError} If min > max.
2545
+ */
2546
+ lengthBetween(min, max) {
2547
+ return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
2548
+ }
2549
+ };
2550
+ function arg(...steps) {
2551
+ return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
2552
+ }
2553
+ /** Target the `msg.sender` context property. */
2554
+ function msgSender() {
2555
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
2556
+ }
2557
+ /** Target the `msg.value` context property. */
2558
+ function msgValue() {
2559
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
2560
+ }
2561
+ /** Target the `block.timestamp` context property. */
2562
+ function blockTimestamp() {
2563
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
2564
+ }
2565
+ /** Target the `block.number` context property. */
2566
+ function blockNumber() {
2567
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
2568
+ }
2569
+ /** Target the `block.chainid` context property. */
2570
+ function chainId() {
2571
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
2572
+ }
2573
+ /** Target the `tx.origin` context property. */
2574
+ function txOrigin() {
2575
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
2576
+ }
2577
+ //#endregion
2578
+ //#region src/reader.ts
2579
+ /** Read 32 bytes at offset, bounds-checked. */
2580
+ function load32(callData, offset) {
2581
+ if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
2582
+ return callData.subarray(offset, offset + 32);
2583
+ }
2584
+ /**
2585
+ * Read ABI pointer (offset or length) from a 32-byte slot.
2586
+ * Validates high 28 bytes are zero (rejects values > 32-bit).
2587
+ */
2588
+ function readPointer(callData, head) {
2589
+ const word = load32(callData, head);
2590
+ if (word instanceof Uint8Array) {
2591
+ for (let i = 0; i < 28; i++) if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
2592
+ return (word[28] << 24 | word[29] << 16 | word[30] << 8 | word[31]) >>> 0;
2593
+ }
2594
+ return word;
2595
+ }
2596
+ /** Return the number of bytes this node occupies in the head region. */
2597
+ function headContribution(node) {
2598
+ return node.isDynamic ? 32 : node.staticSize;
2599
+ }
2600
+ /** Read a single BE16 path step at the given step index. */
2601
+ function readStep(pathBytes, stepIndex) {
2602
+ return readU16(pathBytes, stepIndex * 2);
2603
+ }
2604
+ /** Navigate one path step through a node (tuple field, array element, or quantifier). */
2605
+ function descend(node, callData, head, base, childIndex) {
2606
+ if (node.type === "tuple") return descendTuple(node, callData, head, base, childIndex);
2607
+ if (node.type === "dynamicArray") return descendDynamicArray(node, callData, head, base, childIndex);
2608
+ if (node.type === "staticArray") return descendStaticArray(node, callData, head, base, childIndex);
2609
+ throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type (${node.type}).`);
2610
+ }
2611
+ /** Descend into a tuple field by index. */
2612
+ function descendTuple(node, callData, head, base, childIndex) {
2613
+ if (childIndex >= node.fields.length) throw new CallciumError("INVALID_PATH", `Tuple field index ${childIndex} out of range (${node.fields.length} fields).`);
2614
+ let tupleBase;
2615
+ let cursor;
2616
+ if (node.isDynamic) {
2617
+ const ptrResult = readPointer(callData, head);
2618
+ if (typeof ptrResult !== "number") return {
2619
+ type: "violation",
2620
+ code: ptrResult.code
2621
+ };
2622
+ tupleBase = base + ptrResult;
2623
+ cursor = tupleBase;
2624
+ } else {
2625
+ tupleBase = base;
2626
+ cursor = head;
2627
+ }
2628
+ for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
2629
+ return {
2630
+ type: "ok",
2631
+ head: cursor,
2632
+ base: tupleBase,
2633
+ node: node.fields[childIndex]
2634
+ };
2635
+ }
2636
+ /** Descend into a dynamic array element by index. */
2637
+ function descendDynamicArray(node, callData, head, base, childIndex) {
2638
+ const arrayBaseResult = readPointer(callData, head);
2639
+ if (typeof arrayBaseResult !== "number") return {
2640
+ type: "violation",
2641
+ code: arrayBaseResult.code
2642
+ };
2643
+ const arrayBase = base + arrayBaseResult;
2644
+ const lengthResult = readPointer(callData, arrayBase);
2645
+ if (typeof lengthResult !== "number") return {
2646
+ type: "violation",
2647
+ code: lengthResult.code
2648
+ };
2649
+ if (childIndex >= lengthResult) return {
2650
+ type: "violation",
2651
+ code: "ARRAY_INDEX_OUT_OF_BOUNDS"
2652
+ };
2653
+ const headsBase = arrayBase + 32;
2654
+ const elem = node.element;
2655
+ if (elem.isDynamic) return {
2656
+ type: "ok",
2657
+ head: headsBase + childIndex * 32,
2658
+ base: headsBase,
2659
+ node: elem
2660
+ };
2661
+ return {
2662
+ type: "ok",
2663
+ head: headsBase + childIndex * elem.staticSize,
2664
+ base: arrayBase,
2665
+ node: elem
2666
+ };
2667
+ }
2668
+ /** Descend into a static array element by index. */
2669
+ function descendStaticArray(node, callData, head, base, childIndex) {
2670
+ if (childIndex >= node.length) throw new CallciumError("INVALID_PATH", `Static array index ${childIndex} out of range (length ${node.length}).`);
2671
+ const elem = node.element;
2672
+ if (elem.isDynamic) {
2673
+ const ptrResult = readPointer(callData, head);
2674
+ if (typeof ptrResult !== "number") return {
2675
+ type: "violation",
2676
+ code: ptrResult.code
2677
+ };
2678
+ const arrayBase = base + ptrResult;
2679
+ return {
2680
+ type: "ok",
2681
+ head: arrayBase + childIndex * 32,
2682
+ base: arrayBase,
2683
+ node: elem
2684
+ };
2685
+ }
2686
+ return {
2687
+ type: "ok",
2688
+ head: head + childIndex * elem.staticSize,
2689
+ base,
2690
+ node: elem
2691
+ };
2692
+ }
2693
+ /**
2694
+ * Resolve a path to a calldata location, stopping at a leaf or quantifier.
2695
+ *
2696
+ * The path is a sequence of BE16 steps. The first step selects the top-level
2697
+ * parameter; subsequent steps descend into tuples and arrays. If a quantifier
2698
+ * step is encountered, returns a QuantifierResult with the array location and
2699
+ * remaining path suffix.
2700
+ */
2701
+ function locate(tree, callData, pathBytes, baseOffset) {
2702
+ const stepCount = pathBytes.length / 2;
2703
+ if (stepCount === 0) throw new CallciumError("INVALID_PATH", "Path is empty.");
2704
+ const paramIndex = readStep(pathBytes, 0);
2705
+ if (paramIndex >= tree.length) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (${tree.length} params).`);
2706
+ let head = baseOffset;
2707
+ for (let i = 0; i < paramIndex; i++) head += headContribution(tree[i]);
2708
+ let base = baseOffset;
2709
+ let node = tree[paramIndex];
2710
+ for (let s = 1; s < stepCount; s++) {
2711
+ const step = readStep(pathBytes, s);
2712
+ if (isQuantifier(step)) {
2713
+ const remaining = pathBytes.subarray((s + 1) * 2);
2714
+ for (let r = 0; r < remaining.length / 2; r++) if (isQuantifier(readStep(remaining, r))) throw new CallciumError("INVALID_QUANTIFIER", "Nested quantifiers are not supported.");
2715
+ return {
2716
+ ok: true,
2717
+ type: "quantifier",
2718
+ quantifier: {
2719
+ quantifier: step,
2720
+ location: {
2721
+ head,
2722
+ base,
2723
+ node
2724
+ },
2725
+ remainingPath: remaining
2726
+ }
2727
+ };
2728
+ }
2729
+ const result = descend(node, callData, head, base, step);
2730
+ if (result.type === "violation") return {
2731
+ ok: false,
2732
+ code: result.code
2733
+ };
2734
+ head = result.head;
2735
+ base = result.base;
2736
+ node = result.node;
2737
+ }
2738
+ return {
2739
+ ok: true,
2740
+ type: "leaf",
2741
+ location: {
2742
+ head,
2743
+ base,
2744
+ node
2745
+ }
2746
+ };
2747
+ }
2748
+ /** Extract array shape from a location pointing to an array node. */
2749
+ function arrayShape(callData, location) {
2750
+ const { head, base, node } = location;
2751
+ if (node.type === "dynamicArray") {
2752
+ const arrayBaseResult = readPointer(callData, head);
2753
+ if (typeof arrayBaseResult !== "number") return {
2754
+ ok: false,
2755
+ code: arrayBaseResult.code
2756
+ };
2757
+ const arrayBase = base + arrayBaseResult;
2758
+ const lengthResult = readPointer(callData, arrayBase);
2759
+ if (typeof lengthResult !== "number") return {
2760
+ ok: false,
2761
+ code: lengthResult.code
2762
+ };
2763
+ const elem = node.element;
2764
+ const headsBase = arrayBase + 32;
2765
+ if (elem.isDynamic) return {
2766
+ ok: true,
2767
+ shape: {
2768
+ length: lengthResult,
2769
+ elementNode: elem,
2770
+ elementIsDynamic: true,
2771
+ elementStaticSize: 0,
2772
+ headsBase,
2773
+ dataOffset: 0,
2774
+ compositeBase: headsBase
2775
+ }
2776
+ };
2777
+ return {
2778
+ ok: true,
2779
+ shape: {
2780
+ length: lengthResult,
2781
+ elementNode: elem,
2782
+ elementIsDynamic: false,
2783
+ elementStaticSize: elem.staticSize,
2784
+ headsBase: 0,
2785
+ dataOffset: headsBase,
2786
+ compositeBase: arrayBase
2787
+ }
2788
+ };
2789
+ }
2790
+ if (node.type === "staticArray") {
2791
+ const elem = node.element;
2792
+ if (elem.isDynamic) {
2793
+ const arrayBaseResult = readPointer(callData, head);
2794
+ if (typeof arrayBaseResult !== "number") return {
2795
+ ok: false,
2796
+ code: arrayBaseResult.code
2797
+ };
2798
+ const arrayBase = base + arrayBaseResult;
2799
+ return {
2800
+ ok: true,
2801
+ shape: {
2802
+ length: node.length,
2803
+ elementNode: elem,
2804
+ elementIsDynamic: true,
2805
+ elementStaticSize: 0,
2806
+ headsBase: arrayBase,
2807
+ dataOffset: 0,
2808
+ compositeBase: arrayBase
2809
+ }
2810
+ };
2811
+ }
2812
+ return {
2813
+ ok: true,
2814
+ shape: {
2815
+ length: node.length,
2816
+ elementNode: elem,
2817
+ elementIsDynamic: false,
2818
+ elementStaticSize: elem.staticSize,
2819
+ headsBase: 0,
2820
+ dataOffset: head,
2821
+ compositeBase: base
2822
+ }
2823
+ };
2824
+ }
2825
+ throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
2826
+ }
2827
+ /** Compute the location for element N within a precomputed array shape. */
2828
+ function arrayElementAt(shape, index, callData) {
2829
+ if (index >= shape.length) return {
2830
+ ok: false,
2831
+ code: "ARRAY_INDEX_OUT_OF_BOUNDS"
2832
+ };
2833
+ let head;
2834
+ let base;
2835
+ if (!shape.elementIsDynamic) {
2836
+ head = shape.dataOffset + index * shape.elementStaticSize;
2837
+ base = shape.compositeBase;
2838
+ } else {
2839
+ head = shape.headsBase + index * 32;
2840
+ base = shape.headsBase;
2841
+ }
2842
+ if (head + 32 > callData.length) return {
2843
+ ok: false,
2844
+ code: "CALLDATA_OUT_OF_BOUNDS"
2845
+ };
2846
+ return {
2847
+ ok: true,
2848
+ location: {
2849
+ head,
2850
+ base,
2851
+ node: shape.elementNode
2852
+ }
2853
+ };
2854
+ }
2855
+ /** Load a 32-byte scalar value from a resolved location. */
2856
+ function loadScalar(callData, location) {
2857
+ const result = load32(callData, location.head);
2858
+ if (result instanceof Uint8Array) return {
2859
+ ok: true,
2860
+ value: result
2861
+ };
2862
+ return {
2863
+ ok: false,
2864
+ code: result.code
2865
+ };
2866
+ }
2867
+ /** Load the data offset and logical length for a dynamic type (bytes, string, or dynamic array). */
2868
+ function loadSlice(callData, location) {
2869
+ const ptrResult = readPointer(callData, location.head);
2870
+ if (typeof ptrResult !== "number") return {
2871
+ ok: false,
2872
+ code: ptrResult.code
2873
+ };
2874
+ const dataStart = location.base + ptrResult;
2875
+ const lengthResult = readPointer(callData, dataStart);
2876
+ if (typeof lengthResult !== "number") return {
2877
+ ok: false,
2878
+ code: lengthResult.code
2879
+ };
2880
+ return {
2881
+ ok: true,
2882
+ dataOffset: dataStart + 32,
2883
+ length: lengthResult
2884
+ };
2885
+ }
2886
+ /** Continue navigating through additional path steps from a resolved location. */
2887
+ function descendPath(callData, location, pathBytes) {
2888
+ const stepCount = pathBytes.length / 2;
2889
+ let { head, base, node } = location;
2890
+ for (let s = 0; s < stepCount; s++) {
2891
+ const step = readStep(pathBytes, s);
2892
+ const result = descend(node, callData, head, base, step);
2893
+ if (result.type === "violation") return {
2894
+ ok: false,
2895
+ code: result.code
2896
+ };
2897
+ head = result.head;
2898
+ base = result.base;
2899
+ node = result.node;
2900
+ }
2901
+ return {
2902
+ ok: true,
2903
+ location: {
2904
+ head,
2905
+ base,
2906
+ node
2907
+ }
2908
+ };
2909
+ }
2910
+ //#endregion
2911
+ //#region src/policy-enforcer.ts
2912
+ /** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
2913
+ function addressToBigInt(hex) {
2914
+ const bytes = hexToBytes(hex);
2915
+ const padded = new Uint8Array(32);
2916
+ padded.set(bytes, 32 - bytes.length);
2917
+ return toBigInt(padded, 0);
2918
+ }
2919
+ /** Format a bigint as a compact hex string without leading-zero padding. */
2920
+ function compactHex(value) {
2921
+ if (value === 0n) return "0x0";
2922
+ return `0x${value.toString(16)}`;
2923
+ }
2924
+ /** Truncate a hex string to at most maxChars characters, adding ellipsis if needed. */
2925
+ function truncateHexForMessage(hex, maxChars = 66) {
2926
+ if (hex.length <= maxChars) return hex;
2927
+ return `${hex.slice(0, maxChars - 1)}…`;
2928
+ }
2929
+ /** Format a decoded operand as decimal for length ops, otherwise compact hex. */
2930
+ function formatOperandValue(value, opCode) {
2931
+ return isLengthOp(opCode) ? value.toString(10) : compactHex(value);
2932
+ }
2933
+ /** Format the constraint (operator + operands) from a single lookupOp call. */
2934
+ function formatConstraint(opCode, operandData) {
2935
+ const negated = (opCode & Op.NOT) !== 0;
2936
+ const { label, operands } = lookupOp(opCode);
2937
+ const op = negated ? `not ${label}` : label;
2938
+ let formattedOperands;
2939
+ if (operands === "variadic") formattedOperands = `(${operandData.length / 32} values)`;
2940
+ else if (operands === "range") {
2941
+ const min = toBigInt(operandData, 0);
2942
+ const max = toBigInt(operandData, 32);
2943
+ formattedOperands = `[${formatOperandValue(min, opCode)}, ${formatOperandValue(max, opCode)}]`;
2944
+ } else formattedOperands = formatOperandValue(toBigInt(operandData, 0), opCode);
2945
+ return `${op} ${formattedOperands}`;
2946
+ }
2947
+ /** Build a self-contained VALUE_MISMATCH message for a scalar or length comparison. */
2948
+ function mismatchMessage(opCode, actual, operandData) {
2949
+ const display = typeof actual === "number" ? String(actual) : truncateHexForMessage(compactHex(actual));
2950
+ return `constraint ${formatConstraint(opCode, operandData)} violated by ${display}`;
2951
+ }
2952
+ /**
2953
+ * Check a policy against ABI-encoded call data without throwing on violations.
2954
+ * @param policy - Binary policy blob as 0x-prefixed hex string.
2955
+ * @param callData - ABI-encoded call data as 0x-prefixed hex string.
2956
+ * @param context - Optional execution context for context-scoped rules.
2957
+ * @returns Pass with matched group index, or fail with one violation per failed group.
2958
+ * @throws {CallciumError} If the policy blob is structurally malformed.
2959
+ */
2960
+ function check(policy, callData, context) {
2961
+ const { policy: decoded, tree } = decodePolicy(policy);
2962
+ const callDataBytes = hexToBytes(callData);
2963
+ if (!decoded.isSelectorless) {
2964
+ if (callDataBytes.length < 4) return {
2965
+ ok: false,
2966
+ violations: [{
2967
+ code: "MISSING_SELECTOR",
2968
+ message: "Calldata too short to contain a selector"
2969
+ }]
2970
+ };
2971
+ const expectedSelector = decoded.selector.value;
2972
+ const actualSelector = bytesToHex(callDataBytes.subarray(0, 4));
2973
+ if (actualSelector !== expectedSelector) return {
2974
+ ok: false,
2975
+ violations: [{
2976
+ code: "SELECTOR_MISMATCH",
2977
+ message: `Expected selector ${expectedSelector}, got ${actualSelector}`,
2978
+ resolvedValue: actualSelector
2979
+ }]
2980
+ };
2981
+ }
2982
+ const baseOffset = decoded.isSelectorless ? 0 : PolicyFormat.SELECTOR_SIZE;
2983
+ const allViolations = [];
2984
+ for (let groupIndex = 0; groupIndex < decoded.groups.length; groupIndex++) {
2985
+ const group = decoded.groups[groupIndex];
2986
+ let groupFailed = false;
2987
+ for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
2988
+ const rule = group.rules[ruleIndex];
2989
+ const violation = evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
2990
+ if (violation !== null) {
2991
+ allViolations.push(violation);
2992
+ groupFailed = true;
2993
+ break;
2994
+ }
2995
+ }
2996
+ if (!groupFailed) return {
2997
+ ok: true,
2998
+ matchedGroup: groupIndex
2999
+ };
3000
+ }
3001
+ return {
3002
+ ok: false,
3003
+ violations: allViolations
3004
+ };
3005
+ }
3006
+ /**
3007
+ * Enforce a policy against ABI-encoded call data, throwing on violation.
3008
+ * @param policy - Binary policy blob as 0x-prefixed hex string.
3009
+ * @param callData - ABI-encoded call data as 0x-prefixed hex string.
3010
+ * @param context - Optional execution context for context-scoped rules.
3011
+ * @throws {PolicyViolationError} If the policy rejects the call data.
3012
+ * @throws {CallciumError} If the policy blob is structurally malformed.
3013
+ */
3014
+ function enforce(policy, callData, context) {
3015
+ const result = check(policy, callData, context);
3016
+ if (!result.ok) throw new PolicyViolationError(result.violations);
3017
+ }
3018
+ /** Evaluate a single rule against calldata or context, returning a violation or null on pass. */
3019
+ function evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context) {
3020
+ const scope = rule.scope.value;
3021
+ const pathBytes = hexToBytes(rule.path.value);
3022
+ const opCode = rule.opCode.value;
3023
+ const operandData = hexToBytes(rule.data.value);
3024
+ if (pathBytes.length / 2 > Limits.MAX_PATH_DEPTH) throw new CallciumError("INVALID_PATH", `Path depth ${pathBytes.length / 2} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
3025
+ if (scope === Scope.CONTEXT) return evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
3026
+ const locResult = locate(tree, callDataBytes, pathBytes, baseOffset);
3027
+ if (!locResult.ok) return {
3028
+ group: groupIndex,
3029
+ rule: ruleIndex,
3030
+ code: locResult.code,
3031
+ message: `Navigation failed: ${locResult.code}`,
3032
+ path: rule.path.value
3033
+ };
3034
+ if (locResult.type === "quantifier") return evaluateQuantifier(locResult.quantifier, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
3035
+ return evaluateLeaf(locResult.location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
3036
+ }
3037
+ /** Evaluate a context-scoped rule by resolving the property from the execution context. */
3038
+ function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
3039
+ const propertyId = readU16(pathBytes, 0);
3040
+ if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PATH", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
3041
+ const propInfo = lookupContextProperty(propertyId);
3042
+ const contextValue = context?.[propInfo.contextKey];
3043
+ if (contextValue === void 0) return {
3044
+ group: groupIndex,
3045
+ rule: ruleIndex,
3046
+ code: "MISSING_CONTEXT",
3047
+ message: `Context property ${propInfo.label} not provided`,
3048
+ path: pathHex
3049
+ };
3050
+ let value;
3051
+ if (typeof contextValue === "string") value = addressToBigInt(contextValue);
3052
+ else value = contextValue;
3053
+ if (!applyOperator(opCode, value, 32, operandData, TypeCode.UINT_MAX)) return {
3054
+ group: groupIndex,
3055
+ rule: ruleIndex,
3056
+ code: "VALUE_MISMATCH",
3057
+ message: mismatchMessage(opCode, value, operandData),
3058
+ path: pathHex,
3059
+ resolvedValue: bigintToHex(value)
3060
+ };
3061
+ return null;
3062
+ }
3063
+ /** Load a value from calldata at the given location and apply the operator. */
3064
+ function applyLeafOperator(callDataBytes, location, opCode, operandData) {
3065
+ const { node } = location;
3066
+ if (isLengthOp(opCode)) {
3067
+ let valueLength;
3068
+ if (node.isDynamic) {
3069
+ const lengthResult = loadSlice(callDataBytes, location);
3070
+ if (!lengthResult.ok) return { error: lengthResult.code };
3071
+ valueLength = lengthResult.length;
3072
+ } else valueLength = node.staticSize;
3073
+ return {
3074
+ passed: applyOperator(opCode, 0n, valueLength, operandData, node.typeCode),
3075
+ length: valueLength
3076
+ };
3077
+ }
3078
+ const result = loadScalar(callDataBytes, location);
3079
+ if (!result.ok) return { error: result.code };
3080
+ const value = toBigInt(result.value, 0);
3081
+ return {
3082
+ passed: applyOperator(opCode, value, 32, operandData, node.typeCode),
3083
+ value
3084
+ };
3085
+ }
3086
+ /** Evaluate a leaf rule, producing a violation on failure or null on pass. */
3087
+ function evaluateLeaf(location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3088
+ const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
3089
+ if ("error" in result) {
3090
+ const message = isLengthOp(opCode) ? `Failed to read length: ${result.error}` : `Failed to load value: ${result.error}`;
3091
+ return {
3092
+ group: groupIndex,
3093
+ rule: ruleIndex,
3094
+ code: result.error,
3095
+ message,
3096
+ path: pathHex
3097
+ };
3098
+ }
3099
+ if (!result.passed) {
3100
+ const actual = result.value !== void 0 ? result.value : result.length ?? 0;
3101
+ return {
3102
+ group: groupIndex,
3103
+ rule: ruleIndex,
3104
+ code: "VALUE_MISMATCH",
3105
+ message: mismatchMessage(opCode, actual, operandData),
3106
+ path: pathHex,
3107
+ resolvedValue: bigintToHex(typeof actual === "number" ? BigInt(actual) : actual)
3108
+ };
3109
+ }
3110
+ return null;
3111
+ }
3112
+ /** Evaluate a quantified rule by iterating over array elements with ALL/ANY semantics. */
3113
+ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3114
+ const { quantifier, location: arrayLocation, remainingPath } = qResult;
3115
+ const shapeResult = arrayShape(callDataBytes, arrayLocation);
3116
+ if (!shapeResult.ok) return {
3117
+ group: groupIndex,
3118
+ rule: ruleIndex,
3119
+ code: shapeResult.code,
3120
+ message: `Failed to read array shape: ${shapeResult.code}`,
3121
+ path: pathHex
3122
+ };
3123
+ const shape = shapeResult.shape;
3124
+ if (shape.length > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) return {
3125
+ group: groupIndex,
3126
+ rule: ruleIndex,
3127
+ code: "QUANTIFIER_LIMIT_EXCEEDED",
3128
+ message: `Array length ${shape.length} exceeds maximum ${Limits.MAX_QUANTIFIED_ARRAY_LENGTH}`,
3129
+ path: pathHex
3130
+ };
3131
+ if (shape.length === 0) {
3132
+ if (quantifier === Quantifier.ALL_OR_EMPTY) return null;
3133
+ return {
3134
+ group: groupIndex,
3135
+ rule: ruleIndex,
3136
+ code: "QUANTIFIER_EMPTY_ARRAY",
3137
+ message: "Quantifier applied to empty array",
3138
+ path: pathHex
3139
+ };
3140
+ }
3141
+ const isUniversal = quantifier === Quantifier.ALL_OR_EMPTY || quantifier === Quantifier.ALL;
3142
+ const hasSuffix = remainingPath.length > 0;
3143
+ for (let elemIndex = 0; elemIndex < shape.length; elemIndex++) {
3144
+ const elemResult = arrayElementAt(shape, elemIndex, callDataBytes);
3145
+ if (!elemResult.ok) {
3146
+ if (isUniversal) return {
3147
+ group: groupIndex,
3148
+ rule: ruleIndex,
3149
+ code: elemResult.code,
3150
+ message: `Quantifier element ${elemIndex}: ${elemResult.code}`,
3151
+ path: pathHex
3152
+ };
3153
+ continue;
3154
+ }
3155
+ const elemLocation = elemResult.location;
3156
+ let elemPassed;
3157
+ if (hasSuffix) {
3158
+ const leafResult = descendPath(callDataBytes, elemLocation, remainingPath);
3159
+ if (!leafResult.ok) {
3160
+ if (isUniversal) return {
3161
+ group: groupIndex,
3162
+ rule: ruleIndex,
3163
+ code: leafResult.code,
3164
+ message: `Quantifier element ${elemIndex} navigation failed: ${leafResult.code}`,
3165
+ path: pathHex
3166
+ };
3167
+ continue;
3168
+ }
3169
+ elemPassed = evaluateLeafValue(leafResult.location, callDataBytes, opCode, operandData);
3170
+ } else {
3171
+ const leafResult = applyLeafOperator(callDataBytes, elemLocation, opCode, operandData);
3172
+ if ("error" in leafResult) {
3173
+ if (isUniversal) return {
3174
+ group: groupIndex,
3175
+ rule: ruleIndex,
3176
+ code: leafResult.error,
3177
+ message: `Quantifier element ${elemIndex}: ${leafResult.error}`,
3178
+ path: pathHex
3179
+ };
3180
+ continue;
3181
+ }
3182
+ elemPassed = leafResult.passed;
3183
+ }
3184
+ if (isUniversal && !elemPassed) return {
3185
+ group: groupIndex,
3186
+ rule: ruleIndex,
3187
+ code: "VALUE_MISMATCH",
3188
+ message: `constraint ${formatConstraint(opCode, operandData)} violated at element ${elemIndex}`,
3189
+ path: pathHex
3190
+ };
3191
+ if (!isUniversal && elemPassed) return null;
3192
+ }
3193
+ if (isUniversal) return null;
3194
+ return {
3195
+ group: groupIndex,
3196
+ rule: ruleIndex,
3197
+ code: "VALUE_MISMATCH",
3198
+ message: `constraint ${formatConstraint(opCode, operandData)} violated by all elements`,
3199
+ path: pathHex
3200
+ };
3201
+ }
3202
+ /** Apply a leaf operator and return a boolean. Returns false on calldata read failure. */
3203
+ function evaluateLeafValue(location, callDataBytes, opCode, operandData) {
3204
+ const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
3205
+ if ("error" in result) return false;
3206
+ return result.passed;
3207
+ }
3208
+ /** Check and enforce policies against ABI-encoded call data. */
3209
+ const PolicyEnforcer = {
3210
+ check,
3211
+ enforce
3212
+ };
3213
+ //#endregion
3214
+ export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, arg, blockNumber, blockTimestamp, bytesToHex, chainId, hexToBytes, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
3215
+
3216
+ //# sourceMappingURL=index.js.map