@callcium/sdk 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +202 -145
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +763 -667
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bytes.ts +9 -9
- package/src/calldata-reader.ts +14 -1
- package/src/constants.ts +85 -181
- package/src/constraint.ts +131 -94
- package/src/descriptor-coder.ts +53 -84
- package/src/descriptor.ts +65 -36
- package/src/errors.ts +12 -5
- package/src/index.ts +8 -17
- package/src/operators.ts +147 -11
- package/src/path.ts +80 -0
- package/src/policy-builder.ts +107 -44
- package/src/policy-coder.ts +102 -77
- package/src/policy-enforcer.ts +98 -128
- package/src/policy-validator.ts +70 -56
- package/src/signature.ts +21 -19
- package/src/type-desc.ts +15 -15
- package/src/types.ts +70 -3
- package/src/validation-issue.ts +4 -4
package/src/descriptor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readU16, readU24, writeBE16, writeBE32 } from "./bytes";
|
|
2
|
-
import { DescriptorFormat as DF, PolicyFormat as PF,
|
|
2
|
+
import { DescriptorFormat as DF, PolicyFormat as PF, TypeCode } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
|
+
import { classifyTypeCode } from "./operators";
|
|
5
|
+
import { Quantifier, isQuantifier } from "./path";
|
|
4
6
|
|
|
5
7
|
///////////////////////////////////////////////////////////////////////////
|
|
6
8
|
// Public types
|
|
@@ -40,15 +42,51 @@ type HintWalk = {
|
|
|
40
42
|
// Internal helpers
|
|
41
43
|
///////////////////////////////////////////////////////////////////////////
|
|
42
44
|
|
|
45
|
+
/** A descriptor node's type code, static word count, and total byte length. */
|
|
46
|
+
type Node = { typeCode: number; staticWords: number; nodeLength: number };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read the node at `offset`.
|
|
50
|
+
* Elementary nodes occupy exactly one byte; composite nodes encode their static
|
|
51
|
+
* word count and length in the 24-bit meta field that follows the type code.
|
|
52
|
+
* @throws {CallciumError} When the node starts past the descriptor, carries an undefined type code,
|
|
53
|
+
* or declares a length below its own header or beyond the descriptor.
|
|
54
|
+
*/
|
|
55
|
+
export function readNode(desc: Uint8Array, offset: number): Node {
|
|
56
|
+
if (offset >= desc.length) {
|
|
57
|
+
throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
58
|
+
}
|
|
59
|
+
const typeCode = desc[offset]!;
|
|
60
|
+
const info = classifyTypeCode(typeCode);
|
|
61
|
+
if (info.typeClass === "elementary") {
|
|
62
|
+
return { typeCode, staticWords: info.isDynamic ? 0 : 1, nodeLength: DF.TYPECODE_SIZE };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE > desc.length) {
|
|
66
|
+
throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
67
|
+
}
|
|
68
|
+
const meta = readU24(desc, offset + DF.TYPECODE_SIZE);
|
|
69
|
+
const length = meta & DF.META_NODE_LENGTH_MASK;
|
|
70
|
+
const minHeader = info.typeClass === "tuple" ? DF.TUPLE_HEADER_SIZE : DF.ARRAY_HEADER_SIZE;
|
|
71
|
+
if (length < minHeader) {
|
|
72
|
+
throw new CallciumError(
|
|
73
|
+
"NODE_LENGTH_TOO_SMALL",
|
|
74
|
+
`Composite node length ${length} is smaller than minimum header ${minHeader}`,
|
|
75
|
+
offset,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (offset + length > desc.length) {
|
|
79
|
+
throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
80
|
+
}
|
|
81
|
+
return { typeCode, staticWords: meta >> DF.META_STATIC_WORDS_SHIFT, nodeLength: length };
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
/**
|
|
44
85
|
* Return the byte length of the node at `offset`.
|
|
45
|
-
*
|
|
46
|
-
* length in the lower 12 bits of the 24-bit meta field.
|
|
86
|
+
* @throws {CallciumError} When the descriptor holds no readable node at `offset`.
|
|
47
87
|
*/
|
|
48
88
|
function nodeLength(desc: Uint8Array, offset: number): number {
|
|
49
|
-
|
|
50
|
-
if (typeCode < TypeCode.STATIC_ARRAY) return DF.TYPECODE_SIZE;
|
|
51
|
-
return readU24(desc, offset + 1) & DF.META_NODE_LENGTH_MASK;
|
|
89
|
+
return readNode(desc, offset).nodeLength;
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
/**
|
|
@@ -67,16 +105,22 @@ function tupleFieldOffset(desc: Uint8Array, tupleOffset: number, fieldIndex: num
|
|
|
67
105
|
// Public interface
|
|
68
106
|
///////////////////////////////////////////////////////////////////////////
|
|
69
107
|
|
|
70
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Return the declared top-level parameter count.
|
|
110
|
+
* @throws {CallciumError} When the descriptor is too short to hold a header.
|
|
111
|
+
*/
|
|
71
112
|
function paramCount(desc: Uint8Array): number {
|
|
113
|
+
if (desc.length < DF.HEADER_SIZE) {
|
|
114
|
+
throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
115
|
+
}
|
|
72
116
|
return desc[1]!;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
/** Return byte offset of the N-th top-level param (0-indexed). */
|
|
76
120
|
function paramOffset(desc: Uint8Array, index: number): number {
|
|
77
|
-
const count = desc
|
|
121
|
+
const count = paramCount(desc);
|
|
78
122
|
if (index >= count) {
|
|
79
|
-
throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})
|
|
123
|
+
throw new CallciumError("PARAM_INDEX_OUT_OF_BOUNDS", `Param index ${index} out of range (paramCount=${count})`);
|
|
80
124
|
}
|
|
81
125
|
let cursor = DF.HEADER_SIZE;
|
|
82
126
|
for (let i = 0; i < index; i++) {
|
|
@@ -90,17 +134,11 @@ function paramOffset(desc: Uint8Array, index: number): number {
|
|
|
90
134
|
* @param desc - Raw descriptor bytes.
|
|
91
135
|
* @param offset - Byte position of the node.
|
|
92
136
|
* @returns typeCode, isDynamic, and staticSize in bytes.
|
|
137
|
+
* @throws {CallciumError} When the descriptor holds no readable node at `offset`.
|
|
93
138
|
*/
|
|
94
139
|
function inspect(desc: Uint8Array, offset: number): TypeInfo {
|
|
95
|
-
const typeCode = desc
|
|
96
|
-
|
|
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 };
|
|
140
|
+
const { typeCode, staticWords } = readNode(desc, offset);
|
|
141
|
+
return { typeCode, isDynamic: staticWords === 0, staticSize: staticWords * 32 };
|
|
104
142
|
}
|
|
105
143
|
|
|
106
144
|
/**
|
|
@@ -130,19 +168,10 @@ function typeAt(desc: Uint8Array, steps: number[]): TypeInfo {
|
|
|
130
168
|
*/
|
|
131
169
|
function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quantifiedStaticLength: number } {
|
|
132
170
|
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
|
-
);
|
|
171
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
143
172
|
}
|
|
144
173
|
|
|
145
|
-
let cursor = paramOffset(desc,
|
|
174
|
+
let cursor = paramOffset(desc, steps[0]!);
|
|
146
175
|
let quantifiedStaticLength = 0;
|
|
147
176
|
|
|
148
177
|
for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
|
|
@@ -154,7 +183,7 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
|
|
|
154
183
|
if (step >= fields) {
|
|
155
184
|
throw new CallciumError(
|
|
156
185
|
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
157
|
-
`Tuple field index ${step} out of range (tuple has ${fields} fields)
|
|
186
|
+
`Tuple field index ${step} out of range (tuple has ${fields} fields)`,
|
|
158
187
|
);
|
|
159
188
|
}
|
|
160
189
|
cursor = tupleFieldOffset(desc, cursor, step);
|
|
@@ -169,14 +198,14 @@ function walkPath(desc: Uint8Array, steps: number[]): { typeInfo: TypeInfo; quan
|
|
|
169
198
|
if (step >= length) {
|
|
170
199
|
throw new CallciumError(
|
|
171
200
|
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
172
|
-
`Array index ${step} out of range (length=${length})
|
|
201
|
+
`Array index ${step} out of range (length=${length})`,
|
|
173
202
|
);
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
}
|
|
177
206
|
cursor = arrayElementOffset(cursor);
|
|
178
207
|
} else {
|
|
179
|
-
throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}
|
|
208
|
+
throw new CallciumError("NOT_COMPOSITE", `Cannot descend into elementary type at offset ${cursor}`);
|
|
180
209
|
}
|
|
181
210
|
}
|
|
182
211
|
|
|
@@ -199,8 +228,8 @@ function tupleFieldCount(desc: Uint8Array, offset: number): number {
|
|
|
199
228
|
*/
|
|
200
229
|
function staticArrayLength(desc: Uint8Array, offset: number): number {
|
|
201
230
|
const elemOffset = arrayElementOffset(offset);
|
|
202
|
-
const
|
|
203
|
-
const lengthOffset = elemOffset +
|
|
231
|
+
const elemNodeLength = nodeLength(desc, elemOffset);
|
|
232
|
+
const lengthOffset = elemOffset + elemNodeLength;
|
|
204
233
|
return readU16(desc, lengthOffset);
|
|
205
234
|
}
|
|
206
235
|
|
|
@@ -214,7 +243,7 @@ function arrayElementOffset(offset: number): number {
|
|
|
214
243
|
|
|
215
244
|
/** Build the error for a path step that does not resolve to a hop-chain target. */
|
|
216
245
|
function uncompilablePath(stepIndex: number): CallciumError {
|
|
217
|
-
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target
|
|
246
|
+
return new CallciumError("UNCOMPILABLE_PATH", `Path step ${stepIndex} does not resolve to a hop-chain target`);
|
|
218
247
|
}
|
|
219
248
|
|
|
220
249
|
/** Return the combined head slot span of the `count` nodes at `offset`, and the offset of the node past them. */
|
|
@@ -353,7 +382,7 @@ function encodeHint(walk: HintWalk, targetMeta: number, typeCode: number, depth:
|
|
|
353
382
|
*/
|
|
354
383
|
function compileHint(desc: Uint8Array, steps: number[]): Uint8Array {
|
|
355
384
|
if (steps.length === 0) {
|
|
356
|
-
throw new CallciumError("EMPTY_PATH", "Path must have at least one step
|
|
385
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step");
|
|
357
386
|
}
|
|
358
387
|
|
|
359
388
|
const argIndex = steps[0]!;
|
package/src/errors.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Machine-readable error code for structural decoding and validation failures.
|
|
3
3
|
*
|
|
4
|
-
* Each code names one invariant
|
|
5
|
-
*
|
|
4
|
+
* Each code names one invariant. Where Solidity detects the same invariant, the code is its error
|
|
5
|
+
* name transliterated from `PascalCase` to `SCREAMING_SNAKE_CASE`. The codes under "Inputs and
|
|
6
|
+
* validation" have no counterpart: they name input a Solidity signature cannot express.
|
|
7
|
+
* `OUT_OF_PHYSICAL_BOUNDS` names an invariant Solidity carries twice, as the error
|
|
8
|
+
* `PolicyBuilder.OutOfPhysicalBounds` and as an issue code, so the name is shared with `IssueCode`.
|
|
6
9
|
*/
|
|
7
10
|
export type CallciumErrorCode =
|
|
8
11
|
// Policy and descriptor framing.
|
|
@@ -47,6 +50,7 @@ export type CallciumErrorCode =
|
|
|
47
50
|
| "EMPTY_SET"
|
|
48
51
|
| "SET_TOO_LARGE"
|
|
49
52
|
| "INVALID_RANGE"
|
|
53
|
+
| "OUT_OF_PHYSICAL_BOUNDS"
|
|
50
54
|
// Descriptor nodes.
|
|
51
55
|
| "UNKNOWN_TYPE_CODE"
|
|
52
56
|
| "NODE_LENGTH_TOO_SMALL"
|
|
@@ -72,6 +76,10 @@ export type CallciumErrorCode =
|
|
|
72
76
|
// Inputs and validation.
|
|
73
77
|
| "UNKNOWN_QUANTIFIER"
|
|
74
78
|
| "INVALID_HEX"
|
|
79
|
+
| "MALFORMED_SELECTOR"
|
|
80
|
+
| "PATH_STEP_OVERFLOW"
|
|
81
|
+
| "OPERAND_OVERFLOW"
|
|
82
|
+
| "CONTEXT_VALUE_OVERFLOW"
|
|
75
83
|
| "VALIDATION_ERROR";
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -88,7 +96,8 @@ export class CallciumError extends Error {
|
|
|
88
96
|
super(`${prefix}${message}`);
|
|
89
97
|
this.name = "CallciumError";
|
|
90
98
|
this.code = code;
|
|
91
|
-
|
|
99
|
+
// The property is declared optional, so it stays absent rather than present and undefined.
|
|
100
|
+
if (offset !== undefined) this.offset = offset;
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -106,7 +115,6 @@ export class ValidationError extends CallciumError {
|
|
|
106
115
|
super("VALIDATION_ERROR", issues[0]?.message ?? "Policy validation failed");
|
|
107
116
|
this.name = "ValidationError";
|
|
108
117
|
this.issues = issues;
|
|
109
|
-
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
120
|
|
|
@@ -126,7 +134,6 @@ export class PolicyViolationError extends Error {
|
|
|
126
134
|
super(formatDiagnostic(violations[0]));
|
|
127
135
|
this.name = "PolicyViolationError";
|
|
128
136
|
this.violations = violations;
|
|
129
|
-
Object.setPrototypeOf(this, PolicyViolationError.prototype);
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
///////////////////////////////////////////////////////////////////////////
|
|
4
4
|
|
|
5
5
|
export { PolicyBuilder } from "./policy-builder";
|
|
6
|
-
export { PolicyCoder
|
|
6
|
+
export { PolicyCoder } from "./policy-coder";
|
|
7
|
+
export { isQuantifier, lookupQuantifier, parsePathSteps, Quantifier } from "./path";
|
|
8
|
+
export type { QuantifierInfo } from "./path";
|
|
7
9
|
export { isOpAllowed, PolicyValidator } from "./policy-validator";
|
|
8
10
|
export {
|
|
9
11
|
arg,
|
|
@@ -41,29 +43,17 @@ export {
|
|
|
41
43
|
Op,
|
|
42
44
|
TypeCode,
|
|
43
45
|
PolicyFormat,
|
|
44
|
-
Quantifier,
|
|
45
46
|
Scope,
|
|
46
47
|
ContextProperty,
|
|
47
|
-
Limits,
|
|
48
48
|
MAX_CONTEXT_PROPERTY_ID,
|
|
49
|
+
findContextProperty,
|
|
49
50
|
lookupOp,
|
|
50
51
|
lookupScope,
|
|
51
52
|
lookupContextProperty,
|
|
52
|
-
lookupQuantifier,
|
|
53
|
-
lookupTypeCode,
|
|
54
|
-
isQuantifier,
|
|
55
|
-
} from "./constants";
|
|
56
|
-
export { isLengthOp } from "./operators";
|
|
57
|
-
export type {
|
|
58
|
-
Operands,
|
|
59
|
-
OpInfo,
|
|
60
|
-
ScopeInfo,
|
|
61
|
-
ContextPropertyInfo,
|
|
62
|
-
QuantifierInfo,
|
|
63
|
-
TypeCodeInfo,
|
|
64
|
-
TypeClassInfo,
|
|
65
|
-
TypeClass,
|
|
66
53
|
} from "./constants";
|
|
54
|
+
export { isLengthOp, lookupTypeCode } from "./operators";
|
|
55
|
+
export type { TypeCodeInfo, TypeClassInfo, TypeClass } from "./operators";
|
|
56
|
+
export type { Operands, OpInfo, ScopeInfo, ContextPropertyInfo } from "./constants";
|
|
67
57
|
|
|
68
58
|
///////////////////////////////////////////////////////////////////////////
|
|
69
59
|
// Types
|
|
@@ -81,6 +71,7 @@ export type {
|
|
|
81
71
|
DecodedRule,
|
|
82
72
|
DecodedParam,
|
|
83
73
|
Issue,
|
|
74
|
+
IssueCode,
|
|
84
75
|
IssueSeverity,
|
|
85
76
|
IssueCategory,
|
|
86
77
|
Context,
|
package/src/operators.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hexToBytes } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, formatCode, Op, PolicyFormat as PF, TypeCode, operandsOf } from "./constants";
|
|
2
3
|
import { CallciumError } from "./errors";
|
|
3
4
|
|
|
5
|
+
import type { Hex } from "./types";
|
|
6
|
+
|
|
4
7
|
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
5
8
|
export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
|
|
6
9
|
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
@@ -28,7 +31,7 @@ export function isLeftAligned(typeCode: number): boolean {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type
|
|
34
|
+
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type.
|
|
32
35
|
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
33
36
|
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
34
37
|
* comparison use the canonical ABI value rather than the raw bytes.
|
|
@@ -103,14 +106,14 @@ function compareTyped(a: bigint, b: bigint, typeCode: number): number {
|
|
|
103
106
|
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
104
107
|
function isIn(value: bigint, data: Uint8Array): boolean {
|
|
105
108
|
const count = data.length / 32;
|
|
106
|
-
let
|
|
107
|
-
let
|
|
108
|
-
while (
|
|
109
|
-
const mid = (
|
|
109
|
+
let low = 0;
|
|
110
|
+
let high = count - 1;
|
|
111
|
+
while (low <= high) {
|
|
112
|
+
const mid = (low + high) >>> 1;
|
|
110
113
|
const elem = toBigInt(data, mid * 32);
|
|
111
114
|
if (value === elem) return true;
|
|
112
|
-
if (value < elem)
|
|
113
|
-
else
|
|
115
|
+
if (value < elem) high = mid - 1;
|
|
116
|
+
else low = mid + 1;
|
|
114
117
|
}
|
|
115
118
|
return false;
|
|
116
119
|
}
|
|
@@ -128,7 +131,7 @@ function isIn(value: bigint, data: Uint8Array): boolean {
|
|
|
128
131
|
export function applyOperator(
|
|
129
132
|
opCode: number,
|
|
130
133
|
value: bigint,
|
|
131
|
-
valueLength:
|
|
134
|
+
valueLength: bigint,
|
|
132
135
|
operandData: Uint8Array,
|
|
133
136
|
typeCode: number,
|
|
134
137
|
): boolean {
|
|
@@ -138,7 +141,7 @@ export function applyOperator(
|
|
|
138
141
|
// value and the opcode maps onto its EQ/GT/LT/GTE/LTE/BETWEEN twin. Counts are non-negative,
|
|
139
142
|
// so the comparison is always unsigned — force an unsigned type code regardless of the target.
|
|
140
143
|
if (isLengthOp(base)) {
|
|
141
|
-
value =
|
|
144
|
+
value = valueLength;
|
|
142
145
|
typeCode = TypeCode.UINT_MAX;
|
|
143
146
|
base = lengthToValueOp(base);
|
|
144
147
|
}
|
|
@@ -196,9 +199,142 @@ export function applyOperator(
|
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
default:
|
|
199
|
-
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code
|
|
202
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code ${formatCode(base, PF.RULE_OPCODE_SIZE)}`);
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
const negate = (opCode & Op.NOT) !== 0;
|
|
203
206
|
return negate ? !result : result;
|
|
204
207
|
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check whether an operator's data payload has the correct length.
|
|
211
|
+
* @param opBase - Base operator code with the NOT flag stripped.
|
|
212
|
+
* @param dataLength - Byte length of the operator's data payload.
|
|
213
|
+
* @returns True if the data length is valid for the given operator.
|
|
214
|
+
*/
|
|
215
|
+
export function isValidOperatorData(opBase: number, dataLength: number): boolean {
|
|
216
|
+
const operands = operandsOf(opBase);
|
|
217
|
+
if (operands === "single") return dataLength === 32;
|
|
218
|
+
if (operands === "range") return dataLength === 64;
|
|
219
|
+
if (operands === "variadic") return dataLength > 0 && dataLength % 32 === 0;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read an operator's encoding as bytes: one opcode byte followed by its data payload.
|
|
225
|
+
* @param operator - 0x-prefixed hex string holding a single operator.
|
|
226
|
+
* @returns The operator's bytes, opcode first.
|
|
227
|
+
* @throws {CallciumError} If the hex is malformed or carries no opcode byte.
|
|
228
|
+
*/
|
|
229
|
+
export function toOperatorBytes(operator: Hex): Uint8Array {
|
|
230
|
+
const bytes = hexToBytes(operator);
|
|
231
|
+
if (bytes.length < 1) {
|
|
232
|
+
throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
|
|
233
|
+
}
|
|
234
|
+
return bytes;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
///////////////////////////////////////////////////////////////////////////
|
|
238
|
+
// Type code classification
|
|
239
|
+
///////////////////////////////////////////////////////////////////////////
|
|
240
|
+
|
|
241
|
+
/** Structural category for a descriptor type code. */
|
|
242
|
+
export type TypeClass = "elementary" | "tuple" | "staticArray" | "dynamicArray";
|
|
243
|
+
|
|
244
|
+
/** Structural classification without label. */
|
|
245
|
+
export type TypeClassInfo = { typeClass: TypeClass; isDynamic: boolean };
|
|
246
|
+
|
|
247
|
+
/** Display metadata for a descriptor type code. */
|
|
248
|
+
export type TypeCodeInfo = TypeClassInfo & { label: string };
|
|
249
|
+
|
|
250
|
+
/** Throw an UNKNOWN_TYPE_CODE error. */
|
|
251
|
+
function unknownTypeCode(code: number): never {
|
|
252
|
+
throw new CallciumError("UNKNOWN_TYPE_CODE", `Unknown type code ${formatCode(code, DF.TYPECODE_SIZE)}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Pre-allocated constant objects for fixed type codes (avoids per-call allocation).
|
|
256
|
+
const ELEMENTARY = { typeClass: "elementary", isDynamic: false } as const;
|
|
257
|
+
const ELEMENTARY_DYNAMIC = { typeClass: "elementary", isDynamic: true } as const;
|
|
258
|
+
const STATIC_ARR = { typeClass: "staticArray", isDynamic: false } as const;
|
|
259
|
+
const DYNAMIC_ARR = { typeClass: "dynamicArray", isDynamic: true } as const;
|
|
260
|
+
const TUPLE = { typeClass: "tuple", isDynamic: false } as const;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Classify a type code into its structural category and dynamism.
|
|
264
|
+
* Lightweight variant of `lookupTypeCode` that skips label computation.
|
|
265
|
+
* @param code - A single-byte descriptor type code.
|
|
266
|
+
* @returns Type class and whether the type is ABI-dynamic.
|
|
267
|
+
* @throws {CallciumError} If the code is not a recognised type code.
|
|
268
|
+
*/
|
|
269
|
+
export function classifyTypeCode(code: number): TypeClassInfo {
|
|
270
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return ELEMENTARY;
|
|
271
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return ELEMENTARY;
|
|
272
|
+
if (code === TypeCode.ADDRESS || code === TypeCode.BOOL || code === TypeCode.FUNCTION) return ELEMENTARY;
|
|
273
|
+
if (code >= 0x44 && code <= 0x4f) unknownTypeCode(code);
|
|
274
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX) return ELEMENTARY;
|
|
275
|
+
if (code === TypeCode.BYTES || code === TypeCode.STRING) return ELEMENTARY_DYNAMIC;
|
|
276
|
+
if (code >= 0x72 && code <= 0x7f) unknownTypeCode(code);
|
|
277
|
+
if (code === TypeCode.STATIC_ARRAY) return STATIC_ARR;
|
|
278
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return DYNAMIC_ARR;
|
|
279
|
+
if (code >= 0x82 && code <= 0x8f) unknownTypeCode(code);
|
|
280
|
+
if (code === TypeCode.TUPLE) return TUPLE;
|
|
281
|
+
unknownTypeCode(code);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Determine whether a type code names an elementary type.
|
|
286
|
+
* @param code - A single-byte descriptor type code.
|
|
287
|
+
* @returns True for an elementary code; false for a composite or unassigned one.
|
|
288
|
+
*/
|
|
289
|
+
export function isElementary(code: number): boolean {
|
|
290
|
+
try {
|
|
291
|
+
// An undefined code is reported by a throw, and names no elementary type either way.
|
|
292
|
+
return classifyTypeCode(code).typeClass === "elementary";
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Determine whether a type code names a value an operator can read: a scalar word or a declared
|
|
300
|
+
* length. Tuples, static arrays, and undefined codes address nothing.
|
|
301
|
+
* @param code - A single-byte descriptor type code.
|
|
302
|
+
* @returns True when an operator can be applied to a target of this type.
|
|
303
|
+
*/
|
|
304
|
+
export function isAddressableTarget(code: number): boolean {
|
|
305
|
+
let info: TypeClassInfo;
|
|
306
|
+
try {
|
|
307
|
+
// An undefined code is reported by a throw, and addresses nothing either way.
|
|
308
|
+
info = classifyTypeCode(code);
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return info.typeClass === "elementary" || info.typeClass === "dynamicArray";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Compute the ABI type label for a type code. */
|
|
316
|
+
function typeCodeLabel(code: number): string {
|
|
317
|
+
if (code >= TypeCode.UINT_MIN && code <= TypeCode.UINT_MAX) return `uint${(code - TypeCode.UINT_MIN + 1) * 8}`;
|
|
318
|
+
if (code >= TypeCode.INT_MIN && code <= TypeCode.INT_MAX) return `int${(code - TypeCode.INT_MIN + 1) * 8}`;
|
|
319
|
+
if (code === TypeCode.ADDRESS) return "address";
|
|
320
|
+
if (code === TypeCode.BOOL) return "bool";
|
|
321
|
+
if (code === TypeCode.FUNCTION) return "function";
|
|
322
|
+
if (code >= TypeCode.FIXED_BYTES_MIN && code <= TypeCode.FIXED_BYTES_MAX)
|
|
323
|
+
return `bytes${code - TypeCode.FIXED_BYTES_MIN + 1}`;
|
|
324
|
+
if (code === TypeCode.BYTES) return "bytes";
|
|
325
|
+
if (code === TypeCode.STRING) return "string";
|
|
326
|
+
if (code === TypeCode.STATIC_ARRAY) return "T[k]";
|
|
327
|
+
if (code === TypeCode.DYNAMIC_ARRAY) return "T[]";
|
|
328
|
+
if (code === TypeCode.TUPLE) return "tuple";
|
|
329
|
+
return `0x${code.toString(16).padStart(2, "0")}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Map a raw type code byte to its ABI type label, structural category, and dynamism.
|
|
334
|
+
* @param code - A single-byte descriptor type code.
|
|
335
|
+
* @returns Label, 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 lookupTypeCode(code: number): TypeCodeInfo {
|
|
339
|
+
return { label: typeCodeLabel(code), ...classifyTypeCode(code) };
|
|
340
|
+
}
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes, readU16, writeBE16 } from "./bytes";
|
|
2
|
+
import { buildCodeMap, formatCode, PolicyFormat as PF } from "./constants";
|
|
3
|
+
import { CallciumError } from "./errors";
|
|
4
|
+
|
|
5
|
+
import type { Hex } from "./types";
|
|
6
|
+
|
|
7
|
+
///////////////////////////////////////////////////////////////////////////
|
|
8
|
+
// Quantifier steps
|
|
9
|
+
///////////////////////////////////////////////////////////////////////////
|
|
10
|
+
|
|
11
|
+
const QUANTIFIER_TABLE = [
|
|
12
|
+
{ key: "ALL", code: 0xffff, label: "all" },
|
|
13
|
+
{ key: "ANY", code: 0xfffe, label: "any" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** Reserved path step values that trigger quantified evaluation over array elements. */
|
|
17
|
+
export const Quantifier = buildCodeMap(QUANTIFIER_TABLE);
|
|
18
|
+
|
|
19
|
+
/** Display metadata for a quantifier step. */
|
|
20
|
+
export type QuantifierInfo = { label: string };
|
|
21
|
+
|
|
22
|
+
const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
|
|
23
|
+
QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** Check whether a path step is a quantifier (ALL or ANY). */
|
|
27
|
+
export function isQuantifier(step: number): boolean {
|
|
28
|
+
return step >= Quantifier.ANY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map a quantifier path step to its display label.
|
|
33
|
+
* @param code - Quantifier step value.
|
|
34
|
+
* @returns Display metadata for the quantifier.
|
|
35
|
+
* @throws {CallciumError} If the code is not a recognised quantifier.
|
|
36
|
+
*/
|
|
37
|
+
export function lookupQuantifier(code: number): QuantifierInfo {
|
|
38
|
+
const info = quantifierByCode.get(code);
|
|
39
|
+
if (!info)
|
|
40
|
+
throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier step ${formatCode(code, PF.PATH_STEP_SIZE)}`);
|
|
41
|
+
return info;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
///////////////////////////////////////////////////////////////////////////
|
|
45
|
+
// Path steps
|
|
46
|
+
///////////////////////////////////////////////////////////////////////////
|
|
47
|
+
|
|
48
|
+
/**
|
|
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. A wrapped step would
|
|
51
|
+
* address a different argument.
|
|
52
|
+
*/
|
|
53
|
+
export function encodePath(steps: readonly number[]): Hex {
|
|
54
|
+
const buffer = new Uint8Array(steps.length * PF.PATH_STEP_SIZE);
|
|
55
|
+
for (let i = 0; i < steps.length; i++) {
|
|
56
|
+
const step = steps[i]!;
|
|
57
|
+
if (!Number.isInteger(step) || step < 0 || step > PF.PATH_STEP_MAX) {
|
|
58
|
+
throw new CallciumError("PATH_STEP_OVERFLOW", `Path step ${step} is outside the path step field`);
|
|
59
|
+
}
|
|
60
|
+
writeBE16(buffer, i * PF.PATH_STEP_SIZE, step);
|
|
61
|
+
}
|
|
62
|
+
return bytesToHex(buffer);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a BE16-encoded hex path into an array of step values.
|
|
67
|
+
* @param path - 0x-prefixed hex string containing BE16-encoded path steps.
|
|
68
|
+
* @returns Array of numeric step values.
|
|
69
|
+
*/
|
|
70
|
+
export function parsePathSteps(path: Hex): number[] {
|
|
71
|
+
const bytes = hexToBytes(path);
|
|
72
|
+
if (bytes.length % PF.PATH_STEP_SIZE !== 0) {
|
|
73
|
+
throw new CallciumError("MALFORMED_PATH", `Path byte length ${bytes.length} is not a whole number of steps`);
|
|
74
|
+
}
|
|
75
|
+
const steps: number[] = [];
|
|
76
|
+
for (let offset = 0; offset < bytes.length; offset += PF.PATH_STEP_SIZE) {
|
|
77
|
+
steps.push(readU16(bytes, offset));
|
|
78
|
+
}
|
|
79
|
+
return steps;
|
|
80
|
+
}
|