@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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, PolicyFormat as PF, Quantifier, TypeCode, isQuantifier } from "./constants";
|
|
3
|
+
import { CallciumError } from "./errors";
|
|
4
|
+
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////
|
|
6
|
+
// Public types
|
|
7
|
+
///////////////////////////////////////////////////////////////////////////
|
|
8
|
+
|
|
9
|
+
/** Structural type information extracted from a descriptor node. */
|
|
10
|
+
export type TypeInfo = {
|
|
11
|
+
typeCode: number;
|
|
12
|
+
isDynamic: boolean;
|
|
13
|
+
staticSize: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
///////////////////////////////////////////////////////////////////////////
|
|
17
|
+
// Internal types
|
|
18
|
+
///////////////////////////////////////////////////////////////////////////
|
|
19
|
+
|
|
20
|
+
/** A hop entry of a compiled chain: [delta, index, meta]. */
|
|
21
|
+
type Hop = [number, number, number];
|
|
22
|
+
|
|
23
|
+
/** State threaded through the compilation of a path into a hint block. */
|
|
24
|
+
type HintWalk = {
|
|
25
|
+
/** Hops of the chain currently being compiled. */
|
|
26
|
+
chain: Hop[];
|
|
27
|
+
/** Hops of the main chain, captured when a quantifier closes it. */
|
|
28
|
+
mainHops: Hop[];
|
|
29
|
+
/** Byte offset of the next node relative to the chain's current base. */
|
|
30
|
+
delta: number;
|
|
31
|
+
/** Frame prefix fields, captured when a quantifier closes the main chain. */
|
|
32
|
+
frame: { arrayDelta: number; count: number; meta: number };
|
|
33
|
+
/** Header kind the path resolves to. */
|
|
34
|
+
kind: number;
|
|
35
|
+
/** Set while the chain already ends inside the current node, which then takes no entry hop. */
|
|
36
|
+
entered: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
///////////////////////////////////////////////////////////////////////////
|
|
40
|
+
// Internal helpers
|
|
41
|
+
///////////////////////////////////////////////////////////////////////////
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 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.
|
|
47
|
+
*/
|
|
48
|
+
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;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return the byte offset of the `fieldIndex`-th field inside a tuple node.
|
|
56
|
+
* Starts at the first field and skips `fieldIndex` nodes using `nodeLength`.
|
|
57
|
+
*/
|
|
58
|
+
function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: number): number {
|
|
59
|
+
let cursor = tupleOffset + DF.TUPLE_HEADER_SIZE;
|
|
60
|
+
for (let i = 0; i < fieldIndex; i++) {
|
|
61
|
+
cursor += nodeLength(desc, cursor);
|
|
62
|
+
}
|
|
63
|
+
return cursor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
///////////////////////////////////////////////////////////////////////////
|
|
67
|
+
// Public interface
|
|
68
|
+
///////////////////////////////////////////////////////////////////////////
|
|
69
|
+
|
|
70
|
+
/** Return paramCount from header byte 1. */
|
|
71
|
+
function paramCount(desc: Uint8Array): number {
|
|
72
|
+
return desc[1]!;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Return byte offset of the N-th top-level param (0-indexed). */
|
|
76
|
+
function paramOffset(desc: Uint8Array, index: number): number {
|
|
77
|
+
const count = desc[1]!;
|
|
78
|
+
if (index >= count) {
|
|
79
|
+
throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count}).`);
|
|
80
|
+
}
|
|
81
|
+
let cursor = DF.HEADER_SIZE;
|
|
82
|
+
for (let i = 0; i < index; i++) {
|
|
83
|
+
cursor += nodeLength(desc, cursor);
|
|
84
|
+
}
|
|
85
|
+
return cursor;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Inspect the type node at a given byte offset.
|
|
90
|
+
* @param desc - Raw descriptor bytes.
|
|
91
|
+
* @param offset - Byte position of the node.
|
|
92
|
+
* @returns typeCode, isDynamic, and staticSize in bytes.
|
|
93
|
+
*/
|
|
94
|
+
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 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the type at a calldata path (array of step indices).
|
|
108
|
+
*
|
|
109
|
+
* The first step selects a top-level param. Each subsequent step descends
|
|
110
|
+
* into the current node: for tuples it selects a field, for arrays it
|
|
111
|
+
* advances to the element descriptor.
|
|
112
|
+
*
|
|
113
|
+
* @param desc - Raw descriptor bytes.
|
|
114
|
+
* @param steps - Path steps, length >= 1.
|
|
115
|
+
* @returns TypeInfo for the node at the resolved path.
|
|
116
|
+
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
117
|
+
*/
|
|
118
|
+
function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
|
|
119
|
+
return walkPath(desc, steps).typeInfo;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the type at the path and the declared length of the static array a
|
|
124
|
+
* quantifier step descends into (zero when the path holds no quantifier step
|
|
125
|
+
* over a static array).
|
|
126
|
+
* @param desc - Raw descriptor bytes.
|
|
127
|
+
* @param steps - Path steps, length >= 1.
|
|
128
|
+
* @returns TypeInfo at the resolved path and the quantified static array length.
|
|
129
|
+
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent into elementary type.
|
|
130
|
+
*/
|
|
131
|
+
function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
|
|
132
|
+
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
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let cursor = paramOffset(desc, paramIndex);
|
|
146
|
+
let quantifiedStaticLength = 0;
|
|
147
|
+
|
|
148
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
149
|
+
const typeCode = desc[cursor]!;
|
|
150
|
+
const step = steps[stepIndex]!;
|
|
151
|
+
|
|
152
|
+
if (typeCode === TypeCode.TUPLE) {
|
|
153
|
+
const fields = tupleFieldCount(desc, cursor);
|
|
154
|
+
if (step >= fields) {
|
|
155
|
+
throw new CallciumError(
|
|
156
|
+
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
157
|
+
`Tuple field index ${step} out of range (tuple has ${fields} fields).`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
cursor = tupleFieldOffset(desc, cursor, step);
|
|
161
|
+
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
162
|
+
// Quantifier sentinels descend into the element type; only concrete
|
|
163
|
+
// indices are bounds-checked against the declared array length.
|
|
164
|
+
if (typeCode === TypeCode.STATIC_ARRAY) {
|
|
165
|
+
if (isQuantifier(step)) {
|
|
166
|
+
quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
167
|
+
} else {
|
|
168
|
+
const length = staticArrayLength(desc, cursor);
|
|
169
|
+
if (step >= length) {
|
|
170
|
+
throw new CallciumError(
|
|
171
|
+
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
172
|
+
`Array index ${step} out of range (length=${length}).`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
cursor = arrayElementOffset(cursor);
|
|
178
|
+
} else {
|
|
179
|
+
throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { typeInfo: inspect(desc, cursor), quantifiedStaticLength };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Return tuple field count at a tuple node offset.
|
|
188
|
+
* @param desc - Raw descriptor bytes.
|
|
189
|
+
* @param offset - Byte position of the tuple node.
|
|
190
|
+
*/
|
|
191
|
+
function tupleFieldCount(desc: Uint8Array, offset: number): number {
|
|
192
|
+
return readU16(desc, offset + 4);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return static array length at a static array node offset.
|
|
197
|
+
* @param desc - Raw descriptor bytes.
|
|
198
|
+
* @param offset - Byte position of the static array node.
|
|
199
|
+
*/
|
|
200
|
+
function staticArrayLength(desc: Uint8Array, offset: number): number {
|
|
201
|
+
const elemOffset = arrayElementOffset(offset);
|
|
202
|
+
const elemLen = nodeLength(desc, elemOffset);
|
|
203
|
+
const lengthOffset = elemOffset + elemLen;
|
|
204
|
+
return readU16(desc, lengthOffset);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Return the byte offset of the array element descriptor.
|
|
209
|
+
* @param offset - Byte position of the array node (static or dynamic).
|
|
210
|
+
*/
|
|
211
|
+
function arrayElementOffset(offset: number): number {
|
|
212
|
+
return offset + DF.ARRAY_HEADER_SIZE;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Build the error for a path step that does not resolve to a hop-chain target. */
|
|
216
|
+
function uncompilablePath(stepIndex: number): CallciumError {
|
|
217
|
+
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
|
|
221
|
+
function headSpan(desc: Uint8Array, offset: number, count: number): { span: number; nodeOffset: number } {
|
|
222
|
+
let span = 0;
|
|
223
|
+
let nodeOffset = offset;
|
|
224
|
+
for (let i = 0; i < count; i++) {
|
|
225
|
+
const node = inspect(desc, nodeOffset);
|
|
226
|
+
// An indirected node occupies a single offset word.
|
|
227
|
+
span += node.isDynamic ? 32 : node.staticSize;
|
|
228
|
+
nodeOffset += nodeLength(desc, nodeOffset);
|
|
229
|
+
}
|
|
230
|
+
return { span, nodeOffset };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Return the meta word describing the array node at `arrayOffset`, and whether its elements are dynamic. */
|
|
234
|
+
function arrayMeta(desc: Uint8Array, arrayOffset: number, code: number): { meta: number; elemIsDynamic: boolean } {
|
|
235
|
+
const element = inspect(desc, arrayElementOffset(arrayOffset));
|
|
236
|
+
// A dynamic element occupies one offset word within the element region.
|
|
237
|
+
let meta = element.isDynamic ? 1 : element.staticSize >> 5;
|
|
238
|
+
if (element.isDynamic) meta |= PF.HINT_META_ELEM_DYNAMIC;
|
|
239
|
+
if (code === TypeCode.DYNAMIC_ARRAY) meta |= PF.HINT_META_DYNAMIC_ARRAY;
|
|
240
|
+
return { meta, elemIsDynamic: element.isDynamic };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Append the hop entering an indirected node's payload and rebase the offset accumulator. */
|
|
244
|
+
function enterNode(walk: HintWalk, needed: boolean): void {
|
|
245
|
+
if (!needed) return;
|
|
246
|
+
walk.chain.push([walk.delta, PF.HINT_NO_INDEX, 0]);
|
|
247
|
+
walk.delta = 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Apply one path step to `walk` and return the descriptor offset the step reaches. */
|
|
251
|
+
function walkStep(desc: Uint8Array, walk: HintWalk, descOffset: number, step: number, stepIndex: number): number {
|
|
252
|
+
const node = inspect(desc, descOffset);
|
|
253
|
+
|
|
254
|
+
// A quantifier reaches an array alone, every other code reading its value as an index.
|
|
255
|
+
if (node.typeCode === TypeCode.TUPLE) {
|
|
256
|
+
if (step >= tupleFieldCount(desc, descOffset)) throw uncompilablePath(stepIndex);
|
|
257
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
258
|
+
|
|
259
|
+
const field = headSpan(desc, descOffset + DF.TUPLE_HEADER_SIZE, step);
|
|
260
|
+
walk.delta += field.span;
|
|
261
|
+
walk.entered = false;
|
|
262
|
+
return field.nodeOffset;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (node.typeCode !== TypeCode.STATIC_ARRAY && node.typeCode !== TypeCode.DYNAMIC_ARRAY) {
|
|
266
|
+
throw uncompilablePath(stepIndex);
|
|
267
|
+
}
|
|
268
|
+
if (node.typeCode === TypeCode.STATIC_ARRAY && !isQuantifier(step) && step >= staticArrayLength(desc, descOffset)) {
|
|
269
|
+
throw uncompilablePath(stepIndex);
|
|
270
|
+
}
|
|
271
|
+
const { meta, elemIsDynamic } = arrayMeta(desc, descOffset, node.typeCode);
|
|
272
|
+
|
|
273
|
+
if (isQuantifier(step)) {
|
|
274
|
+
if (walk.kind !== PF.HINT_KIND_NONE) throw uncompilablePath(stepIndex);
|
|
275
|
+
enterNode(walk, node.isDynamic && !walk.entered);
|
|
276
|
+
|
|
277
|
+
const count = node.typeCode === TypeCode.DYNAMIC_ARRAY ? 0 : staticArrayLength(desc, descOffset);
|
|
278
|
+
walk.frame = { arrayDelta: walk.delta, count, meta };
|
|
279
|
+
walk.kind = step === Quantifier.ALL ? PF.HINT_KIND_ALL : PF.HINT_KIND_ANY;
|
|
280
|
+
|
|
281
|
+
walk.mainHops = walk.chain;
|
|
282
|
+
walk.chain = [];
|
|
283
|
+
walk.delta = 0;
|
|
284
|
+
} else if (node.isDynamic) {
|
|
285
|
+
// An indirected array holds its elements behind an offset word.
|
|
286
|
+
enterNode(walk, !walk.entered);
|
|
287
|
+
walk.chain.push([0, step, meta]);
|
|
288
|
+
walk.delta = 0;
|
|
289
|
+
} else {
|
|
290
|
+
// A static array of static elements is inline, so the index folds into the accumulator.
|
|
291
|
+
walk.delta += step * (meta & PF.HINT_META_STRIDE_MASK) * 32;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
walk.entered = elemIsDynamic;
|
|
295
|
+
return arrayElementOffset(descOffset);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Serialize hop entries at `offset` and return the offset past them. */
|
|
299
|
+
function writeHops(out: Uint8Array, offset: number, hops: Hop[]): number {
|
|
300
|
+
for (const [delta, index, meta] of hops) {
|
|
301
|
+
writeBE32(out, offset, delta);
|
|
302
|
+
writeBE16(out, offset + PF.HINT_HOP_INDEX_OFFSET, index);
|
|
303
|
+
writeBE16(out, offset + PF.HINT_HOP_META_OFFSET, meta);
|
|
304
|
+
offset += PF.HINT_HOP_SIZE;
|
|
305
|
+
}
|
|
306
|
+
return offset;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Serialize a completed walk and its target block into the wire hint block. */
|
|
310
|
+
function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth: number): Uint8Array {
|
|
311
|
+
let suffixHops: Hop[] = [];
|
|
312
|
+
if (walk.kind === PF.HINT_KIND_NONE) walk.mainHops = walk.chain;
|
|
313
|
+
else suffixHops = walk.chain;
|
|
314
|
+
|
|
315
|
+
const mainHopCount = walk.mainHops.length;
|
|
316
|
+
const suffixHopCount = suffixHops.length;
|
|
317
|
+
if (mainHopCount > PF.HINT_HOP_COUNT_MASK || suffixHopCount > PF.HINT_HOP_COUNT_MASK) {
|
|
318
|
+
throw uncompilablePath(depth);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const quantified = walk.kind !== PF.HINT_KIND_NONE;
|
|
322
|
+
const frameSize = quantified
|
|
323
|
+
? PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE + suffixHopCount * PF.HINT_HOP_SIZE
|
|
324
|
+
: 0;
|
|
325
|
+
const out = new Uint8Array(PF.HINT_HEADER_SIZE + mainHopCount * PF.HINT_HOP_SIZE + frameSize + PF.HINT_TARGET_SIZE);
|
|
326
|
+
|
|
327
|
+
let offset = 0;
|
|
328
|
+
out[offset] = (walk.kind << PF.HINT_KIND_SHIFT) | mainHopCount;
|
|
329
|
+
offset = writeHops(out, offset + PF.HINT_HEADER_SIZE, walk.mainHops);
|
|
330
|
+
|
|
331
|
+
if (quantified) {
|
|
332
|
+
writeBE32(out, offset, walk.frame.arrayDelta);
|
|
333
|
+
writeBE16(out, offset + PF.HINT_FRAME_COUNT_OFFSET, walk.frame.count);
|
|
334
|
+
writeBE16(out, offset + PF.HINT_FRAME_META_OFFSET, walk.frame.meta);
|
|
335
|
+
out[offset + PF.HINT_FRAME_PREFIX_SIZE] = suffixHopCount;
|
|
336
|
+
offset = writeHops(out, offset + PF.HINT_FRAME_PREFIX_SIZE + PF.HINT_HEADER_SIZE, suffixHops);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
writeBE32(out, offset, walk.delta);
|
|
340
|
+
writeBE16(out, offset + PF.HINT_TARGET_META_OFFSET, targetMeta);
|
|
341
|
+
out[offset + PF.HINT_TARGET_TYPECODE_OFFSET] = typeCode;
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Compile a calldata rule path into its wire hint block.
|
|
347
|
+
*
|
|
348
|
+
* @param desc - Raw descriptor bytes.
|
|
349
|
+
* @param steps - Path steps, length >= 1.
|
|
350
|
+
* @returns The hint block bytes.
|
|
351
|
+
* @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over
|
|
352
|
+
* a non-array node, or repeats a quantifier.
|
|
353
|
+
*/
|
|
354
|
+
function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
|
|
355
|
+
if (steps.length === 0) {
|
|
356
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const argIndex = steps[0]!;
|
|
360
|
+
if (argIndex >= paramCount(desc)) throw uncompilablePath(0);
|
|
361
|
+
|
|
362
|
+
// The argument's head slot sits past the slots of every preceding parameter.
|
|
363
|
+
const arg = headSpan(desc, DF.HEADER_SIZE, argIndex);
|
|
364
|
+
const walk: HintWalk = {
|
|
365
|
+
chain: [],
|
|
366
|
+
mainHops: [],
|
|
367
|
+
delta: arg.span,
|
|
368
|
+
frame: { arrayDelta: 0, count: 0, meta: 0 },
|
|
369
|
+
kind: PF.HINT_KIND_NONE,
|
|
370
|
+
entered: false,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
let descOffset = arg.nodeOffset;
|
|
374
|
+
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
375
|
+
descOffset = walkStep(desc, walk, descOffset, steps[stepIndex]!, stepIndex);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const target = inspect(desc, descOffset);
|
|
379
|
+
enterNode(walk, target.isDynamic && !walk.entered);
|
|
380
|
+
|
|
381
|
+
const targetMeta = target.typeCode === TypeCode.DYNAMIC_ARRAY ? arrayMeta(desc, descOffset, target.typeCode).meta : 0;
|
|
382
|
+
|
|
383
|
+
return encodeHint(walk, targetMeta, target.typeCode, steps.length);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Inspect and navigate raw descriptor bytes. */
|
|
387
|
+
export const Descriptor = {
|
|
388
|
+
paramCount,
|
|
389
|
+
paramOffset,
|
|
390
|
+
inspect,
|
|
391
|
+
typeAt,
|
|
392
|
+
tupleFieldOffset,
|
|
393
|
+
tupleFieldCount,
|
|
394
|
+
staticArrayLength,
|
|
395
|
+
nodeLength,
|
|
396
|
+
arrayElementOffset,
|
|
397
|
+
walkPath,
|
|
398
|
+
compileHint,
|
|
399
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable error code for structural decoding and validation failures.
|
|
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`.
|
|
6
|
+
*/
|
|
7
|
+
export type CallciumErrorCode =
|
|
8
|
+
// Policy and descriptor framing.
|
|
9
|
+
| "UNSUPPORTED_VERSION"
|
|
10
|
+
| "MALFORMED_HEADER"
|
|
11
|
+
| "UNEXPECTED_END"
|
|
12
|
+
| "TRAILING_BYTES"
|
|
13
|
+
// Groups and rules.
|
|
14
|
+
| "EMPTY_POLICY"
|
|
15
|
+
| "EMPTY_GROUP"
|
|
16
|
+
| "GROUP_TOO_SMALL"
|
|
17
|
+
| "GROUP_SIZE_MISMATCH"
|
|
18
|
+
| "GROUP_OVERFLOW"
|
|
19
|
+
| "RULE_TOO_SMALL"
|
|
20
|
+
| "RULE_SIZE_MISMATCH"
|
|
21
|
+
| "RULE_FIELD_OUT_OF_BOUNDS"
|
|
22
|
+
| "RULE_OVERFLOW"
|
|
23
|
+
// Paths.
|
|
24
|
+
| "EMPTY_PATH"
|
|
25
|
+
| "MALFORMED_PATH"
|
|
26
|
+
| "PATH_TOO_DEEP"
|
|
27
|
+
| "PARAM_INDEX_OUT_OF_BOUNDS"
|
|
28
|
+
| "TUPLE_FIELD_OUT_OF_BOUNDS"
|
|
29
|
+
| "STATIC_ARRAY_INDEX_OUT_OF_BOUNDS"
|
|
30
|
+
| "NOT_COMPOSITE"
|
|
31
|
+
| "UNCOMPILABLE_PATH"
|
|
32
|
+
| "QUANTIFIER_ON_NON_ARRAY"
|
|
33
|
+
| "NESTED_QUANTIFIER"
|
|
34
|
+
// Scope and context.
|
|
35
|
+
| "INVALID_SCOPE"
|
|
36
|
+
| "INVALID_CONTEXT_PATH"
|
|
37
|
+
| "UNKNOWN_CONTEXT_PROPERTY"
|
|
38
|
+
// Operators and hints.
|
|
39
|
+
| "UNKNOWN_OPERATOR"
|
|
40
|
+
| "INVALID_OPERATOR_BYTES"
|
|
41
|
+
| "OPERATOR_TARGET_MISMATCH"
|
|
42
|
+
| "UNSORTED_IN_SET"
|
|
43
|
+
| "MALFORMED_HINT"
|
|
44
|
+
// Constraint building.
|
|
45
|
+
| "NO_CONSTRAINT_OPERATORS"
|
|
46
|
+
| "DUPLICATE_PATH_IN_GROUP"
|
|
47
|
+
| "EMPTY_SET"
|
|
48
|
+
| "SET_TOO_LARGE"
|
|
49
|
+
| "INVALID_RANGE"
|
|
50
|
+
// Descriptor nodes.
|
|
51
|
+
| "UNKNOWN_TYPE_CODE"
|
|
52
|
+
| "NODE_LENGTH_TOO_SMALL"
|
|
53
|
+
| "NODE_LENGTH_TOO_LARGE"
|
|
54
|
+
| "NODE_OVERFLOW"
|
|
55
|
+
| "NESTING_TOO_DEEP"
|
|
56
|
+
| "INVALID_ARRAY_LENGTH"
|
|
57
|
+
| "INVALID_TUPLE_FIELD_COUNT"
|
|
58
|
+
| "STATIC_WORDS_TOO_LARGE"
|
|
59
|
+
| "PARAM_COUNT_MISMATCH"
|
|
60
|
+
| "TOO_MANY_PARAMS"
|
|
61
|
+
// Type strings and signatures.
|
|
62
|
+
| "MALFORMED_TYPE_STRING"
|
|
63
|
+
| "UNKNOWN_TYPE"
|
|
64
|
+
| "MALFORMED_SIGNATURE"
|
|
65
|
+
| "SIGNATURE_CONTAINS_WHITESPACE"
|
|
66
|
+
| "INVALID_FUNCTION_NAME"
|
|
67
|
+
// Encoder field widths.
|
|
68
|
+
| "GROUP_COUNT_OVERFLOW"
|
|
69
|
+
| "RULE_COUNT_OVERFLOW"
|
|
70
|
+
| "RULE_SIZE_OVERFLOW"
|
|
71
|
+
| "DESC_LENGTH_OVERFLOW"
|
|
72
|
+
// Inputs and validation.
|
|
73
|
+
| "UNKNOWN_QUANTIFIER"
|
|
74
|
+
| "INVALID_HEX"
|
|
75
|
+
| "VALIDATION_ERROR";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when a policy or descriptor blob is structurally malformed.
|
|
79
|
+
* The optional `offset` indicates the byte position in the source blob where the error was detected.
|
|
80
|
+
*/
|
|
81
|
+
export class CallciumError extends Error {
|
|
82
|
+
public readonly code: CallciumErrorCode;
|
|
83
|
+
/** Byte position in the source blob where the error was detected, if applicable. */
|
|
84
|
+
public readonly offset?: number;
|
|
85
|
+
|
|
86
|
+
constructor(code: CallciumErrorCode, message: string, offset?: number) {
|
|
87
|
+
const prefix = offset !== undefined ? `[offset ${offset}] ` : "";
|
|
88
|
+
super(`${prefix}${message}`);
|
|
89
|
+
this.name = "CallciumError";
|
|
90
|
+
this.code = code;
|
|
91
|
+
Object.setPrototypeOf(this, CallciumError.prototype);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Thrown by `PolicyBuilder.build` when validation reports any issue.
|
|
97
|
+
*
|
|
98
|
+
* Carries every issue the validator found. The `Error.message` is the first issue's
|
|
99
|
+
* message — a diagnostic summary, not a presentation contract. Consumers rendering
|
|
100
|
+
* issues to users should iterate `issues` directly.
|
|
101
|
+
*/
|
|
102
|
+
export class ValidationError extends CallciumError {
|
|
103
|
+
public readonly issues: import("./types").Issue[];
|
|
104
|
+
|
|
105
|
+
constructor(issues: import("./types").Issue[]) {
|
|
106
|
+
super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
|
|
107
|
+
this.name = "ValidationError";
|
|
108
|
+
this.issues = issues;
|
|
109
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement.
|
|
115
|
+
* Distinct from `ValidationError`, which signals static issues in the policy itself.
|
|
116
|
+
*
|
|
117
|
+
* Carries the full list of structured violations (one per failed group). The
|
|
118
|
+
* `Error.message` is a minimal non-lossy diagnostic summary built from the
|
|
119
|
+
* first violation's structured fields — not a presentation contract. Consumers
|
|
120
|
+
* rendering violations to users should iterate `violations` directly.
|
|
121
|
+
*/
|
|
122
|
+
export class PolicyViolationError extends Error {
|
|
123
|
+
public readonly violations: import("./types").Violation[];
|
|
124
|
+
|
|
125
|
+
constructor(violations: import("./types").Violation[]) {
|
|
126
|
+
super(formatDiagnostic(violations[0]));
|
|
127
|
+
this.name = "PolicyViolationError";
|
|
128
|
+
this.violations = violations;
|
|
129
|
+
Object.setPrototypeOf(this, PolicyViolationError.prototype);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Build a stack-trace-friendly diagnostic line from a violation's code and coordinates. */
|
|
134
|
+
function formatDiagnostic(violation: import("./types").Violation | undefined): string {
|
|
135
|
+
if (!violation) return "Policy violation";
|
|
136
|
+
const location = "group" in violation ? ` at group ${violation.group} rule ${violation.rule}` : "";
|
|
137
|
+
return `Policy violation: ${violation.code}${location}`;
|
|
138
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Builder pipeline
|
|
3
|
+
///////////////////////////////////////////////////////////////////////////
|
|
4
|
+
|
|
5
|
+
export { PolicyBuilder } from "./policy-builder";
|
|
6
|
+
export { PolicyCoder, parsePathSteps } from "./policy-coder";
|
|
7
|
+
export { isOpAllowed, PolicyValidator } from "./policy-validator";
|
|
8
|
+
export {
|
|
9
|
+
arg,
|
|
10
|
+
msgSender,
|
|
11
|
+
msgValue,
|
|
12
|
+
blockTimestamp,
|
|
13
|
+
blockNumber,
|
|
14
|
+
chainId,
|
|
15
|
+
txOrigin,
|
|
16
|
+
baseFee,
|
|
17
|
+
gasPrice,
|
|
18
|
+
} from "./constraint";
|
|
19
|
+
export type { ConstraintBuilder, ScalarValue } from "./constraint";
|
|
20
|
+
|
|
21
|
+
///////////////////////////////////////////////////////////////////////////
|
|
22
|
+
// Enforcement
|
|
23
|
+
///////////////////////////////////////////////////////////////////////////
|
|
24
|
+
|
|
25
|
+
export { PolicyEnforcer } from "./policy-enforcer";
|
|
26
|
+
|
|
27
|
+
///////////////////////////////////////////////////////////////////////////
|
|
28
|
+
// Utilities
|
|
29
|
+
///////////////////////////////////////////////////////////////////////////
|
|
30
|
+
|
|
31
|
+
export { toAddress, hexToBytes, bytesToHex } from "./bytes";
|
|
32
|
+
export { Descriptor } from "./descriptor";
|
|
33
|
+
export { DescriptorCoder } from "./descriptor-coder";
|
|
34
|
+
export type { TypeInfo } from "./descriptor";
|
|
35
|
+
|
|
36
|
+
///////////////////////////////////////////////////////////////////////////
|
|
37
|
+
// Constants
|
|
38
|
+
///////////////////////////////////////////////////////////////////////////
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
Op,
|
|
42
|
+
TypeCode,
|
|
43
|
+
PolicyFormat,
|
|
44
|
+
Quantifier,
|
|
45
|
+
Scope,
|
|
46
|
+
ContextProperty,
|
|
47
|
+
Limits,
|
|
48
|
+
lookupOp,
|
|
49
|
+
lookupScope,
|
|
50
|
+
lookupContextProperty,
|
|
51
|
+
lookupQuantifier,
|
|
52
|
+
lookupTypeCode,
|
|
53
|
+
isQuantifier,
|
|
54
|
+
} from "./constants";
|
|
55
|
+
export { isLengthOp } from "./operators";
|
|
56
|
+
export type {
|
|
57
|
+
Operands,
|
|
58
|
+
OpInfo,
|
|
59
|
+
ScopeInfo,
|
|
60
|
+
ContextPropertyInfo,
|
|
61
|
+
QuantifierInfo,
|
|
62
|
+
TypeCodeInfo,
|
|
63
|
+
TypeClassInfo,
|
|
64
|
+
TypeClass,
|
|
65
|
+
} from "./constants";
|
|
66
|
+
|
|
67
|
+
///////////////////////////////////////////////////////////////////////////
|
|
68
|
+
// Types
|
|
69
|
+
///////////////////////////////////////////////////////////////////////////
|
|
70
|
+
|
|
71
|
+
export type {
|
|
72
|
+
Hex,
|
|
73
|
+
Address,
|
|
74
|
+
Span,
|
|
75
|
+
Field,
|
|
76
|
+
PolicyData,
|
|
77
|
+
Constraint,
|
|
78
|
+
DecodedPolicy,
|
|
79
|
+
DecodedGroup,
|
|
80
|
+
DecodedRule,
|
|
81
|
+
DecodedParam,
|
|
82
|
+
Issue,
|
|
83
|
+
IssueSeverity,
|
|
84
|
+
IssueCategory,
|
|
85
|
+
Context,
|
|
86
|
+
EnforceResult,
|
|
87
|
+
Violation,
|
|
88
|
+
ViolationCode,
|
|
89
|
+
NavigationViolationCode,
|
|
90
|
+
MissingSelectorViolation,
|
|
91
|
+
SelectorMismatchViolation,
|
|
92
|
+
MissingContextViolation,
|
|
93
|
+
ValueMismatchViolation,
|
|
94
|
+
CalldataNavigationViolation,
|
|
95
|
+
NonCanonicalValueViolation,
|
|
96
|
+
QuantifierLimitExceededViolation,
|
|
97
|
+
QuantifierEmptyArrayViolation,
|
|
98
|
+
} from "./types";
|
|
99
|
+
|
|
100
|
+
///////////////////////////////////////////////////////////////////////////
|
|
101
|
+
// Errors
|
|
102
|
+
///////////////////////////////////////////////////////////////////////////
|
|
103
|
+
|
|
104
|
+
export { CallciumError, PolicyViolationError, ValidationError } from "./errors";
|
|
105
|
+
export type { CallciumErrorCode } from "./errors";
|