@callcium/sdk 0.11.1 → 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/README.md +2 -2
- package/dist/index.d.ts +683 -622
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2297 -2051
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/bytes.ts +9 -25
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +156 -82
- package/src/constraint.ts +61 -33
- package/src/descriptor-coder.ts +139 -125
- package/src/descriptor.ts +82 -292
- package/src/errors.ts +38 -30
- package/src/hint.ts +237 -0
- package/src/index.ts +30 -12
- package/src/operators.ts +173 -242
- package/src/path.ts +7 -32
- package/src/policy-builder.ts +205 -190
- package/src/policy-coder.ts +1160 -536
- package/src/policy-enforcer.ts +499 -488
- package/src/policy-validator.ts +563 -1045
- package/src/signature.ts +7 -11
- package/src/type-desc.ts +12 -12
- package/src/types.ts +232 -136
- package/src/validation-issue.ts +78 -465
package/src/constants.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { CallciumError } from "./errors";
|
|
|
7
7
|
/** Binary layout constants for the Callcium descriptor format. */
|
|
8
8
|
export const DescriptorFormat = {
|
|
9
9
|
VERSION: 0x02,
|
|
10
|
+
HEADER_PARAMCOUNT_OFFSET: 1,
|
|
10
11
|
HEADER_SIZE: 2,
|
|
11
12
|
TYPECODE_SIZE: 1,
|
|
12
13
|
COMPOSITE_META_SIZE: 3,
|
|
@@ -28,27 +29,23 @@ export const DescriptorFormat = {
|
|
|
28
29
|
// Policy format
|
|
29
30
|
///////////////////////////////////////////////////////////////////////////
|
|
30
31
|
|
|
31
|
-
/**
|
|
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. */
|
|
32
|
+
/** Highest value a big-endian field of `byteWidth` bytes can represent. */
|
|
41
33
|
export function fieldMax(byteWidth: number): number {
|
|
42
34
|
return 256 ** byteWidth - 1;
|
|
43
35
|
}
|
|
44
36
|
|
|
37
|
+
/** Lowest minor version, the minor of a blob that uses no extension. */
|
|
38
|
+
const MINOR = 0;
|
|
39
|
+
|
|
45
40
|
/** Binary layout constants and normative limits for the Callcium policy format. */
|
|
46
41
|
export const PolicyFormat = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
MAJOR: 3,
|
|
43
|
+
MINOR,
|
|
44
|
+
SUPPORTED_MINOR: MINOR,
|
|
45
|
+
MAJOR_SHIFT: 5,
|
|
46
|
+
MINOR_SHIFT: 1,
|
|
47
|
+
MINOR_MASK: 0x0f,
|
|
48
|
+
FLAG_NO_SELECTOR: 0x01,
|
|
52
49
|
SELECTOR_OFFSET: 1,
|
|
53
50
|
SELECTOR_SIZE: 4,
|
|
54
51
|
DESC_LENGTH_OFFSET: 5,
|
|
@@ -56,18 +53,20 @@ export const PolicyFormat = {
|
|
|
56
53
|
DESC_OFFSET: 7,
|
|
57
54
|
GROUP_COUNT_SIZE: 1,
|
|
58
55
|
GROUP_RULECOUNT_SIZE: 2,
|
|
59
|
-
GROUP_SIZE_SIZE: 4,
|
|
60
56
|
GROUP_HEADER_SIZE: 6,
|
|
61
57
|
RULE_SIZE_SIZE: 2,
|
|
62
58
|
RULE_SCOPE_OFFSET: 2,
|
|
59
|
+
RULE_SCOPE_SIZE: 1,
|
|
63
60
|
RULE_DEPTH_OFFSET: 3,
|
|
61
|
+
RULE_DEPTH_SIZE: 1,
|
|
64
62
|
RULE_PATH_OFFSET: 4,
|
|
65
|
-
PATH_STEP_SIZE,
|
|
66
|
-
|
|
63
|
+
PATH_STEP_SIZE: 2,
|
|
64
|
+
PROPERTY_ID_SIZE: 2,
|
|
65
|
+
RULE_QUANTIFIER_OFFSET: 3,
|
|
66
|
+
RULE_QUANTIFIER_SIZE: 1,
|
|
67
|
+
LOOP_DEPTH_OFFSET: 4,
|
|
67
68
|
RULE_OPCODE_SIZE: 1,
|
|
68
|
-
RULE_DATALENGTH_SIZE,
|
|
69
|
-
RULE_FIXED_OVERHEAD,
|
|
70
|
-
RULE_MIN_SIZE: RULE_FIXED_OVERHEAD + PATH_STEP_SIZE,
|
|
69
|
+
RULE_DATALENGTH_SIZE: 2,
|
|
71
70
|
HINT_HEADER_SIZE: 1,
|
|
72
71
|
HINT_HOP_SIZE: 8,
|
|
73
72
|
HINT_HOP_INDEX_OFFSET: 4,
|
|
@@ -78,15 +77,10 @@ export const PolicyFormat = {
|
|
|
78
77
|
HINT_TARGET_SIZE: 7,
|
|
79
78
|
HINT_TARGET_META_OFFSET: 4,
|
|
80
79
|
HINT_TARGET_TYPECODE_OFFSET: 6,
|
|
81
|
-
HINT_KIND_NONE: 0x0,
|
|
82
|
-
HINT_KIND_ALL: 0x1,
|
|
83
|
-
HINT_KIND_ANY: 0x2,
|
|
84
|
-
HINT_KIND_MAX: 0x2,
|
|
85
|
-
HINT_KIND_SHIFT: 6,
|
|
86
80
|
HINT_HOP_COUNT_MASK: 0x3f,
|
|
87
|
-
|
|
81
|
+
HINT_HEADER_RESERVED_MASK: 0xc0,
|
|
88
82
|
HINT_NO_INDEX: 0xffff,
|
|
89
|
-
|
|
83
|
+
PATH_MARKER_BASE: 0xff00,
|
|
90
84
|
HINT_META_ELEM_DYNAMIC: 0x8000,
|
|
91
85
|
HINT_META_DYNAMIC_ARRAY: 0x4000,
|
|
92
86
|
HINT_META_RESERVED_MASK: 0x3000,
|
|
@@ -94,7 +88,6 @@ export const PolicyFormat = {
|
|
|
94
88
|
// Normative limits.
|
|
95
89
|
MAX_PATH_DEPTH: 32,
|
|
96
90
|
MAX_QUANTIFIED_ARRAY_LENGTH: 256,
|
|
97
|
-
MAX_SET_MEMBERS: Math.floor(fieldMax(RULE_DATALENGTH_SIZE) / 32),
|
|
98
91
|
} as const satisfies Record<string, number>;
|
|
99
92
|
|
|
100
93
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -113,7 +106,7 @@ type CodeMap<T extends readonly { readonly key: string; readonly code: number }[
|
|
|
113
106
|
* Render a wire code as fixed-width hex.
|
|
114
107
|
* @param code - Code value.
|
|
115
108
|
* @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
|
|
109
|
+
* @returns The code as `0x`-prefixed hex, or its decimal form when no such field can represent it.
|
|
117
110
|
*/
|
|
118
111
|
export function formatCode(code: number, byteWidth: number): string {
|
|
119
112
|
if (!Number.isInteger(code) || code < 0 || code > fieldMax(byteWidth)) return String(code);
|
|
@@ -128,37 +121,6 @@ export function buildCodeMap<T extends readonly { readonly key: string; readonly
|
|
|
128
121
|
return Object.fromEntries(table.map((entry) => [entry.key, entry.code])) as CodeMap<T>;
|
|
129
122
|
}
|
|
130
123
|
|
|
131
|
-
///////////////////////////////////////////////////////////////////////////
|
|
132
|
-
// Scope codes
|
|
133
|
-
///////////////////////////////////////////////////////////////////////////
|
|
134
|
-
|
|
135
|
-
const SCOPE_TABLE = [
|
|
136
|
-
{ key: "CONTEXT", code: 0x00, label: "context" },
|
|
137
|
-
{ key: "CALLDATA", code: 0x01, label: "calldata" },
|
|
138
|
-
] as const;
|
|
139
|
-
|
|
140
|
-
/** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
|
|
141
|
-
export const Scope = buildCodeMap(SCOPE_TABLE);
|
|
142
|
-
|
|
143
|
-
/** Display metadata for a scope code. */
|
|
144
|
-
export type ScopeInfo = { label: string };
|
|
145
|
-
|
|
146
|
-
const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
|
|
147
|
-
SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Map a scope code to its display label.
|
|
152
|
-
* @param code - Scope byte value.
|
|
153
|
-
* @returns Display metadata for the scope.
|
|
154
|
-
* @throws {CallciumError} If the code is not a recognised scope.
|
|
155
|
-
*/
|
|
156
|
-
export function lookupScope(code: number): ScopeInfo {
|
|
157
|
-
const info = scopeByCode.get(code);
|
|
158
|
-
if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
|
|
159
|
-
return info;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
124
|
///////////////////////////////////////////////////////////////////////////
|
|
163
125
|
// Type code ranges
|
|
164
126
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -233,17 +195,96 @@ export function lookupContextProperty(code: number): ContextPropertyInfo {
|
|
|
233
195
|
if (!info) {
|
|
234
196
|
throw new CallciumError(
|
|
235
197
|
"UNKNOWN_CONTEXT_PROPERTY",
|
|
236
|
-
`Unknown context property ID ${formatCode(code,
|
|
198
|
+
`Unknown context property ID ${formatCode(code, PolicyFormat.PROPERTY_ID_SIZE)}`,
|
|
237
199
|
);
|
|
238
200
|
}
|
|
239
201
|
return info;
|
|
240
202
|
}
|
|
241
203
|
|
|
204
|
+
///////////////////////////////////////////////////////////////////////////
|
|
205
|
+
// Scope codes
|
|
206
|
+
///////////////////////////////////////////////////////////////////////////
|
|
207
|
+
|
|
208
|
+
const SCOPE_TABLE = [
|
|
209
|
+
{ key: "CONTEXT", code: 0x00, label: "context" },
|
|
210
|
+
{ key: "CALLDATA", code: 0x01, label: "calldata" },
|
|
211
|
+
{ key: "LOOP", code: 0x02, label: "loop" },
|
|
212
|
+
] as const;
|
|
213
|
+
|
|
214
|
+
/** Rule scope discriminant: context (EVM environment), calldata (ABI payload) or loop (array elements). */
|
|
215
|
+
export const Scope = buildCodeMap(SCOPE_TABLE);
|
|
216
|
+
|
|
217
|
+
/** A defined scope code. */
|
|
218
|
+
export type ScopeCode = (typeof SCOPE_TABLE)[number]["code"];
|
|
219
|
+
|
|
220
|
+
/** Display metadata for a scope code. */
|
|
221
|
+
export type ScopeInfo = { label: string };
|
|
222
|
+
|
|
223
|
+
const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
|
|
224
|
+
SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/** Check whether a scope byte is a defined scope code. */
|
|
228
|
+
export function isScopeCode(code: number): code is ScopeCode {
|
|
229
|
+
return scopeByCode.has(code);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Map a scope code to its display label.
|
|
234
|
+
* @param code - Scope byte value.
|
|
235
|
+
* @returns Display metadata for the scope.
|
|
236
|
+
* @throws {CallciumError} If the code is not a recognised scope.
|
|
237
|
+
*/
|
|
238
|
+
export function lookupScope(code: number): ScopeInfo {
|
|
239
|
+
const info = scopeByCode.get(code);
|
|
240
|
+
if (!info) throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${code}`);
|
|
241
|
+
return info;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
///////////////////////////////////////////////////////////////////////////
|
|
245
|
+
// Quantifier codes
|
|
246
|
+
///////////////////////////////////////////////////////////////////////////
|
|
247
|
+
|
|
248
|
+
const QUANTIFIER_TABLE = [
|
|
249
|
+
{ key: "ALL", code: 0x01, label: "all" },
|
|
250
|
+
{ key: "ANY", code: 0x02, label: "any" },
|
|
251
|
+
] as const;
|
|
252
|
+
|
|
253
|
+
/** Loop quantifier: the body holds for every element (`ALL`) or for some element (`ANY`). */
|
|
254
|
+
export const LoopQuantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
255
|
+
|
|
256
|
+
/** A defined loop quantifier code. */
|
|
257
|
+
export type QuantifierCode = (typeof QUANTIFIER_TABLE)[number]["code"];
|
|
258
|
+
|
|
259
|
+
/** Display metadata for a loop quantifier code. */
|
|
260
|
+
export type QuantifierInfo = { label: string };
|
|
261
|
+
|
|
262
|
+
const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
|
|
263
|
+
QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
/** Check whether a quantifier byte is a defined quantifier code. */
|
|
267
|
+
export function isQuantifierCode(code: number): code is QuantifierCode {
|
|
268
|
+
return quantifierByCode.has(code);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Map a loop quantifier code to its display label.
|
|
273
|
+
* @param code - Quantifier byte value.
|
|
274
|
+
* @returns Display metadata for the quantifier.
|
|
275
|
+
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
276
|
+
*/
|
|
277
|
+
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
278
|
+
const info = quantifierByCode.get(code);
|
|
279
|
+
if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${code}`);
|
|
280
|
+
return info;
|
|
281
|
+
}
|
|
282
|
+
|
|
242
283
|
///////////////////////////////////////////////////////////////////////////
|
|
243
284
|
// Operator codes
|
|
244
285
|
///////////////////////////////////////////////////////////////////////////
|
|
245
286
|
|
|
246
|
-
/** Operand count category
|
|
287
|
+
/** Operand count category of an operator. */
|
|
247
288
|
export type Operands = "single" | "range" | "variadic";
|
|
248
289
|
|
|
249
290
|
/** @internal */
|
|
@@ -255,20 +296,33 @@ const OP_TABLE = [
|
|
|
255
296
|
{ key: "LTE", code: 0x05, label: "<=", operands: "single" },
|
|
256
297
|
{ key: "BETWEEN", code: 0x06, label: "between", operands: "range" },
|
|
257
298
|
{ key: "IN", code: 0x07, label: "in", operands: "variadic" },
|
|
258
|
-
{ key: "EQ_CTX", code: 0x08, label: "== ctx", operands: "single" },
|
|
259
299
|
{ key: "BITMASK_ALL", code: 0x10, label: "bitmask all", operands: "single" },
|
|
260
300
|
{ key: "BITMASK_ANY", code: 0x11, label: "bitmask any", operands: "single" },
|
|
261
301
|
{ key: "BITMASK_NONE", code: 0x12, label: "bitmask none", operands: "single" },
|
|
262
|
-
{ key: "LENGTH_EQ", code:
|
|
263
|
-
{ key: "LENGTH_GT", code:
|
|
264
|
-
{ key: "LENGTH_LT", code:
|
|
265
|
-
{ key: "LENGTH_GTE", code:
|
|
266
|
-
{ key: "LENGTH_LTE", code:
|
|
267
|
-
{ key: "LENGTH_BETWEEN", code:
|
|
302
|
+
{ key: "LENGTH_EQ", code: 0x18, label: "length ==", operands: "single" },
|
|
303
|
+
{ key: "LENGTH_GT", code: 0x19, label: "length >", operands: "single" },
|
|
304
|
+
{ key: "LENGTH_LT", code: 0x1a, label: "length <", operands: "single" },
|
|
305
|
+
{ key: "LENGTH_GTE", code: 0x1b, label: "length >=", operands: "single" },
|
|
306
|
+
{ key: "LENGTH_LTE", code: 0x1c, label: "length <=", operands: "single" },
|
|
307
|
+
{ key: "LENGTH_BETWEEN", code: 0x1d, label: "length between", operands: "range" },
|
|
268
308
|
] as const;
|
|
269
309
|
|
|
270
|
-
/** Callcium policy operator codes. */
|
|
271
|
-
export const Op
|
|
310
|
+
/** Callcium policy operator codes, with the NOT flag and the operand source fields. */
|
|
311
|
+
export const Op = { ...buildCodeMap(OP_TABLE), NOT: 0x80, SOURCE_SHIFT: 5, COMPARISON_MASK: 0x1f } as const;
|
|
312
|
+
|
|
313
|
+
/** A defined comparison code. */
|
|
314
|
+
export type ComparisonCode = (typeof OP_TABLE)[number]["code"];
|
|
315
|
+
|
|
316
|
+
const SOURCE_TABLE = [
|
|
317
|
+
{ key: "CONSTANT", code: 0, labelSuffix: "" },
|
|
318
|
+
{ key: "CONTEXT", code: 1, labelSuffix: " ctx" },
|
|
319
|
+
] as const;
|
|
320
|
+
|
|
321
|
+
/** Operand source: constant values, or a context property. */
|
|
322
|
+
export const Source = buildCodeMap(SOURCE_TABLE);
|
|
323
|
+
|
|
324
|
+
/** A defined operand source code. */
|
|
325
|
+
export type SourceCode = (typeof SOURCE_TABLE)[number]["code"];
|
|
272
326
|
|
|
273
327
|
/** Display metadata for an operator code. */
|
|
274
328
|
export type OpInfo = { label: string; operands: Operands };
|
|
@@ -277,24 +331,44 @@ const opByCode: ReadonlyMap<number, OpInfo> = new Map<number, OpInfo>(
|
|
|
277
331
|
OP_TABLE.map((entry) => [entry.code, { label: entry.label, operands: entry.operands }]),
|
|
278
332
|
);
|
|
279
333
|
|
|
334
|
+
const labelSuffixBySource: ReadonlyMap<number, string> = new Map<number, string>(
|
|
335
|
+
SOURCE_TABLE.map((entry) => [entry.code, entry.labelSuffix]),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
/** Check whether a comparison is a defined comparison code. */
|
|
339
|
+
export function isComparisonCode(code: number): code is ComparisonCode {
|
|
340
|
+
return opByCode.has(code);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Check whether a source is a defined operand source code. */
|
|
344
|
+
export function isSourceCode(code: number): code is SourceCode {
|
|
345
|
+
return labelSuffixBySource.has(code);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The comparison bits of an operator code. */
|
|
349
|
+
export function comparisonOf(opCode: number): number {
|
|
350
|
+
return opCode & Op.COMPARISON_MASK;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The operand source bits of an operator code. */
|
|
354
|
+
export function sourceOf(opCode: number): number {
|
|
355
|
+
return (opCode & ~Op.NOT) >> Op.SOURCE_SHIFT;
|
|
356
|
+
}
|
|
357
|
+
|
|
280
358
|
/**
|
|
281
|
-
* Map an operator code to its display label. Strips the NOT flag automatically.
|
|
282
|
-
* @param code - Operator byte value (
|
|
359
|
+
* Map an operator code to its display label. Strips the NOT flag automatically. A context source adds ` ctx`.
|
|
360
|
+
* @param code - Operator byte value (can include the NOT flag).
|
|
283
361
|
* @returns Display metadata for the base operator.
|
|
284
362
|
* @throws {CallciumError} If the base code is not a recognised operator.
|
|
285
363
|
*/
|
|
286
364
|
export function lookupOp(code: number): OpInfo {
|
|
287
365
|
const base = code & ~Op.NOT;
|
|
288
|
-
const info = opByCode.get(base);
|
|
289
|
-
|
|
366
|
+
const info = opByCode.get(comparisonOf(base));
|
|
367
|
+
const labelSuffix = labelSuffixBySource.get(sourceOf(base));
|
|
368
|
+
if (!info || labelSuffix === undefined)
|
|
290
369
|
throw new CallciumError(
|
|
291
370
|
"UNKNOWN_OPERATOR",
|
|
292
371
|
`Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`,
|
|
293
372
|
);
|
|
294
|
-
return info;
|
|
295
|
-
}
|
|
296
|
-
|
|
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;
|
|
373
|
+
return { label: `${info.label}${labelSuffix}`, operands: info.operands };
|
|
300
374
|
}
|
package/src/constraint.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { bytesToHex, hexToBytes, toAddress } from "./bytes";
|
|
2
|
-
import {
|
|
2
|
+
import { ContextProperty, lookupContextProperty, Op, Scope, Source } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
4
|
import { encodePath } from "./path";
|
|
5
5
|
|
|
6
|
-
import type { Address, Hex
|
|
6
|
+
import type { Address, Hex } from "./types";
|
|
7
|
+
import type { Constraint } from "./types";
|
|
7
8
|
|
|
8
9
|
///////////////////////////////////////////////////////////////////////////
|
|
9
10
|
// Value encoding helpers
|
|
@@ -32,6 +33,17 @@ function addressBody(value: string): string {
|
|
|
32
33
|
return toAddress(value).slice(2);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Require an operand that reached the builder untyped to be a bigint and return it.
|
|
38
|
+
* @throws {CallciumError} When the operand is of another type, as a JavaScript `number` is.
|
|
39
|
+
*/
|
|
40
|
+
function requireBigint(value: unknown): bigint {
|
|
41
|
+
if (typeof value !== "bigint") {
|
|
42
|
+
throw new CallciumError("MALFORMED_OPERAND", "Operand is not a bigint, a boolean or an address string");
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/** Convert a scalar value to a 32-byte big-endian word. */
|
|
36
48
|
function encodeWord(value: ScalarValue): Uint8Array {
|
|
37
49
|
const word = new Uint8Array(32);
|
|
@@ -47,6 +59,8 @@ function encodeWord(value: ScalarValue): Uint8Array {
|
|
|
47
59
|
return word;
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
requireBigint(value);
|
|
63
|
+
|
|
50
64
|
// A negative operand occupies the word in two's complement, which is the signed target's encoding.
|
|
51
65
|
let bigValue = checkOperandRange(value);
|
|
52
66
|
for (let i = 31; i >= 0; i--) {
|
|
@@ -79,6 +93,9 @@ function rangeOp(opCode: number, min: bigint, max: bigint): Hex {
|
|
|
79
93
|
return bytesToHex(buffer);
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
/** The `source` bits of an operator code whose operand is a context property. */
|
|
97
|
+
const CONTEXT_SOURCE = Source.CONTEXT << Op.SOURCE_SHIFT;
|
|
98
|
+
|
|
82
99
|
/**
|
|
83
100
|
* Require an assigned context property ID and return it as an operand word.
|
|
84
101
|
* @throws {CallciumError} When the ID names no assigned property.
|
|
@@ -90,14 +107,13 @@ function contextOperand(contextPropertyId: number): bigint {
|
|
|
90
107
|
|
|
91
108
|
/** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
|
|
92
109
|
function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
|
|
93
|
-
// PWF-21 orders members by their 32-byte encodings, so a negative operand and its unsigned
|
|
94
|
-
//
|
|
95
|
-
// out-of-range value an error rather than folding it into the word.
|
|
110
|
+
// PWF-21 orders members by their 32-byte encodings, so a negative operand and its unsigned alias are one member at
|
|
111
|
+
// one position. Normalising after the domain check keeps an out-of-range value an error rather than a word.
|
|
96
112
|
const words = values.map((value) => {
|
|
97
113
|
if (typeof value === "bigint") return BigInt.asUintN(256, checkOperandRange(value));
|
|
98
114
|
if (typeof value === "boolean") return value ? 1n : 0n;
|
|
99
|
-
|
|
100
|
-
return
|
|
115
|
+
if (typeof value === "string") return BigInt("0x" + addressBody(value));
|
|
116
|
+
return requireBigint(value);
|
|
101
117
|
});
|
|
102
118
|
|
|
103
119
|
const deduped = [...new Set(words)].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
@@ -105,9 +121,6 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
|
|
|
105
121
|
if (deduped.length === 0) {
|
|
106
122
|
throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
107
123
|
}
|
|
108
|
-
if (deduped.length > PolicyFormat.MAX_SET_MEMBERS) {
|
|
109
|
-
throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${PolicyFormat.MAX_SET_MEMBERS} values`);
|
|
110
|
-
}
|
|
111
124
|
|
|
112
125
|
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
113
126
|
buffer[0] = opCode;
|
|
@@ -122,8 +135,8 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
|
|
|
122
135
|
///////////////////////////////////////////////////////////////////////////
|
|
123
136
|
|
|
124
137
|
/**
|
|
125
|
-
* Extremes of the numeric operands a builder's
|
|
126
|
-
* a negative operand onto its unsigned alias, so the sign survives nowhere else.
|
|
138
|
+
* Extremes of the numeric operands a builder's equality, comparison and set operators received, as written. Encoding
|
|
139
|
+
* folds a negative operand onto its unsigned alias, so the sign survives nowhere else.
|
|
127
140
|
*/
|
|
128
141
|
export type OperandExtremes = {
|
|
129
142
|
/** Most negative operand written, or zero when none was negative. */
|
|
@@ -133,10 +146,9 @@ export type OperandExtremes = {
|
|
|
133
146
|
};
|
|
134
147
|
|
|
135
148
|
/**
|
|
136
|
-
* Extremes of the numeric operands the value operators of `constraint` received, as written.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* operators arrive already encoded.
|
|
149
|
+
* Extremes of the numeric operands the value operators of `constraint` received, as written. They outlive edits to
|
|
150
|
+
* `operators`: removing an operator does not remove what was written to produce it, and a fresh builder starts at zero.
|
|
151
|
+
* Undefined for a plain `Constraint`, whose operators arrive already encoded.
|
|
140
152
|
*/
|
|
141
153
|
export function readOperandExtremes(constraint: Constraint | ConstraintBuilder): OperandExtremes | undefined {
|
|
142
154
|
return "operandExtremes" in constraint ? constraint.operandExtremes : undefined;
|
|
@@ -147,23 +159,21 @@ export function readOperandExtremes(constraint: Constraint | ConstraintBuilder):
|
|
|
147
159
|
///////////////////////////////////////////////////////////////////////////
|
|
148
160
|
|
|
149
161
|
/**
|
|
150
|
-
* Mutable builder that accumulates operators targeting a single path.
|
|
151
|
-
*
|
|
162
|
+
* Mutable builder that accumulates operators targeting a single path. Implements the `Constraint` interface so it can
|
|
163
|
+
* be passed directly to policy builders.
|
|
152
164
|
*/
|
|
153
165
|
export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implements Constraint {
|
|
154
|
-
readonly scope:
|
|
166
|
+
readonly scope: Constraint["scope"];
|
|
155
167
|
readonly path: Hex;
|
|
156
168
|
readonly operators: Hex[];
|
|
157
169
|
/**
|
|
158
|
-
* Extremes of the numeric operands the value operators received, as written.
|
|
159
|
-
* can offend: an operand folds into a target's range from below it or from above it, and
|
|
160
|
-
* whichever reaches furthest arrives first. Read it through {@link readOperandExtremes}.
|
|
170
|
+
* Extremes of the numeric operands the value operators received, as written.
|
|
161
171
|
* @internal
|
|
162
172
|
*/
|
|
163
173
|
readonly operandExtremes: OperandExtremes;
|
|
164
174
|
|
|
165
175
|
/** @internal */
|
|
166
|
-
constructor(scope:
|
|
176
|
+
constructor(scope: Constraint["scope"], path: Hex) {
|
|
167
177
|
this.scope = scope;
|
|
168
178
|
this.path = path;
|
|
169
179
|
this.operators = [];
|
|
@@ -171,8 +181,8 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
|
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/**
|
|
174
|
-
* Push a pre-encoded operator hex string and return this for chaining.
|
|
175
|
-
*
|
|
184
|
+
* Push a pre-encoded operator hex string and return this for chaining. `values` are the operands as written. The
|
|
185
|
+
* numeric ones join the record.
|
|
176
186
|
*/
|
|
177
187
|
private push(opHex: Hex, values: readonly ScalarValue[] = []): this {
|
|
178
188
|
for (const value of values) {
|
|
@@ -200,12 +210,32 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
|
|
|
200
210
|
|
|
201
211
|
/** Assert the value equals the context property `contextPropertyId`. */
|
|
202
212
|
eqCtx(contextPropertyId: number): this {
|
|
203
|
-
return this.push(singleOp(Op.
|
|
213
|
+
return this.push(singleOp(Op.EQ | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
/** Assert the value does not equal the context property `contextPropertyId`. */
|
|
207
217
|
neqCtx(contextPropertyId: number): this {
|
|
208
|
-
return this.push(singleOp(Op.
|
|
218
|
+
return this.push(singleOp(Op.EQ | CONTEXT_SOURCE | Op.NOT, contextOperand(contextPropertyId)));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Assert the value is greater than the context property `contextPropertyId`. */
|
|
222
|
+
gtCtx(contextPropertyId: number): this {
|
|
223
|
+
return this.push(singleOp(Op.GT | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Assert the value is less than the context property `contextPropertyId`. */
|
|
227
|
+
ltCtx(contextPropertyId: number): this {
|
|
228
|
+
return this.push(singleOp(Op.LT | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Assert the value is greater than or equal to the context property `contextPropertyId`. */
|
|
232
|
+
gteCtx(contextPropertyId: number): this {
|
|
233
|
+
return this.push(singleOp(Op.GTE | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Assert the value is less than or equal to the context property `contextPropertyId`. */
|
|
237
|
+
lteCtx(contextPropertyId: number): this {
|
|
238
|
+
return this.push(singleOp(Op.LTE | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
/** Assert the value is greater than `bound`. */
|
|
@@ -241,9 +271,8 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
|
|
|
241
271
|
///////////////////////////////////////////////////////////////////////////
|
|
242
272
|
|
|
243
273
|
/**
|
|
244
|
-
* Assert the value is a member of the set.
|
|
245
|
-
*
|
|
246
|
-
* @throws {CallciumError} If the set is empty after deduplication.
|
|
274
|
+
* Assert the value is a member of the set. Values are sorted and deduplicated before encoding.
|
|
275
|
+
* @throws {CallciumError} If the set is empty or a value is not encodable as an operand word.
|
|
247
276
|
*/
|
|
248
277
|
isIn(values: readonly Operand[]): this {
|
|
249
278
|
return this.push(setOp(Op.IN, values), values);
|
|
@@ -251,7 +280,7 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
|
|
|
251
280
|
|
|
252
281
|
/**
|
|
253
282
|
* Assert the value is not a member of the set.
|
|
254
|
-
* @throws {CallciumError} If the set is empty
|
|
283
|
+
* @throws {CallciumError} If the set is empty or a value is not encodable as an operand word.
|
|
255
284
|
*/
|
|
256
285
|
notIn(values: readonly Operand[]): this {
|
|
257
286
|
return this.push(setOp(Op.IN | Op.NOT, values), values);
|
|
@@ -319,8 +348,7 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
|
|
|
319
348
|
///////////////////////////////////////////////////////////////////////////
|
|
320
349
|
|
|
321
350
|
/**
|
|
322
|
-
* Target a calldata argument by path.
|
|
323
|
-
* Each argument is a big-endian uint16 step; multiple steps navigate into nested types.
|
|
351
|
+
* Target a calldata argument by path. Each argument is a big-endian uint16 step. Further steps navigate nested types.
|
|
324
352
|
*/
|
|
325
353
|
export function arg(p0: number, ...rest: number[]): ConstraintBuilder;
|
|
326
354
|
export function arg(...steps: number[]): ConstraintBuilder {
|