@callcium/sdk 0.9.0 → 0.10.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/src/operators.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Op, TypeCode } from "./constants";
1
+ import { Op, TypeCode, operandsOf } from "./constants";
2
2
  import { CallciumError } from "./errors";
3
3
 
4
4
  /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
@@ -28,7 +28,7 @@ export function isLeftAligned(typeCode: number): boolean {
28
28
  }
29
29
 
30
30
  /**
31
- * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
31
+ * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type.
32
32
  * A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
33
33
  * unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
34
34
  * comparison use the canonical ABI value rather than the raw bytes.
@@ -103,14 +103,14 @@ function compareTyped(a: bigint, b: bigint, typeCode: number): number {
103
103
  /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
104
104
  function isIn(value: bigint, data: Uint8Array): boolean {
105
105
  const count = data.length / 32;
106
- let lo = 0;
107
- let hi = count - 1;
108
- while (lo <= hi) {
109
- const mid = (lo + hi) >>> 1;
106
+ let low = 0;
107
+ let high = count - 1;
108
+ while (low <= high) {
109
+ const mid = (low + high) >>> 1;
110
110
  const elem = toBigInt(data, mid * 32);
111
111
  if (value === elem) return true;
112
- if (value < elem) hi = mid - 1;
113
- else lo = mid + 1;
112
+ if (value < elem) high = mid - 1;
113
+ else low = mid + 1;
114
114
  }
115
115
  return false;
116
116
  }
@@ -196,9 +196,114 @@ export function applyOperator(
196
196
  }
197
197
 
198
198
  default:
199
- throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
199
+ throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
200
200
  }
201
201
 
202
202
  const negate = (opCode & Op.NOT) !== 0;
203
203
  return negate ? !result : result;
204
204
  }
205
+
206
+ /**
207
+ * Check whether an operator's data payload has the correct length.
208
+ * @param opBase - Base operator code with the NOT flag stripped.
209
+ * @param dataLength - Byte length of the operator's data payload.
210
+ * @returns True if the data length is valid for the given operator.
211
+ */
212
+ export function isValidOperatorData(opBase: number, dataLength: number): boolean {
213
+ const operands = operandsOf(opBase);
214
+ if (operands === "single") return dataLength === 32;
215
+ if (operands === "range") return dataLength === 64;
216
+ if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
217
+ return false;
218
+ }
219
+
220
+ ///////////////////////////////////////////////////////////////////////////
221
+ // Type code classification
222
+ ///////////////////////////////////////////////////////////////////////////
223
+
224
+ /** Structural category for a descriptor type code. */
225
+ export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
226
+
227
+ /** Structural classification without label. */
228
+ export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
229
+
230
+ /** Display metadata for a descriptor type code. */
231
+ export type TypeCodeInfo = TypeClassInfo & { label: string };
232
+
233
+ /** Throw an UNKNOWN_TYPE_CODE error. */
234
+ function unknownTypeCode(code: number): never {
235
+ throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
236
+ }
237
+
238
+ // Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
239
+ const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
240
+ const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
241
+ const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
242
+ const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
243
+ const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
244
+
245
+ /**
246
+ * Classify a type code into its structural category and dynamism.
247
+ * Lightweight variant of `lookupTypeCode` that skips label computation.
248
+ * @param code - A single-byte descriptor type code.
249
+ * @returns Type class and whether the type is ABI-dynamic.
250
+ * @throws {CallciumError} If the code is not a recognised type code.
251
+ */
252
+ export function classifyTypeCode(code: number): TypeClassInfo {
253
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
254
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
255
+ if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
256
+ if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
257
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
258
+ if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
259
+ if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
260
+ if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
261
+ if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
262
+ if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
263
+ if (code === TypeCode.TUPLE) return TUPLE;
264
+ unknownTypeCode(code);
265
+ }
266
+
267
+ /**
268
+ * Determine whether a type code names a value an operator can read: a scalar word or a declared
269
+ * length. Tuples, static arrays, and undefined codes address nothing.
270
+ * @param code - A single-byte descriptor type code.
271
+ * @returns True when an operator can be applied to a target of this type.
272
+ */
273
+ export function isAddressableTarget(code: number): boolean {
274
+ let info: TypeClassInfo;
275
+ try {
276
+ // An undefined code is reported by a throw, and addresses nothing either way.
277
+ info = classifyTypeCode(code);
278
+ } catch {
279
+ return false;
280
+ }
281
+ return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
282
+ }
283
+
284
+ /** Compute the ABI type label for a type code. */
285
+ function typeCodeLabel(code: number): string {
286
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
287
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
288
+ if (code === TypeCode.ADDRESS) return "address";
289
+ if (code === TypeCode.BOOL) return "bool";
290
+ if (code === TypeCode.FUNCTION) return "function";
291
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
292
+ return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
293
+ if (code === TypeCode.BYTES) return "bytes";
294
+ if (code === TypeCode.STRING) return "string";
295
+ if (code === TypeCode.STATIC_ARRAY) return "T[k]";
296
+ if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
297
+ if (code === TypeCode.TUPLE) return "tuple";
298
+ return `0x${code.toString(16).padStart(2, "0")}`;
299
+ }
300
+
301
+ /**
302
+ * Map a raw type code byte to its ABI type label, structural category, and dynamism.
303
+ * @param code - A single-byte descriptor type code.
304
+ * @returns Label, type class, and whether the type is ABI-dynamic.
305
+ * @throws {CallciumError} If the code is not a recognised type code.
306
+ */
307
+ export function lookupTypeCode(code: number): TypeCodeInfo {
308
+ return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
309
+ }
package/src/path.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
2
+ import { buildCodeMap, PolicyFormat as PF } from "./constants";
3
+ import { CallciumError } from "./errors";
4
+
5
+ import type { Hex } from "./types";
6
+
7
+ ///////////////////////////////////////////////////////////////////////////
8
+ // Quantifier steps
9
+ ///////////////////////////////////////////////////////////////////////////
10
+
11
+ const QUANTIFIER_TABLE = [
12
+ { key: "ALL", code: 0xffff, label: "all" },
13
+ { key: "ANY", code: 0xfffe, label: "any" },
14
+ ] as const;
15
+
16
+ /** Reserved path step values that trigger quantified evaluation over array elements. */
17
+ export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
18
+
19
+ /** Display metadata for a quantifier step. */
20
+ export type QuantifierInfo = { label: string };
21
+
22
+ const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
23
+ QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]),
24
+ );
25
+
26
+ /** Check whether a path step is a quantifier (ALL or ANY). */
27
+ export function isQuantifier(step: number): boolean {
28
+ return step >= Quantifier.ANY;
29
+ }
30
+
31
+ /**
32
+ * Map a quantifier path step to its display label.
33
+ * @param code - Quantifier step value.
34
+ * @returns Display metadata for the quantifier.
35
+ * @throws {CallciumError} If the code is not a recognised quantifier.
36
+ */
37
+ export function lookupQuantifier(code: number): QuantifierInfo {
38
+ const info = quantifierByCode.get(code);
39
+ if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
40
+ return info;
41
+ }
42
+
43
+ ///////////////////////////////////////////////////////////////////////////
44
+ // Path steps
45
+ ///////////////////////////////////////////////////////////////////////////
46
+
47
+ /** Encode a sequence of uint16 path steps as a big-endian hex string. */
48
+ export function encodePath(steps: readonly number[]): Hex {
49
+ const buffer = new Uint8Array(steps.length * 2);
50
+ for (let i = 0; i < steps.length; i++) {
51
+ writeBE16(buffer, i * 2, steps[i]!);
52
+ }
53
+ return bytesToHex(buffer);
54
+ }
55
+
56
+ /**
57
+ * Parse a BE16-encoded hex path into an array of step values.
58
+ * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
59
+ * @returns Array of numeric step values.
60
+ */
61
+ export function parsePathSteps(path: Hex): number[] {
62
+ const bytes = hexToBytes(path);
63
+ if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
64
+ throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
65
+ }
66
+ const steps: number[] = [];
67
+ for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
68
+ steps.push(readU16(bytes, offset));
69
+ }
70
+ return steps;
71
+ }
@@ -1,10 +1,11 @@
1
1
  import { bytesToHex } from "./bytes";
