@callcium/sdk 0.9.0 → 0.11.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,6 +1,8 @@
1
1
  import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
2
- import { DescriptorFormat as DF, PolicyFormat as PF, Quantifier, TypeCode, isQuantifier } from "./constants";
2
+ import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
+ import { classifyTypeCode } from "./operators";
5
+ import { Quantifier, isQuantifier } from "./path";
4
6
 
5
7
  ///////////////////////////////////////////////////////////////////////////
6
8
  // Public types
@@ -40,15 +42,51 @@ type HintWalk = {
40
42
  // Internal helpers
41
43
  ///////////////////////////////////////////////////////////////////////////
42
44
 
45
+ /** A descriptor node's type code, static word count, and total byte length. */
46
+ type Node = { typeCode: number; staticWords: number; nodeLength: number };
47
+
48
+ /**
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.
54
+ */
55
+ export function readNode(desc: Uint8Array, offset: number): Node {
56
+ if (offset >= desc.length) {
57
+ throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
58
+ }
59
+ const typeCode = desc[offset]!;
60
+ const info = classifyTypeCode(typeCode);
61
+ if (info.typeClass === "elementary") {
62
+ return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
63
+ }
64
+
65
+ if (offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE > desc.length) {
66
+ throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
67
+ }
68
+ const meta = readU24(desc, offset + DF.TYPECODE_SIZE);
69
+ const length = meta & DF.META_NODE_LENGTH_MASK;
70
+ const minHeader = info.typeClass === "tuple" ? DF.TUPLE_HEADER_SIZE : DF.ARRAY_HEADER_SIZE;
71
+ if (length < minHeader) {
72
+ throw new CallciumError(
73
+ "NODE_LENGTH_TOO_SMALL",
74
+ `Composite node length ${length} is smaller than minimum header ${minHeader}`,
75
+ offset,
76
+ );
77
+ }
78
+ if (offset + length > desc.length) {
79
+ throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
80
+ }
81
+ return { typeCode, staticWords: meta >> DF.META_STATIC_WORDS_SHIFT, nodeLength: length };
82
+ }
83
+
43
84
  /**
44
85
  * Return the byte length of the node at `offset`.
45
- * Elementary nodes occupy exactly one byte; composite nodes encode their
46
- * length in the lower 12 bits of the 24-bit meta field.
86
+ * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
47
87
  */
48
88
  function nodeLength(desc: Uint8Array, offset: number): number {
49
- const typeCode = desc[offset]!;
50
- if (typeCode < TypeCode.STATIC_ARRAY) return DF.TYPECODE_SIZE;
51
- return readU24(desc, offset + 1) & DF.META_NODE_LENGTH_MASK;
89
+ return readNode(desc, offset).nodeLength;
52
90
  }
53
91
 
54
92
  /**
@@ -67,16 +105,22 @@ function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: num
67
105
  // Public interface
68
106
  ///////////////////////////////////////////////////////////////////////////
69
107
 
70
- /** Return paramCount from header byte 1. */
108
+ /**
109
+ * Return the declared top-level parameter count.
110
+ * @throws {CallciumError} When the descriptor is too short to hold a header.
111
+ */
71
112
  function paramCount(desc: Uint8Array): number {
113
+ if (desc.length < DF.HEADER_SIZE) {
114
+ throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
115
+ }
72
116
  return desc[1]!;
73
117
  }
74
118
 
75
119
  /** Return byte offset of the N-th top-level param (0-indexed). */
76
120
  function paramOffset(desc: Uint8Array, index: number): number {
77
- const count = desc[1]!;
121
+ const count = paramCount(desc);
78
122
  if (index >= count) {
79
- throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count}).`);
123
+ throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
80
124
  }
81
125
  let cursor = DF.HEADER_SIZE;
82
126
  for (let i = 0; i < index; i++) {
@@ -90,17 +134,11 @@ function paramOffset(desc: Uint8Array, index: number): number {
90
134
  * @param desc - Raw descriptor bytes.
91
135
  * @param offset - Byte position of the node.
92
136
  * @returns typeCode, isDynamic, and staticSize in bytes.
137
+ * @throws {CallciumError} When the descriptor holds no readable node at `offset`.
93
138
  */
94
139
  function inspect(desc: Uint8Array, offset: number): TypeInfo {
95
- const typeCode = desc[offset]!;
96
- let staticWords: number;
97
- if (typeCode < TypeCode.STATIC_ARRAY) {
98
- staticWords = typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
99
- } else {
100
- staticWords = readU24(desc, offset + 1) >> DF.META_STATIC_WORDS_SHIFT;
101
- }
102
- const isDynamic = staticWords === 0;
103
- return { typeCode, isDynamic, staticSize: staticWords * 32 };
140
+ const { typeCode, staticWords } = readNode(desc, offset);
141
+ return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
104
142
  }
105
143
 
106
144
  /**
@@ -130,19 +168,10 @@ function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
130
168
  */
131
169
  function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
132
170
  if (steps.length === 0) {
133
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
134
- }
135
-
136
- const paramIndex = steps[0]!;
137
- const count = desc[1]!;
138
- if (paramIndex >= count) {
139
- throw new CallciumError(
140
- "PARAM_INDEX_OUT_OF_BOUNDS",
141
- `Param index ${paramIndex} out of range (paramCount=${count}).`,
142
- );
171
+ throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
143
172
  }
144
173
 
145
- let cursor = paramOffset(desc, paramIndex);
174
+ let cursor = paramOffset(desc, steps[0]!);
146
175
  let quantifiedStaticLength = 0;
147
176
 
148
177
  for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
@@ -154,7 +183,7 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
154
183
  if (step >= fields) {
155
184
  throw new CallciumError(
156
185
  "TUPLE_FIELD_OUT_OF_BOUNDS",
157
- `Tuple field index ${step} out of range (tuple has ${fields} fields).`,
186
+ `Tuple field index ${step} out of range (tuple has ${fields} fields)`,
158
187
  );
159
188
  }
160
189
  cursor = tupleFieldOffset(desc, cursor, step);
@@ -169,14 +198,14 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
169
198
  if (step >= length) {
170
199
  throw new CallciumError(
171
200
  "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
172
- `Array index ${step} out of range (length=${length}).`,
201
+ `Array index ${step} out of range (length=${length})`,
173
202
  );
174
203
  }
175
204
  }
176
205
  }
177
206
  cursor = arrayElementOffset(cursor);
178
207
  } else {
179
- throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
208
+ throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
180
209
  }
181
210
  }
182
211
 
@@ -199,8 +228,8 @@ function tupleFieldCount(desc: Uint8Array, offset: number): number {
199
228
  */
200
229
  function staticArrayLength(desc: Uint8Array, offset: number): number {
201
230
  const elemOffset = arrayElementOffset(offset);
202
- const elemLen = nodeLength(desc, elemOffset);
203
- const lengthOffset = elemOffset + elemLen;
231
+ const elemNodeLength = nodeLength(desc, elemOffset);
232
+ const lengthOffset = elemOffset + elemNodeLength;
204
233
  return readU16(desc, lengthOffset);
205
234
  }
206
235
 
@@ -214,7 +243,7 @@ function arrayElementOffset(offset: number): number {
214
243
 
215
244
  /** Build the error for a path step that does not resolve to a hop-chain target. */
216
245
  function uncompilablePath(stepIndex: number): CallciumError {
217
- return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target.`);
246
+ return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
218
247
  }
