@callcium/sdk 0.11.1 → 0.13.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/descriptor.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
2
- import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
1
+ import { readU16, readU24 } from "./bytes";
2
+ import { DescriptorFormat as DF, TypeCode } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
- import { classifyTypeCode } from "./operators";
5
- import { Quantifier, isQuantifier } from "./path";
4
+ import { lookupTypeCode } from "./operators";
6
5
 
7
6
  ///////////////////////////////////////////////////////////////////////////
8
7
  // Public types
@@ -15,29 +14,6 @@ export type TypeInfo = {
15
14
  staticSize: number;
16
15
  };
17
16
 
18
- ///////////////////////////////////////////////////////////////////////////
19
- // Internal types
20
- ///////////////////////////////////////////////////////////////////////////
21
-
22
- /** A hop entry of a compiled chain: [delta, index, meta]. */
23
- type Hop = [number, number, number];
24
-
25
- /** State threaded through the compilation of a path into a hint block. */
26
- type HintWalk = {
27
- /** Hops of the chain currently being compiled. */
28
- chain: Hop[];
29
- /** Hops of the main chain, captured when a quantifier closes it. */
30
- mainHops: Hop[];
31
- /** Byte offset of the next node relative to the chain's current base. */
32
- delta: number;
33
- /** Frame prefix fields, captured when a quantifier closes the main chain. */
34
- frame: { arrayDelta: number; count: number; meta: number };
35
- /** Header kind the path resolves to. */
36
- kind: number;
37
- /** Set while the chain already ends inside the current node, which then takes no entry hop. */
38
- entered: boolean;
39
- };
40
-
41
17
  ///////////////////////////////////////////////////////////////////////////
42
18
  // Internal helpers
43
19
  ///////////////////////////////////////////////////////////////////////////
