@callcium/sdk 0.5.0 → 0.7.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
1
  import { keccak_256 } from "@noble/hashes/sha3";
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 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 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,43 +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
- if (typeCode === TypeCode.STATIC_ARRAY && isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
534
+ if (typeCode === TypeCode.STATIC_ARRAY) if (isQuantifier(step)) quantifiedStaticLength = staticArrayLength(desc, cursor);
535
+ else {
536
+ const length = staticArrayLength(desc, cursor);
537
+ if (step >= length) throw new CallciumError("STATIC_ARRAY_INDEX_OUT_OF_BOUNDS", `Array index ${step} out of range (length=${length}).`);
538
+ }
607
539
  cursor = arrayElementOffset(cursor);
608
- } 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}.`);
609
541
  }
610
542
  return {
611
543
  typeInfo: inspect$1(desc, cursor),
612
544
  quantifiedStaticLength
613
545
  };
614
546
  }
615
- /**
616
- * Return tuple field count at a tuple node offset.
617
- * @param desc - Raw descriptor bytes.
618
- * @param offset - Byte position of the tuple node.
619
- */
620
547
  function tupleFieldCount(desc, offset) {
621
548
  return readU16(desc, offset + 4);
622
549
  }
623
- /**
624
- * Return static array length at a static array node offset.
625
- * @param desc - Raw descriptor bytes.
626
- * @param offset - Byte position of the static array node.
627
- */
628
550
  function staticArrayLength(desc, offset) {
629
551
  const elemOffset = arrayElementOffset(offset);
630
552
  return readU16(desc, elemOffset + nodeLength(desc, elemOffset));
631
553
  }
632
- /**
633
- * Return the byte offset of the array element descriptor.
634
- * @param offset - Byte position of the array node (static or dynamic).
635
- */
636
554
  function arrayElementOffset(offset) {
637
555
  return offset + DescriptorFormat.ARRAY_HEADER_SIZE;
638
556
  }
639
- /** 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
+ }
640
687
  const Descriptor = {
641
688
  paramCount,
642
689
  paramOffset,
@@ -647,88 +694,47 @@ const Descriptor = {
647
694
  staticArrayLength,
648
695
  nodeLength,
649
696
  arrayElementOffset,
650
- walkPath
697
+ walkPath,
698
+ compileHint
651
699
  };
652
700
  //#endregion
653
701
  //#region src/type-desc.ts
654
- /**
655
- * Extract the staticWords count from a descriptor node.
656
- *
657
- * For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
658
- * BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
659
- * reads the 24-bit meta field at offset 1 and shifts right by
660
- * META_STATIC_WORDS_SHIFT.
661
- */
662
702
  function extractStaticWords(desc) {
663
703
  const typeCode = desc[0];
664
704
  if (typeCode < TypeCode.STATIC_ARRAY) return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
665
705
  return readU24(desc, 1) >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
666
706
  }
667
- /** Build a descriptor node for `address`. */
668
707
  function address() {
669
708
  return new Uint8Array([TypeCode.ADDRESS]);
670
709
  }
671
- /** Build a descriptor node for `bool`. */
672
710
  function bool() {
673
711
  return new Uint8Array([TypeCode.BOOL]);
674
712
  }
675
- /** Build a descriptor node for `function`. */
676
713
  function function_() {
677
714
  return new Uint8Array([TypeCode.FUNCTION]);
678
715
  }
679
- /** Build a descriptor node for `uint256`. */
680
- function uint256() {
681
- return new Uint8Array([TypeCode.UINT_MAX]);
682
- }
683
- /**
684
- * Build a descriptor node for `uintN`.
685
- * @param bits - Bit width, 8–256 in steps of 8.
686
- * @throws {CallciumError} If bits is out of range or not a multiple of 8.
687
- */
688
716
  function uintN(bits) {
689
- 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.`);
690
718
  return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
691
719
  }
692
- /**
693
- * Build a descriptor node for `intN`.
694
- * @param bits - Bit width, 8–256 in steps of 8.
695
- * @throws {CallciumError} If bits is out of range or not a multiple of 8.
696
- */
697
720
  function intN(bits) {
698
- 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.`);
699
722
  return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
700
723
  }
701
- /** Build a descriptor node for `bytes` (dynamic). */
702
724
  function bytes() {
703
725
  return new Uint8Array([TypeCode.BYTES]);
704
726
  }
705
- /** Build a descriptor node for `string`. */
706
727
  function string_() {
707
728
  return new Uint8Array([TypeCode.STRING]);
708
729
  }
709
- /**
710
- * Build a descriptor node for `bytesN`.
711
- * @param n - Byte width, 1–32.
712
- * @throws {CallciumError} If n is out of range.
713
- */
714
730
  function bytesN(n) {
715
- 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.`);
716
732
  return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
717
733
  }
718
- /**
719
- * Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
720
- *
721
- * Dynamic form: `[0x81][meta:3][elemDesc]`
722
- * Static form: `[0x80][meta:3][elemDesc][length:be16]`
723
- *
724
- * @param elemDesc - Descriptor bytes for the element type.
725
- * @param length - Fixed array length. When omitted, produces a dynamic array.
726
- * @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
727
- */
728
734
  function array(elemDesc, length) {
729
735
  if (length === void 0) {
730
736
  const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length;
731
- 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}.`);
732
738
  const meta24 = 0 << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
733
739
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length);
734
740
  buf[0] = TypeCode.DYNAMIC_ARRAY;
@@ -738,10 +744,10 @@ function array(elemDesc, length) {
738
744
  }
739
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}.`);
740
746
  const nodeLength = DescriptorFormat.ARRAY_HEADER_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE;
741
- 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}.`);
742
748
  const elemStaticWords = extractStaticWords(elemDesc);
743
749
  const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
744
- 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}.`);
745
751
  const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
746
752
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + elemDesc.length + DescriptorFormat.ARRAY_LENGTH_SIZE);
747
753
  buf[0] = TypeCode.STATIC_ARRAY;
@@ -750,19 +756,11 @@ function array(elemDesc, length) {
750
756
  writeBE16(buf, 4 + elemDesc.length, length);
751
757
  return buf;
752
758
  }
753
- /**
754
- * Build a descriptor node for a tuple.
755
- *
756
- * Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
757
- *
758
- * @param fieldDescs - Descriptor bytes for each field, in order.
759
- * @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
760
- */
761
759
  function tuple(fieldDescs) {
762
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}.`);
763
761
  const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
764
762
  const nodeLength = DescriptorFormat.TUPLE_HEADER_SIZE + totalFieldBytes;
765
- 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}.`);
766
764
  let anyDynamic = false;
767
765
  let staticWordsSum = 0;
768
766
  for (const field of fieldDescs) {
@@ -771,7 +769,7 @@ function tuple(fieldDescs) {
771
769
  staticWordsSum += staticWords;
772
770
  }
773
771
  const staticWords = anyDynamic ? 0 : staticWordsSum;
774
- 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}.`);
775
773
  const meta24 = staticWords << DescriptorFormat.META_STATIC_WORDS_SHIFT | nodeLength;
776
774
  const buf = new Uint8Array(1 + DescriptorFormat.COMPOSITE_META_SIZE + DescriptorFormat.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
777
775
  buf[0] = TypeCode.TUPLE;
@@ -786,10 +784,6 @@ function tuple(fieldDescs) {
786
784
  }
787
785
  //#endregion
788
786
  //#region src/descriptor-coder.ts
789
- /**
790
- * Collect the positions of depth-0 commas in input[start..end).
791
- * Commas inside `()` or `[]` are skipped.
792
- */
793
787
  function commaPositions(input, start, end) {
794
788
  const positions = [];
795
789
  let depth = 0;
@@ -801,26 +795,21 @@ function commaPositions(input, start, end) {
801
795
  }
802
796
  return positions;
803
797
  }
804
- /**
805
- * Parse a decimal integer from input[start..end).
806
- * Returns -1 if the substring is empty or contains non-digit characters.
807
- */
798
+ const MAX_TYPE_STRING_NUMBER = 256 ** DescriptorFormat.ARRAY_LENGTH_SIZE - 1;
808
799
  function parseUint(input, start, end) {
809
- if (start >= end) 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}`);
810
802
  let value = 0;
811
803
  for (let i = start; i < end; i++) {
812
804
  const code = input.charCodeAt(i);
813
- 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}`);
814
806
  value = value * 10 + (code - 48);
815
807
  }
808
+ if (value > MAX_TYPE_STRING_NUMBER) throw new CallciumError("MALFORMED_TYPE_STRING", `Number at position ${start} is too wide`);
816
809
  return value;
817
810
  }
818
- /**
819
- * Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
820
- * `start` and the substring to parse ends before `end`.
821
- */
822
811
  function parseTuple(input, start, end) {
823
- 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}`);
824
813
  let depth = 0;
825
814
  let closePos = -1;
826
815
  for (let i = start; i < end; i++) if (input[i] === "(") depth++;
@@ -831,7 +820,8 @@ function parseTuple(input, start, end) {
831
820
  break;
832
821
  }
833
822
  }
834
- 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}`);
835
825
  const innerStart = start + 1;
836
826
  const innerEnd = closePos;
837
827
  const commas = commaPositions(input, innerStart, innerEnd);
@@ -842,10 +832,9 @@ function parseTuple(input, start, end) {
842
832
  segStart = comma + 1;
843
833
  }
844
834
  segments.push([segStart, innerEnd]);
845
- 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([]);
846
836
  return tuple(segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd)));
847
837
  }
848
- /** Parse the base type (no array suffixes) from input[start..end). */
849
838
  function parseBaseType(input, start, end) {
850
839
  const segment = input.slice(start, end);
851
840
  if (segment === "address") return address();
@@ -853,63 +842,31 @@ function parseBaseType(input, start, end) {
853
842
  if (segment === "function") return function_();
854
843
  if (segment === "bytes") return bytes();
855
844
  if (segment === "string") return string_();
856
- if (segment === "uint256") return uint256();
857
- if (segment === "int256") return intN(256);
858
845
  if (segment.startsWith("(")) return parseTuple(input, start, end);
859
- if (segment.startsWith("uint")) {
860
- const bits = parseUint(input, start + 4, end);
861
- if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
862
- return uintN(bits);
863
- }
864
- if (segment.startsWith("int")) {
865
- const bits = parseUint(input, start + 3, end);
866
- if (bits === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
867
- return intN(bits);
868
- }
869
- if (segment.startsWith("bytes")) {
870
- const n = parseUint(input, start + 5, end);
871
- if (n === -1) throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
872
- return bytesN(n);
873
- }
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));
874
849
  throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
875
850
  }
876
- /**
877
- * Parse a type segment from input[start..end).
878
- *
879
- * Scans backward from end to collect all `[...]` suffixes, identifies the
880
- * base type extent, parses the base, then applies suffixes left-to-right.
881
- */
882
851
  function parseType(input, start, end) {
883
- 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");
884
853
  const suffixes = [];
885
854
  let baseEnd = end;
886
855
  while (baseEnd > start && input[baseEnd - 1] === "]") {
887
856
  const closePos = baseEnd - 1;
888
857
  let openPos = closePos - 1;
889
858
  while (openPos > start && input[openPos] !== "[") openPos--;
890
- 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}`);
891
860
  const innerStart = openPos + 1;
892
861
  const innerEnd = closePos;
893
- if (innerStart === innerEnd) suffixes.unshift(void 0);
894
- else {
895
- const length = parseUint(input, innerStart, innerEnd);
896
- if (length === -1) throw new CallciumError("INVALID_TYPE_STRING", `Invalid array length at position ${innerStart}`);
897
- suffixes.unshift(length);
898
- }
862
+ suffixes.unshift(innerStart === innerEnd ? void 0 : parseUint(input, innerStart, innerEnd));
899
863
  baseEnd = openPos;
900
864
  }
865
+ if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
901
866
  let desc = parseBaseType(input, start, baseEnd);
902
867
  for (const length of suffixes) desc = array(desc, length);
903
868
  return desc;
904
869
  }
905
- /**
906
- * Encode a comma-separated list of ABI type strings into a binary descriptor.
907
- *
908
- * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
909
- * @returns Binary descriptor bytes starting with the version+paramCount header.
910
- * @throws {CallciumError} With code `INVALID_TYPE_STRING` for malformed input.
911
- * @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
912
- */
913
870
  function fromTypes(typesCsv) {
914
871
  if (typesCsv === "") return new Uint8Array([DescriptorFormat.VERSION, 0]);
915
872
  const commas = commaPositions(typesCsv, 0, typesCsv.length);
@@ -921,7 +878,7 @@ function fromTypes(typesCsv) {
921
878
  }
922
879
  segments.push([segStart, typesCsv.length]);
923
880
  const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
924
- 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}.`);
925
882
  const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
926
883
  const result = new Uint8Array(DescriptorFormat.HEADER_SIZE + totalBytes);
927
884
  result[0] = DescriptorFormat.VERSION;
@@ -933,7 +890,6 @@ function fromTypes(typesCsv) {
933
890
  }
934
891
  return result;
935
892
  }
936
- /** Reconstruct a Solidity type string from a descriptor node at the given offset. */
937
893
  function nodeToTypeString(desc, offset) {
938
894
  const typeCode = desc[offset];
939
895
  if (typeCode === TypeCode.TUPLE) {
@@ -953,15 +909,6 @@ function nodeToTypeString(desc, offset) {
953
909
  if (typeCode === TypeCode.DYNAMIC_ARRAY) return `${nodeToTypeString(desc, Descriptor.arrayElementOffset(offset))}[]`;
954
910
  return lookupTypeCode(typeCode).label;
955
911
  }
956
- /**
957
- * Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
958
- *
959
- * Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
960
- *
961
- * @param desc - Binary descriptor bytes (with version+paramCount header).
962
- * @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
963
- * @throws {CallciumError} If the descriptor is malformed.
964
- */
965
912
  function toTypes(desc) {
966
913
  const count = Descriptor.paramCount(desc);
967
914
  const types = [];
@@ -972,23 +919,15 @@ function toTypes(desc) {
972
919
  }
973
920
  return types.join(",");
974
921
  }
975
- /** Recursively parse a single descriptor node starting at offset. */
976
922
  function parseNode(data, offset, depth) {
977
923
  if (offset >= data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
978
924
  const code = data[offset];
979
925
  const info = classifyTypeCode(code);
980
926
  const metaOffset = offset + DescriptorFormat.TYPECODE_SIZE;
981
927
  if (info.typeClass === "elementary") return {
982
- node: {
983
- type: "elementary",
984
- typeCode: code,
985
- isDynamic: info.isDynamic,
986
- staticSize: info.isDynamic ? 0 : 32,
987
- span: {
988
- start: offset,
989
- end: metaOffset
990
- }
991
- },
928
+ typeCode: code,
929
+ isDynamic: info.isDynamic,
930
+ staticSize: info.isDynamic ? 0 : 32,
992
931
  next: metaOffset
993
932
  };
994
933
  if (depth > DescriptorFormat.MAX_NESTING_DEPTH) throw new CallciumError("NESTING_TOO_DEEP", `Composite nesting exceeds maximum depth ${DescriptorFormat.MAX_NESTING_DEPTH}`, offset);
@@ -998,7 +937,7 @@ function parseNode(data, offset, depth) {
998
937
  const staticWords = meta >> DescriptorFormat.META_STATIC_WORDS_SHIFT;
999
938
  const nodeLength = meta & DescriptorFormat.META_NODE_LENGTH_MASK;
1000
939
  const minHeader = info.typeClass === "tuple" ? DescriptorFormat.TUPLE_HEADER_SIZE : DescriptorFormat.ARRAY_HEADER_SIZE;
1001
- 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);
1002
941
  if (offset + nodeLength > data.length) throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
1003
942
  const isDynamic = staticWords === 0;
1004
943
  const staticSize = isDynamic ? 0 : staticWords * 32;
@@ -1009,89 +948,54 @@ function parseNode(data, offset, depth) {
1009
948
  const fieldCount = readU16(data, fieldCountOffset);
1010
949
  if (fieldCount === 0) throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
1011
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);
1012
- const fields = [];
1013
951
  let cursor = offset + DescriptorFormat.TUPLE_HEADER_SIZE;
1014
- for (let i = 0; i < fieldCount; i++) {
1015
- const result = parseNode(data, cursor, depth + 1);
1016
- fields.push(result.node);
1017
- cursor = result.next;
1018
- }
952
+ for (let i = 0; i < fieldCount; i++) cursor = parseNode(data, cursor, depth + 1).next;
1019
953
  return {
1020
- node: {
1021
- type: "tuple",
1022
- typeCode: code,
1023
- isDynamic,
1024
- staticSize,
1025
- fields,
1026
- span: {
1027
- start: offset,
1028
- end: nodeEnd
1029
- }
1030
- },
954
+ typeCode: code,
955
+ isDynamic,
956
+ staticSize,
1031
957
  next: nodeEnd
1032
958
  };
1033
959
  }
1034
960
  if (info.typeClass === "staticArray") {
1035
- const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
1036
- const lengthOffset = elemResult.next;
961
+ const lengthOffset = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).next;
1037
962
  if (lengthOffset + DescriptorFormat.ARRAY_LENGTH_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
1038
963
  const length = readU16(data, lengthOffset);
1039
964
  if (length === 0) throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
1040
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);
1041
966
  return {
1042
- node: {
1043
- type: "staticArray",
1044
- typeCode: code,
1045
- isDynamic,
1046
- staticSize,
1047
- element: elemResult.node,
1048
- length,
1049
- span: {
1050
- start: offset,
1051
- end: nodeEnd
1052
- }
1053
- },
967
+ typeCode: code,
968
+ isDynamic,
969
+ staticSize,
1054
970
  next: nodeEnd
1055
971
  };
1056
972
  }
973
+ parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
1057
974
  return {
1058
- node: {
1059
- type: "dynamicArray",
1060
- typeCode: code,
1061
- isDynamic: true,
1062
- staticSize: 0,
1063
- element: parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1).node,
1064
- span: {
1065
- start: offset,
1066
- end: nodeEnd
1067
- }
1068
- },
975
+ typeCode: code,
976
+ isDynamic: true,
977
+ staticSize: 0,
1069
978
  next: nodeEnd
1070
979
  };
1071
980
  }
1072
- /** Convert a zero-based param index to a BE16 hex path step. */
1073
981
  function indexToPath(index) {
1074
982
  return `0x${index.toString(16).padStart(4, "0")}`;
1075
983
  }
1076
- /** Decode a binary descriptor blob, returning the structural representation and AST. */
1077
984
  function decodeDescriptor(data) {
1078
985
  if (data.length < 1) throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
1079
986
  const version = data[0];
1080
987
  if (version !== DescriptorFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DescriptorFormat.VERSION})`);
