@callcium/sdk 0.6.0 → 0.8.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 CHANGED
@@ -1,12 +1,7 @@
1
- import { keccak_256 } from "@noble/hashes/sha3";
1
+ import { keccak_256 } from "@noble/hashes/sha3.js";
2
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
3
  var CallciumError = class CallciumError extends Error {
8
4
  code;
9
- /** Byte position in the source blob where the error was detected, if applicable. */
10
5
  offset;
11
6
  constructor(code, message, offset) {
12
7
  const prefix = offset !== void 0 ? `[offset ${offset}] ` : "";
@@ -16,16 +11,15 @@ var CallciumError = class CallciumError extends Error {
16
11
  Object.setPrototypeOf(this, CallciumError.prototype);
17
12
  }
18
13
  };
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
- *
24
- * Carries the full list of structured violations (one per failed group). The
25
- * `Error.message` is a minimal non-lossy diagnostic summary built from the
26
- * first violation's structured fields — not a presentation contract. Consumers
27
- * rendering violations to users should iterate `violations` directly.
28
- */
14
+ var ValidationError = class ValidationError extends CallciumError {
15
+ issues;
16
+ constructor(issues) {
17
+ super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
18
+ this.name = "ValidationError";
19
+ this.issues = issues;
20
+ Object.setPrototypeOf(this, ValidationError.prototype);
21
+ }
22
+ };
29
23
  var PolicyViolationError = class PolicyViolationError extends Error {
30
24
  violations;
31
25
  constructor(violations) {
@@ -35,7 +29,6 @@ var PolicyViolationError = class PolicyViolationError extends Error {
35
29
  Object.setPrototypeOf(this, PolicyViolationError.prototype);
36
30
  }
37
31
  };
38
- /** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
39
32
  function formatDiagnostic(violation) {
40
33
  if (!violation) return "Policy violation";
41
34
  const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
@@ -43,56 +36,61 @@ function formatDiagnostic(violation) {
43
36
  }
44
37
  //#endregion
45
38
  //#region src/bytes.ts
46
- /** Regex that matches a valid hex body (even number of hex chars). */
47
39
  const HEX_BODY_RE = /^[0-9a-fA-F]*$/;
48
- /** Convert a hex string to a byte array, stripping the 0x prefix if present. */
40
+ const HEX_DIGITS = "0123456789abcdef";
41
+ const NIBBLE = new Int8Array(128).fill(-1);
42
+ for (let value = 0; value < 16; value++) {
43
+ NIBBLE[HEX_DIGITS.charCodeAt(value)] = value;
44
+ NIBBLE[HEX_DIGITS.toUpperCase().charCodeAt(value)] = value;
45
+ }
49
46
  function hexToBytes(hex) {
50
47
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
51
48
  if (clean.length % 2 !== 0) throw new CallciumError("INVALID_HEX", "Odd-length hex string");
52
- if (!HEX_BODY_RE.test(clean)) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
53
49
  const bytes = new Uint8Array(clean.length / 2);
54
- for (let i = 0; i < clean.length; i += 2) bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
50
+ for (let i = 0; i < bytes.length; i++) {
51
+ const high = NIBBLE[clean.charCodeAt(i * 2)] ?? -1;
52
+ const low = NIBBLE[clean.charCodeAt(i * 2 + 1)] ?? -1;
53
+ if (high < 0 || low < 0) throw new CallciumError("INVALID_HEX", "Invalid hex characters");
54
+ bytes[i] = high << 4 | low;
55
+ }
55
56
  return bytes;
56
57
  }
57
58
  const HEX_LUT = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
58
- /** Convert a byte array to a 0x-prefixed lowercase hex string. */
59
59
  function bytesToHex(bytes) {
60
60
  let body = "";
61
61
  for (let i = 0; i < bytes.length; i++) body += HEX_LUT[bytes[i]];
62
62
  return `0x${body}`;
63
63
  }
64
- /** Extract a subarray and return it as a 0x-prefixed hex string. */
65
64
  function toHex(data, start, end) {
66
65
  return bytesToHex(data.subarray(start, end));
67
66
  }
68
- /** Read a big-endian uint16 from a byte array at the given offset. */
69
67
  function readU16(data, offset) {
70
68
  return data[offset] << 8 | data[offset + 1];
71
69
  }
72
- /** Read a big-endian uint24 from a byte array at the given offset. */
73
70
  function readU24(data, offset) {
74
71
  return data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
75
72
  }
76
- /** Read a big-endian uint32 from a byte array at the given offset. */
77
73
  function readU32(data, offset) {
78
74
  return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
79
75
  }
80
- /** Write a 16-bit big-endian value into a Uint8Array at offset. */
81
76
  function writeBE16(buffer, offset, value) {
82
77
  buffer[offset] = value >>> 8 & 255;
83
78
  buffer[offset + 1] = value & 255;
84
79
  }
85
- /** Write a 24-bit big-endian value into a Uint8Array at offset. */
86
80
  function writeBE24(buffer, offset, value) {
87
81
  buffer[offset] = value >>> 16 & 255;
88
82
  buffer[offset + 1] = value >>> 8 & 255;
89
83
  buffer[offset + 2] = value & 255;
90
84
  }
91
- /** Format a bigint as a zero-padded 32-byte hex string. */
85
+ function writeBE32(buffer, offset, value) {
86
+ buffer[offset] = value >>> 24 & 255;
87
+ buffer[offset + 1] = value >>> 16 & 255;
88
+ buffer[offset + 2] = value >>> 8 & 255;
89
+ buffer[offset + 3] = value & 255;
90
+ }
92
91
  function bigintToHex(value) {
93
92
  return `0x${value.toString(16).padStart(64, "0")}`;
94
93
  }
95
- /** Validate and brand a hex string as an address. Throws if the runtime length is not exactly 20 bytes. */
96
94
  function toAddress(hex) {
97
95
  const body = hex.startsWith("0x") ? hex.slice(2) : hex;
98
96
  if (body.length !== 40) throw new CallciumError("INVALID_HEX", `Invalid address length: expected 40 hex chars, got ${body.length}`);
@@ -101,9 +99,8 @@ function toAddress(hex) {
101
99
  }
102
100
  //#endregion
103
101
  //#region src/constants.ts
104
- /** Binary layout constants for the Callcium descriptor format. */
105
102
  const DescriptorFormat = {
106
- VERSION: 1,
103
+ VERSION: 2,
107
104
  HEADER_SIZE: 2,
108
105
  TYPECODE_SIZE: 1,
109
106
  COMPOSITE_META_SIZE: 3,
@@ -116,12 +113,12 @@ const DescriptorFormat = {
116
113
  MAX_NODE_LENGTH: 4095,
117
114
  MAX_STATIC_ARRAY_LENGTH: 4095,
118
115
  MAX_TUPLE_FIELDS: 4089,
116
+ MAX_STATIC_WORDS: 4095,
119
117
  MAX_PARAMS: 255,
120
118
  MAX_NESTING_DEPTH: 64
121
119
  };
122
- /** Binary layout constants for the Callcium policy format. */
123
120
  const PolicyFormat = {
124
- VERSION: 1,
121
+ VERSION: 2,
125
122
  VERSION_MASK: 15,
126
123
  FLAG_NO_SELECTOR: 16,
127
124
  RESERVED_MASK: 224,
@@ -143,9 +140,31 @@ const PolicyFormat = {
143
140
  RULE_OPCODE_SIZE: 1,
144
141
  RULE_DATALENGTH_SIZE: 2,
145
142
  RULE_FIXED_OVERHEAD: 7,
146
- RULE_MIN_SIZE: 9
143
+ RULE_MIN_SIZE: 9,
144
+ HINT_HEADER_SIZE: 1,
145
+ HINT_HOP_SIZE: 8,
146
+ HINT_HOP_INDEX_OFFSET: 4,
147
+ HINT_HOP_META_OFFSET: 6,
148
+ HINT_FRAME_PREFIX_SIZE: 8,
149
+ HINT_FRAME_COUNT_OFFSET: 4,
150
+ HINT_FRAME_META_OFFSET: 6,
151
+ HINT_TARGET_SIZE: 7,
152
+ HINT_TARGET_META_OFFSET: 4,
153
+ HINT_TARGET_TYPECODE_OFFSET: 6,
154
+ HINT_KIND_NONE: 0,
155
+ HINT_KIND_ALL: 1,
156
+ HINT_KIND_ANY: 2,
157
+ HINT_KIND_MAX: 2,
158
+ HINT_KIND_SHIFT: 6,
159
+ HINT_HOP_COUNT_MASK: 63,
160
+ HINT_SUFFIX_RESERVED_MASK: 192,
161
+ HINT_NO_INDEX: 65535,
162
+ HINT_INDEX_RESERVED: 65534,
163
+ HINT_META_ELEM_DYNAMIC: 32768,
164
+ HINT_META_DYNAMIC_ARRAY: 16384,
165
+ HINT_META_RESERVED_MASK: 12288,
166
+ HINT_META_STRIDE_MASK: 4095
147
167
  };
148
- /** Build a plain `{ KEY: code }` object from a table at runtime. */
149
168
  function buildCodeMap(table) {
150
169
  return Object.fromEntries(table.map((e) => [e.key, e.code]));
151
170
  }
@@ -158,29 +177,21 @@ const SCOPE_TABLE = [{
158
177
  code: 1,
159
178
  label: "calldata"
160
179
  }];
161
- /** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
162
180
  const Scope = buildCodeMap(SCOPE_TABLE);
163
181
  const scopeByCode = new Map(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
164
- /**
165
- * Map a scope code to its display label.
166
- * @param code - Scope byte value.
167
- * @returns Display metadata for the scope.
168
- * @throws {CallciumError} If the code is not a recognised scope.
169
- */
170
182
  function lookupScope(code) {
171
183
  const info = scopeByCode.get(code);
172
184
  if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
173
185
  return info;
174
186
  }
175
- /** ABI type code ranges and sentinel values for the descriptor format. */
176
187
  const TypeCode = {
177
- UINT_MIN: 0,
178
- UINT_MAX: 31,
179
- INT_MIN: 32,
180
- INT_MAX: 63,
181
- ADDRESS: 64,
182
- BOOL: 65,
183
- FUNCTION: 66,
188
+ UINT_MIN: 1,
189
+ UINT_MAX: 32,
190
+ INT_MIN: 33,
191
+ INT_MAX: 64,
192
+ ADDRESS: 65,
193
+ BOOL: 66,
194
+ FUNCTION: 67,
184
195
  FIXED_BYTES_MIN: 80,
185
196
  FIXED_BYTES_MAX: 111,
186
197
  BYTES: 112,
@@ -239,11 +250,8 @@ const CTX_PROP_TABLE = [
239
250
  typeCode: TypeCode.UINT_MAX
240
251
  }
241
252
  ];
242
- /** Well-known context property IDs for context-scope rules. */
243
253
  const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
244
- /** Maximum valid context property ID. */
245
254
  const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
246
- /** Maps each context property table key to its camelCase Context key. */
247
255
  const CTX_KEY_MAP = {
248
256
  MSG_SENDER: "msgSender",
249
257
  MSG_VALUE: "msgValue",
@@ -259,23 +267,16 @@ const ctxPropByCode = new Map(CTX_PROP_TABLE.map((e) => [e.code, {
259
267
  contextKey: CTX_KEY_MAP[e.key],
260
268
  typeCode: e.typeCode
261
269
  }]));
262
- /**
263
- * Map a context property code to its display label and ABI type code.
264
- * @param code - Context property ID.
265
- * @returns Display metadata including the Solidity type code for the property value.
266
- * @throws {CallciumError} If the code is not a recognised context property.
267
- */
268
270
  function lookupContextProperty(code) {
269
271
  const info = ctxPropByCode.get(code);
270
- if (!info) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Unknown context property ${code}`);
272
+ if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
271
273
  return info;
272
274
  }
273
- /** Protocol-imposed safety limits for path depth and quantifier array size. */
274
275
  const Limits = {
275
276
  MAX_PATH_DEPTH: 32,
276
- MAX_QUANTIFIED_ARRAY_LENGTH: 256
277
+ MAX_QUANTIFIED_ARRAY_LENGTH: 256,
278
+ MAX_SET_MEMBERS: Math.floor((256 ** PolicyFormat.RULE_DATALENGTH_SIZE - 1) / 32)
277
279
  };
278
- /** @internal */
279
280
  const OP_TABLE = [
280
281
  {
281
282
  key: "EQ",
@@ -374,7 +375,6 @@ const OP_TABLE = [
374
375
  operands: "range"
375
376
  }
376
377
  ];
377
- /** Callcium policy operator codes. */
378
378
  const Op = {
379
379
  ...buildCodeMap(OP_TABLE),
380
380
  NOT: 128
@@ -383,24 +383,12 @@ const opByCode = new Map(OP_TABLE.map((e) => [e.code, {
383
383
  label: e.label,
384
384
  operands: e.operands
385
385
  }]));
386
- /**
387
- * Map an operator code to its display label. Strips the NOT flag automatically.
388
- * @param code - Operator byte value (may include the NOT flag).
389
- * @returns Display metadata for the base operator.
390
- * @throws {CallciumError} If the base code is not a recognised operator.
391
- */
392
386
  function lookupOp(code) {
393
387
  const base = code & ~Op.NOT;
394
388
  const info = opByCode.get(base);
395
- if (!info) throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
389
+ if (!info) throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
396
390
  return info;
397
391
  }
398
- /**
399
- * Check whether an operator's data payload has the correct length.
400
- * @param opBase - Base operator code with the NOT flag stripped.
401
- * @param dataLength - Byte length of the operator's data payload.
402
- * @returns True if the data length is valid for the given operator.
403
- */
404
392
  function isValidOperatorData(opBase, dataLength) {
405
393
  const operands = opByCode.get(opBase)?.operands;
406
394
  if (operands === "single") return dataLength === 32;
@@ -408,42 +396,25 @@ function isValidOperatorData(opBase, dataLength) {
408
396
  if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
409
397
  return false;
410
398
  }
411
- const QUANTIFIER_TABLE = [
412
- {
413
- key: "ALL_OR_EMPTY",
414
- code: 65535,
415
- label: "all or empty"
416
- },
417
- {
418
- key: "ALL",
419
- code: 65534,
420
- label: "all"
421
- },
422
- {
423
- key: "ANY",
424
- code: 65533,
425
- label: "any"
426
- }
427
- ];
428
- /** Reserved path step values that trigger quantified evaluation over array elements. */
399
+ const QUANTIFIER_TABLE = [{
400
+ key: "ALL",
401
+ code: 65535,
402
+ label: "all"
403
+ }, {
404
+ key: "ANY",
405
+ code: 65534,
406
+ label: "any"
407
+ }];
429
408
  const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
430
409
  const quantifierByCode = new Map(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
431
- /** Check whether a path step is a quantifier (ALL_OR_EMPTY, ALL, or ANY). */
432
410
  function isQuantifier(step) {
433
411
  return step >= Quantifier.ANY;
434
412
  }
435
- /**
436
- * Map a quantifier path step to its display label.
437
- * @param code - Quantifier step value.
438
- * @returns Display metadata for the quantifier.
439
- * @throws {CallciumError} If the code is not a recognised quantifier.
440
- */
441
413
  function lookupQuantifier(code) {
442
414
  const info = quantifierByCode.get(code);
443
- if (!info) throw new CallciumError("INVALID_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
415
+ if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
444
416
  return info;
445
417
  }
446
- /** Throw an UNKNOWN_TYPE_CODE error. */
447
418
  function unknownTypeCode(code) {
448
419
  throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
449
420
  }
@@ -467,18 +438,11 @@ const TUPLE = {
467
438
  typeClass: "tuple",
468
439
  isDynamic: false
469
440
  };
470
- /**
471
- * Classify a type code into its structural category and dynamism.
472
- * Lightweight variant of `lookupTypeCode` that skips label computation.
473
- * @param code - A single-byte descriptor type code.
474
- * @returns Type class and whether the type is ABI-dynamic.
475
- * @throws {CallciumError} If the code is not a recognised type code.
476
- */
477
441
  function classifyTypeCode(code) {
478
442
  if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
479
443
  if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
480
444
  if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
481
- if (code >= 67 && code <= 79) unknownTypeCode(code);
445
+ if (code >= 68 && code <= 79) unknownTypeCode(code);
482
446
  if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
483
447
  if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
484
448
  if (code >= 114 && code <= 127) unknownTypeCode(code);
@@ -488,7 +452,15 @@ function classifyTypeCode(code) {
488
452
  if (code === TypeCode.TUPLE) return TUPLE;
489
453
  unknownTypeCode(code);
490
454
  }
491
- /** Compute the Solidity type label for a type code. */
455
+ function isAddressableTarget(code) {
456
+ let info;
457
+ try {
458
+ info = classifyTypeCode(code);
459
+ } catch {
460
+ return false;
461
+ }
462
+ return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
463
+ }
492
464
  function typeCodeLabel(code) {
493
465
  if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
494
466
  if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
@@ -503,12 +475,6 @@ function typeCodeLabel(code) {
503
475
  if (code === TypeCode.TUPLE) return "tuple";
504
476
  return `0x${code.toString(16).padStart(2, "0")}`;
505
477
  }
506
- /**
507
- * Map a raw type code byte to its Solidity label, structural category, and dynamism.
508
- * @param code - A single-byte descriptor type code.
509
- * @returns Label, type class, and whether the type is ABI-dynamic.
510
- * @throws {CallciumError} If the code is not a recognised type code.
511
- */
512
478
  function lookupTypeCode(code) {
513
479
  return {
514
480
  label: typeCodeLabel(code),
@@ -517,42 +483,25 @@ function lookupTypeCode(code) {
517
483
  }
518
484
  //#endregion
519
485
  //#region src/descriptor.ts
520
- /**
521
- * Return the byte length of the node at `offset`.
522
- * Elementary nodes occupy exactly one byte; composite nodes encode their
523
- * length in the lower 12 bits of the 24-bit meta field.
524
- */
525
486
  function nodeLength(desc, offset) {
526
487
  if (desc[offset] < TypeCode.STATIC_ARRAY) return DescriptorFormat.TYPECODE_SIZE;
527
488
  return readU24(desc, offset + 1) & DescriptorFormat.META_NODE_LENGTH_MASK;
528
489
  }
529
- /**
530
- * Return the byte offset of the `fieldIndex`-th field inside a tuple node.
531
- * Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
532
- */
533
490
  function tupleFieldOffset(desc, tupleOffset, fieldIndex) {
534
491
  let cursor = tupleOffset + DescriptorFormat.TUPLE_HEADER_SIZE;
535
492
  for (let i = 0; i < fieldIndex; i++) cursor += nodeLength(desc, cursor);
536
493
  return cursor;
537
494
  }
538
- /** Return paramCount from header byte 1. */
539
495
  function paramCount(desc) {
540
496
  return desc[1];
541
497
  }
542
- /** Return byte offset of the N-th top-level param (0-indexed). */
543
498
  function paramOffset(desc, index) {
544
499
  const count = desc[1];
545
- if (index >= count) throw new CallciumError("INVALID_PATH", `Param index ${index} out of range (paramCount=${count}).`);
500
+ if (index >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count}).`);
546
501
  let cursor = DescriptorFormat.HEADER_SIZE;
547
502
  for (let i = 0; i < index; i++) cursor += nodeLength(desc, cursor);
548
503
  return cursor;
549
504
  }
550
- /**
551
- * Inspect the type node at a given byte offset.
552
- * @param desc - Raw descriptor bytes.
553
- * @param offset - Byte position of the node.
554
- * @returns typeCode, isDynamic, and staticSize in bytes.
555
- */
556
505
  function inspect$1(desc, offset) {
557
506
  const typeCode = desc[offset];
558
507
  let staticWords;
@@ -564,35 +513,14 @@ function inspect$1(desc, offset) {
564
513
  staticSize: staticWords * 32
565
514
  };
566
515
  }
567
- /**
568
- * Resolve the type at a calldata path (array of step indices).
569
- *
570
- * The first step selects a top-level param. Each subsequent step descends
571
- * into the current node: for tuples it selects a field, for arrays it
572
- * advances to the element descriptor.
573
- *
574
- * @param desc - Raw descriptor bytes.
575
- * @param steps - Path steps, length >= 1.
576
- * @returns TypeInfo for the node at the resolved path.
577
- * @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
578
- */
579
516
  function typeAt(desc, steps) {
580
517
  return walkPath(desc, steps).typeInfo;
581
518
  }
582
- /**
583
- * Resolve the type at the path and the declared length of the static array a
584
- * quantifier step descends into (zero when the path holds no quantifier step
585
- * over a static array).
586
- * @param desc - Raw descriptor bytes.
587
- * @param steps - Path steps, length >= 1.
588
- * @returns TypeInfo at the resolved path and the quantified static array length.
589
- * @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
590
- */
591
519
  function walkPath(desc, steps) {
592
- if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Path must have at least one step.");
520
+ if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
593
521
  const paramIndex = steps[0];
594
522
  const count = desc[1];
595
- if (paramIndex >= count) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (paramCount=${count}).`);
523
+ if (paramIndex >= count) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${paramIndex} out of range (paramCount=${count}).`);
596
524
  let cursor = paramOffset(desc, paramIndex);
597
525
  let quantifiedStaticLength = 0;
598
526
  for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
@@ -600,47 +528,162 @@ function walkPath(desc, steps) {
600
528
  const step = steps[stepIndex];
601
529
  if (typeCode === TypeCode.TUPLE) {
602
530
  const fields = tupleFieldCount(desc, cursor);
603
- if (step >= fields) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
531
+ if (step >= fields) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fields} fields).`);
604
532
  cursor = tupleFieldOffset(desc, cursor, step);
605
533
  } else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
606
534
  if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
607
535
  else {
608
536
  const length = staticArrayLength(desc, cursor);
609
- if (step >= length) throw new CallciumError("INVALID_PATH", `Array index ${step} out of range (length=${length}).`);
537
+ if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length}).`);
610
538
  }
611
539
  cursor = arrayElementOffset(cursor);
612
- } else throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type at offset ${cursor}.`);
540
+ } else throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
613
541
  }
614
542
  return {
615
543
  typeInfo: inspect$1(desc, cursor),
616
544
  quantifiedStaticLength
617
545
  };
618
546
  }
619
- /**
620
- * Return tuple field count at a tuple node offset.
621
- * @param desc - Raw descriptor bytes.
622
- * @param offset - Byte position of the tuple node.
623
- */
624
547
  function tupleFieldCount(desc, offset) {
625
548
  return readU16(desc, offset + 4);
626
549
  }
627
- /**
628
- * Return static array length at a static array node offset.
629
- * @param desc - Raw descriptor bytes.
630
- * @param offset - Byte position of the static array node.
631
- */
632
550
  function staticArrayLength(desc, offset) {
633
551
  const elemOffset = arrayElementOffset(offset);
634
552
  return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
635
553
  }
636
- /**
637
- * Return the byte offset of the array element descriptor.
638
- * @param offset - Byte position of the array node (static or dynamic).
639
- */
640
554
  function arrayElementOffset(offset) {
641
555
  return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
642
556
  }
643
- /** Inspect and navigate raw descriptor bytes. */
557
+ function uncompilablePath(stepIndex) {
558
+ return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target.`);
559
+ }
560
+ function headSpan(desc, offset, count) {
561
+ let span = 0;
562
+ let nodeOffset = offset;
563
+ for (let i = 0; i < count; i++) {
564
+ const node = inspect$1(desc, nodeOffset);
565
+ span += node.isDynamic ? 32 : node.staticSize;
566
+ nodeOffset += nodeLength(desc, nodeOffset);
567
+ }
568
+ return {
569
+ span,
570
+ nodeOffset
571
+ };
572
+ }
573
+ function arrayMeta(desc, arrayOffset, code) {
574
+ const element = inspect$1(desc, arrayElementOffset(arrayOffset));
575
+ let meta = element.isDynamic ? 1 : element.staticSize >> 5;
576
+ if (element.isDynamic) meta |= PolicyFormat.HINT_META_ELEM_DYNAMIC;
577
+ if (code === TypeCode.DYNAMIC_ARRAY) meta |= PolicyFormat.HINT_META_DYNAMIC_ARRAY;
578
+ return {
579
+ meta,
580
+ elemIsDynamic: element.isDynamic
581
+ };
582
+ }
583
+ function enterNode(walk, needed) {
584
+ if (!needed) return;
585
+ walk.chain.push([
586
+ walk.delta,
587
+ PolicyFormat.HINT_NO_INDEX,
588
+ 0
589
+ ]);
590
+ walk.delta = 0;
591
+ }
592
+ function walkStep(desc, walk, descOffset, step, stepIndex) {
593
+ const node = inspect$1(desc, descOffset);
594
+ if (node.typeCode === TypeCode.TUPLE) {
595
+ if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
596
+ enterNode(walk, node.isDynamic && !walk.entered);
597
+ const field = headSpan(desc, descOffset + DescriptorFormat.TUPLE_HEADER_SIZE, step);
598
+ walk.delta += field.span;
599
+ walk.entered = false;
600
+ return field.nodeOffset;
601
+ }
602
+ if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) throw uncompilablePath(stepIndex);
603
+ if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) throw uncompilablePath(stepIndex);
604
+ const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
605
+ if (isQuantifier(step)) {
606
+ if (walk.kind !== PolicyFormat.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
607
+ enterNode(walk, node.isDynamic && !walk.entered);
608
+ const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
609
+ walk.frame = {
610
+ arrayDelta: walk.delta,
611
+ count,
612
+ meta
613
+ };
614
+ walk.kind = step === Quantifier.ALL ? PolicyFormat.HINT_KIND_ALL : PolicyFormat.HINT_KIND_ANY;
615
+ walk.mainHops = walk.chain;
616
+ walk.chain = [];
617
+ walk.delta = 0;
618
+ } else if (node.isDynamic) {
619
+ enterNode(walk, !walk.entered);
620
+ walk.chain.push([
621
+ 0,
622
+ step,
623
+ meta
624
+ ]);
625
+ walk.delta = 0;
626
+ } else walk.delta += step * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
627
+ walk.entered = elemIsDynamic;
628
+ return arrayElementOffset(descOffset);
629
+ }
630
+ function writeHops(out, offset, hops) {
631
+ for (const [delta, index, meta] of hops) {
632
+ writeBE32(out, offset, delta);
633
+ writeBE16(out, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET, index);
634
+ writeBE16(out, offset + PolicyFormat.HINT_HOP_META_OFFSET, meta);
635
+ offset += PolicyFormat.HINT_HOP_SIZE;
636
+ }
637
+ return offset;
638
+ }
639
+ function encodeHint(walk, targetMeta, typeCode, depth) {
640
+ let suffixHops = [];
641
+ if (walk.kind === PolicyFormat.HINT_KIND_NONE) walk.mainHops = walk.chain;
642
+ else suffixHops = walk.chain;
643
+ const mainHopCount = walk.mainHops.length;
644
+ const suffixHopCount = suffixHops.length;
645
+ if (mainHopCount > PolicyFormat.HINT_HOP_COUNT_MASK || suffixHopCount > PolicyFormat.HINT_HOP_COUNT_MASK) throw uncompilablePath(depth);
646
+ const quantified = walk.kind !== PolicyFormat.HINT_KIND_NONE;
647
+ const frameSize = quantified ? PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + suffixHopCount * PolicyFormat.HINT_HOP_SIZE : 0;
648
+ const out = new Uint8Array(PolicyFormat.HINT_HEADER_SIZE + mainHopCount * PolicyFormat.HINT_HOP_SIZE + frameSize + PolicyFormat.HINT_TARGET_SIZE);
649
+ let offset = 0;
650
+ out[offset] = walk.kind << PolicyFormat.HINT_KIND_SHIFT | mainHopCount;
651
+ offset = writeHops(out, offset + PolicyFormat.HINT_HEADER_SIZE, walk.mainHops);
652
+ if (quantified) {
653
+ writeBE32(out, offset, walk.frame.arrayDelta);
654
+ writeBE16(out, offset + PolicyFormat.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
655
+ writeBE16(out, offset + PolicyFormat.HINT_FRAME_META_OFFSET, walk.frame.meta);
656
+ out[offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
657
+ offset = writeHops(out, offset + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, suffixHops);
658
+ }
659
+ writeBE32(out, offset, walk.delta);
660
+ writeBE16(out, offset + PolicyFormat.HINT_TARGET_META_OFFSET, targetMeta);
661
+ out[offset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
662
+ return out;
663
+ }
664
+ function compileHint(desc, steps) {
665
+ if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
666
+ const argIndex = steps[0];
667
+ if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
668
+ const arg = headSpan(desc, DescriptorFormat.HEADER_SIZE, argIndex);
669
+ const walk = {
670
+ chain: [],
671
+ mainHops: [],
672
+ delta: arg.span,
673
+ frame: {
674
+ arrayDelta: 0,
675
+ count: 0,
676
+ meta: 0
677
+ },
678
+ kind: PolicyFormat.HINT_KIND_NONE,
679
+ entered: false
680
+ };
681
+ let descOffset = arg.nodeOffset;
682
+ for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) descOffset = walkStep(desc, walk, descOffset, steps[stepIndex], stepIndex);
683
+ const target = inspect$1(desc, descOffset);
684
+ enterNode(walk, target.isDynamic && !walk.entered);
685
+ return encodeHint(walk, target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0, target.typeCode, steps.length);
686
+ }
644
687
  const Descriptor = {
645
688
  paramCount,
646
689
  paramOffset,
@@ -651,88 +694,47 @@ const Descriptor = {
651
694
  staticArrayLength,
652
695
  nodeLength,
653
696
  arrayElementOffset,
654
- walkPath
697
+ walkPath,
698
+ compileHint
655
699
  };
656
700
  //#endregion
657
701
  //#region src/type-desc.ts
658
- /**
659
- * Extract the staticWords count from a descriptor node.
660
- *
661
- * For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
662
- * BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
663
- * reads the 24-bit meta field at offset 1 and shifts right by
664
- * META_STATIC_WORDS_SHIFT.
665
- */
666
702
  function extractStaticWords(desc) {
667
703
  const typeCode = desc[0];
668
704
  if (typeCode < TypeCode.STATIC_ARRAY) return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
669
705
  return readU24(desc, 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
670
706
  }
671
- /** Build a descriptor node for `address`. */
672
707
  function address() {
673
708
  return new Uint8Array([TypeCode.ADDRESS]);
674
709
  }
675
- /** Build a descriptor node for `bool`. */
676
710
  function bool() {
677
711
  return new Uint8Array([TypeCode.BOOL]);
678
712
  }
679
- /** Build a descriptor node for `function`. */
680
713
  function function_() {
681
714
  return new Uint8Array([TypeCode.FUNCTION]);
682
715
  }
683
- /** Build a descriptor node for `uint256`. */
684
- function uint256() {
685
- return new Uint8Array([TypeCode.UINT_MAX]);
686
- }
687
- /**
688
- * Build a descriptor node for `uintN`.
689
- * @param bits - Bit width, 8–256 in steps of 8.
690
- * @throws {CallciumError} If bits is out of range or not a multiple of 8.
691
- */
692
716
  function uintN(bits) {
693
- 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.`);
717
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8.`);
694
718
  return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
695
719
  }
696
- /**
697
- * Build a descriptor node for `intN`.
698
- * @param bits - Bit width, 8–256 in steps of 8.
699
- * @throws {CallciumError} If bits is out of range or not a multiple of 8.
700
- */
701
720
  function intN(bits) {
702
- 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.`);
721
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8.`);
703
722
  return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
704
723
  }
705
- /** Build a descriptor node for `bytes` (dynamic). */
706
724
  function bytes() {
707
725
  return new Uint8Array([TypeCode.BYTES]);
708
726
  }
709
- /** Build a descriptor node for `string`. */
710
727
  function string_() {
711
728
  return new Uint8Array([TypeCode.STRING]);
712
729
  }
713
- /**
714
- * Build a descriptor node for `bytesN`.
715
- * @param n - Byte width, 1–32.
716
- * @throws {CallciumError} If n is out of range.
717
- */
718
730
  function bytesN(n) {
719
- if (n < 1 || n > 32) throw new CallciumError("INVALID_TYPE_STRING", `Invalid bytesN size: ${n}. Must be 1–32.`);
731
+ if (n < 1 || n > 32) throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32.`);
720
732
  return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
721
733
  }
722
- /**
723
- * Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
724
- *
725
- * Dynamic form: `[0x81][meta:3][elemDesc]`
726
- * Static form: `[0x80][meta:3][elemDesc][length:be16]`
727
- *
728
- * @param elemDesc - Descriptor bytes for the element type.
729
- * @param length - Fixed array length. When omitted, produces a dynamic array.
730
- * @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
731
- */
732
734
  function array(elemDesc, length) {
733
735
  if (length === void 0) {
734
736
  const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
735
- if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
737
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Dynamic array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
736
738
  const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
737
739
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
738
740
  buf[0] = TypeCode.DYNAMIC_ARRAY;
@@ -742,10 +744,10 @@ function array(elemDesc, length) {
742
744
  }
743
745
  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}.`);
744
746
  const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
745
- if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
747
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Static array node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
746
748
  const elemStaticWords = extractStaticWords(elemDesc);
747
749
  const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
748
- 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}.`);
750
+ if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Static array static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}.`);
749
751
  const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