219
248
 
220
249
  /** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
@@ -353,7 +382,7 @@ function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth:
353
382
  */
354
383
  function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
355
384
  if (steps.length === 0) {
356
- throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
385
+ throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
357
386
  }
358
387
 
359
388
  const argIndex = steps[0]!;
package/src/errors.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Machine-readable error code for structural decoding and validation failures.
3
3
  *
4
- * Each code names one invariant and matches the Solidity error raised for the same
5
- * invariant, transliterated from `PascalCase` to `SCREAMING_SNAKE_CASE`.
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
9
  */
7
10
  export type CallciumErrorCode =
8
11
  // Policy and descriptor framing.
@@ -47,6 +50,7 @@ export type CallciumErrorCode =
47
50
  | "EMPTY_SET"
48
51
  | "SET_TOO_LARGE"
49
52
  | "INVALID_RANGE"
53
+ | "OUT_OF_PHYSICAL_BOUNDS"
50
54
  // Descriptor nodes.
51
55
  | "UNKNOWN_TYPE_CODE"
52
56
  | "NODE_LENGTH_TOO_SMALL"
@@ -72,6 +76,10 @@ export type CallciumErrorCode =
72
76
  // Inputs and validation.
73
77
  | "UNKNOWN_QUANTIFIER"
74
78
  | "INVALID_HEX"