1081
988
  if (data.length < DescriptorFormat.HEADER_SIZE) throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
1082
- const declaredCount = data[1];
1083
989
  const params = [];
1084
- const tree = [];
1085
990
  let cursor = DescriptorFormat.HEADER_SIZE;
1086
991
  while (cursor < data.length) {
1087
- if (params.length >= DescriptorFormat.MAX_PARAMS) throw new CallciumError("PARAM_COUNT_MISMATCH", `Descriptor exceeds the maximum of ${DescriptorFormat.MAX_PARAMS} top-level params`);
1088
- const { node, next } = parseNode(data, cursor, 1);
1089
- 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);
1090
994
  params.push({
1091
995
  index: params.length,
1092
- typeCode: node.typeCode,
1093
- isDynamic: node.isDynamic,
1094
- staticSize: node.staticSize,
996
+ typeCode,
997
+ isDynamic,
998
+ staticSize,
1095
999
  path: indexToPath(params.length),
1096
1000
  span: {
1097
1001
  start: cursor,
@@ -1100,34 +1004,146 @@ function decodeDescriptor(data) {
1100
1004
  });
1101
1005
  cursor = next;
1102
1006
  }
1007
+ const declaredCount = data[1];
1103
1008
  if (params.length !== declaredCount) throw new CallciumError("PARAM_COUNT_MISMATCH", `Header declares ${declaredCount} params but ${params.length} were parsed`);
1104
- return {
1105
- descriptor: {
1106
- version,
1107
- params
1108
- },
1109
- tree
1110
- };
1009
+ return { descriptor: {
1010
+ version,
1011
+ params
1012
+ } };
1111
1013
  }
1112
- /** Encode and decode descriptors. */
1113
1014
  const DescriptorCoder = {
1114
1015
  fromTypes,
1115
1016
  toTypes
1116
1017
  };
1117
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
1118
1139
  //#region src/policy-coder.ts
1119
- /**
1120
- * Parse a BE16-encoded hex path into an array of step values.
1121
- * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
1122
- * @returns Array of numeric step values.
1123
- */
1124
1140
  function parsePathSteps(path) {
1125
- 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`);
1126
1143
  const steps = [];
1127
- 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));
1128
1145
  return steps;
1129
1146
  }
1130
- /** Wrap a value with its byte span for positional tracking. */
1131
1147
  function field(value, start, end) {
1132
1148
  return {
1133
1149
  value,
@@ -1137,7 +1153,60 @@ function field(value, start, end) {
1137
1153
  }
1138
1154
  };
1139
1155
  }
1140
- /** 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
+ }
1141
1210
  function decodePolicy(blob) {
1142
1211
  const data = hexToBytes(blob);
1143
1212
  if (data.length < PolicyFormat.DESC_OFFSET + 1) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
@@ -1145,10 +1214,10 @@ function decodePolicy(blob) {
1145
1214
  const version = headerByte & PolicyFormat.VERSION_MASK;
1146
1215
  if (version !== PolicyFormat.VERSION) throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${PolicyFormat.VERSION})`);
1147
1216
  if ((headerByte & PolicyFormat.RESERVED_MASK) !== 0) throw new CallciumError("MALFORMED_HEADER", "Reserved header bits must be zero");
1148
- const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
1149
1217
  const selectorStart = PolicyFormat.SELECTOR_OFFSET;
1150
1218
  const selectorEnd = selectorStart + PolicyFormat.SELECTOR_SIZE;
1151
1219
  const selectorHex = toHex(data, selectorStart, selectorEnd);
1220
+ const isSelectorless = (headerByte & PolicyFormat.FLAG_NO_SELECTOR) !== 0;
1152
1221
  if (isSelectorless && selectorHex !== "0x00000000") throw new CallciumError("MALFORMED_HEADER", "Selectorless policy must have a zeroed selector slot");
1153
1222
  const descLengthStart = PolicyFormat.DESC_LENGTH_OFFSET;
1154
1223
  const descLengthValue = readU16(data, descLengthStart);
@@ -1157,7 +1226,7 @@ function decodePolicy(blob) {
1157
1226
  const descEnd = descStart + descLengthValue;
1158
1227
  const groupCountOffset = descEnd;
1159
1228
  if (data.length < groupCountOffset + PolicyFormat.GROUP_COUNT_SIZE) throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short for descriptor and group count");
1160
- const { descriptor: desc, tree } = decodeDescriptor(data.subarray(descStart, descEnd));
1229
+ const { descriptor: desc } = decodeDescriptor(data.subarray(descStart, descEnd));
1161
1230
  const params = desc.params.map((param) => ({
1162
1231
  ...param,
1163
1232
  span: {
@@ -1174,55 +1243,66 @@ function decodePolicy(blob) {
1174
1243
  const groups = [];
1175
1244
  for (let groupIndex = 0; groupIndex < groupCountValue; groupIndex++) {
1176
1245
  if (offset + PolicyFormat.GROUP_HEADER_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading group header", offset);
1177
- const ruleCountStart = offset;
1178
- const ruleCountValue = readU16(data, ruleCountStart);
1179
1246
  const groupSizeStart = offset + PolicyFormat.GROUP_RULECOUNT_SIZE;
1180
1247
  const groupSizeValue = readU32(data, groupSizeStart);
1181
1248
  const groupBodyStart = offset + PolicyFormat.GROUP_HEADER_SIZE;
1182
1249
  const groupEnd = groupBodyStart + groupSizeValue;
1250
+ const ruleCountStart = offset;
1251
+ const ruleCountValue = readU16(data, ruleCountStart);
1183
1252
  if (ruleCountValue === 0) throw new CallciumError("EMPTY_GROUP", "Group must contain at least one rule", offset);
1184
- 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);
1185
1254
  if (groupEnd > data.length) throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
1186
1255
  const rules = [];
1187
1256
  let ruleOffset = groupBodyStart;
1188
1257
  for (let ruleIndex = 0; ruleIndex < ruleCountValue; ruleIndex++) {
1189
1258
  if (ruleOffset + PolicyFormat.RULE_SIZE_SIZE > data.length) throw new CallciumError("UNEXPECTED_END", "Unexpected end while reading rule size", ruleOffset);
1190
1259
  const ruleSizeValue = readU16(data, ruleOffset);
1191
- 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);
1192
1261
  if (ruleOffset + ruleSizeValue > groupEnd) throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
1193
1262
  const scopeOffset = ruleOffset + PolicyFormat.RULE_SCOPE_OFFSET;
1194
1263
  const scopeValue = data[scopeOffset];
1195
1264
  if (scopeValue !== Scope.CONTEXT && scopeValue !== Scope.CALLDATA) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${scopeValue}`, ruleOffset);
1196
1265
  const depthOffset = ruleOffset + PolicyFormat.RULE_DEPTH_OFFSET;
1197
1266
  const depthValue = data[depthOffset];
1198
- if (depthValue === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
1199
- if (depthValue > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`, ruleOffset);
1200
- if (scopeValue === Scope.CONTEXT && depthValue !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step", ruleOffset);
1201
1267
  const pathStart = ruleOffset + PolicyFormat.RULE_PATH_OFFSET;
1202
1268
  const pathLength = depthValue * PolicyFormat.PATH_STEP_SIZE;
1203
- const pathHex = toHex(data, pathStart, pathStart + pathLength);
1204
- if (scopeValue === Scope.CONTEXT) {
1205
- const contextPropertyId = readU16(data, pathStart);
1206
- 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);
1207
1275
  }
1208
- const opCodeOffset = pathStart + pathLength;
1209
- const opCodeValue = data[opCodeOffset];
1276
+ const opCodeOffset = hintStart + hintSize;
1210
1277
  const dataLengthOffset = opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE;
1211
1278
  const dataLengthValue = readU16(data, dataLengthOffset);
1212
- const dataStart = dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE;
1213
- const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + dataLengthValue;
1279
+ const expectedRuleSize = PolicyFormat.RULE_FIXED_OVERHEAD + pathLength + hintSize + dataLengthValue;
1214
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];
1215
1282
  const opBase = opCodeValue & ~Op.NOT;
1216
- 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;
1217
1288
  if (opBase === Op.IN) {
1218
1289
  const wordCount = dataLengthValue / 32;
1219
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);
1220
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);
1221
1300
  rules.push({
1222
1301
  ruleSize: field(ruleSizeValue, ruleOffset, ruleOffset + PolicyFormat.RULE_SIZE_SIZE),
1223
1302
  scope: field(scopeValue, scopeOffset, scopeOffset + 1),
1224
1303
  pathDepth: field(depthValue, depthOffset, depthOffset + 1),
1225
1304
  path: field(pathHex, pathStart, pathStart + pathLength),
1305
+ ...hintSize > 0 && { hint: field(toHex(data, hintStart, hintStart + hintSize), hintStart, hintStart + hintSize) },
1226
1306
  opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PolicyFormat.RULE_OPCODE_SIZE),
1227
1307
  dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PolicyFormat.RULE_DATALENGTH_SIZE),
1228
1308
  data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
@@ -1272,25 +1352,30 @@ function decodePolicy(blob) {
1272
1352
  version,
1273
1353
  isSelectorless
1274
1354
  },
1275
- tree
1355
+ data
1276
1356
  };
1277
1357
  }
1278
- /** Flatten a Constraint into one Rule per operator. */
1279
- 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).");
1280
1361
  const path = hexToBytes(constraint.path);
1281
- 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) => ({
1282
1368
  scope: constraint.scope,
1283
1369
  path,
1284
- operator: hexToBytes(op)
1370
+ operator,
1371
+ hint
1285
1372
  }));
1286
1373
  }
1287
- /** Compare two byte arrays lexicographically. */
1288
1374
  function compareBytes(a, b) {
1289
1375
  const length = Math.min(a.length, b.length);
1290
1376
  for (let i = 0; i < length; i++) if (a[i] !== b[i]) return a[i] - b[i];
1291
1377
  return a.length - b.length;
1292
1378
  }
1293
- /** Sort rules by (scope, pathDepth, pathBytes, operatorBytes). */
1294
1379
  function sortRules(rules) {
1295
1380
  rules.sort((a, b) => {
1296
1381
  if (a.scope !== b.scope) return a.scope - b.scope;
@@ -1302,30 +1387,24 @@ function sortRules(rules) {
1302
1387
  return compareBytes(a.operator, b.operator);
1303
1388
  });
1304
1389
  }
1305
- /** Serialize a single rule to its wire format bytes. */
1306
1390
  function encodeRule(rule) {
1307
- if (rule.path.length === 0) throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
1308
- if ((rule.path.length & 1) !== 0) throw new CallciumError("INVALID_PATH", "Path byte length must be even.");
1309
- if (rule.operator.length < 1) throw new CallciumError("INVALID_OPERATOR", "Operator must have at least one byte (opcode).");
1310
1391
  const depth = rule.path.length / 2;
1311
- if (depth > Limits.MAX_PATH_DEPTH) throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
1312
1392
  if (rule.scope === Scope.CONTEXT && depth !== 1) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
1313
- const opCode = rule.operator[0];
1314
1393
  const data = rule.operator.subarray(1);
1315
- 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;
1316
1395
  if (ruleSize > 65535) throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
1317
1396
  const buf = new Uint8Array(ruleSize);
1318
1397
  writeBE16(buf, 0, ruleSize);
1319
1398
  buf[2] = rule.scope;
1320
1399
  buf[3] = depth;
1321
1400
  buf.set(rule.path, 4);
1322
- const opOffset = 4 + rule.path.length;
1323
- 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];
1324
1404
  writeBE16(buf, opOffset + 1, data.length);
1325
1405
  buf.set(data, opOffset + 3);
1326
1406
  return buf;
1327
1407
  }
1328
- /** Serialize all rules in a group to a single byte array. */
1329
1408
  function encodeGroupRules(rules) {
1330
1409
  const parts = rules.map(encodeRule);
1331
1410
  const totalSize = parts.reduce((sum, p) => sum + p.length, 0);
@@ -1337,40 +1416,35 @@ function encodeGroupRules(rules) {
1337
1416
  }
1338
1417
  return buf;
1339
1418
  }
1340
- /** Build a single operator hex string from a decoded rule's opCode and data. */
1341
1419
  function buildOperatorHex(rule) {
1342
1420
  return `0x${rule.opCode.value.toString(16).padStart(2, "0")}${rule.data.value.slice(2)}`;
1343
1421
  }
