@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/src/decoder.ts ADDED
@@ -0,0 +1,572 @@
1
+ import type {
2
+ ArrayType,
3
+ EnumType,
4
+ Expression,
5
+ PrimitiveName,
6
+ SizeDiscriminatedUnionType,
7
+ StructType,
8
+ TypeKind,
9
+ UnionType,
10
+ } from "./abiSchema";
11
+ import { parseAbiDocument, TypeRefType } from "./abiSchema";
12
+ import type {
13
+ DecodedArrayValue,
14
+ DecodedEnumValue,
15
+ DecodedField,
16
+ DecodedPrimitiveValue,
17
+ DecodedSizeDiscriminatedUnionValue,
18
+ DecodedStructValue,
19
+ DecodedUnionValue,
20
+ DecodedValue,
21
+ } from "./decodedValue";
22
+ import { AbiDecodeError } from "./errors";
23
+ import { addFieldToScope, createScope, evaluateExpression, FieldScope } from "./expression";
24
+ import { buildTypeRegistry, TypeRegistry } from "./typeRegistry";
25
+ import { alignUp, bytesToHex } from "./utils/bytes";
26
+
27
+ interface DecodeState {
28
+ registry: TypeRegistry;
29
+ data: Uint8Array;
30
+ view: DataView;
31
+ offset: number;
32
+ scope?: FieldScope;
33
+ }
34
+
35
+ interface PreviewResult {
36
+ value?: DecodedValue;
37
+ size?: number;
38
+ error?: AbiDecodeError;
39
+ }
40
+
41
+ export function decodeData(yamlText: string, typeName: string, data: Uint8Array): DecodedValue {
42
+ if (!(data instanceof Uint8Array)) {
43
+ throw new AbiDecodeError("decodeData expects account data as a Uint8Array");
44
+ }
45
+ const trimmedTypeName = typeName?.trim();
46
+ if (!trimmedTypeName) {
47
+ throw new AbiDecodeError("decodeData requires a non-empty typeName argument");
48
+ }
49
+
50
+ const abiDocument = parseAbiDocument(yamlText);
51
+ const registry = buildTypeRegistry(abiDocument);
52
+ return decodeWithRegistry(registry, trimmedTypeName, data);
53
+ }
54
+
55
+ function decodeWithRegistry(registry: TypeRegistry, typeName: string, data: Uint8Array): DecodedValue {
56
+ const type = registry.get(typeName);
57
+ const state: DecodeState = {
58
+ registry,
59
+ data,
60
+ view: new DataView(data.buffer, data.byteOffset, data.byteLength),
61
+ offset: 0,
62
+ scope: undefined,
63
+ };
64
+
65
+ const value = decodeKind(type.kind, state, typeName, type.name, state.view.byteLength);
66
+
67
+ if (state.offset !== data.byteLength) {
68
+ throw new AbiDecodeError("Decoded data did not consume the full buffer", {
69
+ expectedLength: data.byteLength,
70
+ consumedLength: state.offset,
71
+ remainingBytes: data.byteLength - state.offset,
72
+ });
73
+ }
74
+
75
+ return value;
76
+ }
77
+
78
+ function decodeKind(kind: TypeKind, state: DecodeState, context: string, typeName?: string, byteBudget?: number): DecodedValue {
79
+ switch (kind.kind) {
80
+ case "primitive":
81
+ return decodePrimitive(kind.primitive, state, context, typeName);
82
+ case "struct":
83
+ return decodeStruct(kind, state, context, typeName, byteBudget);
84
+ case "array":
85
+ return decodeArray(kind, state, context, typeName);
86
+ case "enum":
87
+ return decodeEnum(kind, state, context, typeName);
88
+ case "union":
89
+ return decodeUnion(kind, state, context, typeName);
90
+ case "size-discriminated-union":
91
+ return decodeSizeDiscriminatedUnion(kind, state, context, typeName, byteBudget);
92
+ case "type-ref":
93
+ return decodeTypeReference(kind, state, context, byteBudget);
94
+ default:
95
+ kind satisfies never;
96
+ throw new AbiDecodeError(`Type '${context}' uses unsupported kind '${(kind as TypeKind).kind}'`);
97
+ }
98
+ }
99
+
100
+ function decodeTypeReference(typeRef: TypeRefType, state: DecodeState, context: string, byteBudget?: number): DecodedValue {
101
+ const referenced = state.registry.get(typeRef.name);
102
+ return decodeKind(referenced.kind, state, context, typeRef.name, byteBudget);
103
+ }
104
+
105
+ function decodePrimitive(primitive: PrimitiveName, state: DecodeState, context: string, typeName?: string): DecodedPrimitiveValue {
106
+ const info = primitiveInfo[primitive];
107
+ if (!info) {
108
+ throw new AbiDecodeError(`Primitive type '${primitive}' is not supported yet`, { context });
109
+ }
110
+ if (info.byteLength > 0) {
111
+ ensureAvailable(state, info.byteLength, context);
112
+ }
113
+ const start = state.offset;
114
+ const value = info.read(state.view, state.offset);
115
+ state.offset += info.byteLength;
116
+ const rawHex = sliceHex(state.data, start, state.offset);
117
+ return {
118
+ kind: "primitive",
119
+ primitiveType: primitive,
120
+ value,
121
+ byteOffset: start,
122
+ byteLength: info.byteLength,
123
+ rawHex,
124
+ typeName,
125
+ };
126
+ }
127
+
128
+ function decodeStruct(struct: StructType, state: DecodeState, context: string, typeName?: string, byteBudget?: number): DecodedStructValue {
129
+ const start = state.offset;
130
+ const previousScope = state.scope;
131
+ const scope = createScope(previousScope);
132
+ state.scope = scope;
133
+
134
+ const trailingSizes = computeTrailingConstantSizes(struct, state.registry);
135
+ const fields: Record<string, DecodedValue> = {};
136
+ const fieldOrder: DecodedField[] = [];
137
+
138
+ try {
139
+ struct.fields.forEach((field, index) => {
140
+ const fieldContext = `${context}.${field.name}`;
141
+
142
+ // Apply padding if struct is not packed
143
+ if (!struct.attributes.packed) {
144
+ const alignment = getTypeAlignment(field.type, state.registry);
145
+ state.offset = alignUp(state.offset, alignment);
146
+ }
147
+
148
+ const tailSize = trailingSizes[index];
149
+ const bytesConsumed = state.offset - start;
150
+ const availableBytes =
151
+ byteBudget !== undefined ? Math.max(byteBudget - bytesConsumed, 0) : state.view.byteLength - state.offset;
152
+ const fieldBudget = tailSize !== null ? Math.max(availableBytes - tailSize, 0) : undefined;
153
+ const value = decodeKind(field.type, state, fieldContext, undefined, fieldBudget);
154
+ fields[field.name] = value;
155
+ fieldOrder.push({ name: field.name, value });
156
+ addFieldToScope(scope, field.name, value);
157
+ });
158
+ } finally {
159
+ state.scope = previousScope;
160
+ }
161
+
162
+ const end = state.offset;
163
+
164
+ return {
165
+ kind: "struct",
166
+ typeName,
167
+ fields,
168
+ fieldOrder,
169
+ byteOffset: start,
170
+ byteLength: end - start,
171
+ rawHex: sliceHex(state.data, start, end),
172
+ };
173
+ }
174
+
175
+ function decodeArray(array: ArrayType, state: DecodeState, context: string, typeName?: string): DecodedArrayValue {
176
+ const lengthBigInt = evaluateExpression(array.size, state.scope, `${context}[size]`, state.registry);
177
+ const length = bigintToLength(lengthBigInt, context);
178
+ const elements: DecodedValue[] = [];
179
+ const start = state.offset;
180
+
181
+ for (let i = 0; i < length; i++) {
182
+ const elementContext = `${context}[${i}]`;
183
+ elements.push(decodeKind(array.elementType, state, elementContext));
184
+ }
185
+
186
+ const end = state.offset;
187
+
188
+ return {
189
+ kind: "array",
190
+ typeName,
191
+ length,
192
+ elements,
193
+ byteOffset: start,
194
+ byteLength: end - start,
195
+ rawHex: sliceHex(state.data, start, end),
196
+ };
197
+ }
198
+
199
+ function decodeEnum(enumType: EnumType, state: DecodeState, context: string, typeName?: string): DecodedEnumValue {
200
+ const tagBigInt = evaluateExpression(enumType.tagExpression, state.scope, `${context}[tag]`, state.registry);
201
+ const tagValue = bigintToNumber(tagBigInt, context);
202
+ const variant = enumType.variants.find((entry) => entry.tagValue === tagValue);
203
+ if (!variant) {
204
+ throw new AbiDecodeError(`Enum '${context}' has no variant with tag ${tagValue}`, {
205
+ availableVariants: enumType.variants.map((entry) => entry.tagValue),
206
+ });
207
+ }
208
+
209
+ const start = state.offset;
210
+ const value = decodeKind(variant.type, state, `${context}.${variant.name}`);
211
+ const end = state.offset;
212
+
213
+ return {
214
+ kind: "enum",
215
+ typeName,
216
+ tagValue,
217
+ variantName: variant.name,
218
+ value,
219
+ byteOffset: start,
220
+ byteLength: end - start,
221
+ rawHex: sliceHex(state.data, start, end),
222
+ };
223
+ }
224
+
225
+ function decodeUnion(union: UnionType, state: DecodeState, context: string, typeName?: string): DecodedUnionValue {
226
+ const start = state.offset;
227
+ const variantViews: DecodedField[] = [];
228
+ let unionSize = 0;
229
+
230
+ for (const variant of union.variants) {
231
+ const preview = previewDecode(variant.type, state, `${context}.${variant.name}`, start);
232
+ if (preview.value) {
233
+ unionSize = Math.max(unionSize, preview.size ?? 0);
234
+ variantViews.push({ name: variant.name, value: preview.value });
235
+ } else if (preview.error) {
236
+ variantViews.push({
237
+ name: variant.name,
238
+ value: createOpaqueValue(preview.error.message, state.data, start, start, variant.name),
239
+ });
240
+ }
241
+ }
242
+
243
+ ensureAvailable(state, unionSize, context);
244
+ state.offset = start + unionSize;
245
+
246
+ return {
247
+ kind: "union",
248
+ typeName,
249
+ variants: variantViews,
250
+ note: "Union decoding is ambiguous; showing all variant interpretations.",
251
+ byteOffset: start,
252
+ byteLength: unionSize,
253
+ rawHex: sliceHex(state.data, start, start + unionSize),
254
+ };
255
+ }
256
+
257
+ function decodeSizeDiscriminatedUnion(
258
+ union: SizeDiscriminatedUnionType,
259
+ state: DecodeState,
260
+ context: string,
261
+ typeName?: string,
262
+ byteBudget?: number,
263
+ ): DecodedSizeDiscriminatedUnionValue {
264
+ const start = state.offset;
265
+ const matches: Array<{ variantName: string; value: DecodedValue; size: number; expected: number }> = [];
266
+ const attempts: Record<string, string> = {};
267
+
268
+ for (const variant of union.variants) {
269
+ if (byteBudget !== undefined && variant.expectedSize > byteBudget) {
270
+ continue;
271
+ }
272
+ const preview = previewDecode(variant.type, state, `${context}.${variant.name}`, start, variant.expectedSize);
273
+ if (preview.value && preview.size === variant.expectedSize) {
274
+ matches.push({ variantName: variant.name, value: preview.value, size: preview.size ?? 0, expected: variant.expectedSize });
275
+ } else if (preview.error) {
276
+ attempts[variant.name] = preview.error.message;
277
+ }
278
+ }
279
+
280
+ if (matches.length === 0) {
281
+ throw new AbiDecodeError(`No size-discriminated union variant in '${context}' matched the provided data`, {
282
+ attempts,
283
+ });
284
+ }
285
+ let winner = matches[0];
286
+ if (matches.length > 1) {
287
+ if (byteBudget !== undefined) {
288
+ const exact = matches.filter((match) => match.expected === byteBudget);
289
+ if (exact.length === 1) {
290
+ winner = exact[0];
291
+ } else {
292
+ throw new AbiDecodeError(`Multiple size-discriminated union variants in '${context}' matched the provided data`, {
293
+ matches: matches.map((match) => match.variantName),
294
+ });
295
+ }
296
+ } else {
297
+ throw new AbiDecodeError(`Multiple size-discriminated union variants in '${context}' matched the provided data`, {
298
+ matches: matches.map((match) => match.variantName),
299
+ });
300
+ }
301
+ }
302
+ ensureAvailable(state, winner.size, context);
303
+ state.offset = start + winner.size;
304
+
305
+ return {
306
+ kind: "size-discriminated-union",
307
+ typeName,
308
+ variantName: winner.variantName,
309
+ expectedSize: winner.expected,
310
+ value: winner.value,
311
+ byteOffset: start,
312
+ byteLength: winner.size,
313
+ rawHex: sliceHex(state.data, start, start + winner.size),
314
+ };
315
+ }
316
+
317
+ function previewDecode(kind: TypeKind, state: DecodeState, context: string, start: number, limit?: number): PreviewResult {
318
+ const snapshotOffset = state.offset;
319
+ const snapshotScope = state.scope;
320
+ const snapshotView = state.view;
321
+ const snapshotData = state.data;
322
+
323
+ if (limit !== undefined) {
324
+ if (start + limit > snapshotView.byteLength) {
325
+ return { error: new AbiDecodeError(`Variant '${context}' requires ${limit} bytes but only ${snapshotView.byteLength - start} remain`) };
326
+ }
327
+ state.view = new DataView(snapshotView.buffer, snapshotView.byteOffset + start, limit);
328
+ state.data = new Uint8Array(snapshotData.buffer, snapshotData.byteOffset + start, limit);
329
+ state.offset = 0;
330
+ } else {
331
+ state.offset = start;
332
+ }
333
+
334
+ try {
335
+ const value = decodeKind(kind, state, context);
336
+ const size = limit !== undefined ? state.offset : state.offset - start;
337
+ return { value, size };
338
+ } catch (error) {
339
+ if (error instanceof AbiDecodeError) {
340
+ return { error };
341
+ }
342
+ return { error: new AbiDecodeError((error as Error).message ?? `Failed to decode variant for ${context}`) };
343
+ } finally {
344
+ state.offset = snapshotOffset;
345
+ state.scope = snapshotScope;
346
+ state.view = snapshotView;
347
+ state.data = snapshotData;
348
+ }
349
+ }
350
+
351
+ function bigintToLength(length: bigint, context: string): number {
352
+ if (length < 0n) {
353
+ throw new AbiDecodeError(`Array length expression in '${context}' evaluated to a negative value`);
354
+ }
355
+ const number = Number(length);
356
+ if (!Number.isSafeInteger(number)) {
357
+ throw new AbiDecodeError(`Array length expression in '${context}' exceeds JavaScript's safe integer range`);
358
+ }
359
+ return number;
360
+ }
361
+
362
+ function bigintToNumber(value: bigint, context: string): number {
363
+ const number = Number(value);
364
+ if (!Number.isSafeInteger(number)) {
365
+ throw new AbiDecodeError(`Expression in '${context}' resulted in a value outside of JS safe integer range`);
366
+ }
367
+ return number;
368
+ }
369
+
370
+ function ensureAvailable(state: DecodeState, size: number, context: string) {
371
+ if (state.offset + size > state.view.byteLength) {
372
+ throw new AbiDecodeError(`Insufficient data while decoding '${context}'`, {
373
+ requested: size,
374
+ remaining: state.view.byteLength - state.offset,
375
+ });
376
+ }
377
+ }
378
+
379
+ function sliceHex(buffer: Uint8Array, start: number, end: number): string {
380
+ return bytesToHex(buffer.subarray(start, end));
381
+ }
382
+
383
+ function createOpaqueValue(description: string, buffer: Uint8Array, start: number, end: number, typeName?: string) {
384
+ return {
385
+ kind: "opaque",
386
+ description,
387
+ byteOffset: start,
388
+ byteLength: end - start,
389
+ rawHex: sliceHex(buffer, start, end),
390
+ typeName,
391
+ } as const;
392
+ }
393
+
394
+ function computeTrailingConstantSizes(struct: StructType, registry: TypeRegistry): Array<number | null> {
395
+ const memo = new Map<string, number | null>();
396
+ const sizes: Array<number | null> = [];
397
+ for (let i = 0; i < struct.fields.length; i++) {
398
+ let total = 0;
399
+ let deterministic = true;
400
+ for (let j = i + 1; j < struct.fields.length; j++) {
401
+ const size = getConstSize(struct.fields[j].type, registry, memo, new Set());
402
+ if (size === null) {
403
+ deterministic = false;
404
+ break;
405
+ }
406
+ total += size;
407
+ }
408
+ sizes.push(deterministic ? total : null);
409
+ }
410
+ return sizes;
411
+ }
412
+
413
+ export function getConstSize(
414
+ kind: TypeKind,
415
+ registry: TypeRegistry,
416
+ memo: Map<string, number | null>,
417
+ stack: Set<string>,
418
+ ): number | null {
419
+ switch (kind.kind) {
420
+ case "primitive":
421
+ return primitiveInfo[kind.primitive].byteLength;
422
+ case "array": {
423
+ const elementSize = getConstSize(kind.elementType, registry, memo, stack);
424
+ if (elementSize === null) return null;
425
+ const length = evaluateConstExpression(kind.size);
426
+ if (length === null) return null;
427
+ return elementSize * Number(length);
428
+ }
429
+ case "struct": {
430
+ let total = 0;
431
+ for (const field of kind.fields) {
432
+ const size = getConstSize(field.type, registry, memo, stack);
433
+ if (size === null) return null;
434
+ total += size;
435
+ }
436
+ return total;
437
+ }
438
+ case "enum": {
439
+ let variantSize: number | null = null;
440
+ for (const variant of kind.variants) {
441
+ const size = getConstSize(variant.type, registry, memo, stack);
442
+ if (size === null) return null;
443
+ if (variantSize === null) variantSize = size;
444
+ else if (variantSize !== size) return null;
445
+ }
446
+ return variantSize;
447
+ }
448
+ case "union": {
449
+ let maxSize = 0;
450
+ for (const variant of kind.variants) {
451
+ const size = getConstSize(variant.type, registry, memo, stack);
452
+ if (size === null) return null;
453
+ maxSize = Math.max(maxSize, size);
454
+ }
455
+ return maxSize;
456
+ }
457
+ case "size-discriminated-union":
458
+ return null;
459
+ case "type-ref":
460
+ if (memo.has(kind.name)) {
461
+ return memo.get(kind.name) ?? null;
462
+ }
463
+ if (stack.has(kind.name)) {
464
+ return null;
465
+ }
466
+ stack.add(kind.name);
467
+ const resolved = registry.get(kind.name);
468
+ const resolvedSize = getConstSize(resolved.kind, registry, memo, stack);
469
+ stack.delete(kind.name);
470
+ memo.set(kind.name, resolvedSize);
471
+ return resolvedSize;
472
+ default:
473
+ kind satisfies never;
474
+ return null;
475
+ }
476
+ }
477
+
478
+ function evaluateConstExpression(expression: Expression): bigint | null {
479
+ switch (expression.type) {
480
+ case "literal":
481
+ return expression.value;
482
+ case "binary": {
483
+ const left = evaluateConstExpression(expression.left);
484
+ const right = evaluateConstExpression(expression.right);
485
+ if (left === null || right === null) return null;
486
+ switch (expression.op) {
487
+ case "add":
488
+ return left + right;
489
+ case "sub":
490
+ return left - right;
491
+ case "mul":
492
+ return left * right;
493
+ case "div":
494
+ if (right === 0n) return null;
495
+ return left / right;
496
+ default:
497
+ return null;
498
+ }
499
+ }
500
+ case "field-ref":
501
+ case "unary":
502
+ case "sizeof":
503
+ case "alignof":
504
+ return null;
505
+ default:
506
+ expression satisfies never;
507
+ return null;
508
+ }
509
+ }
510
+ type PrimitiveReadFn = (view: DataView, offset: number) => number | bigint;
511
+
512
+ const primitiveInfo: Record<
513
+ PrimitiveName,
514
+ {
515
+ byteLength: number;
516
+ read: PrimitiveReadFn;
517
+ }
518
+ > = {
519
+ u8: { byteLength: 1, read: (view, offset) => view.getUint8(offset) },
520
+ i8: { byteLength: 1, read: (view, offset) => view.getInt8(offset) },
521
+ u16: { byteLength: 2, read: (view, offset) => view.getUint16(offset, true) },
522
+ i16: { byteLength: 2, read: (view, offset) => view.getInt16(offset, true) },
523
+ u32: { byteLength: 4, read: (view, offset) => view.getUint32(offset, true) },
524
+ i32: { byteLength: 4, read: (view, offset) => view.getInt32(offset, true) },
525
+ u64: { byteLength: 8, read: (view, offset) => view.getBigUint64(offset, true) },
526
+ i64: { byteLength: 8, read: (view, offset) => view.getBigInt64(offset, true) },
527
+ f32: { byteLength: 4, read: (view, offset) => view.getFloat32(offset, true) },
528
+ f64: { byteLength: 8, read: (view, offset) => view.getFloat64(offset, true) },
529
+ f16: {
530
+ byteLength: 2,
531
+ read: (view, offset) => {
532
+ // Read as u16, treat as opaque f16 for now (no native JS f16 support yet)
533
+ // Future: convert to f32 using a helper if needed
534
+ return view.getUint16(offset, true);
535
+ },
536
+ },
537
+ };
538
+
539
+ export function getTypeAlignment(kind: TypeKind, registry: TypeRegistry): number {
540
+ switch (kind.kind) {
541
+ case "primitive":
542
+ return primitiveInfo[kind.primitive].byteLength;
543
+ case "struct":
544
+ if (kind.attributes.aligned > 0) return kind.attributes.aligned;
545
+ // Default struct alignment is max of its fields' alignments
546
+ // If packed, alignment is 1 (but we only call this if !packed usually)
547
+ // However, standard C rules say struct alignment is max field alignment
548
+ return kind.fields.reduce((max, field) => Math.max(max, getTypeAlignment(field.type, registry)), 1);
549
+ case "array":
550
+ return getTypeAlignment(kind.elementType, registry);
551
+ case "enum":
552
+ // Alignment of the tag? Or the variants?
553
+ // Enums in ABI usually are just the tag + payload.
554
+ // Alignment is likely determined by the tag type or the union of variants?
555
+ // Rust implementation suggests it aligns based on the tag type first?
556
+ // Actually, for a safe default, we can assume alignment of the largest member or just 1 if packed.
557
+ // Let's check if it has explicit alignment
558
+ if (kind.attributes.aligned > 0) return kind.attributes.aligned;
559
+ return 1; // Fallback
560
+ case "union":
561
+ if (kind.attributes.aligned > 0) return kind.attributes.aligned;
562
+ return kind.variants.reduce((max, variant) => Math.max(max, getTypeAlignment(variant.type, registry)), 1);
563
+ case "size-discriminated-union":
564
+ if (kind.attributes.aligned > 0) return kind.attributes.aligned;
565
+ return kind.variants.reduce((max, variant) => Math.max(max, getTypeAlignment(variant.type, registry)), 1);
566
+ case "type-ref":
567
+ return getTypeAlignment(registry.get(kind.name).kind, registry);
568
+ default:
569
+ return 1;
570
+ }
571
+ }
572
+
package/src/errors.ts ADDED
@@ -0,0 +1,32 @@
1
+ export type AbiErrorCode = "PARSE_ERROR" | "VALIDATION_ERROR" | "DECODE_ERROR";
2
+
3
+ export class AbiError extends Error {
4
+ readonly code: AbiErrorCode;
5
+ readonly details?: Record<string, unknown>;
6
+
7
+ constructor(code: AbiErrorCode, message: string, details?: Record<string, unknown>) {
8
+ super(message);
9
+ this.code = code;
10
+ this.details = details;
11
+ this.name = this.constructor.name;
12
+ }
13
+ }
14
+
15
+ export class AbiParseError extends AbiError {
16
+ constructor(message: string, details?: Record<string, unknown>) {
17
+ super("PARSE_ERROR", message, details);
18
+ }
19
+ }
20
+
21
+ export class AbiValidationError extends AbiError {
22
+ constructor(message: string, details?: Record<string, unknown>) {
23
+ super("VALIDATION_ERROR", message, details);
24
+ }
25
+ }
26
+
27
+ export class AbiDecodeError extends AbiError {
28
+ constructor(message: string, details?: Record<string, unknown>) {
29
+ super("DECODE_ERROR", message, details);
30
+ }
31
+ }
32
+