2
- import { Scope, TypeCode, isQuantifier, MAX_CONTEXT_PROPERTY_ID } from "./constants";
2
+ import { Scope, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
3
3
  import { ConstraintBuilder } from "./constraint";
4
4
  import { Descriptor } from "./descriptor";
5
5
  import { DescriptorCoder } from "./descriptor-coder";
6
6
  import { CallciumError, ValidationError } from "./errors";
7
- import { PolicyCoder, parsePathSteps } from "./policy-coder";
7
+ import { isQuantifier, parsePathSteps } from "./path";
8
+ import { PolicyCoder } from "./policy-coder";
8
9
  import { PolicyValidator } from "./policy-validator";
9
10
  import { SignatureParser } from "./signature";
10
11
 
@@ -29,13 +30,13 @@ type PolicyDraft = {
29
30
  /** Validate a context-scope path. */
30
31
  function validateContextPath(steps: number[]): void {
31
32
  if (steps.length !== 1) {
32
- throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step.");
33
+ throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step");
33
34
  }
34
35
  const step = steps[0]!;
35
36
  if (step > MAX_CONTEXT_PROPERTY_ID) {
36
37
  throw new CallciumError(
37
38
  "UNKNOWN_CONTEXT_PROPERTY",
38
- `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`,
39
+ `Unknown context property ID 0x${step.toString(16).padStart(4, "0")}`,
39
40
  );
40
41
  }
41
42
  }
@@ -47,7 +48,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
47
48
  if (argIndex >= paramCount) {
48
49
  throw new CallciumError(
49
50
  "PARAM_INDEX_OUT_OF_BOUNDS",
50
- `Param index ${argIndex} out of range (descriptor has ${paramCount} params).`,
51
+ `Param index ${argIndex} out of range (descriptor has ${paramCount} params)`,
51
52
  );
52
53
  }
53
54
 
@@ -61,10 +62,10 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
61
62
 
62
63
  if (isQuantifier(step)) {
63
64
  if (!isArray) {
64
- throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node.");
65
+ throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node");
65
66
  }
66
67
  if (hasQuantifier) {
67
- throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed.");
68
+ throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed");
68
69
  }
69
70
  hasQuantifier = true;
70
71
  }
@@ -74,7 +75,7 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
74
75
  if (step >= fieldCount) {
75
76
  throw new CallciumError(
76
77
  "TUPLE_FIELD_OUT_OF_BOUNDS",
77
- `Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`,
78
+ `Tuple field index ${step} out of range (tuple has ${fieldCount} fields)`,
78
79
  );
79
80
  }
80
81
  offset = Descriptor.tupleFieldOffset(desc, offset, step);
@@ -84,13 +85,13 @@ function validateCalldataPath(steps: number[], desc: Uint8Array): void {
84
85
  if (step >= arrayLength) {
85
86
  throw new CallciumError(
86
87
  "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
87
- `Array index ${step} out of range (static array has ${arrayLength} elements).`,
88
+ `Array index ${step} out of range (static array has ${arrayLength} elements)`,
88
89
  );
89
90
  }
90
91
  }
91
92
  offset = Descriptor.arrayElementOffset(offset);
92
93
  } else {
93
- throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type.");
94
+ throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type");
94
95
  }
95
96
  }