1344
- /**
1345
- * Encode a PolicyData structure into the canonical binary format.
1346
- * @param data - The policy data to encode.
1347
- * @returns The encoded policy as a 0x-prefixed hex string.
1348
- */
1349
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`);
1350
1426
  const sortedGroups = data.groups.map((group) => {
1351
- const rules = group.flatMap(flattenConstraint);
1427
+ const rules = group.flatMap((constraint) => flattenConstraint(constraint, descBytes));
1352
1428
  sortRules(rules);
1353
1429
  return rules;
1354
1430
  });
1355
- if (sortedGroups.length === 0) throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
1356
- if (sortedGroups.length > 255) throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${sortedGroups.length} exceeds maximum 255`);
1357
- 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) => {
1358
1433
  if (rules.length === 0) throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
1359
1434
  if (rules.length > 65535) throw new CallciumError("RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`);
1360
- const wireBytes = encodeGroupRules(rules);
1361
1435
  return {
1362
- wireBytes,
1363
- hash: keccak_256(wireBytes),
1436
+ wireBytes: encodeGroupRules(rules),
1364
1437
  ruleCount: rules.length
1365
1438
  };
1366
1439
  });
1367
- groupsWithHash.sort((a, b) => compareBytes(a.hash, b.hash));
1368
- const descBytes = hexToBytes(data.descriptor);
1369
- 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
+ }
1370
1444
  const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
1371
1445
  const headerByte = PolicyFormat.VERSION | (data.isSelectorless ? PolicyFormat.FLAG_NO_SELECTOR : 0);
1372
1446
  let totalSize = PolicyFormat.HEADER_SIZE + PolicyFormat.SELECTOR_SIZE + PolicyFormat.DESC_LENGTH_SIZE + descBytes.length + PolicyFormat.GROUP_COUNT_SIZE;
1373
- 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;
1374
1448
  const out = new Uint8Array(totalSize);
1375
1449
  let offset = 0;
1376
1450
  out[offset++] = headerByte;
