@callcium/sdk 0.5.0 → 0.7.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/dist/index.d.ts +136 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +871 -1411
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bytes.ts +100 -0
- package/src/calldata-reader.ts +38 -0
- package/src/constants.ts +395 -0
- package/src/constraint.ts +313 -0
- package/src/descriptor-coder.ts +461 -0
- package/src/descriptor.ts +399 -0
- package/src/errors.ts +138 -0
- package/src/index.ts +105 -0
- package/src/operators.ts +204 -0
- package/src/policy-builder.ts +250 -0
- package/src/policy-coder.ts +668 -0
- package/src/policy-enforcer.ts +600 -0
- package/src/policy-validator.ts +1112 -0
- package/src/signature.ts +97 -0
- package/src/type-desc.ts +205 -0
- package/src/types.ts +280 -0
- package/src/validation-issue.ts +569 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callcium/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "TypeScript implementation of the Callcium policy engine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abi",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"directory": "packages/sdk"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
26
27
|
],
|
|
27
28
|
"type": "module",
|
|
28
29
|
"sideEffects": false,
|
package/src/bytes.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CallciumError } from "./errors";
|
|
2
|
+
|
|
3
|
+
import type { Address, Hex } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Regex that matches a valid hex body (even number of hex chars). */
|
|
6
|
+
const HEX_BODY_RE = /^[0-9a-fA-F]*$/;
|
|
7
|
+
|
|
8
|
+
// Nibble value per ASCII code, -1 for every character that is not a hex digit.
|
|
9
|
+
const HEX_DIGITS = "0123456789abcdef";
|
|
10
|
+
const NIBBLE = new Int8Array(128).fill(-1);
|
|
11
|
+
for (let value = 0; value < 16; value++) {
|
|
12
|
+
NIBBLE[HEX_DIGITS.charCodeAt(value)] = value;
|
|
13
|
+
NIBBLE[HEX_DIGITS.toUpperCase().charCodeAt(value)] = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Convert a hex string to a byte array, stripping the 0x prefix if present. */
|
|
17
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
18
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
19
|
+
if (clean.length % 2 !== 0) {
|
|
20
|
+
throw new CallciumError("INVALID_HEX", "Odd-length hex string");
|
|
21
|
+
}
|
|
22
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
23
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
24
|
+
const high = NIBBLE[clean.charCodeAt(i * 2)] ?? -1;
|
|
25
|
+
const low = NIBBLE[clean.charCodeAt(i * 2 + 1)] ?? -1;
|
|
26
|
+
if (high < 0 || low < 0) {
|
|
27
|
+
throw new CallciumError("INVALID_HEX", "Invalid hex characters");
|
|
28
|
+
}
|
|
29
|
+
bytes[i] = (high << 4) | low;
|
|
30
|
+
}
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pre-computed hex lookup avoids per-byte toString + padStart in the hot path.
|
|
35
|
+
const HEX_LUT = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
36
|
+
|
|
37
|
+
/** Convert a byte array to a 0x-prefixed lowercase hex string. */
|
|
38
|
+
export function bytesToHex(bytes: Uint8Array): Hex {
|
|
39
|
+
let body = "";
|
|
40
|
+
for (let i = 0; i < bytes.length; i++) body += HEX_LUT[bytes[i]!];
|
|
41
|
+
return `0x${body}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Extract a subarray and return it as a 0x-prefixed hex string. */
|
|
45
|
+
export function toHex(data: Uint8Array, start: number, end: number): Hex {
|
|
46
|
+
return bytesToHex(data.subarray(start, end));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read a big-endian uint16 from a byte array at the given offset. */
|
|
50
|
+
export function readU16(data: Uint8Array, offset: number): number {
|
|
51
|
+
return (data[offset]! << 8) | data[offset + 1]!;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read a big-endian uint24 from a byte array at the given offset. */
|
|
55
|
+
export function readU24(data: Uint8Array, offset: number): number {
|
|
56
|
+
return (data[offset]! << 16) | (data[offset + 1]! << 8) | data[offset + 2]!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read a big-endian uint32 from a byte array at the given offset. */
|
|
60
|
+
export function readU32(data: Uint8Array, offset: number): number {
|
|
61
|
+
return ((data[offset]! << 24) | (data[offset + 1]! << 16) | (data[offset + 2]! << 8) | data[offset + 3]!) >>> 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Write a 16-bit big-endian value into a Uint8Array at offset. */
|
|
65
|
+
export function writeBE16(buffer: Uint8Array, offset: number, value: number): void {
|
|
66
|
+
buffer[offset] = (value >>> 8) & 0xff;
|
|
67
|
+
buffer[offset + 1] = value & 0xff;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Write a 24-bit big-endian value into a Uint8Array at offset. */
|
|
71
|
+
export function writeBE24(buffer: Uint8Array, offset: number, value: number): void {
|
|
72
|
+
buffer[offset] = (value >>> 16) & 0xff;
|
|
73
|
+
buffer[offset + 1] = (value >>> 8) & 0xff;
|
|
74
|
+
buffer[offset + 2] = value & 0xff;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Write a 32-bit big-endian value into a Uint8Array at offset. */
|
|
78
|
+
export function writeBE32(buffer: Uint8Array, offset: number, value: number): void {
|
|
79
|
+
buffer[offset] = (value >>> 24) & 0xff;
|
|
80
|
+
buffer[offset + 1] = (value >>> 16) & 0xff;
|
|
81
|
+
buffer[offset + 2] = (value >>> 8) & 0xff;
|
|
82
|
+
buffer[offset + 3] = value & 0xff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Format a bigint as a zero-padded 32-byte hex string. */
|
|
86
|
+
export function bigintToHex(value: bigint): Hex {
|
|
87
|
+
return `0x${value.toString(16).padStart(64, "0")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Validate and brand a hex string as an address. Throws if the runtime length is not exactly 20 bytes. */
|
|
91
|
+
export function toAddress(hex: string): Address {
|
|
92
|
+
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
93
|
+
if (body.length !== 40) {
|
|
94
|
+
throw new CallciumError("INVALID_HEX", `Invalid address length: expected 40 hex chars, got ${body.length}`);
|
|
95
|
+
}
|
|
96
|
+
if (!HEX_BODY_RE.test(body)) {
|
|
97
|
+
throw new CallciumError("INVALID_HEX", "Invalid hex characters in address");
|
|
98
|
+
}
|
|
99
|
+
return `0x${body}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NavigationViolationCode } from "./types";
|
|
2
|
+
|
|
3
|
+
///////////////////////////////////////////////////////////////////////////
|
|
4
|
+
// Result types
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////
|
|
6
|
+
|
|
7
|
+
/** Result of a calldata read: success with data, or failure with a navigation violation code. */
|
|
8
|
+
export type ReadResult<T> = T | { code: NavigationViolationCode };
|
|
9
|
+
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Calldata reading primitives
|
|
12
|
+
///////////////////////////////////////////////////////////////////////////
|
|
13
|
+
|
|
14
|
+
/** Read 32 bytes at offset, bounds-checked. */
|
|
15
|
+
export function loadWord(callData: Uint8Array, offset: number): ReadResult<Uint8Array> {
|
|
16
|
+
if (offset < 0 || offset + 32 > callData.length) {
|
|
17
|
+
return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
18
|
+
}
|
|
19
|
+
return callData.subarray(offset, offset + 32);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read an ABI pointer (offset or length) from a 32-byte slot.
|
|
24
|
+
*
|
|
25
|
+
* A number cannot hold a 256-bit word, so the high 28 bytes must be zero for the narrowing to be
|
|
26
|
+
* lossless; a wider value is rejected as out of bounds, which is the verdict a bounds comparison
|
|
27
|
+
* over the full word reaches for any offset that far past the end of calldata.
|
|
28
|
+
*/
|
|
29
|
+
export function readPointer(callData: Uint8Array, head: number): ReadResult<number> {
|
|
30
|
+
const word = loadWord(callData, head);
|
|
31
|
+
if (word instanceof Uint8Array) {
|
|
32
|
+
for (let i = 0; i < 28; i++) {
|
|
33
|
+
if (word[i] !== 0) return { code: "CALLDATA_OUT_OF_BOUNDS" };
|
|
34
|
+
}
|
|
35
|
+
return ((word[28]! << 24) | (word[29]! << 16) | (word[30]! << 8) | word[31]!) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
return word; // propagate CALLDATA_OUT_OF_BOUNDS
|
|
38
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { CallciumError } from "./errors";
|
|
2
|
+
|
|
3
|
+
///////////////////////////////////////////////////////////////////////////
|
|
4
|
+
// Descriptor format
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////
|
|
6
|
+
|
|
7
|
+
/** Binary layout constants for the Callcium descriptor format. */
|
|
8
|
+
export const DescriptorFormat = {
|
|
9
|
+
VERSION: 0x02,
|
|
10
|
+
HEADER_SIZE: 2,
|
|
11
|
+
TYPECODE_SIZE: 1,
|
|
12
|
+
COMPOSITE_META_SIZE: 3,
|
|
13
|
+
TUPLE_HEADER_SIZE: 6,
|
|
14
|
+
ARRAY_HEADER_SIZE: 4,
|
|
15
|
+
ARRAY_LENGTH_SIZE: 2,
|
|
16
|
+
TUPLE_FIELDCOUNT_SIZE: 2,
|
|
17
|
+
META_STATIC_WORDS_SHIFT: 12,
|
|
18
|
+
META_NODE_LENGTH_MASK: 0x0fff,
|
|
19
|
+
MAX_NODE_LENGTH: 0x0fff,
|
|
20
|
+
MAX_STATIC_ARRAY_LENGTH: 4095,
|
|
21
|
+
MAX_TUPLE_FIELDS: 4089,
|
|
22
|
+
MAX_STATIC_WORDS: 4095,
|
|
23
|
+
MAX_PARAMS: 255,
|
|
24
|
+
MAX_NESTING_DEPTH: 64,
|
|
25
|
+
} as const satisfies Record<string, number>;
|
|
26
|
+
|
|
27
|
+
///////////////////////////////////////////////////////////////////////////
|
|
28
|
+
// Policy format
|
|
29
|
+
///////////////////////////////////////////////////////////////////////////
|
|
30
|
+
|
|
31
|
+
/** Binary layout constants for the Callcium policy format. */
|
|
32
|
+
export const PolicyFormat = {
|
|
33
|
+
VERSION: 0x02,
|
|
34
|
+
VERSION_MASK: 0x0f,
|
|
35
|
+
FLAG_NO_SELECTOR: 0x10,
|
|
36
|
+
RESERVED_MASK: 0xe0,
|
|
37
|
+
HEADER_SIZE: 1,
|
|
38
|
+
SELECTOR_OFFSET: 1,
|
|
39
|
+
SELECTOR_SIZE: 4,
|
|
40
|
+
DESC_LENGTH_OFFSET: 5,
|
|
41
|
+
DESC_LENGTH_SIZE: 2,
|
|
42
|
+
DESC_OFFSET: 7,
|
|
43
|
+
GROUP_COUNT_SIZE: 1,
|
|
44
|
+
GROUP_RULECOUNT_SIZE: 2,
|
|
45
|
+
GROUP_SIZE_SIZE: 4,
|
|
46
|
+
GROUP_HEADER_SIZE: 6,
|
|
47
|
+
RULE_SIZE_SIZE: 2,
|
|
48
|
+
RULE_SCOPE_OFFSET: 2,
|
|
49
|
+
RULE_DEPTH_OFFSET: 3,
|
|
50
|
+
RULE_PATH_OFFSET: 4,
|
|
51
|
+
PATH_STEP_SIZE: 2,
|
|
52
|
+
RULE_OPCODE_SIZE: 1,
|
|
53
|
+
RULE_DATALENGTH_SIZE: 2,
|
|
54
|
+
RULE_FIXED_OVERHEAD: 7,
|
|
55
|
+
RULE_MIN_SIZE: 9,
|
|
56
|
+
HINT_HEADER_SIZE: 1,
|
|
57
|
+
HINT_HOP_SIZE: 8,
|
|
58
|
+
HINT_HOP_INDEX_OFFSET: 4,
|
|
59
|
+
HINT_HOP_META_OFFSET: 6,
|
|
60
|
+
HINT_FRAME_PREFIX_SIZE: 8,
|
|
61
|
+
HINT_FRAME_COUNT_OFFSET: 4,
|
|
62
|
+
HINT_FRAME_META_OFFSET: 6,
|
|
63
|
+
HINT_TARGET_SIZE: 7,
|
|
64
|
+
HINT_TARGET_META_OFFSET: 4,
|
|
65
|
+
HINT_TARGET_TYPECODE_OFFSET: 6,
|
|
66
|
+
HINT_KIND_NONE: 0x0,
|
|
67
|
+
HINT_KIND_ALL: 0x1,
|
|
68
|
+
HINT_KIND_ANY: 0x2,
|
|
69
|
+
HINT_KIND_MAX: 0x2,
|
|
70
|
+
HINT_KIND_SHIFT: 6,
|
|
71
|
+
HINT_HOP_COUNT_MASK: 0x3f,
|
|
72
|
+
HINT_SUFFIX_RESERVED_MASK: 0xc0,
|
|
73
|
+
HINT_NO_INDEX: 0xffff,
|
|
74
|
+
HINT_INDEX_RESERVED: 0xfffe,
|
|
75
|
+
HINT_META_ELEM_DYNAMIC: 0x8000,
|
|
76
|
+
HINT_META_DYNAMIC_ARRAY: 0x4000,
|
|
77
|
+
HINT_META_RESERVED_MASK: 0x3000,
|
|
78
|
+
HINT_META_STRIDE_MASK: 0x0fff,
|
|
79
|
+
} as const satisfies Record<string, number>;
|
|
80
|
+
|
|
81
|
+
///////////////////////////////////////////////////////////////////////////
|
|
82
|
+
// Table-derived helpers
|
|
83
|
+
///////////////////////////////////////////////////////////////////////////
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract a `{ KEY: code }` map from a table with `key` and `code` fields.
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
type CodeMap<T extends readonly { readonly key: string; readonly code: number }[]> = {
|
|
90
|
+
readonly [E in T[number] as E["key"]]: E["code"];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** 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> {
|
|
95
|
+
// 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>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
///////////////////////////////////////////////////////////////////////////
|
|
100
|
+
// Scope codes
|
|
101
|
+
///////////////////////////////////////////////////////////////////////////
|
|
102
|
+
|
|
103
|
+
const SCOPE_TABLE = [
|
|
104
|
+
{ key: "CONTEXT", code: 0x00, label: "context" },
|
|
105
|
+
{ key: "CALLDATA", code: 0x01, label: "calldata" },
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
108
|
+
/** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
|
|
109
|
+
export const Scope = buildCodeMap(SCOPE_TABLE);
|
|
110
|
+
|
|
111
|
+
/** Display metadata for a scope code. */
|
|
112
|
+
export type ScopeInfo = { label: string };
|
|
113
|
+
|
|
114
|
+
const scopeByCode = new Map<number, ScopeInfo>(SCOPE_TABLE.map((e) => [e.code, { label: e.label }]));
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Map a scope code to its display label.
|
|
118
|
+
* @param code - Scope byte value.
|
|
119
|
+
* @returns Display metadata for the scope.
|
|
120
|
+
* @throws {CallciumError} If the code is not a recognised scope.
|
|
121
|
+
*/
|
|
122
|
+
export function lookupScope(code: number): ScopeInfo {
|
|
123
|
+
const info = scopeByCode.get(code);
|
|
124
|
+
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
125
|
+
return info;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
///////////////////////////////////////////////////////////////////////////
|
|
129
|
+
// Type code ranges
|
|
130
|
+
///////////////////////////////////////////////////////////////////////////
|
|
131
|
+
|
|
132
|
+
/** ABI type code ranges and sentinel values for the descriptor format. */
|
|
133
|
+
export const TypeCode = {
|
|
134
|
+
UINT_MIN: 0x01,
|
|
135
|
+
UINT_MAX: 0x20,
|
|
136
|
+
INT_MIN: 0x21,
|
|
137
|
+
INT_MAX: 0x40,
|
|
138
|
+
ADDRESS: 0x41,
|
|
139
|
+
BOOL: 0x42,
|
|
140
|
+
FUNCTION: 0x43,
|
|
141
|
+
FIXED_BYTES_MIN: 0x50,
|
|
142
|
+
FIXED_BYTES_MAX: 0x6f,
|
|
143
|
+
BYTES: 0x70,
|
|
144
|
+
STRING: 0x71,
|
|
145
|
+
STATIC_ARRAY: 0x80,
|
|
146
|
+
DYNAMIC_ARRAY: 0x81,
|
|
147
|
+
TUPLE: 0x90,
|
|
148
|
+
} as const satisfies Record<string, number>;
|
|
149
|
+
|
|
150
|
+
///////////////////////////////////////////////////////////////////////////
|
|
151
|
+
// Context property IDs
|
|
152
|
+
///////////////////////////////////////////////////////////////////////////
|
|
153
|
+
|
|
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 },
|
|
163
|
+
] as const;
|
|
164
|
+
|
|
165
|
+
/** Well-known context property IDs for context-scope rules. */
|
|
166
|
+
export const ContextProperty = buildCodeMap(CTX_PROP_TABLE);
|
|
167
|
+
|
|
168
|
+
/** Maximum valid context property ID. */
|
|
169
|
+
export const MAX_CONTEXT_PROPERTY_ID = Math.max(...CTX_PROP_TABLE.map((e) => e.code));
|
|
170
|
+
|
|
171
|
+
/** Display metadata for a context property code. */
|
|
172
|
+
export type ContextPropertyInfo = { label: string; contextKey: keyof import("./types").Context; typeCode: number };
|
|
173
|
+
|
|
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 }]),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Map a context property code to its display label and ABI type code.
|
|
192
|
+
* @param code - Context property ID.
|
|
193
|
+
* @returns Display metadata including the ABI type code for the property value.
|
|
194
|
+
* @throws {CallciumError} If the code is not a recognised context property.
|
|
195
|
+
*/
|
|
196
|
+
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}`);
|
|
199
|
+
return info;
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
+
///////////////////////////////////////////////////////////////////////////
|
|
214
|
+
// Operator codes
|
|
215
|
+
///////////////////////////////////////////////////////////////////////////
|
|
216
|
+
|
|
217
|
+
/** Operand count category for operator data validation. */
|
|
218
|
+
export type Operands = "single" | "range" | "variadic";
|
|
219
|
+
|
|
220
|
+
/** @internal */
|
|
221
|
+
const OP_TABLE = [
|
|
222
|
+
{ key: "EQ", code: 0x01, label: "==", operands: "single" },
|
|
223
|
+
{ key: "GT", code: 0x02, label: ">", operands: "single" },
|
|
224
|
+
{ key: "LT", code: 0x03, label: "<", operands: "single" },
|
|
225
|
+
{ key: "GTE", code: 0x04, label: ">=", operands: "single" },
|
|
226
|
+
{ key: "LTE", code: 0x05, label: "<=", operands: "single" },
|
|
227
|
+
{ key: "BETWEEN", code: 0x06, label: "between", operands: "range" },
|
|
228
|
+
{ key: "IN", code: 0x07, label: "in", operands: "variadic" },
|
|
229
|
+
{ key: "BITMASK_ALL", code: 0x10, label: "bitmask all", operands: "single" },
|
|
230
|
+
{ key: "BITMASK_ANY", code: 0x11, label: "bitmask any", operands: "single" },
|
|
231
|
+
{ key: "BITMASK_NONE", code: 0x12, label: "bitmask none", operands: "single" },
|
|
232
|
+
{ key: "LENGTH_EQ", code: 0x20, label: "length ==", operands: "single" },
|
|
233
|
+
{ key: "LENGTH_GT", code: 0x21, label: "length >", operands: "single" },
|
|
234
|
+
{ key: "LENGTH_LT", code: 0x22, label: "length <", operands: "single" },
|
|
235
|
+
{ key: "LENGTH_GTE", code: 0x23, label: "length >=", operands: "single" },
|
|
236
|
+
{ key: "LENGTH_LTE", code: 0x24, label: "length <=", operands: "single" },
|
|
237
|
+
{ key: "LENGTH_BETWEEN", code: 0x25, label: "length between", operands: "range" },
|
|
238
|
+
] as const;
|
|
239
|
+
|
|
240
|
+
/** Callcium policy operator codes. */
|
|
241
|
+
export const Op: CodeMap<typeof OP_TABLE> & { readonly NOT: 0x80 } = { ...buildCodeMap(OP_TABLE), NOT: 0x80 };
|
|
242
|
+
|
|
243
|
+
/** Display metadata for an operator code. */
|
|
244
|
+
export type OpInfo = { label: string; operands: Operands };
|
|
245
|
+
|
|
246
|
+
const opByCode = new Map<number, OpInfo>(OP_TABLE.map((e) => [e.code, { label: e.label, operands: e.operands }]));
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Map an operator code to its display label. Strips the NOT flag automatically.
|
|
250
|
+
* @param code - Operator byte value (may include the NOT flag).
|
|
251
|
+
* @returns Display metadata for the base operator.
|
|
252
|
+
* @throws {CallciumError} If the base code is not a recognised operator.
|
|
253
|
+
*/
|
|
254
|
+
export function lookupOp(code: number): OpInfo {
|
|
255
|
+
const base = code & ~Op.NOT;
|
|
256
|
+
const info = opByCode.get(base);
|
|
257
|
+
if (!info)
|
|
258
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}`);
|
|
259
|
+
return info;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check whether an operator's data payload has the correct length.
|
|
264
|
+
* @param opBase - Base operator code with the NOT flag stripped.
|
|
265
|
+
* @param dataLength - Byte length of the operator's data payload.
|
|
266
|
+
* @returns True if the data length is valid for the given operator.
|
|
267
|
+
*/
|
|
268
|
+
export function isValidOperatorData(opBase: number, dataLength: number): boolean {
|
|
269
|
+
const operands = opByCode.get(opBase)?.operands;
|
|
270
|
+
if (operands === "single") return dataLength === 32;
|
|
271
|
+
if (operands === "range") return dataLength === 64;
|
|
272
|
+
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
///////////////////////////////////////////////////////////////////////////
|
|
277
|
+
// Quantifier steps
|
|
278
|
+
///////////////////////////////////////////////////////////////////////////
|
|
279
|
+
|
|
280
|
+
const QUANTIFIER_TABLE = [
|
|
281
|
+
{ key: "ALL", code: 0xffff, label: "all" },
|
|
282
|
+
{ key: "ANY", code: 0xfffe, label: "any" },
|
|
283
|
+
] as const;
|
|
284
|
+
|
|
285
|
+
/** Reserved path step values that trigger quantified evaluation over array elements. */
|
|
286
|
+
export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
287
|
+
|
|
288
|
+
/** Display metadata for a quantifier step. */
|
|
289
|
+
export type QuantifierInfo = { label: string };
|
|
290
|
+
|
|
291
|
+
const quantifierByCode = new Map<number, QuantifierInfo>(QUANTIFIER_TABLE.map((e) => [e.code, { label: e.label }]));
|
|
292
|
+
|
|
293
|
+
/** Check whether a path step is a quantifier (ALL or ANY). */
|
|
294
|
+
export function isQuantifier(step: number): boolean {
|
|
295
|
+
return step >= Quantifier.ANY;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Map a quantifier path step to its display label.
|
|
300
|
+
* @param code - Quantifier step value.
|
|
301
|
+
* @returns Display metadata for the quantifier.
|
|
302
|
+
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
303
|
+
*/
|
|
304
|
+
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
305
|
+
const info = quantifierByCode.get(code);
|
|
306
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step 0x${code.toString(16)}`);
|
|
307
|
+
return info;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Structural category for a descriptor type code. */
|
|
311
|
+
export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
|
|
312
|
+
|
|
313
|
+
/** Structural classification without label. */
|
|
314
|
+
export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
|
|
315
|
+
|
|
316
|
+
/** Display metadata for a descriptor type code. */
|
|
317
|
+
export type TypeCodeInfo = TypeClassInfo & { label: string };
|
|
318
|
+
|
|
319
|
+
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
320
|
+
function unknownTypeCode(code: number): never {
|
|
321
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code 0x${code.toString(16).padStart(2, "0")}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
|
|
325
|
+
const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
|
|
326
|
+
const ELEMENTARY_DYN = { typeClass: "elementary", isDynamic: true } as const;
|
|
327
|
+
const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
|
|
328
|
+
const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
|
|
329
|
+
const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Classify a type code into its structural category and dynamism.
|
|
333
|
+
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
334
|
+
* @param code - A single-byte descriptor type code.
|
|
335
|
+
* @returns 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 classifyTypeCode(code: number): TypeClassInfo {
|
|
339
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
|
|
340
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
|
|
341
|
+
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
342
|
+
if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
|
|
343
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
344
|
+
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYN;
|
|
345
|
+
if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
|
|
346
|
+
if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
|
|
347
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
|
|
348
|
+
if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
|
|
349
|
+
if (code === TypeCode.TUPLE) return TUPLE;
|
|
350
|
+
unknownTypeCode(code);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
355
|
+
* length. Tuples, static arrays, and undefined codes address nothing.
|
|
356
|
+
* @param code - A single-byte descriptor type code.
|
|
357
|
+
* @returns True when an operator can be applied to a target of this type.
|
|
358
|
+
*/
|
|
359
|
+
export function isAddressableTarget(code: number): boolean {
|
|
360
|
+
let info: TypeClassInfo;
|
|
361
|
+
try {
|
|
362
|
+
// An undefined code is reported by a throw, and addresses nothing either way.
|
|
363
|
+
info = classifyTypeCode(code);
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Compute the ABI type label for a type code. */
|
|
371
|
+
function typeCodeLabel(code: number): string {
|
|
372
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
|
|
373
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
|
|
374
|
+
if (code === TypeCode.ADDRESS) return "address";
|
|
375
|
+
if (code === TypeCode.BOOL) return "bool";
|
|
376
|
+
if (code === TypeCode.FUNCTION) return "function";
|
|
377
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
|
|
378
|
+
return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
|
|
379
|
+
if (code === TypeCode.BYTES) return "bytes";
|
|
380
|
+
if (code === TypeCode.STRING) return "string";
|
|
381
|
+
if (code === TypeCode.STATIC_ARRAY) return "T[k]";
|
|
382
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
|
|
383
|
+
if (code === TypeCode.TUPLE) return "tuple";
|
|
384
|
+
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Map a raw type code byte to its ABI type label, structural category, and dynamism.
|
|
389
|
+
* @param code - A single-byte descriptor type code.
|
|
390
|
+
* @returns Label, type class, and whether the type is ABI-dynamic.
|
|
391
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
392
|
+
*/
|
|
393
|
+
export function lookupTypeCode(code: number): TypeCodeInfo {
|
|
394
|
+
return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
|
|
395
|
+
}
|