@callcium/sdk 0.11.1 → 0.12.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 +101 -88
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bytes.ts +8 -25
- package/src/calldata-reader.ts +3 -3
- package/src/constants.ts +5 -1
- package/src/constraint.ts +40 -22
- package/src/descriptor-coder.ts +14 -23
- package/src/descriptor.ts +18 -19
- package/src/errors.ts +14 -17
- package/src/index.ts +2 -3
- package/src/operators.ts +65 -35
- package/src/path.ts +1 -2
- package/src/policy-builder.ts +15 -20
- package/src/policy-coder.ts +20 -29
- package/src/policy-enforcer.ts +21 -23
- package/src/policy-validator.ts +51 -44
- package/src/signature.ts +6 -6
- package/src/type-desc.ts +6 -6
- package/src/types.ts +11 -16
package/src/descriptor-coder.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { concatBytes } from "@noble/hashes/utils.js";
|
|
2
|
+
|
|
1
3
|
import { readU16 } from "./bytes";
|
|
2
4
|
import { DescriptorFormat as DF, TypeCode } from "./constants";
|
|
3
5
|
import { Descriptor, readNode } from "./descriptor";
|
|
@@ -12,8 +14,7 @@ import type { DecodedParam, Hex } from "./types";
|
|
|
12
14
|
///////////////////////////////////////////////////////////////////////////
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Collect the positions of depth-0 commas in input[start..end).
|
|
16
|
-
* Commas inside `()` or `[]` are skipped.
|
|
17
|
+
* Collect the positions of depth-0 commas in input[start..end). Commas inside `()` or `[]` are skipped.
|
|
17
18
|
*/
|
|
18
19
|
function commaPositions(input: string, start: number, end: number): number[] {
|
|
19
20
|
const positions: number[] = [];
|
|
@@ -47,8 +48,8 @@ const MAX_TYPE_STRING_NUMBER = 256 ** DF.ARRAY_LENGTH_SIZE - 1;
|
|
|
47
48
|
/**
|
|
48
49
|
* Parse a decimal integer from input[start..end).
|
|
49
50
|
*
|
|
50
|
-
* Rejects an empty span, a non-digit, a leading zero, and a value too wide for the fields a type
|
|
51
|
-
*
|
|
51
|
+
* Rejects an empty span, a non-digit, a leading zero, and a value too wide for the fields a type string feeds. Width
|
|
52
|
+
* and length validity are out of scope here.
|
|
52
53
|
*/
|
|
53
54
|
function parseUint(input: string, start: number, end: number): number {
|
|
54
55
|
if (start >= end) {
|
|
@@ -77,8 +78,7 @@ function parseUint(input: string, start: number, end: number): number {
|
|
|
77
78
|
///////////////////////////////////////////////////////////////////////////
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
* Parse a tuple literal `(field0,field1,...)`
|
|
81
|
-
* `start` and the substring to parse ends before `end`.
|
|
81
|
+
* Parse a tuple literal `(field0,field1,...)` whose opening `(` is at `start` and whose content ends before `end`.
|
|
82
82
|
*/
|
|
83
83
|
function parseTuple(input: string, start: number, end: number): Uint8Array {
|
|
84
84
|
if (input[start] !== "(") {
|
|
@@ -137,8 +137,8 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
|
|
|
137
137
|
if (segment === "string") return string_();
|
|
138
138
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
139
139
|
|
|
140
|
-
// A prefix with no width after it spells no type, so it falls through to the unknown-type report
|
|
141
|
-
//
|
|
140
|
+
// A prefix with no width after it spells no type, so it falls through to the unknown-type report rather than being
|
|
141
|
+
// read as a width that is not there.
|
|
142
142
|
if (segment.length > 4 && segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
143
143
|
if (segment.length > 3 && segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
144
144
|
if (segment.length > 5 && segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
@@ -153,16 +153,16 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
|
|
|
153
153
|
/**
|
|
154
154
|
* Parse a type segment from input[start..end).
|
|
155
155
|
*
|
|
156
|
-
* Scans backward from end to collect all `[...]` suffixes, identifies the
|
|
157
|
-
*
|
|
156
|
+
* Scans backward from end to collect all `[...]` suffixes, identifies the base type extent, parses the base, then
|
|
157
|
+
* applies suffixes left-to-right.
|
|
158
158
|
*/
|
|
159
159
|
function parseType(input: string, start: number, end: number): Uint8Array {
|
|
160
160
|
if (start >= end) {
|
|
161
161
|
throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// Locate array suffixes by scanning backward, reading no length yet: the base names the type, so
|
|
165
|
-
//
|
|
164
|
+
// Locate array suffixes by scanning backward, reading no length yet: the base names the type, so an unrecognised one
|
|
165
|
+
// is reported before a suffix it carries is parsed.
|
|
166
166
|
const spans: Array<[number, number]> = [];
|
|
167
167
|
let baseEnd = end;
|
|
168
168
|
|
|
@@ -182,8 +182,7 @@ function parseType(input: string, start: number, end: number): Uint8Array {
|
|
|
182
182
|
|
|
183
183
|
let desc = parseBaseType(input, start, baseEnd);
|
|
184
184
|
|
|
185
|
-
// Apply suffixes left-to-right: the leftmost suffix is the
|
|
186
|
-
// A dynamic array spells no length at all.
|
|
185
|
+
// Apply suffixes left-to-right: the leftmost suffix is the innermost array. A dynamic array spells no length at all.
|
|
187
186
|
for (const [innerStart, innerEnd] of spans) {
|
|
188
187
|
desc = array(desc, innerStart === innerEnd ? undefined : parseUint(input, innerStart, innerEnd));
|
|
189
188
|
}
|
|
@@ -223,15 +222,7 @@ function fromTypes(typesCsv: string): Uint8Array {
|
|
|
223
222
|
if (paramDescs.length > DF.MAX_PARAMS) {
|
|
224
223
|
throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}`);
|
|
225
224
|
}
|
|
226
|
-
const
|
|
227
|
-
const result = new Uint8Array(DF.HEADER_SIZE + totalBytes);
|
|
228
|
-
result[0] = DF.VERSION;
|
|
229
|
-
result[1] = paramDescs.length;
|
|
230
|
-
let offset = DF.HEADER_SIZE;
|
|
231
|
-
for (const desc of paramDescs) {
|
|
232
|
-
result.set(desc, offset);
|
|
233
|
-
offset += desc.length;
|
|
234
|
-
}
|
|
225
|
+
const result = concatBytes(Uint8Array.of(DF.VERSION, paramDescs.length), ...paramDescs);
|
|
235
226
|
// Nesting depth is a property of the assembled descriptor, not of any single node.
|
|
236
227
|
decodeDescriptor(result);
|
|
237
228
|
return result;
|
package/src/descriptor.ts
CHANGED
|
@@ -46,11 +46,10 @@ type HintWalk = {
|
|
|
46
46
|
type Node = { typeCode: number; staticWords: number; nodeLength: number };
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Read the node at `offset`.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* or declares a length below its own header or beyond the descriptor.
|
|
49
|
+
* Read the node at `offset`. Elementary nodes occupy exactly one byte; composite nodes encode their static word count
|
|
50
|
+
* and length in the 24-bit meta field that follows the type code.
|
|
51
|
+
* @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code, or declares a
|
|
52
|
+
* length below its own header or beyond the descriptor.
|
|
54
53
|
*/
|
|
55
54
|
export function readNode(desc: Uint8Array, offset: number): Node {
|
|
56
55
|
if (offset >= desc.length) {
|
|
@@ -90,8 +89,8 @@ function nodeLength(desc: Uint8Array, offset: number): number {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
93
|
-
* Return the byte offset of the `fieldIndex`-th field inside a tuple node.
|
|
94
|
-
*
|
|
92
|
+
* Return the byte offset of the `fieldIndex`-th field inside a tuple node. Starts at the first field and skips
|
|
93
|
+
* `fieldIndex` nodes using `nodeLength`.
|
|
95
94
|
*/
|
|
96
95
|
function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: number): number {
|
|
97
96
|
let cursor = tupleOffset + DF.TUPLE_HEADER_SIZE;
|
|
@@ -144,27 +143,27 @@ function inspect(desc: Uint8Array, offset: number): TypeInfo {
|
|
|
144
143
|
/**
|
|
145
144
|
* Resolve the type at a calldata path (array of step indices).
|
|
146
145
|
*
|
|
147
|
-
* The first step selects a top-level param. Each subsequent step descends
|
|
148
|
-
*
|
|
149
|
-
* advances to the element descriptor.
|
|
146
|
+
* The first step selects a top-level param. Each subsequent step descends into the current node: for tuples it selects
|
|
147
|
+
* a field, for arrays it advances to the element descriptor.
|
|
150
148
|
*
|
|
151
149
|
* @param desc - Raw descriptor bytes.
|
|
152
150
|
* @param steps - Path steps, length >= 1.
|
|
153
151
|
* @returns TypeInfo for the node at the resolved path.
|
|
154
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent
|
|
152
|
+
* @throws {CallciumError} On empty steps, an out-of-bounds param index, tuple field or static array index, or descent
|
|
153
|
+
* into an elementary type.
|
|
155
154
|
*/
|
|
156
155
|
function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
|
|
157
156
|
return walkPath(desc, steps).typeInfo;
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
/**
|
|
161
|
-
* Resolve the type at the path and the declared length of the static array a
|
|
162
|
-
*
|
|
163
|
-
* over a static array).
|
|
160
|
+
* Resolve the type at the path and the declared length of the static array a quantifier step descends into (zero when
|
|
161
|
+
* the path holds no quantifier step over a static array).
|
|
164
162
|
* @param desc - Raw descriptor bytes.
|
|
165
163
|
* @param steps - Path steps, length >= 1.
|
|
166
164
|
* @returns TypeInfo at the resolved path and the quantified static array length.
|
|
167
|
-
* @throws {CallciumError} On empty steps, out-of-bounds param or array index, or descent
|
|
165
|
+
* @throws {CallciumError} On empty steps, an out-of-bounds param index, tuple field or static array index, or descent
|
|
166
|
+
* into an elementary type.
|
|
168
167
|
*/
|
|
169
168
|
function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
|
|
170
169
|
if (steps.length === 0) {
|
|
@@ -188,8 +187,8 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
|
|
|
188
187
|
}
|
|
189
188
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
190
189
|
} else if (typeCode === TypeCode.STATIC_ARRAY || typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
191
|
-
// Quantifier sentinels descend into the element type; only concrete
|
|
192
|
-
//
|
|
190
|
+
// Quantifier sentinels descend into the element type; only concrete indices are bounds-checked against the
|
|
191
|
+
// declared array length.
|
|
193
192
|
if (typeCode === TypeCode.STATIC_ARRAY) {
|
|
194
193
|
if (isQuantifier(step)) {
|
|
195
194
|
quantifiedStaticLength = staticArrayLength(desc, cursor);
|
|
@@ -377,8 +376,8 @@ function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth:
|
|
|
377
376
|
* @param desc - Raw descriptor bytes.
|
|
378
377
|
* @param steps - Path steps, length >= 1.
|
|
379
378
|
* @returns The hint block bytes.
|
|
380
|
-
* @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over
|
|
381
|
-
*
|
|
379
|
+
* @throws {CallciumError} When a step leaves the structure the descriptor declares, quantifies over a non-array node,
|
|
380
|
+
* or repeats a quantifier.
|
|
382
381
|
*/
|
|
383
382
|
function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
|
|
384
383
|
if (steps.length === 0) {
|
package/src/errors.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Machine-readable error code for structural decoding and validation failures.
|
|
3
3
|
*
|
|
4
|
-
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
* `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
|
|
4
|
+
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error name transliterated
|
|
5
|
+
* from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and validation" have no counterpart: they name
|
|
6
|
+
* input a Solidity signature cannot express. `OUT_OF_PHYSICAL_BOUNDS` names an invariant Solidity carries twice, as the
|
|
7
|
+
* error `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
|
|
9
8
|
*/
|
|
10
9
|
export type CallciumErrorCode =
|
|
11
10
|
// Policy and descriptor framing.
|
|
@@ -73,18 +72,18 @@ export type CallciumErrorCode =
|
|
|
73
72
|
| "RULE_COUNT_OVERFLOW"
|
|
74
73
|
| "RULE_SIZE_OVERFLOW"
|
|
75
74
|
| "DESC_LENGTH_OVERFLOW"
|
|
75
|
+
// Strict build.
|
|
76
|
+
| "VALIDATION_ERROR"
|
|
76
77
|
// Inputs and validation.
|
|
77
78
|
| "UNKNOWN_QUANTIFIER"
|
|
78
79
|
| "INVALID_HEX"
|
|
79
80
|
| "MALFORMED_SELECTOR"
|
|
80
81
|
| "PATH_STEP_OVERFLOW"
|
|
81
82
|
| "OPERAND_OVERFLOW"
|
|
82
|
-
| "CONTEXT_VALUE_OVERFLOW"
|
|
83
|
-
| "VALIDATION_ERROR";
|
|
83
|
+
| "CONTEXT_VALUE_OVERFLOW";
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Thrown
|
|
87
|
-
* The optional `offset` indicates the byte position in the source blob where the error was detected.
|
|
86
|
+
* Thrown on malformed or inadmissible SDK input. `offset`, when set, is the error's byte position in the source blob.
|
|
88
87
|
*/
|
|
89
88
|
export class CallciumError extends Error {
|
|
90
89
|
public readonly code: CallciumErrorCode;
|
|
@@ -104,9 +103,8 @@ export class CallciumError extends Error {
|
|
|
104
103
|
/**
|
|
105
104
|
* Thrown by `PolicyBuilder.build` when validation reports any issue.
|
|
106
105
|
*
|
|
107
|
-
* Carries every issue the validator found. The `Error.message` is the first issue's
|
|
108
|
-
*
|
|
109
|
-
* issues to users should iterate `issues` directly.
|
|
106
|
+
* Carries every issue the validator found. The `Error.message` is the first issue's message — a diagnostic summary, not
|
|
107
|
+
* a presentation contract. Consumers rendering issues to users should iterate `issues` directly.
|
|
110
108
|
*/
|
|
111
109
|
export class ValidationError extends CallciumError {
|
|
112
110
|
public readonly issues: import("./types").Issue[];
|
|
@@ -119,12 +117,11 @@ export class ValidationError extends CallciumError {
|
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
/**
|
|
122
|
-
* Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement.
|
|
123
|
-
*
|
|
120
|
+
* Thrown by `PolicyEnforcer.enforce` when calldata fails policy enforcement. Distinct from `ValidationError`, which
|
|
121
|
+
* signals static issues in the policy itself.
|
|
124
122
|
*
|
|
125
|
-
* Carries the full list of structured violations (one per failed group). The
|
|
126
|
-
*
|
|
127
|
-
* first violation's structured fields — not a presentation contract. Consumers
|
|
123
|
+
* Carries the full list of structured violations (one per failed group). The `Error.message` is a minimal non-lossy
|
|
124
|
+
* diagnostic summary built from the first violation's structured fields — not a presentation contract. Consumers
|
|
128
125
|
* rendering violations to users should iterate `violations` directly.
|
|
129
126
|
*/
|
|
130
127
|
export class PolicyViolationError extends Error {
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { PolicyEnforcer } from "./policy-enforcer";
|
|
|
30
30
|
// Utilities
|
|
31
31
|
///////////////////////////////////////////////////////////////////////////
|
|
32
32
|
|
|
33
|
-
export {
|
|
33
|
+
export { hexToBytes, bytesToHex } from "./bytes";
|
|
34
34
|
export { Descriptor } from "./descriptor";
|
|
35
35
|
export { DescriptorCoder } from "./descriptor-coder";
|
|
36
36
|
export type { TypeInfo } from "./descriptor";
|
|
@@ -45,13 +45,12 @@ export {
|
|
|
45
45
|
PolicyFormat,
|
|
46
46
|
Scope,
|
|
47
47
|
ContextProperty,
|
|
48
|
-
MAX_CONTEXT_PROPERTY_ID,
|
|
49
48
|
findContextProperty,
|
|
50
49
|
lookupOp,
|
|
51
50
|
lookupScope,
|
|
52
51
|
lookupContextProperty,
|
|
53
52
|
} from "./constants";
|
|
54
|
-
export { isLengthOp, lookupTypeCode } from "./operators";
|
|
53
|
+
export { isContextOp, isLengthOp, lookupTypeCode } from "./operators";
|
|
55
54
|
export type { TypeCodeInfo, TypeClassInfo, TypeClass } from "./operators";
|
|
56
55
|
export type { Operands, OpInfo, ScopeInfo, ContextPropertyInfo } from "./constants";
|
|
57
56
|
|
package/src/operators.ts
CHANGED
|
@@ -21,8 +21,8 @@ export function isSigned(typeCode: number): boolean {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
25
|
-
*
|
|
24
|
+
* Return true when the type code represents a left-aligned type (fixed bytes or function), whose value occupies the
|
|
25
|
+
* high bytes of the word with zero padding below.
|
|
26
26
|
*/
|
|
27
27
|
export function isLeftAligned(typeCode: number): boolean {
|
|
28
28
|
return (
|
|
@@ -31,11 +31,9 @@ export function isLeftAligned(typeCode: number): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* comparison use the canonical ABI value rather than the raw bytes.
|
|
38
|
-
* @param value - The raw 256-bit word loaded from calldata.
|
|
34
|
+
* Return the canonical encoding of a 256-bit word for the declared type. Masks unsigned/address/bool to width, clears
|
|
35
|
+
* bytesN/function padding, and sign-extends signed integers. A word is canonical when this leaves it unchanged.
|
|
36
|
+
* @param value - The 256-bit word.
|
|
39
37
|
* @param typeCode - The declared type code of the value.
|
|
40
38
|
* @returns The canonicalized 256-bit value.
|
|
41
39
|
*/
|
|
@@ -78,14 +76,10 @@ export function isLengthOp(opCode: number): boolean {
|
|
|
78
76
|
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
/**
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (base === Op.LENGTH_LT) return Op.LT;
|
|
86
|
-
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
87
|
-
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
88
|
-
return Op.BETWEEN;
|
|
79
|
+
/** Return true when the operator code (with or without NOT flag) is a *_CTX variant. */
|
|
80
|
+
export function isContextOp(opCode: number): boolean {
|
|
81
|
+
const base = opCode & ~Op.NOT;
|
|
82
|
+
return base >= Op.EQ_CTX && base <= Op.LTE_CTX;
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
@@ -119,12 +113,12 @@ function isIn(value: bigint, data: Uint8Array): boolean {
|
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
/**
|
|
122
|
-
* Apply a single operator against a value and operand data.
|
|
123
|
-
* Handles the NOT flag internally by inverting the base operator result.
|
|
116
|
+
* Apply a single operator against a value and operand data. Handles the NOT flag by inverting the base result.
|
|
124
117
|
* @param opCode - Operator code, potentially OR'd with the NOT flag.
|
|
125
118
|
* @param value - The 256-bit value to test (ignored for LENGTH_* operators).
|
|
126
119
|
* @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
|
|
127
|
-
* @param operandData - Raw operand bytes (32 per operand, 64 for range operators)
|
|
120
|
+
* @param operandData - Raw operand bytes (32 per operand, 64 for range operators); for a context reference operator,
|
|
121
|
+
* the referenced property's value as one word.
|
|
128
122
|
* @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
|
|
129
123
|
* @returns Whether the value satisfies the operator.
|
|
130
124
|
*/
|
|
@@ -135,17 +129,7 @@ export function applyOperator(
|
|
|
135
129
|
operandData: Uint8Array,
|
|
136
130
|
typeCode: number,
|
|
137
131
|
): boolean {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Length operators reuse the value-comparison core: the runtime count becomes the compared
|
|
141
|
-
// value and the opcode maps onto its EQ/GT/LT/GTE/LTE/BETWEEN twin. Counts are non-negative,
|
|
142
|
-
// so the comparison is always unsigned — force an unsigned type code regardless of the target.
|
|
143
|
-
if (isLengthOp(base)) {
|
|
144
|
-
value = valueLength;
|
|
145
|
-
typeCode = TypeCode.UINT_MAX;
|
|
146
|
-
base = lengthToValueOp(base);
|
|
147
|
-
}
|
|
148
|
-
|
|
132
|
+
const base = opCode & ~Op.NOT;
|
|
149
133
|
let result: boolean;
|
|
150
134
|
|
|
151
135
|
switch (base) {
|
|
@@ -198,6 +182,53 @@ export function applyOperator(
|
|
|
198
182
|
break;
|
|
199
183
|
}
|
|
200
184
|
|
|
185
|
+
case Op.LENGTH_EQ:
|
|
186
|
+
result = valueLength === toBigInt(operandData, 0);
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case Op.LENGTH_GT:
|
|
190
|
+
result = valueLength > toBigInt(operandData, 0);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case Op.LENGTH_LT:
|
|
194
|
+
result = valueLength < toBigInt(operandData, 0);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case Op.LENGTH_GTE:
|
|
198
|
+
result = valueLength >= toBigInt(operandData, 0);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case Op.LENGTH_LTE:
|
|
202
|
+
result = valueLength <= toBigInt(operandData, 0);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case Op.LENGTH_BETWEEN: {
|
|
206
|
+
const lower = toBigInt(operandData, 0);
|
|
207
|
+
const upper = toBigInt(operandData, 32);
|
|
208
|
+
result = valueLength >= lower && valueLength <= upper;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case Op.EQ_CTX:
|
|
213
|
+
result = value === toBigInt(operandData, 0);
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case Op.GT_CTX:
|
|
217
|
+
result = value > toBigInt(operandData, 0);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case Op.LT_CTX:
|
|
221
|
+
result = value < toBigInt(operandData, 0);
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
case Op.GTE_CTX:
|
|
225
|
+
result = value >= toBigInt(operandData, 0);
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case Op.LTE_CTX:
|
|
229
|
+
result = value <= toBigInt(operandData, 0);
|
|
230
|
+
break;
|
|
231
|
+
|
|
201
232
|
default:
|
|
202
233
|
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PF.RULE_OPCODE_SIZE)}`);
|
|
203
234
|
}
|
|
@@ -260,10 +291,9 @@ const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
|
|
|
260
291
|
const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
|
|
261
292
|
|
|
262
293
|
/**
|
|
263
|
-
* Classify a type code into its structural category and dynamism.
|
|
264
|
-
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
294
|
+
* Classify a type code into its structural category and dynamism. A `lookupTypeCode` variant without labels.
|
|
265
295
|
* @param code - A single-byte descriptor type code.
|
|
266
|
-
* @returns Type class and whether the
|
|
296
|
+
* @returns Type class, and whether the code alone implies ABI dynamism (false for tuples and static arrays).
|
|
267
297
|
* @throws {CallciumError} If the code is not a recognised type code.
|
|
268
298
|
*/
|
|
269
299
|
export function classifyTypeCode(code: number): TypeClassInfo {
|
|
@@ -296,8 +326,8 @@ export function isElementary(code: number): boolean {
|
|
|
296
326
|
}
|
|
297
327
|
|
|
298
328
|
/**
|
|
299
|
-
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
300
|
-
*
|
|
329
|
+
* Determine whether a type code names a value an operator can read: a scalar word or a declared length. Tuples, static
|
|
330
|
+
* arrays, and undefined codes address nothing.
|
|
301
331
|
* @param code - A single-byte descriptor type code.
|
|
302
332
|
* @returns True when an operator can be applied to a target of this type.
|
|
303
333
|
*/
|
|
@@ -332,7 +362,7 @@ function typeCodeLabel(code: number): string {
|
|
|
332
362
|
/**
|
|
333
363
|
* Map a raw type code byte to its ABI type label, structural category, and dynamism.
|
|
334
364
|
* @param code - A single-byte descriptor type code.
|
|
335
|
-
* @returns Label, type class, and whether the
|
|
365
|
+
* @returns Label, type class, and whether the code alone implies ABI dynamism (false for tuples and static arrays).
|
|
336
366
|
* @throws {CallciumError} If the code is not a recognised type code.
|
|
337
367
|
*/
|
|
338
368
|
export function lookupTypeCode(code: number): TypeCodeInfo {
|
package/src/path.ts
CHANGED
|
@@ -47,8 +47,7 @@ export function lookupQuantifier(code: number): QuantifierInfo {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Encode a sequence of uint16 path steps as a big-endian hex string.
|
|
50
|
-
* @throws {CallciumError} When a step is not an integer the field holds
|
|
51
|
-
* address a different argument.
|
|
50
|
+
* @throws {CallciumError} When a step is not an integer the field holds; a wrapped step addresses another argument.
|
|
52
51
|
*/
|
|
53
52
|
export function encodePath(steps: readonly number[]): Hex {
|
|
54
53
|
const buffer = new Uint8Array(steps.length * PF.PATH_STEP_SIZE);
|
package/src/policy-builder.ts
CHANGED
|
@@ -124,26 +124,24 @@ function reject(written: bigint, folded: bigint): never {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* Reject an operand that two's complement folds onto a value the target admits.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* reaches a signed range only at that type's own minimum word.
|
|
127
|
+
* Reject an operand that two's complement folds onto a value the target admits. Such an operand and the value it folds
|
|
128
|
+
* onto encode to one word, so the operands as written are the only place the two stay distinct. An operand spans no
|
|
129
|
+
* more than one word, which leaves two ways across: a negative's alias clears every target narrower than the whole
|
|
130
|
+
* word, and a positive reaches a signed range only at that type's own minimum word.
|
|
132
131
|
*/
|
|
133
132
|
function checkOperandDomain(constraint: Constraint, typeCode: number): void {
|
|
134
133
|
const bounds = targetBounds(typeCode);
|
|
135
134
|
const extremes = readOperandExtremes(constraint);
|
|
136
135
|
if (bounds === null || extremes === undefined) return;
|
|
137
136
|
|
|
138
|
-
// A negative operand
|
|
139
|
-
// the whole word can hold too.
|
|
137
|
+
// A negative operand encodes as its unsigned alias's word, which only a whole-word target can hold too.
|
|
140
138
|
if (extremes.leastNegative < 0n && typeCode === TypeCode.UINT_MAX) {
|
|
141
139
|
reject(extremes.leastNegative, extremes.leastNegative + WORD_VALUES);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
if (!isSigned(typeCode)) return;
|
|
145
|
-
// An operand at or above the signed minimum's own word denotes that negative, and every such
|
|
146
|
-
//
|
|
143
|
+
// An operand at or above the signed minimum's own word denotes that negative, and every such operand already exceeds
|
|
144
|
+
// the signed maximum.
|
|
147
145
|
if (extremes.greatest >= WORD_VALUES + bounds.min) {
|
|
148
146
|
reject(extremes.greatest, extremes.greatest - WORD_VALUES);
|
|
149
147
|
}
|
|
@@ -195,9 +193,9 @@ export class PolicyBuilder {
|
|
|
195
193
|
/**
|
|
196
194
|
* Add a constraint to the current group.
|
|
197
195
|
* @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
|
|
198
|
-
* @throws {CallciumError} With code `OUT_OF_PHYSICAL_BOUNDS` when a numeric operand lies outside
|
|
199
|
-
*
|
|
200
|
-
*
|
|
196
|
+
* @throws {CallciumError} With code `OUT_OF_PHYSICAL_BOUNDS` when a numeric operand lies outside the target type's
|
|
197
|
+
* range and encodes as a value inside it. Operands are read as written, so a `Constraint` carrying encoded operator
|
|
198
|
+
* bytes is left to {@link validate}.
|
|
201
199
|
*/
|
|
202
200
|
add(constraint: Constraint): this {
|
|
203
201
|
// A builder compiles no hint; one arrives only on a constraint that came already encoded.
|
|
@@ -251,14 +249,12 @@ export class PolicyBuilder {
|
|
|
251
249
|
}
|
|
252
250
|
|
|
253
251
|
/**
|
|
254
|
-
* Build the policy into an encoded binary blob with strict validation.
|
|
255
|
-
*
|
|
256
|
-
* inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
252
|
+
* Build the policy into an encoded binary blob with strict validation. Throws on any issue, regardless of severity.
|
|
253
|
+
* Use {@link validate} to inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
257
254
|
* @returns The policy as a 0x-prefixed hex string.
|
|
258
255
|
* @throws {CallciumError} If any group is empty.
|
|
259
256
|
* @throws {ValidationError} If validation finds any issue.
|
|
260
|
-
* @throws {CallciumError} With code `RULE_SIZE_OVERFLOW` when a rule's encoded bytes exceed the
|
|
261
|
-
* width of the rule size field.
|
|
257
|
+
* @throws {CallciumError} With code `RULE_SIZE_OVERFLOW` when a rule's encoded bytes exceed the rule size field.
|
|
262
258
|
*/
|
|
263
259
|
build(): Hex {
|
|
264
260
|
this.checkGroups();
|
|
@@ -271,9 +267,8 @@ export class PolicyBuilder {
|
|
|
271
267
|
}
|
|
272
268
|
|
|
273
269
|
/**
|
|
274
|
-
* Build the policy into an encoded binary blob without validation.
|
|
275
|
-
*
|
|
276
|
-
* only to knowingly bypass a reported issue.
|
|
270
|
+
* Build the policy into an encoded binary blob without validation. The resulting policy may be invalid. Prefer {@link
|
|
271
|
+
* build}; use this only to knowingly bypass a reported issue.
|
|
277
272
|
* @returns The policy as a 0x-prefixed hex string.
|
|
278
273
|
* @throws {CallciumError} If any group is empty.
|
|
279
274
|
*/
|