96
97
  }
@@ -150,13 +151,13 @@ export class PolicyBuilder {
150
151
  };
151
152
 
152
153
  if (c.operators.length === 0) {
153
- throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator.");
154
+ throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator");
154
155
  }
155
156
 
156
157
  // Path shape is established before the scope decides how to navigate it.
157
158
  const steps = parsePathSteps(c.path);
158
159
  if (steps.length === 0) {
159
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
160
+ throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
160
161
  }
161
162
 
162
163
  if (c.scope === Scope.CONTEXT) {
@@ -164,13 +165,13 @@ export class PolicyBuilder {
164
165
  } else if (c.scope === Scope.CALLDATA) {
165
166
  validateCalldataPath(steps, this.draft.descriptor);
166
167
  } else {
167
- throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
168
+ throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}`);
168
169
  }
169
170
 
170
171
  const key = `${c.scope}:${c.path.toLowerCase()}`;
171
172
  const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1]!;
172
173
  if (currentHashes.has(key)) {
173
- throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group.`);
174
+ throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group`);
174
175
  }
175
176
 
176
177
  currentHashes.add(key);
@@ -182,7 +183,7 @@ export class PolicyBuilder {
182
183
  or(): this {
183
184
  const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
184
185
  if (lastGroup.length === 0) {
185
- throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty.");
186
+ throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty");
186
187
  }
187
188
  this.draft.groups.push([]);
188
189
  this.draft.pathHashes.push(new Set());
@@ -233,7 +234,7 @@ export class PolicyBuilder {
233
234
  private checkGroups(): void {
234
235
  for (let i = 0; i < this.draft.groups.length; i++) {
235
236
  if (this.draft.groups[i]!.length === 0) {
236
- throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty.`);
237
+ throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty`);
237
238
  }
238
239
  }
239
240
  }
@@ -1,21 +1,12 @@
1
1
  import { keccak_256 } from "@noble/hashes/sha3.js";
2
2
 
3
3
  import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
4
- import {
5
- DescriptorFormat as DF,
6
- PolicyFormat as PF,
7
- Scope,
8
- Limits,
9
- Op,
10
- TypeCode,
11
- isValidOperatorData,
12
- isAddressableTarget,
13
- MAX_CONTEXT_PROPERTY_ID,
14
- } from "./constants";
4
+ import { DescriptorFormat as DF, PolicyFormat as PF, Scope, Op, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
15
5
  import { Descriptor } from "./descriptor";
16
6
  import { decodeDescriptor } from "./descriptor-coder";
17
7
  import { CallciumError } from "./errors";
18
- import { isLengthOp, isLengthValidType, toBigInt } from "./operators";
8
+ import { isLengthOp, isLengthValidType, toBigInt, isAddressableTarget, isValidOperatorData } from "./operators";
9
+ import { parsePathSteps } from "./path";
19
10
 
20
11
  import type {
21
12
  Constraint,
@@ -28,27 +19,6 @@ import type {
28
19
  PolicyData,
29
20
  } from "./types";
30
21
 
31
- ///////////////////////////////////////////////////////////////////////////
32
- // Path helper
33
- ///////////////////////////////////////////////////////////////////////////
34
-
35
- /**
36
- * Parse a BE16-encoded hex path into an array of step values.
37
- * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
38
- * @returns Array of numeric step values.
39
- */
40
- export function parsePathSteps(path: Hex): number[] {
41
- const bytes = hexToBytes(path);
42
- if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
43
- throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
44
- }
45
- const steps: number[] = [];
46
- for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
47
- steps.push(readU16(bytes, offset));
48
- }
49
- return steps;
50
- }
51
-
52
22
  ///////////////////////////////////////////////////////////////////////////
53
23
  // Policy field helper
54
24
  ///////////////////////////////////////////////////////////////////////////
@@ -336,10 +306,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
336
306
  if (depthValue === 0) {
337
307
  throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step", ruleOffset);
338
308
  }
339
- if (depthValue > Limits.MAX_PATH_DEPTH) {
309
+ if (depthValue > PF.MAX_PATH_DEPTH) {
340
310
  throw new CallciumError(
341
311
  "PATH_TOO_DEEP",
342
- `Path depth ${depthValue} exceeds maximum ${Limits.MAX_PATH_DEPTH}`,
312
+ `Path depth ${depthValue} exceeds maximum ${PF.MAX_PATH_DEPTH}`,
343
313
  ruleOffset,
344
314
  );
345
315
  }
@@ -430,7 +400,7 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
430
400
  const operators = constraint.operators.map(hexToBytes);
431
401
  for (const operator of operators) {
432
402
  if (operator.length < 1) {
433
- throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode).");
403
+ throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
434
404
  }
435
405
  }
436
406
 
@@ -438,14 +408,14 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
438
408
 
439
409
  // Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
440
410
  if (path.length === 0) {
441
- throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step.");
411
+ throw new CallciumError("EMPTY_PATH", "Rule path must have at least one step");
442
412
  }
443
413
  if ((path.length & 1) !== 0) {
444
- throw new CallciumError("MALFORMED_PATH", "Path byte length must be even.");
414
+ throw new CallciumError("MALFORMED_PATH", "Path byte length must be even");
445
415
  }
446
416
  const depth = path.length / 2;
447
- if (depth > Limits.MAX_PATH_DEPTH) {
448
- throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${Limits.MAX_PATH_DEPTH}.`);
417
+ if (depth > PF.MAX_PATH_DEPTH) {
418
+ throw new CallciumError("PATH_TOO_DEEP", `Path depth ${depth} exceeds maximum ${PF.MAX_PATH_DEPTH}`);
449
419
  }
