@callcium/sdk 0.12.0 → 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/operators.ts CHANGED
@@ -1,8 +1,20 @@
1
- import { hexToBytes } from "./bytes";
2
- import { DescriptorFormat as DF, formatCode, Op, PolicyFormat as PF, TypeCode, operandsOf } from "./constants";
1
+ import { bigintToHex, readU16 } from "./bytes";
2
+ import {
3
+ comparisonOf,
4
+ ContextProperty,
5
+ DescriptorFormat as DF,
6
+ formatCode,
7
+ isComparisonCode,
8
+ isSourceCode,
9
+ Op,
10
+ PolicyFormat as PF,
11
+ Source,
12
+ sourceOf,
13
+ TypeCode,
14
+ } from "./constants";
3
15
  import { CallciumError } from "./errors";
4
16
 
5
- import type { Hex } from "./types";
17
+ import type { DecodedOperator, Hex } from "./types";
6
18
 
7
19
  /** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
8
20
  export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
@@ -15,6 +27,74 @@ export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
15
27
  );
16
28
  }
17
29
 
30
+ /** Type code of the type a length compares as, `uint32`. */
31
+ export const LENGTH_TYPE = 0x04;
32
+
33
+ /** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
34
+ export function isLengthOp(opCode: number): boolean {
35
+ const base = comparisonOf(opCode);
36
+ return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
37
+ }
38
+
39
+ /** Return true when the operator admits a context property operand: EQ, GT, LT, GTE or LTE. */
40
+ export function acceptsContextSource(opBase: number): boolean {
41
+ return opBase >= Op.EQ && opBase <= Op.LTE;
42
+ }
43
+
44
+ /** Return true when the type code has its length encoded in calldata (bytes, string, or dynamic array). */
45
+ export function hasCalldataLength(typeCode: number): boolean {
46
+ return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
47
+ }
48
+
49
+ /**
50
+ * Return the byte width of a value of a static elementary type.
51
+ * @param typeCode - A single-byte descriptor type code.
52
+ * @returns The width in bytes, or undefined when the code names no static elementary type.
53
+ */
54
+ export function operandWidth(typeCode: number): number | undefined {
55
+ if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) return typeCode - TypeCode.UINT_MIN + 1;
56
+ if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) return typeCode - TypeCode.INT_MIN + 1;
57
+ if (typeCode === TypeCode.ADDRESS) return 20;
58
+ if (typeCode === TypeCode.BOOL) return 1;
59
+ if (typeCode === TypeCode.FUNCTION) return 24;
60
+ if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
61
+ return typeCode - TypeCode.FIXED_BYTES_MIN + 1;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ /** Return the type code a context property carries: `address` for msg.sender and tx.origin, `uint256` otherwise. */
67
+ export function contextPropertyType(contextPropertyId: number): number {
68
+ const isAddress = contextPropertyId === ContextProperty.MSG_SENDER || contextPropertyId === ContextProperty.TX_ORIGIN;
69
+ return isAddress ? TypeCode.ADDRESS : TypeCode.UINT_MAX;
70
+ }
71
+
72
+ /**
73
+ * Return the type a comparison's constants take against a target type. A length comparison compares a type with a
74
+ * calldata length as `LENGTH_TYPE`, any other comparison a static elementary type as itself.
75
+ * @param comparison - Comparison bits of the operator code.
76
+ * @param typeCode - The target's type code.
77
+ * @returns The operand type code, or undefined where the comparison does not apply to the target.
78
+ */
79
+ export function operandTypeOf(comparison: number, typeCode: number): number | undefined {
80
+ if (isLengthOp(comparison)) return hasCalldataLength(typeCode) ? LENGTH_TYPE : undefined;
81
+ return operandWidth(typeCode) === undefined ? undefined : typeCode;
82
+ }
83
+
84
+ /**
85
+ * Check whether an operator's data payload has the correct length.
86
+ * @param opBase - Comparison bits of the operator code, a defined comparison.
87
+ * @param dataLength - Byte length of the operator's data payload.
88
+ * @param width - Byte width of one operand, at least one.
89
+ * @returns True if the data length is valid for the given operator.
90
+ */
91
+ export function isValidPayloadSize(opBase: number, dataLength: number, width: number): boolean {
92
+ // Range operators carry a lower and an upper operand; IN carries one or more; every other operator one.
93
+ if (opBase === Op.BETWEEN || opBase === Op.LENGTH_BETWEEN) return dataLength === 2 * width;
94
+ if (opBase === Op.IN) return dataLength > 0 && dataLength % width === 0;
95
+ return dataLength === width;
96
+ }
97
+
18
98
  /** Return true when the type code represents a signed integer (int8 through int256). */