750
752
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
751
753
  buf[0] = TypeCode.STATIC_ARRAY;
@@ -754,19 +756,11 @@ function array(elemDesc, length) {
754
756
  writeBE16(buf, 4 + elemDesc.length, length);
755
757
  return buf;
756
758
  }
757
- /**
758
- * Build a descriptor node for a tuple.
759
- *
760
- * Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
761
- *
762
- * @param fieldDescs - Descriptor bytes for each field, in order.
763
- * @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
764
- */
765
759
  function tuple(fieldDescs) {
766
760
  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}.`);
767
761
  const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
768
762
  const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
769
- if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
763
+ if (nodeLength > DescriptorFormat.MAX_NODE_LENGTH) throw new CallciumError("NODE_LENGTH_TOO_LARGE", `Tuple node length ${nodeLength} exceeds maximum ${DescriptorFormat.MAX_NODE_LENGTH}.`);
770
764
  let anyDynamic = false;
771
765
  let staticWordsSum = 0;
772
766
  for (const field of fieldDescs) {
@@ -775,7 +769,7 @@ function tuple(fieldDescs) {
775
769
  staticWordsSum += staticWords;
776
770
  }
777
771
  const staticWords = anyDynamic ? 0 : staticWordsSum;
778
- 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}.`);
772
+ if (staticWords > DescriptorFormat.MAX_STATIC_WORDS) throw new CallciumError("STATIC_WORDS_TOO_LARGE", `Tuple static words ${staticWords} exceeds maximum ${DescriptorFormat.MAX_STATIC_WORDS}.`);
779
773
  const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
780
774
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
781
775
  buf[0] = TypeCode.TUPLE;
@@ -790,10 +784,6 @@ function tuple(fieldDescs) {
790
784
  }
791
785
  //#endregion
792
786
  //#region src/descriptor-coder.ts
793
- /**
794
- * Collect the positions of depth-0 commas in input[start..end).
795
- * Commas inside `()` or `[]` are skipped.
796
- */
797
787
  function commaPositions(input, start, end) {
798
788
  const positions = [];
799
789
  let depth = 0;
@@ -805,27 +795,21 @@ function commaPositions(input, start, end) {
805
795
  }
806
796
  return positions;
807
797
  }
808
- /**
809
- * Parse a decimal integer from input[start..end).
810
- * Returns -1 if the substring is empty or contains non-digit characters.
811
- */
798
+ const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
812
799
  function parseUint(input, start, end) {
813
- if (start >= end) return -1;
814
- if (end - start > 1 && input.charCodeAt(start) === 48) return -1;
800
+ if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", `Missing number at position ${start}`);
801
+ if (end - start > 1 && input.charCodeAt(start) === 48) throw new CallciumError("MALFORMED_TYPE_STRING", `Zero-padded number at position ${start}`);
815
802
  let value = 0;
816
803
  for (let i = start; i < end; i++) {
817
804
  const code = input.charCodeAt(i);
818
- if (code < 48 || code > 57) return -1;
805
+ if (code < 48 || code > 57) throw new CallciumError("MALFORMED_TYPE_STRING", `Non-digit character at position ${i}`);
819
806
  value = value * 10 + (code - 48);
820
807
  }
808
+ if (value > MAX_TYPE_STRING_NUMBER) throw new CallciumError("MALFORMED_TYPE_STRING", `Number at position ${start} is too wide`);
821
809
  return value;
822
810
  }
823
- /**
824
- * Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
825
- * `start` and the substring to parse ends before `end`.
826
- */
827
811
  function parseTuple(input, start, end) {
828
- if (input[start] !== "(") throw new CallciumError("INVALID_TYPE_STRING", `Expected '(' at position ${start}`);
812
+ if (input[start] !== "(") throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
829
813
  let depth = 0;
830
814
  let closePos = -1;
831
815
  for (let i = start; i < end; i++) if (input[i] === "(") depth++;
@@ -836,7 +820,8 @@ function parseTuple(input, start, end) {
836
820
  break;
837
821
  }
838
822
  }
839
- if (closePos === -1) throw new CallciumError("INVALID_TYPE_STRING", `Unmatched '(' at position ${start}`);
823
+ if (closePos === -1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
824
+ if (closePos !== end - 1) throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closePos + 1}`);
840
825
  const innerStart = start + 1;
841
826
  const innerEnd = closePos;
842
827
  const commas = commaPositions(input, innerStart, innerEnd);
@@ -847,10 +832,9 @@ function parseTuple(input, start, end) {
847
832
  segStart = comma + 1;
848
833
  }
849
834
  segments.push([segStart, innerEnd]);
850
- if (segments.length === 1 && segments[0][0] === segments[0][1]) throw new CallciumError("INVALID_TYPE_STRING", "Empty tuple is not allowed");
835
+ if (segments.length === 1 && segments[0][0] === segments[0][1]) return tuple([]);
851
836
  return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
852
837
  }
853
- /** Parse the base type (no array suffixes) from input[start..end). */
854
838
  function parseBaseType(input, start, end) {
855
839
  const segment = input.slice(start, end);
856
840
  if (segment === "address") return address();
@@ -858,63 +842,31 @@ function parseBaseType(input, start, end) {
858
842
  if (segment === "function") return function_();
859
843
  if (segment === "bytes") return bytes();
860
844
  if (segment === "string") return string_();
861
- if (segment === "uint256") return uint256();
862
- if (segment === "int256") return intN(256);
863
845
  if (segment.startsWith("(")) return parseTuple(input, start, end);
864
- if (segment.startsWith("uint")) {
865
- const bits = parseUint(input, start + 4, end);
866
- if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
867
- return uintN(bits);
868
- }
869
- if (segment.startsWith("int")) {
870
- const bits = parseUint(input, start + 3, end);
871
- if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
872
- return intN(bits);
873
- }
874
- if (segment.startsWith("bytes")) {
875
- const n = parseUint(input, start + 5, end);
876
- if (n === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
877
- return bytesN(n);
878
- }
846
+ if (segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
847
+ if (segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
848
+ if (segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
879
849
  throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
880
850
  }
881
- /**
882
- * Parse a type segment from input[start..end).
883
- *
884
- * Scans backward from end to collect all `[...]` suffixes, identifies the
885
- * base type extent, parses the base, then applies suffixes left-to-right.
886
- */
887
851
  function parseType(input, start, end) {
888
- if (start >= end) throw new CallciumError("INVALID_TYPE_STRING", "Empty type segment");
852
+ if (start >= end) throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
889
853
  const suffixes = [];
890
854
  let baseEnd = end;
891
855
  while (baseEnd > start && input[baseEnd - 1] === "]") {
892
856
  const closePos = baseEnd - 1;
893
857
  let openPos = closePos - 1;
894
858
  while (openPos > start && input[openPos] !== "[") openPos--;
895
- if (input[openPos] !== "[") throw new CallciumError("INVALID_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
859
+ if (input[openPos] !== "[") throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
896
860
  const innerStart = openPos + 1;
897
861
  const innerEnd = closePos;
898
- if (innerStart === innerEnd) suffixes.unshift(void 0);
899
- else {
900
- const length = parseUint(input, innerStart, innerEnd);
901
- if (length === -1) throw new CallciumError("INVALID_TYPE_STRING", `Invalid array length at position ${innerStart}`);
902
- suffixes.unshift(length);
903
- }
862
+ suffixes.unshift(innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
904
863
  baseEnd = openPos;
905
864
  }
865
+ if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
906
866
  let desc = parseBaseType(input, start, baseEnd);
907
867
  for (const length of suffixes) desc = array(desc, length);
908
868
  return desc;
909
869
  }
910
- /**
911
- * Encode a comma-separated list of ABI type strings into a binary descriptor.
912
- *
913
- * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
914
- * @returns Binary descriptor bytes starting with the version+paramCount header.
915
- * @throws {CallciumError} With code `INVALID_TYPE_STRING` for malformed input.
916
- * @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
917
- */
918
870
  function fromTypes(typesCsv) {
919
871
  if (typesCsv === "") return new Uint8Array([DescriptorFormat.VERSION, 0]);
920
872
  const commas = commaPositions(typesCsv, 0, typesCsv.length);
@@ -926,7 +878,7 @@ function fromTypes(typesCsv) {
926
878
  }
927
879
  segments.push([segStart, typesCsv.length]);
928
880
  const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
929
- if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("DESCRIPTOR_TOO_LARGE", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}.`);
881
+ if (paramDescs.length > DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DescriptorFormat.MAX_PARAMS}.`);
930
882
  const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
931
883
  const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
932
884
  result[0] = DescriptorFormat.VERSION;
@@ -938,7 +890,6 @@ function fromTypes(typesCsv) {
938
890
  }
939
891
  return result;
940
892
  }
941
- /** Reconstruct a Solidity type string from a descriptor node at the given offset. */
942
893
  function nodeToTypeString(desc, offset) {
943
894
  const typeCode = desc[offset];
944
895
  if (typeCode === TypeCode.TUPLE) {
@@ -958,15 +909,6 @@ function nodeToTypeString(desc, offset) {
958
909
  if (typeCode === TypeCode.DYNAMIC_ARRAY) return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[]`;
