@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/constants.ts CHANGED
@@ -28,7 +28,21 @@ export const DescriptorFormat = {
28
28
  // Policy format
29
29
  ///////////////////////////////////////////////////////////////////////////
30
30
 
31
- /** Binary layout constants for the Callcium policy format. */
31
+ /** Byte width of a rule's data length field. */
32
+ const RULE_DATALENGTH_SIZE = 2;
33
+
34
+ /** Byte width of one path step. */
35
+ const PATH_STEP_SIZE = 2;
36
+
37
+ /** Byte overhead of a rule header, excluding the path and the operator payload. */
38
+ const RULE_FIXED_OVERHEAD = 7;
39
+
40
+ /** Highest value a big-endian field of `byteWidth` bytes holds. */
41
+ export function fieldMax(byteWidth: number): number {
42
+ return 256 ** byteWidth - 1;
43
+ }
44
+
45
+ /** Binary layout constants and normative limits for the Callcium policy format. */
32
46
  export const PolicyFormat = {
33
47
  VERSION: 0x02,
34
48
  VERSION_MASK: 0x0f,
@@ -48,11 +62,12 @@ export const PolicyFormat = {
48
62
  RULE_SCOPE_OFFSET: 2,
49
63
  RULE_DEPTH_OFFSET: 3,
50
64
  RULE_PATH_OFFSET: 4,
51
- PATH_STEP_SIZE: 2,
65
+ PATH_STEP_SIZE,
66
+ PATH_STEP_MAX: fieldMax(PATH_STEP_SIZE),
52
67
  RULE_OPCODE_SIZE: 1,
53
- RULE_DATALENGTH_SIZE: 2,
54
- RULE_FIXED_OVERHEAD: 7,
55
- RULE_MIN_SIZE: 9,
68
+ RULE_DATALENGTH_SIZE,
69
+ RULE_FIXED_OVERHEAD,
70
+ RULE_MIN_SIZE: RULE_FIXED_OVERHEAD + PATH_STEP_SIZE,
56
71
  HINT_HEADER_SIZE: 1,
57
72
  HINT_HOP_SIZE: 8,
58
73
  HINT_HOP_INDEX_OFFSET: 4,
@@ -76,6 +91,10 @@ export const PolicyFormat = {
76
91
  HINT_META_DYNAMIC_ARRAY: 0x4000,
77
92
  HINT_META_RESERVED_MASK: 0x3000,
78
93
  HINT_META_STRIDE_MASK: 0x0fff,
94
+ // Normative limits.
95
+ MAX_PATH_DEPTH: 32,
96
+ MAX_QUANTIFIED_ARRAY_LENGTH: 256,
97
+ MAX_SET_MEMBERS: Math.floor(fieldMax(RULE_DATALENGTH_SIZE) / 32),
79
98
  } as const satisfies Record<string, number>;
80
99
 
81
100
  ///////////////////////////////////////////////////////////////////////////
@@ -90,10 +109,23 @@ type CodeMap<T extends readonly { readonly key: string; readonly code: number }[
90
109
  readonly [E in T[number] as E["key"]]: E["code"];
91
110
  };
92
111
 
112
+ /**
113
+ * Render a wire code as fixed-width hex.
114
+ * @param code - Code value.
115
+ * @param byteWidth - Byte width of the field the code occupies.
116
+ * @returns The code as `0x`-prefixed hex, or its decimal form when no such field holds it.
117
+ */
118
+ export function formatCode(code: number, byteWidth: number): string {
119
+ if (!Number.isInteger(code) || code < 0 || code > fieldMax(byteWidth)) return String(code);
120
+ return `0x${code.toString(16).padStart(byteWidth * 2, "0")}`;
121
+ }
122
+
93
123
  /** Build a plain `{ KEY: code }` object from a table at runtime. */
94
- function buildCodeMap<T extends readonly { readonly key: string; readonly code: number }[]>(table: T): CodeMap<T> {
124
+ export function buildCodeMap<T extends readonly { readonly key: string; readonly code: number }[]>(
125
+ table: T,
126
+ ): CodeMap<T> {
95
127
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- derived from the same const table that defines the type.
96
- return Object.fromEntries(table.map((e) => [e.key, e.code])) as CodeMap<T>;
128
+ return Object.fromEntries(table.map((entry) => [entry.key, entry.code])) as CodeMap<T>;
97
129
  }
98
130
 
99
131
  ///////////////////////////////////////////////////////////////////////////
@@ -111,7 +143,9 @@ export const Scope = buildCodeMap(SCOPE_TABLE);
111
143
  /** Display metadata for a scope code. */
112
144
  export type ScopeInfo = { label: string };
113
145
 
114
- const scopeByCode = new Map<number, ScopeInfo>(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
146
+ const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
147
+ SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]),
148
+ );
115
149
 
116
150
  /**
117
151
  * Map a scope code to its display label.
@@ -151,42 +185,43 @@ export const TypeCode = {
151
185
  // Context property IDs
152
186
  ///////////////////////////////////////////////////////////////////////////
153
187
 
154
- const CTX_PROP_TABLE = [
155
- { key: "MSG_SENDER", code: 0x0000, label: "msg.sender", typeCode: TypeCode.ADDRESS },
156
- { key: "MSG_VALUE", code: 0x0001, label: "msg.value", typeCode: TypeCode.UINT_MAX },
157
- { key: "BLOCK_TIMESTAMP", code: 0x0002, label: "block.timestamp", typeCode: TypeCode.UINT_MAX },
158
- { key: "BLOCK_NUMBER", code: 0x0003, label: "block.number", typeCode: TypeCode.UINT_MAX },
159
- { key: "CHAIN_ID", code: 0x0004, label: "block.chainid", typeCode: TypeCode.UINT_MAX },
160
- { key: "TX_ORIGIN", code: 0x0005, label: "tx.origin", typeCode: TypeCode.ADDRESS },
161
- { key: "BASE_FEE", code: 0x0006, label: "block.basefee", typeCode: TypeCode.UINT_MAX },
162
- { key: "GAS_PRICE", code: 0x0007, label: "tx.gasprice", typeCode: TypeCode.UINT_MAX },
188
+ // oxfmt-ignore
189
+ const CTX_PROPERTY_TABLE = [
190
+ { key: "MSG_SENDER", code: 0x0000, label: "msg.sender", contextKey: "msgSender", typeCode: TypeCode.ADDRESS },
191
+ { key: "MSG_VALUE", code: 0x0001, label: "msg.value", contextKey: "msgValue", typeCode: TypeCode.UINT_MAX },
192
+ { key: "BLOCK_TIMESTAMP", code: 0x0002, label: "block.timestamp", contextKey: "blockTimestamp", typeCode: TypeCode.UINT_MAX },
193
+ { key: "BLOCK_NUMBER", code: 0x0003, label: "block.number", contextKey: "blockNumber", typeCode: TypeCode.UINT_MAX },
194
+ { key: "CHAIN_ID", code: 0x0004, label: "block.chainid", contextKey: "chainId", typeCode: TypeCode.UINT_MAX },
195
+ { key: "TX_ORIGIN", code: 0x0005, label: "tx.origin", contextKey: "txOrigin", typeCode: TypeCode.ADDRESS },
196
+ { key: "BASE_FEE", code: 0x0006, label: "block.basefee", contextKey: "baseFee", typeCode: TypeCode.UINT_MAX },
197
+ { key: "GAS_PRICE", code: 0x0007, label: "tx.gasprice", contextKey: "gasPrice", typeCode: TypeCode.UINT_MAX },
163
198
  ] as const;
164
199
 
165
200
  /** Well-known context property IDs for context-scope rules. */
166
- export const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
201
+ export const ContextProperty = buildCodeMap(CTX_PROPERTY_TABLE);
167
202
 
168
203
  /** Maximum valid context property ID. */
169
- export const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
204
+ export const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROPERTY_TABLE.map((entry) => entry.code));
170
205
 
171
206
  /** Display metadata for a context property code. */
172
207
  export type ContextPropertyInfo = { label: string; contextKey: keyof import("./types").Context; typeCode: number };
173
208
 
174
- /** Maps each context property table key to its camelCase Context key. */
175
- const CTX_KEY_MAP: Record<(typeof CTX_PROP_TABLE)[number]["key"], keyof import("./types").Context> = {
176
- MSG_SENDER: "msgSender",
177
- MSG_VALUE: "msgValue",
178
- BLOCK_TIMESTAMP: "blockTimestamp",
179
- BLOCK_NUMBER: "blockNumber",
180
- CHAIN_ID: "chainId",
181
- TX_ORIGIN: "txOrigin",
182
- BASE_FEE: "baseFee",
183
- GAS_PRICE: "gasPrice",
184
- };
185
-
186
- const ctxPropByCode = new Map<number, ContextPropertyInfo>(
187
- CTX_PROP_TABLE.map((e) => [e.code, { label: e.label, contextKey: CTX_KEY_MAP[e.key], typeCode: e.typeCode }]),
209
+ const ctxPropertyByCode: ReadonlyMap<number, ContextPropertyInfo> = new Map<number, ContextPropertyInfo>(
210
+ CTX_PROPERTY_TABLE.map((entry) => [
211
+ entry.code,
212
+ { label: entry.label, contextKey: entry.contextKey, typeCode: entry.typeCode },
213
+ ]),
188
214
  );
189
215
 
216
+ /**
217
+ * Look up a context property code in the assigned set.
218
+ * @param code - Context property ID.
219
+ * @returns Display metadata, or undefined when the ID names no assigned property.
220
+ */
221
+ export function findContextProperty(code: number): ContextPropertyInfo | undefined {
222
+ return ctxPropertyByCode.get(code);
223
+ }
224
+
190
225
  /**
191
226
  * Map a context property code to its display label and ABI type code.
192
227
  * @param code - Context property ID.
@@ -194,22 +229,16 @@ const ctxPropByCode = new Map<number, ContextPropertyInfo>(
194
229
  * @throws {CallciumError} If the code is not a recognised context property.
195
230
  */
196
231
  export function lookupContextProperty(code: number): ContextPropertyInfo {
197
- const info = ctxPropByCode.get(code);
198
- if (!info) throw new CallciumError("UNKNOWN_CONTEXT_PROPERTY", `Unknown context property ${code}`);
232
+ const info = ctxPropertyByCode.get(code);
233
+ if (!info) {
234
+ throw new CallciumError(
235
+ "UNKNOWN_CONTEXT_PROPERTY",
236
+ `Unknown context property ID ${formatCode(code, PATH_STEP_SIZE)}`,
237
+ );
238
+ }
199
239
  return info;
200
240
  }
201
241
 
202
- ///////////////////////////////////////////////////////////////////////////
203
- // Protocol limits
204
- ///////////////////////////////////////////////////////////////////////////
205
-
206
- /** Protocol-imposed safety limits for path depth and quantifier array size. */
207
- export const Limits = {
208
- MAX_PATH_DEPTH: 32,
209
- MAX_QUANTIFIED_ARRAY_LENGTH: 256,
210
- MAX_SET_MEMBERS: Math.floor((256 ** PolicyFormat.RULE_DATALENGTH_SIZE - 1) / 32),
211
- } as const satisfies Record<string, number>;
212
-
213
242
  ///////////////////////////////////////////////////////////////////////////
214
243
  // Operator codes
215
244
  ///////////////////////////////////////////////////////////////////////////
@@ -244,7 +273,9 @@ export const Op: CodeMap<typeof OP_TABLE> & { readonly NOT: 0x80 } = { ...buildC
244
273
  /** Display metadata for an operator code. */
245
274
  export type OpInfo = { label: string; operands: Operands };
246
275
 
247
- const opByCode = new Map<number, OpInfo>(OP_TABLE.map((e) => [e.code, { label: e.label, operands: e.operands }]));
276
+ const opByCode: ReadonlyMap<number, OpInfo> = new Map<number, OpInfo>(
277
+ OP_TABLE.map((entry) => [entry.code, { label: entry.label, operands: entry.operands }]),
278
+ );
248
279
 
249
280
  /**
250
281
  * Map an operator code to its display label. Strips the NOT flag automatically.
@@ -256,141 +287,14 @@ export function lookupOp(code: number): OpInfo {
256
287
  const base = code & ~Op.NOT;
257
288
  const info = opByCode.get(base);
258
289
  if (!info)
259
- throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
260
- return info;
261
- }
262
-
263
- /**
264
- * Check whether an operator's data payload has the correct length.
265
- * @param opBase - Base operator code with the NOT flag stripped.
266
- * @param dataLength - Byte length of the operator's data payload.
267
- * @returns True if the data length is valid for the given operator.
268
- */
269
- export function isValidOperatorData(opBase: number, dataLength: number): boolean {
270
- const operands = opByCode.get(opBase)?.operands;
271
- if (operands === "single") return dataLength === 32;
272
- if (operands === "range") return dataLength === 64;
273
- if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
274
- return false;
275
- }
276
-
277
- ///////////////////////////////////////////////////////////////////////////
278
- // Quantifier steps
279
- ///////////////////////////////////////////////////////////////////////////
280
-
281
- const QUANTIFIER_TABLE = [
282
- { key: "ALL", code: 0xffff, label: "all" },
283
- { key: "ANY", code: 0xfffe, label: "any" },
284
- ] as const;
285
-
286
- /** Reserved path step values that trigger quantified evaluation over array elements. */
287
- export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
288
-
289
- /** Display metadata for a quantifier step. */
290
- export type QuantifierInfo = { label: string };
291
-
292
- const quantifierByCode = new Map<number, QuantifierInfo>(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
293
-
294
- /** Check whether a path step is a quantifier (ALL or ANY). */
295
- export function isQuantifier(step: number): boolean {
296
- return step >= Quantifier.ANY;
297
- }
298
-
299
- /**
300
- * Map a quantifier path step to its display label.
301
- * @param code - Quantifier step value.
302
- * @returns Display metadata for the quantifier.
303
- * @throws {CallciumError} If the code is not a recognised quantifier.
304
- */
305
- export function lookupQuantifier(code: number): QuantifierInfo {
306
- const info = quantifierByCode.get(code);
307
- if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
290
+ throw new CallciumError(
291
+ "UNKNOWN_OPERATOR",
292
+ `Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`,
293
+ );
308
294
  return info;
309
295
  }
310
296
 
311
- /** Structural category for a descriptor type code. */
312
- export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
313
-
314
- /** Structural classification without label. */
315
- export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
316
-
317
- /** Display metadata for a descriptor type code. */
318
- export type TypeCodeInfo = TypeClassInfo & { label: string };
319
-
320
- /** Throw an UNKNOWN_TYPE_CODE error. */
321
- function unknownTypeCode(code: number): never {
322
- throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
323
- }
324
-
325
- // Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
326
- const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
327
- const ELEMENTARY_DYN = { typeClass: "elementary", isDynamic: true } as const;
328
- const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
329
- const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
330
- const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
331
-
332
- /**
333
- * Classify a type code into its structural category and dynamism.
334
- * Lightweight variant of `lookupTypeCode` that skips label computation.
335
- * @param code - A single-byte descriptor type code.
336
- * @returns Type class and whether the type is ABI-dynamic.
337
- * @throws {CallciumError} If the code is not a recognised type code.
338
- */
339
- export function classifyTypeCode(code: number): TypeClassInfo {
340
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
341
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
342
- if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
343
- if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
344
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
345
- if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
346
- if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
347
- if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
348
- if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
349
- if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
350
- if (code === TypeCode.TUPLE) return TUPLE;
351
- unknownTypeCode(code);
352
- }
353
-
354
- /**
355
- * Determine whether a type code names a value an operator can read: a scalar word or a declared
356
- * length. Tuples, static arrays, and undefined codes address nothing.
357
- * @param code - A single-byte descriptor type code.
358
- * @returns True when an operator can be applied to a target of this type.
359
- */
360
- export function isAddressableTarget(code: number): boolean {
361
- let info: TypeClassInfo;
362
- try {
363
- // An undefined code is reported by a throw, and addresses nothing either way.
364
- info = classifyTypeCode(code);
365
- } catch {
366
- return false;
367
- }
368
- return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
369
- }
370
-
371
- /** Compute the ABI type label for a type code. */
372
- function typeCodeLabel(code: number): string {
373
- if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
374
- if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
375
- if (code === TypeCode.ADDRESS) return "address";
376
- if (code === TypeCode.BOOL) return "bool";
377
- if (code === TypeCode.FUNCTION) return "function";
378
- if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
379
- return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
380
- if (code === TypeCode.BYTES) return "bytes";
381
- if (code === TypeCode.STRING) return "string";
382
- if (code === TypeCode.STATIC_ARRAY) return "T[k]";
383
- if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
384
- if (code === TypeCode.TUPLE) return "tuple";
385
- return `0x${code.toString(16).padStart(2, "0")}`;
386
- }
387
-
388
- /**
389
- * Map a raw type code byte to its ABI type label, structural category, and dynamism.
390
- * @param code - A single-byte descriptor type code.
391
- * @returns Label, type class, and whether the type is ABI-dynamic.
392
- * @throws {CallciumError} If the code is not a recognised type code.
393
- */
394
- export function lookupTypeCode(code: number): TypeCodeInfo {
395
- return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
297
+ /** Operand count category for an operator code, or undefined when unrecognised. */
298
+ export function operandsOf(opBase: number): Operands | undefined {
299
+ return opByCode.get(opBase)?.operands;
396
300
  }