@callcium/sdk 0.6.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 +114 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +837 -1439
- 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
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { readU16, readU24 } from "./bytes";
|
|
2
|
+
import { DescriptorFormat as DF, TypeCode, lookupTypeCode, classifyTypeCode } from "./constants";
|
|
3
|
+
import { Descriptor } from "./descriptor";
|
|
4
|
+
import { CallciumError } from "./errors";
|
|
5
|
+
import { address, array, bool, bytes, bytesN, function_, intN, string_, tuple, uintN } from "./type-desc";
|
|
6
|
+
|
|
7
|
+
import type { DecodedParam, Hex } from "./types";
|
|
8
|
+
|
|
9
|
+
///////////////////////////////////////////////////////////////////////////
|
|
10
|
+
// Splitting helpers
|
|
11
|
+
///////////////////////////////////////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collect the positions of depth-0 commas in input[start..end).
|
|
15
|
+
* Commas inside `()` or `[]` are skipped.
|
|
16
|
+
*/
|
|
17
|
+
function commaPositions(input: string, start: number, end: number): number[] {
|
|
18
|
+
const positions: number[] = [];
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let i = start; i < end; i++) {
|
|
21
|
+
const char = input[i];
|
|
22
|
+
if (char === "(" || char === "[") {
|
|
23
|
+
depth++;
|
|
24
|
+
} else if (char === ")" || char === "]") {
|
|
25
|
+
depth--;
|
|
26
|
+
} else if (char === "," && depth === 0) {
|
|
27
|
+
positions.push(i);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return positions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
///////////////////////////////////////////////////////////////////////////
|
|
34
|
+
// Integer parser
|
|
35
|
+
///////////////////////////////////////////////////////////////////////////
|
|
36
|
+
|
|
37
|
+
/** Widest number a type string can spell: the array length field is the widest field one feeds. */
|
|
38
|
+
const MAX_TYPE_STRING_NUMBER = 256 ** DF.ARRAY_LENGTH_SIZE - 1;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a decimal integer from input[start..end).
|
|
42
|
+
*
|
|
43
|
+
* Rejects an empty span, a non-digit, a leading zero, and a value too wide for the fields a type
|
|
44
|
+
* string feeds. Width and length validity are out of scope here.
|
|
45
|
+
*/
|
|
46
|
+
function parseUint(input: string, start: number, end: number): number {
|
|
47
|
+
if (start >= end) {
|
|
48
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Missing number at position ${start}`);
|
|
49
|
+
}
|
|
50
|
+
// A leading zero is never canonical ABI spelling (uint08, bytes01, [03]).
|
|
51
|
+
if (end - start > 1 && input.charCodeAt(start) === 48) {
|
|
52
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Zero-padded number at position ${start}`);
|
|
53
|
+
}
|
|
54
|
+
let value = 0;
|
|
55
|
+
for (let i = start; i < end; i++) {
|
|
56
|
+
const code = input.charCodeAt(i);
|
|
57
|
+
if (code < 48 || code > 57) {
|
|
58
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Non-digit character at position ${i}`);
|
|
59
|
+
}
|
|
60
|
+
value = value * 10 + (code - 48);
|
|
61
|
+
}
|
|
62
|
+
if (value > MAX_TYPE_STRING_NUMBER) {
|
|
63
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Number at position ${start} is too wide`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
///////////////////////////////////////////////////////////////////////////
|
|
69
|
+
// Tuple parser
|
|
70
|
+
///////////////////////////////////////////////////////////////////////////
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
|
|
74
|
+
* `start` and the substring to parse ends before `end`.
|
|
75
|
+
*/
|
|
76
|
+
function parseTuple(input: string, start: number, end: number): Uint8Array {
|
|
77
|
+
if (input[start] !== "(") {
|
|
78
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Expected '(' at position ${start}`);
|
|
79
|
+
}
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let closePos = -1;
|
|
82
|
+
for (let i = start; i < end; i++) {
|
|
83
|
+
if (input[i] === "(") depth++;
|
|
84
|
+
else if (input[i] === ")") {
|
|
85
|
+
depth--;
|
|
86
|
+
if (depth === 0) {
|
|
87
|
+
closePos = i;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (closePos === -1) {
|
|
93
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched '(' at position ${start}`);
|
|
94
|
+
}
|
|
95
|
+
if (closePos !== end - 1) {
|
|
96
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unexpected characters after ')' at position ${closePos + 1}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const innerStart = start + 1;
|
|
100
|
+
const innerEnd = closePos;
|
|
101
|
+
const commas = commaPositions(input, innerStart, innerEnd);
|
|
102
|
+
|
|
103
|
+
const segments: Array<[number, number]> = [];
|
|
104
|
+
let segStart = innerStart;
|
|
105
|
+
for (const comma of commas) {
|
|
106
|
+
segments.push([segStart, comma]);
|
|
107
|
+
segStart = comma + 1;
|
|
108
|
+
}
|
|
109
|
+
segments.push([segStart, innerEnd]);
|
|
110
|
+
|
|
111
|
+
// An empty tuple has no fields to parse; the tuple builder rejects the count.
|
|
112
|
+
if (segments.length === 1 && segments[0]![0] === segments[0]![1]) return tuple([]);
|
|
113
|
+
|
|
114
|
+
const fieldDescs = segments.map(([fieldStart, fieldEnd]) => parseType(input, fieldStart, fieldEnd));
|
|
115
|
+
return tuple(fieldDescs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
///////////////////////////////////////////////////////////////////////////
|
|
119
|
+
// Base type parser
|
|
120
|
+
///////////////////////////////////////////////////////////////////////////
|
|
121
|
+
|
|
122
|
+
/** Parse the base type (no array suffixes) from input[start..end). */
|
|
123
|
+
function parseBaseType(input: string, start: number, end: number): Uint8Array {
|
|
124
|
+
const segment = input.slice(start, end);
|
|
125
|
+
|
|
126
|
+
if (segment === "address") return address();
|
|
127
|
+
if (segment === "bool") return bool();
|
|
128
|
+
if (segment === "function") return function_();
|
|
129
|
+
if (segment === "bytes") return bytes();
|
|
130
|
+
if (segment === "string") return string_();
|
|
131
|
+
if (segment.startsWith("(")) return parseTuple(input, start, end);
|
|
132
|
+
|
|
133
|
+
if (segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
|
|
134
|
+
if (segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
|
|
135
|
+
if (segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
|
|
136
|
+
|
|
137
|
+
throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
///////////////////////////////////////////////////////////////////////////
|
|
141
|
+
// Array suffix collector
|
|
142
|
+
///////////////////////////////////////////////////////////////////////////
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a type segment from input[start..end).
|
|
146
|
+
*
|
|
147
|
+
* Scans backward from end to collect all `[...]` suffixes, identifies the
|
|
148
|
+
* base type extent, parses the base, then applies suffixes left-to-right.
|
|
149
|
+
*/
|
|
150
|
+
function parseType(input: string, start: number, end: number): Uint8Array {
|
|
151
|
+
if (start >= end) {
|
|
152
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Collect array suffixes by scanning backward.
|
|
156
|
+
// Each suffix is either `[]` (dynamic) or `[N]` (static with length N).
|
|
157
|
+
const suffixes: Array<number | undefined> = [];
|
|
158
|
+
let baseEnd = end;
|
|
159
|
+
|
|
160
|
+
while (baseEnd > start && input[baseEnd - 1] === "]") {
|
|
161
|
+
const closePos = baseEnd - 1;
|
|
162
|
+
let openPos = closePos - 1;
|
|
163
|
+
// Walk back past digits (for static arrays).
|
|
164
|
+
while (openPos > start && input[openPos] !== "[") {
|
|
165
|
+
openPos--;
|
|
166
|
+
}
|
|
167
|
+
if (input[openPos] !== "[") {
|
|
168
|
+
throw new CallciumError("MALFORMED_TYPE_STRING", `Unmatched ']' at position ${closePos}`);
|
|
169
|
+
}
|
|
170
|
+
const innerStart = openPos + 1;
|
|
171
|
+
const innerEnd = closePos;
|
|
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)}'`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let desc = parseBaseType(input, start, baseEnd);
|
|
184
|
+
|
|
185
|
+
// Apply suffixes left-to-right: the leftmost suffix is the outermost array.
|
|
186
|
+
for (const length of suffixes) {
|
|
187
|
+
desc = array(desc, length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return desc;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
///////////////////////////////////////////////////////////////////////////
|
|
194
|
+
// Public interface
|
|
195
|
+
///////////////////////////////////////////////////////////////////////////
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Encode a comma-separated list of ABI type strings into a binary descriptor.
|
|
199
|
+
*
|
|
200
|
+
* @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
|
|
201
|
+
* @returns Binary descriptor bytes starting with the version+paramCount header.
|
|
202
|
+
* @throws {CallciumError} With code `MALFORMED_TYPE_STRING` for malformed input.
|
|
203
|
+
* @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
|
|
204
|
+
*/
|
|
205
|
+
function fromTypes(typesCsv: string): Uint8Array {
|
|
206
|
+
if (typesCsv === "") {
|
|
207
|
+
return new Uint8Array([DF.VERSION, 0x00]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const commas = commaPositions(typesCsv, 0, typesCsv.length);
|
|
211
|
+
|
|
212
|
+
const segments: Array<[number, number]> = [];
|
|
213
|
+
let segStart = 0;
|
|
214
|
+
for (const comma of commas) {
|
|
215
|
+
segments.push([segStart, comma]);
|
|
216
|
+
segStart = comma + 1;
|
|
217
|
+
}
|
|
218
|
+
segments.push([segStart, typesCsv.length]);
|
|
219
|
+
|
|
220
|
+
const paramDescs = segments.map(([start, end]) => parseType(typesCsv, start, end));
|
|
221
|
+
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
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
|
|
228
|
+
const result = new Uint8Array(DF.HEADER_SIZE + totalBytes);
|
|
229
|
+
result[0] = DF.VERSION;
|
|
230
|
+
result[1] = paramDescs.length;
|
|
231
|
+
let offset = DF.HEADER_SIZE;
|
|
232
|
+
for (const desc of paramDescs) {
|
|
233
|
+
result.set(desc, offset);
|
|
234
|
+
offset += desc.length;
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
///////////////////////////////////////////////////////////////////////////
|
|
240
|
+
// Inverse: descriptor bytes → type string
|
|
241
|
+
///////////////////////////////////////////////////////////////////////////
|
|
242
|
+
|
|
243
|
+
/** Reconstruct an ABI type string from a descriptor node at the given offset. */
|
|
244
|
+
function nodeToTypeString(desc: Uint8Array, offset: number): string {
|
|
245
|
+
const typeCode = desc[offset]!;
|
|
246
|
+
|
|
247
|
+
if (typeCode === TypeCode.TUPLE) {
|
|
248
|
+
const fieldCount = Descriptor.tupleFieldCount(desc, offset);
|
|
249
|
+
const fieldTypes: string[] = [];
|
|
250
|
+
let fieldOffset = offset + DF.TUPLE_HEADER_SIZE;
|
|
251
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
252
|
+
fieldTypes.push(nodeToTypeString(desc, fieldOffset));
|
|
253
|
+
fieldOffset += Descriptor.nodeLength(desc, fieldOffset);
|
|
254
|
+
}
|
|
255
|
+
return `(${fieldTypes.join(",")})`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeCode === TypeCode.STATIC_ARRAY) {
|
|
259
|
+
const length = Descriptor.staticArrayLength(desc, offset);
|
|
260
|
+
const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
|
|
261
|
+
return `${elemType}[${length}]`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (typeCode === TypeCode.DYNAMIC_ARRAY) {
|
|
265
|
+
const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
|
|
266
|
+
return `${elemType}[]`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return lookupTypeCode(typeCode).label;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
|
|
274
|
+
*
|
|
275
|
+
* Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
|
|
276
|
+
*
|
|
277
|
+
* @param desc - Binary descriptor bytes (with version+paramCount header).
|
|
278
|
+
* @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
|
|
279
|
+
* @throws {CallciumError} If the descriptor is malformed.
|
|
280
|
+
*/
|
|
281
|
+
function toTypes(desc: Uint8Array): string {
|
|
282
|
+
const count = Descriptor.paramCount(desc);
|
|
283
|
+
const types: string[] = [];
|
|
284
|
+
let offset = DF.HEADER_SIZE;
|
|
285
|
+
for (let i = 0; i < count; i++) {
|
|
286
|
+
types.push(nodeToTypeString(desc, offset));
|
|
287
|
+
offset += Descriptor.nodeLength(desc, offset);
|
|
288
|
+
}
|
|
289
|
+
return types.join(",");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
///////////////////////////////////////////////////////////////////////////
|
|
293
|
+
// Descriptor binary decoder
|
|
294
|
+
///////////////////////////////////////////////////////////////////////////
|
|
295
|
+
|
|
296
|
+
/** Shape of a parsed descriptor node and the offset just past it. */
|
|
297
|
+
type ParseResult = { typeCode: number; isDynamic: boolean; staticSize: number; next: number };
|
|
298
|
+
|
|
299
|
+
/** Recursively parse a single descriptor node starting at offset. */
|
|
300
|
+
function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult {
|
|
301
|
+
if (offset >= data.length) {
|
|
302
|
+
throw new CallciumError("UNEXPECTED_END", "Unexpected end of descriptor", offset);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const code = data[offset]!;
|
|
306
|
+
const info = classifyTypeCode(code);
|
|
307
|
+
const metaOffset = offset + DF.TYPECODE_SIZE;
|
|
308
|
+
|
|
309
|
+
if (info.typeClass === "elementary") {
|
|
310
|
+
return {
|
|
311
|
+
typeCode: code,
|
|
312
|
+
isDynamic: info.isDynamic,
|
|
313
|
+
staticSize: info.isDynamic ? 0 : 32,
|
|
314
|
+
next: metaOffset,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Only composites nest; a leaf below the deepest allowed composite is fine.
|
|
319
|
+
if (depth > DF.MAX_NESTING_DEPTH) {
|
|
320
|
+
throw new CallciumError(
|
|
321
|
+
"NESTING_TOO_DEEP",
|
|
322
|
+
`Composite nesting exceeds maximum depth ${DF.MAX_NESTING_DEPTH}`,
|
|
323
|
+
offset,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
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
|
+
if (info.typeClass === "tuple") {
|
|
353
|
+
const fieldCountOffset = metaEnd;
|
|
354
|
+
if (fieldCountOffset + 2 > data.length) {
|
|
355
|
+
throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
|
|
356
|
+
}
|
|
357
|
+
const fieldCount = readU16(data, fieldCountOffset);
|
|
358
|
+
if (fieldCount === 0) {
|
|
359
|
+
throw new CallciumError("INVALID_TUPLE_FIELD_COUNT", "Tuple must have at least one field", offset);
|
|
360
|
+
}
|
|
361
|
+
if (fieldCount > DF.MAX_TUPLE_FIELDS) {
|
|
362
|
+
throw new CallciumError(
|
|
363
|
+
"INVALID_TUPLE_FIELD_COUNT",
|
|
364
|
+
`Tuple field count ${fieldCount} exceeds maximum ${DF.MAX_TUPLE_FIELDS}`,
|
|
365
|
+
offset,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let cursor = offset + DF.TUPLE_HEADER_SIZE;
|
|
370
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
371
|
+
cursor = parseNode(data, cursor, depth + 1).next;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (info.typeClass === "staticArray") {
|
|
378
|
+
const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
379
|
+
const lengthOffset = elemResult.next;
|
|
380
|
+
if (lengthOffset + DF.ARRAY_LENGTH_SIZE > data.length) {
|
|
381
|
+
throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
|
|
382
|
+
}
|
|
383
|
+
const length = readU16(data, lengthOffset);
|
|
384
|
+
if (length === 0) {
|
|
385
|
+
throw new CallciumError("INVALID_ARRAY_LENGTH", "Static array length must be greater than zero", offset);
|
|
386
|
+
}
|
|
387
|
+
if (length > DF.MAX_STATIC_ARRAY_LENGTH) {
|
|
388
|
+
throw new CallciumError(
|
|
389
|
+
"INVALID_ARRAY_LENGTH",
|
|
390
|
+
`Static array length ${length} exceeds maximum ${DF.MAX_STATIC_ARRAY_LENGTH}`,
|
|
391
|
+
offset,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
|
|
399
|
+
return { typeCode: code, isDynamic: true, staticSize: 0, next: nodeEnd };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Convert a zero-based param index to a BE16 hex path step. */
|
|
403
|
+
function indexToPath(index: number): Hex {
|
|
404
|
+
return `0x${index.toString(16).padStart(4, "0")}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
type DecodedDescriptor = {
|
|
408
|
+
version: number;
|
|
409
|
+
params: DecodedParam[];
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** Decode a binary descriptor blob, returning its structural representation. */
|
|
413
|
+
export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescriptor } {
|
|
414
|
+
if (data.length < 1) {
|
|
415
|
+
throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const version = data[0]!;
|
|
419
|
+
if (version !== DF.VERSION) {
|
|
420
|
+
throw new CallciumError("UNSUPPORTED_VERSION", `Version ${version} is not supported (expected ${DF.VERSION})`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (data.length < DF.HEADER_SIZE) {
|
|
424
|
+
throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const params: DecodedParam[] = [];
|
|
428
|
+
let cursor: number = DF.HEADER_SIZE;
|
|
429
|
+
|
|
430
|
+
while (cursor < data.length) {
|
|
431
|
+
if (params.length >= DF.MAX_PARAMS) {
|
|
432
|
+
throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DF.MAX_PARAMS} top-level params`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
|
|
436
|
+
|
|
437
|
+
params.push({
|
|
438
|
+
index: params.length,
|
|
439
|
+
typeCode,
|
|
440
|
+
isDynamic,
|
|
441
|
+
staticSize,
|
|
442
|
+
path: indexToPath(params.length),
|
|
443
|
+
span: { start: cursor, end: next },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
cursor = next;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const declaredCount = data[1]!;
|
|
450
|
+
if (params.length !== declaredCount) {
|
|
451
|
+
throw new CallciumError(
|
|
452
|
+
"PARAM_COUNT_MISMATCH",
|
|
453
|
+
`Header declares ${declaredCount} params but ${params.length} were parsed`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { descriptor: { version, params } };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Encode and decode descriptors. */
|
|
461
|
+
export const DescriptorCoder = { fromTypes, toTypes };
|