959
910
  return lookupTypeCode(typeCode).label;
960
911
  }
961
- /**
962
- * Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
963
- *
964
- * Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
965
- *
966
- * @param desc - Binary descriptor bytes (with version+paramCount header).
967
- * @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
968
- * @throws {CallciumError} If the descriptor is malformed.
969
- */
970
912
  function toTypes(desc) {
971
913
  const count = Descriptor.paramCount(desc);
972
914
  const types = [];
@@ -977,23 +919,15 @@ function toTypes(desc) {
977
919
  }
978
920
  return types.join(",");
979
921
  }
980
- /** Recursively parse a single descriptor node starting at offset. */
981
922
  function parseNode(data, offset, depth) {
982
923
  if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
983
924
  const code = data[offset];
984
925
  const info = classifyTypeCode(code);
985
926
  const metaOffset = offset + DescriptorFormat.TYPECODE_SIZE;
986
927
  if (info.typeClass === "elementary") return {
987
- node: {
988
- type: "elementary",
989
- typeCode: code,
990
- isDynamic: info.isDynamic,
991
- staticSize: info.isDynamic ? 0 : 32,
992
- span: {
993
- start: offset,
994
- end: metaOffset
995
- }
996
- },
928
+ typeCode: code,
929
+ isDynamic: info.isDynamic,
930
+ staticSize: info.isDynamic ? 0 : 32,
997
931
  next: metaOffset
998
932
  };
999
933
  if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
@@ -1003,7 +937,7 @@ function parseNode(data, offset, depth) {
1003
937
  const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
1004
938
  const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
1005
939
  const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
1006
- if (nodeLength < minHeader) throw new CallciumError("MALFORMED_HEADER", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
940
+ if (nodeLength < minHeader) throw new CallciumError("NODE_LENGTH_TOO_SMALL", `Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`, offset);
1007
941
  if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
1008
942
  const isDynamic = staticWords === 0;
1009
943
  const staticSize = isDynamic ? 0 : staticWords * 32;
@@ -1014,89 +948,54 @@ function parseNode(data, offset, depth) {
1014
948
  const fieldCount = readU16(data, fieldCountOffset);
1015
949
  if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
1016
950
  if (fieldCount > DescriptorFormat.MAX_TUPLE_FIELDS) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", `Tuple field count ${fieldCount} exceeds maximum ${DescriptorFormat.MAX_TUPLE_FIELDS}`, offset);
1017
- const fields = [];
1018
951
  let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
1019
- for (let i = 0; i < fieldCount; i++) {
1020
- const result = parseNode(data, cursor, depth + 1);
1021
- fields.push(result.node);
1022
- cursor = result.next;
1023
- }
952
+ for (let i = 0; i < fieldCount; i++) cursor = parseNode(data, cursor, depth + 1).next;
1024
953
  return {
1025
- node: {
1026
- type: "tuple",
1027
- typeCode: code,
1028
- isDynamic,
1029
- staticSize,
1030
- fields,
1031
- span: {
1032
- start: offset,
1033
- end: nodeEnd
1034
- }
1035
- },
954
+ typeCode: code,
955
+ isDynamic,
956
+ staticSize,
1036
957
  next: nodeEnd
1037
958
  };
1038
959
  }
1039
960
  if (info.typeClass === "staticArray") {
1040
- const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
1041
- const lengthOffset = elemResult.next;
961
+ const lengthOffset = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).next;
1042
962
  if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
1043
963
  const length = readU16(data, lengthOffset);
1044
964
  if (length === 0) throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
1045
965
  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);
1046
966
  return {
1047
- node: {
1048
- type: "staticArray",
1049
- typeCode: code,
1050
- isDynamic,
1051
- staticSize,
1052
- element: elemResult.node,
1053
- length,
1054
- span: {
1055
- start: offset,
1056
- end: nodeEnd
1057
- }
1058
- },
967
+ typeCode: code,
968
+ isDynamic,
969
+ staticSize,
1059
970
  next: nodeEnd
1060
971
  };
1061
972
  }
973
+ parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
1062
974
  return {
1063
- node: {
1064
- type: "dynamicArray",
1065
- typeCode: code,
1066
- isDynamic: true,
1067
- staticSize: 0,
1068
- element: parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).node,
1069
- span: {
1070
- start: offset,
1071
- end: nodeEnd
1072
- }
1073
- },
975
+ typeCode: code,
976
+ isDynamic: true,
977
+ staticSize: 0,
1074
978
  next: nodeEnd
1075
979
  };
1076
980
  }
1077
- /** Convert a zero-based param index to a BE16 hex path step. */
1078
981
  function indexToPath(index) {
1079
982
  return `0x${index.toString(16).padStart(4, "0")}`;
1080
983
  }
1081
- /** Decode a binary descriptor blob, returning the structural representation and AST. */
1082
984
  function decodeDescriptor(data) {
1083
985
  if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
1084
986
  const version = data[0];
1085
987
  if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
1086
988
  if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
1087
- const declaredCount = data[1];
1088
989
  const params = [];
1089
- const tree = [];
1090
990
  let cursor = DescriptorFormat.HEADER_SIZE;
1091
991
  while (cursor < data.length) {
1092
- if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("PARAM_COUNT_MISMATCH", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
1093
- const { node, next } = parseNode(data, cursor, 1);
1094
- tree.push(node);
992
+ if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
993
+ const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
1095
994
  params.push({
1096
995
  index: params.length,
1097
- typeCode: node.typeCode,
1098
- isDynamic: node.isDynamic,
1099
- staticSize: node.staticSize,
996
+ typeCode,
997
+ isDynamic,
998
+ staticSize,
1100
999
  path: indexToPath(params.length),
1101
1000
  span: {
1102
1001
  start: cursor,
@@ -1105,34 +1004,146 @@ function decodeDescriptor(data) {
1105
1004
  });
1106
1005
  cursor = next;
1107
1006
  }
1007
+ const declaredCount = data[1];
1108
1008
  if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
1109
- return {
1110
- descriptor: {
1111
- version,
1112
- params
1113
- },
1114
- tree
1115
- };
1009
+ return { descriptor: {
1010
+ version,
1011
+ params
1012
+ } };
1116
1013
  }
1117
- /** Encode and decode descriptors. */
1118
1014
  const DescriptorCoder = {
1119
1015
  fromTypes,
1120
1016
  toTypes
1121
1017
  };
1122
1018
  //#endregion
1019
+ //#region src/operators.ts
1020
+ function toBigInt(bytes, offset = 0) {
1021
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
1022
+ return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
1023
+ }
1024
+ function isSigned(typeCode) {
1025
+ return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1026
+ }
1027
+ function isLeftAligned(typeCode) {
1028
+ return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
1029
+ }
1030
+ function canonicalize(value, typeCode) {
1031
+ if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
1032
+ const bits = BigInt(typeCode * 8);
1033
+ return bits === 256n ? value : value & (1n << bits) - 1n;
1034
+ }
1035
+ if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
1036
+ const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
1037
+ return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
1038
+ }
1039
+ if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
1040
+ if (typeCode === TypeCode.BOOL) return value & 1n;
1041
+ if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
1042
+ if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
1043
+ const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
1044
+ if (n === 32) return value;
1045
+ const padBits = BigInt((32 - n) * 8);
1046
+ return value >> padBits << padBits;
1047
+ }
1048
+ return value;
1049
+ }
1050
+ function isLengthOp(opCode) {
1051
+ const base = opCode & ~Op.NOT;
1052
+ return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
1053
+ }
1054
+ function lengthToValueOp(base) {
1055
+ if (base === Op.LENGTH_EQ) return Op.EQ;
1056
+ if (base === Op.LENGTH_GT) return Op.GT;
1057
+ if (base === Op.LENGTH_LT) return Op.LT;
1058
+ if (base === Op.LENGTH_GTE) return Op.GTE;
1059
+ if (base === Op.LENGTH_LTE) return Op.LTE;
1060
+ return Op.BETWEEN;
1061
+ }
1062
+ function isLengthValidType(typeCode) {
1063
+ return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
1064
+ }
1065
+ function compareTyped(a, b, typeCode) {
1066
+ if (isSigned(typeCode)) {
1067
+ const sa = BigInt.asIntN(256, a);
1068
+ const sb = BigInt.asIntN(256, b);
1069
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
1070
+ }
1071
+ return a < b ? -1 : a > b ? 1 : 0;
1072
+ }
1073
+ function isIn(value, data) {
1074
+ const count = data.length / 32;
1075
+ let lo = 0;
1076
+ let hi = count - 1;
1077
+ while (lo <= hi) {
1078
+ const mid = lo + hi >>> 1;
1079
+ const elem = toBigInt(data, mid * 32);
1080
+ if (value === elem) return true;
1081
+ if (value < elem) hi = mid - 1;
1082
+ else lo = mid + 1;
1083
+ }
1084
+ return false;
1085
+ }
1086
+ function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1087
+ let base = opCode & ~Op.NOT;
1088
+ if (isLengthOp(base)) {
1089
+ value = BigInt(valueLength);
1090
+ typeCode = TypeCode.UINT_MAX;
1091
+ base = lengthToValueOp(base);
1092
+ }
1093
+ let result;
1094
+ switch (base) {
1095
+ case Op.EQ:
1096
+ result = value === toBigInt(operandData, 0);
1097
+ break;
1098
+ case Op.GT:
1099
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
1100
+ break;
1101
+ case Op.LT:
1102
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
1103
+ break;
1104
+ case Op.GTE:
1105
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
1106
+ break;
1107
+ case Op.LTE:
1108
+ result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
1109
+ break;
1110
+ case Op.BETWEEN: {
1111
+ const min = toBigInt(operandData, 0);
1112
+ const max = toBigInt(operandData, 32);
1113
+ result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
1114
+ break;
1115
+ }
1116
+ case Op.IN:
1117
+ result = isIn(value, operandData);
1118
+ break;
1119
+ case Op.BITMASK_ALL: {
1120
+ const mask = toBigInt(operandData, 0);
1121
+ result = (value & mask) === mask;
1122
+ break;
1123
+ }
1124
+ case Op.BITMASK_ANY: {
1125
+ const mask = toBigInt(operandData, 0);
1126
+ result = (value & mask) !== 0n;
1127
+ break;
1128
+ }
1129
+ case Op.BITMASK_NONE: {
1130
+ const mask = toBigInt(operandData, 0);
1131
+ result = (value & mask) === 0n;
1132
+ break;
1133
+ }
1134
+ default: throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
1135
+ }
1136
+ return (opCode & Op.NOT) !== 0 ? !result : result;
1137
+ }
1138
+ //#endregion
1123
1139
  //#region src/policy-coder.ts
1124
- /**
1125
- * Parse a BE16-encoded hex path into an array of step values.
1126
- * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
1127
- * @returns Array of numeric step values.
1128
- */
1129
1140
  function parsePathSteps(path) {
1130
- const body = path.slice(2);
1141
+ const bytes = hexToBytes(path);
1142
+ if (bytes.length % PolicyFormat.PATH_STEP_SIZE !== 0) throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
1131
1143
  const steps = [];
1132
- for (let i = 0; i < body.length; i += 4) steps.push(parseInt(body.slice(i, i + 4), 16));
1144
+ for (let offset = 0; offset < bytes.length; offset += PolicyFormat.PATH_STEP_SIZE) steps.push(readU16(bytes, offset));
1133
1145
  return steps;
1134
1146
  }
1135
- /** Wrap a value with its byte span for positional tracking. */
1136
1147
  function field(value, start, end) {
1137
1148
  return {
1138
1149
  value,
@@ -1142,7 +1153,60 @@ function field(value, start, end) {
1142
1153
  }
1143
1154
  };
1144
1155
  }