@@ -46,18 +22,17 @@ type HintWalk = {
46
22
  type Node = { typeCode: number; staticWords: number; nodeLength: number };
47
23
 
48
24
  /**
49
- * Read the node at `offset`.
50
- * Elementary nodes occupy exactly one byte; composite nodes encode their static
51
- * word count and length in the 24-bit meta field that follows the type code.
52
- * @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code,
53
- * or declares a length below its own header or beyond the descriptor.
25
+ * Read the node at `offset`. Elementary nodes occupy exactly one byte. Composite nodes encode their static word count
26
+ * and length in the 24-bit meta field that follows the type code.
27
+ * @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code, or declares a
28
+ * length below its own header or beyond the descriptor.
54
29
  */
55
30
  export function readNode(desc: Uint8Array, offset: number): Node {
56
31
  if (offset >= desc.length) {
57
32
  throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
58
33
  }
59
34
  const typeCode = desc[offset]!;
60
- const info = classifyTypeCode(typeCode);
35
+ const info = lookupTypeCode(typeCode);
61
36
  if (info.typeClass === "elementary") {
62
37
  return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
63
38
  }
@@ -83,50 +58,25 @@ export function readNode(desc: Uint8Array, offset: number): Node {
83
58
 
84
59
  /**
85
60
  * Return the byte length of the node at `offset`.
86
- * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
61
+ * @throws {CallciumError} When the descriptor contains no readable node at `offset`.
87
62
  */
88
63
  function nodeLength(desc: Uint8Array, offset: number): number {
89
64
  return readNode(desc, offset).nodeLength;
90
65
  }
91
66
 
92
- /**
93
- * Return the byte offset of the `fieldIndex`-th field inside a tuple node.
94
- * Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
95
- */
96
- function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: number): number {
97
- let cursor = tupleOffset + DF.TUPLE_HEADER_SIZE;
98
- for (let i = 0; i < fieldIndex; i++) {
99
- cursor += nodeLength(desc, cursor);
100
- }
101
- return cursor;
102
- }
103
-
104
67
  ///////////////////////////////////////////////////////////////////////////
105
68
  // Public interface
106
69
  ///////////////////////////////////////////////////////////////////////////
107
70
 
108
71
  /**
109
72
  * Return the declared top-level parameter count.
110
- * @throws {CallciumError} When the descriptor is too short to hold a header.
73
+ * @throws {CallciumError} When the descriptor is too short for a header.
111
74
  */
112
75
  function paramCount(desc: Uint8Array): number {
113
76
  if (desc.length < DF.HEADER_SIZE) {
114
77
  throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
115
78
  }
116
- return desc[1]!;
117
- }
118
-
119
- /** Return byte offset of the N-th top-level param (0-indexed). */
120
- function paramOffset(desc: Uint8Array, index: number): number {
121
- const count = paramCount(desc);
122
- if (index >= count) {
123
- throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
124
- }
125
- let cursor = DF.HEADER_SIZE;
126
- for (let i = 0; i < index; i++) {
127
- cursor += nodeLength(desc, cursor);
128
- }
129
- return cursor;
79
+ return desc[DF.HEADER_PARAMCOUNT_OFFSET]!;
130
80
  }
131
81
 
132
82
  /**
@@ -134,91 +84,20 @@ function paramOffset(desc: Uint8Array, index: number): number {
134
84
  * @param desc - Raw descriptor bytes.
135
85
  * @param offset - Byte position of the node.
136
86
  * @returns typeCode, isDynamic, and staticSize in bytes.
137
- * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
87
+ * @throws {CallciumError} When the descriptor contains no readable node at `offset`.
138
88
  */
139
89
  function inspect(desc: Uint8Array, offset: number): TypeInfo {
140
90
  const { typeCode, staticWords } = readNode(desc, offset);
141
91
  return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
142
92
  }
143
93
 
144
- /**
145
- * Resolve the type at a calldata path (array of step indices).
146
- *
147
- * The first step selects a top-level param. Each subsequent step descends
148
- * into the current node: for tuples it selects a field, for arrays it
149
- * advances to the element descriptor.
150
- *
151
- * @param desc - Raw descriptor bytes.
152
- * @param steps - Path steps, length >= 1.
153
- * @returns TypeInfo for the node at the resolved path.
154
- * @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
155
- */
156
- function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
157
- return walkPath(desc, steps).typeInfo;
158
- }
159
-
160
- /**
161
- * Resolve the type at the path and the declared length of the static array a
162
- * quantifier step descends into (zero when the path holds no quantifier step
163
- * over a static array).
164
- * @param desc - Raw descriptor bytes.
165
- * @param steps - Path steps, length >= 1.
166
- * @returns TypeInfo at the resolved path and the quantified static array length.
167
- * @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
168
- */
169
- function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
170
- if (steps.length === 0) {
171
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
172
- }
173
-
174
- let cursor = paramOffset(desc, steps[0]!);
175
- let quantifiedStaticLength = 0;
176
-
177
- for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
178
- const typeCode = desc[cursor]!;
179
- const step = steps[stepIndex]!;
180
-
181
- if (typeCode === TypeCode.TUPLE) {
182
- const fields = tupleFieldCount(desc, cursor);
183
- if (step >= fields) {
184
- throw new CallciumError(
185
- "TUPLE_FIELD_OUT_OF_BOUNDS",
186
- `Tuple field index ${step} out of range (tuple has ${fields} fields)`,
187
- );
188
- }
189
- cursor = tupleFieldOffset(desc, cursor, step);
190
- } else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
191
- // Quantifier sentinels descend into the element type; only concrete
192
- // indices are bounds-checked against the declared array length.
193
- if (typeCode === TypeCode.STATIC_ARRAY) {
194
- if (isQuantifier(step)) {
195
- quantifiedStaticLength = staticArrayLength(desc, cursor);
196
- } else {
197
- const length = staticArrayLength(desc, cursor);
198
- if (step >= length) {
199
- throw new CallciumError(
200
- "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
201
- `Array index ${step} out of range (length=${length})`,
202
- );
203
- }
204
- }
205
- }
206
- cursor = arrayElementOffset(cursor);
207
- } else {
208
- throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
209
- }
210
- }
211
-
212
- return { typeInfo: inspect(desc, cursor), quantifiedStaticLength };
213
- }
214
-
215
94
  /**
216
95
  * Return tuple field count at a tuple node offset.
217
96
  * @param desc - Raw descriptor bytes.
218
97
  * @param offset - Byte position of the tuple node.
219
98
  */