450
420
 
451
421
  const hint =
@@ -484,7 +454,7 @@ function sortRules(rules: Rule[]): void {
484
454
  function encodeRule(rule: Rule): Uint8Array {
485
455
  const depth = rule.path.length / 2;
486
456
  if (rule.scope === Scope.CONTEXT && depth !== 1) {
487
- throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step.");
457
+ throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope rules must have exactly one path step");
488
458
  }
489
459
  const data = rule.operator.subarray(1);
490
460
  const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
@@ -1,17 +1,8 @@
1
- import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex } from "./bytes";
1
+ import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex, toAddress } from "./bytes";
2
2
  import { loadWord, readPointer } from "./calldata-reader";
3
- import {
4
- PolicyFormat as PF,
5
- Scope,
6
- MAX_CONTEXT_PROPERTY_ID,
7
- Limits,
8
- Op,
9
- TypeCode,
10
- classifyTypeCode,
11
- lookupContextProperty,
12
- } from "./constants";
3
+ import { PolicyFormat as PF, Scope, MAX_CONTEXT_PROPERTY_ID, Op, TypeCode, lookupContextProperty } from "./constants";
13
4
  import { CallciumError, PolicyViolationError } from "./errors";
14
- import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize } from "./operators";
5
+ import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize, classifyTypeCode } from "./operators";
15
6
  import { decodePolicy } from "./policy-coder";
