@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/constraint.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import { bytesToHex,
|
|
2
|
-
import { Op, Scope, ContextProperty,
|
|
1
|
+
import { bytesToHex, hexToBytes, toAddress } from "./bytes";
|
|
2
|
+
import { PolicyFormat, Op, Scope, ContextProperty, lookupContextProperty } from "./constants";
|
|
3
3
|
import { CallciumError } from "./errors";
|
|
4
|
+
import { encodePath } from "./path";
|
|
4
5
|
|
|
5
|
-
import type { Hex, Constraint } from "./types";
|
|
6
|
+
import type { Address, Hex, Constraint } from "./types";
|
|
6
7
|
|
|
7
8
|
///////////////////////////////////////////////////////////////////////////
|
|
8
9
|
// Value encoding helpers
|
|
9
10
|
///////////////////////////////////////////////////////////////////////////
|
|
10
11
|
|
|
11
12
|
/** Accepted scalar value types for operator arguments. */
|
|
12
|
-
export type ScalarValue = bigint |
|
|
13
|
+
export type ScalarValue = bigint | boolean | string;
|
|
14
|
+
|
|
15
|
+
// Bounds of the integers a 32-byte operand word represents, spanning signed and unsigned targets.
|
|
16
|
+
const OPERAND_MIN = -(2n ** 255n);
|
|
17
|
+
const OPERAND_MAX = 2n ** 256n - 1n;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Require an operand within the integers a 32-byte word represents and return it.
|
|
21
|
+
* @throws {CallciumError} When the operand lies outside the word's range.
|
|
22
|
+
*/
|
|
23
|
+
function checkOperandRange(value: bigint): bigint {
|
|
24
|
+
if (value < OPERAND_MIN || value > OPERAND_MAX) {
|
|
25
|
+
throw new CallciumError("OPERAND_OVERFLOW", `Operand ${value} is outside the range a 32-byte word represents`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
13
29
|
|
|
14
30
|
/** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
|
|
15
31
|
function addressBody(value: string): string {
|
|
@@ -27,15 +43,12 @@ function encodeWord(value: ScalarValue): Uint8Array {
|
|
|
27
43
|
|
|
28
44
|
if (typeof value === "string") {
|
|
29
45
|
// Address encoding: validate 20-byte hex and right-align into 32 bytes.
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < 20; i++) {
|
|
32
|
-
word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
|
|
33
|
-
}
|
|
46
|
+
word.set(hexToBytes(addressBody(value)), 12);
|
|
34
47
|
return word;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
//
|
|
38
|
-
let bigValue =
|
|
50
|
+
// A negative operand occupies the word in two's complement, which is the signed target's encoding.
|
|
51
|
+
let bigValue = checkOperandRange(value);
|
|
39
52
|
for (let i = 31; i >= 0; i--) {
|
|
40
53
|
word[i] = Number(bigValue & 0xffn);
|
|
41
54
|
bigValue >>= 8n;
|
|
@@ -53,51 +66,47 @@ function singleOp(opCode: number, value: ScalarValue): Hex {
|
|
|
53
66
|
|
|
54
67
|
/** Pack a range operator (opCode byte + min word + max word). */
|
|
55
68
|
function rangeOp(opCode: number, min: bigint, max: bigint): Hex {
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
// Order compares the operands as written; the encoded words of a signed range run the other way.
|
|
70
|
+
const minValue = checkOperandRange(min);
|
|
71
|
+
const maxValue = checkOperandRange(max);
|
|
72
|
+
if (minValue > maxValue) {
|
|
73
|
+
throw new CallciumError("INVALID_RANGE", `Range min (${minValue}) must not exceed max (${maxValue})`);
|
|
58
74
|
}
|
|
59
75
|
const buffer = new Uint8Array(65);
|
|
60
76
|
buffer[0] = opCode;
|
|
61
|
-
buffer.set(encodeWord(
|
|
62
|
-
buffer.set(encodeWord(
|
|
77
|
+
buffer.set(encodeWord(minValue), 1);
|
|
78
|
+
buffer.set(encodeWord(maxValue), 33);
|
|
63
79
|
return bytesToHex(buffer);
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Require an assigned context property ID and return it as an operand word.
|
|
84
|
+
* @throws {CallciumError} When the ID names no assigned property.
|
|
85
|
+
*/
|
|
86
|
+
function contextOperand(contextPropertyId: number): bigint {
|
|
87
|
+
lookupContextProperty(contextPropertyId);
|
|
88
|
+
return BigInt(contextPropertyId);
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
/** Largest member count whose 32-byte words still fit a rule's data length field. */
|
|
75
|
-
export const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
|
|
76
|
-
|
|
77
91
|
/** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
|
|
78
92
|
function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
// PWF-21 orders members by their 32-byte encodings, so a negative operand and its unsigned
|
|
94
|
+
// alias are one member at one position. Normalising after the domain check keeps an
|
|
95
|
+
// out-of-range value an error rather than folding it into the word.
|
|
96
|
+
const words = values.map((value) => {
|
|
97
|
+
if (typeof value === "bigint") return BigInt.asUintN(256, checkOperandRange(value));
|
|
98
|
+
if (typeof value === "boolean") return value ? 1n : 0n;
|
|
83
99
|
// String address.
|
|
84
|
-
return BigInt("0x" + addressBody(
|
|
100
|
+
return BigInt("0x" + addressBody(value));
|
|
85
101
|
});
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const deduped: bigint[] = [];
|
|
90
|
-
for (const value of bigs) {
|
|
91
|
-
if (deduped.length === 0 || deduped[deduped.length - 1] !== value) {
|
|
92
|
-
deduped.push(value);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
103
|
+
const deduped = [...new Set(words)].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
95
104
|
|
|
96
105
|
if (deduped.length === 0) {
|
|
97
106
|
throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
|
|
98
107
|
}
|
|
99
|
-
if (deduped.length > MAX_SET_MEMBERS) {
|
|
100
|
-
throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
|
|
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`);
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
const buffer = new Uint8Array(1 + deduped.length * 32);
|
|
@@ -109,16 +118,28 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
|
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
///////////////////////////////////////////////////////////////////////////
|
|
112
|
-
//
|
|
121
|
+
// Operand provenance
|
|
113
122
|
///////////////////////////////////////////////////////////////////////////
|
|
114
123
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Extremes of the numeric operands a builder's value operators received, as written. Encoding folds
|
|
126
|
+
* a negative operand onto its unsigned alias, so the sign survives nowhere else.
|
|
127
|
+
*/
|
|
128
|
+
export type OperandExtremes = {
|
|
129
|
+
/** Most negative operand written, or zero when none was negative. */
|
|
130
|
+
leastNegative: bigint;
|
|
131
|
+
/** Greatest operand written, or zero when none exceeded it. */
|
|
132
|
+
greatest: bigint;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extremes of the numeric operands the value operators of `constraint` received, as written.
|
|
137
|
+
* They outlive edits to `operators`: removing an operator does not remove what was written to
|
|
138
|
+
* produce it, and a fresh builder starts at zero. Undefined for a plain `Constraint`, whose
|
|
139
|
+
* operators arrive already encoded.
|
|
140
|
+
*/
|
|
141
|
+
export function readOperandExtremes(constraint: Constraint | ConstraintBuilder): OperandExtremes | undefined {
|
|
142
|
+
return "operandExtremes" in constraint ? constraint.operandExtremes : undefined;
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -129,20 +150,36 @@ function encodePath(steps: readonly number[]): Hex {
|
|
|
129
150
|
* Mutable builder that accumulates operators targeting a single path.
|
|
130
151
|
* Implements the `Constraint` interface so it can be passed directly to policy builders.
|
|
131
152
|
*/
|
|
132
|
-
export class ConstraintBuilder implements Constraint {
|
|
153
|
+
export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implements Constraint {
|
|
133
154
|
readonly scope: number;
|
|
134
155
|
readonly path: Hex;
|
|
135
156
|
readonly operators: Hex[];
|
|
157
|
+
/**
|
|
158
|
+
* Extremes of the numeric operands the value operators received, as written. Only the extremes
|
|
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}.
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
readonly operandExtremes: OperandExtremes;
|
|
136
164
|
|
|
137
165
|
/** @internal */
|
|
138
166
|
constructor(scope: number, path: Hex) {
|
|
139
167
|
this.scope = scope;
|
|
140
168
|
this.path = path;
|
|
141
169
|
this.operators = [];
|
|
170
|
+
this.operandExtremes = { leastNegative: 0n, greatest: 0n };
|
|
142
171
|
}
|
|
143
172
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Push a pre-encoded operator hex string and return this for chaining.
|
|
175
|
+
* `values` are the operands as written; the numeric ones join the record.
|
|
176
|
+
*/
|
|
177
|
+
private push(opHex: Hex, values: readonly ScalarValue[] = []): this {
|
|
178
|
+
for (const value of values) {
|
|
179
|
+
if (typeof value !== "bigint") continue;
|
|
180
|
+
if (value < this.operandExtremes.leastNegative) this.operandExtremes.leastNegative = value;
|
|
181
|
+
if (value > this.operandExtremes.greatest) this.operandExtremes.greatest = value;
|
|
182
|
+
}
|
|
146
183
|
this.operators.push(opHex);
|
|
147
184
|
return this;
|
|
148
185
|
}
|
|
@@ -152,51 +189,51 @@ export class ConstraintBuilder implements Constraint {
|
|
|
152
189
|
///////////////////////////////////////////////////////////////////////////
|
|
153
190
|
|
|
154
191
|
/** Assert the value equals `value`. */
|
|
155
|
-
eq(value:
|
|
156
|
-
return this.push(singleOp(Op.EQ, value));
|
|
192
|
+
eq(value: Operand): this {
|
|
193
|
+
return this.push(singleOp(Op.EQ, value), [value]);
|
|
157
194
|
}
|
|
158
195
|
|
|
159
196
|
/** Assert the value does not equal `value`. */
|
|
160
|
-
neq(value:
|
|
161
|
-
return this.push(singleOp(Op.EQ | Op.NOT, value));
|
|
197
|
+
neq(value: Operand): this {
|
|
198
|
+
return this.push(singleOp(Op.EQ | Op.NOT, value), [value]);
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
/** Assert the value equals the context property `contextPropertyId`. */
|
|
165
202
|
eqCtx(contextPropertyId: number): this {
|
|
166
|
-
return this.push(singleOp(Op.EQ_CTX,
|
|
203
|
+
return this.push(singleOp(Op.EQ_CTX, contextOperand(contextPropertyId)));
|
|
167
204
|
}
|
|
168
205
|
|
|
169
206
|
/** Assert the value does not equal the context property `contextPropertyId`. */
|
|
170
207
|
neqCtx(contextPropertyId: number): this {
|
|
171
|
-
return this.push(singleOp(Op.EQ_CTX | Op.NOT,
|
|
208
|
+
return this.push(singleOp(Op.EQ_CTX | Op.NOT, contextOperand(contextPropertyId)));
|
|
172
209
|
}
|
|
173
210
|
|
|
174
211
|
/** Assert the value is greater than `bound`. */
|
|
175
|
-
gt(bound: bigint
|
|
176
|
-
return this.push(singleOp(Op.GT, bound));
|
|
212
|
+
gt(bound: bigint): this {
|
|
213
|
+
return this.push(singleOp(Op.GT, bound), [bound]);
|
|
177
214
|
}
|
|
178
215
|
|
|
179
216
|
/** Assert the value is less than `bound`. */
|
|
180
|
-
lt(bound: bigint
|
|
181
|
-
return this.push(singleOp(Op.LT, bound));
|
|
217
|
+
lt(bound: bigint): this {
|
|
218
|
+
return this.push(singleOp(Op.LT, bound), [bound]);
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
/** Assert the value is greater than or equal to `bound`. */
|
|
185
|
-
gte(bound: bigint
|
|
186
|
-
return this.push(singleOp(Op.GTE, bound));
|
|
222
|
+
gte(bound: bigint): this {
|
|
223
|
+
return this.push(singleOp(Op.GTE, bound), [bound]);
|
|
187
224
|
}
|
|
188
225
|
|
|
189
226
|
/** Assert the value is less than or equal to `bound`. */
|
|
190
|
-
lte(bound: bigint
|
|
191
|
-
return this.push(singleOp(Op.LTE, bound));
|
|
227
|
+
lte(bound: bigint): this {
|
|
228
|
+
return this.push(singleOp(Op.LTE, bound), [bound]);
|
|
192
229
|
}
|
|
193
230
|
|
|
194
231
|
/**
|
|
195
232
|
* Assert the value is within [min, max] inclusive.
|
|
196
233
|
* @throws {CallciumError} If min > max.
|
|
197
234
|
*/
|
|
198
|
-
between(min: bigint
|
|
199
|
-
return this.push(rangeOp(Op.BETWEEN,
|
|
235
|
+
between(min: bigint, max: bigint): this {
|
|
236
|
+
return this.push(rangeOp(Op.BETWEEN, min, max), [min, max]);
|
|
200
237
|
}
|
|
201
238
|
|
|
202
239
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -208,16 +245,16 @@ export class ConstraintBuilder implements Constraint {
|
|
|
208
245
|
* Values are sorted and deduplicated before encoding.
|
|
209
246
|
* @throws {CallciumError} If the set is empty after deduplication.
|
|
210
247
|
*/
|
|
211
|
-
isIn(values: readonly
|
|
212
|
-
return this.push(setOp(Op.IN, values));
|
|
248
|
+
isIn(values: readonly Operand[]): this {
|
|
249
|
+
return this.push(setOp(Op.IN, values), values);
|
|
213
250
|
}
|
|
214
251
|
|
|
215
252
|
/**
|
|
216
253
|
* Assert the value is not a member of the set.
|
|
217
254
|
* @throws {CallciumError} If the set is empty after deduplication.
|
|
218
255
|
*/
|
|
219
|
-
notIn(values: readonly
|
|
220
|
-
return this.push(setOp(Op.IN | Op.NOT, values));
|
|
256
|
+
notIn(values: readonly Operand[]): this {
|
|
257
|
+
return this.push(setOp(Op.IN | Op.NOT, values), values);
|
|
221
258
|
}
|
|
222
259
|
|
|
223
260
|
///////////////////////////////////////////////////////////////////////////
|
|
@@ -243,37 +280,37 @@ export class ConstraintBuilder implements Constraint {
|
|
|
243
280
|
// Length operators
|
|
244
281
|
///////////////////////////////////////////////////////////////////////////
|
|
245
282
|
|
|
246
|
-
/** Assert the runtime length equals `
|
|
247
|
-
lengthEq(
|
|
248
|
-
return this.push(singleOp(Op.LENGTH_EQ,
|
|
283
|
+
/** Assert the runtime length equals `length`. */
|
|
284
|
+
lengthEq(length: bigint): this {
|
|
285
|
+
return this.push(singleOp(Op.LENGTH_EQ, length));
|
|
249
286
|
}
|
|
250
287
|
|
|
251
|
-
/** Assert the runtime length is greater than `
|
|
252
|
-
lengthGt(
|
|
253
|
-
return this.push(singleOp(Op.LENGTH_GT,
|
|
288
|
+
/** Assert the runtime length is greater than `length`. */
|
|
289
|
+
lengthGt(length: bigint): this {
|
|
290
|
+
return this.push(singleOp(Op.LENGTH_GT, length));
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
/** Assert the runtime length is less than `
|
|
257
|
-
lengthLt(
|
|
258
|
-
return this.push(singleOp(Op.LENGTH_LT,
|
|
293
|
+
/** Assert the runtime length is less than `length`. */
|
|
294
|
+
lengthLt(length: bigint): this {
|
|
295
|
+
return this.push(singleOp(Op.LENGTH_LT, length));
|
|
259
296
|
}
|
|
260
297
|
|
|
261
|
-
/** Assert the runtime length is greater than or equal to `
|
|
262
|
-
lengthGte(
|
|
263
|
-
return this.push(singleOp(Op.LENGTH_GTE,
|
|
298
|
+
/** Assert the runtime length is greater than or equal to `length`. */
|
|
299
|
+
lengthGte(length: bigint): this {
|
|
300
|
+
return this.push(singleOp(Op.LENGTH_GTE, length));
|
|
264
301
|
}
|
|
265
302
|
|
|
266
|
-
/** Assert the runtime length is less than or equal to `
|
|
267
|
-
lengthLte(
|
|
268
|
-
return this.push(singleOp(Op.LENGTH_LTE,
|
|
303
|
+
/** Assert the runtime length is less than or equal to `length`. */
|
|
304
|
+
lengthLte(length: bigint): this {
|
|
305
|
+
return this.push(singleOp(Op.LENGTH_LTE, length));
|
|
269
306
|
}
|
|
270
307
|
|
|
271
308
|
/**
|
|
272
309
|
* Assert the runtime length is within [min, max] inclusive.
|
|
273
310
|
* @throws {CallciumError} If min > max.
|
|
274
311
|
*/
|
|
275
|
-
lengthBetween(min: bigint
|
|
276
|
-
return this.push(rangeOp(Op.LENGTH_BETWEEN,
|
|
312
|
+
lengthBetween(min: bigint, max: bigint): this {
|
|
313
|
+
return this.push(rangeOp(Op.LENGTH_BETWEEN, min, max));
|
|
277
314
|
}
|
|
278
315
|
}
|
|
279
316
|
|
|
@@ -291,41 +328,41 @@ export function arg(...steps: number[]): ConstraintBuilder {
|
|
|
291
328
|
}
|
|
292
329
|
|
|
293
330
|
/** Target the `msg.sender` context property. */
|
|
294
|
-
export function msgSender(): ConstraintBuilder {
|
|
331
|
+
export function msgSender(): ConstraintBuilder<Address> {
|
|
295
332
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
|
|
296
333
|
}
|
|
297
334
|
|
|
298
335
|
/** Target the `msg.value` context property. */
|
|
299
|
-
export function msgValue(): ConstraintBuilder {
|
|
336
|
+
export function msgValue(): ConstraintBuilder<bigint> {
|
|
300
337
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
/** Target the `block.timestamp` context property. */
|
|
304
|
-
export function blockTimestamp(): ConstraintBuilder {
|
|
341
|
+
export function blockTimestamp(): ConstraintBuilder<bigint> {
|
|
305
342
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
|
|
306
343
|
}
|
|
307
344
|
|
|
308
345
|
/** Target the `block.number` context property. */
|
|
309
|
-
export function blockNumber(): ConstraintBuilder {
|
|
346
|
+
export function blockNumber(): ConstraintBuilder<bigint> {
|
|
310
347
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
|
|
311
348
|
}
|
|
312
349
|
|
|
313
350
|
/** Target the `block.chainid` context property. */
|
|
314
|
-
export function chainId(): ConstraintBuilder {
|
|
351
|
+
export function chainId(): ConstraintBuilder<bigint> {
|
|
315
352
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
|
|
316
353
|
}
|
|
317
354
|
|
|
318
355
|
/** Target the `tx.origin` context property. */
|
|
319
|
-
export function txOrigin(): ConstraintBuilder {
|
|
356
|
+
export function txOrigin(): ConstraintBuilder<Address> {
|
|
320
357
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
|
|
321
358
|
}
|
|
322
359
|
|
|
323
360
|
/** Target the `block.basefee` context property. */
|
|
324
|
-
export function baseFee(): ConstraintBuilder {
|
|
361
|
+
export function baseFee(): ConstraintBuilder<bigint> {
|
|
325
362
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
|
|
326
363
|
}
|
|
327
364
|
|
|
328
365
|
/** Target the `tx.gasprice` context property. */
|
|
329
|
-
export function gasPrice(): ConstraintBuilder {
|
|
366
|
+
export function gasPrice(): ConstraintBuilder<bigint> {
|
|
330
367
|
return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
|
|
331
368
|
}
|
package/src/descriptor-coder.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readU16
|
|
2
|
-
import { DescriptorFormat as DF, TypeCode
|
|
3
|
-
import { Descriptor } from "./descriptor";
|
|
1
|
+
import { readU16 } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, TypeCode } from "./constants";
|
|
3
|
+
import { Descriptor, readNode } from "./descriptor";
|
|
4
4
|
import { CallciumError } from "./errors";
|
|
5
|
+
import { classifyTypeCode, lookupTypeCode } from "./operators";
|
|
5
6
|
import { address, array, bool, bytes, bytesN, function_, intN, string_, tuple, uintN } from "./type-desc";
|
|
6
7
|
|
|
7
8
|
import type { DecodedParam, Hex } from "./types";
|
|
@@ -22,11 +23,17 @@ function commaPositions(input: string, start: number, end: number): number[] {
|
|
|
22
23
|
if (char === "(" || char === "[") {
|
|
23
24
|
depth++;
|
|
24
25
|
} else if (char === ")" || char === "]") {
|
|
26
|
+
if (depth === 0) {
|
|
27
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '${char}' at position ${i}`);
|
|
28
|
+
}
|
|
25
29
|
depth--;
|
|
26
30
|
} else if (char === "," && depth === 0) {
|
|
27
31
|
positions.push(i);
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
if (depth !== 0) {
|
|
35
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unclosed '${input.slice(start, end)}'`);
|
|
36
|
+
}
|
|
30
37
|
return positions;
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -78,35 +85,35 @@ function parseTuple(input: string, start: number, end: number): Uint8Array {
|
|
|
78
85
|
throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
|
|
79
86
|
}
|
|
80
87
|
let depth = 0;
|
|
81
|
-
let
|
|
88
|
+
let closeIndex = -1;
|
|
82
89
|
for (let i = start; i < end; i++) {
|
|
83
90
|
if (input[i] === "(") depth++;
|
|
84
91
|
else if (input[i] === ")") {
|
|
85
92
|
depth--;
|
|
86
93
|
if (depth === 0) {
|
|
87
|
-
|
|
94
|
+
closeIndex = i;
|
|
88
95
|
break;
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
|
-
if (
|
|
99
|
+
if (closeIndex === -1) {
|
|
93
100
|
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
|
|
94
101
|
}
|
|
95
|
-
if (
|
|
96
|
-
throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${
|
|
102
|
+
if (closeIndex !== end - 1) {
|
|
103
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closeIndex + 1}`);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
const innerStart = start + 1;
|
|
100
|
-
const innerEnd =
|
|
107
|
+
const innerEnd = closeIndex;
|
|
101
108
|
const commas = commaPositions(input, innerStart, innerEnd);
|
|
102
109
|
|
|
103
110
|
const segments: Array<[number, number]> = [];
|
|
104
|
-
let
|
|
111
|
+
let segmentStart = innerStart;
|
|
105
112
|
for (const comma of commas) {
|
|
106
|
-
segments.push([
|
|
107
|
-
|
|
113
|
+
segments.push([segmentStart, comma]);
|
|
114
|
+
segmentStart = comma + 1;
|
|
108
115
|
}
|
|
109
|
-
segments.push([
|
|
116
|
+
segments.push([segmentStart, innerEnd]);
|
|
110
117
|
|
|
111
118
|
// An empty tuple has no fields to parse; the tuple builder rejects the count.
|
|
112
119
|
if (segments.length === 1 && segments[0]![0] === segments[0]![1]) return tuple([]);
|
|
@@ -130,9 +137,11 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
|
|
|
130
137
|
if (segment === "string") return string_();
|
|
131
138
|
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (segment.startsWith("
|
|
140
|
+
// A prefix with no width after it spells no type, so it falls through to the unknown-type report
|
|
141
|
+
// rather than being read as a width that is not there.
|
|
142
|
+
if (segment.length > 4 && segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
143
|
+
if (segment.length > 3 && segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
144
|
+
if (segment.length > 5 && segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
136
145
|
|
|
137
146
|
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
138
147
|
}
|
|
@@ -152,39 +161,31 @@ function parseType(input: string, start: number, end: number): Uint8Array {
|
|
|
152
161
|
throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
153
162
|
}
|
|
154
163
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
const
|
|
164
|
+
// Locate array suffixes by scanning backward, reading no length yet: the base names the type, so
|
|
165
|
+
// an unrecognised one is reported before a suffix it carries is parsed.
|
|
166
|
+
const spans: Array<[number, number]> = [];
|
|
158
167
|
let baseEnd = end;
|
|
159
168
|
|
|
160
169
|
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
161
|
-
const
|
|
162
|
-
let
|
|
170
|
+
const closeIndex = baseEnd - 1;
|
|
171
|
+
let openIndex = closeIndex - 1;
|
|
163
172
|
// Walk back past digits (for static arrays).
|
|
164
|
-
while (
|
|
165
|
-
|
|
173
|
+
while (openIndex > start && input[openIndex] !== "[") {
|
|
174
|
+
openIndex--;
|
|
166
175
|
}
|
|
167
|
-
if (input[
|
|
168
|
-
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${
|
|
176
|
+
if (input[openIndex] !== "[") {
|
|
177
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closeIndex}`);
|
|
169
178
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// A dynamic array spells no length at all.
|
|
173
|
-
suffixes.unshift(innerStart === innerEnd ? undefined : parseUint(input, innerStart, innerEnd));
|
|
174
|
-
baseEnd = openPos;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Every `[` a base carries belongs to a suffix the scan above already took; one left behind is
|
|
178
|
-
// unclosed. A tuple base is exempt: the brackets inside it belong to its fields.
|
|
179
|
-
if (input[start] !== "(" && input.slice(start, baseEnd).includes("[")) {
|
|
180
|
-
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '[' in '${input.slice(start, baseEnd)}'`);
|
|
179
|
+
spans.unshift([openIndex + 1, closeIndex]);
|
|
180
|
+
baseEnd = openIndex;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
let desc = parseBaseType(input, start, baseEnd);
|
|
184
184
|
|
|
185
185
|
// Apply suffixes left-to-right: the leftmost suffix is the outermost array.
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// A dynamic array spells no length at all.
|
|
187
|
+
for (const [innerStart, innerEnd] of spans) {
|
|
188
|
+
desc = array(desc, innerStart === innerEnd ? undefined : parseUint(input, innerStart, innerEnd));
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
return desc;
|
|
@@ -201,6 +202,7 @@ function parseType(input: string, start: number, end: number): Uint8Array {
|
|
|
201
202
|
* @returns Binary descriptor bytes starting with the version+paramCount header.
|
|
202
203
|
* @throws {CallciumError} With code `MALFORMED_TYPE_STRING` for malformed input.
|
|
203
204
|
* @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
|
|
205
|
+
* @throws {CallciumError} With code `NESTING_TOO_DEEP` when composite nesting exceeds `MAX_NESTING_DEPTH`.
|
|
204
206
|
*/
|
|
205
207
|
function fromTypes(typesCsv: string): Uint8Array {
|
|
206
208
|
if (typesCsv === "") {
|
|
@@ -219,10 +221,7 @@ function fromTypes(typesCsv: string): Uint8Array {
|
|
|
219
221
|
|
|
220
222
|
const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
|
|
221
223
|
if (paramDescs.length > DF.MAX_PARAMS) {
|
|
222
|
-
throw new CallciumError(
|
|
223
|
-
"TOO_MANY_PARAMS",
|
|
224
|
-
`Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}.`,
|
|
225
|
-
);
|
|
224
|
+
throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}`);
|
|
226
225
|
}
|
|
227
226
|
const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
|
|
228
227
|
const result = new Uint8Array(DF.HEADER_SIZE + totalBytes);
|
|
@@ -233,6 +232,8 @@ function fromTypes(typesCsv: string): Uint8Array {
|
|
|
233
232
|
result.set(desc, offset);
|
|
234
233
|
offset += desc.length;
|
|
235
234
|
}
|
|
235
|
+
// Nesting depth is a property of the assembled descriptor, not of any single node.
|
|
236
|
+
decodeDescriptor(result);
|
|
236
237
|
return result;
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -242,7 +243,7 @@ function fromTypes(typesCsv: string): Uint8Array {
|
|
|
242
243
|
|
|
243
244
|
/** Reconstruct an ABI type string from a descriptor node at the given offset. */
|
|
244
245
|
function nodeToTypeString(desc: Uint8Array, offset: number): string {
|
|
245
|
-
const typeCode = desc
|
|
246
|
+
const { typeCode } = Descriptor.inspect(desc, offset);
|
|
246
247
|
|
|
247
248
|
if (typeCode === TypeCode.TUPLE) {
|
|
248
249
|
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
@@ -298,21 +299,14 @@ type ParseResult = { typeCode: number; isDynamic: boolean; staticSize: number; n
|
|
|
298
299
|
|
|
299
300
|
/** Recursively parse a single descriptor node starting at offset. */
|
|
300
301
|
function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult {
|
|
301
|
-
|
|
302
|
-
throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const code = data[offset]!;
|
|
302
|
+
const { typeCode: code, staticWords, nodeLength } = readNode(data, offset);
|
|
306
303
|
const info = classifyTypeCode(code);
|
|
307
|
-
const
|
|
304
|
+
const isDynamic = staticWords === 0;
|
|
305
|
+
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
306
|
+
const nodeEnd = offset + nodeLength;
|
|
308
307
|
|
|
309
308
|
if (info.typeClass === "elementary") {
|
|
310
|
-
return {
|
|
311
|
-
typeCode: code,
|
|
312
|
-
isDynamic: info.isDynamic,
|
|
313
|
-
staticSize: info.isDynamic ? 0 : 32,
|
|
314
|
-
next: metaOffset,
|
|
315
|
-
};
|
|
309
|
+
return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
|
|
316
310
|
}
|
|
317
311
|
|
|
318
312
|
// Only composites nest; a leaf below the deepest allowed composite is fine.
|
|
@@ -324,33 +318,8 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
|
|
|
324
318
|
);
|
|
325
319
|
}
|
|
326
320
|
|
|
327
|
-
const metaEnd = metaOffset + DF.COMPOSITE_META_SIZE;
|
|
328
|
-
if (metaEnd > data.length) {
|
|
329
|
-
throw new CallciumError("UNEXPECTED_END", "Incomplete composite metadata", offset);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const meta = readU24(data, metaOffset);
|
|
333
|
-
const staticWords = meta >> DF.META_STATIC_WORDS_SHIFT;
|
|
334
|
-
const nodeLength = meta & DF.META_NODE_LENGTH_MASK;
|
|
335
|
-
|
|
336
|
-
const minHeader = info.typeClass === "tuple" ? DF.TUPLE_HEADER_SIZE : DF.ARRAY_HEADER_SIZE;
|
|
337
|
-
if (nodeLength < minHeader) {
|
|
338
|
-
throw new CallciumError(
|
|
339
|
-
"NODE_LENGTH_TOO_SMALL",
|
|
340
|
-
`Composite node length ${nodeLength} is smaller than minimum header ${minHeader}`,
|
|
341
|
-
offset,
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
if (offset + nodeLength > data.length) {
|
|
345
|
-
throw new CallciumError("NODE_OVERFLOW", "Composite node extends beyond descriptor", offset);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const isDynamic = staticWords === 0;
|
|
349
|
-
const staticSize = isDynamic ? 0 : staticWords * 32;
|
|
350
|
-
const nodeEnd = offset + nodeLength;
|
|
351
|
-
|
|
352
321
|
if (info.typeClass === "tuple") {
|
|
353
|
-
const fieldCountOffset =
|
|
322
|
+
const fieldCountOffset = offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE;
|
|
354
323
|
if (fieldCountOffset + 2 > data.length) {
|
|
355
324
|
throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
|
|
356
325
|
}
|
|
@@ -428,12 +397,12 @@ export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescrip
|
|
|
428
397
|
let cursor: number = DF.HEADER_SIZE;
|
|
429
398
|
|
|
430
399
|
while (cursor < data.length) {
|
|
431
|
-
|
|
400
|
+
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
401
|
+
|
|
402
|
+
if (params.length === DF.MAX_PARAMS) {
|
|
432
403
|
throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DF.MAX_PARAMS} top-level params`);
|
|
433
404
|
}
|
|
434
405
|
|
|
435
|
-
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
436
|
-
|
|
437
406
|
params.push({
|
|
438
407
|
index: params.length,
|
|
439
408
|
typeCode,
|