@thru/abi 0.1.29
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/README.md +174 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +1106 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/abiSchema.ts +525 -0
- package/src/decodedValue.ts +69 -0
- package/src/decoder.ts +572 -0
- package/src/errors.ts +32 -0
- package/src/expression.ts +165 -0
- package/src/index.test.ts +249 -0
- package/src/index.ts +15 -0
- package/src/typeRegistry.ts +172 -0
- package/src/utils/bytes.ts +7 -0
- package/test/decode-examples.ts +109 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +14 -0
package/src/abiSchema.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import { AbiParseError, AbiValidationError } from "./errors";
|
|
3
|
+
|
|
4
|
+
export type PrimitiveName = "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f16" | "f32" | "f64";
|
|
5
|
+
const PRIMITIVE_NAMES: readonly PrimitiveName[] = ["u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f16", "f32", "f64"];
|
|
6
|
+
|
|
7
|
+
export interface ContainerAttributes {
|
|
8
|
+
packed: boolean;
|
|
9
|
+
aligned: number;
|
|
10
|
+
comment?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AbiMetadata {
|
|
14
|
+
package: string;
|
|
15
|
+
abiVersion: number;
|
|
16
|
+
packageVersion?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AbiDocument {
|
|
21
|
+
metadata: AbiMetadata;
|
|
22
|
+
types: TypeDefinition[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TypeDefinition {
|
|
26
|
+
name: string;
|
|
27
|
+
kind: TypeKind;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TypeKind =
|
|
31
|
+
| PrimitiveType
|
|
32
|
+
| StructType
|
|
33
|
+
| ArrayType
|
|
34
|
+
| EnumType
|
|
35
|
+
| UnionType
|
|
36
|
+
| SizeDiscriminatedUnionType
|
|
37
|
+
| TypeRefType;
|
|
38
|
+
|
|
39
|
+
export interface PrimitiveType {
|
|
40
|
+
kind: "primitive";
|
|
41
|
+
primitive: PrimitiveName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StructField {
|
|
45
|
+
name: string;
|
|
46
|
+
type: TypeKind;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StructType {
|
|
50
|
+
kind: "struct";
|
|
51
|
+
attributes: ContainerAttributes;
|
|
52
|
+
fields: StructField[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ArrayType {
|
|
56
|
+
kind: "array";
|
|
57
|
+
attributes: ContainerAttributes;
|
|
58
|
+
elementType: TypeKind;
|
|
59
|
+
size: Expression;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface EnumVariant {
|
|
63
|
+
name: string;
|
|
64
|
+
tagValue: number;
|
|
65
|
+
type: TypeKind;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EnumType {
|
|
69
|
+
kind: "enum";
|
|
70
|
+
attributes: ContainerAttributes;
|
|
71
|
+
tagExpression: Expression;
|
|
72
|
+
variants: EnumVariant[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UnionVariant {
|
|
76
|
+
name: string;
|
|
77
|
+
type: TypeKind;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface UnionType {
|
|
81
|
+
kind: "union";
|
|
82
|
+
attributes: ContainerAttributes;
|
|
83
|
+
variants: UnionVariant[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SizeDiscriminatedVariant {
|
|
87
|
+
name: string;
|
|
88
|
+
expectedSize: number;
|
|
89
|
+
type: TypeKind;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SizeDiscriminatedUnionType {
|
|
93
|
+
kind: "size-discriminated-union";
|
|
94
|
+
attributes: ContainerAttributes;
|
|
95
|
+
variants: SizeDiscriminatedVariant[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TypeRefType {
|
|
99
|
+
kind: "type-ref";
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type Expression = LiteralExpression | FieldRefExpression | BinaryExpression | UnaryExpression | SizeOfExpression | AlignOfExpression;
|
|
104
|
+
|
|
105
|
+
export interface LiteralExpression {
|
|
106
|
+
type: "literal";
|
|
107
|
+
literalType: PrimitiveName;
|
|
108
|
+
value: bigint;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface FieldRefExpression {
|
|
112
|
+
type: "field-ref";
|
|
113
|
+
path: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type BinaryOperator = "add" | "sub" | "mul" | "div" | "mod" | "bit-and" | "bit-or" | "bit-xor" | "left-shift" | "right-shift";
|
|
117
|
+
|
|
118
|
+
export interface BinaryExpression {
|
|
119
|
+
type: "binary";
|
|
120
|
+
op: BinaryOperator;
|
|
121
|
+
left: Expression;
|
|
122
|
+
right: Expression;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type UnaryOperator = "bit-not";
|
|
126
|
+
|
|
127
|
+
export interface UnaryExpression {
|
|
128
|
+
type: "unary";
|
|
129
|
+
op: UnaryOperator;
|
|
130
|
+
operand: Expression;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SizeOfExpression {
|
|
134
|
+
type: "sizeof";
|
|
135
|
+
typeName: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface AlignOfExpression {
|
|
139
|
+
type: "alignof";
|
|
140
|
+
typeName: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function parseAbiDocument(yamlText: string): AbiDocument {
|
|
144
|
+
let parsed: unknown;
|
|
145
|
+
try {
|
|
146
|
+
parsed = YAML.parse(yamlText, { intAsBigInt: true });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new AbiParseError("Failed to parse ABI YAML", {
|
|
149
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const root = requireRecord(parsed, "ABI document");
|
|
154
|
+
const abiNode = requireRecord(root.abi, "abi metadata");
|
|
155
|
+
const typesNode = root.types;
|
|
156
|
+
|
|
157
|
+
if (!Array.isArray(typesNode)) {
|
|
158
|
+
throw new AbiValidationError("ABI file must contain a 'types' array");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const metadata = parseMetadata(abiNode);
|
|
162
|
+
const types = typesNode.map((entry, index) => parseTypeDefinition(entry, index));
|
|
163
|
+
|
|
164
|
+
ensureTypeNamesUnique(types);
|
|
165
|
+
|
|
166
|
+
return { metadata, types };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseMetadata(node: Record<string, unknown>): AbiMetadata {
|
|
170
|
+
const pkg = requireString(node.package, "abi.package");
|
|
171
|
+
const abiVersionRaw = node["abi-version"];
|
|
172
|
+
if (abiVersionRaw === undefined || abiVersionRaw === null) {
|
|
173
|
+
throw new AbiValidationError("abi.abi-version is required");
|
|
174
|
+
}
|
|
175
|
+
const abiVersion = Number(abiVersionRaw);
|
|
176
|
+
if (!Number.isFinite(abiVersion)) {
|
|
177
|
+
throw new AbiValidationError("abi.abi-version must be a number");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const imports = node.imports;
|
|
181
|
+
if (imports !== undefined) {
|
|
182
|
+
const importList = Array.isArray(imports) ? imports : [];
|
|
183
|
+
if (importList.length > 0) {
|
|
184
|
+
throw new AbiValidationError("Flattened ABI files cannot contain 'imports'");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const metadata: AbiMetadata = {
|
|
189
|
+
package: pkg,
|
|
190
|
+
abiVersion,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (typeof node["package-version"] === "string") {
|
|
194
|
+
metadata.packageVersion = node["package-version"];
|
|
195
|
+
}
|
|
196
|
+
if (typeof node.description === "string") {
|
|
197
|
+
metadata.description = node.description;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return metadata;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseTypeDefinition(entry: unknown, index: number): TypeDefinition {
|
|
204
|
+
const node = requireRecord(entry, `types[${index}]`);
|
|
205
|
+
const name = requireString(node.name, `types[${index}].name`);
|
|
206
|
+
const kindNode = node.kind;
|
|
207
|
+
if (!kindNode || typeof kindNode !== "object") {
|
|
208
|
+
throw new AbiValidationError(`Type '${name}' is missing its 'kind' definition`);
|
|
209
|
+
}
|
|
210
|
+
const kind = parseTypeKind(kindNode as Record<string, unknown>, name);
|
|
211
|
+
return { name, kind };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseTypeKind(node: Record<string, unknown>, context: string): TypeKind {
|
|
215
|
+
const keys = Object.keys(node);
|
|
216
|
+
if (keys.length !== 1) {
|
|
217
|
+
throw new AbiValidationError(`Type '${context}' kind must be a single-entry object`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const key = keys[0];
|
|
221
|
+
const value = node[key];
|
|
222
|
+
|
|
223
|
+
switch (key) {
|
|
224
|
+
case "primitive":
|
|
225
|
+
return parsePrimitiveType(value, context);
|
|
226
|
+
case "struct":
|
|
227
|
+
return parseStructType(requireRecord(value, `struct for ${context}`), context);
|
|
228
|
+
case "array":
|
|
229
|
+
return parseArrayType(requireRecord(value, `array for ${context}`), context);
|
|
230
|
+
case "enum":
|
|
231
|
+
return parseEnumType(requireRecord(value, `enum for ${context}`), context);
|
|
232
|
+
case "union":
|
|
233
|
+
return parseUnionType(requireRecord(value, `union for ${context}`), context);
|
|
234
|
+
case "size-discriminated-union":
|
|
235
|
+
return parseSizeDiscriminatedUnionType(requireRecord(value, `size-discriminated-union for ${context}`), context);
|
|
236
|
+
case "type-ref":
|
|
237
|
+
return parseTypeRef(requireRecord(value, `type-ref for ${context}`), context);
|
|
238
|
+
default:
|
|
239
|
+
throw new AbiValidationError(`Type '${context}' uses unsupported kind '${key}'`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parsePrimitiveType(value: unknown, context: string): PrimitiveType {
|
|
244
|
+
if (typeof value !== "string") {
|
|
245
|
+
throw new AbiValidationError(`Primitive type for '${context}' must be a string`);
|
|
246
|
+
}
|
|
247
|
+
if (!PRIMITIVE_NAMES.includes(value as PrimitiveName)) {
|
|
248
|
+
throw new AbiValidationError(`Type '${context}' references unknown primitive '${value}'`);
|
|
249
|
+
}
|
|
250
|
+
return { kind: "primitive", primitive: value as PrimitiveName };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseStructType(node: Record<string, unknown>, context: string): StructType {
|
|
254
|
+
const attributes = parseContainerAttributes(node);
|
|
255
|
+
const fieldsNode = node.fields;
|
|
256
|
+
if (!Array.isArray(fieldsNode)) {
|
|
257
|
+
throw new AbiValidationError(`Struct '${context}' must define a 'fields' array`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fields = fieldsNode.map((fieldNode, index) => {
|
|
261
|
+
const field = requireRecord(fieldNode, `field ${index} in struct '${context}'`);
|
|
262
|
+
const name = requireString(field.name, `field ${index} name in struct '${context}'`);
|
|
263
|
+
const fieldTypeNode = field["field-type"];
|
|
264
|
+
if (!fieldTypeNode || typeof fieldTypeNode !== "object") {
|
|
265
|
+
throw new AbiValidationError(`Field '${name}' in struct '${context}' is missing 'field-type'`);
|
|
266
|
+
}
|
|
267
|
+
const type = parseTypeKind(fieldTypeNode as Record<string, unknown>, `${context}.${name}`);
|
|
268
|
+
return { name, type };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return { kind: "struct", attributes, fields };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseArrayType(node: Record<string, unknown>, context: string): ArrayType {
|
|
275
|
+
const attributes = parseContainerAttributes(node);
|
|
276
|
+
const elementNode = node["element-type"];
|
|
277
|
+
if (!elementNode || typeof elementNode !== "object") {
|
|
278
|
+
throw new AbiValidationError(`Array '${context}' is missing 'element-type'`);
|
|
279
|
+
}
|
|
280
|
+
const elementType = parseTypeKind(elementNode as Record<string, unknown>, `${context}[]`);
|
|
281
|
+
const sizeNode = node.size;
|
|
282
|
+
if (!sizeNode || typeof sizeNode !== "object") {
|
|
283
|
+
throw new AbiValidationError(`Array '${context}' is missing its 'size' expression`);
|
|
284
|
+
}
|
|
285
|
+
const size = parseExpression(sizeNode as Record<string, unknown>, `array '${context}' size`);
|
|
286
|
+
return { kind: "array", attributes, elementType, size };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseEnumType(node: Record<string, unknown>, context: string): EnumType {
|
|
290
|
+
const attributes = parseContainerAttributes(node);
|
|
291
|
+
const tagRefNode = node["tag-ref"];
|
|
292
|
+
if (!tagRefNode || typeof tagRefNode !== "object") {
|
|
293
|
+
throw new AbiValidationError(`Enum '${context}' must define 'tag-ref'`);
|
|
294
|
+
}
|
|
295
|
+
const tagExpression = parseExpression(tagRefNode as Record<string, unknown>, `enum '${context}' tag-ref`);
|
|
296
|
+
|
|
297
|
+
const variantsNode = node.variants;
|
|
298
|
+
if (!Array.isArray(variantsNode) || variantsNode.length === 0) {
|
|
299
|
+
throw new AbiValidationError(`Enum '${context}' must include at least one variant`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const variants = variantsNode.map((variantNode, index) => {
|
|
303
|
+
const variant = requireRecord(variantNode, `variant ${index} in enum '${context}'`);
|
|
304
|
+
const name = requireString(variant.name, `variant ${index} name in enum '${context}'`);
|
|
305
|
+
const tagValueRaw = variant["tag-value"];
|
|
306
|
+
if (tagValueRaw === undefined || tagValueRaw === null) {
|
|
307
|
+
throw new AbiValidationError(`Variant '${name}' in enum '${context}' must define 'tag-value'`);
|
|
308
|
+
}
|
|
309
|
+
const tagValue = Number(tagValueRaw);
|
|
310
|
+
if (!Number.isSafeInteger(tagValue)) {
|
|
311
|
+
throw new AbiValidationError(`Variant '${name}' in enum '${context}' has invalid tag-value`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const variantTypeNode = variant["variant-type"];
|
|
315
|
+
if (!variantTypeNode || typeof variantTypeNode !== "object") {
|
|
316
|
+
throw new AbiValidationError(`Variant '${name}' in enum '${context}' is missing 'variant-type'`);
|
|
317
|
+
}
|
|
318
|
+
const type = parseTypeKind(variantTypeNode as Record<string, unknown>, `${context}.${name}`);
|
|
319
|
+
return { name, tagValue, type };
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return { kind: "enum", attributes, tagExpression, variants };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseUnionType(node: Record<string, unknown>, context: string): UnionType {
|
|
326
|
+
const attributes = parseContainerAttributes(node);
|
|
327
|
+
const variantsNode = node.variants;
|
|
328
|
+
if (!Array.isArray(variantsNode) || variantsNode.length === 0) {
|
|
329
|
+
throw new AbiValidationError(`Union '${context}' must include at least one variant`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const variants = variantsNode.map((variantNode, index) => {
|
|
333
|
+
const variant = requireRecord(variantNode, `variant ${index} in union '${context}'`);
|
|
334
|
+
const name = requireString(variant.name, `variant ${index} name in union '${context}'`);
|
|
335
|
+
const variantTypeNode = variant["variant-type"];
|
|
336
|
+
if (!variantTypeNode || typeof variantTypeNode !== "object") {
|
|
337
|
+
throw new AbiValidationError(`Variant '${name}' in union '${context}' is missing 'variant-type'`);
|
|
338
|
+
}
|
|
339
|
+
const type = parseTypeKind(variantTypeNode as Record<string, unknown>, `${context}.${name}`);
|
|
340
|
+
return { name, type };
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return { kind: "union", attributes, variants };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseSizeDiscriminatedUnionType(node: Record<string, unknown>, context: string): SizeDiscriminatedUnionType {
|
|
347
|
+
const attributes = parseContainerAttributes(node);
|
|
348
|
+
const variantsNode = node.variants;
|
|
349
|
+
if (!Array.isArray(variantsNode) || variantsNode.length === 0) {
|
|
350
|
+
throw new AbiValidationError(`Size-discriminated union '${context}' must include variants`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const variants = variantsNode.map((variantNode, index) => {
|
|
354
|
+
const variant = requireRecord(variantNode, `variant ${index} in size-discriminated union '${context}'`);
|
|
355
|
+
const name = requireString(variant.name, `variant ${index} name in size-discriminated union '${context}'`);
|
|
356
|
+
const sizeRaw = variant["expected-size"];
|
|
357
|
+
if (sizeRaw === undefined || sizeRaw === null) {
|
|
358
|
+
throw new AbiValidationError(`Variant '${name}' in '${context}' must define 'expected-size'`);
|
|
359
|
+
}
|
|
360
|
+
const expectedSize = Number(sizeRaw);
|
|
361
|
+
if (!Number.isSafeInteger(expectedSize) || expectedSize < 0) {
|
|
362
|
+
throw new AbiValidationError(`Variant '${name}' in '${context}' has invalid expected-size`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const variantTypeNode = variant["variant-type"];
|
|
366
|
+
if (!variantTypeNode || typeof variantTypeNode !== "object") {
|
|
367
|
+
throw new AbiValidationError(`Variant '${name}' in '${context}' is missing 'variant-type'`);
|
|
368
|
+
}
|
|
369
|
+
const type = parseTypeKind(variantTypeNode as Record<string, unknown>, `${context}.${name}`);
|
|
370
|
+
return { name, expectedSize, type };
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return { kind: "size-discriminated-union", attributes, variants };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function parseTypeRef(node: Record<string, unknown>, context: string): TypeRefType {
|
|
377
|
+
const name = requireString(node.name, `type-ref in '${context}'`);
|
|
378
|
+
return { kind: "type-ref", name };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function parseContainerAttributes(node: Record<string, unknown>): ContainerAttributes {
|
|
382
|
+
const packed = node.packed === true;
|
|
383
|
+
const alignedRaw = node.aligned;
|
|
384
|
+
const aligned = alignedRaw === undefined ? 0 : Number(alignedRaw);
|
|
385
|
+
if (aligned < 0 || !Number.isFinite(aligned)) {
|
|
386
|
+
throw new AbiValidationError("Container alignment must be a positive number when specified");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const attrs: ContainerAttributes = { packed, aligned };
|
|
390
|
+
if (typeof node.comment === "string") {
|
|
391
|
+
attrs.comment = node.comment;
|
|
392
|
+
}
|
|
393
|
+
return attrs;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function parseExpression(node: Record<string, unknown>, context: string): Expression {
|
|
397
|
+
const keys = Object.keys(node);
|
|
398
|
+
if (keys.length !== 1) {
|
|
399
|
+
throw new AbiValidationError(`Expression for ${context} must contain exactly one operator`);
|
|
400
|
+
}
|
|
401
|
+
const key = keys[0];
|
|
402
|
+
const value = node[key];
|
|
403
|
+
|
|
404
|
+
switch (key) {
|
|
405
|
+
case "literal":
|
|
406
|
+
return parseLiteralExpression(requireRecord(value, `literal expression in ${context}`), context);
|
|
407
|
+
case "field-ref":
|
|
408
|
+
return parseFieldRefExpression(requireRecord(value, `field-ref expression in ${context}`), context);
|
|
409
|
+
case "add":
|
|
410
|
+
case "sub":
|
|
411
|
+
case "mul":
|
|
412
|
+
case "div":
|
|
413
|
+
case "mod":
|
|
414
|
+
case "bit-and":
|
|
415
|
+
case "bit-or":
|
|
416
|
+
case "bit-xor":
|
|
417
|
+
case "left-shift":
|
|
418
|
+
case "right-shift":
|
|
419
|
+
return parseBinaryExpression(key, requireRecord(value, `${key} expression in ${context}`), context);
|
|
420
|
+
case "bit-not":
|
|
421
|
+
return parseUnaryExpression(key, requireRecord(value, `${key} expression in ${context}`), context);
|
|
422
|
+
case "sizeof":
|
|
423
|
+
return parseSizeOfExpression(requireRecord(value, `sizeof expression in ${context}`), context);
|
|
424
|
+
case "alignof":
|
|
425
|
+
return parseAlignOfExpression(requireRecord(value, `alignof expression in ${context}`), context);
|
|
426
|
+
default:
|
|
427
|
+
throw new AbiValidationError(`Expression '${key}' in ${context} is not supported yet`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function parseLiteralExpression(node: Record<string, unknown>, context: string): LiteralExpression {
|
|
432
|
+
const keys = Object.keys(node);
|
|
433
|
+
if (keys.length !== 1) {
|
|
434
|
+
throw new AbiValidationError(`Literal expression for ${context} must specify exactly one primitive type`);
|
|
435
|
+
}
|
|
436
|
+
const literalType = keys[0] as PrimitiveName;
|
|
437
|
+
if (!PRIMITIVE_NAMES.includes(literalType)) {
|
|
438
|
+
throw new AbiValidationError(`Literal in ${context} references unknown primitive '${literalType}'`);
|
|
439
|
+
}
|
|
440
|
+
const rawValue = node[literalType];
|
|
441
|
+
if (typeof rawValue !== "number" && typeof rawValue !== "bigint") {
|
|
442
|
+
throw new AbiValidationError(`Literal in ${context} must be a number`);
|
|
443
|
+
}
|
|
444
|
+
const value = toBigInt(rawValue);
|
|
445
|
+
return { type: "literal", literalType, value };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function parseFieldRefExpression(node: Record<string, unknown>, context: string): FieldRefExpression {
|
|
449
|
+
const pathNode = node.path;
|
|
450
|
+
if (!Array.isArray(pathNode) || pathNode.length === 0) {
|
|
451
|
+
throw new AbiValidationError(`field-ref in ${context} must define a non-empty path array`);
|
|
452
|
+
}
|
|
453
|
+
const path = pathNode.map((segment, index) => {
|
|
454
|
+
if (typeof segment !== "string") {
|
|
455
|
+
throw new AbiValidationError(`field-ref segment ${index} in ${context} must be a string`);
|
|
456
|
+
}
|
|
457
|
+
return segment;
|
|
458
|
+
});
|
|
459
|
+
return { type: "field-ref", path };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseBinaryExpression(op: string, node: Record<string, unknown>, context: string): BinaryExpression {
|
|
463
|
+
const leftNode = node.left;
|
|
464
|
+
const rightNode = node.right;
|
|
465
|
+
if (!leftNode || typeof leftNode !== "object" || !rightNode || typeof rightNode !== "object") {
|
|
466
|
+
throw new AbiValidationError(`Binary expression '${op}' in ${context} must include 'left' and 'right'`);
|
|
467
|
+
}
|
|
468
|
+
const left = parseExpression(leftNode as Record<string, unknown>, `${context} (left operand)`);
|
|
469
|
+
const right = parseExpression(rightNode as Record<string, unknown>, `${context} (right operand)`);
|
|
470
|
+
return { type: "binary", op: op as BinaryOperator, left, right };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function parseUnaryExpression(op: string, node: Record<string, unknown>, context: string): UnaryExpression {
|
|
474
|
+
const operandNode = node.operand;
|
|
475
|
+
if (!operandNode || typeof operandNode !== "object") {
|
|
476
|
+
throw new AbiValidationError(`Unary expression '${op}' in ${context} must include 'operand'`);
|
|
477
|
+
}
|
|
478
|
+
const operand = parseExpression(operandNode as Record<string, unknown>, `${context} (operand)`);
|
|
479
|
+
return { type: "unary", op: op as UnaryOperator, operand };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function parseSizeOfExpression(node: Record<string, unknown>, context: string): SizeOfExpression {
|
|
483
|
+
const typeName = requireString(node["type-name"], `${context}.type-name`);
|
|
484
|
+
return { type: "sizeof", typeName };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function parseAlignOfExpression(node: Record<string, unknown>, context: string): AlignOfExpression {
|
|
488
|
+
const typeName = requireString(node["type-name"], `${context}.type-name`);
|
|
489
|
+
return { type: "alignof", typeName };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function ensureTypeNamesUnique(types: TypeDefinition[]) {
|
|
493
|
+
const seen = new Set<string>();
|
|
494
|
+
for (const type of types) {
|
|
495
|
+
if (seen.has(type.name)) {
|
|
496
|
+
throw new AbiValidationError(`Duplicate type definition '${type.name}' found in ABI`);
|
|
497
|
+
}
|
|
498
|
+
seen.add(type.name);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function requireRecord(value: unknown, context: string): Record<string, unknown> {
|
|
503
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
504
|
+
throw new AbiValidationError(`${context} must be an object`);
|
|
505
|
+
}
|
|
506
|
+
return value as Record<string, unknown>;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function requireString(value: unknown, context: string): string {
|
|
510
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
511
|
+
throw new AbiValidationError(`${context} must be a non-empty string`);
|
|
512
|
+
}
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function toBigInt(value: number | bigint): bigint {
|
|
517
|
+
if (typeof value === "bigint") {
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
521
|
+
throw new AbiValidationError("Literal values must be integers");
|
|
522
|
+
}
|
|
523
|
+
return BigInt(value);
|
|
524
|
+
}
|
|
525
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PrimitiveName } from "./abiSchema";
|
|
2
|
+
|
|
3
|
+
type DecodedValueKind = "primitive" | "struct" | "array" | "enum" | "union" | "size-discriminated-union" | "opaque";
|
|
4
|
+
|
|
5
|
+
interface BaseDecodedValue {
|
|
6
|
+
kind: DecodedValueKind;
|
|
7
|
+
typeName?: string;
|
|
8
|
+
byteOffset: number;
|
|
9
|
+
byteLength: number;
|
|
10
|
+
rawHex: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DecodedPrimitiveValue extends BaseDecodedValue {
|
|
14
|
+
kind: "primitive";
|
|
15
|
+
primitiveType: PrimitiveName;
|
|
16
|
+
value: number | bigint;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DecodedField {
|
|
20
|
+
name: string;
|
|
21
|
+
value: DecodedValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DecodedStructValue extends BaseDecodedValue {
|
|
25
|
+
kind: "struct";
|
|
26
|
+
fields: Record<string, DecodedValue>;
|
|
27
|
+
fieldOrder: DecodedField[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DecodedArrayValue extends BaseDecodedValue {
|
|
31
|
+
kind: "array";
|
|
32
|
+
length: number;
|
|
33
|
+
elements: DecodedValue[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DecodedEnumValue extends BaseDecodedValue {
|
|
37
|
+
kind: "enum";
|
|
38
|
+
tagValue: number;
|
|
39
|
+
variantName: string;
|
|
40
|
+
value: DecodedValue | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DecodedUnionValue extends BaseDecodedValue {
|
|
44
|
+
kind: "union";
|
|
45
|
+
variants: DecodedField[];
|
|
46
|
+
note?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DecodedSizeDiscriminatedUnionValue extends BaseDecodedValue {
|
|
50
|
+
kind: "size-discriminated-union";
|
|
51
|
+
variantName: string;
|
|
52
|
+
expectedSize: number;
|
|
53
|
+
value: DecodedValue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DecodedOpaqueValue extends BaseDecodedValue {
|
|
57
|
+
kind: "opaque";
|
|
58
|
+
description: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type DecodedValue =
|
|
62
|
+
| DecodedPrimitiveValue
|
|
63
|
+
| DecodedStructValue
|
|
64
|
+
| DecodedArrayValue
|
|
65
|
+
| DecodedEnumValue
|
|
66
|
+
| DecodedUnionValue
|
|
67
|
+
| DecodedSizeDiscriminatedUnionValue
|
|
68
|
+
| DecodedOpaqueValue;
|
|
69
|
+
|