79
+ | "MALFORMED_SELECTOR"
80
+ | "PATH_STEP_OVERFLOW"
81
+ | "OPERAND_OVERFLOW"
82
+ | "CONTEXT_VALUE_OVERFLOW"
75
83
  | "VALIDATION_ERROR";
76
84
 
77
85
  /**
@@ -88,7 +96,8 @@ export class CallciumError extends Error {
88
96
  super(`${prefix}${message}`);
89
97
  this.name = "CallciumError";
90
98
  this.code = code;
91
- Object.setPrototypeOf(this, CallciumError.prototype);
99
+ // The property is declared optional, so it stays absent rather than present and undefined.
100
+ if (offset !== undefined) this.offset = offset;
92
101
  }
93
102
  }
94
103
 
@@ -106,7 +115,6 @@ export class ValidationError extends CallciumError {
106
115
  super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
107
116
  this.name = "ValidationError";
108
117
  this.issues = issues;
109
- Object.setPrototypeOf(this, ValidationError.prototype);
110
118
  }
111
119
  }
112
120
 
@@ -126,7 +134,6 @@ export class PolicyViolationError extends Error {
126
134
  super(formatDiagnostic(violations[0]));
127
135
  this.name = "PolicyViolationError";
128
136
  this.violations = violations;
129
- Object.setPrototypeOf(this, PolicyViolationError.prototype);
130
137
  }
131
138
  }
132
139
 
package/src/index.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  ///////////////////////////////////////////////////////////////////////////
4
4
 
5
5
  export { PolicyBuilder } from "./policy-builder";
6
- export { PolicyCoder, parsePathSteps } from "./policy-coder";
6
+ export { PolicyCoder } from "./policy-coder";
7
+ export { isQuantifier, lookupQuantifier, parsePathSteps, Quantifier } from "./path";
8
+ export type { QuantifierInfo } from "./path";
7
9
  export { isOpAllowed, PolicyValidator } from "./policy-validator";
8
10
  export {
9
11
  arg,
@@ -41,29 +43,17 @@ export {
41
43
  Op,
42
44
  TypeCode,
43
45
  PolicyFormat,
44
- Quantifier,
45
46
  Scope,
46
47
  ContextProperty,
47
- Limits,
48
48
  MAX_CONTEXT_PROPERTY_ID,
49
+ findContextProperty,
49
50
  lookupOp,
50
51
  lookupScope,
51
52
  lookupContextProperty,
52
- lookupQuantifier,
53
- lookupTypeCode,
54
- isQuantifier,
55
- } from "./constants";
56
- export { isLengthOp } from "./operators";
57
- export type {
58
- Operands,
59
- OpInfo,
60
- ScopeInfo,
61
- ContextPropertyInfo,
62
- QuantifierInfo,
63
- TypeCodeInfo,
64
- TypeClassInfo,
65
- TypeClass,
66
53
  } from "./constants";
54
+ export { isLengthOp, lookupTypeCode } from "./operators";
55
+ export type { TypeCodeInfo, TypeClassInfo, TypeClass } from "./operators";
56
+ export type { Operands, OpInfo, ScopeInfo, ContextPropertyInfo } from "./constants";
67
57
 
68
58
  ///////////////////////////////////////////////////////////////////////////
69
59
  // Types
@@ -81,6 +71,7 @@ export type {
81
71
  DecodedRule,
82
72
  DecodedParam,
83
73
  Issue,
74
+ IssueCode,
84
75
  IssueSeverity,
85
76
  IssueCategory,
86
77
  Context,
package/src/operators.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { Op, TypeCode } from "./constants";
1
+ import { hexToBytes } from "./bytes";
2
+ import { DescriptorFormat as DF, formatCode, Op, PolicyFormat as PF, TypeCode, operandsOf } from "./constants";
2
3
  import { CallciumError } from "./errors";
3
4
 
5
+ import type { Hex } from "./types";
6
+
4
7
  /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
5
8
  export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
6
9
  const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
@@ -28,7 +31,7 @@ export function isLeftAligned(typeCode: number): boolean {
28
31
  }
29
32
 
30
33
  /**
31
- * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
34
+ * Canonicalize a raw 256-bit calldata word to its ABI value for the declared type.
32
35
  * A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
33
36
  * unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
34
37
  * comparison use the canonical ABI value rather than the raw bytes.
@@ -103,14 +106,14 @@ function compareTyped(a: bigint, b: bigint, typeCode: number): number {
103
106
  /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
104
107
  function isIn(value: bigint, data: Uint8Array): boolean {
105
108
  const count = data.length / 32;
106
- let lo = 0;
107
- let hi = count - 1;
108
- while (lo <= hi) {
109
- const mid = (lo + hi) >>> 1;
109
+ let low = 0;
110
+ let high = count - 1;
111
+ while (low <= high) {
112
+ const mid = (low + high) >>> 1;
110
113
  const elem = toBigInt(data, mid * 32);
111
114
  if (value === elem) return true;
112
- if (value < elem) hi = mid - 1;
113
- else lo = mid + 1;
115
+ if (value < elem) high = mid - 1;
116
+ else low = mid + 1;
114
117
  }
115
118
  return false;
116
119
  }
@@ -128,7 +131,7 @@ function isIn(value: bigint, data: Uint8Array): boolean {
128
131
  export function applyOperator(
129
132
  opCode: number,
130
133
  value: bigint,
131
- valueLength: number,
134
+ valueLength: bigint,
132
135
  operandData: Uint8Array,
133
136
  typeCode: number,
134
137
  ): boolean {
@@ -138,7 +141,7 @@ export function applyOperator(
138
141
  // value and the opcode maps onto its EQ/GT/LT/GTE/LTE/BETWEEN twin. Counts are non-negative,
139
142
  // so the comparison is always unsigned — force an unsigned type code regardless of the target.
140
143
  if (isLengthOp(base)) {
141
- value = BigInt(valueLength);
144
+ value = valueLength;
142
145
  typeCode = TypeCode.UINT_MAX;
143
146
  base = lengthToValueOp(base);
144
147
  }
@@ -196,9 +199,142 @@ export function applyOperator(
196
199
  }
197
200
 
198
201
  default:
199
- throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
202
+ throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PF.RULE_OPCODE_SIZE)}`);
200
203
  }
201
204
 
202
205
  const negate = (opCode & Op.NOT) !== 0;
203
206
  return negate ? !result : result;
204
207
  }
208
+
209
+ /**
210
+ * Check whether an operator's data payload has the correct length.
211
+ * @param opBase - Base operator code with the NOT flag stripped.
212
+ * @param dataLength - Byte length of the operator's data payload.
213
+ * @returns True if the data length is valid for the given operator.
214
+ */
215
+ export function isValidOperatorData(opBase: number, dataLength: number): boolean {
216
+ const operands = operandsOf(opBase);
217
+ if (operands === "single") return dataLength === 32;
218
+ if (operands === "range") return dataLength === 64;
219
+ if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
220
+ return false;
221
+ }
222
+
223
+ /**
224
+ * Read an operator's encoding as bytes: one opcode byte followed by its data payload.
225
+ * @param operator - 0x-prefixed hex string holding a single operator.
226
+ * @returns The operator's bytes, opcode first.
227
+ * @throws {CallciumError} If the hex is malformed or carries no opcode byte.
228
+ */
229
+ export function toOperatorBytes(operator: Hex): Uint8Array {
230
+ const bytes = hexToBytes(operator);
231
+ if (bytes.length < 1) {
232
+ throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
233
+ }
234
+ return bytes;
235
+ }
236
+
237
+ ///////////////////////////////////////////////////////////////////////////
238
+ // Type code classification
239
+ ///////////////////////////////////////////////////////////////////////////
240
+
241
+ /** Structural category for a descriptor type code. */
242
+ export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
243
+
244
+ /** Structural classification without label. */
245
+ export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
246
+
247
+ /** Display metadata for a descriptor type code. */
248
+ export type TypeCodeInfo = TypeClassInfo & { label: string };
249
+
250
+ /** Throw an UNKNOWN_TYPE_CODE error. */
251
+ function unknownTypeCode(code: number): never {
252
+ throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DF.TYPECODE_SIZE)}`);
253
+ }
254
+
255
+ // Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
256
+ const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
257
+ const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
258
+ const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
259
+ const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
260
+ const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
261
+
262
+ /**
263
+ * Classify a type code into its structural category and dynamism.
264
+ * Lightweight variant of `lookupTypeCode` that skips label computation.
265
+ * @param code - A single-byte descriptor type code.
266
+ * @returns Type class and whether the type is ABI-dynamic.
267
+ * @throws {CallciumError} If the code is not a recognised type code.
268
+ */
269
+ export function classifyTypeCode(code: number): TypeClassInfo {
270
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
271
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
272
+ if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
273
+ if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
274
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
275
+ if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
276
+ if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
277
+ if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
278
+ if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
279
+ if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
280
+ if (code === TypeCode.TUPLE) return TUPLE;
281
+ unknownTypeCode(code);
282
+ }
283
+
284
+ /**
285
+ * Determine whether a type code names an elementary type.
286
+ * @param code - A single-byte descriptor type code.
287
+ * @returns True for an elementary code; false for a composite or unassigned one.
288
+ */
289
+ export function isElementary(code: number): boolean {
290
+ try {
291
+ // An undefined code is reported by a throw, and names no elementary type either way.
292
+ return classifyTypeCode(code).typeClass === "elementary";
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Determine whether a type code names a value an operator can read: a scalar word or a declared
300
+ * length. Tuples, static arrays, and undefined codes address nothing.
301
+ * @param code - A single-byte descriptor type code.
302
+ * @returns True when an operator can be applied to a target of this type.
303
+ */
304
+ export function isAddressableTarget(code: number): boolean {
305
+ let info: TypeClassInfo;
306
+ try {
307
+ // An undefined code is reported by a throw, and addresses nothing either way.
308
+ info = classifyTypeCode(code);
309
+ } catch {
310
+ return false;
311
+ }
312
+ return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
313
+ }
314
+
315
+ /** Compute the ABI type label for a type code. */
316
+ function typeCodeLabel(code: number): string {
317
+ if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
318
+ if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
319
+ if (code === TypeCode.ADDRESS) return "address";
320
+ if (code === TypeCode.BOOL) return "bool";
321
+ if (code === TypeCode.FUNCTION) return "function";
322
+ if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
323
+ return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
324
+ if (code === TypeCode.BYTES) return "bytes";
325
+ if (code === TypeCode.STRING) return "string";
326
+ if (code === TypeCode.STATIC_ARRAY) return "T[k]";
327
+ if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
328
+ if (code === TypeCode.TUPLE) return "tuple";
329
+ return `0x${code.toString(16).padStart(2, "0")}`;
330
+ }
331
+
332
+ /**
333
+ * Map a raw type code byte to its ABI type label, structural category, and dynamism.
334
+ * @param code - A single-byte descriptor type code.
335
+ * @returns Label, type class, and whether the type is ABI-dynamic.
336
+ * @throws {CallciumError} If the code is not a recognised type code.
337
+ */
338
+ export function lookupTypeCode(code: number): TypeCodeInfo {
339
+ return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
340
+ }
package/src/path.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
2
+ import { buildCodeMap, formatCode, 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((entry) => [entry.code, { label: entry.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)
40
+ throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step ${formatCode(code, PF.PATH_STEP_SIZE)}`);
41
+ return info;
42
+ }
43
+
44
+ ///////////////////////////////////////////////////////////////////////////
45
+ // Path steps
46
+ ///////////////////////////////////////////////////////////////////////////
47
+
48
+ /**
49
+ * Encode a sequence of uint16 path steps as a big-endian hex string.
50
+ * @throws {CallciumError} When a step is not an integer the field holds. A wrapped step would
51
+ * address a different argument.
52
+ */
53
+ export function encodePath(steps: readonly number[]): Hex {
54
+ const buffer = new Uint8Array(steps.length * PF.PATH_STEP_SIZE);
55
+ for (let i = 0; i < steps.length; i++) {
56
+ const step = steps[i]!;
57
+ if (!Number.isInteger(step) || step < 0 || step > PF.PATH_STEP_MAX) {
58
+ throw new CallciumError("PATH_STEP_OVERFLOW", `Path step ${step} is outside the path step field`);
59
+ }
60
+ writeBE16(buffer, i * PF.PATH_STEP_SIZE, step);
61
+ }
62
+ return bytesToHex(buffer);
63
+ }
64
+
65
+ /**
66
+ * Parse a BE16-encoded hex path into an array of step values.
67
+ * @param path - 0x-prefixed hex string containing BE16-encoded path steps.
68
+ * @returns Array of numeric step values.
69
+ */
70
+ export function parsePathSteps(path: Hex): number[] {
71
+ const bytes = hexToBytes(path);
72
+ if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
73
+ throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
74
+ }
75
+ const steps: number[] = [];
76
+ for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
77
+ steps.push(readU16(bytes, offset));
78
+ }
79
+ return steps;
80
+ }