@@ -1380,32 +1454,25 @@ function encode(data) {
1380
1454
  offset += PolicyFormat.DESC_LENGTH_SIZE;
1381
1455
  out.set(descBytes, offset);
1382
1456
  offset += descBytes.length;
1383
- out[offset++] = groupsWithHash.length;
1384
- for (const g of groupsWithHash) {
1457
+ out[offset++] = encodedGroups.length;
1458
+ for (const g of encodedGroups) {
1385
1459
  writeBE16(out, offset, g.ruleCount);
1386
1460
  offset += PolicyFormat.GROUP_RULECOUNT_SIZE;
1387
- out[offset++] = g.wireBytes.length >>> 24 & 255;
1388
- out[offset++] = g.wireBytes.length >>> 16 & 255;
1389
- out[offset++] = g.wireBytes.length >>> 8 & 255;
1390
- out[offset++] = g.wireBytes.length & 255;
1461
+ writeBE32(out, offset, g.wireBytes.length);
1462
+ offset += PolicyFormat.GROUP_SIZE_SIZE;
1391
1463
  out.set(g.wireBytes, offset);
1392
1464
  offset += g.wireBytes.length;
1393
1465
  }
1394
1466
  return bytesToHex(out);
1395
1467
  }
1396
- /**
1397
- * Decode a binary policy blob into a PolicyData structure.
1398
- * @param blob - Binary policy as 0x-prefixed hex string.
1399
- * @returns The decoded policy data with constraints grouped by scope and path.
1400
- * @throws {CallciumError} If the blob is structurally malformed.
1401
- */
1402
1468
  function decode(blob) {
1403
1469
  const { policy } = decodePolicy(blob);
1404
1470
  const groups = policy.groups.map((group) => {
1405
1471
  const constraintMap = /* @__PURE__ */ new Map();
1406
1472
  const constraintOrder = [];
1407
1473
  for (const rule of group.rules) {
1408
- const key = `${rule.scope.value}:${rule.path.value}`;
1474
+ const hintHex = rule.hint?.value;
1475
+ const key = `${rule.scope.value}:${rule.path.value}:${hintHex ?? ""}`;
1409
1476
  const opHex = buildOperatorHex(rule);
1410
1477
  const existing = constraintMap.get(key);
1411
1478
  if (existing !== void 0) existing.operators.push(opHex);
@@ -1414,6 +1481,7 @@ function decode(blob) {
1414
1481
  scope: rule.scope.value,
1415
1482
  path: rule.path.value,
1416
1483
  operators: [opHex],
1484
+ ...hintHex !== void 0 && { hint: hintHex },
1417
1485
  span: rule.span
1418
1486
  };
1419
1487
  constraintMap.set(key, constraint);
@@ -1426,180 +1494,21 @@ function decode(blob) {
1426
1494
  isSelectorless: policy.isSelectorless,
1427
1495
  selector: policy.selector.value,
1428
1496
  descriptor: policy.descriptor.raw,
1429
- groups,
1430
- span: policy.span
1431
- };
1432
- }
1433
- /**
1434
- * Inspect a binary policy blob, returning the structural representation with full byte-level spans.
1435
- * @param blob - Binary policy as 0x-prefixed hex string.
1436
- * @returns The inspected policy with per-field spans for every structural element.
1437
- * @throws {CallciumError} If the blob is structurally malformed.
1438
- */
1439
- function inspect(blob) {
1440
- return decodePolicy(blob).policy;
1441
- }
1442
- /** Encode, decode, and inspect policies in the canonical binary format. */
1443
- const PolicyCoder = {
1444
- encode,
1445
- decode,
1446
- inspect
1447
- };
1448
- //#endregion
1449
- //#region src/operators.ts
1450
- /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
1451
- function toBigInt(bytes, offset = 0) {
1452
- const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
1453
- return view.getBigUint64(0, false) << 192n | view.getBigUint64(8, false) << 128n | view.getBigUint64(16, false) << 64n | view.getBigUint64(24, false);
1454
- }
1455
- /** Return true when the type code represents a signed integer (int8 through int256). */
1456
- function isSigned(typeCode) {
1457
- return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
1458
- }
1459
- /**
1460
- * Return true when the type code represents a left-aligned type (fixed bytes or function),
1461
- * whose value occupies the high bytes of the word with zero padding below.
1462
- */
1463
- function isLeftAligned(typeCode) {
1464
- return typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX || typeCode === TypeCode.FUNCTION;
1465
- }
1466
- /**
1467
- * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
1468
- * A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
1469
- * unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
1470
- * comparison use the canonical ABI value rather than the raw bytes.
1471
- * @param value - The raw 256-bit word loaded from calldata.
1472
- * @param typeCode - The declared type code of the value.
1473
- * @returns The canonicalized 256-bit value.
1474
- */
1475
- function canonicalize(value, typeCode) {
1476
- if (typeCode <= TypeCode.UINT_MAX) {
1477
- const bits = BigInt((typeCode + 1) * 8);
1478
- return bits === 256n ? value : value & (1n << bits) - 1n;
1479
- }
1480
- if (typeCode <= TypeCode.INT_MAX) {
1481
- const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
1482
- return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
1483
- }
1484
- if (typeCode === TypeCode.ADDRESS) return value & (1n << 160n) - 1n;
1485
- if (typeCode === TypeCode.BOOL) return value & 1n;
1486
- if (typeCode === TypeCode.FUNCTION) return value >> 64n << 64n;
1487
- if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
1488
- const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
1489
- if (n === 32) return value;
1490
- const padBits = BigInt((32 - n) * 8);
1491
- return value >> padBits << padBits;
1492
- }
1493
- return value;
1494
- }
1495
- /** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
1496
- function isLengthOp(opCode) {
1497
- const base = opCode & ~Op.NOT;
1498
- return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
1499
- }
1500
- /** Map a LENGTH_* opcode base to its core value-comparison twin. */
1501
- function lengthToValueOp(base) {
1502
- if (base === Op.LENGTH_EQ) return Op.EQ;
1503
- if (base === Op.LENGTH_GT) return Op.GT;
1504
- if (base === Op.LENGTH_LT) return Op.LT;
1505
- if (base === Op.LENGTH_GTE) return Op.GTE;
1506
- if (base === Op.LENGTH_LTE) return Op.LTE;
1507
- return Op.BETWEEN;
1508
- }
1509
- /** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
1510
- function isLengthValidType(typeCode) {
1511
- return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
1512
- }
1513
- /** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
1514
- function compareTyped(a, b, typeCode) {
1515
- if (isSigned(typeCode)) {
1516
- const sa = BigInt.asIntN(256, a);
1517
- const sb = BigInt.asIntN(256, b);
1518
- return sa < sb ? -1 : sa > sb ? 1 : 0;
1519
- }
1520
- return a < b ? -1 : a > b ? 1 : 0;
1521
- }
1522
- /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
1523
- function isIn(value, data) {
1524
- const count = data.length / 32;
1525
- let lo = 0;
1526
- let hi = count - 1;
1527
- while (lo <= hi) {
1528
- const mid = lo + hi >>> 1;
1529
- const elem = toBigInt(data, mid * 32);
1530
- if (value === elem) return true;
1531
- if (value < elem) hi = mid - 1;
1532
- else lo = mid + 1;
1533
- }
1534
- return false;
1535
- }
1536
- /**
1537
- * Apply a single operator against a value and operand data.
1538
- * Handles the NOT flag internally by inverting the base operator result.
1539
- * @param opCode - Operator code, potentially OR'd with the NOT flag.
1540
- * @param value - The 256-bit value to test (ignored for LENGTH_* operators).
1541
- * @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
1542
- * @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
1543
- * @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
1544
- * @returns Whether the value satisfies the operator.
1545
- */
1546
- function applyOperator(opCode, value, valueLength, operandData, typeCode) {
1547
- const negate = (opCode & Op.NOT) !== 0;
1548
- let base = opCode & ~Op.NOT;
1549
- if (isLengthOp(base)) {
1550
- value = BigInt(valueLength);
1551
- typeCode = TypeCode.UINT_MAX;
1552
- base = lengthToValueOp(base);
1553
- }
1554
- let result;
1555
- switch (base) {
1556
- case Op.EQ:
1557
- result = value === toBigInt(operandData, 0);
1558
- break;
1559
- case Op.GT:
1560
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
1561
- break;
1562
- case Op.LT:
1563
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
1564
- break;
1565
- case Op.GTE:
1566
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
1567
- break;
1568
- case Op.LTE:
1569
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
1570
- break;
1571
- case Op.BETWEEN: {
1572
- const min = toBigInt(operandData, 0);
1573
- const max = toBigInt(operandData, 32);
1574
- result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
1575
- break;
1576
- }
1577
- case Op.IN:
1578
- result = isIn(value, operandData);
1579
- break;
1580
- case Op.BITMASK_ALL: {
1581
- const mask = toBigInt(operandData, 0);
1582
- result = (value & mask) === mask;
1583
- break;
1584
- }
1585
- case Op.BITMASK_ANY: {
1586
- const mask = toBigInt(operandData, 0);
1587
- result = (value & mask) !== 0n;
1588
- break;
1589
- }
1590
- case Op.BITMASK_NONE: {
1591
- const mask = toBigInt(operandData, 0);
1592
- result = (value & mask) === 0n;
1593
- break;
1594
- }
1595
- default: throw new CallciumError("INVALID_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
1596
- }
1597
- return negate ? !result : result;
1497
+ groups,
1498
+ span: policy.span
1499
+ };
1500
+ }
1501
+ function inspect(blob) {
1502
+ return decodePolicy(blob).policy;
1598
1503
  }
1504
+ const PolicyCoder = {
1505
+ encode,
1506
+ decode,
1507
+ inspect
1508
+ };
1599
1509
  //#endregion
1600
1510
  //#region src/validation-issue.ts
1601
1511
  const ZERO = `0x${"0".repeat(64)}`;
1602
- /** Create an issue from operator compatibility results. */
1603
1512
  function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1604
1513
  return {
1605
1514
  severity: "error",
@@ -1612,7 +1521,42 @@ function fromOpRule(code, message, groupIndex, constraintIndex, opCode) {
1612
1521
  message
1613
1522
  };
1614
1523
  }
1615
- /** Issue for an operand that is not canonically encoded for the declared type. */
1524
+ function unnavigablePath(groupIndex, constraintIndex) {
1525
+ return {
1526
+ severity: "error",
1527
+ category: "typeMismatch",
1528
+ groupIndex,
1529
+ constraintIndex,
1530
+ code: "UNNAVIGABLE_PATH",
1531
+ value1: ZERO,
1532
+ value2: ZERO,
1533
+ message: "Path does not navigate the descriptor"
1534
+ };
1535
+ }
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
+ }
1616
1560
  function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
1617
1561
  return {
1618
1562
  severity: "error",
@@ -1625,7 +1569,6 @@ function nonCanonicalOperand(groupIndex, constraintIndex, operand, canonical) {
1625
1569
  message: "Operand is not the canonical encoding for the declared type"
1626
1570
  };
1627
1571
  }
1628
- /** eq(v)/neq(v) or lengthEq(v)/lengthNeq(v) on the same path. */
1629
1572
  function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
1630
1573
  return {
1631
1574
  severity: "error",
@@ -1638,7 +1581,6 @@ function eqNeqContradiction(isLength, groupIndex, constraintIndex, value) {
1638
1581
  message: isLength ? "lengthEq(v) and lengthNeq(v) on same path" : "eq(v) and neq(v) on same path"
1639
1582
  };
1640
1583
  }
1641
- /** Multiple eq()/lengthEq() operators with different values. */
1642
1584
  function conflictingEquality(isLength, groupIndex, constraintIndex, existing, newValue) {
1643
1585
  return {
1644
1586
  severity: "error",
@@ -1651,7 +1593,6 @@ function conflictingEquality(isLength, groupIndex, constraintIndex, existing, ne
1651
1593
  message: isLength ? "Multiple lengthEq() operators with different values" : "Multiple eq() operators with different values"
1652
1594
  };
1653
1595
  }
1654
- /** Value/length is outside the physical range of its type. */
1655
1596
  function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
1656
1597
  return {
1657
1598
  severity: "error",
@@ -1664,7 +1605,6 @@ function outOfPhysicalBounds(isLength, groupIndex, constraintIndex, value) {
1664
1605
  message: isLength ? "Length value is outside the physical range" : "Value is outside the physical range of the type"
1665
1606
  };
1666
1607
  }
1667
- /** gt()/lengthGt() on the maximum is impossible. */
1668
1608
  function impossibleGt(isLength, groupIndex, constraintIndex, value) {
1669
1609
  return {
1670
1610
  severity: "error",
@@ -1677,7 +1617,6 @@ function impossibleGt(isLength, groupIndex, constraintIndex, value) {
1677
1617
  message: isLength ? "lengthGt() on maximum length is impossible" : "gt() on type maximum is impossible"
1678
1618
  };
1679
1619
  }
1680
- /** lt()/lengthLt() on the minimum is impossible. */
1681
1620
  function impossibleLt(isLength, groupIndex, constraintIndex, value) {
1682
1621
  return {
1683
1622
  severity: "error",
@@ -1690,7 +1629,6 @@ function impossibleLt(isLength, groupIndex, constraintIndex, value) {
1690
1629
  message: isLength ? "lengthLt() on minimum length is impossible" : "lt() on type minimum is impossible"
1691
1630
  };
1692
1631
  }
1693
- /** eq()/lengthEq() value is excluded by a bound. */
1694
1632
  function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound) {
1695
1633
  return {
1696
1634
  severity: "error",
@@ -1703,7 +1641,6 @@ function boundsExcludeEquality(isLength, groupIndex, constraintIndex, eq, bound)
1703
1641
  message: isLength ? "lengthEq() value is excluded by bound" : "eq() value is excluded by bound"
1704
1642
  };
1705
1643
  }
1706
- /** Lower bound/length exceeds the upper bound. */
1707
1644
  function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
1708
1645
  return {
1709
1646
  severity: "error",
@@ -1716,7 +1653,6 @@ function impossibleRange(isLength, groupIndex, constraintIndex, lower, upper) {
1716
1653
  message: isLength ? "Lower length bound exceeds upper bound" : "Lower bound exceeds upper bound"
1717
1654
  };
1718
1655
  }
1719
- /** Set operation excludes existing eq() value. */
1720
1656
  function setExcludesEquality(groupIndex, constraintIndex, value) {
1721
1657
  return {
1722
1658
  severity: "error",
@@ -1729,7 +1665,6 @@ function setExcludesEquality(groupIndex, constraintIndex, value) {
1729
1665
  message: "Set operation excludes existing eq() value"
1730
1666
  };
1731
1667
  }
1732
- /** Multiple isIn() sets have no intersection. */
1733
1668
  function emptySetIntersection(groupIndex, constraintIndex) {
1734
1669
  return {
1735
1670
  severity: "error",
@@ -1742,7 +1677,6 @@ function emptySetIntersection(groupIndex, constraintIndex) {
1742
1677
  message: "Multiple isIn() sets have no intersection"
1743
1678
  };
1744
1679
  }
1745
- /** All values in isIn() set are excluded by neq/notIn. */
1746
1680
  function setFullyExcluded(groupIndex, constraintIndex) {
1747
1681
  return {
1748
1682
  severity: "error",
@@ -1755,7 +1689,6 @@ function setFullyExcluded(groupIndex, constraintIndex) {
1755
1689
  message: "All values in isIn() set are excluded by neq/notIn"
1756
1690
  };
1757
1691
  }
1758
- /** Bitmask operators conflict. */
1759
1692
  function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1760
1693
  return {
1761
1694
  severity: "error",
@@ -1768,7 +1701,6 @@ function bitmaskContradiction(groupIndex, constraintIndex, mask, conflicting) {
1768
1701
  message: "Bitmask operators conflict"
1769
1702
  };
1770
1703
  }
1771
- /** bitmaskAny is impossible because all bits are forbidden. */
1772
1704
  function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1773
1705
  return {
1774
1706
  severity: "error",
@@ -1781,7 +1713,6 @@ function bitmaskAnyImpossible(groupIndex, constraintIndex, mask, mustBeZero) {
1781
1713
  message: "bitmaskAny is impossible because all bits are forbidden"
1782
1714
  };
1783
1715
  }
1784
- /** isIn/notIn set is not strictly sorted and deduplicated. */
1785
1716
  function unsortedInSet(groupIndex, constraintIndex) {
1786
1717
  return {
1787
1718
  severity: "error",
@@ -1794,7 +1725,6 @@ function unsortedInSet(groupIndex, constraintIndex) {
1794
1725
  message: "isIn/notIn set is not strictly sorted and deduplicated."
1795
1726
  };
1796
1727
  }
1797
- /** Group contains zero constraints. */
1798
1728
  function emptyGroup(groupIndex) {
1799
1729
  return {
1800
1730
  severity: "error",
@@ -1807,7 +1737,6 @@ function emptyGroup(groupIndex) {
1807
1737
  message: "Group contains zero constraints."
1808
1738
  };
1809
1739
  }
1810
- /** Numeric/length bound dominated by a stricter bound. */
1811
1740
  function dominatedBound(isLength, groupIndex, constraintIndex, value) {
1812
1741
  return {
1813
1742
  severity: "warning",
@@ -1820,7 +1749,6 @@ function dominatedBound(isLength, groupIndex, constraintIndex, value) {
1820
1749
  message: isLength ? "Length bound is dominated by a stricter bound" : "Numeric bound is dominated by a stricter bound"
1821
1750
  };
1822
1751
  }
1823
- /** Bound redundant because eq()/lengthEq() is set. */
1824
1752
  function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
1825
1753
  return {
1826
1754
  severity: "warning",
@@ -1833,7 +1761,6 @@ function redundantBound(isLength, groupIndex, constraintIndex, bound, eq) {
1833
1761
  message: isLength ? "Length bound is redundant because lengthEq() is set" : "Bound is redundant because eq() is set"
1834
1762
  };
1835
1763
  }
1836
- /** notIn() value was present in isIn() set. */
1837
1764
  function setReduction(groupIndex, constraintIndex, value) {
1838
1765
  return {
1839
1766
  severity: "warning",
@@ -1846,7 +1773,6 @@ function setReduction(groupIndex, constraintIndex, value) {
1846
1773
  message: "notIn() value was present in isIn() set"
1847
1774
  };
1848
1775
  }
1849
- /** isIn() sets partially overlap. */
1850
1776
  function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1851
1777
  return {
1852
1778
  severity: "warning",
@@ -1859,7 +1785,6 @@ function setRedundancy(groupIndex, constraintIndex, intersectionCount) {
1859
1785
  message: "isIn() sets partially overlap"
1860
1786
  };
1861
1787
  }
1862
- /** Some values in isIn() set are excluded by neq/notIn. */
1863
1788
  function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1864
1789
  return {
1865
1790
  severity: "warning",
@@ -1872,7 +1797,6 @@ function setPartiallyExcluded(groupIndex, constraintIndex, excludedCount) {
1872
1797
  message: "Some values in isIn() set are excluded by neq/notIn"
1873
1798
  };
1874
1799
  }
1875
- /** Bitmask operation is redundant. */
1876
1800
  function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1877
1801
  return {
1878
1802
  severity: "warning",
@@ -1885,7 +1809,6 @@ function redundantBitmask(groupIndex, constraintIndex, mask, existing) {
1885
1809
  message: "Bitmask operation is redundant"
1886
1810
  };
1887
1811
  }
1888
- /** gte/lte pair fusible into a single range operator. */
1889
1812
  function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
1890
1813
  return {
1891
1814
  severity: "warning",
@@ -1898,7 +1821,6 @@ function fusibleRange(isLength, groupIndex, constraintIndex, low, high) {
1898
1821
  message: isLength ? "lengthGte() and lengthLte() fuse into a single lengthBetween()" : "gte() and lte() fuse into a single between()"
1899
1822
  };
1900
1823
  }
1901
- /** Duplicate operator in constraint. */
1902
1824
  function duplicateConstraint(groupIndex, constraintIndex) {
1903
1825
  return {
1904
1826
  severity: "warning",
@@ -1911,7 +1833,6 @@ function duplicateConstraint(groupIndex, constraintIndex) {
1911
1833
  message: "Duplicate operator in constraint"
1912
1834
  };
1913
1835
  }
1914
- /** gte(min)/lengthGte(0) is always true. */
1915
1836
  function vacuousGte(isLength, groupIndex, constraintIndex, value) {
1916
1837
  return {
1917
1838
  severity: "info",
@@ -1924,7 +1845,6 @@ function vacuousGte(isLength, groupIndex, constraintIndex, value) {
1924
1845
  message: isLength ? "lengthGte(0) is always true" : "gte() bound equals type minimum (always true)"
1925
1846
  };
1926
1847
  }
1927
- /** lte(max)/lengthLte(max) is always true. */
1928
1848
  function vacuousLte(isLength, groupIndex, constraintIndex, value) {
1929
1849
  return {
1930
1850
  severity: "info",
@@ -1937,7 +1857,18 @@ function vacuousLte(isLength, groupIndex, constraintIndex, value) {
1937
1857
  message: isLength ? "lengthLte() bound equals maximum (always true)" : "lte() bound equals type maximum (always true)"
1938
1858
  };
1939
1859
  }
1940
- /** Path deeper than the reference enforcer's cap. */
1860
+ function vacuousNegatedRange(isLength, groupIndex, constraintIndex, low, high) {
1861
+ return {
1862
+ severity: "info",
1863
+ category: "vacuity",
1864
+ groupIndex,
1865
+ constraintIndex,
1866
+ code: isLength ? "VACUOUS_NEGATED_LENGTH_RANGE" : "VACUOUS_NEGATED_RANGE",
1867
+ value1: low,
1868
+ value2: high,
1869
+ message: isLength ? "Negated lengthBetween() with low > high excludes nothing (always true)" : "Negated between() with low > high excludes nothing (always true)"
1870
+ };
1871
+ }
1941
1872
  function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
1942
1873
  return {
1943
1874
  severity: "warning",
@@ -1947,23 +1878,21 @@ function pathDepthExceeded(groupIndex, constraintIndex, depth, maxDepth) {
1947
1878
  code: "PATH_DEPTH_EXCEEDED",
1948
1879
  value1: depth,
1949
1880
  value2: maxDepth,
1950
- message: "Path depth exceeds the reference enforcer cap"
1881
+ message: "Path depth exceeds the maximum a well-formed policy may declare"
1951
1882
  };
1952
1883
  }
1953
- /** Quantifier over a static array beyond the reference enforcer's iteration cap. */
1954
1884
  function quantifierOverStaticLimit(groupIndex, constraintIndex, arrayLength, maxLength) {
1955
1885
  return {
1956
- severity: "warning",
1886
+ severity: "error",
1957
1887
  category: "compatibility",
1958
1888
  groupIndex,
1959
1889
  constraintIndex,
1960
1890
  code: "QUANTIFIER_OVER_STATIC_LIMIT",
1961
1891
  value1: arrayLength,
1962
1892
  value2: maxLength,
1963
- message: "Quantifier over static array exceeds the reference enforcer cap"
1893
+ message: "Quantifier over static array exceeds the maximum quantified length"
1964
1894
  };
1965
1895
  }
1966
- /** Context property ID outside the assigned set. */
1967
1896
  function unknownContextProperty(groupIndex, constraintIndex, contextId, maxContextId) {
1968
1897
  return {
1969
1898
  severity: "warning",
@@ -1976,7 +1905,6 @@ function unknownContextProperty(groupIndex, constraintIndex, contextId, maxConte
1976
1905
  message: "Unknown context property ID"
1977
1906
  };
1978
1907
  }
1979
- /** Negated operator under the existential quantifier. */
1980
1908
  function negationUnderAny(groupIndex, constraintIndex, opCode) {
1981
1909
  return {
1982
1910
  severity: "warning",
@@ -1991,41 +1919,29 @@ function negationUnderAny(groupIndex, constraintIndex, opCode) {
1991
1919
  }
1992
1920
  //#endregion
1993
1921
  //#region src/policy-validator.ts
1994
- /**
1995
- * Maximum number of tracked "holes" (neq exclusions) and notIn values per
1996
- * domain. A hole is a single value excluded via a negated equality (neq).
1997
- * During cross-operator analysis, holes punch values out of isIn sets — if
1998
- * every member of an isIn set is excluded by holes or notIn, the constraint
1999
- * is unsatisfiable (SET_FULLY_EXCLUDED). When only some are excluded, the
2000
- * validator emits SET_PARTIALLY_EXCLUDED.
2001
- *
2002
- * Both caps are protocol-defined limits. Beyond these limits,
2003
- * additional exclusions are silently ignored — the validator becomes
2004
- * best-effort for set-reduction diagnostics but never produces false
2005
- * positives, only potential false negatives (missed redundancy warnings).
2006
- */
2007
- const MAX_HOLES = 8;
2008
- const MAX_NOT_IN = 8;
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
+ ]);
2009
1929
  const LENGTH_MAX = 4294967295n;
2010
1930
  const UINT256_MAX = (1n << 256n) - 1n;
2011
1931
  const INT256_SIGN_BIT = 1n << 255n;
2012
- /** Signed-aware difference of two raw uint256 values: negative if a<b, zero if equal, positive if a>b. */
2013
1932
  function signedCompare(a, b, signed) {
2014
1933
  if (!signed) return a - b;
2015
1934
  const toS = (v) => v >= INT256_SIGN_BIT ? v - (1n << 256n) : v;
2016
1935
  return toS(a) - toS(b);
2017
1936
  }
2018
- /** True for uint and int type codes. */
2019
1937
  function isNumericType(typeCode) {
2020
- 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;
2021
1939
  }
2022
- /** True for uint and bytes32 type codes. */
2023
1940
  function isBitmaskCompatible(typeCode) {
2024
- 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;
2025
1942
  }
2026
- /** Physical domain limits for a type code (returned as raw uint256 bigints). */
2027
1943
  function getDomainLimits(typeCode) {
2028
- if (typeCode <= TypeCode.UINT_MAX) {
1944
+ if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
2029
1945
  const bits = BigInt(typeCode - TypeCode.UINT_MIN + 1) * 8n;
2030
1946
  return {
2031
1947
  min: 0n,
@@ -2058,19 +1974,15 @@ function getDomainLimits(typeCode) {
2058
1974
  max: UINT256_MAX
2059
1975
  };
2060
1976
  }
2061
- /** True for EQ, GT, LT, GTE, LTE, BETWEEN, IN, BITMASK_*. */
2062
1977
  function isValueOp(opBase) {
2063
1978
  return opBase <= Op.BITMASK_NONE;
2064
1979
  }
2065
- /** True for GT, LT, GTE, LTE, BETWEEN. */
2066
1980
  function isComparisonOp(opBase) {
2067
1981
  return opBase >= Op.GT && opBase <= Op.BETWEEN;
2068
1982
  }
2069
- /** True for BITMASK_ALL, BITMASK_ANY, BITMASK_NONE. */
2070
1983
  function isBitmaskOp(opBase) {
2071
1984
  return opBase >= Op.BITMASK_ALL && opBase <= Op.BITMASK_NONE;
2072
1985
  }
2073
- /** Return the incompatibility reason, or null if the operator is allowed. */
2074
1986
  function getIncompat(opBase, typeInfo) {
2075
1987
  const { typeCode, isDynamic, staticSize } = typeInfo;
2076
1988
  if (isValueOp(opBase)) {
@@ -2108,18 +2020,12 @@ function getIncompat(opBase, typeInfo) {
2108
2020
  message: "Unknown operator code"
2109
2021
  };
2110
2022
  }
2111
- /**
2112
- * True if operator `opCode` is allowed on a target with the given type info.
2113
- * Tolerates the NOT flag on `opCode`.
2114
- */
2115
2023
  function isOpAllowed(opCode, typeInfo) {
2116
2024
  return getIncompat(opCode & ~Op.NOT, typeInfo) === null;
2117
2025
  }
2118
- /** Read a single uint256 from operator hex (bytes after the opcode byte). */
2119
2026
  function readValue(opHex) {
2120
2027
  return BigInt(`0x${opHex.slice(4)}`);
2121
2028
  }
2122
- /** First payload word deviating from the canonical encoding for the type, with its canonical form, or null. */
2123
2029
  function findNonCanonicalWord(opHex, typeCode) {
2124
2030
  const body = opHex.slice(4);
2125
2031
  for (let i = 0; i + 64 <= body.length; i += 64) {
@@ -2132,7 +2038,6 @@ function findNonCanonicalWord(opHex, typeCode) {
2132
2038
  }
2133
2039
  return null;
2134
2040
  }
2135
- /** Read a pair of uint256 values from a BETWEEN operator. */
2136
2041
  function readPair(opHex) {
2137
2042
  const body = opHex.slice(4);
2138
2043
  return {
@@ -2140,7 +2045,6 @@ function readPair(opHex) {
2140
2045
  high: BigInt(`0x${body.slice(64, 128)}`)
2141
2046
  };
2142
2047
  }
2143
- /** Unpack an IN operator's payload into an array of bigint values. */
2144
2048
  function unpackSet(opHex) {
2145
2049
  const body = opHex.slice(4);
2146
2050
  const count = body.length / 64;
@@ -2148,12 +2052,10 @@ function unpackSet(opHex) {
2148
2052
  for (let i = 0; i < count; i++) values.push(BigInt(`0x${body.slice(i * 64, (i + 1) * 64)}`));
2149
2053
  return values;
2150
2054
  }
2151
- /** True if every element is strictly greater than the previous. */
2152
2055
  function isStrictlyAscending(values) {
2153
2056
  for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return false;
2154
2057
  return true;
2155
2058
  }
2156
- /** Convert LENGTH_* opcode to its core comparison equivalent. */
2157
2059
  function normalizeLengthOp(base) {
2158
2060
  if (base === Op.LENGTH_EQ) return Op.EQ;
2159
2061
  if (base === Op.LENGTH_GT) return Op.GT;
@@ -2162,7 +2064,6 @@ function normalizeLengthOp(base) {
2162
2064
  if (base === Op.LENGTH_LTE) return Op.LTE;
2163
2065
  return base;
2164
2066
  }
2165
- /** Convert a negated bound opcode to its positive equivalent. */
2166
2067
  function negateBoundOp(base) {
2167
2068
  if (base === Op.GT) return Op.LTE;
2168
2069
  if (base === Op.GTE) return Op.LT;
@@ -2170,7 +2071,6 @@ function negateBoundOp(base) {
2170
2071
  if (base === Op.LTE) return Op.GT;
2171
2072
  return base;
2172
2073
  }
2173
- /** Fresh bound domain with no constraints recorded yet. */
2174
2074
  function emptyBoundDomain(boundIsSigned, min, max) {
2175
2075
  return {
2176
2076
  isSigned: boundIsSigned,
@@ -2187,12 +2087,12 @@ function emptyBoundDomain(boundIsSigned, min, max) {
2187
2087
  holes: []
2188
2088
  };
2189
2089
  }
2190
- /** Create a fresh constraint context with domain limits from the type. */
2191
- function initContext(scope, path, typeInfo) {
2090
+ function initContext(scope, path, steps, typeInfo) {
2192
2091
  const { min, max } = getDomainLimits(typeInfo.typeCode);
2193
2092
  return {
2194
2093
  scope,
2195
2094
  path,
2095
+ steps,
2196
2096
  typeInfo,
2197
2097
  numeric: emptyBoundDomain(isSigned(typeInfo.typeCode), min, max),
2198
2098
  bitmask: {
@@ -2207,11 +2107,7 @@ function initContext(scope, path, typeInfo) {
2207
2107
  }
2208
2108
  };
2209
2109
  }
2210
- /** Narrow the numeric bound domain with an operator and emit issues on contradiction or redundancy. */
2211
2110
  function updateBound(domain, base, isNegated, value, isLength, groupIndex, constraintIndex, issues) {
2212
- let changedEq = false;
2213
- let changedLower = false;
2214
- let changedUpper = false;
2215
2111
  if (isNegated) {
2216
2112
  if (base === Op.EQ) {
2217
2113
  if (domain.hasEq && domain.eq === value) issues.push(eqNeqContradiction(isLength, groupIndex, constraintIndex, bigintToHex(value)));
@@ -2220,7 +2116,7 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
2220
2116
  alreadyHole = true;
2221
2117
  break;
2222
2118
  }
2223
- if (!alreadyHole && domain.holes.length < MAX_HOLES) domain.holes.push(value);
2119
+ if (!alreadyHole) domain.holes.push(value);
2224
2120
  } else updateBound(domain, negateBoundOp(base), false, value, isLength, groupIndex, constraintIndex, issues);
2225
2121
  return;
2226
2122
  }
@@ -2229,6 +2125,9 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
2229
2125
  if (signedCompare(value, domain.min, domain.isSigned) < 0n || signedCompare(value, domain.max, domain.isSigned) > 0n) issues.push(outOfPhysicalBounds(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2230
2126
  else if (base === Op.GT && value === domain.max) issues.push(impossibleGt(isLength, groupIndex, constraintIndex, bigintToHex(value)));
2231
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;
2232
2131
  if (base === Op.EQ) {
2233
2132
  if (!domain.hasEq || domain.eq !== value) changedEq = true;
2234
2133
  if (domain.hasEq) {
@@ -2300,7 +2199,6 @@ function updateBound(domain, base, isNegated, value, isLength, groupIndex, const
2300
2199
  }
2301
2200
  }
2302
2201
  }
2303
- /** Update the bitmask domain and emit issues on contradiction or redundancy. */
2304
2202
  function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex, issues) {
2305
2203
  if (isNegated) return;
2306
2204
  if (base === Op.BITMASK_ALL) {
@@ -2316,7 +2214,6 @@ function updateBitmask(ctx, base, isNegated, mask, groupIndex, constraintIndex,
2316
2214
  if ((ctx.bitmask.mustBeOne & mask) !== 0n) issues.push(redundantBitmask(groupIndex, constraintIndex, bigintToHex(mask), bigintToHex(ctx.bitmask.mustBeOne)));
2317
2215
  }
2318
2216
  }
2319
- /** Emit an issue if the IN set has been fully excluded by NOT_IN or NEQ holes. */
2320
2217
  function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
2321
2218
  if (!ctx.set.hasIn) return;
2322
2219
  let possibleCount = 0;
@@ -2339,7 +2236,6 @@ function checkSetEmpty(ctx, groupIndex, constraintIndex, issues) {
2339
2236
  if (possibleCount === 0) issues.push(setFullyExcluded(groupIndex, constraintIndex));
2340
2237
  else if (possibleCount < inCount) issues.push(setPartiallyExcluded(groupIndex, constraintIndex, bigintToHex(BigInt(inCount - possibleCount))));
2341
2238
  }
2342
- /** Update the set domain with IN or NOT_IN values and emit issues on contradiction or redundancy. */
2343
2239
  function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues) {
2344
2240
  if (isNegated) {
2345
2241
  for (const value of values) {
@@ -2352,10 +2248,11 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
2352
2248
  }
2353
2249
  if (inSet) issues.push(setReduction(groupIndex, constraintIndex, bigintToHex(value)));
2354
2250
  }
2355
- if (ctx.set.notInValues.length < MAX_NOT_IN) ctx.set.notInValues.push(value);
2251
+ ctx.set.notInValues.push(value);
2356
2252
  }
2357
2253
  checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2358
2254
  } else {
2255
+ for (const value of values) if (signedCompare(value, ctx.numeric.min, ctx.numeric.isSigned) < 0n || signedCompare(value, ctx.numeric.max, ctx.numeric.isSigned) > 0n) issues.push(outOfPhysicalBounds(false, groupIndex, constraintIndex, bigintToHex(value)));
2359
2256
  if (ctx.numeric.hasEq) {
2360
2257
  let found = false;
2361
2258
  for (const candidate of values) if (candidate === ctx.numeric.eq) {
@@ -2380,7 +2277,6 @@ function updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues)
2380
2277
  checkSetEmpty(ctx, groupIndex, constraintIndex, issues);
2381
2278
  }
2382
2279
  }
2383
- /** Emit a warning if the constraint contains any identical operators. */
2384
2280
  function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2385
2281
  const seen = /* @__PURE__ */ new Set();
2386
2282
  for (const op of operators) {
@@ -2392,18 +2288,12 @@ function checkDuplicates(issues, operators, groupIndex, constraintIndex) {
2392
2288
  seen.add(key);
2393
2289
  }
2394
2290
  }
2395
- /**
2396
- * Report a lone unnegated gte/lte pair (value or length domain) as fusible into the
2397
- * corresponding single range operator. Fires only for a satisfiable pair on a compatible
2398
- * type; contradictions and dominated bounds are already reported by the domain checks.
2399
- */
2400
2291
  function checkFusibleRange(ctx, operators, groupIndex, constraintIndex, issues) {
2401
2292
  const numericPair = findLonePair(operators, Op.GTE, Op.LTE);
2402
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)));
2403
2294
  const lengthPair = findLonePair(operators, Op.LENGTH_GTE, Op.LENGTH_LTE);
2404
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)));
2405
2296
  }
2406
- /** Return the operand pair when the operators contain exactly one of each unnegated bound opcode. */
2407
2297
  function findLonePair(operators, lowerOp, upperOp) {
2408
2298
  let lowerCount = 0;
2409
2299
  let upperCount = 0;
@@ -2426,19 +2316,18 @@ function findLonePair(operators, lowerOp, upperOp) {
2426
2316
  high
2427
2317
  } : null;
2428
2318
  }
2429
- /** Validate all operators on a single constraint against its domain context. */
2430
2319
  function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues) {
2431
2320
  const operators = constraint.operators;
2432
- const underAny = constraint.scope === Scope.CALLDATA && parsePathSteps(constraint.path).includes(Quantifier.ANY);
2321
+ const underAny = constraint.scope === Scope.CALLDATA && ctx.steps.includes(Quantifier.ANY);
2433
2322
  for (const opHex of operators) {
2434
2323
  const opCode = parseInt(opHex.slice(2, 4), 16);
2435
2324
  const base = opCode & ~Op.NOT;
2436
- const isNegated = (opCode & Op.NOT) !== 0;
2437
2325
  const dataLength = (opHex.length - 4) / 2;
2438
2326
  if (dataLength > 65535 || !isValidOperatorData(base, dataLength)) {
2439
2327
  issues.push(fromOpRule("UNKNOWN_OPERATOR", "Unknown operator code", groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2440
2328
  continue;
2441
2329
  }
2330
+ const isNegated = (opCode & Op.NOT) !== 0;
2442
2331
  if (underAny && isNegated) issues.push(negationUnderAny(groupIndex, constraintIndex, bigintToHex(BigInt(opCode))));
2443
2332
  const incompat = getIncompat(base, ctx.typeInfo);
2444
2333
  if (incompat) {
@@ -2455,11 +2344,11 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2455
2344
  }
2456
2345
  }
2457
2346
  if (base >= Op.EQ && base <= Op.BETWEEN) if (base === Op.BETWEEN) {
2347
+ const { low, high } = readPair(opHex);
2458
2348
  if (!isNegated) {
2459
- const { low, high } = readPair(opHex);
2460
2349
  updateBound(ctx.numeric, Op.GTE, false, low, false, groupIndex, constraintIndex, issues);
2461
2350
  updateBound(ctx.numeric, Op.LTE, false, high, false, groupIndex, constraintIndex, issues);
2462
- }
2351
+ } else if (signedCompare(low, high, ctx.numeric.isSigned) > 0n) issues.push(vacuousNegatedRange(false, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
2463
2352
  } else {
2464
2353
  const value = readValue(opHex);
2465
2354
  const holesBefore = ctx.numeric.holes.length;
@@ -2473,11 +2362,11 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2473
2362
  else updateSet(ctx, isNegated, values, groupIndex, constraintIndex, issues);
2474
2363
  }
2475
2364
  } else if (isLengthOp(base)) if (base === Op.LENGTH_BETWEEN) {
2365
+ const { low, high } = readPair(opHex);
2476
2366
  if (!isNegated) {
2477
- const { low, high } = readPair(opHex);
2478
2367
  updateBound(ctx.length, Op.GTE, false, low, true, groupIndex, constraintIndex, issues);
2479
2368
  updateBound(ctx.length, Op.LTE, false, high, true, groupIndex, constraintIndex, issues);
2480
- }
2369
+ } else if (low > high) issues.push(vacuousNegatedRange(true, groupIndex, constraintIndex, bigintToHex(low), bigintToHex(high)));
2481
2370
  } else {
2482
2371
  const value = readValue(opHex);
2483
2372
  updateBound(ctx.length, normalizeLengthOp(base), isNegated, value, true, groupIndex, constraintIndex, issues);
@@ -2485,7 +2374,6 @@ function validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues
2485
2374
  }
2486
2375
  checkDuplicates(issues, operators, groupIndex, constraintIndex);
2487
2376
  }
2488
- /** Validate all constraints in a group, building per-path contexts for cross-constraint analysis. */
2489
2377
  function validateGroup(data, descBytes, groupIndex, issues) {
2490
2378
  const constraints = data.groups[groupIndex];
2491
2379
  const contexts = [];
@@ -2498,15 +2386,28 @@ function validateGroup(data, descBytes, groupIndex, issues) {
2498
2386
  break;
2499
2387
  }
2500
2388
  if (!ctx) {
2389
+ const steps = parsePathSteps(constraint.path);
2501
2390
  let typeInfo;
2502
2391
  if (constraint.scope === Scope.CALLDATA) {
2503
- const steps = parsePathSteps(constraint.path);
2504
2392
  if (steps.length > Limits.MAX_PATH_DEPTH) issues.push(pathDepthExceeded(groupIndex, constraintIndex, bigintToHex(BigInt(steps.length)), bigintToHex(BigInt(Limits.MAX_PATH_DEPTH))));
2505
- const walk = Descriptor.walkPath(descBytes, steps);
2393
+ let walk;
2394
+ try {
2395
+ walk = Descriptor.walkPath(descBytes, steps);
2396
+ } catch (error) {
2397
+ if (error instanceof CallciumError && NAVIGABILITY_CODES.has(error.code)) {
2398
+ issues.push(unnavigablePath(groupIndex, constraintIndex));
2399
+ continue;
2400
+ }
2401
+ throw error;
2402
+ }
2403
+ if (steps.filter(isQuantifier).length > 1) {
2404
+ issues.push(nestedQuantifier(groupIndex, constraintIndex));
2405
+ continue;
2406
+ }
2506
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))));
2507
2408
  typeInfo = walk.typeInfo;
2508
2409
  } else {
2509
- const ctxId = parsePathSteps(constraint.path)[0];
2410
+ const ctxId = steps[0];
2510
2411
  if (ctxId > MAX_CONTEXT_PROPERTY_ID) {
2511
2412
  issues.push(unknownContextProperty(groupIndex, constraintIndex, bigintToHex(BigInt(ctxId)), bigintToHex(BigInt(MAX_CONTEXT_PROPERTY_ID))));
2512
2413
  typeInfo = {
@@ -2520,21 +2421,20 @@ function validateGroup(data, descBytes, groupIndex, issues) {
2520
2421
  staticSize: 32
2521
2422
  };
2522
2423
  }
2523
- ctx = initContext(constraint.scope, normalizedPath, typeInfo);
2424
+ ctx = initContext(constraint.scope, normalizedPath, steps, typeInfo);
2524
2425
  contexts.push(ctx);
2525
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
+ }
2526
2430
  validateConstraint(ctx, constraint, groupIndex, constraintIndex, issues);
2527
2431
  checkFusibleRange(ctx, constraint.operators, groupIndex, constraintIndex, issues);
2528
2432
  }
2529
2433
  }
2530
- /**
2531
- * Validate a policy for type mismatches, contradictions, redundancies, and vacuities.
2532
- * @param data - The canonical policy data to validate.
2533
- * @returns All validation issues found, ordered by group and constraint index.
2534
- */
2535
2434
  function validate(data) {
2536
- const issues = [];
2537
2435
  const descBytes = hexToBytes(data.descriptor);
2436
+ decodeDescriptor(descBytes);
2437
+ const issues = [];
2538
2438
  for (let groupIndex = 0; groupIndex < data.groups.length; groupIndex++) {
2539
2439
  if (data.groups[groupIndex].length === 0) {
2540
2440
  issues.push(emptyGroup(groupIndex));
@@ -2544,102 +2444,80 @@ function validate(data) {
2544
2444
  }
2545
2445
  return issues;
2546
2446
  }
2547
- /** Semantic validation for policies. */
2548
2447
  const PolicyValidator = { validate };
2549
2448
  //#endregion
2550
2449
  //#region src/signature.ts
2551
- /** Return true if the char code is an ASCII letter. */
2552
2450
  function isAlpha(c) {
2553
2451
  return c >= 65 && c <= 90 || c >= 97 && c <= 122;
2554
2452
  }
2555
- /** Return true if the char code is an ASCII alphanumeric character. */
2556
2453
  function isAlphanum(c) {
2557
2454
  return isAlpha(c) || c >= 48 && c <= 57;
2558
2455
  }
2559
- /**
2560
- * Parse an ABI function signature into its selector and types.
2561
- *
2562
- * Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
2563
- * structure must be `name(types)` with the final `)` as the last character.
2564
- *
2565
- * @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
2566
- * @returns The 4-byte selector and the comma-separated types string.
2567
- * @throws {CallciumError} with code `"INVALID_SIGNATURE"` if the signature is malformed.
2568
- */
2569
2456
  function parse(signature) {
2570
- 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}"`);
2571
2458
  let openParen = -1;
2572
2459
  for (let i = 0; i < signature.length; i++) {
2573
2460
  const c = signature.charCodeAt(i);
2574
- if (c === 32 || c === 9 || c === 10 || c === 13) throw new CallciumError("INVALID_SIGNATURE", "Signature must not contain whitespace");
2575
- 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");
2576
2463
  if (openParen === -1 && c === 40) openParen = i;
2577
2464
  }
2578
- if (openParen < 1) throw new CallciumError("INVALID_SIGNATURE", `Invalid signature: must have a function name followed by parentheses, got "${signature}"`);
2579
- 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}"`);
2580
2467
  const firstChar = signature.charCodeAt(0);
2581
- 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");
2582
2469
  for (let i = 1; i < openParen; i++) {
2583
2470
  const c = signature.charCodeAt(i);
2584
- 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");
2585
2472
  }
2586
- const types = signature.slice(openParen + 1, signature.length - 1);
2587
2473
  const encoded = new Uint8Array(signature.length);
2588
2474
  for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
2589
2475
  return {
2590
2476
  selector: bytesToHex(keccak_256(encoded).subarray(0, 4)),
2591
- types
2477
+ types: signature.slice(openParen + 1, signature.length - 1)
2592
2478
  };
2593
2479
  }
2594
- /** ABI function signature parser. */
2595
2480
  const SignatureParser = { parse };
2596
2481
  //#endregion
2597
2482
  //#region src/policy-builder.ts
2598
- /** Validate a context-scope path. */
2599
- function validateContextPath(pathBytes) {
2600
- if (pathBytes.length !== 2) throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step (2 bytes).");
2601
- const step = readU16(pathBytes, 0);
2602
- if (step > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PROPERTY", `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`);
2603
- }
2604
- /** Validate a calldata-scope path against the descriptor. */
2605
- function validateCalldataPath(path, desc) {
2606
- const steps = parsePathSteps(path);
2607
- 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) {
2608
2489
  const argIndex = steps[0];
2609
2490
  const paramCount = Descriptor.paramCount(desc);
2610
- 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).`);
2611
2492
  let offset = Descriptor.paramOffset(desc, argIndex);
2612
2493
  let hasQuantifier = false;
2613
2494
  for (let i = 1; i < steps.length; i++) {
2614
2495
  const step = steps[i];
2615
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
+ }
2616
2503
  if (info.typeCode === TypeCode.TUPLE) {
2617
- if (isQuantifier(step)) throw new CallciumError("INVALID_PATH", "Quantifier step is not valid on a tuple node.");
2618
2504
  const fieldCount = Descriptor.tupleFieldCount(desc, offset);
2619
- 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).`);
2620
2506
  offset = Descriptor.tupleFieldOffset(desc, offset, step);
2621
- } else if (info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY) {
2622
- if (isQuantifier(step)) {
2623
- if (hasQuantifier) throw new CallciumError("INVALID_PATH", "Nested quantifiers are not allowed.");
2624
- hasQuantifier = true;
2625
- } else if (info.typeCode === TypeCode.STATIC_ARRAY) {
2507
+ } else if (isArray) {
2508
+ if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
2626
2509
  const arrayLength = Descriptor.staticArrayLength(desc, offset);
2627
- 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).`);
2628
2511
  }
2629
2512
  offset = Descriptor.arrayElementOffset(offset);
2630
- } 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.");
2631
2514
  }
2632
2515
  }
2633
- /** Fluent builder for constructing Callcium policies. */
2634
2516
  var PolicyBuilder = class PolicyBuilder {
2635
2517
  draft;
2636
2518
  constructor(draft) {
2637
2519
  this.draft = draft;
2638
2520
  }
2639
- /**
2640
- * Create a builder from a function signature.
2641
- * @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
2642
- */
2643
2521
  static create(signature) {
2644
2522
  const parsed = SignatureParser.parse(signature);
2645
2523
  const descriptor = DescriptorCoder.fromTypes(parsed.types);
@@ -2651,10 +2529,6 @@ var PolicyBuilder = class PolicyBuilder {
2651
2529
  pathHashes: [/* @__PURE__ */ new Set()]
2652
2530
  });
2653
2531
  }
2654
- /**
2655
- * Create a selectorless builder from a raw type string.
2656
- * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
2657
- */
2658
2532
  static createRaw(typesCsv) {
2659
2533
  return new PolicyBuilder({
2660
2534
  isSelectorless: true,
@@ -2664,73 +2538,50 @@ var PolicyBuilder = class PolicyBuilder {
2664
2538
  pathHashes: [/* @__PURE__ */ new Set()]
2665
2539
  });
2666
2540
  }
2667
- /**
2668
- * Add a constraint to the current group.
2669
- * @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
2670
- */
2671
2541
  add(constraint) {
2672
2542
  const c = {
2673
2543
  scope: constraint.scope,
2674
2544
  path: constraint.path,
2675
2545
  operators: [...constraint.operators]
2676
2546
  };
2677
- if (c.operators.length === 0) throw new CallciumError("INVALID_CONSTRAINT", "Constraint must have at least one operator.");
2678
- if (c.scope === Scope.CONTEXT) validateContextPath(hexToBytes(c.path));
2679
- 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);
2680
2552
  else throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
2681
2553
  const key = `${c.scope}:${c.path.toLowerCase()}`;
2682
2554
  const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1];
2683
- 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.`);
2684
2556
  currentHashes.add(key);
2685
2557
  this.draft.groups[this.draft.groups.length - 1].push(c);
2686
2558
  return this;
2687
2559
  }
2688
- /** Start a new constraint group (OR branch). */
2689
2560
  or() {
2690
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.");
2691
2562
  this.draft.groups.push([]);
2692
2563
  this.draft.pathHashes.push(/* @__PURE__ */ new Set());
2693
2564
  return this;
2694
2565
  }
2695
- /**
2696
- * Build the policy into an encoded binary blob with strict validation.
2697
- * Throws on any issue, regardless of severity. Use {@link validate} to
2698
- * inspect issues, or {@link buildUnsafe} to encode without validation.
2699
- * @returns The policy as a 0x-prefixed hex string.
2700
- * @throws {CallciumError} If any group is empty or validation finds any issue.
2701
- */
2702
2566
  build() {
2703
2567
  this.checkGroups();
2704
2568
  const policyData = this.toPolicyData();
2705
2569
  const issues = PolicyValidator.validate(policyData);
2706
- if (issues.length > 0) throw new CallciumError("VALIDATION_ERROR", issues[0].message);
2570
+ if (issues.length > 0) throw new ValidationError(issues);
2707
2571
  return PolicyCoder.encode(policyData);
2708
2572
  }
2709
- /**
2710
- * Build the policy into an encoded binary blob without validation.
2711
- * The resulting policy may be invalid. Prefer {@link build}; use this
2712
- * only to knowingly bypass a reported issue.
2713
- * @returns The policy as a 0x-prefixed hex string.
2714
- * @throws {CallciumError} If any group is empty.
2715
- */
2716
2573
  buildUnsafe() {
2717
2574
  this.checkGroups();
2718
2575
  return PolicyCoder.encode(this.toPolicyData());
2719
2576
  }
2720
- /**
2721
- * Validate the policy without encoding.
2722
- * @returns All validation issues found.
2723
- */
2724
2577
  validate() {
2725
2578
  this.checkGroups();
2726
2579
  const policyData = this.toPolicyData();
2727
2580
  return PolicyValidator.validate(policyData);
2728
2581
  }
2729
- /** Throw if any group is empty. */
2730
2582
  checkGroups() {
2731
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.`);
2732
2584
  }
2733
- /** Convert the draft to a PolicyData structure. */
2734
2585
  toPolicyData() {
2735
2586
  return {
2736
2587
  isSelectorless: this.draft.isSelectorless,
@@ -2742,13 +2593,9 @@ var PolicyBuilder = class PolicyBuilder {
2742
2593
  };
2743
2594
  //#endregion
2744
2595
  //#region src/constraint.ts
2745
- /** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
2746
2596
  function addressBody(value) {
2747
- const body = value.startsWith("0x") ? value.slice(2) : value;
2748
- if (body.length !== 40) throw new CallciumError("INVALID_CONSTRAINT", `Invalid address length: expected 40 hex chars, got ${body.length}`);
2749
- return body;
2597
+ return toAddress(value).slice(2);
2750
2598
  }
2751
- /** Convert a scalar value to a 32-byte big-endian word. */
2752
2599
  function encodeWord(value) {
2753
2600
  const word = new Uint8Array(32);
2754
2601
  if (typeof value === "boolean") {
@@ -2757,11 +2604,7 @@ function encodeWord(value) {
2757
2604
  }
2758
2605
  if (typeof value === "string") {
2759
2606
  const body = addressBody(value);
2760
- for (let i = 0; i < 20; i++) {
2761
- const byte = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2762
- if (Number.isNaN(byte)) throw new CallciumError("INVALID_CONSTRAINT", `Invalid hex character in address at position ${i * 2}`);
2763
- word[12 + i] = byte;
2764
- }
2607
+ for (let i = 0; i < 20; i++) word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
2765
2608
  return word;
2766
2609
  }
2767
2610
  let bigValue = typeof value === "number" ? BigInt(value) : value;
@@ -2771,14 +2614,12 @@ function encodeWord(value) {
2771
2614
  }
2772
2615
  return word;
2773
2616
  }
2774
- /** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
2775
2617
  function singleOp(opCode, value) {
2776
2618
  const buffer = new Uint8Array(33);
2777
2619
  buffer[0] = opCode;
2778
2620
  buffer.set(encodeWord(value), 1);
2779
2621
  return bytesToHex(buffer);
2780
2622
  }
2781
- /** Pack a range operator (opCode byte + min word + max word). */
2782
2623
  function rangeOp(opCode, min, max) {
2783
2624
  if (min > max) throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
2784
2625
  const buffer = new Uint8Array(65);
@@ -2787,7 +2628,7 @@ function rangeOp(opCode, min, max) {
2787
2628
  buffer.set(encodeWord(max), 33);
2788
2629
  return bytesToHex(buffer);
2789
2630
  }
2790
- /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
2631
+ const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
2791
2632
  function setOp(opCode, values) {
2792
2633
  const bigs = values.map((v) => {
2793
2634
  if (typeof v === "bigint") return v;
@@ -2799,118 +2640,81 @@ function setOp(opCode, values) {
2799
2640
  const deduped = [];
2800
2641
  for (const value of bigs) if (deduped.length === 0 || deduped[deduped.length - 1] !== value) deduped.push(value);
2801
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`);
2802
2644
  const buffer = new Uint8Array(1 + deduped.length * 32);
2803
2645
  buffer[0] = opCode;
2804
2646
  for (let i = 0; i < deduped.length; i++) buffer.set(encodeWord(deduped[i]), 1 + i * 32);
2805
2647
  return bytesToHex(buffer);
2806
2648
  }
2807
- /** Encode a sequence of uint16 path steps as a big-endian hex string. */
2808
2649
  function encodePath(steps) {
2809
2650
  const buffer = new Uint8Array(steps.length * 2);
2810
2651
  for (let i = 0; i < steps.length; i++) writeBE16(buffer, i * 2, steps[i]);
2811
2652
  return bytesToHex(buffer);
2812
2653
  }
2813
- /**
2814
- * Mutable builder that accumulates operators targeting a single path.
2815
- * Implements the `Constraint` interface so it can be passed directly to policy builders.
2816
- */
2817
2654
  var ConstraintBuilder = class {
2818
2655
  scope;
2819
2656
  path;
2820
2657
  operators;
2821
- /** @internal */
2822
2658
  constructor(scope, path) {
2823
2659
  this.scope = scope;
2824
2660
  this.path = path;
2825
2661
  this.operators = [];
2826
2662
  }
2827
- /** Push a pre-encoded operator hex string and return this for chaining. */
2828
2663
  push(opHex) {
2829
2664
  this.operators.push(opHex);
2830
2665
  return this;
2831
2666
  }
2832
- /** Assert the value equals `value`. */
2833
2667
  eq(value) {
2834
2668
  return this.push(singleOp(Op.EQ, value));
2835
2669
  }
2836
- /** Assert the value does not equal `value`. */
2837
2670
  neq(value) {
2838
2671
  return this.push(singleOp(Op.EQ | Op.NOT, value));
2839
2672
  }
2840
- /** Assert the value is greater than `bound`. */
2841
2673
  gt(bound) {
2842
2674
  return this.push(singleOp(Op.GT, bound));
2843
2675
  }
2844
- /** Assert the value is less than `bound`. */
2845
2676
  lt(bound) {
2846
2677
  return this.push(singleOp(Op.LT, bound));
2847
2678
  }
2848
- /** Assert the value is greater than or equal to `bound`. */
2849
2679
  gte(bound) {
2850
2680
  return this.push(singleOp(Op.GTE, bound));
2851
2681
  }
2852
- /** Assert the value is less than or equal to `bound`. */
2853
2682
  lte(bound) {
2854
2683
  return this.push(singleOp(Op.LTE, bound));
2855
2684
  }
2856
- /**
2857
- * Assert the value is within [min, max] inclusive.
2858
- * @throws {CallciumError} If min > max.
2859
- */
2860
2685
  between(min, max) {
2861
2686
  return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
2862
2687
  }
2863
- /**
2864
- * Assert the value is a member of the set.
2865
- * Values are sorted and deduplicated before encoding.
2866
- * @throws {CallciumError} If the set is empty after deduplication.
2867
- */
2868
2688
  isIn(values) {
2869
2689
  return this.push(setOp(Op.IN, values));
2870
2690
  }
2871
- /**
2872
- * Assert the value is not a member of the set.
2873
- * @throws {CallciumError} If the set is empty after deduplication.
2874
- */
2875
2691
  notIn(values) {
2876
2692
  return this.push(setOp(Op.IN | Op.NOT, values));
2877
2693
  }
2878
- /** Assert all bits in `mask` are set. */
2879
2694
  bitmaskAll(mask) {
2880
2695
  return this.push(singleOp(Op.BITMASK_ALL, mask));
2881
2696
  }
2882
- /** Assert at least one bit in `mask` is set. */
2883
2697
  bitmaskAny(mask) {
2884
2698
  return this.push(singleOp(Op.BITMASK_ANY, mask));
2885
2699
  }
2886
- /** Assert no bit in `mask` is set. */
2887
2700
  bitmaskNone(mask) {
2888
2701
  return this.push(singleOp(Op.BITMASK_NONE, mask));
2889
2702
  }
2890
- /** Assert the runtime length equals `n`. */
2891
2703
  lengthEq(n) {
2892
2704
  return this.push(singleOp(Op.LENGTH_EQ, n));
2893
2705
  }
2894
- /** Assert the runtime length is greater than `n`. */
2895
2706
  lengthGt(n) {
2896
2707
  return this.push(singleOp(Op.LENGTH_GT, n));
2897
2708
  }
2898
- /** Assert the runtime length is less than `n`. */
2899
2709
  lengthLt(n) {
2900
2710
  return this.push(singleOp(Op.LENGTH_LT, n));
2901
2711
  }
2902
- /** Assert the runtime length is greater than or equal to `n`. */
2903
2712
  lengthGte(n) {
2904
2713
  return this.push(singleOp(Op.LENGTH_GTE, n));
2905
2714
  }
2906
- /** Assert the runtime length is less than or equal to `n`. */
2907
2715
  lengthLte(n) {
2908
2716
  return this.push(singleOp(Op.LENGTH_LTE, n));
2909
2717
  }
2910
- /**
2911
- * Assert the runtime length is within [min, max] inclusive.
2912
- * @throws {CallciumError} If min > max.
2913
- */
2914
2718
  lengthBetween(min, max) {
2915
2719
  return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
2916
2720
  }
@@ -2918,418 +2722,60 @@ var ConstraintBuilder = class {
2918
2722
  function arg(...steps) {
2919
2723
  return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
2920
2724
  }
2921
- /** Target the `msg.sender` context property. */
2922
2725
  function msgSender() {
2923
2726
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
2924
2727
  }
2925
- /** Target the `msg.value` context property. */
2926
2728
  function msgValue() {
2927
2729
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
2928
2730
  }
2929
- /** Target the `block.timestamp` context property. */
2930
2731
  function blockTimestamp() {
2931
2732
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
2932
2733
  }
2933
- /** Target the `block.number` context property. */
2934
2734
  function blockNumber() {
2935
2735
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
2936
2736
  }
2937
- /** Target the `block.chainid` context property. */
2938
2737
  function chainId() {
2939
2738
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
2940
2739
  }
2941
- /** Target the `tx.origin` context property. */
2942
2740
  function txOrigin() {
2943
2741
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
2944
2742
  }
2945
- /** Target the `block.basefee` context property. */
2946
2743
  function baseFee() {
2947
2744
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
2948
2745
  }
2949
- /** Target the `tx.gasprice` context property. */
2950
2746
  function gasPrice() {
2951
2747
  return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
2952
2748
  }
2953
2749
  //#endregion
2954
- //#region src/reader.ts
2955
- /** Read 32 bytes at offset, bounds-checked. */
2956
- function load32(callData, offset) {
2750
+ //#region src/calldata-reader.ts
2751
+ function loadWord(callData, offset) {
2957
2752
  if (offset < 0 || offset + 32 > callData.length) return { code: "CALLDATA_OUT_OF_BOUNDS" };
2958
2753
  return callData.subarray(offset, offset + 32);
2959
2754
  }
2960
- /**
2961
- * Read ABI pointer (offset or length) from a 32-byte slot.
2962
- * Validates high 28 bytes are zero (rejects values > 32-bit).
2963
- */
2964
2755
  function readPointer(callData, head) {
2965
- const word = load32(callData, head);
2756
+ const word = loadWord(callData, head);
2966
2757
  if (word instanceof Uint8Array) {
2967
2758
  for (let i = 0; i < 28; i++) if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
2968
2759
  return (word[28] << 24 | word[29] << 16 | word[30] << 8 | word[31]) >>> 0;
2969
2760
  }
2970
2761
  return word;
2971
2762
  }
2972
- /** Return the number of bytes this node occupies in the head region. */
2973
- function headContribution(node) {
2974
- return node.isDynamic ? 32 : node.staticSize;
2975
- }
2976
- /** Read a single BE16 path step at the given step index. */
2977
- function readStep(pathBytes, stepIndex) {
2978
- return readU16(pathBytes, stepIndex * 2);
2979
- }
2980
- /** Navigate one path step through a node (tuple field, array element, or quantifier). */
2981
- function descend(node, callData, head, base, childIndex) {
2982
- if (node.type === "tuple") return descendTuple(node, callData, head, base, childIndex);
2983
- if (node.type === "dynamicArray") return descendDynamicArray(node, callData, head, base, childIndex);
2984
- if (node.type === "staticArray") return descendStaticArray(node, callData, head, base, childIndex);
2985
- throw new CallciumError("INVALID_PATH", `Cannot descend into elementary type (${node.type}).`);
2986
- }
2987
- /** Descend into a tuple field by index. */
2988
- function descendTuple(node, callData, head, base, childIndex) {
2989
- if (childIndex >= node.fields.length) throw new CallciumError("INVALID_PATH", `Tuple field index ${childIndex} out of range (${node.fields.length} fields).`);
2990
- let tupleBase;
2991
- let cursor;
2992
- if (node.isDynamic) {
2993
- const ptrResult = readPointer(callData, head);
2994
- if (typeof ptrResult !== "number") return {
2995
- ok: false,
2996
- code: ptrResult.code
2997
- };
2998
- tupleBase = base + ptrResult;
2999
- cursor = tupleBase;
3000
- } else {
3001
- tupleBase = base;
3002
- cursor = head;
3003
- }
3004
- for (let i = 0; i < childIndex; i++) cursor += headContribution(node.fields[i]);
3005
- return {
3006
- ok: true,
3007
- location: {
3008
- head: cursor,
3009
- base: tupleBase,
3010
- node: node.fields[childIndex]
3011
- }
3012
- };
3013
- }
3014
- /** Descend into a dynamic array element by index. */
3015
- function descendDynamicArray(node, callData, head, base, childIndex) {
3016
- const arrayBaseResult = readPointer(callData, head);
3017
- if (typeof arrayBaseResult !== "number") return {
3018
- ok: false,
3019
- code: arrayBaseResult.code
3020
- };
3021
- const arrayBase = base + arrayBaseResult;
3022
- const lengthResult = readPointer(callData, arrayBase);
3023
- if (typeof lengthResult !== "number") return {
3024
- ok: false,
3025
- code: lengthResult.code
3026
- };
3027
- if (childIndex >= lengthResult) return {
3028
- ok: false,
3029
- code: "ARRAY_INDEX_OUT_OF_BOUNDS"
3030
- };
3031
- const headsBase = arrayBase + 32;
3032
- const elem = node.element;
3033
- if (elem.isDynamic) return {
3034
- ok: true,
3035
- location: {
3036
- head: headsBase + childIndex * 32,
3037
- base: headsBase,
3038
- node: elem
3039
- }
3040
- };
3041
- return {
3042
- ok: true,
3043
- location: {
3044
- head: headsBase + childIndex * elem.staticSize,
3045
- base: arrayBase,
3046
- node: elem
3047
- }
3048
- };
3049
- }
3050
- /** Descend into a static array element by index. */
3051
- function descendStaticArray(node, callData, head, base, childIndex) {
3052
- if (childIndex >= node.length) throw new CallciumError("INVALID_PATH", `Static array index ${childIndex} out of range (length ${node.length}).`);
3053
- const elem = node.element;
3054
- if (elem.isDynamic) {
3055
- const ptrResult = readPointer(callData, head);
3056
- if (typeof ptrResult !== "number") return {
3057
- ok: false,
3058
- code: ptrResult.code
3059
- };
3060
- const arrayBase = base + ptrResult;
3061
- return {
3062
- ok: true,
3063
- location: {
3064
- head: arrayBase + childIndex * 32,
3065
- base: arrayBase,
3066
- node: elem
3067
- }
3068
- };
3069
- }
3070
- return {
3071
- ok: true,
3072
- location: {
3073
- head: head + childIndex * elem.staticSize,
3074
- base,
3075
- node: elem
3076
- }
3077
- };
3078
- }
3079
- /**
3080
- * Resolve a path to a calldata location, stopping at a leaf or quantifier.
3081
- *
3082
- * The path is a sequence of BE16 steps. The first step selects the top-level
3083
- * parameter; subsequent steps descend into tuples and arrays. If a quantifier
3084
- * step is encountered, returns a QuantifierResult with the array location and
3085
- * remaining path suffix.
3086
- */
3087
- function locate(tree, callData, pathBytes, baseOffset) {
3088
- const stepCount = pathBytes.length / 2;
3089
- if (stepCount === 0) throw new CallciumError("INVALID_PATH", "Path is empty.");
3090
- const paramIndex = readStep(pathBytes, 0);
3091
- if (paramIndex >= tree.length) throw new CallciumError("INVALID_PATH", `Param index ${paramIndex} out of range (${tree.length} params).`);
3092
- let head = baseOffset;
3093
- for (let i = 0; i < paramIndex; i++) head += headContribution(tree[i]);
3094
- let base = baseOffset;
3095
- let node = tree[paramIndex];
3096
- for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
3097
- const step = readStep(pathBytes, stepIndex);
3098
- if (isQuantifier(step)) {
3099
- const remaining = pathBytes.subarray((stepIndex + 1) * 2);
3100
- for (let remainingStepIndex = 0; remainingStepIndex < remaining.length / 2; remainingStepIndex++) if (isQuantifier(readStep(remaining, remainingStepIndex))) throw new CallciumError("INVALID_QUANTIFIER", "Nested quantifiers are not supported.");
3101
- return {
3102
- ok: true,
3103
- type: "quantifier",
3104
- quantifier: {
3105
- quantifier: step,
3106
- location: {
3107
- head,
3108
- base,
3109
- node
3110
- },
3111
- remainingPath: remaining
3112
- }
3113
- };
3114
- }
3115
- const result = descend(node, callData, head, base, step);
3116
- if (!result.ok) return {
3117
- ok: false,
3118
- code: result.code
3119
- };
3120
- head = result.location.head;
3121
- base = result.location.base;
3122
- node = result.location.node;
3123
- }
3124
- return {
3125
- ok: true,
3126
- type: "leaf",
3127
- location: {
3128
- head,
3129
- base,
3130
- node
3131
- }
3132
- };
3133
- }
3134
- /**
3135
- * Build the element-shape fields shared by dynamic- and static-array nodes, branching only on
3136
- * whether the element itself is dynamic.
3137
- */
3138
- function makeArrayShape(length, elem, dynamicHeadsBase, staticDataOffset, staticCompositeBase) {
3139
- return elem.isDynamic ? {
3140
- length,
3141
- elementNode: elem,
3142
- elementIsDynamic: true,
3143
- elementStaticSize: 0,
3144
- headsBase: dynamicHeadsBase,
3145
- dataOffset: 0,
3146
- compositeBase: dynamicHeadsBase
3147
- } : {
3148
- length,
3149
- elementNode: elem,
3150
- elementIsDynamic: false,
3151
- elementStaticSize: elem.staticSize,
3152
- headsBase: 0,
3153
- dataOffset: staticDataOffset,
3154
- compositeBase: staticCompositeBase
3155
- };
3156
- }
3157
- /** Extract array shape from a location pointing to an array node. */
3158
- function arrayShape(callData, location) {
3159
- const { head, base, node } = location;
3160
- if (node.type === "dynamicArray") {
3161
- const arrayBaseResult = readPointer(callData, head);
3162
- if (typeof arrayBaseResult !== "number") return {
3163
- ok: false,
3164
- code: arrayBaseResult.code
3165
- };
3166
- const arrayBase = base + arrayBaseResult;
3167
- const lengthResult = readPointer(callData, arrayBase);
3168
- if (typeof lengthResult !== "number") return {
3169
- ok: false,
3170
- code: lengthResult.code
3171
- };
3172
- const headsBase = arrayBase + 32;
3173
- return {
3174
- ok: true,
3175
- shape: makeArrayShape(lengthResult, node.element, headsBase, headsBase, arrayBase)
3176
- };
3177
- }
3178
- if (node.type === "staticArray") {
3179
- const elem = node.element;
3180
- let dynamicHeadsBase = 0;
3181
- if (elem.isDynamic) {
3182
- const arrayBaseResult = readPointer(callData, head);
3183
- if (typeof arrayBaseResult !== "number") return {
3184
- ok: false,
3185
- code: arrayBaseResult.code
3186
- };
3187
- dynamicHeadsBase = base + arrayBaseResult;
3188
- }
3189
- return {
3190
- ok: true,
3191
- shape: makeArrayShape(node.length, elem, dynamicHeadsBase, head, base)
3192
- };
3193
- }
3194
- throw new CallciumError("INVALID_PATH", `Cannot compute array shape for non-array node (${node.type}).`);
3195
- }
3196
- /** Compute the location for element N within a precomputed array shape. */
3197
- function arrayElementAt(shape, index, callData) {
3198
- if (index >= shape.length) return {
3199
- ok: false,
3200
- code: "ARRAY_INDEX_OUT_OF_BOUNDS"
3201
- };
3202
- let head;
3203
- let base;
3204
- if (!shape.elementIsDynamic) {
3205
- head = shape.dataOffset + index * shape.elementStaticSize;
3206
- base = shape.compositeBase;
3207
- } else {
3208
- head = shape.headsBase + index * 32;
3209
- base = shape.headsBase;
3210
- }
3211
- if (head + 32 > callData.length) return {
3212
- ok: false,
3213
- code: "CALLDATA_OUT_OF_BOUNDS"
3214
- };
3215
- return {
3216
- ok: true,
3217
- location: {
3218
- head,
3219
- base,
3220
- node: shape.elementNode
3221
- }
3222
- };
3223
- }
3224
- /** Load a 32-byte scalar value from a resolved location. */
3225
- function loadScalar(callData, location) {
3226
- const result = load32(callData, location.head);
3227
- if (result instanceof Uint8Array) return {
3228
- ok: true,
3229
- value: result
3230
- };
3231
- return {
3232
- ok: false,
3233
- code: result.code
3234
- };
3235
- }
3236
- /** Load the data offset and logical length for a dynamic type (bytes, string, or dynamic array). */
3237
- function loadSlice(callData, location) {
3238
- const ptrResult = readPointer(callData, location.head);
3239
- if (typeof ptrResult !== "number") return {
3240
- ok: false,
3241
- code: ptrResult.code
3242
- };
3243
- const dataStart = location.base + ptrResult;
3244
- const lengthResult = readPointer(callData, dataStart);
3245
- if (typeof lengthResult !== "number") return {
3246
- ok: false,
3247
- code: lengthResult.code
3248
- };
3249
- const dataOffset = dataStart + 32;
3250
- const node = location.node;
3251
- const stride = node.type === "dynamicArray" ? node.element.isDynamic ? 32 : node.element.staticSize : 1;
3252
- if (dataOffset > callData.length || lengthResult > Math.floor((callData.length - dataOffset) / stride)) return {
3253
- ok: false,
3254
- code: "CALLDATA_OUT_OF_BOUNDS"
3255
- };
3256
- return {
3257
- ok: true,
3258
- dataOffset,
3259
- length: lengthResult
3260
- };
3261
- }
3262
- /** Continue navigating through additional path steps from a resolved location. */
3263
- function descendPath(callData, location, pathBytes) {
3264
- const stepCount = pathBytes.length / 2;
3265
- let { head, base, node } = location;
3266
- for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
3267
- const step = readStep(pathBytes, stepIndex);
3268
- const result = descend(node, callData, head, base, step);
3269
- if (!result.ok) return {
3270
- ok: false,
3271
- code: result.code
3272
- };
3273
- head = result.location.head;
3274
- base = result.location.base;
3275
- node = result.location.node;
3276
- }
3277
- return {
3278
- ok: true,
3279
- location: {
3280
- head,
3281
- base,
3282
- node
3283
- }
3284
- };
3285
- }
3286
2763
  //#endregion
3287
2764
  //#region src/policy-enforcer.ts
3288
- /** Violations that abort evaluation instead of failing only their group (spec §10.3). */
3289
2765
  const ABORT_VIOLATION_CODES = new Set([
3290
2766
  "CALLDATA_OUT_OF_BOUNDS",
3291
2767
  "ARRAY_INDEX_OUT_OF_BOUNDS",
2768
+ "NON_CANONICAL_VALUE",
3292
2769
  "QUANTIFIER_LIMIT_EXCEEDED"
3293
2770
  ]);
3294
- /** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
3295
2771
  function addressToBigInt(hex) {
3296
2772
  const bytes = hexToBytes(hex);
3297
2773
  const padded = new Uint8Array(32);
3298
2774
  padded.set(bytes, 32 - bytes.length);
3299
2775
  return toBigInt(padded, 0);
3300
2776
  }
3301
- /**
3302
- * Walk a descriptor subtree through path steps and return the terminal leaf type code.
3303
- *
3304
- * Used only for violation metadata. Valid policies have structurally valid
3305
- * quantifier suffixes (enforced by `PolicyValidator`); if a hand-crafted blob
3306
- * violates that invariant, return the last resolvable node's type code as
3307
- * best-effort diagnostic context.
3308
- */
3309
- function resolveLeafTypeCode(elementNode, remainingPath) {
3310
- let node = elementNode;
3311
- const stepCount = remainingPath.length / 2;
3312
- for (let step = 0; step < stepCount; step++) {
3313
- const childIndex = readU16(remainingPath, step * 2);
3314
- if (node.type === "tuple") {
3315
- const field = node.fields[childIndex];
3316
- if (!field) break;
3317
- node = field;
3318
- } else if (node.type === "staticArray" || node.type === "dynamicArray") node = node.element;
3319
- else break;
3320
- }
3321
- return node.typeCode;
3322
- }
3323
- /**
3324
- * Check a policy against ABI-encoded call data without throwing on violations.
3325
- * @param policy - Binary policy blob as 0x-prefixed hex string.
3326
- * @param callData - ABI-encoded call data as 0x-prefixed hex string.
3327
- * @param context - Optional execution context for context-scoped rules.
3328
- * @returns Pass with matched group index, or fail with one violation per evaluated failing group.
3329
- * @throws {CallciumError} If the policy blob is structurally malformed.
3330
- */
3331
2777
  function check(policy, callData, context) {
3332
- const { policy: decoded, tree } = decodePolicy(policy);
2778
+ const { policy: decoded, data: policyBytes } = decodePolicy(policy);
3333
2779
  const callDataBytes = hexToBytes(callData);
3334
2780
  if (!decoded.isSelectorless) {
3335
2781
  if (callDataBytes.length < 4) return {
@@ -3354,7 +2800,7 @@ function check(policy, callData, context) {
3354
2800
  let groupFailed = false;
3355
2801
  for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
3356
2802
  const rule = group.rules[ruleIndex];
3357
- const violation = evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
2803
+ const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
3358
2804
  if (violation !== null) {
3359
2805
  allViolations.push(violation);
3360
2806
  if (ABORT_VIOLATION_CODES.has(violation.code)) return {
@@ -3375,153 +2821,183 @@ function check(policy, callData, context) {
3375
2821
  violations: allViolations
3376
2822
  };
3377
2823
  }
3378
- /**
3379
- * Enforce a policy against ABI-encoded call data, throwing on violation.
3380
- * @param policy - Binary policy blob as 0x-prefixed hex string.
3381
- * @param callData - ABI-encoded call data as 0x-prefixed hex string.
3382
- * @param context - Optional execution context for context-scoped rules.
3383
- * @throws {PolicyViolationError} If the policy rejects the call data.
3384
- * @throws {CallciumError} If the policy blob is structurally malformed.
3385
- */
3386
2824
  function enforce(policy, callData, context) {
3387
2825
  const result = check(policy, callData, context);
3388
2826
  if (!result.ok) throw new PolicyViolationError(result.violations);
3389
2827
  }
3390
- /** Evaluate a single rule against calldata or context, returning a violation or null on pass. */
3391
- function evaluateRule(rule, tree, callDataBytes, baseOffset, groupIndex, ruleIndex, context) {
3392
- const scope = rule.scope.value;
3393
- 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) {
3394
2832
  const opCode = rule.opCode.value;
3395
- const operandData = hexToBytes(rule.data.value);
3396
- if (scope === Scope.CONTEXT) return evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
3397
- const locResult = locate(tree, callDataBytes, pathBytes, baseOffset);
3398
- if (!locResult.ok) return {
3399
- group: groupIndex,
3400
- rule: ruleIndex,
3401
- code: locResult.code,
3402
- scope: Scope.CALLDATA,
3403
- path: rule.path.value,
3404
- opCode,
3405
- operandData: rule.data.value
3406
- };
3407
- if (locResult.type === "quantifier") return evaluateQuantifier(locResult.quantifier, callDataBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value);
3408
- 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);
3409
2837
  }
3410
- /** Evaluate a context-scoped rule by resolving the property from the execution context. */
3411
- function evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, pathHex, context) {
3412
- const propertyId = readU16(pathBytes, 0);
3413
- if (propertyId > MAX_CONTEXT_PROPERTY_ID) throw new CallciumError("INVALID_CONTEXT_PATH", `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`);
3414
- const propInfo = lookupContextProperty(propertyId);
3415
- const contextValue = context?.[propInfo.contextKey];
3416
- if (contextValue === void 0) return {
3417
- group: groupIndex,
3418
- rule: ruleIndex,
3419
- code: "MISSING_CONTEXT",
3420
- scope: Scope.CONTEXT,
3421
- path: pathHex,
3422
- typeCode: propInfo.typeCode
3423
- };
3424
- let value;
3425
- if (typeof contextValue === "string") value = addressToBigInt(contextValue);
3426
- else value = contextValue;
3427
- if (!applyOperator(opCode, value, 32, operandData, propInfo.typeCode)) return {
3428
- group: groupIndex,
3429
- rule: ruleIndex,
3430
- code: "VALUE_MISMATCH",
3431
- scope: Scope.CONTEXT,
3432
- path: pathHex,
3433
- opCode,
3434
- operandData: bytesToHex(operandData),
3435
- typeCode: propInfo.typeCode,
3436
- resolvedValue: bigintToHex(value)
3437
- };
3438
- 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;
3439
2876
  }
3440
- /**
3441
- * Load a value from calldata at the given location and apply the operator.
3442
- *
3443
- * `resolvedValue` is normalised at the decision point: length operators encode
3444
- * the byte/element count as a hex bigint; scalar operators preserve the full
3445
- * 32-byte ABI word so downstream rendering can decode left-aligned `bytesN` faithfully.
3446
- */
3447
- function applyLeafOperator(callDataBytes, location, opCode, operandData) {
3448
- const { node } = location;
3449
- if (isLengthOp(opCode)) {
3450
- let valueLength;
3451
- if (node.isDynamic) {
3452
- const lengthResult = loadSlice(callDataBytes, location);
3453
- if (!lengthResult.ok) return { error: lengthResult.code };
3454
- valueLength = lengthResult.length;
3455
- } 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" };
3456
2886
  return {
3457
- passed: applyOperator(opCode, 0n, valueLength, operandData, node.typeCode),
3458
- resolvedValue: bigintToHex(BigInt(valueLength))
2887
+ passed: applyOperator(opCode, 0n, length, operandData, typeCode),
2888
+ value: BigInt(length)
3459
2889
  };
3460
2890
  }
3461
- if (node.isDynamic || node.type !== "elementary") throw new CallciumError("NOT_SCALAR", "Value operator on a target without a scalar word.");
3462
- const result = loadScalar(callDataBytes, location);
3463
- if (!result.ok) return { error: result.code };
3464
- const value = canonicalize(toBigInt(result.value, 0), node.typeCode);
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 {
2896
+ error: "NON_CANONICAL_VALUE",
2897
+ value
2898
+ };
3465
2899
  return {
3466
- passed: applyOperator(opCode, value, 32, operandData, node.typeCode),
3467
- resolvedValue: bigintToHex(value)
2900
+ passed: applyOperator(opCode, value, 32, operandData, typeCode),
2901
+ value
3468
2902
  };
3469
2903
  }
3470
- /** Evaluate a leaf rule, producing a violation on failure or null on pass. */
3471
- function evaluateLeaf(location, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3472
- const result = applyLeafOperator(callDataBytes, location, opCode, operandData);
3473
- const typeCode = location.node.typeCode;
3474
- if ("error" in result) return {
3475
- group: groupIndex,
3476
- rule: ruleIndex,
3477
- code: result.error,
2904
+ function navigationViolation(frame, code, elementIndex) {
2905
+ return {
2906
+ group: frame.group,
2907
+ rule: frame.rule,
2908
+ code,
3478
2909
  scope: Scope.CALLDATA,
3479
- path: pathHex,
3480
- opCode,
3481
- operandData: bytesToHex(operandData),
3482
- typeCode
3483
- };
3484
- if (!result.passed) return {
3485
- group: groupIndex,
3486
- rule: ruleIndex,
2910
+ path: frame.path,
2911
+ opCode: frame.opCode,
2912
+ operandData: bytesToHex(frame.operandData),
2913
+ typeCode: frame.typeCode,
2914
+ ...elementIndex !== void 0 && { elementIndex }
2915
+ };
2916
+ }
2917
+ function targetViolation(frame, result, elementIndex) {
2918
+ if ("error" in result) {
2919
+ if (result.error === "NON_CANONICAL_VALUE") return {
2920
+ group: frame.group,
2921
+ rule: frame.rule,
2922
+ code: result.error,
2923
+ scope: Scope.CALLDATA,
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 }
2930
+ };
2931
+ return navigationViolation(frame, result.error, elementIndex);
2932
+ }
2933
+ if (result.passed) return null;
2934
+ return {
2935
+ group: frame.group,
2936
+ rule: frame.rule,
3487
2937
  code: "VALUE_MISMATCH",
3488
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,
3489
2957
  path: pathHex,
3490
2958
  opCode,
3491
- operandData: bytesToHex(operandData),
3492
- typeCode,
3493
- resolvedValue: result.resolvedValue
3494
- };
3495
- return null;
3496
- }
3497
- /** Evaluate a quantified rule by iterating over array elements with ALL/ANY semantics. */
3498
- function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIndex, ruleIndex, pathHex) {
3499
- const { quantifier, location: arrayLocation, remainingPath } = qResult;
3500
- const operandHex = bytesToHex(operandData);
3501
- const arrayNode = arrayLocation.node;
3502
- const leafTypeCode = resolveLeafTypeCode(arrayNode.type === "staticArray" || arrayNode.type === "dynamicArray" ? arrayNode.element : arrayNode, remainingPath);
3503
- const shapeResult = arrayShape(callDataBytes, arrayLocation);
3504
- 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 = {
3505
2973
  group: groupIndex,
3506
2974
  rule: ruleIndex,
3507
- code: shapeResult.code,
3508
- scope: Scope.CALLDATA,
3509
2975
  path: pathHex,
3510
2976
  opCode,
3511
- operandData: operandHex,
3512
- typeCode: leafTypeCode
3513
- };
3514
- const shape = shapeResult.shape;
3515
- 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 {
3516
2991
  group: groupIndex,
3517
2992
  rule: ruleIndex,
3518
2993
  code: "QUANTIFIER_LIMIT_EXCEEDED",
3519
2994
  scope: Scope.CALLDATA,
3520
2995
  path: pathHex,
3521
- resolvedValue: bigintToHex(BigInt(shape.length))
2996
+ resolvedValue: bigintToHex(BigInt(count))
3522
2997
  };
3523
- if (shape.length === 0) {
3524
- 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;
3525
3001
  return {
3526
3002
  group: groupIndex,
3527
3003
  rule: ruleIndex,
@@ -3530,72 +3006,28 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
3530
3006
  path: pathHex
3531
3007
  };
3532
3008
  }
3533
- const isUniversal = quantifier === Quantifier.ALL_OR_EMPTY || quantifier === Quantifier.ALL;
3534
- const hasSuffix = remainingPath.length > 0;
3535
- for (let elemIndex = 0; elemIndex < shape.length; elemIndex++) {
3536
- const elemResult = arrayElementAt(shape, elemIndex, callDataBytes);
3537
- if (!elemResult.ok) {
3538
- if (isUniversal) return {
3539
- group: groupIndex,
3540
- rule: ruleIndex,
3541
- code: elemResult.code,
3542
- scope: Scope.CALLDATA,
3543
- path: pathHex,
3544
- opCode,
3545
- operandData: operandHex,
3546
- typeCode: leafTypeCode,
3547
- elementIndex: elemIndex
3548
- };
3549
- 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;
3550
3019
  }
3551
- const elemLocation = elemResult.location;
3552
- let leafLocation;
3553
- if (hasSuffix) {
3554
- const navResult = descendPath(callDataBytes, elemLocation, remainingPath);
3555
- if (!navResult.ok) {
3556
- if (isUniversal) return {
3557
- group: groupIndex,
3558
- rule: ruleIndex,
3559
- code: navResult.code,
3560
- scope: Scope.CALLDATA,
3561
- path: pathHex,
3562
- opCode,
3563
- operandData: operandHex,
3564
- typeCode: leafTypeCode,
3565
- elementIndex: elemIndex
3566
- };
3567
- continue;
3568
- }
3569
- leafLocation = navResult.location;
3570
- } else leafLocation = elemLocation;
3571
- const applied = applyLeafOperator(callDataBytes, leafLocation, opCode, operandData);
3572
- if ("error" in applied) {
3573
- if (isUniversal) return {
3574
- group: groupIndex,
3575
- rule: ruleIndex,
3576
- code: applied.error,
3577
- scope: Scope.CALLDATA,
3578
- path: pathHex,
3579
- opCode,
3580
- operandData: operandHex,
3581
- typeCode: leafTypeCode,
3582
- elementIndex: elemIndex
3583
- };
3584
- 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;
3585
3025
  }
3586
- if (isUniversal && !applied.passed) return {
3587
- group: groupIndex,
3588
- rule: ruleIndex,
3589
- code: "VALUE_MISMATCH",
3590
- scope: Scope.CALLDATA,
3591
- path: pathHex,
3592
- opCode,
3593
- operandData: operandHex,
3594
- typeCode: leafTypeCode,
3595
- resolvedValue: applied.resolvedValue,
3596
- elementIndex: elemIndex
3597
- };
3598
- 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);
3599
3031
  }
3600
3032
  if (isUniversal) return null;
3601
3033
  return {
@@ -3605,16 +3037,44 @@ function evaluateQuantifier(qResult, callDataBytes, opCode, operandData, groupIn
3605
3037
  scope: Scope.CALLDATA,
3606
3038
  path: pathHex,
3607
3039
  opCode,
3608
- operandData: operandHex,
3609
- 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)
3610
3070
  };
3071
+ return null;
3611
3072
  }
3612
- /** Check and enforce policies against ABI-encoded call data. */
3613
3073
  const PolicyEnforcer = {
3614
3074
  check,
3615
3075
  enforce
3616
3076
  };
3617
3077
  //#endregion
3618
- 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 };
3619
3079
 
3620
3080
  //# sourceMappingURL=index.js.map