220
99
  function tupleFieldCount(desc: Uint8Array, offset: number): number {
221
- return readU16(desc, offset + 4);
100
+ return readU16(desc, offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE);
222
101
  }
223
102
 
224
103
  /**
@@ -241,188 +120,99 @@ function arrayElementOffset(offset: number): number {
241
120
  return offset + DF.ARRAY_HEADER_SIZE;
242
121
  }
243
122
 
244
- /** Build the error for a path step that does not resolve to a hop-chain target. */
245
- function uncompilablePath(stepIndex: number): CallciumError {
246
- return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
247
- }
248
-
249
- /** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
250
- function headSpan(desc: Uint8Array, offset: number, count: number): { span: number; nodeOffset: number } {
251
- let span = 0;
252
- let nodeOffset = offset;
253
- for (let i = 0; i < count; i++) {
254
- const node = inspect(desc, nodeOffset);
255
- // An indirected node occupies a single offset word.
256
- span += node.isDynamic ? 32 : node.staticSize;
257
- nodeOffset += nodeLength(desc, nodeOffset);
258
- }
259
- return { span, nodeOffset };
260
- }
261
-
262
- /** Return the meta word describing the array node at `arrayOffset`, and whether its elements are dynamic. */
263
- function arrayMeta(desc: Uint8Array, arrayOffset: number, code: number): { meta: number; elemIsDynamic: boolean } {
264
- const element = inspect(desc, arrayElementOffset(arrayOffset));
265
- // A dynamic element occupies one offset word within the element region.
266
- let meta = element.isDynamic ? 1 : element.staticSize >> 5;
267
- if (element.isDynamic) meta |= PF.HINT_META_ELEM_DYNAMIC;
268
- if (code === TypeCode.DYNAMIC_ARRAY) meta |= PF.HINT_META_DYNAMIC_ARRAY;
269
- return { meta, elemIsDynamic: element.isDynamic };
270
- }
271
-
272
- /** Append the hop entering an indirected node's payload and rebase the offset accumulator. */
273
- function enterNode(walk: HintWalk, needed: boolean): void {
274
- if (!needed) return;
275
- walk.chain.push([walk.delta, PF.HINT_NO_INDEX, 0]);
276
- walk.delta = 0;
277
- }
278
-
279
- /** Apply one path step to `walk` and return the descriptor offset the step reaches. */
280
- function walkStep(desc: Uint8Array, walk: HintWalk, descOffset: number, step: number, stepIndex: number): number {
281
- const node = inspect(desc, descOffset);
123
+ ///////////////////////////////////////////////////////////////////////////
124
+ // Metadata validity
125
+ ///////////////////////////////////////////////////////////////////////////
282
126
 
283
- // A quantifier reaches an array alone, every other code reading its value as an index.
284
- if (node.typeCode === TypeCode.TUPLE) {
285
- if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
286
- enterNode(walk, node.isDynamic && !walk.entered);
127
+ /** Composite node metadata that disagrees with the node's children. */
128
+ export type MetaFault = "NODE_LENGTH" | "STATIC_WORDS";
287
129
 
288
- const field = headSpan(desc, descOffset + DF.TUPLE_HEADER_SIZE, step);
289
- walk.delta += field.span;
290
- walk.entered = false;
291
- return field.nodeOffset;
292
- }
130
+ /** A node's span and its type's static word count, or the first node whose metadata disagrees with them. */
131
+ type MetaWalk = { span: number; staticWords: number } | { fault: MetaFault; offset: number };
293
132
 
294
- if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) {
295
- throw uncompilablePath(stepIndex);
133
+ /** Walk the node at `offset` by the spans of its children. */
134
+ function walkMeta(desc: Uint8Array, offset: number): MetaWalk {
135
+ const node = readNode(desc, offset);
136
+ if (lookupTypeCode(node.typeCode).typeClass === "elementary") {
137
+ return { span: DF.TYPECODE_SIZE, staticWords: node.staticWords };
296
138
  }
297
- if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) {
298
- throw uncompilablePath(stepIndex);
299
- }
300
- const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
301
-
302
- if (isQuantifier(step)) {
303
- if (walk.kind !== PF.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
304
- enterNode(walk, node.isDynamic && !walk.entered);
305
139
 
306
- const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
307
- walk.frame = { arrayDelta: walk.delta, count, meta };
308
- walk.kind = step === Quantifier.ALL ? PF.HINT_KIND_ALL : PF.HINT_KIND_ANY;
309
-
310
- walk.mainHops = walk.chain;
311
- walk.chain = [];
312
- walk.delta = 0;
313
- } else if (node.isDynamic) {
314
- // An indirected array holds its elements behind an offset word.
315
- enterNode(walk, !walk.entered);
316
- walk.chain.push([0, step, meta]);
317
- walk.delta = 0;
140
+ let span: number;
141
+ let staticWords = 0;
142
+ if (node.typeCode === TypeCode.TUPLE) {
143
+ span = DF.TUPLE_HEADER_SIZE;
144
+ let hasDynamicField = false;
145
+ const fieldCount = tupleFieldCount(desc, offset);
146
+ for (let i = 0; i < fieldCount; i++) {
147
+ const field = walkMeta(desc, offset + span);
148
+ if ("fault" in field) return field;
149
+ span += field.span;
150
+ staticWords += field.staticWords;
151
+ if (field.staticWords === 0) hasDynamicField = true;
152
+ }
153
+ if (hasDynamicField) staticWords = 0;
318
154
  } else {
319
- // A static array of static elements is inline, so the index folds into the accumulator.
320
- walk.delta += step * (meta & PF.HINT_META_STRIDE_MASK) * 32;
321
- }
322
-
323
- walk.entered = elemIsDynamic;
324
- return arrayElementOffset(descOffset);
325
- }
326
-
327
- /** Serialize hop entries at `offset` and return the offset past them. */
328
- function writeHops(out: Uint8Array, offset: number, hops: Hop[]): number {
329
- for (const [delta, index, meta] of hops) {
330
- writeBE32(out, offset, delta);
331
- writeBE16(out, offset + PF.HINT_HOP_INDEX_OFFSET, index);
332
- writeBE16(out, offset + PF.HINT_HOP_META_OFFSET, meta);
333
- offset += PF.HINT_HOP_SIZE;
155
+ const element = walkMeta(desc, arrayElementOffset(offset));
156
+ if ("fault" in element) return element;
157
+ span = DF.ARRAY_HEADER_SIZE + element.span;
158
+ if (node.typeCode === TypeCode.STATIC_ARRAY) {
159
+ staticWords = element.staticWords * readU16(desc, offset + span);
160
+ span += DF.ARRAY_LENGTH_SIZE;
161
+ }
334
162
  }
335
- return offset;
163
+ if (span !== node.nodeLength) return { fault: "NODE_LENGTH", offset };
164
+ if (staticWords !== node.staticWords) return { fault: "STATIC_WORDS", offset };
165
+ return { span, staticWords };
336
166
  }
337
167
 
338
- /** Serialize a completed walk and its target block into the wire hint block. */
339
- function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth: number): Uint8Array {
340
- let suffixHops: Hop[] = [];
341
- if (walk.kind === PF.HINT_KIND_NONE) walk.mainHops = walk.chain;
342
- else suffixHops = walk.chain;
343
-
344
- const mainHopCount = walk.mainHops.length;
345
- const suffixHopCount = suffixHops.length;
346
- if (mainHopCount > PF.HINT_HOP_COUNT_MASK || suffixHopCount > PF.HINT_HOP_COUNT_MASK) {
347
- throw uncompilablePath(depth);
348
- }
349
-
350
- const quantified = walk.kind !== PF.HINT_KIND_NONE;
351
- const frameSize = quantified
352
- ? PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + suffixHopCount * PF.HINT_HOP_SIZE
353
- : 0;
354
- const out = new Uint8Array(PF.HINT_HEADER_SIZE + mainHopCount * PF.HINT_HOP_SIZE + frameSize + PF.HINT_TARGET_SIZE);
355
-
356
- let offset = 0;
357
- out[offset] = (walk.kind << PF.HINT_KIND_SHIFT) | mainHopCount;
358
- offset = writeHops(out, offset + PF.HINT_HEADER_SIZE, walk.mainHops);
359
-
360
- if (quantified) {
361
- writeBE32(out, offset, walk.frame.arrayDelta);
362
- writeBE16(out, offset + PF.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
363
- writeBE16(out, offset + PF.HINT_FRAME_META_OFFSET, walk.frame.meta);
364
- out[offset + PF.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
365
- offset = writeHops(out, offset + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, suffixHops);
168
+ /**
169
+ * Find the first composite node whose metadata disagrees with its children (DV-1, DV-2). A node's children are checked
170
+ * before the node.
171
+ * @param desc - A well-formed descriptor.
172
+ * @returns The disagreement and the node's offset, or `undefined` when every node agrees.
173
+ */
174
+ function tryValidateMeta(desc: Uint8Array): { fault: MetaFault; offset: number } | undefined {
175
+ let cursor = DF.HEADER_SIZE;
176
+ const count = paramCount(desc);
177
+ for (let i = 0; i < count; i++) {
178
+ const walk = walkMeta(desc, cursor);
179
+ if ("fault" in walk) return walk;
180
+ cursor += walk.span;
366
181
  }
367
-
368
- writeBE32(out, offset, walk.delta);
369
- writeBE16(out, offset + PF.HINT_TARGET_META_OFFSET, targetMeta);
370
- out[offset + PF.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
371
- return out;
182
+ return undefined;
372
183
  }
373
184
 
374
185
  /**
375
- * Compile a calldata rule path into its wire hint block.
376
- *
377
- * @param desc - Raw descriptor bytes.
378
- * @param steps - Path steps, length >= 1.
379
- * @returns The hint block bytes.
380
- * @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over
381
- * a non-array node, or repeats a quantifier.
186
+ * Validate every composite node's metadata against its children (DV-1, DV-2).
187
+ * @param desc - A well-formed descriptor.
188
+ * @throws {CallciumError} With `NODE_LENGTH_MISMATCH` or `STATIC_WORDS_MISMATCH` at the first node at fault.
382
189
  */
383
- function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
384
- if (steps.length === 0) {
385
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
190
+ function validateMeta(desc: Uint8Array): void {
191
+ const found = tryValidateMeta(desc);
192
+ if (found?.fault === "NODE_LENGTH") {
193
+ throw new CallciumError(
194
+ "NODE_LENGTH_MISMATCH",
195
+ "Composite node length differs from the span of its children",
196
+ found.offset,
197
+ );
386
198
  }
387
-
388
- const argIndex = steps[0]!;
389
- if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
390
-
391
- // The argument's head slot sits past the slots of every preceding parameter.
392
- const arg = headSpan(desc, DF.HEADER_SIZE, argIndex);
393
- const walk: HintWalk = {
394
- chain: [],
395
- mainHops: [],
396
- delta: arg.span,
397
- frame: { arrayDelta: 0, count: 0, meta: 0 },
398
- kind: PF.HINT_KIND_NONE,
399
- entered: false,
400
- };
401
-
402
- let descOffset = arg.nodeOffset;
403
- for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
404
- descOffset = walkStep(desc, walk, descOffset, steps[stepIndex]!, stepIndex);
199
+ if (found?.fault === "STATIC_WORDS") {
200
+ throw new CallciumError(
201
+ "STATIC_WORDS_MISMATCH",
202
+ "Composite node static word count differs from its type's head size",
203
+ found.offset,
204
+ );
405
205
  }
406
-
407
- const target = inspect(desc, descOffset);
408
- enterNode(walk, target.isDynamic && !walk.entered);
409
-
410
- const targetMeta = target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0;
411
-
412
- return encodeHint(walk, targetMeta, target.typeCode, steps.length);
413
206
  }
414
207
 
415
208
  /** Inspect and navigate raw descriptor bytes. */
416
209
  export const Descriptor = {
417
210
  paramCount,
418
- paramOffset,
419
211
  inspect,
420
- typeAt,
421
- tupleFieldOffset,
422
212
  tupleFieldCount,
423
213
  staticArrayLength,
424
214
  nodeLength,
425
215
  arrayElementOffset,
426
- walkPath,
427
- compileHint,
216
+ validateMeta,
217
+ tryValidateMeta,
428
218
  };
package/src/errors.ts CHANGED
@@ -1,27 +1,27 @@
1
+ import type { Issue, Violation } from "./types";
2
+
1
3
  /**
2
4
  * Machine-readable error code for structural decoding and validation failures.
3
5
  *
4
- * Each code names one invariant. Where Solidity detects the same invariant, the code is its error
5
- * name transliterated from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and
6
- * validation" have no counterpart: they name input a Solidity signature cannot express.
7
- * `OUT_OF_PHYSICAL_BOUNDS` names an invariant Solidity carries twice, as the error
8
- * `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
6
+ * Each code names one invariant. Where Solidity detects the same invariant, the code is its error name transliterated
7
+ * from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and validation" have no counterpart: they name
8
+ * input a Solidity signature cannot express.
9
9
  */
10
10
  export type CallciumErrorCode =
11
11
  // Policy and descriptor framing.
12
12
  | "UNSUPPORTED_VERSION"
13
+ | "UNSUPPORTED_MINOR"
14
+ | "MINOR_MISMATCH"
13
15
  | "MALFORMED_HEADER"
14
16
  | "UNEXPECTED_END"
15
17
  | "TRAILING_BYTES"
16
18
  // Groups and rules.
17
19
  | "EMPTY_POLICY"
18
20
  | "EMPTY_GROUP"
19
- | "GROUP_TOO_SMALL"
20
21
  | "GROUP_SIZE_MISMATCH"
21
22
  | "GROUP_OVERFLOW"
22
23
  | "RULE_TOO_SMALL"
23
24
  | "RULE_SIZE_MISMATCH"
24
- | "RULE_FIELD_OUT_OF_BOUNDS"
25
25
  | "RULE_OVERFLOW"
26
26
  // Paths.
27
27
  | "EMPTY_PATH"
@@ -31,22 +31,25 @@ export type CallciumErrorCode =
31
31
  | "TUPLE_FIELD_OUT_OF_BOUNDS"
32
32
  | "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS"
33
33
  | "NOT_COMPOSITE"
34
- | "UNCOMPILABLE_PATH"
35
34
  | "QUANTIFIER_ON_NON_ARRAY"
36
- | "NESTED_QUANTIFIER"
35
+ | "UNKNOWN_PATH_STEP"
36
+ // Loops.
37
+ | "UNKNOWN_QUANTIFIER"
38
+ | "INVALID_NESTED_LOOP"
37
39
  // Scope and context.
38
- | "INVALID_SCOPE"
40
+ | "UNKNOWN_SCOPE"
39
41
  | "INVALID_CONTEXT_PATH"
40
42
  | "UNKNOWN_CONTEXT_PROPERTY"
41
43
  // Operators and hints.
42
44
  | "UNKNOWN_OPERATOR"
43
45
  | "INVALID_OPERATOR_BYTES"
44
46
  | "OPERATOR_TARGET_MISMATCH"
47
+ | "INVALID_CONTEXT_OPERAND"
48
+ | "DATA_LENGTH_MISMATCH"
45
49
  | "UNSORTED_IN_SET"
46
50
  | "MALFORMED_HINT"
47
51
  // Constraint building.
48
52
  | "NO_CONSTRAINT_OPERATORS"
49
- | "DUPLICATE_PATH_IN_GROUP"
50
53
  | "EMPTY_SET"
51
54
  | "SET_TOO_LARGE"
52
55
  | "INVALID_RANGE"
@@ -62,6 +65,12 @@ export type CallciumErrorCode =
62
65
  | "STATIC_WORDS_TOO_LARGE"
63
66
  | "PARAM_COUNT_MISMATCH"
64
67
  | "TOO_MANY_PARAMS"
68
+ | "NODE_LENGTH_MISMATCH"
69
+ | "STATIC_WORDS_MISMATCH"
70
+ // Type codes.
71
+ | "INVALID_UINT_BITS"
72
+ | "INVALID_INT_BITS"
73
+ | "INVALID_BYTES_LENGTH"
65
74
  // Type strings and signatures.
66
75
  | "MALFORMED_TYPE_STRING"
67
76
  | "UNKNOWN_TYPE"
@@ -73,18 +82,19 @@ export type CallciumErrorCode =
73
82
  | "RULE_COUNT_OVERFLOW"
74
83
  | "RULE_SIZE_OVERFLOW"
75
84
  | "DESC_LENGTH_OVERFLOW"
85
+ // Strict build.
86
+ | "VALIDATION_ERROR"
76
87
  // Inputs and validation.
77
- | "UNKNOWN_QUANTIFIER"
78
88
  | "INVALID_HEX"
79
89
  | "MALFORMED_SELECTOR"
80
90
  | "PATH_STEP_OVERFLOW"
81
91
  | "OPERAND_OVERFLOW"
82
92
  | "CONTEXT_VALUE_OVERFLOW"
83
- | "VALIDATION_ERROR";
93
+ | "MALFORMED_CONTEXT_VALUE"
94
+ | "MALFORMED_OPERAND";
84
95
 
85
96
  /**
86
- * Thrown when a policy or descriptor blob is structurally malformed.
87
- * The optional `offset` indicates the byte position in the source blob where the error was detected.
97
+ * Thrown on malformed or inadmissible SDK input. `offset`, when set, is the error's byte position in the source blob.
88
98
  */
89
99
  export class CallciumError extends Error {
90
100
  public readonly code: CallciumErrorCode;
@@ -96,7 +106,7 @@ export class CallciumError extends Error {
96
106
  super(`${prefix}${message}`);
97
107
  this.name = "CallciumError";
98
108
  this.code = code;
99
- // The property is declared optional, so it stays absent rather than present and undefined.
109
+ // `exactOptionalPropertyTypes` admits no `undefined` in the optional property.
100
110
  if (offset !== undefined) this.offset = offset;
101
111
  }
102
112
  }
@@ -104,14 +114,13 @@ export class CallciumError extends Error {
104
114
  /**
105
115
  * Thrown by `PolicyBuilder.build` when validation reports any issue.
106
116
  *
107
- * Carries every issue the validator found. The `Error.message` is the first issue's
108
- * message — a diagnostic summary, not a presentation contract. Consumers rendering
109
- * issues to users should iterate `issues` directly.
117
+ * Carries every issue the validator found. The `Error.message` is the first issue's message — a diagnostic summary, not
118
+ * a presentation contract. To render issues to users, iterate `issues` directly.
110
119
  */
111
120
  export class ValidationError extends CallciumError {
112
- public readonly issues: import("./types").Issue[];
121
+ public readonly issues: Issue[];
113
122
 
114
- constructor(issues: import("./types").Issue[]) {
123
+ constructor(issues: Issue[]) {
115
124
  super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
116
125
  this.name = "ValidationError";
117
126
  this.issues = issues;
@@ -119,18 +128,17 @@ export class ValidationError extends CallciumError {
119
128
  }
120
129
 
121
130
  /**
122
- * Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement.
123
- * Distinct from `ValidationError`, which signals static issues in the policy itself.
131
+ * Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement. Distinct from `ValidationError`, which
132
+ * signals static issues in the policy itself.
124
133
  *
125
- * Carries the full list of structured violations (one per failed group). The
126
- * `Error.message` is a minimal non-lossy diagnostic summary built from the
127
- * first violation's structured fields — not a presentation contract. Consumers
128
- * rendering violations to users should iterate `violations` directly.
134
+ * Carries the full list of structured violations (one per failed group). The `Error.message` is a minimal non-lossy
135
+ * diagnostic summary built from the first violation's structured fields — not a presentation contract. To render
136
+ * violations to users, iterate `violations` directly.
129
137
  */
130
138
  export class PolicyViolationError extends Error {
131
- public readonly violations: import("./types").Violation[];
139
+ public readonly violations: Violation[];
132
140
 
133
- constructor(violations: import("./types").Violation[]) {
141
+ constructor(violations: Violation[]) {
134
142
  super(formatDiagnostic(violations[0]));
135
143
  this.name = "PolicyViolationError";
136
144
  this.violations = violations;
@@ -138,7 +146,7 @@ export class PolicyViolationError extends Error {
138
146
  }
139
147
 
140
148
  /** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
141
- function formatDiagnostic(violation: import("./types").Violation | undefined): string {
149
+ function formatDiagnostic(violation: Violation | undefined): string {
142
150
  if (!violation) return "Policy violation";
143
151
  const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
144
152
  return `Policy violation: ${violation.code}${location}`;