1145
- /** Decode a policy blob, returning the structural representation and descriptor AST. */
1156
+ function resolveHintSize(data, hintStart, ruleEnd, ruleOffset) {
1157
+ const overrun = () => {
1158
+ throw new CallciumError("RULE_FIELD_OUT_OF_BOUNDS", "Rule is too small to hold a hint block", ruleOffset);
1159
+ };
1160
+ if (hintStart + PolicyFormat.HINT_HEADER_SIZE > ruleEnd) overrun();
1161
+ const header = data[hintStart];
1162
+ const hopsEnd = hintStart + PolicyFormat.HINT_HEADER_SIZE + (header & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
1163
+ let hintSize = hopsEnd - hintStart + PolicyFormat.HINT_TARGET_SIZE;
1164
+ if (header >> PolicyFormat.HINT_KIND_SHIFT !== PolicyFormat.HINT_KIND_NONE) {
1165
+ const suffixHeaderOffset = hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE;
1166
+ if (suffixHeaderOffset >= ruleEnd) overrun();
1167
+ const suffixHeader = data[suffixHeaderOffset];
1168
+ hintSize += PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE + (suffixHeader & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
1169
+ }
1170
+ if (hintStart + hintSize > ruleEnd) overrun();
1171
+ return hintSize;
1172
+ }
1173
+ function malformedHint(detail, ruleOffset) {
1174
+ throw new CallciumError("MALFORMED_HINT", detail, ruleOffset);
1175
+ }
1176
+ function validateHops(data, start, end, ruleOffset) {
1177
+ for (let offset = start; offset < end; offset += PolicyFormat.HINT_HOP_SIZE) {
1178
+ const index = readU16(data, offset + PolicyFormat.HINT_HOP_INDEX_OFFSET);
1179
+ if (index === PolicyFormat.HINT_INDEX_RESERVED) malformedHint("Hop index is a reserved value", ruleOffset);
1180
+ const meta = readU16(data, offset + PolicyFormat.HINT_HOP_META_OFFSET);
1181
+ if (index === PolicyFormat.HINT_NO_INDEX) {
1182
+ if (meta !== 0) malformedHint("Plain hop carries a meta word", ruleOffset);
1183
+ } else {
1184
+ if (readU32(data, offset) !== 0) malformedHint("Element hop carries a delta", ruleOffset);
1185
+ if ((meta & PolicyFormat.HINT_META_RESERVED_MASK) !== 0) malformedHint("Hop meta carries reserved bits", ruleOffset);
1186
+ }
1187
+ }
1188
+ }
1189
+ function validateHint(data, hintStart, hintSize, ruleOffset) {
1190
+ const header = data[hintStart];
1191
+ const kind = header >> PolicyFormat.HINT_KIND_SHIFT;
1192
+ if (kind > PolicyFormat.HINT_KIND_MAX) malformedHint("Header kind is a reserved value", ruleOffset);
1193
+ const hopsStart = hintStart + PolicyFormat.HINT_HEADER_SIZE;
1194
+ const hopsEnd = hopsStart + (header & PolicyFormat.HINT_HOP_COUNT_MASK) * PolicyFormat.HINT_HOP_SIZE;
1195
+ validateHops(data, hopsStart, hopsEnd, ruleOffset);
1196
+ const targetOffset = hintStart + hintSize - PolicyFormat.HINT_TARGET_SIZE;
1197
+ if (kind !== PolicyFormat.HINT_KIND_NONE) {
1198
+ if ((readU16(data, hopsEnd + PolicyFormat.HINT_FRAME_META_OFFSET) & PolicyFormat.HINT_META_RESERVED_MASK) !== 0) malformedHint("Frame meta carries reserved bits", ruleOffset);
1199
+ if ((data[hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE] & PolicyFormat.HINT_SUFFIX_RESERVED_MASK) !== 0) malformedHint("Suffix header carries reserved bits", ruleOffset);
1200
+ validateHops(data, hopsEnd + PolicyFormat.HINT_FRAME_PREFIX_SIZE + PolicyFormat.HINT_HEADER_SIZE, targetOffset, ruleOffset);
1201
+ }
1202
+ const targetMeta = readU16(data, targetOffset + PolicyFormat.HINT_TARGET_META_OFFSET);
1203
+ const targetTypeCode = data[targetOffset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET];
1204
+ if (!(targetTypeCode === TypeCode.DYNAMIC_ARRAY ? (targetMeta & PolicyFormat.HINT_META_RESERVED_MASK) === 0 : targetMeta === 0)) malformedHint("Target meta carries reserved or unused state", ruleOffset);
1205
+ if (!isAddressableTarget(targetTypeCode)) malformedHint("Target type code addresses no value", ruleOffset);
1206
+ }
1207
+ function hintTargetTypeCode(data, hintStart, hintSize) {
1208
+ return data[hintStart + hintSize - PolicyFormat.HINT_TARGET_SIZE + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET];
1209
+ }
1146
1210
  function decodePolicy(blob) {
1147
1211
  const data = hexToBytes(blob);
1148
1212
  if (data.length < PolicyFormat.DESC_OFFSET + 1) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
@@ -1150,10 +1214,10 @@ function decodePolicy(blob) {
1150
1214
  const version = headerByte & PolicyFormat.VERSION_MASK;
1151
1215
  if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
1152
1216
  if ((headerByte & PolicyFormat.RESERVED_MASK) !== 0) throw new CallciumError("MALFORMED_HEADER", "Reserved header bits must be zero");
1153
- const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
1154
1217
  const selectorStart = PolicyFormat.SELECTOR_OFFSET;
1155
1218
  const selectorEnd = selectorStart + PolicyFormat.SELECTOR_SIZE;
1156
1219
  const selectorHex = toHex(data, selectorStart, selectorEnd);
1220
+ const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
1157
1221
  if (isSelectorless && selectorHex !== "0x00000000") throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
1158
1222
  const descLengthStart = PolicyFormat.DESC_LENGTH_OFFSET;
1159
1223
  const descLengthValue = readU16(data, descLengthStart);
@@ -1162,7 +1226,7 @@ function decodePolicy(blob) {
1162
1226
  const descEnd = descStart + descLengthValue;
1163
1227
  const groupCountOffset = descEnd;
1164
1228
  if (data.length < groupCountOffset + PolicyFormat.GROUP_COUNT_SIZE) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
1165
- const { descriptor: desc, tree } = decodeDescriptor(data.subarray(descStart, descEnd));
1229
+ const { descriptor: desc } = decodeDescriptor(data.subarray(descStart, descEnd));
1166
1230
  const params = desc.params.map((param) => ({
1167
1231
  ...param,
1168
1232
  span: {
@@ -1179,55 +1243,66 @@ function decodePolicy(blob) {
1179
1243
  const groups = [];
1180
1244
  for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
1181
1245
  if (offset + PolicyFormat.GROUP_HEADER_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
1182
- const ruleCountStart = offset;
1183
- const ruleCountValue = readU16(data, ruleCountStart);
1184
1246
  const groupSizeStart = offset + PolicyFormat.GROUP_RULECOUNT_SIZE;
1185
1247
  const groupSizeValue = readU32(data, groupSizeStart);
1186
1248
  const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
1187
1249
  const groupEnd = groupBodyStart + groupSizeValue;
1250
+ const ruleCountStart = offset;
1251
+ const ruleCountValue = readU16(data, ruleCountStart);
1188
1252
  if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
1189
- if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("GROUP_SIZE_MISMATCH", "Declared group size is too small for its rule count", offset);
1253
+ if (groupSizeValue < ruleCountValue * PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
1190
1254
  if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
1191
1255
  const rules = [];
1192
1256
  let ruleOffset = groupBodyStart;
1193
1257
  for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
1194
1258
  if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
1195
1259
  const ruleSizeValue = readU16(data, ruleOffset);
1196
- if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_SIZE_MISMATCH", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
1260
+ if (ruleSizeValue < PolicyFormat.RULE_MIN_SIZE) throw new CallciumError("RULE_TOO_SMALL", `Rule size ${ruleSizeValue} is below minimum ${PolicyFormat.RULE_MIN_SIZE}`, ruleOffset);
1197
1261
  if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
1198
1262
  const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
1199
1263
  const scopeValue = data[scopeOffset];
1200
1264
  if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
1201
1265
  const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
1202
1266
  const depthValue = data[depthOffset];
1203
- if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
1204
- if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
1205
- if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
1206
1267
  const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
1207
1268
  const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
1208
- const pathHex = toHex(data, pathStart, pathStart + pathLength);
1209
- if (scopeValue === Scope.CONTEXT) {
1210
- const contextPropertyId = readU16(data, pathStart);
1211
- if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Context rule references undefined context property 0x${contextPropertyId.toString(16).padStart(4, "0")}`, ruleOffset);
1269
+ const ruleEnd = ruleOffset + ruleSizeValue;
1270
+ const hintStart = pathStart + pathLength;
1271
+ let hintSize = 0;
1272
+ if (scopeValue === Scope.CALLDATA) {
1273
+ hintSize = resolveHintSize(data, hintStart, ruleEnd, ruleOffset);
1274
+ validateHint(data, hintStart, hintSize, ruleOffset);
1212
1275
  }
1213
- const opCodeOffset = pathStart + pathLength;
1214
- const opCodeValue = data[opCodeOffset];
1276
+ const opCodeOffset = hintStart + hintSize;
1215
1277
  const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
1216
1278
  const dataLengthValue = readU16(data, dataLengthOffset);
1217
- const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
1218
- const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + dataLengthValue;
1279
+ const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + hintSize + dataLengthValue;
1219
1280
  if (ruleSizeValue !== expectedRuleSize) throw new CallciumError("RULE_SIZE_MISMATCH", `Declared rule size ${ruleSizeValue} does not match computed size ${expectedRuleSize}`, ruleOffset);
1281
+ const opCodeValue = data[opCodeOffset];
1220
1282
  const opBase = opCodeValue & ~Op.NOT;
1221
- if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("INVALID_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
1283
+ if (opBase === 0 || !isValidOperatorData(opBase, dataLengthValue)) throw new CallciumError("UNKNOWN_OPERATOR", "Unrecognized or malformed operator", ruleOffset);
1284
+ if (hintSize !== 0) {
1285
+ if (isLengthValidType(hintTargetTypeCode(data, hintStart, hintSize)) !== isLengthOp(opBase)) throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator family does not match the type the target declares", ruleOffset);
1286
+ }
1287
+ const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
1222
1288
  if (opBase === Op.IN) {
1223
1289
  const wordCount = dataLengthValue / 32;
1224
1290
  for (let word = 1; word < wordCount; word++) if (compareBytes(data.subarray(dataStart + (word - 1) * 32, dataStart + word * 32), data.subarray(dataStart + word * 32, dataStart + (word + 1) * 32)) >= 0) throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
1225
1291
  }
1292
+ if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
1293
+ if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
1294
+ if (scopeValue === Scope.CONTEXT) {
1295
+ if (depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
1296
+ const contextPropertyId = readU16(data, pathStart);
1297
+ if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Context rule references undefined context property 0x${contextPropertyId.toString(16).padStart(4, "0")}`, ruleOffset);
1298
+ }
1299
+ const pathHex = toHex(data, pathStart, pathStart + pathLength);
1226
1300
  rules.push({
1227
1301
  ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
1228
1302
  scope: field(scopeValue, scopeOffset, scopeOffset + 1),
1229
1303
  pathDepth: field(depthValue, depthOffset, depthOffset + 1),
1230
1304
  path: field(pathHex, pathStart, pathStart + pathLength),
1305
+ ...hintSize > 0 && { hint: field(toHex(data, hintStart, hintStart + hintSize), hintStart, hintStart + hintSize) },
1231
1306
  opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
1232
1307
  dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
1233
1308
  data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
@@ -1277,25 +1352,30 @@ function decodePolicy(blob) {
1277
1352
  version,
1278
1353
  isSelectorless
1279
1354
  },
1280
- tree
1355
+ data
1281
1356
  };
1282
1357
  }
1283
- /** Flatten a Constraint into one Rule per operator. */
1284
- function flattenConstraint(constraint) {
1358
+ function flattenConstraint(constraint, desc) {
1359
+ const operators = constraint.operators.map(hexToBytes);
1360
+ for (const operator of operators) if (operator.length < 1) throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode).");
1285
1361
  const path = hexToBytes(constraint.path);
1286
- return constraint.operators.map((op) => ({
1362
+ if (path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
1363
+ if ((path.length & 1) !== 0) throw new CallciumError("MALFORMED_PATH", "Path byte length must be even.");
1364
+ const depth = path.length / 2;
1365
+ if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
1366
+ const hint = constraint.scope === Scope.CALLDATA ? Descriptor.compileHint(desc, parsePathSteps(constraint.path)) : new Uint8Array(0);
1367
+ return operators.map((operator) => ({
1287
1368
  scope: constraint.scope,
1288
1369
  path,
1289
- operator: hexToBytes(op)
1370
+ operator,
1371
+ hint
1290
1372
  }));
1291
1373
  }
1292
- /** Compare two byte arrays lexicographically. */
1293
1374
  function compareBytes(a, b) {
1294
1375
  const length = Math.min(a.length, b.length);
1295
1376
  for (let i = 0; i < length; i++) if (a[i] !== b[i]) return a[i] - b[i];
1296
1377
  return a.length - b.length;
1297
1378
  }
1298
- /** Sort rules by (scope, pathDepth, pathBytes, operatorBytes). */
1299
1379
  function sortRules(rules) {
1300
1380
  rules.sort((a, b) => {
1301
1381
  if (a.scope !== b.scope) return a.scope - b.scope;
@@ -1307,30 +1387,24 @@ function sortRules(rules) {
1307
1387
  return compareBytes(a.operator, b.operator);
1308
1388
  });
1309
1389
  }
1310
- /** Serialize a single rule to its wire format bytes. */
1311
1390
  function encodeRule(rule) {
1312
- if (rule.path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
1313
- if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
1314
- if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
1315
1391
  const depth = rule.path.length / 2;
1316
- if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
1317
1392
  if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
1318
- const opCode = rule.operator[0];
1319
1393
  const data = rule.operator.subarray(1);
1320
- const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + data.length;
1394
+ const ruleSize = PolicyFormat.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
1321
1395
  if (ruleSize > 65535) throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
1322
1396
  const buf = new Uint8Array(ruleSize);
1323
1397
  writeBE16(buf, 0, ruleSize);
1324
1398
  buf[2] = rule.scope;
1325
1399
  buf[3] = depth;
1326
1400
  buf.set(rule.path, 4);
1327
- const opOffset = 4 + rule.path.length;
1328
- buf[opOffset] = opCode;
1401
+ buf.set(rule.hint, 4 + rule.path.length);
1402
+ const opOffset = 4 + rule.path.length + rule.hint.length;
1403
+ buf[opOffset] = rule.operator[0];
1329
1404
  writeBE16(buf, opOffset + 1, data.length);
1330
1405
  buf.set(data, opOffset + 3);
1331
1406
  return buf;
1332
1407
  }
1333
- /** Serialize all rules in a group to a single byte array. */
1334
1408
  function encodeGroupRules(rules) {
1335
1409
  const parts = rules.map(encodeRule);
1336
1410
  const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
@@ -1342,40 +1416,35 @@ function encodeGroupRules(rules) {
1342
1416
  }
1343
1417
  return buf;
1344
1418
  }
1345
- /** Build a single operator hex string from a decoded rule's opCode and data. */
1346
1419
  function buildOperatorHex(rule) {
1347
1420
  return `0x${rule.opCode.value.toString(16).padStart(2, "0")}${rule.data.value.slice(2)}`;
1348
1421
  }
1349
- /**
1350
- * Encode a PolicyData structure into the canonical binary format.
1351
- * @param data - The policy data to encode.
1352
- * @returns The encoded policy as a 0x-prefixed hex string.
1353
- */
1354
1422
  function encode(data) {
1423
+ const descBytes = hexToBytes(data.descriptor);
1424
+ if (data.groups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
1425
+ if (data.groups.length > 255) throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${data.groups.length} exceeds maximum 255`);
1355
1426
  const sortedGroups = data.groups.map((group) => {
1356
- const rules = group.flatMap(flattenConstraint);
1427
+ const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
1357
1428
  sortRules(rules);
1358
1429
  return rules;
1359
1430
  });
1360
- if (sortedGroups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
1361
- if (sortedGroups.length > 255) throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${sortedGroups.length} exceeds maximum 255`);
1362
- const groupsWithHash = sortedGroups.map((rules, groupIndex) => {
1431
+ if (descBytes.length > 65535) throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
1432
+ const encodedGroups = sortedGroups.map((rules, groupIndex) => {
1363
1433
  if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
1364
1434
  if (rules.length > 65535) throw new CallciumError("RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`);
1365
- const wireBytes = encodeGroupRules(rules);
1366
1435
  return {
1367
- wireBytes,
1368
- hash: keccak_256(wireBytes),
1436
+ wireBytes: encodeGroupRules(rules),
1369
1437
  ruleCount: rules.length
1370
1438
  };
1371
1439
  });
1372
- groupsWithHash.sort((a, b) => compareBytes(a.hash, b.hash));
1373
- const descBytes = hexToBytes(data.descriptor);
1374
- if (descBytes.length > 65535) throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
1440
+ if (encodedGroups.length > 1) {
1441
+ const hashes = new Map(encodedGroups.map((group) => [group, keccak_256(group.wireBytes)]));
1442
+ encodedGroups.sort((a, b) => compareBytes(hashes.get(a), hashes.get(b)));
1443
+ }
1375
1444
  const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
1376
1445
  const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
1377
1446
  let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
1378
- for (const g of groupsWithHash) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
1447
+ for (const g of encodedGroups) totalSize += PolicyFormat.GROUP_HEADER_SIZE + g.wireBytes.length;
1379
1448
  const out = new Uint8Array(totalSize);
1380
1449
  let offset = 0;
1381
1450
  out[offset++] = headerByte;
@@ -1385,226 +1454,61 @@ function encode(data) {
1385
1454
  offset += PolicyFormat.DESC_LENGTH_SIZE;
1386
1455
  out.set(descBytes, offset);
1387
1456
  offset += descBytes.length;
1388
- out[offset++] = groupsWithHash.length;
1389
- for (const g of groupsWithHash) {
1457
+ out[offset++] = encodedGroups.length;
1458
+ for (const g of encodedGroups) {
1390
1459
  writeBE16(out, offset, g.ruleCount);
1391
1460
  offset += PolicyFormat.GROUP_RULECOUNT_SIZE;
1392
- out[offset++] = g.wireBytes.length >>> 24 & 255;
1393
- out[offset++] = g.wireBytes.length >>> 16 & 255;
1394
- out[offset++] = g.wireBytes.length >>> 8 & 255;
1395
- out[offset++] = g.wireBytes.length & 255;
1461
+ writeBE32(out, offset, g.wireBytes.length);
1462
+ offset += PolicyFormat.GROUP_SIZE_SIZE;
1396
1463
  out.set(g.wireBytes, offset);
1397
1464
  offset += g.wireBytes.length;
1398
1465
  }
1399
1466
  return bytesToHex(out);
1400
1467
  }
1401
- /**
1402
- * Decode a binary policy blob into a PolicyData structure.
1403
- * @param blob - Binary policy as 0x-prefixed hex string.
1404
- * @returns The decoded policy data with constraints grouped by scope and path.
1405
- * @throws {CallciumError} If the blob is structurally malformed.
1406
- */
1407
1468
  function decode(blob) {
1408
1469
  const { policy } = decodePolicy(blob);
1409
1470
  const groups = policy.groups.map((group) => {
1410
1471
  const constraintMap = /* @__PURE__ */ new Map();
1411
1472
  const constraintOrder = [];
1412
1473
  for (const rule of group.rules) {
1413
- const key = `${rule.scope.value}:${rule.path.value}`;
1414
- const opHex = buildOperatorHex(rule);
1415
- const existing = constraintMap.get(key);
1416
- if (existing !== void 0) existing.operators.push(opHex);
1417
- else {
1418
- const constraint = {
1419
- scope: rule.scope.value,
1420
- path: rule.path.value,
1421
- operators: [opHex],
1422
- span: rule.span
1423
- };
1424
- constraintMap.set(key, constraint);
1425
- constraintOrder.push(key);
1426
- }
1427
- }
1428
- return constraintOrder.map((k) => constraintMap.get(k));
1429
- });
1430
- return {
1431
- isSelectorless: policy.isSelectorless,
1432
- selector: policy.selector.value,
1433
- descriptor: policy.descriptor.raw,
1434
- groups,
1435
- span: policy.span
1436
- };
1437
- }
1438
- /**
1439
- * Inspect a binary policy blob, returning the structural representation with full byte-level spans.
1440
- * @param blob - Binary policy as 0x-prefixed hex string.
1441
- * @returns The inspected policy with per-field spans for every structural element.
1442
- * @throws {CallciumError} If the blob is structurally malformed.
1443
- */
1444
- function inspect(blob) {
1445
- return decodePolicy(blob).policy;
1446
- }
1447
- /** Encode, decode, and inspect policies in the canonical binary format. */
1448
- const PolicyCoder = {
1449
- encode,
1450
- decode,
1451
- inspect
1452
- };
1453
- //#endregion
1454
- //#region src/operators.ts
1455
- /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
1456
- function toBigInt(bytes, offset = 0) {
1457
- const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
1458
- return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
1459
- }
1460
- /** Return true when the type code represents a signed integer (int8 through int256). */
1461
- function isSigned(typeCode) {
1462
- return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1463
- }
1464
- /**
1465
- * Return true when the type code represents a left-aligned type (fixed bytes or function),
1466
- * whose value occupies the high bytes of the word with zero padding below.
1467
- */
1468
- function isLeftAligned(typeCode) {
1469
- return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
1470
- }
1471
- /**
1472
- * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
1473
- * A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
1474
- * unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
1475
- * comparison use the canonical ABI value rather than the raw bytes.
1476
- * @param value - The raw 256-bit word loaded from calldata.
1477
- * @param typeCode - The declared type code of the value.
1478
- * @returns The canonicalized 256-bit value.
1479
- */
1480
- function canonicalize(value, typeCode) {
1481
- if (typeCode <= TypeCode.UINT_MAX) {
1482
- const bits = BigInt((typeCode + 1) * 8);
1483
- return bits === 256n ? value : value & (1n << bits) - 1n;
1484
- }
1485
- if (typeCode <= TypeCode.INT_MAX) {
1486
- const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
1487
- return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
1488
- }
1489
- if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
1490
- if (typeCode === TypeCode.BOOL) return value & 1n;
1491
- if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
1492
- if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
1493
- const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
1494
- if (n === 32) return value;
1495
- const padBits = BigInt((32 - n) * 8);
1496
- return value >> padBits << padBits;
1497
- }
1498
- return value;
1499
- }
1500
- /** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
1501
- function isLengthOp(opCode) {
1502
- const base = opCode & ~Op.NOT;
1503
- return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
1504
- }
1505
- /** Map a LENGTH_* opcode base to its core value-comparison twin. */
1506
- function lengthToValueOp(base) {
1507
- if (base === Op.LENGTH_EQ) return Op.EQ;
1508
- if (base === Op.LENGTH_GT) return Op.GT;
1509
- if (base === Op.LENGTH_LT) return Op.LT;
1510
- if (base === Op.LENGTH_GTE) return Op.GTE;
1511
- if (base === Op.LENGTH_LTE) return Op.LTE;
1512
- return Op.BETWEEN;
1513
- }
1514
- /** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
1515
- function isLengthValidType(typeCode) {
1516
- return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
1517
- }
1518
- /** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
1519
- function compareTyped(a, b, typeCode) {
1520
- if (isSigned(typeCode)) {
1521
- const sa = BigInt.asIntN(256, a);
1522
- const sb = BigInt.asIntN(256, b);
1523
- return sa < sb ? -1 : sa > sb ? 1 : 0;
1524
- }
1525
- return a < b ? -1 : a > b ? 1 : 0;
1526
- }
1527
- /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
1528
- function isIn(value, data) {
1529
- const count = data.length / 32;
1530
- let lo = 0;
1531
- let hi = count - 1;
1532
- while (lo <= hi) {
1533
- const mid = lo + hi >>> 1;
1534
- const elem = toBigInt(data, mid * 32);
1535
- if (value === elem) return true;
1536
- if (value < elem) hi = mid - 1;
1537
- else lo = mid + 1;
1538
- }
1539
- return false;
1540
- }
1541
- /**
1542
- * Apply a single operator against a value and operand data.
1543
- * Handles the NOT flag internally by inverting the base operator result.
1544
- * @param opCode - Operator code, potentially OR'd with the NOT flag.
1545
- * @param value - The 256-bit value to test (ignored for LENGTH_* operators).
1546
- * @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
1547
- * @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
1548
- * @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
1549
- * @returns Whether the value satisfies the operator.
1550
- */
1551
- function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1552
- const negate = (opCode & Op.NOT) !== 0;
1553
- let base = opCode & ~Op.NOT;
1554
- if (isLengthOp(base)) {
1555
- value = BigInt(valueLength);
1556
- typeCode = TypeCode.UINT_MAX;
1557
- base = lengthToValueOp(base);
1558
- }
1559
- let result;
1560
- switch (base) {
1561
- case Op.EQ:
1562
- result = value === toBigInt(operandData, 0);
1563
- break;
1564
- case Op.GT:
1565
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
1566
- break;
1567
- case Op.LT:
1568
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
1569
- break;
1570
- case Op.GTE:
1571
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
1572
- break;
1573
- case Op.LTE:
1574
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
1575
- break;
1576
- case Op.BETWEEN: {
1577
- const min = toBigInt(operandData, 0);
1578
- const max = toBigInt(operandData, 32);
1579
- result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
1580
- break;
1581
- }
1582
- case Op.IN:
1583
- result = isIn(value, operandData);
1584
- break;
1585
- case Op.BITMASK_ALL: {
1586
- const mask = toBigInt(operandData, 0);
1587
- result = (value & mask) === mask;
1588
- break;
1589
- }
1590
- case Op.BITMASK_ANY: {
1591
- const mask = toBigInt(operandData, 0);
1592
- result = (value & mask) !== 0n;
1593
- break;
1594
- }
1595
- case Op.BITMASK_NONE: {
1596
- const mask = toBigInt(operandData, 0);
1597
- result = (value & mask) === 0n;
1598
- break;
1474
+ const hintHex = rule.hint?.value;
1475
+ const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
1476
+ const opHex = buildOperatorHex(rule);
1477
+ const existing = constraintMap.get(key);
1478
+ if (existing !== void 0) existing.operators.push(opHex);
1479
+ else {
1480
+ const constraint = {
1481
+ scope: rule.scope.value,
1482
+ path: rule.path.value,
1483
+ operators: [opHex],
1484
+ ...hintHex !== void 0 && { hint: hintHex },
1485
+ span: rule.span
1486
+ };
1487
+ constraintMap.set(key, constraint);
1488
+ constraintOrder.push(key);
1489
+ }
1599
1490
  }
1600
- default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
1601
- }
1602
- return negate ? !result : result;
1491
+ return constraintOrder.map((k) => constraintMap.get(k));
1492
+ });
1493
+ return {
1494
+ isSelectorless: policy.isSelectorless,
1495
+ selector: policy.selector.value,
1496
+ descriptor: policy.descriptor.raw,
1497
+ groups,
1498
+ span: policy.span
1499
+ };
1500
+ }
1501
+ function inspect(blob) {
1502
+ return decodePolicy(blob).policy;
1603
1503
  }
1504
+ const PolicyCoder = {
1505
+ encode,
1506
+ decode,
1507
+ inspect
1508
+ };
1604
1509
  //#endregion
1605
1510
  //#region src/validation-issue.ts
1606
1511
  const ZERO = `0x${"0".repeat(64)}`;
1607
- /** Create an issue from operator compatibility results. */
1608
1512
  function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1609
1513
  return {
1610
1514
  severity: "error",
@@ -1617,7 +1521,6 @@ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1617
1521
  message
1618
1522
  };
1619
1523
  }
1620
- /** Issue for a calldata path that does not navigate the descriptor. */
1621
1524
  function unnavigablePath(groupIndex, constraintIndex) {
1622
1525
  return {
1623
1526
  severity: "error",
@@ -1630,7 +1533,30 @@ function unnavigablePath(groupIndex, constraintIndex) {
1630
1533
  message: "Path does not navigate the descriptor"
1631
1534
  };
1632
1535
  }
1633
- /** Issue for an operand that is not canonically encoded for the declared type. */
1536
+ function nestedQuantifier(groupIndex, constraintIndex) {
1537
+ return {
1538
+ severity: "error",
1539
+ category: "typeMismatch",
1540
+ groupIndex,
1541
+ constraintIndex,
1542
+ code: "NESTED_QUANTIFIER",
1543
+ value1: ZERO,
1544
+ value2: ZERO,
1545
+ message: "Path applies more than one quantifier"
1546
+ };
1547
+ }
1548
+ function hintMismatch(groupIndex, constraintIndex) {
1549
+ return {
1550
+ severity: "error",
1551
+ category: "typeMismatch",
1552
+ groupIndex,
1553
+ constraintIndex,
1554
+ code: "HINT_MISMATCH",
1555
+ value1: ZERO,
1556
+ value2: ZERO,
1557
+ message: "Hint does not match the path compiled against the descriptor"
1558
+ };
1559
+ }
1634
1560
  function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
1635
1561
  return {
1636
1562
  severity: "error",
@@ -1643,7 +1569,6 @@ function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
1643
1569
  message: "Operand is not the canonical encoding for the declared type"
1644
1570
  };
1645
1571
  }
1646
- /** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
1647
1572
  function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
1648
1573
  return {
1649
1574
  severity: "error",
@@ -1656,7 +1581,6 @@ function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
1656
1581
  message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
1657
1582
  };
1658
1583
  }
1659
- /** Multiple eq()/lengthEq() operators with different values. */
1660
1584
  function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
1661
1585
  return {
1662
1586
  severity: "error",
@@ -1669,7 +1593,6 @@ function conflictingEquality(isLength, groupIndex, constraintIndex, existing, ne
1669
1593
  message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
1670
1594
  };
1671
1595
  }
1672
- /** Value/length is outside the physical range of its type. */
1673
1596
  function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
1674
1597
  return {
1675
1598
  severity: "error",
@@ -1682,7 +1605,6 @@ function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
1682
1605
  message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
1683
1606
  };
1684
1607
  }
1685
- /** gt()/lengthGt() on the maximum is impossible. */
1686
1608
  function impossibleGt(isLength, groupIndex, constraintIndex, value) {
1687
1609
  return {
1688
1610
  severity: "error",
@@ -1695,7 +1617,6 @@ function impossibleGt(isLength, groupIndex, constraintIndex, value) {
1695
1617
  message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
1696
1618
  };
1697
1619
  }
1698
- /** lt()/lengthLt() on the minimum is impossible. */
1699
1620
  function impossibleLt(isLength, groupIndex, constraintIndex, value) {
1700
1621
  return {
1701
1622
  severity: "error",
@@ -1708,7 +1629,6 @@ function impossibleLt(isLength, groupIndex, constraintIndex, value) {
1708
1629
  message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
1709
1630
  };
1710
1631
  }
1711
- /** eq()/lengthEq() value is excluded by a bound. */
1712
1632
  function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
1713
1633
  return {
1714
1634
  severity: "error",
@@ -1721,7 +1641,6 @@ function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound)
1721
1641
  message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
1722
1642
  };
1723
1643
  }
1724
- /** Lower bound/length exceeds the upper bound. */
1725
1644
  function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
1726
1645
  return {
1727
1646
  severity: "error",
@@ -1734,7 +1653,6 @@ function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
1734
1653
  message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
1735
1654
  };
1736
1655
  }
1737
- /** Set operation excludes existing eq() value. */
1738
1656
  function setExcludesEquality(groupIndex, constraintIndex, value) {
1739
1657
  return {
1740
1658
  severity: "error",
@@ -1747,7 +1665,6 @@ function setExcludesEquality(groupIndex, constraintIndex, value) {
1747
1665
  message: "Set operation excludes existing eq() value"
1748
1666
  };
1749
1667
  }
1750
- /** Multiple isIn() sets have no intersection. */
1751
1668
  function emptySetIntersection(groupIndex, constraintIndex) {
1752
1669
  return {
1753
1670
  severity: "error",
@@ -1760,7 +1677,6 @@ function emptySetIntersection(groupIndex, constraintIndex) {
1760
1677
  message: "Multiple isIn() sets have no intersection"
1761
1678
  };
1762
1679
  }
1763
- /** All values in isIn() set are excluded by neq/notIn. */
1764
1680
  function setFullyExcluded(groupIndex, constraintIndex) {
1765
1681
  return {
1766
1682
  severity: "error",
@@ -1773,7 +1689,6 @@ function setFullyExcluded(groupIndex, constraintIndex) {
1773
1689
  message: "All values in isIn() set are excluded by neq/notIn"
1774
1690
  };
1775
1691
  }
1776
- /** Bitmask operators conflict. */
1777
1692
  function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1778
1693
  return {
1779
1694
  severity: "error",
@@ -1786,7 +1701,6 @@ function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1786
1701
  message: "Bitmask operators conflict"
1787
1702
  };
1788
1703
  }
1789
- /** bitmaskAny is impossible because all bits are forbidden. */
1790
1704
  function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1791
1705
  return {
1792
1706
  severity: "error",
@@ -1799,7 +1713,6 @@ function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1799
1713
  message: "bitmaskAny is impossible because all bits are forbidden"
1800
1714
  };
1801
1715
  }
1802
- /** isIn/notIn set is not strictly sorted and deduplicated. */
1803
1716
  function unsortedInSet(groupIndex, constraintIndex) {
1804
1717
  return {
1805
1718
  severity: "error",
@@ -1812,7 +1725,6 @@ function unsortedInSet(groupIndex, constraintIndex) {
1812
1725
  message: "isIn/notIn set is not strictly sorted and deduplicated."
1813
1726
  };
1814
1727
  }
1815
- /** Group contains zero constraints. */
1816
1728
  function emptyGroup(groupIndex) {
1817
1729
  return {
1818
1730
  severity: "error",
@@ -1825,7 +1737,6 @@ function emptyGroup(groupIndex) {
1825
1737
  message: "Group contains zero constraints."
1826
1738
  };
1827
1739
  }
1828
- /** Numeric/length bound dominated by a stricter bound. */
1829
1740
  function dominatedBound(isLength, groupIndex, constraintIndex, value) {
1830
1741
  return {
1831
1742
  severity: "warning",
@@ -1838,7 +1749,6 @@ function dominatedBound(isLength, groupIndex, constraintIndex, value) {
1838
1749
  message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
1839
1750
  };
1840
1751
  }
1841
- /** Bound redundant because eq()/lengthEq() is set. */
1842
1752
  function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
1843
1753
  return {
1844
1754
  severity: "warning",
@@ -1851,7 +1761,6 @@ function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
1851
1761
  message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
1852
1762
  };
1853
1763
  }
1854
- /** notIn() value was present in isIn() set. */
1855
1764
  function setReduction(groupIndex, constraintIndex, value) {
1856
1765
  return {
1857
1766
  severity: "warning",
@@ -1864,7 +1773,6 @@ function setReduction(groupIndex, constraintIndex, value) {
1864
1773
  message: "notIn() value was present in isIn() set"
1865
1774
  };
1866
1775
  }
1867
- /** isIn() sets partially overlap. */
1868
1776
  function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1869
1777
  return {
1870
1778
  severity: "warning",
@@ -1877,7 +1785,6 @@ function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1877
1785
  message: "isIn() sets partially overlap"
1878
1786
  };
1879
1787
  }
1880
- /** Some values in isIn() set are excluded by neq/notIn. */
1881
1788
  function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1882
1789
  return {
1883
1790
  severity: "warning",
@@ -1890,7 +1797,6 @@ function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1890
1797
  message: "Some values in isIn() set are excluded by neq/notIn"
1891
1798
  };
1892
1799
  }
1893
- /** Bitmask operation is redundant. */
1894
1800
  function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1895
1801
  return {
1896
1802
  severity: "warning",
@@ -1903,7 +1809,6 @@ function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1903
1809
  message: "Bitmask operation is redundant"
1904
1810
  };
1905
1811
  }
1906
- /** gte/lte pair fusible into a single range operator. */
1907
1812
  function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
1908
1813
  return {
1909
1814
  severity: "warning",
@@ -1916,7 +1821,6 @@ function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
1916
1821
  message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
1917
1822
  };
1918
1823
  }
1919
- /** Duplicate operator in constraint. */
1920
1824
  function duplicateConstraint(groupIndex, constraintIndex) {
1921
1825
  return {
1922
1826
  severity: "warning",
@@ -1929,7 +1833,6 @@ function duplicateConstraint(groupIndex, constraintIndex) {
1929
1833
  message: "Duplicate operator in constraint"
1930
1834
  };
1931
1835
  }
1932
- /** gte(min)/lengthGte(0) is always true. */
1933
1836
  function vacuousGte(isLength, groupIndex, constraintIndex, value) {
1934
1837
  return {
1935
1838
  severity: "info",
@@ -1942,7 +1845,6 @@ function vacuousGte(isLength, groupIndex, constraintIndex, value) {
1942
1845
  message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
1943
1846
  };
1944
1847
  }
1945
- /** lte(max)/lengthLte(max) is always true. */
1946
1848
  function vacuousLte(isLength, groupIndex, constraintIndex, value) {
1947
1849
  return {
1948
1850
  severity: "info",
@@ -1955,7 +1857,6 @@ function vacuousLte(isLength, groupIndex, constraintIndex, value) {
1955
1857
  message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
1956
1858
  };
1957
1859
  }
1958
- /** Negated range with inverted bounds excludes nothing (always true). */
1959
1860
  function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
1960
1861
  return {
1961
1862
  severity: "info",
@@ -1968,7 +1869,6 @@ function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
1968
1869
  message: isLength ? "Negated lengthBetween() with low > high excludes nothing (always true)" : "Negated between() with low > high excludes nothing (always true)"
1969
1870
  };
1970
1871
  }
1971
- /** Path deeper than the reference enforcer's cap. */
1972
1872
  function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
1973
1873
  return {
1974
1874
  severity: "warning",
@@ -1978,23 +1878,21 @@ function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
1978
1878
  code: "PATH_DEPTH_EXCEEDED",
1979
1879
  value1: depth,
1980
1880
  value2: maxDepth,
1981
- message: "Path depth exceeds the reference enforcer cap"
1881
+ message: "Path depth exceeds the maximum a well-formed policy may declare"
1982
1882
  };
1983
1883
  }
1984
- /** Quantifier over a static array beyond the reference enforcer's iteration cap. */
1985
1884
  function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
1986
1885
  return {
1987
- severity: "warning",
1886
+ severity: "error",
1988
1887
  category: "compatibility",
1989
1888
  groupIndex,
1990
1889
  constraintIndex,
1991
1890
  code: "QUANTIFIER_OVER_STATIC_LIMIT",
1992
1891
  value1: arrayLength,
1993
1892
  value2: maxLength,
1994
- message: "Quantifier over static array exceeds the reference enforcer cap"
1893
+ message: "Quantifier over static array exceeds the maximum quantified length"
1995
1894
  };
1996
1895
  }
1997
- /** Context property ID outside the assigned set. */
1998
1896
  function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
1999
1897
  return {
2000
1898
  severity: "warning",
@@ -2007,7 +1905,6 @@ function unknownContextProperty(groupIndex, constraintIndex, contextId, maxConte
2007
1905
  message: "Unknown context property ID"
2008
1906
  };
2009
1907
  }
2010
- /** Negated operator under the existential quantifier. */
2011
1908
  function negationUnderAny(groupIndex, constraintIndex, opCode) {
2012
1909
  return {
2013
1910
  severity: "warning",
@@ -2022,29 +1919,29 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
2022
1919
  }
2023
1920
  //#endregion
2024
1921
  //#region src/policy-validator.ts
2025
- /**
2026
- * Maximum length value for dynamic types.
2027
- */
1922
+ const NAVIGABILITY_CODES = new Set([
1923
+ "EMPTY_PATH",
1924
+ "PARAM_INDEX_OUT_OF_BOUNDS",
1925
+ "TUPLE_FIELD_OUT_OF_BOUNDS",
1926
+ "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
1927
+ "NOT_COMPOSITE"
1928
+ ]);
2028
1929
  const LENGTH_MAX = 4294967295n;
2029
1930
  const UINT256_MAX = (1n << 256n) - 1n;
2030
1931
  const INT256_SIGN_BIT = 1n << 255n;
2031
- /** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
2032
1932
  function signedCompare(a, b, signed) {
2033
1933
  if (!signed) return a - b;
2034
1934
  const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
2035
1935
  return toS(a) - toS(b);
2036
1936
  }
2037
- /** True for uint and int type codes. */
2038
1937
  function isNumericType(typeCode) {
2039
- return typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1938
+ return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
2040
1939
  }
2041
- /** True for uint and bytes32 type codes. */
2042
1940
  function isBitmaskCompatible(typeCode) {
2043
- return typeCode <= TypeCode.UINT_MAX || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
1941
+ return typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX || typeCode === TypeCode.FIXED_BYTES_MIN + 31;
2044
1942
  }
2045
- /** Physical domain limits for a type code (returned as raw uint256 bigints). */
2046
1943
  function getDomainLimits(typeCode) {
2047
- if (typeCode <= TypeCode.UINT_MAX) {
1944
+ if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
2048
1945
  const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
2049
1946
  return {
2050
1947
  min: 0n,
@@ -2077,19 +1974,15 @@ function getDomainLimits(typeCode) {
2077
1974
  max: UINT256_MAX
2078
1975
  };
2079
1976
  }
2080
- /** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
2081
1977
  function isValueOp(opBase) {
2082
1978
  return opBase <= Op.BITMASK_NONE;
2083
1979
  }
2084
- /** True for GT, LT, GTE, LTE, BETWEEN. */
2085
1980
  function isComparisonOp(opBase) {
2086
1981
  return opBase >= Op.GT && opBase <= Op.BETWEEN;
2087
1982
  }
2088
- /** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
2089
1983
  function isBitmaskOp(opBase) {
2090
1984
  return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
2091
1985
  }
2092
- /** Return the incompatibility reason, or null if the operator is allowed. */
2093
1986
  function getIncompat(opBase, typeInfo) {
2094
1987
  const { typeCode, isDynamic, staticSize } = typeInfo;
2095
1988
  if (isValueOp(opBase)) {
@@ -2127,18 +2020,12 @@ function getIncompat(opBase, typeInfo) {
2127
2020
  message: "Unknown operator code"
2128
2021
  };
2129
2022
  }
2130
- /**
2131
- * True if operator `opCode` is allowed on a target with the given type info.
2132
- * Tolerates the NOT flag on `opCode`.
2133
- */
2134
2023
  function isOpAllowed(opCode, typeInfo) {
2135
2024
  return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
2136
2025
  }
2137
- /** Read a single uint256 from operator hex (bytes after the opcode byte). */
2138
2026
  function readValue(opHex) {
2139
2027
  return BigInt(`0x${opHex.slice(4)}`);
2140
2028
  }
2141
- /** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
2142
2029
  function findNonCanonicalWord(opHex, typeCode) {
2143
2030
  const body = opHex.slice(4);
2144
2031
  for (let i = 0; i + 64 <= body.length; i += 64) {
@@ -2151,7 +2038,6 @@ function findNonCanonicalWord(opHex, typeCode) {
2151
2038
  }
2152
2039
  return null;
2153
2040
  }
2154
- /** Read a pair of uint256 values from a BETWEEN operator. */
2155
2041
  function readPair(opHex) {
2156
2042
  const body = opHex.slice(4);
2157
2043
  return {
@@ -2159,7 +2045,6 @@ function readPair(opHex) {
2159
2045
  high: BigInt(`0x${body.slice(64, 128)}`)
2160
2046
  };
2161
2047
  }
2162
- /** Unpack an IN operator's payload into an array of bigint values. */
2163
2048
  function unpackSet(opHex) {
2164
2049
  const body = opHex.slice(4);
2165
2050
  const count = body.length / 64;
@@ -2167,12 +2052,10 @@ function unpackSet(opHex) {
2167
2052
  for (let i = 0; i < count; i++) values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
2168
2053
  return values;
2169
2054
  }
2170
- /** True if every element is strictly greater than the previous. */
2171
2055
  function isStrictlyAscending(values) {
2172
2056
  for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return false;
2173
2057
  return true;
2174
2058
  }
2175
- /** Convert LENGTH_* opcode to its core comparison equivalent. */
2176
2059
  function normalizeLengthOp(base) {
2177
2060
  if (base === Op.LENGTH_EQ) return Op.EQ;
2178
2061
  if (base === Op.LENGTH_GT) return Op.GT;
@@ -2181,7 +2064,6 @@ function normalizeLengthOp(base) {
2181
2064
  if (base === Op.LENGTH_LTE) return Op.LTE;
2182
2065
  return base;
2183
2066
  }
2184
- /** Convert a negated bound opcode to its positive equivalent. */
2185
2067
  function negateBoundOp(base) {
2186
2068
  if (base === Op.GT) return Op.LTE;
2187
2069
  if (base === Op.GTE) return Op.LT;
@@ -2189,7 +2071,6 @@ function negateBoundOp(base) {
2189
2071
  if (base === Op.LTE) return Op.GT;
2190
2072
  return base;
2191
2073
  }
2192
- /** Fresh bound domain with no constraints recorded yet. */
2193
2074
  function emptyBoundDomain(boundIsSigned, min, max) {
2194
2075
  return {
2195
2076
  isSigned: boundIsSigned,
@@ -2206,12 +2087,12 @@ function emptyBoundDomain(boundIsSigned, min, max) {
2206
2087
  holes: []
2207
2088
  };
2208
2089
  }
2209
- /** Create a fresh constraint context with domain limits from the type. */
2210
- function initContext(scope, path, typeInfo) {
2090
+ function initContext(scope, path, steps, typeInfo) {
2211
2091
  const { min, max } = getDomainLimits(typeInfo.typeCode);
2212
2092
  return {
2213
2093
  scope,
2214
2094
  path,
2095
+ steps,
2215
2096
  typeInfo,
2216
2097
  numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
2217
2098
  bitmask: {
@@ -2226,11 +2107,7 @@ function initContext(scope, path, typeInfo) {
2226
2107
  }
2227
2108
  };
2228
2109
  }
2229
- /** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
2230
2110
  function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
2231
- let changedEq = false;
2232
- let changedLower = false;
2233
- let changedUpper = false;
2234
2111
  if (isNegated) {
2235
2112
  if (base === Op.EQ) {
2236
2113
  if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
@@ -2248,6 +2125,9 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
2248
2125
  if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2249
2126
  else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2250
2127
  else if (base === Op.LT && value === domain.min) issues.push(impossibleLt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2128
+ let changedEq = false;
2129
+ let changedLower = false;
2130
+ let changedUpper = false;
2251
2131
  if (base === Op.EQ) {
2252
2132
  if (!domain.hasEq || domain.eq !== value) changedEq = true;
2253
2133
  if (domain.hasEq) {
@@ -2319,7 +2199,6 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
2319
2199
  }
2320
2200
  }
2321
2201
  }
2322
- /** Update the bitmask domain and emit issues on contradiction or redundancy. */
2323
2202
  function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues) {
2324
2203
  if (isNegated) return;
2325
2204
  if (base === Op.BITMASK_ALL) {
@@ -2335,7 +2214,6 @@ function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex,
2335
2214
  if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
2336
2215
  }
2337
2216
  }
2338
- /** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
2339
2217
  function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
2340
2218
  if (!ctx.set.hasIn) return;
2341
2219
  let possibleCount = 0;
@@ -2358,7 +2236,6 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
2358
2236
  if (possibleCount === 0) issues.push(setFullyExcluded(groupIndex, constraintIndex));
2359
2237
  else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
2360
2238
  }
2361
- /** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
2362
2239
  function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
2363
2240
  if (isNegated) {
2364
2241
  for (const value of values) {
@@ -2400,7 +2277,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
2400
2277
  checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2401
2278
  }
2402
2279
  }
2403
- /** Emit a warning if the constraint contains any identical operators. */
2404
2280
  function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2405
2281
  const seen = /* @__PURE__ */ new Set();
2406
2282
  for (const op of operators) {
@@ -2412,18 +2288,12 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2412
2288
  seen.add(key);
2413
2289
  }
2414
2290
  }
2415
- /**
2416
- * Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
2417
- * corresponding single range operator. Fires only for a satisfiable pair on a compatible
2418
- * type; contradictions and dominated bounds are already reported by the domain checks.
2419
- */
2420
2291
  function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
2421
2292
  const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
2422
2293
  if (numericPair && getIncompat(Op.GTE, ctx.typeInfo) === null && signedCompare(numericPair.low, numericPair.high, ctx.numeric.isSigned) <= 0n) issues.push(fusibleRange(false, groupIndex, constraintIndex, bigintToHex(numericPair.low), bigintToHex(numericPair.high)));
2423
2294
  const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
2424
2295
  if (lengthPair && getIncompat(Op.LENGTH_GTE, ctx.typeInfo) === null && lengthPair.low <= lengthPair.high) issues.push(fusibleRange(true, groupIndex, constraintIndex, bigintToHex(lengthPair.low), bigintToHex(lengthPair.high)));
2425
2296
  }
2426
- /** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
2427
2297
  function findLonePair(operators, lowerOp, upperOp) {
2428
2298
  let lowerCount = 0;
2429
2299
  let upperCount = 0;
@@ -2446,19 +2316,18 @@ function findLonePair(operators, lowerOp, upperOp) {
2446
2316
  high
2447
2317
  } : null;
2448
2318
  }
2449
- /** Validate all operators on a single constraint against its domain context. */
2450
2319
  function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
2451
2320
  const operators = constraint.operators;
2452
- const underAny = constraint.scope === Scope.CALLDATA && parsePathSteps(constraint.path).includes(Quantifier.ANY);
2321
+ const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
2453
2322
  for (const opHex of operators) {
2454
2323
  const opCode = parseInt(opHex.slice(2, 4), 16);
2455
2324
  const base = opCode & ~Op.NOT;
2456
- const isNegated = (opCode & Op.NOT) !== 0;
2457
2325
  const dataLength = (opHex.length - 4) / 2;
2458
2326
  if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
2459
2327
  issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2460
2328
  continue;
2461
2329
  }
2330
+ const isNegated = (opCode & Op.NOT) !== 0;
2462
2331
  if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2463
2332
  const incompat = getIncompat(base, ctx.typeInfo);
2464
2333
  if (incompat) {
@@ -2505,7 +2374,6 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2505
2374
  }
2506
2375
  checkDuplicates(issues, operators, groupIndex, constraintIndex);
2507
2376
  }
2508
- /** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
2509
2377
  function validateGroup(data, descBytes, groupIndex, issues) {
2510
2378
  const constraints = data.groups[groupIndex];
2511
2379
  const contexts = [];
@@ -2518,24 +2386,28 @@ function validateGroup(data, descBytes, groupIndex, issues) {
2518
2386
  break;
2519
2387
  }
2520
2388
  if (!ctx) {
2389
+ const steps = parsePathSteps(constraint.path);
2521
2390
  let typeInfo;
2522
2391
  if (constraint.scope === Scope.CALLDATA) {
2523
- const steps = parsePathSteps(constraint.path);
2524
2392
  if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
2525
2393
  let walk;
2526
2394
  try {
2527
2395
  walk = Descriptor.walkPath(descBytes, steps);
2528
2396
  } catch (error) {
2529
- if (error instanceof CallciumError && error.code === "INVALID_PATH") {
2397
+ if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
2530
2398
  issues.push(unnavigablePath(groupIndex, constraintIndex));
2531
2399
  continue;
2532
2400
  }
2533
2401
  throw error;
2534
2402
  }
2403
+ if (steps.filter(isQuantifier).length > 1) {
2404
+ issues.push(nestedQuantifier(groupIndex, constraintIndex));
2405
+ continue;
2406
+ }
2535
2407
  if (walk.quantifiedStaticLength > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) issues.push(quantifierOverStaticLimit(groupIndex, constraintIndex, bigintToHex(BigInt(walk.quantifiedStaticLength)), bigintToHex(BigInt(Limits.MAX_QUANTIFIED_ARRAY_LENGTH))));
2536
2408
  typeInfo = walk.typeInfo;
2537
2409
  } else {
2538
- const ctxId = parsePathSteps(constraint.path)[0];
2410
+ const ctxId = steps[0];
2539
2411
  if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
2540
2412
  issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
2541
2413
  typeInfo = {
@@ -2549,24 +2421,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
2549
2421
  staticSize: 32
2550
2422
  };
2551
2423
  }
2552
- ctx = initContext(constraint.scope, normalizedPath, typeInfo);
2424
+ ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
2553
2425
  contexts.push(ctx);
2554
2426
  }
2427
+ if (constraint.scope === Scope.CALLDATA && constraint.hint !== void 0 && ctx.steps.length <= Limits.MAX_PATH_DEPTH) {
2428
+ if (bytesToHex(Descriptor.compileHint(descBytes, ctx.steps)) !== constraint.hint.toLowerCase()) issues.push(hintMismatch(groupIndex, constraintIndex));
2429
+ }
2555
2430
  validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
2556
2431
  checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
2557
2432
  }
2558
2433
  }
2559
- /**
2560
- * Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
2561
- * @param data - The canonical policy data to validate.
2562
- * @returns All validation issues found, ordered by group and constraint index.
2563
- * @throws {CallciumError} If the descriptor is not well-formed; semantic validation is
2564
- * defined only over a well-formed descriptor.
2565
- */
2566
2434
  function validate(data) {
2567
- const issues = [];
2568
2435
  const descBytes = hexToBytes(data.descriptor);
2569
2436
  decodeDescriptor(descBytes);
2437
+ const issues = [];
2570
2438
  for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
2571
2439
  if (data.groups[groupIndex].length === 0) {
2572
2440
  issues.push(emptyGroup(groupIndex));
@@ -2576,102 +2444,80 @@ function validate(data) {
2576
2444
  }
2577
2445
  return issues;
2578
2446
  }
2579
- /** Semantic validation for policies. */
2580
2447
  const PolicyValidator = { validate };
2581
2448
  //#endregion
2582
2449
  //#region src/signature.ts
2583
- /** Return true if the char code is an ASCII letter. */
2584
2450
  function isAlpha(c) {
2585
2451
  return c >= 65 && c <= 90 || c >= 97 && c <= 122;
2586
2452
  }
2587
- /** Return true if the char code is an ASCII alphanumeric character. */
2588
2453
  function isAlphanum(c) {
2589
2454
  return isAlpha(c) || c >= 48 && c <= 57;
2590
2455
  }
2591
- /**
2592
- * Parse an ABI function signature into its selector and types.
2593
- *
2594
- * Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
2595
- * structure must be `name(types)` with the final `)` as the last character.
2596
- *
2597
- * @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
2598
- * @returns The 4-byte selector and the comma-separated types string.
2599
- * @throws {CallciumError} with code `"INVALID_SIGNATURE"` if the signature is malformed.
2600
- */
2601
2456
  function parse(signature) {
2602
- if (signature.length < 3) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
2457
+ if (signature.length < 3) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
2603
2458
  let openParen = -1;
2604
2459
  for (let i = 0; i < signature.length; i++) {
2605
2460
  const c = signature.charCodeAt(i);
2606
- if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("INVALID_SIGNATURE", "Signature must not contain whitespace");
2607
- if (c > 126) throw new CallciumError("INVALID_SIGNATURE", "Signature must contain only ASCII characters");
2461
+ if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
2462
+ if (c > 126) throw new CallciumError("MALFORMED_SIGNATURE", "Signature must contain only ASCII characters");
2608
2463
  if (openParen === -1 && c === 40) openParen = i;
2609
2464
  }
2610
- if (openParen < 1) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
2611
- if (!signature.endsWith(")")) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
2465
+ if (openParen < 1) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
2466
+ if (!signature.endsWith(")")) throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
2612
2467
  const firstChar = signature.charCodeAt(0);
2613
- if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_SIGNATURE", "Function name must start with a letter or underscore");
2468
+ if (!isAlpha(firstChar) && firstChar !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
2614
2469
  for (let i = 1; i < openParen; i++) {
2615
2470
  const c = signature.charCodeAt(i);
2616
- if (!isAlphanum(c) && c !== 95) throw new CallciumError("INVALID_SIGNATURE", "Function name must contain only alphanumeric characters or underscores");
2471
+ if (!isAlphanum(c) && c !== 95) throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must contain only alphanumeric characters or underscores");
2617
2472
  }
2618
- const types = signature.slice(openParen + 1, signature.length - 1);
2619
2473
  const encoded = new Uint8Array(signature.length);
2620
2474
  for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
2621
2475
  return {
2622
2476
  selector: bytesToHex(keccak_256(encoded).subarray(0, 4)),
2623
- types
2477
+ types: signature.slice(openParen + 1, signature.length - 1)
2624
2478
  };
2625
2479
  }
2626
- /** ABI function signature parser. */
2627
2480
  const SignatureParser = { parse };
2628
2481
  //#endregion
2629
2482
  //#region src/policy-builder.ts
2630
- /** Validate a context-scope path. */
2631
- function validateContextPath(pathBytes) {
2632
- if (pathBytes.length !== 2) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step (2 bytes).");
2633
- const step = readU16(pathBytes, 0);
2634
- if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
2635
- }
2636
- /** Validate a calldata-scope path against the descriptor. */
2637
- function validateCalldataPath(path, desc) {
2638
- const steps = parsePathSteps(path);
2639
- if (steps.length === 0) throw new CallciumError("INVALID_PATH", "Calldata path must have at least one step.");
2483
+ function validateContextPath(steps) {
2484
+ if (steps.length !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step.");
2485
+ const step = steps[0];
2486
+ if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
2487
+ }
2488
+ function validateCalldataPath(steps, desc) {
2640
2489
  const argIndex = steps[0];
2641
2490
  const paramCount = Descriptor.paramCount(desc);
2642
- if (argIndex >= paramCount) throw new CallciumError("INVALID_PATH", `Argument index ${argIndex} out of range (descriptor has ${paramCount} params).`);
2491
+ if (argIndex >= paramCount) throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${argIndex} out of range (descriptor has ${paramCount} params).`);
2643
2492
  let offset = Descriptor.paramOffset(desc, argIndex);
2644
2493
  let hasQuantifier = false;
2645
2494
  for (let i = 1; i < steps.length; i++) {
2646
2495
  const step = steps[i];
2647
2496
  const info = Descriptor.inspect(desc, offset);
2497
+ const isArray = info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY;
2498
+ if (isQuantifier(step)) {
2499
+ if (!isArray) throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node.");
2500
+ if (hasQuantifier) throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed.");
2501
+ hasQuantifier = true;
2502
+ }
2648
2503
  if (info.typeCode === TypeCode.TUPLE) {
2649
- if (isQuantifier(step)) throw new CallciumError("INVALID_PATH", "Quantifier step is not valid on a tuple node.");
2650
2504
  const fieldCount = Descriptor.tupleFieldCount(desc, offset);
2651
- if (step >= fieldCount) throw new CallciumError("INVALID_PATH", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`);
2505
+ if (step >= fieldCount) throw new CallciumError("TUPLE_FIELD_OUT_OF_BOUNDS", `Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`);
2652
2506
  offset = Descriptor.tupleFieldOffset(desc, offset, step);
2653
- } else if (info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY) {
2654
- if (isQuantifier(step)) {
2655
- if (hasQuantifier) throw new CallciumError("INVALID_PATH", "Nested quantifiers are not allowed.");
2656
- hasQuantifier = true;
2657
- } else if (info.typeCode === TypeCode.STATIC_ARRAY) {
2507
+ } else if (isArray) {
2508
+ if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
2658
2509
  const arrayLength = Descriptor.staticArrayLength(desc, offset);
2659
- if (step >= arrayLength) throw new CallciumError("INVALID_PATH", `Array index ${step} out of range (static array has ${arrayLength} elements).`);
2510
+ if (step >= arrayLength) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (static array has ${arrayLength} elements).`);
2660
2511
  }
2661
2512
  offset = Descriptor.arrayElementOffset(offset);
2662
- } else throw new CallciumError("INVALID_PATH", "Cannot descend into an elementary type.");
2513
+ } else throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type.");
2663
2514
  }
2664
2515
  }
2665
- /** Fluent builder for constructing Callcium policies. */
2666
2516
  var PolicyBuilder = class PolicyBuilder {
2667
2517
  draft;
2668
2518
  constructor(draft) {
2669
2519
  this.draft = draft;
2670
2520
  }
2671
- /**
2672
- * Create a builder from a function signature.
2673
- * @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
2674
- */
2675
2521
  static create(signature) {
2676
2522
  const parsed = SignatureParser.parse(signature);
2677
2523
  const descriptor = DescriptorCoder.fromTypes(parsed.types);
@@ -2683,10 +2529,6 @@ var PolicyBuilder = class PolicyBuilder {
2683
2529
  pathHashes: [/* @__PURE__ */ new Set()]
2684
2530
  });
2685
2531
  }
2686
- /**
2687
- * Create a selectorless builder from a raw type string.
2688
- * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
2689
- */
2690
2532
  static createRaw(typesCsv) {
2691
2533
  return new PolicyBuilder({
2692
2534
  isSelectorless: true,
@@ -2696,73 +2538,50 @@ var PolicyBuilder = class PolicyBuilder {
2696
2538
  pathHashes: [/* @__PURE__ */ new Set()]
2697
2539
  });
2698
2540
  }
2699
- /**
2700
- * Add a constraint to the current group.
2701
- * @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
2702
- */
2703
2541
  add(constraint) {
2704
2542
  const c = {
2705
2543
  scope: constraint.scope,
2706
2544
  path: constraint.path,
2707
2545
  operators: [...constraint.operators]
2708
2546
  };
2709
- if (c.operators.length === 0) throw new CallciumError("INVALID_CONSTRAINT", "Constraint must have at least one operator.");
2710
- if (c.scope === Scope.CONTEXT) validateContextPath(hexToBytes(c.path));
2711
- else if (c.scope === Scope.CALLDATA) validateCalldataPath(c.path, this.draft.descriptor);
2547
+ if (c.operators.length === 0) throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator.");
2548
+ const steps = parsePathSteps(c.path);
2549
+ if (steps.length === 0) throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
2550
+ if (c.scope === Scope.CONTEXT) validateContextPath(steps);
2551
+ else if (c.scope === Scope.CALLDATA) validateCalldataPath(steps, this.draft.descriptor);
2712
2552
  else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
2713
2553
  const key = `${c.scope}:${c.path.toLowerCase()}`;
2714
2554
  const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1];
2715
- if (currentHashes.has(key)) throw new CallciumError("DUPLICATE_PATH", `Duplicate path ${c.path} in the same group.`);
2555
+ if (currentHashes.has(key)) throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group.`);
2716
2556
  currentHashes.add(key);
2717
2557
  this.draft.groups[this.draft.groups.length - 1].push(c);
2718
2558
  return this;
2719
2559
  }
2720
- /** Start a new constraint group (OR branch). */
2721
2560
  or() {
2722
2561
  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.");
2723
2562
  this.draft.groups.push([]);
2724
2563
  this.draft.pathHashes.push(/* @__PURE__ */ new Set());
2725
2564
  return this;
2726
2565
  }
2727
- /**
2728
- * Build the policy into an encoded binary blob with strict validation.
2729
- * Throws on any issue, regardless of severity. Use {@link validate} to
2730
- * inspect issues, or {@link buildUnsafe} to encode without validation.
2731
- * @returns The policy as a 0x-prefixed hex string.
2732
- * @throws {CallciumError} If any group is empty or validation finds any issue.
2733
- */
2734
2566
  build() {
2735
2567
  this.checkGroups();
2736
2568
  const policyData = this.toPolicyData();
2737
2569
  const issues = PolicyValidator.validate(policyData);
2738
- if (issues.length > 0) throw new CallciumError("VALIDATION_ERROR", issues[0].message);
2570
+ if (issues.length > 0) throw new ValidationError(issues);
2739
2571
  return PolicyCoder.encode(policyData);
2740
2572
  }
2741
- /**
2742
- * Build the policy into an encoded binary blob without validation.
2743
- * The resulting policy may be invalid. Prefer {@link build}; use this
2744
- * only to knowingly bypass a reported issue.
2745
- * @returns The policy as a 0x-prefixed hex string.
2746
- * @throws {CallciumError} If any group is empty.
2747
- */
2748
2573
  buildUnsafe() {
2749
2574
  this.checkGroups();
2750
2575
  return PolicyCoder.encode(this.toPolicyData());
2751
2576
  }
2752
- /**
2753
- * Validate the policy without encoding.
2754
- * @returns All validation issues found.
2755
- */
2756
2577
  validate() {
2757
2578
  this.checkGroups();
2758
2579
  const policyData = this.toPolicyData();
2759
2580
  return PolicyValidator.validate(policyData);
2760
2581
  }
2761
- /** Throw if any group is empty. */
2762
2582
  checkGroups() {
2763
2583
  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.`);
2764
2584
  }
2765
- /** Convert the draft to a PolicyData structure. */
2766
2585
  toPolicyData() {
2767
2586
  return {
2768
2587
  isSelectorless: this.draft.isSelectorless,
@@ -2774,13 +2593,9 @@ var PolicyBuilder = class PolicyBuilder {
2774
2593
  };
2775
2594
  //#endregion
2776
2595
  //#region src/constraint.ts
2777
- /** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
2778
2596
  function addressBody(value) {
2779
- const body = value.startsWith("0x") ? value.slice(2) : value;
2780
- if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
2781
- return body;
2597
+ return toAddress(value).slice(2);
2782
2598
  }
2783
- /** Convert a scalar value to a 32-byte big-endian word. */
2784
2599
  function encodeWord(value) {
2785
2600
  const word = new Uint8Array(32);
2786
2601
  if (typeof value === "boolean") {
@@ -2789,11 +2604,7 @@ function encodeWord(value) {
2789
2604
  }
2790
2605
  if (typeof value === "string") {
2791
2606
  const body = addressBody(value);
2792
- for (let i = 0; i < 20; i++) {
2793
- const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2794
- if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
2795
- word[12 + i] = byte;
2796
- }
2607
+ for (let i = 0; i < 20; i++) word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2797
2608
  return word;
2798
2609
  }
2799
2610
  let bigValue = typeof value === "number" ? BigInt(value) : value;
@@ -2803,14 +2614,12 @@ function encodeWord(value) {
2803
2614
  }
2804
2615
  return word;
2805
2616
  }
2806
- /** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
2807
2617
  function singleOp(opCode, value) {
2808
2618
  const buffer = new Uint8Array(33);
2809
2619
  buffer[0] = opCode;
2810
2620
  buffer.set(encodeWord(value), 1);
2811
2621
  return bytesToHex(buffer);
2812
2622
  }
2813
- /** Pack a range operator (opCode byte + min word + max word). */
2814
2623
  function rangeOp(opCode, min, max) {
2815
2624
  if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
2816
2625
  const buffer = new Uint8Array(65);
@@ -2819,7 +2628,7 @@ function rangeOp(opCode, min, max) {
2819
2628
  buffer.set(encodeWord(max), 33);
2820
2629
  return bytesToHex(buffer);
2821
2630
  }
2822
- /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
2631
+ const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
2823
2632
  function setOp(opCode, values) {
2824
2633
  const bigs = values.map((v) => {
2825
2634
  if (typeof v === "bigint") return v;
@@ -2831,118 +2640,81 @@ function setOp(opCode, values) {
2831
2640
  const deduped = [];
2832
2641
  for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
2833
2642
  if (deduped.length === 0) throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
2643
+ if (deduped.length > MAX_SET_MEMBERS) throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
2834
2644
  const buffer = new Uint8Array(1 + deduped.length * 32);
2835
2645
  buffer[0] = opCode;
2836
2646
  for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
2837
2647
  return bytesToHex(buffer);
2838
2648
  }
2839
- /** Encode a sequence of uint16 path steps as a big-endian hex string. */
2840
2649
  function encodePath(steps) {
2841
2650
  const buffer = new Uint8Array(steps.length * 2);
2842
2651
  for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
2843
2652
  return bytesToHex(buffer);
2844
2653
  }
2845
- /**
2846
- * Mutable builder that accumulates operators targeting a single path.
2847
- * Implements the `Constraint` interface so it can be passed directly to policy builders.
2848
- */
2849
2654
  var ConstraintBuilder = class {
2850
2655
  scope;
2851
2656
  path;
2852
2657
  operators;
2853
- /** @internal */
2854
2658
  constructor(scope, path) {
2855
2659
  this.scope = scope;
2856
2660
  this.path = path;
2857
2661
  this.operators = [];
2858
2662
  }
2859
- /** Push a pre-encoded operator hex string and return this for chaining. */
2860
2663
  push(opHex) {
2861
2664
  this.operators.push(opHex);
2862
2665
  return this;
2863
2666
  }
2864
- /** Assert the value equals `value`. */
2865
2667
  eq(value) {
2866
2668
  return this.push(singleOp(Op.EQ, value));
2867
2669
  }
2868
- /** Assert the value does not equal `value`. */
2869
2670
  neq(value) {
2870
2671
  return this.push(singleOp(Op.EQ | Op.NOT, value));
2871
2672
  }
2872
- /** Assert the value is greater than `bound`. */
2873
2673
  gt(bound) {
2874
2674
  return this.push(singleOp(Op.GT, bound));
2875
2675
  }
2876
- /** Assert the value is less than `bound`. */
2877
2676
  lt(bound) {
2878
2677
  return this.push(singleOp(Op.LT, bound));
2879
2678
  }
2880
- /** Assert the value is greater than or equal to `bound`. */
2881
2679
  gte(bound) {
2882
2680
  return this.push(singleOp(Op.GTE, bound));
2883
2681
  }
2884
- /** Assert the value is less than or equal to `bound`. */
2885
2682
  lte(bound) {
2886
2683
  return this.push(singleOp(Op.LTE, bound));
2887
2684
  }
2888
- /**
2889
- * Assert the value is within [min, max] inclusive.
2890
- * @throws {CallciumError} If min > max.
2891
- */
2892
2685
  between(min, max) {
2893
2686
  return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
2894
2687
  }
2895
- /**
2896
- * Assert the value is a member of the set.
2897
- * Values are sorted and deduplicated before encoding.
2898
- * @throws {CallciumError} If the set is empty after deduplication.
2899
- */
2900
2688
  isIn(values) {
2901
2689
  return this.push(setOp(Op.IN, values));
2902
2690
  }
2903
- /**
2904
- * Assert the value is not a member of the set.
2905
- * @throws {CallciumError} If the set is empty after deduplication.
2906
- */
2907
2691
  notIn(values) {
2908
2692
  return this.push(setOp(Op.IN | Op.NOT, values));
2909
2693
  }
2910
- /** Assert all bits in `mask` are set. */
2911
2694
  bitmaskAll(mask) {
2912
2695
  return this.push(singleOp(Op.BITMASK_ALL, mask));
2913
2696
  }
2914
- /** Assert at least one bit in `mask` is set. */
2915
2697
  bitmaskAny(mask) {
2916
2698
  return this.push(singleOp(Op.BITMASK_ANY, mask));
2917
2699
  }
2918
- /** Assert no bit in `mask` is set. */
2919
2700
  bitmaskNone(mask) {
2920
2701
  return this.push(singleOp(Op.BITMASK_NONE, mask));
2921
2702
  }
2922
- /** Assert the runtime length equals `n`. */
2923
2703
  lengthEq(n) {
2924
2704
  return this.push(singleOp(Op.LENGTH_EQ, n));
2925
2705
  }
2926
- /** Assert the runtime length is greater than `n`. */
2927
2706
  lengthGt(n) {
2928
2707
  return this.push(singleOp(Op.LENGTH_GT, n));
2929
2708
  }
2930
- /** Assert the runtime length is less than `n`. */
2931
2709
  lengthLt(n) {
2932
2710
  return this.push(singleOp(Op.LENGTH_LT, n));
2933
2711
  }
2934
- /** Assert the runtime length is greater than or equal to `n`. */
2935
2712
  lengthGte(n) {
2936
2713
  return this.push(singleOp(Op.LENGTH_GTE, n));
2937
2714
  }
2938
- /** Assert the runtime length is less than or equal to `n`. */
2939
2715
  lengthLte(n) {
2940
2716
  return this.push(singleOp(Op.LENGTH_LTE, n));
2941
2717
  }
2942
- /**
2943
- * Assert the runtime length is within [min, max] inclusive.
2944
- * @throws {CallciumError} If min > max.
2945
- */
2946
2718
  lengthBetween(min, max) {
2947
2719
  return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
2948
2720
  }
@@ -2950,419 +2722,60 @@ var ConstraintBuilder = class {
2950
2722
  function arg(...steps) {
2951
2723
  return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
2952
2724
  }
2953
- /** Target the `msg.sender` context property. */
2954
2725
  function msgSender() {
2955
2726
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
2956
2727
  }
2957
- /** Target the `msg.value` context property. */
2958
2728
  function msgValue() {
2959
2729
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
2960
2730
  }
2961
- /** Target the `block.timestamp` context property. */
2962
2731
  function blockTimestamp() {
2963
2732
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
2964
2733
  }
2965
- /** Target the `block.number` context property. */
2966
2734
  function blockNumber() {
2967
2735
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
2968
2736
  }
2969
- /** Target the `block.chainid` context property. */
2970
2737
  function chainId() {
2971
2738
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
2972
2739
  }
2973
- /** Target the `tx.origin` context property. */
2974
2740
  function txOrigin() {
2975
2741
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
2976
2742
  }
2977
- /** Target the `block.basefee` context property. */
2978
2743
  function baseFee() {
2979
2744
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
2980
2745
  }
2981
- /** Target the `tx.gasprice` context property. */
2982
2746
  function gasPrice() {
2983
2747
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
2984
2748
  }
2985
2749
  //#endregion
2986
- //#region src/reader.ts
2987
- /** Read 32 bytes at offset, bounds-checked. */
2988
- function load32(callData, offset) {
2750
+ //#region src/calldata-reader.ts
2751
+ function loadWord(callData, offset) {
2989
2752
  if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
2990
2753
  return callData.subarray(offset, offset + 32);
2991
2754
  }
2992
- /**
2993
- * Read ABI pointer (offset or length) from a 32-byte slot.
2994
- * Validates high 28 bytes are zero (rejects values > 32-bit).
2995
- */
2996
2755
  function readPointer(callData, head) {
2997
- const word = load32(callData, head);
2756
+ const word = loadWord(callData, head);
2998
2757
  if (word instanceof Uint8Array) {
2999
2758
  for (let i = 0; i < 28; i++) if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
3000
2759
  return (word[28] << 24 | word[29] << 16 | word[30] << 8 | word[31]) >>> 0;
3001
2760
  }
3002
2761
  return word;
3003
2762
  }
3004
- /** Return the number of bytes this node occupies in the head region. */
3005
- function headContribution(node) {
3006
- return node.isDynamic ? 32 : node.staticSize;
3007
- }
3008
- /** Read a single BE16 path step at the given step index. */
3009
- function readStep(pathBytes, stepIndex) {
3010
- return readU16(pathBytes, stepIndex * 2);
3011
- }
3012
- /** Navigate one path step through a node (tuple field, array element, or quantifier). */
3013
- function descend(node, callData, head, base, childIndex) {
3014
- if (node.type === "tuple") return descendTuple(node, callData, head, base, childIndex);
3015
- if (node.type === "dynamicArray") return descendDynamicArray(node, callData, head, base, childIndex);
3016
- if (node.type === "staticArray") return descendStaticArray(node, callData, head, base, childIndex);
3017
- throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type (${node.type}).`);
3018
- }
3019
- /** Descend into a tuple field by index. */
3020
- function descendTuple(node, callData, head, base, childIndex) {
3021
- if (childIndex >= node.fields.length) throw new CallciumError("INVALID_PATH", `Tuple field index ${childIndex} out of range (${node.fields.length} fields).`);
3022
- let tupleBase;
3023
- let cursor;
3024
- if (node.isDynamic) {
3025
- const ptrResult = readPointer(callData, head);
3026
- if (typeof ptrResult !== "number") return {
3027
- ok: false,
3028
- code: ptrResult.code
3029
- };
3030
- tupleBase = base + ptrResult;
3031
- cursor = tupleBase;
3032
- } else {
3033
- tupleBase = base;
3034
- cursor = head;
3035
- }
3036
- for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
3037
- return {
3038
- ok: true,
3039
- location: {
3040
- head: cursor,
3041
- base: tupleBase,
3042
- node: node.fields[childIndex]
3043
- }
3044
- };
3045
- }
3046
- /** Descend into a dynamic array element by index. */
3047
- function descendDynamicArray(node, callData, head, base, childIndex) {
3048
- const arrayBaseResult = readPointer(callData, head);
3049
- if (typeof arrayBaseResult !== "number") return {
3050
- ok: false,
3051
- code: arrayBaseResult.code
3052
- };
3053
- const arrayBase = base + arrayBaseResult;
3054
- const lengthResult = readPointer(callData, arrayBase);
3055
- if (typeof lengthResult !== "number") return {
3056
- ok: false,
3057
- code: lengthResult.code
3058
- };
3059
- if (childIndex >= lengthResult) return {
3060
- ok: false,
3061
- code: "ARRAY_INDEX_OUT_OF_BOUNDS"
3062
- };
3063
- const headsBase = arrayBase + 32;
3064
- const elem = node.element;
3065
- if (elem.isDynamic) return {
3066
- ok: true,
3067
- location: {
3068
- head: headsBase + childIndex * 32,
3069
- base: headsBase,
3070
- node: elem
3071
- }
3072
- };
3073
- return {
3074
- ok: true,
3075
- location: {
3076
- head: headsBase + childIndex * elem.staticSize,
3077
- base: arrayBase,
3078
- node: elem
3079
- }
3080
- };
3081
- }
3082
- /** Descend into a static array element by index. */
3083
- function descendStaticArray(node, callData, head, base, childIndex) {
3084
- if (childIndex >= node.length) throw new CallciumError("INVALID_PATH", `Static array index ${childIndex} out of range (length ${node.length}).`);
3085
- const elem = node.element;
3086
- if (elem.isDynamic) {
3087
- const ptrResult = readPointer(callData, head);
3088
- if (typeof ptrResult !== "number") return {
3089
- ok: false,
3090
- code: ptrResult.code
3091
- };
3092
- const arrayBase = base + ptrResult;
3093
- return {
3094
- ok: true,
3095
- location: {
3096
- head: arrayBase + childIndex * 32,
3097
- base: arrayBase,
3098
- node: elem
3099
- }
3100
- };
3101
- }
3102
- return {
3103
- ok: true,
3104
- location: {
3105
- head: head + childIndex * elem.staticSize,
3106
- base,
3107
- node: elem
3108
- }
3109
- };
3110
- }
3111
- /**
3112
- * Resolve a path to a calldata location, stopping at a leaf or quantifier.
3113
- *
3114
- * The path is a sequence of BE16 steps. The first step selects the top-level
3115
- * parameter; subsequent steps descend into tuples and arrays. If a quantifier
3116
- * step is encountered, returns a QuantifierResult with the array location and
3117
- * remaining path suffix.
3118
- */
3119
- function locate(tree, callData, pathBytes, baseOffset) {
3120
- const stepCount = pathBytes.length / 2;
3121
- if (stepCount === 0) throw new CallciumError("INVALID_PATH", "Path is empty.");
3122
- const paramIndex = readStep(pathBytes, 0);
3123
- if (paramIndex >= tree.length) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (${tree.length} params).`);
3124
- let head = baseOffset;
3125
- for (let i = 0; i < paramIndex; i++) head += headContribution(tree[i]);
3126
- let base = baseOffset;
3127
- let node = tree[paramIndex];
3128
- for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
3129
- const step = readStep(pathBytes, stepIndex);
3130
- if (isQuantifier(step)) {
3131
- const remaining = pathBytes.subarray((stepIndex + 1) * 2);
3132
- for (let remainingStepIndex = 0; remainingStepIndex < remaining.length / 2; remainingStepIndex++) if (isQuantifier(readStep(remaining, remainingStepIndex))) throw new CallciumError("INVALID_QUANTIFIER", "Nested quantifiers are not supported.");
3133
- return {
3134
- ok: true,
3135
- type: "quantifier",
3136
- quantifier: {
3137
- quantifier: step,
3138
- location: {
3139
- head,
3140
- base,
3141
- node
3142
- },
3143
- remainingPath: remaining
3144
- }
3145
- };
3146
- }
3147
- const result = descend(node, callData, head, base, step);
3148
- if (!result.ok) return {
3149
- ok: false,
3150
- code: result.code
3151
- };
3152
- head = result.location.head;
3153
- base = result.location.base;
3154
- node = result.location.node;
3155
- }
3156
- return {
3157
- ok: true,
3158
- type: "leaf",
3159
- location: {
3160
- head,
3161
- base,
3162
- node
3163
- }
3164
- };
3165
- }
3166
- /**
3167
- * Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
3168
- * whether the element itself is dynamic.
3169
- */
3170
- function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
3171
- return elem.isDynamic ? {
3172
- length,
3173
- elementNode: elem,
3174
- elementIsDynamic: true,
3175
- elementStaticSize: 0,
3176
- headsBase: dynamicHeadsBase,
3177
- dataOffset: 0,
3178
- compositeBase: dynamicHeadsBase
3179
- } : {
3180
- length,
3181
- elementNode: elem,
3182
- elementIsDynamic: false,
3183
- elementStaticSize: elem.staticSize,
3184
- headsBase: 0,
3185
- dataOffset: staticDataOffset,
3186
- compositeBase: staticCompositeBase
3187
- };
3188
- }
3189
- /** Extract array shape from a location pointing to an array node. */
3190
- function arrayShape(callData, location) {
3191
- const { head, base, node } = location;
3192
- if (node.type === "dynamicArray") {
3193
- const arrayBaseResult = readPointer(callData, head);
3194
- if (typeof arrayBaseResult !== "number") return {
3195
- ok: false,
3196
- code: arrayBaseResult.code
3197
- };
3198
- const arrayBase = base + arrayBaseResult;
3199
- const lengthResult = readPointer(callData, arrayBase);
3200
- if (typeof lengthResult !== "number") return {
3201
- ok: false,
3202
- code: lengthResult.code
3203
- };
3204
- const headsBase = arrayBase + 32;
3205
- return {
3206
- ok: true,
3207
- shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
3208
- };
3209
- }
3210
- if (node.type === "staticArray") {
3211
- const elem = node.element;
3212
- let dynamicHeadsBase = 0;
3213
- if (elem.isDynamic) {
3214
- const arrayBaseResult = readPointer(callData, head);
3215
- if (typeof arrayBaseResult !== "number") return {
3216
- ok: false,
3217
- code: arrayBaseResult.code
3218
- };
3219
- dynamicHeadsBase = base + arrayBaseResult;
3220
- }
3221
- return {
3222
- ok: true,
3223
- shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
3224
- };
3225
- }
3226
- throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
3227
- }
3228
- /** Compute the location for element N within a precomputed array shape. */
3229
- function arrayElementAt(shape, index, callData) {
3230
- if (index >= shape.length) return {
3231
- ok: false,
3232
- code: "ARRAY_INDEX_OUT_OF_BOUNDS"
3233
- };
3234
- let head;
3235
- let base;
3236
- if (!shape.elementIsDynamic) {
3237
- head = shape.dataOffset + index * shape.elementStaticSize;
3238
- base = shape.compositeBase;
3239
- } else {
3240
- head = shape.headsBase + index * 32;
3241
- base = shape.headsBase;
3242
- }
3243
- if (head + 32 > callData.length) return {
3244
- ok: false,
3245
- code: "CALLDATA_OUT_OF_BOUNDS"
3246
- };
3247
- return {
3248
- ok: true,
3249
- location: {
3250
- head,
3251
- base,
3252
- node: shape.elementNode
3253
- }
3254
- };
3255
- }
3256
- /** Load a 32-byte scalar value from a resolved location. */
3257
- function loadScalar(callData, location) {
3258
- const result = load32(callData, location.head);
3259
- if (result instanceof Uint8Array) return {
3260
- ok: true,
3261
- value: result
3262
- };
3263
- return {
3264
- ok: false,
3265
- code: result.code
3266
- };
3267
- }
3268
- /** Load the data offset and logical length for a dynamic type (bytes, string, or dynamic array). */
3269
- function loadSlice(callData, location) {
3270
- const ptrResult = readPointer(callData, location.head);
3271
- if (typeof ptrResult !== "number") return {
3272
- ok: false,
3273
- code: ptrResult.code
3274
- };
3275
- const dataStart = location.base + ptrResult;
3276
- const lengthResult = readPointer(callData, dataStart);
3277
- if (typeof lengthResult !== "number") return {
3278
- ok: false,
3279
- code: lengthResult.code
3280
- };
3281
- const dataOffset = dataStart + 32;
3282
- const node = location.node;
3283
- const stride = node.type === "dynamicArray" ? node.element.isDynamic ? 32 : node.element.staticSize : 1;
3284
- if (dataOffset > callData.length || lengthResult > Math.floor((callData.length - dataOffset) / stride)) return {
3285
- ok: false,
3286
- code: "CALLDATA_OUT_OF_BOUNDS"
3287
- };
3288
- return {
3289
- ok: true,
3290
- dataOffset,
3291
- length: lengthResult
3292
- };
3293
- }
3294
- /** Continue navigating through additional path steps from a resolved location. */
3295
- function descendPath(callData, location, pathBytes) {
3296
- const stepCount = pathBytes.length / 2;
3297
- let { head, base, node } = location;
3298
- for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
3299
- const step = readStep(pathBytes, stepIndex);
3300
- const result = descend(node, callData, head, base, step);
3301
- if (!result.ok) return {
3302
- ok: false,
3303
- code: result.code
3304
- };
3305
- head = result.location.head;
3306
- base = result.location.base;
3307
- node = result.location.node;
3308
- }
3309
- return {
3310
- ok: true,
3311
- location: {
3312
- head,
3313
- base,
3314
- node
3315
- }
3316
- };
3317
- }
3318
2763
  //#endregion
3319
2764
  //#region src/policy-enforcer.ts
3320
- /** Violations that abort evaluation instead of failing only their group (spec §9.3). */
3321
2765
  const ABORT_VIOLATION_CODES = new Set([
3322
2766
  "CALLDATA_OUT_OF_BOUNDS",
3323
2767
  "ARRAY_INDEX_OUT_OF_BOUNDS",
3324
2768
  "NON_CANONICAL_VALUE",
3325
2769
  "QUANTIFIER_LIMIT_EXCEEDED"
3326
2770
  ]);
3327
- /** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
3328
2771
  function addressToBigInt(hex) {
3329
2772
  const bytes = hexToBytes(hex);
3330
2773
  const padded = new Uint8Array(32);
3331
2774
  padded.set(bytes, 32 - bytes.length);
3332
2775
  return toBigInt(padded, 0);
3333
2776
  }
3334
- /**
3335
- * Walk a descriptor subtree through path steps and return the terminal leaf type code.
3336
- *
3337
- * Used only for violation metadata. Valid policies have structurally valid
3338
- * quantifier suffixes (enforced by `PolicyValidator`); if a hand-crafted blob
3339
- * violates that invariant, return the last resolvable node's type code as
3340
- * best-effort diagnostic context.
3341
- */
3342
- function resolveLeafTypeCode(elementNode, remainingPath) {
3343
- let node = elementNode;
3344
- const stepCount = remainingPath.length / 2;
3345
- for (let step = 0; step < stepCount; step++) {
3346
- const childIndex = readU16(remainingPath, step * 2);
3347
- if (node.type === "tuple") {
3348
- const field = node.fields[childIndex];
3349
- if (!field) break;
3350
- node = field;
3351
- } else if (node.type === "staticArray" || node.type === "dynamicArray") node = node.element;
3352
- else break;
3353
- }
3354
- return node.typeCode;
3355
- }
3356
- /**
3357
- * Check a policy against ABI-encoded call data without throwing on violations.
3358
- * @param policy - Binary policy blob as 0x-prefixed hex string.
3359
- * @param callData - ABI-encoded call data as 0x-prefixed hex string.
3360
- * @param context - Optional execution context for context-scoped rules.
3361
- * @returns Pass with matched group index, or fail with one violation per evaluated failing group.
3362
- * @throws {CallciumError} If the policy blob is structurally malformed.
3363
- */
3364
2777
  function check(policy, callData, context) {
3365
- const { policy: decoded, tree } = decodePolicy(policy);
2778
+ const { policy: decoded, data: policyBytes } = decodePolicy(policy);
3366
2779
  const callDataBytes = hexToBytes(callData);
3367
2780
  if (!decoded.isSelectorless) {
3368
2781
  if (callDataBytes.length < 4) return {
@@ -3387,7 +2800,7 @@ function check(policy, callData, context) {
3387
2800
  let groupFailed = false;
3388
2801
  for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
3389
2802
  const rule = group.rules[ruleIndex];
3390
- const violation = evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
2803
+ const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
3391
2804
  if (violation !== null) {
3392
2805
  allViolations.push(violation);
3393
2806
  if (ABORT_VIOLATION_CODES.has(violation.code)) return {
@@ -3408,170 +2821,183 @@ function check(policy, callData, context) {
3408
2821
  violations: allViolations
3409
2822
  };
3410
2823
  }
3411
- /**
3412
- * Enforce a policy against ABI-encoded call data, throwing on violation.
3413
- * @param policy - Binary policy blob as 0x-prefixed hex string.
3414
- * @param callData - ABI-encoded call data as 0x-prefixed hex string.
3415
- * @param context - Optional execution context for context-scoped rules.
3416
- * @throws {PolicyViolationError} If the policy rejects the call data.
3417
- * @throws {CallciumError} If the policy blob is structurally malformed.
3418
- */
3419
2824
  function enforce(policy, callData, context) {
3420
2825
  const result = check(policy, callData, context);
3421
2826
  if (!result.ok) throw new PolicyViolationError(result.violations);
3422
2827
  }
3423
- /** Evaluate a single rule against calldata or context, returning a violation or null on pass. */
3424
- function evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context) {
3425
- const scope = rule.scope.value;
3426
- const pathBytes = hexToBytes(rule.path.value);
2828
+ function fieldBytes(policyBytes, field) {
2829
+ return policyBytes.subarray(field.span.start, field.span.end);
2830
+ }
2831
+ function evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context) {
3427
2832
  const opCode = rule.opCode.value;
3428
- const operandData = hexToBytes(rule.data.value);
3429
- if (scope === Scope.CONTEXT) return evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
3430
- const locResult = locate(tree, callDataBytes, pathBytes, baseOffset);
3431
- if (!locResult.ok) return {
3432
- group: groupIndex,
3433
- rule: ruleIndex,
3434
- code: locResult.code,
3435
- scope: Scope.CALLDATA,
3436
- path: rule.path.value,
3437
- opCode,
3438
- operandData: rule.data.value
3439
- };
3440
- if (locResult.type === "quantifier") return evaluateQuantifier(locResult.quantifier, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
3441
- return evaluateLeaf(locResult.location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
2833
+ const operandData = fieldBytes(policyBytes, rule.data);
2834
+ if (rule.scope.value === Scope.CONTEXT) return evaluateContextRule(fieldBytes(policyBytes, rule.path), opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
2835
+ if (rule.hint === void 0) throw new CallciumError("MALFORMED_HINT", "Calldata rule carries no hint block");
2836
+ return evaluateCalldataRule(fieldBytes(policyBytes, rule.hint), callDataBytes, baseOffset, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
3442
2837
  }
3443
- /** Evaluate a context-scoped rule by resolving the property from the execution context. */
3444
- function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
3445
- const propertyId = readU16(pathBytes, 0);
3446
- if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PATH", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
3447
- const propInfo = lookupContextProperty(propertyId);
3448
- const contextValue = context?.[propInfo.contextKey];
3449
- if (contextValue === void 0) return {
3450
- group: groupIndex,
3451
- rule: ruleIndex,
3452
- code: "MISSING_CONTEXT",
3453
- scope: Scope.CONTEXT,
3454
- path: pathHex,
3455
- typeCode: propInfo.typeCode
3456
- };
3457
- let value;
3458
- if (typeof contextValue === "string") value = addressToBigInt(contextValue);
3459
- else value = contextValue;
3460
- if (!applyOperator(opCode, value, 32, operandData, propInfo.typeCode)) return {
3461
- group: groupIndex,
3462
- rule: ruleIndex,
3463
- code: "VALUE_MISMATCH",
3464
- scope: Scope.CONTEXT,
3465
- path: pathHex,
3466
- opCode,
3467
- operandData: bytesToHex(operandData),
3468
- typeCode: propInfo.typeCode,
3469
- resolvedValue: bigintToHex(value)
3470
- };
3471
- return null;
2838
+ function readTargetBlock(hint, targetOffset) {
2839
+ return {
2840
+ targetDelta: readU32(hint, targetOffset),
2841
+ targetMeta: readU16(hint, targetOffset + PolicyFormat.HINT_TARGET_META_OFFSET),
2842
+ typeCode: hint[targetOffset + PolicyFormat.HINT_TARGET_TYPECODE_OFFSET]
2843
+ };
2844
+ }
2845
+ function follow(callData, from, at) {
2846
+ const word = readPointer(callData, at);
2847
+ if (typeof word !== "number") return word;
2848
+ return from + word;
2849
+ }
2850
+ function chainResolve(hint, callData, base, hopsOffset, hopCount) {
2851
+ for (let i = 0; i < hopCount; i++) {
2852
+ const hop = hopsOffset + i * PolicyFormat.HINT_HOP_SIZE;
2853
+ const index = readU16(hint, hop + PolicyFormat.HINT_HOP_INDEX_OFFSET);
2854
+ if (index === PolicyFormat.HINT_NO_INDEX) {
2855
+ const next = follow(callData, base, base + readU32(hint, hop));
2856
+ if (typeof next !== "number") return next;
2857
+ base = next;
2858
+ continue;
2859
+ }
2860
+ const meta = readU16(hint, hop + PolicyFormat.HINT_HOP_META_OFFSET);
2861
+ let elems = base;
2862
+ if ((meta & PolicyFormat.HINT_META_DYNAMIC_ARRAY) !== 0) {
2863
+ const length = readPointer(callData, elems);
2864
+ if (typeof length !== "number") return length;
2865
+ if (index >= length) return { code: "ARRAY_INDEX_OUT_OF_BOUNDS" };
2866
+ elems += 32;
2867
+ }
2868
+ const slot = elems + index * (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
2869
+ if ((meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0) {
2870
+ const next = follow(callData, elems, slot);
2871
+ if (typeof next !== "number") return next;
2872
+ base = next;
2873
+ } else base = slot;
2874
+ }
2875
+ return base;
3472
2876
  }
3473
- /**
3474
- * Load a value from calldata at the given location and apply the operator.
3475
- *
3476
- * `resolvedValue` is normalised at the decision point: length operators encode
3477
- * the byte/element count as a hex bigint; scalar operators preserve the full
3478
- * 32-byte ABI word so downstream rendering can decode left-aligned `bytesN` faithfully.
3479
- */
3480
- function applyLeafOperator(callDataBytes, location, opCode, operandData) {
3481
- const { node } = location;
3482
- if (isLengthOp(opCode)) {
3483
- let valueLength;
3484
- if (node.isDynamic) {
3485
- const lengthResult = loadSlice(callDataBytes, location);
3486
- if (!lengthResult.ok) return { error: lengthResult.code };
3487
- valueLength = lengthResult.length;
3488
- } else valueLength = node.staticSize;
2877
+ function evalTarget(callData, target, block, opCode, operandData) {
2878
+ const typeCode = block.typeCode;
2879
+ if (isLengthValidType(typeCode)) {
2880
+ if (!isLengthOp(opCode)) throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word.");
2881
+ const length = readPointer(callData, target);
2882
+ if (typeof length !== "number") return { error: length.code };
2883
+ const stride = typeCode === TypeCode.DYNAMIC_ARRAY ? (block.targetMeta & PolicyFormat.HINT_META_STRIDE_MASK) * 32 : 1;
2884
+ const room = callData.length - (target + 32);
2885
+ if (stride !== 0 && length > Math.floor(room / stride)) return { error: "CALLDATA_OUT_OF_BOUNDS" };
3489
2886
  return {
3490
- passed: applyOperator(opCode, 0n, valueLength, operandData, node.typeCode),
3491
- resolvedValue: bigintToHex(BigInt(valueLength))
2887
+ passed: applyOperator(opCode, 0n, length, operandData, typeCode),
2888
+ value: BigInt(length)
3492
2889
  };
3493
2890
  }
3494
- if (node.isDynamic || node.type !== "elementary") throw new CallciumError("NOT_SCALAR", "Value operator on a target without a scalar word.");
3495
- const result = loadScalar(callDataBytes, location);
3496
- if (!result.ok) return { error: result.code };
3497
- const value = toBigInt(result.value, 0);
3498
- if (canonicalize(value, node.typeCode) !== value) return {
2891
+ if (classifyTypeCode(typeCode).typeClass !== "elementary") throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word.");
2892
+ const word = loadWord(callData, target);
2893
+ if (!(word instanceof Uint8Array)) return { error: word.code };
2894
+ const value = toBigInt(word, 0);
2895
+ if (canonicalize(value, typeCode) !== value) return {
3499
2896
  error: "NON_CANONICAL_VALUE",
3500
- resolvedValue: bigintToHex(value)
2897
+ value
3501
2898
  };
3502
2899
  return {
3503
- passed: applyOperator(opCode, value, 32, operandData, node.typeCode),
3504
- resolvedValue: bigintToHex(value)
2900
+ passed: applyOperator(opCode, value, 32, operandData, typeCode),
2901
+ value
2902
+ };
2903
+ }
2904
+ function navigationViolation(frame, code, elementIndex) {
2905
+ return {
2906
+ group: frame.group,
2907
+ rule: frame.rule,
2908
+ code,
2909
+ scope: Scope.CALLDATA,
2910
+ path: frame.path,
2911
+ opCode: frame.opCode,
2912
+ operandData: bytesToHex(frame.operandData),
2913
+ typeCode: frame.typeCode,
2914
+ ...elementIndex !== void 0 && { elementIndex }
3505
2915
  };
3506
2916
  }
3507
- /** Evaluate a leaf rule, producing a violation on failure or null on pass. */
3508
- function evaluateLeaf(location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3509
- const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
3510
- const typeCode = location.node.typeCode;
2917
+ function targetViolation(frame, result, elementIndex) {
3511
2918
  if ("error" in result) {
3512
2919
  if (result.error === "NON_CANONICAL_VALUE") return {
3513
- group: groupIndex,
3514
- rule: ruleIndex,
2920
+ group: frame.group,
2921
+ rule: frame.rule,
3515
2922
  code: result.error,
3516
2923
  scope: Scope.CALLDATA,
3517
- path: pathHex,
3518
- opCode,
3519
- operandData: bytesToHex(operandData),
3520
- typeCode,
3521
- resolvedValue: result.resolvedValue
3522
- };
3523
- return {
3524
- group: groupIndex,
3525
- rule: ruleIndex,
3526
- code: result.error,
3527
- scope: Scope.CALLDATA,
3528
- path: pathHex,
3529
- opCode,
3530
- operandData: bytesToHex(operandData),
3531
- typeCode
2924
+ path: frame.path,
2925
+ opCode: frame.opCode,
2926
+ operandData: bytesToHex(frame.operandData),
2927
+ typeCode: frame.typeCode,
2928
+ resolvedValue: bigintToHex(result.value),
2929
+ ...elementIndex !== void 0 && { elementIndex }
3532
2930
  };
2931
+ return navigationViolation(frame, result.error, elementIndex);
3533
2932
  }
3534
- if (!result.passed) return {
3535
- group: groupIndex,
3536
- rule: ruleIndex,
2933
+ if (result.passed) return null;
2934
+ return {
2935
+ group: frame.group,
2936
+ rule: frame.rule,
3537
2937
  code: "VALUE_MISMATCH",
3538
2938
  scope: Scope.CALLDATA,
2939
+ path: frame.path,
2940
+ opCode: frame.opCode,
2941
+ operandData: bytesToHex(frame.operandData),
2942
+ typeCode: frame.typeCode,
2943
+ resolvedValue: bigintToHex(result.value),
2944
+ ...elementIndex !== void 0 && { elementIndex }
2945
+ };
2946
+ }
2947
+ function evaluateCalldataRule(hint, callDataBytes, baseOffset, opCode, operandData, groupIndex, ruleIndex, pathHex) {
2948
+ const header = hint[0];
2949
+ const hopCount = header & PolicyFormat.HINT_HOP_COUNT_MASK;
2950
+ const kind = header >> PolicyFormat.HINT_KIND_SHIFT;
2951
+ const hopsOffset = PolicyFormat.HINT_HEADER_SIZE;
2952
+ if (kind !== PolicyFormat.HINT_KIND_NONE) return evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex);
2953
+ const block = readTargetBlock(hint, hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE);
2954
+ const frame = {
2955
+ group: groupIndex,
2956
+ rule: ruleIndex,
3539
2957
  path: pathHex,
3540
2958
  opCode,
3541
- operandData: bytesToHex(operandData),
3542
- typeCode,
3543
- resolvedValue: result.resolvedValue
3544
- };
3545
- return null;
3546
- }
3547
- /** Evaluate a quantified rule by iterating over array elements with ALL/ANY semantics. */
3548
- function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3549
- const { quantifier, location: arrayLocation, remainingPath } = qResult;
3550
- const operandHex = bytesToHex(operandData);
3551
- const arrayNode = arrayLocation.node;
3552
- const leafTypeCode = resolveLeafTypeCode(arrayNode.type === "staticArray" || arrayNode.type === "dynamicArray" ? arrayNode.element : arrayNode, remainingPath);
3553
- const shapeResult = arrayShape(callDataBytes, arrayLocation);
3554
- if (!shapeResult.ok) return {
2959
+ operandData,
2960
+ typeCode: block.typeCode
2961
+ };
2962
+ const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
2963
+ if (typeof base !== "number") return navigationViolation(frame, base.code);
2964
+ return targetViolation(frame, evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData));
2965
+ }
2966
+ function evaluateQuantified(hint, callDataBytes, baseOffset, kind, hopsOffset, hopCount, opCode, operandData, groupIndex, ruleIndex, pathHex) {
2967
+ const frameOffset = hopsOffset + hopCount * PolicyFormat.HINT_HOP_SIZE;
2968
+ const suffixHeaderOffset = frameOffset + PolicyFormat.HINT_FRAME_PREFIX_SIZE;
2969
+ const suffixHopCount = hint[suffixHeaderOffset] & PolicyFormat.HINT_HOP_COUNT_MASK;
2970
+ const suffixHopsOffset = suffixHeaderOffset + PolicyFormat.HINT_HEADER_SIZE;
2971
+ const block = readTargetBlock(hint, suffixHopsOffset + suffixHopCount * PolicyFormat.HINT_HOP_SIZE);
2972
+ const frame = {
3555
2973
  group: groupIndex,
3556
2974
  rule: ruleIndex,
3557
- code: shapeResult.code,
3558
- scope: Scope.CALLDATA,
3559
2975
  path: pathHex,
3560
2976
  opCode,
3561
- operandData: operandHex,
3562
- typeCode: leafTypeCode
3563
- };
3564
- const shape = shapeResult.shape;
3565
- if (shape.length > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) return {
2977
+ operandData,
2978
+ typeCode: block.typeCode
2979
+ };
2980
+ const chained = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
2981
+ if (typeof chained !== "number") return navigationViolation(frame, chained.code);
2982
+ let elems = chained + readU32(hint, frameOffset);
2983
+ let count = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_COUNT_OFFSET);
2984
+ if (count === 0) {
2985
+ const length = readPointer(callDataBytes, elems);
2986
+ if (typeof length !== "number") return navigationViolation(frame, length.code);
2987
+ count = length;
2988
+ elems += 32;
2989
+ }
2990
+ if (count > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) return {
3566
2991
  group: groupIndex,
3567
2992
  rule: ruleIndex,
3568
2993
  code: "QUANTIFIER_LIMIT_EXCEEDED",
3569
2994
  scope: Scope.CALLDATA,
3570
2995
  path: pathHex,
3571
- resolvedValue: bigintToHex(BigInt(shape.length))
2996
+ resolvedValue: bigintToHex(BigInt(count))
3572
2997
  };
3573
- if (shape.length === 0) {
3574
- if (quantifier === Quantifier.ALL_OR_EMPTY) return null;
2998
+ const isUniversal = kind === PolicyFormat.HINT_KIND_ALL;
2999
+ if (count === 0) {
3000
+ if (isUniversal) return null;
3575
3001
  return {
3576
3002
  group: groupIndex,
3577
3003
  rule: ruleIndex,
@@ -3580,84 +3006,28 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
3580
3006
  path: pathHex
3581
3007
  };
3582
3008
  }
3583
- const isUniversal = quantifier === Quantifier.ALL_OR_EMPTY || quantifier === Quantifier.ALL;
3584
- const hasSuffix = remainingPath.length > 0;
3585
- for (let elemIndex = 0; elemIndex < shape.length; elemIndex++) {
3586
- const elemResult = arrayElementAt(shape, elemIndex, callDataBytes);
3587
- if (!elemResult.ok) {
3588
- if (isUniversal) return {
3589
- group: groupIndex,
3590
- rule: ruleIndex,
3591
- code: elemResult.code,
3592
- scope: Scope.CALLDATA,
3593
- path: pathHex,
3594
- opCode,
3595
- operandData: operandHex,
3596
- typeCode: leafTypeCode,
3597
- elementIndex: elemIndex
3598
- };
3599
- continue;
3009
+ const meta = readU16(hint, frameOffset + PolicyFormat.HINT_FRAME_META_OFFSET);
3010
+ const elemStride = (meta & PolicyFormat.HINT_META_STRIDE_MASK) * 32;
3011
+ const elemIsDynamic = (meta & PolicyFormat.HINT_META_ELEM_DYNAMIC) !== 0;
3012
+ for (let elemIndex = 0; elemIndex < count; elemIndex++) {
3013
+ const slot = elems + elemIndex * elemStride;
3014
+ let elem = slot;
3015
+ if (elemIsDynamic) {
3016
+ const followed = follow(callDataBytes, elems, slot);
3017
+ if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
3018
+ elem = followed;
3600
3019
  }
3601
- const elemLocation = elemResult.location;
3602
- let leafLocation;
3603
- if (hasSuffix) {
3604
- const navResult = descendPath(callDataBytes, elemLocation, remainingPath);
3605
- if (!navResult.ok) {
3606
- if (isUniversal) return {
3607
- group: groupIndex,
3608
- rule: ruleIndex,
3609
- code: navResult.code,
3610
- scope: Scope.CALLDATA,
3611
- path: pathHex,
3612
- opCode,
3613
- operandData: operandHex,
3614
- typeCode: leafTypeCode,
3615
- elementIndex: elemIndex
3616
- };
3617
- continue;
3618
- }
3619
- leafLocation = navResult.location;
3620
- } else leafLocation = elemLocation;
3621
- const applied = applyLeafOperator(callDataBytes, leafLocation, opCode, operandData);
3622
- if ("error" in applied) {
3623
- if (applied.error === "NON_CANONICAL_VALUE") return {
3624
- group: groupIndex,
3625
- rule: ruleIndex,
3626
- code: applied.error,
3627
- scope: Scope.CALLDATA,
3628
- path: pathHex,
3629
- opCode,
3630
- operandData: operandHex,
3631
- typeCode: leafTypeCode,
3632
- elementIndex: elemIndex,
3633
- resolvedValue: applied.resolvedValue
3634
- };
3635
- if (isUniversal) return {
3636
- group: groupIndex,
3637
- rule: ruleIndex,
3638
- code: applied.error,
3639
- scope: Scope.CALLDATA,
3640
- path: pathHex,
3641
- opCode,
3642
- operandData: operandHex,
3643
- typeCode: leafTypeCode,
3644
- elementIndex: elemIndex
3645
- };
3646
- continue;
3020
+ let base = elem;
3021
+ if (suffixHopCount > 0) {
3022
+ const chainedElem = chainResolve(hint, callDataBytes, elem, suffixHopsOffset, suffixHopCount);
3023
+ if (typeof chainedElem !== "number") return navigationViolation(frame, chainedElem.code, elemIndex);
3024
+ base = chainedElem;
3647
3025
  }
3648
- if (isUniversal && !applied.passed) return {
3649
- group: groupIndex,
3650
- rule: ruleIndex,
3651
- code: "VALUE_MISMATCH",
3652
- scope: Scope.CALLDATA,
3653
- path: pathHex,
3654
- opCode,
3655
- operandData: operandHex,
3656
- typeCode: leafTypeCode,
3657
- resolvedValue: applied.resolvedValue,
3658
- elementIndex: elemIndex
3659
- };
3660
- if (!isUniversal && applied.passed) return null;
3026
+ const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData);
3027
+ if ("error" in applied) return targetViolation(frame, applied, elemIndex);
3028
+ if (applied.passed) {
3029
+ if (!isUniversal) return null;
3030
+ } else if (isUniversal) return targetViolation(frame, applied, elemIndex);
3661
3031
  }
3662
3032
  if (isUniversal) return null;
3663
3033
  return {
@@ -3667,16 +3037,44 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
3667
3037
  scope: Scope.CALLDATA,
3668
3038
  path: pathHex,
3669
3039
  opCode,
3670
- operandData: operandHex,
3671
- typeCode: leafTypeCode
3040
+ operandData: bytesToHex(operandData),
3041
+ typeCode: block.typeCode
3042
+ };
3043
+ }
3044
+ function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
3045
+ const propertyId = readU16(pathBytes, 0);
3046
+ if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
3047
+ const propInfo = lookupContextProperty(propertyId);
3048
+ const contextValue = context?.[propInfo.contextKey];
3049
+ if (contextValue === void 0) return {
3050
+ group: groupIndex,
3051
+ rule: ruleIndex,
3052
+ code: "MISSING_CONTEXT",
3053
+ scope: Scope.CONTEXT,
3054
+ path: pathHex,
3055
+ typeCode: propInfo.typeCode
3056
+ };
3057
+ let value;
3058
+ if (typeof contextValue === "string") value = addressToBigInt(contextValue);
3059
+ else value = contextValue;
3060
+ if (!applyOperator(opCode, value, 32, operandData, propInfo.typeCode)) return {
3061
+ group: groupIndex,
3062
+ rule: ruleIndex,
3063
+ code: "VALUE_MISMATCH",
3064
+ scope: Scope.CONTEXT,
3065
+ path: pathHex,
3066
+ opCode,
3067
+ operandData: bytesToHex(operandData),
3068
+ typeCode: propInfo.typeCode,
3069
+ resolvedValue: bigintToHex(value)
3672
3070
  };
3071
+ return null;
3673
3072
  }
3674
- /** Check and enforce policies against ABI-encoded call data. */
3675
3073
  const PolicyEnforcer = {
3676
3074
  check,
3677
3075
  enforce
3678
3076
  };
3679
3077
  //#endregion
3680
- export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
3078
+ export { CallciumError, ContextProperty, Descriptor, DescriptorCoder, Limits, Op, PolicyBuilder, PolicyCoder, PolicyEnforcer, PolicyFormat, PolicyValidator, PolicyViolationError, Quantifier, Scope, TypeCode, ValidationError, arg, baseFee, blockNumber, blockTimestamp, bytesToHex, chainId, gasPrice, hexToBytes, isLengthOp, isOpAllowed, isQuantifier, lookupContextProperty, lookupOp, lookupQuantifier, lookupScope, lookupTypeCode, msgSender, msgValue, parsePathSteps, toAddress, txOrigin };
3681
3079
 
3682
3080
  //# sourceMappingURL=index.js.map