@callcium/sdk 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +136 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +871 -1411
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bytes.ts +100 -0
- package/src/calldata-reader.ts +38 -0
- package/src/constants.ts +395 -0
- package/src/constraint.ts +313 -0
- package/src/descriptor-coder.ts +461 -0
- package/src/descriptor.ts +399 -0
- package/src/errors.ts +138 -0
- package/src/index.ts +105 -0
- package/src/operators.ts +204 -0
- package/src/policy-builder.ts +250 -0
- package/src/policy-coder.ts +668 -0
- package/src/policy-enforcer.ts +600 -0
- package/src/policy-validator.ts +1112 -0
- package/src/signature.ts +97 -0
- package/src/type-desc.ts +205 -0
- package/src/types.ts +280 -0
- package/src/validation-issue.ts +569 -0
package/src/operators.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Op, TypeCode } from "./constants";
|
|
2
|
+
import { CallciumError } from "./errors";
|
|
3
|
+
|
|
4
|
+
/** Read a big-endian 256-bit unsigned integer from a 32-byte window. */
|
|
5
|
+
export function toBigInt(bytes: Uint8Array, offset = 0): bigint {
|
|
6
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 32);
|
|
7
|
+
return (
|
|
8
|
+
(view.getBigUint64(0, false) << 192n) |
|
|
9
|
+
(view.getBigUint64(8, false) << 128n) |
|
|
10
|
+
(view.getBigUint64(16, false) << 64n) |
|
|
11
|
+
view.getBigUint64(24, false)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Return true when the type code represents a signed integer (int8 through int256). */
|
|
16
|
+
export function isSigned(typeCode: number): boolean {
|
|
17
|
+
return typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return true when the type code represents a left-aligned type (fixed bytes or function),
|
|
22
|
+
* whose value occupies the high bytes of the word with zero padding below.
|
|
23
|
+
*/
|
|
24
|
+
export function isLeftAligned(typeCode: number): boolean {
|
|
25
|
+
return (
|
|
26
|
+
(typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) || typeCode === TypeCode.FUNCTION
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonicalize a raw 256-bit calldata word to its ABI value for the declared type (Policy Spec §7.8).
|
|
32
|
+
* A scalar loaded from untrusted calldata may carry bits outside the declared width; masking
|
|
33
|
+
* unsigned/address/bool/function/bytesN to width and sign-extending signed integers makes the
|
|
34
|
+
* comparison use the canonical ABI value rather than the raw bytes.
|
|
35
|
+
* @param value - The raw 256-bit word loaded from calldata.
|
|
36
|
+
* @param typeCode - The declared type code of the value.
|
|
37
|
+
* @returns The canonicalized 256-bit value.
|
|
38
|
+
*/
|
|
39
|
+
export function canonicalize(value: bigint, typeCode: number): bigint {
|
|
40
|
+
// Unsigned integers: mask to the low N bits.
|
|
41
|
+
if (typeCode >= TypeCode.UINT_MIN && typeCode <= TypeCode.UINT_MAX) {
|
|
42
|
+
const bits = BigInt(typeCode * 8);
|
|
43
|
+
return bits === 256n ? value : value & ((1n << bits) - 1n);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Signed integers: sign-extend from the type's most-significant byte.
|
|
47
|
+
if (typeCode >= TypeCode.INT_MIN && typeCode <= TypeCode.INT_MAX) {
|
|
48
|
+
const bits = (typeCode - TypeCode.INT_MIN + 1) * 8;
|
|
49
|
+
return bits === 256 ? value : BigInt.asUintN(256, BigInt.asIntN(bits, value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Address: mask to the low 160 bits.
|
|
53
|
+
if (typeCode === TypeCode.ADDRESS) return value & ((1n << 160n) - 1n);
|
|
54
|
+
|
|
55
|
+
// Boolean: collapse to the low bit.
|
|
56
|
+
if (typeCode === TypeCode.BOOL) return value & 1n;
|
|
57
|
+
|
|
58
|
+
// Function pointer: encoded identical to bytes24 (left-aligned), clear the low 8 padding bytes.
|
|
59
|
+
if (typeCode === TypeCode.FUNCTION) return (value >> 64n) << 64n;
|
|
60
|
+
|
|
61
|
+
// Fixed bytes: left-aligned, clear the low (32 - N) padding bytes.
|
|
62
|
+
if (typeCode >= TypeCode.FIXED_BYTES_MIN && typeCode <= TypeCode.FIXED_BYTES_MAX) {
|
|
63
|
+
const n = typeCode - TypeCode.FIXED_BYTES_MIN + 1;
|
|
64
|
+
if (n === 32) return value;
|
|
65
|
+
const padBits = BigInt((32 - n) * 8);
|
|
66
|
+
return (value >> padBits) << padBits;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Return true when the operator code (with or without NOT flag) is a LENGTH_* variant. */
|
|
73
|
+
export function isLengthOp(opCode: number): boolean {
|
|
74
|
+
const base = opCode & ~Op.NOT;
|
|
75
|
+
return base >= Op.LENGTH_EQ && base <= Op.LENGTH_BETWEEN;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Map a LENGTH_* opcode base to its core value-comparison twin. */
|
|
79
|
+
function lengthToValueOp(base: number): number {
|
|
80
|
+
if (base === Op.LENGTH_EQ) return Op.EQ;
|
|
81
|
+
if (base === Op.LENGTH_GT) return Op.GT;
|
|
82
|
+
if (base === Op.LENGTH_LT) return Op.LT;
|
|
83
|
+
if (base === Op.LENGTH_GTE) return Op.GTE;
|
|
84
|
+
if (base === Op.LENGTH_LTE) return Op.LTE;
|
|
85
|
+
return Op.BETWEEN;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Return true when LENGTH_* operators are valid for the given type code (bytes, string, or dynamic array). */
|
|
89
|
+
export function isLengthValidType(typeCode: number): boolean {
|
|
90
|
+
return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING || typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Compare two 256-bit values, using two's complement interpretation for signed type codes. */
|
|
94
|
+
function compareTyped(a: bigint, b: bigint, typeCode: number): number {
|
|
95
|
+
if (isSigned(typeCode)) {
|
|
96
|
+
const sa = BigInt.asIntN(256, a);
|
|
97
|
+
const sb = BigInt.asIntN(256, b);
|
|
98
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
99
|
+
}
|
|
100
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Binary search over a sorted array of 32-byte words. Comparison is always unsigned. */
|
|
104
|
+
function isIn(value: bigint, data: Uint8Array): boolean {
|
|
105
|
+
const count = data.length / 32;
|
|
106
|
+
let lo = 0;
|
|
107
|
+
let hi = count - 1;
|
|
108
|
+
while (lo <= hi) {
|
|
109
|
+
const mid = (lo + hi) >>> 1;
|
|
110
|
+
const elem = toBigInt(data, mid * 32);
|
|
111
|
+
if (value === elem) return true;
|
|
112
|
+
if (value < elem) hi = mid - 1;
|
|
113
|
+
else lo = mid + 1;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply a single operator against a value and operand data.
|
|
120
|
+
* Handles the NOT flag internally by inverting the base operator result.
|
|
121
|
+
* @param opCode - Operator code, potentially OR'd with the NOT flag.
|
|
122
|
+
* @param value - The 256-bit value to test (ignored for LENGTH_* operators).
|
|
123
|
+
* @param valueLength - Runtime byte or element count, only meaningful for LENGTH_* operators.
|
|
124
|
+
* @param operandData - Raw operand bytes (32 per operand, 64 for range operators).
|
|
125
|
+
* @param typeCode - ABI type code, used to select signed vs. unsigned comparison.
|
|
126
|
+
* @returns Whether the value satisfies the operator.
|
|
127
|
+
*/
|
|
128
|
+
export function applyOperator(
|
|
129
|
+
opCode: number,
|
|
130
|
+
value: bigint,
|
|
131
|
+
valueLength: number,
|
|
132
|
+
operandData: Uint8Array,
|
|
133
|
+
typeCode: number,
|
|
134
|
+
): boolean {
|
|
135
|
+
let base = opCode & ~Op.NOT;
|
|
136
|
+
|
|
137
|
+
// Length operators reuse the value-comparison core: the runtime count becomes the compared
|
|
138
|
+
// value and the opcode maps onto its EQ/GT/LT/GTE/LTE/BETWEEN twin. Counts are non-negative,
|
|
139
|
+
// so the comparison is always unsigned — force an unsigned type code regardless of the target.
|
|
140
|
+
if (isLengthOp(base)) {
|
|
141
|
+
value = BigInt(valueLength);
|
|
142
|
+
typeCode = TypeCode.UINT_MAX;
|
|
143
|
+
base = lengthToValueOp(base);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let result: boolean;
|
|
147
|
+
|
|
148
|
+
switch (base) {
|
|
149
|
+
case Op.EQ:
|
|
150
|
+
result = value === toBigInt(operandData, 0);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case Op.GT:
|
|
154
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) > 0;
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case Op.LT:
|
|
158
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) < 0;
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case Op.GTE:
|
|
162
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) >= 0;
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case Op.LTE:
|
|
166
|
+
result = compareTyped(value, toBigInt(operandData, 0), typeCode) <= 0;
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case Op.BETWEEN: {
|
|
170
|
+
const min = toBigInt(operandData, 0);
|
|
171
|
+
const max = toBigInt(operandData, 32);
|
|
172
|
+
result = compareTyped(value, min, typeCode) >= 0 && compareTyped(value, max, typeCode) <= 0;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case Op.IN:
|
|
177
|
+
result = isIn(value, operandData);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case Op.BITMASK_ALL: {
|
|
181
|
+
const mask = toBigInt(operandData, 0);
|
|
182
|
+
result = (value & mask) === mask;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case Op.BITMASK_ANY: {
|
|
187
|
+
const mask = toBigInt(operandData, 0);
|
|
188
|
+
result = (value & mask) !== 0n;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case Op.BITMASK_NONE: {
|
|
193
|
+
const mask = toBigInt(operandData, 0);
|
|
194
|
+
result = (value & mask) === 0n;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
default:
|
|
199
|
+
throw new CallciumError("UNKNOWN_OPERATOR", `Unknown operator code 0x${base.toString(16).padStart(2, "0")}.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const negate = (opCode & Op.NOT) !== 0;
|
|
203
|
+
return negate ? !result : result;
|
|
204
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { bytesToHex } from "./bytes";
|
|
2
|
+
import { Scope, TypeCode, isQuantifier, MAX_CONTEXT_PROPERTY_ID } from "./constants";
|
|
3
|
+
import { ConstraintBuilder } from "./constraint";
|
|
4
|
+
import { Descriptor } from "./descriptor";
|
|
5
|
+
import { DescriptorCoder } from "./descriptor-coder";
|
|
6
|
+
import { CallciumError, ValidationError } from "./errors";
|
|
7
|
+
import { PolicyCoder, parsePathSteps } from "./policy-coder";
|
|
8
|
+
import { PolicyValidator } from "./policy-validator";
|
|
9
|
+
import { SignatureParser } from "./signature";
|
|
10
|
+
|
|
11
|
+
import type { Constraint, Hex, Issue, PolicyData } from "./types";
|
|
12
|
+
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////
|
|
14
|
+
// Internal types
|
|
15
|
+
///////////////////////////////////////////////////////////////////////////
|
|
16
|
+
|
|
17
|
+
type PolicyDraft = {
|
|
18
|
+
isSelectorless: boolean;
|
|
19
|
+
selector: Hex;
|
|
20
|
+
descriptor: Uint8Array;
|
|
21
|
+
groups: Constraint[][];
|
|
22
|
+
pathHashes: Set<string>[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
///////////////////////////////////////////////////////////////////////////
|
|
26
|
+
// Path validation
|
|
27
|
+
///////////////////////////////////////////////////////////////////////////
|
|
28
|
+
|
|
29
|
+
/** Validate a context-scope path. */
|
|
30
|
+
function validateContextPath(steps: number[]): void {
|
|
31
|
+
if (steps.length !== 1) {
|
|
32
|
+
throw new CallciumError("INVALID_CONTEXT_PATH", "Context-scope path must be exactly one step.");
|
|
33
|
+
}
|
|
34
|
+
const step = steps[0]!;
|
|
35
|
+
if (step > MAX_CONTEXT_PROPERTY_ID) {
|
|
36
|
+
throw new CallciumError(
|
|
37
|
+
"UNKNOWN_CONTEXT_PROPERTY",
|
|
38
|
+
`Unknown context property ID 0x${step.toString(16).padStart(4, "0")}.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Validate a calldata-scope path against the descriptor. */
|
|
44
|
+
function validateCalldataPath(steps: number[], desc: Uint8Array): void {
|
|
45
|
+
const argIndex = steps[0]!;
|
|
46
|
+
const paramCount = Descriptor.paramCount(desc);
|
|
47
|
+
if (argIndex >= paramCount) {
|
|
48
|
+
throw new CallciumError(
|
|
49
|
+
"PARAM_INDEX_OUT_OF_BOUNDS",
|
|
50
|
+
`Param index ${argIndex} out of range (descriptor has ${paramCount} params).`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let offset = Descriptor.paramOffset(desc, argIndex);
|
|
55
|
+
let hasQuantifier = false;
|
|
56
|
+
|
|
57
|
+
for (let i = 1; i < steps.length; i++) {
|
|
58
|
+
const step = steps[i]!;
|
|
59
|
+
const info = Descriptor.inspect(desc, offset);
|
|
60
|
+
const isArray = info.typeCode === TypeCode.STATIC_ARRAY || info.typeCode === TypeCode.DYNAMIC_ARRAY;
|
|
61
|
+
|
|
62
|
+
if (isQuantifier(step)) {
|
|
63
|
+
if (!isArray) {
|
|
64
|
+
throw new CallciumError("QUANTIFIER_ON_NON_ARRAY", "Quantifier step is only valid on an array node.");
|
|
65
|
+
}
|
|
66
|
+
if (hasQuantifier) {
|
|
67
|
+
throw new CallciumError("NESTED_QUANTIFIER", "Nested quantifiers are not allowed.");
|
|
68
|
+
}
|
|
69
|
+
hasQuantifier = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (info.typeCode === TypeCode.TUPLE) {
|
|
73
|
+
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
74
|
+
if (step >= fieldCount) {
|
|
75
|
+
throw new CallciumError(
|
|
76
|
+
"TUPLE_FIELD_OUT_OF_BOUNDS",
|
|
77
|
+
`Tuple field index ${step} out of range (tuple has ${fieldCount} fields).`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
offset = Descriptor.tupleFieldOffset(desc, offset, step);
|
|
81
|
+
} else if (isArray) {
|
|
82
|
+
if (!isQuantifier(step) && info.typeCode === TypeCode.STATIC_ARRAY) {
|
|
83
|
+
const arrayLength = Descriptor.staticArrayLength(desc, offset);
|
|
84
|
+
if (step >= arrayLength) {
|
|
85
|
+
throw new CallciumError(
|
|
86
|
+
"STATIC_ARRAY_INDEX_OUT_OF_BOUNDS",
|
|
87
|
+
`Array index ${step} out of range (static array has ${arrayLength} elements).`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
offset = Descriptor.arrayElementOffset(offset);
|
|
92
|
+
} else {
|
|
93
|
+
throw new CallciumError("NOT_COMPOSITE", "Cannot descend into an elementary type.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
///////////////////////////////////////////////////////////////////////////
|
|
99
|
+
// PolicyBuilder
|
|
100
|
+
///////////////////////////////////////////////////////////////////////////
|
|
101
|
+
|
|
102
|
+
/** Fluent builder for constructing Callcium policies. */
|
|
103
|
+
export class PolicyBuilder {
|
|
104
|
+
private draft: PolicyDraft;
|
|
105
|
+
|
|
106
|
+
private constructor(draft: PolicyDraft) {
|
|
107
|
+
this.draft = draft;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a builder from a function signature.
|
|
112
|
+
* @param signature - ABI function signature, e.g. `"transfer(address,uint256)"`.
|
|
113
|
+
*/
|
|
114
|
+
static create(signature: string): PolicyBuilder {
|
|
115
|
+
const parsed = SignatureParser.parse(signature);
|
|
116
|
+
const descriptor = DescriptorCoder.fromTypes(parsed.types);
|
|
117
|
+
return new PolicyBuilder({
|
|
118
|
+
isSelectorless: false,
|
|
119
|
+
selector: parsed.selector,
|
|
120
|
+
descriptor,
|
|
121
|
+
groups: [[]],
|
|
122
|
+
pathHashes: [new Set()],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a selectorless builder from a raw type string.
|
|
128
|
+
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256"`.
|
|
129
|
+
*/
|
|
130
|
+
static createRaw(typesCsv: string): PolicyBuilder {
|
|
131
|
+
const descriptor = DescriptorCoder.fromTypes(typesCsv);
|
|
132
|
+
return new PolicyBuilder({
|
|
133
|
+
isSelectorless: true,
|
|
134
|
+
selector: "0x00000000",
|
|
135
|
+
descriptor,
|
|
136
|
+
groups: [[]],
|
|
137
|
+
pathHashes: [new Set()],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Add a constraint to the current group.
|
|
143
|
+
* @param constraint - A `Constraint` object or a `ConstraintBuilder` instance.
|
|
144
|
+
*/
|
|
145
|
+
add(constraint: Constraint | ConstraintBuilder): this {
|
|
146
|
+
const c: Constraint = {
|
|
147
|
+
scope: constraint.scope,
|
|
148
|
+
path: constraint.path,
|
|
149
|
+
operators: [...constraint.operators],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (c.operators.length === 0) {
|
|
153
|
+
throw new CallciumError("NO_CONSTRAINT_OPERATORS", "Constraint must have at least one operator.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Path shape is established before the scope decides how to navigate it.
|
|
157
|
+
const steps = parsePathSteps(c.path);
|
|
158
|
+
if (steps.length === 0) {
|
|
159
|
+
throw new CallciumError("EMPTY_PATH", "Path must have at least one step.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (c.scope === Scope.CONTEXT) {
|
|
163
|
+
validateContextPath(steps);
|
|
164
|
+
} else if (c.scope === Scope.CALLDATA) {
|
|
165
|
+
validateCalldataPath(steps, this.draft.descriptor);
|
|
166
|
+
} else {
|
|
167
|
+
throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${c.scope}.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const key = `${c.scope}:${c.path.toLowerCase()}`;
|
|
171
|
+
const currentHashes = this.draft.pathHashes[this.draft.pathHashes.length - 1]!;
|
|
172
|
+
if (currentHashes.has(key)) {
|
|
173
|
+
throw new CallciumError("DUPLICATE_PATH_IN_GROUP", `Duplicate path ${c.path} in the same group.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
currentHashes.add(key);
|
|
177
|
+
this.draft.groups[this.draft.groups.length - 1]!.push(c);
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Start a new constraint group (OR branch). */
|
|
182
|
+
or(): this {
|
|
183
|
+
const lastGroup = this.draft.groups[this.draft.groups.length - 1]!;
|
|
184
|
+
if (lastGroup.length === 0) {
|
|
185
|
+
throw new CallciumError("EMPTY_GROUP", "Cannot start a new group when the current group is empty.");
|
|
186
|
+
}
|
|
187
|
+
this.draft.groups.push([]);
|
|
188
|
+
this.draft.pathHashes.push(new Set());
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build the policy into an encoded binary blob with strict validation.
|
|
194
|
+
* Throws on any issue, regardless of severity. Use {@link validate} to
|
|
195
|
+
* inspect issues, or {@link buildUnsafe} to encode without validation.
|
|
196
|
+
* @returns The policy as a 0x-prefixed hex string.
|
|
197
|
+
* @throws {CallciumError} If any group is empty.
|
|
198
|
+
* @throws {ValidationError} If validation finds any issue.
|
|
199
|
+
*/
|
|
200
|
+
build(): Hex {
|
|
201
|
+
this.checkGroups();
|
|
202
|
+
const policyData = this.toPolicyData();
|
|
203
|
+
const issues = PolicyValidator.validate(policyData);
|
|
204
|
+
if (issues.length > 0) {
|
|
205
|
+
throw new ValidationError(issues);
|
|
206
|
+
}
|
|
207
|
+
return PolicyCoder.encode(policyData);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build the policy into an encoded binary blob without validation.
|
|
212
|
+
* The resulting policy may be invalid. Prefer {@link build}; use this
|
|
213
|
+
* only to knowingly bypass a reported issue.
|
|
214
|
+
* @returns The policy as a 0x-prefixed hex string.
|
|
215
|
+
* @throws {CallciumError} If any group is empty.
|
|
216
|
+
*/
|
|
217
|
+
buildUnsafe(): Hex {
|
|
218
|
+
this.checkGroups();
|
|
219
|
+
return PolicyCoder.encode(this.toPolicyData());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate the policy without encoding.
|
|
224
|
+
* @returns All validation issues found.
|
|
225
|
+
*/
|
|
226
|
+
validate(): Issue[] {
|
|
227
|
+
this.checkGroups();
|
|
228
|
+
const policyData = this.toPolicyData();
|
|
229
|
+
return PolicyValidator.validate(policyData);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Throw if any group is empty. */
|
|
233
|
+
private checkGroups(): void {
|
|
234
|
+
for (let i = 0; i < this.draft.groups.length; i++) {
|
|
235
|
+
if (this.draft.groups[i]!.length === 0) {
|
|
236
|
+
throw new CallciumError("EMPTY_GROUP", `Group ${i} is empty.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Convert the draft to a PolicyData structure. */
|
|
242
|
+
private toPolicyData(): PolicyData {
|
|
243
|
+
return {
|
|
244
|
+
isSelectorless: this.draft.isSelectorless,
|
|
245
|
+
selector: this.draft.selector,
|
|
246
|
+
descriptor: bytesToHex(this.draft.descriptor),
|
|
247
|
+
groups: this.draft.groups,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|