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