16
7
 
17
8
  import type { ReadResult } from "./calldata-reader";
@@ -30,20 +21,17 @@ import type {
30
21
  // Helpers
31
22
  ///////////////////////////////////////////////////////////////////////////
32
23
 
33
- /** Violations that abort evaluation instead of failing only their group (spec §9.3). */
34
- const ABORT_VIOLATION_CODES: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
24
+ /** Violations that abort evaluation instead of failing only their group. */
25
+ const abortViolationCodes: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
35
26
  "CALLDATA_OUT_OF_BOUNDS",
36
27
  "ARRAY_INDEX_OUT_OF_BOUNDS",
37
28
  "NON_CANONICAL_VALUE",
38
29
  "QUANTIFIER_LIMIT_EXCEEDED",
39
30
  ]);
40
31
 
41
- /** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
32
+ /** Validate a hex address and read it as a 256-bit bigint. */
42
33
  function addressToBigInt(hex: string): bigint {
43
- const bytes = hexToBytes(hex);
44
- const padded = new Uint8Array(32);
45
- padded.set(bytes, 32 - bytes.length);
46
- return toBigInt(padded, 0);
34
+ return BigInt(toAddress(hex));
47
35
  }
48
36
 
49
37
  ///////////////////////////////////////////////////////////////////////////
@@ -100,8 +88,8 @@ function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
100
88
  const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