19
99
  export function isSigned(typeCode: number): boolean {
20
100
  return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
@@ -40,7 +120,7 @@ export function isLeftAligned(typeCode: number): boolean {
40
120
  export function canonicalize(value: bigint, typeCode: number): bigint {
41
121
  // Unsigned integers: mask to the low N bits.
42
122
  if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
43
- const bits = BigInt(typeCode * 8);
123
+ const bits = BigInt((typeCode - TypeCode.UINT_MIN + 1) * 8);
44
124
  return bits === 256n ? value : value & ((1n << bits) - 1n);
45
125
  }
46
126
 
@@ -61,208 +141,81 @@ export function canonicalize(value: bigint, typeCode: number): bigint {
61
141
 
62
142
  // Fixed bytes: left-aligned, clear the low (32 - N) padding bytes.
63
143
  if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
64
- const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
65
- if (n === 32) return value;
66
- const padBits = BigInt((32 - n) * 8);
144
+ const width = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
145
+ if (width === 32) return value;
146
+ const padBits = BigInt((32 - width) * 8);
67
147
  return (value >> padBits) << padBits;
68
148
  }
69
149
 
70
150
  return value;
71
151
  }
72
152
 
73
- /** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
74
- export function isLengthOp(opCode: number): boolean {
75
- const base = opCode & ~Op.NOT;
76
- return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
77
- }
78
-
79
- /** Return true when the operator code (with or without NOT flag) is a *_CTX variant. */
80
- export function isContextOp(opCode: number): boolean {
81
- const base = opCode & ~Op.NOT;
82
- return base >= Op.EQ_CTX && base <= Op.LTE_CTX;
83
- }
84
-
85
- /** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
86
- export function isLengthValidType(typeCode: number): boolean {
87
- return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
88
- }
89
-
90
- /** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
91
- function compareTyped(a: bigint, b: bigint, typeCode: number): number {
92
- if (isSigned(typeCode)) {
93
- const sa = BigInt.asIntN(256, a);
94
- const sb = BigInt.asIntN(256, b);
95
- return sa < sb ? -1 : sa > sb ? 1 : 0;
96
- }
97
- return a < b ? -1 : a > b ? 1 : 0;
153
+ /** Read the `width`-byte big-endian member at `offset`. */
154
+ export function readMember(data: Uint8Array, offset: number, width: number): bigint {
155
+ let value = 0n;
156
+ for (let index = 0; index < width; index++) value = (value << 8n) | BigInt(data[offset + index]!);
157
+ return value;
98
158
  }
99
159
 
100
- /** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
101
- function isIn(value: bigint, data: Uint8Array): boolean {
102
- const count = data.length / 32;
103
- let low = 0;
104
- let high = count - 1;
105
- while (low <= high) {
106
- const mid = (low + high) >>> 1;
107
- const elem = toBigInt(data, mid * 32);
108
- if (value === elem) return true;
109
- if (value < elem) high = mid - 1;
110
- else low = mid + 1;
111
- }
112
- return false;
160
+ /**
161
+ * Return the word an operand of a static elementary type loads to: placed at its type's alignment, and sign-extended
162
+ * for a signed integer.
163
+ * @param data - The bytes containing the operand.
164
+ * @param offset - Byte offset of the operand.
165
+ * @param typeCode - Type code of the operand.
166
+ * @returns The loaded 256-bit word.
167
+ * @throws {CallciumError} If the type code names no static elementary type.
168
+ */
169
+ export function loadOperand(data: Uint8Array, offset: number, typeCode: number): bigint {
170
+ const width = operandWidth(typeCode);
171
+ if (width === undefined) throw new CallciumError("MALFORMED_HINT", "Target type code addresses no value");
172
+ const member = readMember(data, offset, width);
173
+ if (isLeftAligned(typeCode)) return member << BigInt(256 - 8 * width);
174
+ if (isSigned(typeCode)) return BigInt.asUintN(256, BigInt.asIntN(8 * width, member));
175
+ return member;
113
176
  }
114
177
 
115
178
  /**
116
- * Apply a single operator against a value and operand data. Handles the NOT flag by inverting the base result.
117
- * @param opCode - Operator code, potentially OR'd with the NOT flag.
118
- * @param value - The 256-bit value to test (ignored for LENGTH_* operators).
119
- * @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
120
- * @param operandData - Raw operand bytes (32 per operand, 64 for range operators); for a context reference operator,
121
- * the referenced property's value as one word.
122
- * @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
123
- * @returns Whether the value satisfies the operator.
179
+ * Read the operator at `opOffset` of a policy, applied to a target of `typeCode`.
180
+ * @param data - A policy blob storage validation accepts.
181
+ * @param opOffset - Byte offset of the operator code.
182
+ * @param typeCode - The target's type code.
183
+ * @returns The operator with its comparison, its polarity and its operands, each constant loaded to its 32-byte word.
184
+ * @throws {CallciumError} If the operator code names no defined comparison or source.
124
185
  */
125
- export function applyOperator(
126
- opCode: number,
127
- value: bigint,
128
- valueLength: bigint,
129
- operandData: Uint8Array,
130
- typeCode: number,
131
- ): boolean {
132
- const base = opCode & ~Op.NOT;
133
- let result: boolean;
134
-
135
- switch (base) {
136
- case Op.EQ:
137
- result = value === toBigInt(operandData, 0);
138
- break;
139
-
140
- case Op.GT:
141
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
142
- break;
143
-
144
- case Op.LT:
145
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
146
- break;
147
-
148
- case Op.GTE:
149
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
150
- break;
151
-
152
- case Op.LTE:
153
- result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
154
- break;
155
-
156
- case Op.BETWEEN: {
157
- const min = toBigInt(operandData, 0);
158
- const max = toBigInt(operandData, 32);
159
- result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
160
- break;
161
- }
162
-
163
- case Op.IN:
164
- result = isIn(value, operandData);
165
- break;
166
-
167
- case Op.BITMASK_ALL: {
168
- const mask = toBigInt(operandData, 0);
169
- result = (value & mask) === mask;
170
- break;
171
- }
172
-
173
- case Op.BITMASK_ANY: {
174
- const mask = toBigInt(operandData, 0);
175
- result = (value & mask) !== 0n;
176
- break;
177
- }
178
-
179
- case Op.BITMASK_NONE: {
180
- const mask = toBigInt(operandData, 0);
181
- result = (value & mask) === 0n;
182
- break;
186
+ export function readOperator(data: Uint8Array, opOffset: number, typeCode: number): DecodedOperator {
187
+ const opCode = data[opOffset]!;
188
+ const comparison = comparisonOf(opCode);
189
+ const source = sourceOf(opCode);
190
+ if (!isComparisonCode(comparison) || !isSourceCode(source)) {
191
+ throw new CallciumError(
192
+ "UNKNOWN_OPERATOR",
193
+ `Unknown operator code ${formatCode(opCode & ~Op.NOT, PF.RULE_OPCODE_SIZE)}`,
194
+ opOffset,
195
+ );
196
+ }
197
+ const negated = (opCode & Op.NOT) !== 0;
198
+ const dataLength = readU16(data, opOffset + PF.RULE_OPCODE_SIZE);
199
+ const dataOffset = opOffset + PF.RULE_OPCODE_SIZE + PF.RULE_DATALENGTH_SIZE;
200
+
201
+ switch (source) {
202
+ case Source.CONTEXT: {
203
+ const contextPropertyId = readU16(data, dataOffset);
204
+ const operandType = contextPropertyType(contextPropertyId);
205
+ return { opCode, comparison, negated, operandType, source, contextPropertyId };
183
206
  }
184
-
185
- case Op.LENGTH_EQ:
186
- result = valueLength === toBigInt(operandData, 0);
187
- break;
188
-
189
- case Op.LENGTH_GT:
190
- result = valueLength > toBigInt(operandData, 0);
191
- break;
192
-
193
- case Op.LENGTH_LT:
194
- result = valueLength < toBigInt(operandData, 0);
195
- break;
196
-
197
- case Op.LENGTH_GTE:
198
- result = valueLength >= toBigInt(operandData, 0);
199
- break;
200
-
201
- case Op.LENGTH_LTE:
202
- result = valueLength <= toBigInt(operandData, 0);
203
- break;
204
-
205
- case Op.LENGTH_BETWEEN: {
206
- const lower = toBigInt(operandData, 0);
207
- const upper = toBigInt(operandData, 32);
208
- result = valueLength >= lower && valueLength <= upper;
209
- break;
207
+ case Source.CONSTANT: {
208
+ const operandType = operandTypeOf(comparison, typeCode)!;
209
+ const width = operandWidth(operandType)!;
210
+ const values: Hex[] = [];
211
+ for (let offset = 0; offset < dataLength; offset += width) {
212
+ values.push(bigintToHex(loadOperand(data, dataOffset + offset, operandType)));
213
+ }
214
+ return { opCode, comparison, negated, operandType, source, values };
210
215
  }
211
-
212
- case Op.EQ_CTX:
213
- result = value === toBigInt(operandData, 0);
214
- break;
215
-
216
- case Op.GT_CTX:
217
- result = value > toBigInt(operandData, 0);
218
- break;
219
-
220
- case Op.LT_CTX:
221
- result = value < toBigInt(operandData, 0);
222
- break;
223
-
224
- case Op.GTE_CTX:
225
- result = value >= toBigInt(operandData, 0);
226
- break;
227
-
228
- case Op.LTE_CTX:
229
- result = value <= toBigInt(operandData, 0);
230
- break;
231
-
232
216
  default:
233
- throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PF.RULE_OPCODE_SIZE)}`);
217
+ return source satisfies never;
234
218
  }
235
-
236
- const negate = (opCode & Op.NOT) !== 0;
237
- return negate ? !result : result;
238
- }
239
-
240
- /**
241
- * Check whether an operator's data payload has the correct length.
242
- * @param opBase - Base operator code with the NOT flag stripped.
243
- * @param dataLength - Byte length of the operator's data payload.
244
- * @returns True if the data length is valid for the given operator.
245
- */
246
- export function isValidOperatorData(opBase: number, dataLength: number): boolean {
247
- const operands = operandsOf(opBase);
248
- if (operands === "single") return dataLength === 32;
249
- if (operands === "range") return dataLength === 64;
250
- if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
251
- return false;
252
- }
253
-
254
- /**
255
- * Read an operator's encoding as bytes: one opcode byte followed by its data payload.
256
- * @param operator - 0x-prefixed hex string holding a single operator.
257
- * @returns The operator's bytes, opcode first.
258
- * @throws {CallciumError} If the hex is malformed or carries no opcode byte.
259
- */
260
- export function toOperatorBytes(operator: Hex): Uint8Array {
261
- const bytes = hexToBytes(operator);
262
- if (bytes.length < 1) {
263
- throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
264
- }
265
- return bytes;
266
219
  }
267
220
 
268
221
  ///////////////////////////////////////////////////////////////////////////
@@ -278,86 +231,32 @@ export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
278
231
  /** Display metadata for a descriptor type code. */
279
232
  export type TypeCodeInfo = TypeClassInfo & { label: string };
280
233
 
281
- /** Throw an UNKNOWN_TYPE_CODE error. */
282
- function unknownTypeCode(code: number): never {
283
- throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DF.TYPECODE_SIZE)}`);
284
- }
285
-
286
- // Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
287
- const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
288
- const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
289
- const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
290
- const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
291
- const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
292
-
293
- /**
294
- * Classify a type code into its structural category and dynamism. A `lookupTypeCode` variant without labels.
295
- * @param code - A single-byte descriptor type code.
296
- * @returns Type class, and whether the code alone implies ABI dynamism (false for tuples and static arrays).
297
- * @throws {CallciumError} If the code is not a recognised type code.
298
- */
299
- export function classifyTypeCode(code: number): TypeClassInfo {
300
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
301
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
302
- if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
303
- if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
304
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
305
- if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
306
- if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
307
- if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
308
- if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
309
- if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
310
- if (code === TypeCode.TUPLE) return TUPLE;
311
- unknownTypeCode(code);
312
- }
313
-
314
- /**
315
- * Determine whether a type code names an elementary type.
316
- * @param code - A single-byte descriptor type code.
317
- * @returns True for an elementary code; false for a composite or unassigned one.
318
- */
319
- export function isElementary(code: number): boolean {
320
- try {
321
- // An undefined code is reported by a throw, and names no elementary type either way.
322
- return classifyTypeCode(code).typeClass === "elementary";
323
- } catch {
324
- return false;
234
+ /** Each defined type code's ABI type label, structural category, and dynamism. */
235
+ function typeCodeEntries(): [number, TypeCodeInfo][] {
236
+ const entries: [number, TypeCodeInfo][] = [];
237
+ const add = (code: number, label: string, typeClass: TypeClass, isDynamic: boolean) =>
238
+ entries.push([code, { label, typeClass, isDynamic }]);
239
+ for (let code = TypeCode.UINT_MIN; code <= TypeCode.UINT_MAX; code++) {
240
+ add(code, `uint${(code - TypeCode.UINT_MIN + 1) * 8}`, "elementary", false);
325
241
  }
326
- }
327
-
328
- /**
329
- * Determine whether a type code names a value an operator can read: a scalar word or a declared length. Tuples, static
330
- * arrays, and undefined codes address nothing.
331
- * @param code - A single-byte descriptor type code.
332
- * @returns True when an operator can be applied to a target of this type.
333
- */
334
- export function isAddressableTarget(code: number): boolean {
335
- let info: TypeClassInfo;
336
- try {
337
- // An undefined code is reported by a throw, and addresses nothing either way.
338
- info = classifyTypeCode(code);
339
- } catch {
340
- return false;
242
+ for (let code = TypeCode.INT_MIN; code <= TypeCode.INT_MAX; code++) {
243
+ add(code, `int${(code - TypeCode.INT_MIN + 1) * 8}`, "elementary", false);
244
+ }
245
+ add(TypeCode.ADDRESS, "address", "elementary", false);
246
+ add(TypeCode.BOOL, "bool", "elementary", false);
247
+ add(TypeCode.FUNCTION, "function", "elementary", false);
248
+ for (let code = TypeCode.FIXED_BYTES_MIN; code <= TypeCode.FIXED_BYTES_MAX; code++) {
249
+ add(code, `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`, "elementary", false);
341
250
  }
342
- return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
251
+ add(TypeCode.BYTES, "bytes", "elementary", true);
252
+ add(TypeCode.STRING, "string", "elementary", true);
253
+ add(TypeCode.STATIC_ARRAY, "T[k]", "staticArray", false);
254
+ add(TypeCode.DYNAMIC_ARRAY, "T[]", "dynamicArray", true);
255
+ add(TypeCode.TUPLE, "tuple", "tuple", false);
256
+ return entries;
343
257
  }
344
258
 
345
- /** Compute the ABI type label for a type code. */
346
- function typeCodeLabel(code: number): string {
347
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
348
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
349
- if (code === TypeCode.ADDRESS) return "address";
350
- if (code === TypeCode.BOOL) return "bool";
351
- if (code === TypeCode.FUNCTION) return "function";
352
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
353
- return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
354
- if (code === TypeCode.BYTES) return "bytes";
355
- if (code === TypeCode.STRING) return "string";
356
- if (code === TypeCode.STATIC_ARRAY) return "T[k]";
357
- if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
358
- if (code === TypeCode.TUPLE) return "tuple";
359
- return `0x${code.toString(16).padStart(2, "0")}`;
360
- }
259
+ const typeCodeByCode: ReadonlyMap<number, TypeCodeInfo> = new Map<number, TypeCodeInfo>(typeCodeEntries());
361
260
 
362
261
  /**
363
262
  * Map a raw type code byte to its ABI type label, structural category, and dynamism.
@@ -366,5 +265,7 @@ function typeCodeLabel(code: number): string {
366
265
  * @throws {CallciumError} If the code is not a recognised type code.
367
266
  */
368
267
  export function lookupTypeCode(code: number): TypeCodeInfo {
369
- return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
268
+ const info = typeCodeByCode.get(code);
269
+ if (!info) throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DF.TYPECODE_SIZE)}`);
270
+ return info;
370
271
  }
package/src/path.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
2
- import { buildCodeMap, formatCode, PolicyFormat as PF } from "./constants";
2
+ import { buildCodeMap, fieldMax, PolicyFormat as PF } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
4
 
5
5
  import type { Hex } from "./types";
@@ -9,51 +9,27 @@ import type { Hex } from "./types";
9
9
  ///////////////////////////////////////////////////////////////////////////
10
10
 
11
11
  const QUANTIFIER_TABLE = [
12
- { key: "ALL", code: 0xffff, label: "all" },
13
- { key: "ANY", code: 0xfffe, label: "any" },
12
+ { key: "ALL", code: 0xffff },
13
+ { key: "ANY", code: 0xfffe },
14
14
  ] as const;
15
15
 
16
- /** Reserved path step values that trigger quantified evaluation over array elements. */
16
+ /** Path steps that quantify over the array before them: `ALL` over every element, `ANY` over some element. */
17
17
  export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
18
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
19
  ///////////////////////////////////////////////////////////////////////////
45
20
  // Path steps
46
21
  ///////////////////////////////////////////////////////////////////////////
47
22
 
48
23
  /**
49
24
  * 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 addresses another argument.
25
+ * @throws {CallciumError} When a step is not an integer the field can represent.
26
+ * A wrapped step addresses another argument.
51
27
  */
52
28
  export function encodePath(steps: readonly number[]): Hex {
53
29
  const buffer = new Uint8Array(steps.length * PF.PATH_STEP_SIZE);
54
30
  for (let i = 0; i < steps.length; i++) {
55
31
  const step = steps[i]!;
56
- if (!Number.isInteger(step) || step < 0 || step > PF.PATH_STEP_MAX) {
32
+ if (!Number.isInteger(step) || step < 0 || step > fieldMax(PF.PATH_STEP_SIZE)) {
57
33
  throw new CallciumError("PATH_STEP_OVERFLOW", `Path step ${step} is outside the path step field`);
58
34
  }
59
35
  writeBE16(buffer, i * PF.PATH_STEP_SIZE, step);