101
89
  if (violation !== null) {
102
90
  allViolations.push(violation);
103
- if (ABORT_VIOLATION_CODES.has(violation.code)) {
104
- // Abort violations reject the policy outright; later groups are not consulted (spec §9.3).
91
+ if (abortViolationCodes.has(violation.code)) {
92
+ // Abort violations reject the policy outright; later groups are not consulted.
105
93
  return { ok: false, violations: allViolations };
106
94
  }
107
95
  groupFailed = true;
@@ -182,9 +170,9 @@ function evaluateRule(
182
170
  * Returns the missing property's type code when the context does not supply it.
183
171
  */
184
172
  function resolveContextOperand(operandData: Uint8Array, context?: Context): bigint | { ctxTypeCode: number } {
185
- const propInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
186
- const value = context?.[propInfo.contextKey];
187
- if (value === undefined) return { ctxTypeCode: propInfo.typeCode };
173
+ const propertyInfo = lookupContextProperty(Number(toBigInt(operandData, 0)));
174
+ const value = context?.[propertyInfo.contextKey];
175
+ if (value === undefined) return { ctxTypeCode: propertyInfo.typeCode };
188
176
  return typeof value === "string" ? addressToBigInt(value) : value;
189
177
  }
190
178
 
@@ -285,7 +273,7 @@ function evalTarget(
285
273
  // A dynamic target's chain ends at its payload, so the word there is the declared length.
286
274
  if (isLengthValidType(typeCode)) {
287
275
  if (!isLengthOp(opCode)) {
288
- throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word.");
276
+ throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word");
289
277
  }
290
278
  const length = readPointer(callData, target);
291
279
  if (typeof length !== "number") return { error: length.code };
@@ -300,7 +288,7 @@ function evalTarget(
300
288
  }
301
289
 
302
290
  if (classifyTypeCode(typeCode).typeClass !== "elementary") {
303
- throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word.");
291
+ throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word");
304
292
  }
305
293
  const word = loadWord(callData, target);
306
294
  if (!(word instanceof Uint8Array)) return { error: word.code };
@@ -500,7 +488,7 @@ function evaluateQuantified(
500
488
  elems += 32;
501
489
  }
502
490
 
503
- if (count > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) {
491
+ if (count > PF.MAX_QUANTIFIED_ARRAY_LENGTH) {
504
492
  return {
505
493
  group: groupIndex,
506
494
  rule: ruleIndex,
@@ -535,7 +523,7 @@ function evaluateQuantified(
535
523
  if (elemIsDynamic) {
536
524
  const followed = follow(callDataBytes, elems, slot);
537
525
  // Navigation failures abort under every quantifier: calldata the enforcer cannot read is
538
- // not an element that merely fails the operator (spec §9.3).
526
+ // not an element that merely fails the operator.
539
527
  if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
540
528
  elem = followed;
541
529
  }
@@ -550,7 +538,7 @@ function evaluateQuantified(
550
538
  const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData, context);
551
539
  if ("error" in applied) {
552
540
  // Error results end the rule whatever the quantifier: a later element cannot rescue
553
- // calldata the enforcer cannot read (abort effects, spec §9.3), and a missing context
541
+ // calldata the enforcer cannot read (an abort effect), and a missing context
554
542
  // property is missing for every element alike (group-local).
555
543
  return targetViolation(frame, applied, elemIndex);
556
544
  }
@@ -602,8 +590,8 @@ function evaluateContextRule(
602
590
  );
603
591
  }
604
592
 
605
- const propInfo = lookupContextProperty(propertyId);
606
- const contextValue = context?.[propInfo.contextKey];
593
+ const propertyInfo = lookupContextProperty(propertyId);
594
+ const contextValue = context?.[propertyInfo.contextKey];
607
595
 
608
596
  if (contextValue === undefined) {
609
597
  return {
@@ -612,7 +600,7 @@ function evaluateContextRule(
612
600
  code: "MISSING_CONTEXT",
613
601
  scope: Scope.CONTEXT,
614
602
  path: pathHex,
615
- typeCode: propInfo.typeCode,
603
+ typeCode: propertyInfo.typeCode,
616
604
  };
617
605
  }
618
606
 
@@ -642,7 +630,7 @@ function evaluateContextRule(
642
630
  result = (value === operand) !== ((opCode & Op.NOT) !== 0);
643
631
  ctxOperand = operand;
644
632
  } else {
645
- result = applyOperator(opCode, value, 32, operandData, propInfo.typeCode);
633
+ result = applyOperator(opCode, value, 32, operandData, propertyInfo.typeCode);
646
634
  }
647
635
 
648
636
  if (!result) {
@@ -654,7 +642,7 @@ function evaluateContextRule(
654
642
  path: pathHex,
655
643
  opCode,
656
644
  operandData: bytesToHex(operandData),
657
- typeCode: propInfo.typeCode,
645
+ typeCode: propertyInfo.typeCode,
658
646
  resolvedValue: bigintToHex(value),
659
647
  ...(ctxOperand !== undefined && { resolvedOperand: bigintToHex(ctxOperand) }),
660
648
  };