@thru/abi 0.1.30 → 0.1.34

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 CHANGED
@@ -1,174 +1,111 @@
1
- # @thru/abi – TypeScript ABI Reflection Layer
1
+ # @thru/abi – WASM-backed ABI reflection
2
2
 
3
- This package provides a browser-friendly ABI reflection+decoding engine that matches the thru-net on-chain ABI semantics. The goal is to let Explorer-grade UIs take any **flattened ABI YAML** plus raw account bytes and produce fully structured, well-annotated decoded values without generating code ahead of time.
4
-
5
- The README is intentionally verbose so a future engineer (human or AI) can understand the full behavior surface without spelunking through source.
3
+ This package is now a thin TypeScript wrapper around the Rust `abi_reflect`
4
+ runtime. We compile the Rust crate to WebAssembly, ship both Node + bundler
5
+ targets, and expose a small async API for reflecting ABI YAML + binary payloads.
6
+ All layout math and validation run inside the Rust engine so TypeScript stays
7
+ lightweight and automatically inherits IR upgrades.
6
8
 
7
9
  ---
8
10
 
9
- ## Quick Start
11
+ ## Quick start
10
12
 
11
13
  ```ts
12
- import { decodeData } from "@thru/abi";
13
- import myAbi from "./abi/counter.abi.yaml?raw";
14
+ import { ensureWasmLoaded, formatReflection, reflect } from "@thru/abi";
15
+ import tokenAbi from "./abi/token_program.abi.yaml?raw";
14
16
 
15
- const decoded = decodeData(myAbi, "CounterAccount", accountDataUint8Array);
17
+ async function example() {
18
+ await ensureWasmLoaded(); // formatter + reflector live inside WASM
16
19
 
17
- if (decoded.kind === "struct") {
18
- console.log(decoded.fields.count);
19
- }
20
- ```
20
+ const payload = new Uint8Array([0x01, 0, 0, 0, 0, 0, 0, 0]);
21
+ const reflection = await reflect(tokenAbi, "TokenInstruction", {
22
+ type: "binary",
23
+ value: payload,
24
+ });
21
25
 
22
- * `decodeData(yamlText, typeName, data)` is the only public runtime API.
23
- * `yamlText` **must be flattened** (imports already resolved). This is enforced at parse time.
24
- * The returned `DecodedValue` tree contains both semantic data and raw byte slices for UI inspection.
26
+ console.log(reflection.value); // JSON emitted by abi_reflect
25
27
 
26
- ---
28
+ // Collapse the verbose JSON tree into something human-readable
29
+ const formatted = formatReflection(reflection);
30
+ console.log(formatted.value.payload.variant); // "initialize_mint"
31
+ }
27
32
 
28
- ## Package Layout
33
+ example();
34
+ ```
29
35
 
30
- | Path | Purpose |
31
- | ---- | ------- |
32
- | `src/abiSchema.ts` | YAML parser + TypeScript interfaces aligned with the thru-net ABI schema. |
33
- | `src/typeRegistry.ts` | Builds validated registry, resolves type refs, detects cycles. |
34
- | `src/decoder.ts` | Core reflection engine. Handles arrays, structs, unions, enums, SDUs, padding, f16, etc. |
35
- | `src/expression.ts` | Evaluates ABI expressions (field refs, arithmetic, bitwise, sizeof/alignof). |
36
- | `src/decodedValue.ts` | Canonical decoded shape consumed by Explorer UI. |
37
- | `test/` | Hand-authored fixtures mirroring Rust compliance tests (e.g., `structs.abi.yaml`). |
36
+ * The ABI text **must already be flattened** (imports resolved). The Rust
37
+ resolver enforces this.
38
+ * Results are JSON blobs straight from `serde_json`. They include the full type
39
+ info + value trees used by the CLI tooling.
38
40
 
39
41
  ---
40
42
 
41
- ## Feature Matrix
42
-
43
- ### ✅ Implemented
44
-
45
- * **Schema Parsing**
46
- * Matches the thru-net ABI AST (primitives, structs, arrays, unions, enums, size-discriminated unions, type-refs).
47
- * Validates flattened files (no `imports`), duplicate names, dangling refs, type cycles.
48
-
49
- * **Expression Engine**
50
- * Literals (u/i 8–64), field references with lexical scopes (`["..","parent"]` supported).
51
- * Arithmetic: `add`, `sub`, `mul`, `div`, `mod`.
52
- * Bitwise: `bit-and`, `bit-or`, `bit-xor`, `bit-not`, `left-shift`, `right-shift`.
53
- * Meta: `sizeof(type-name)`, `alignof(type-name)` leveraging shared footprint helpers.
54
-
55
- * **Decoding Semantics**
56
- * **Structs:** assumes `packed: true` containers (our blockchain layout never inserts padding). `aligned` overrides are reserved for future use.
57
- * **Arrays:** dynamic element count via expressions referencing previously decoded fields.
58
- * **Enums:** tag derived from expressions; variant payload decoded inline.
59
- * **Unions:** best-effort “preview all variants” strategy—each variant is decoded in isolation, results presented side-by-side (important for Explorer UX).
60
- * **Size-Discriminated Unions:** tries each variant with byte budgets, supports placement mid-struct by reserving trailing fixed sizes.
61
- * **Type-Refs:** recursion-safe (cycle detection done in registry); `decodeKind` transparently resolves nested refs.
62
- * **Primitives:** all integer + float types from ABI spec, including `f16` (returned as `number` representing raw `u16` for now).
63
-
64
- * **DecodedValue Shape**
65
- * Each node exposes `kind`, `typeName`, `byteOffset`, `byteLength`, `rawHex`.
66
- * Structs provide both `fields` (object) and `fieldOrder` (array preserving declaration order).
67
- * Arrays expose `length`, `elements`.
68
- * Enums/Unions/SDUs capture tag or variant metadata.
69
- * When something can’t be safely decoded (e.g., ambiguous union variant), an `opaque` node contains context + `rawHex`.
70
-
71
- * **Testing Harness**
72
- * Uses `tsx`/`vitest`. The repo includes sample fixture script `test/verify-rectangle.ts` showing end-to-end usage.
73
- * Additional tests exist under `src/index.test.ts` covering primitives, arrays, expressions, unions, etc.
74
-
75
- ### ⚠️ Known Limitations (as of this snapshot)
76
-
77
- * **Runtime Performance**
78
- * YAML is parsed on every `decodeData` call. No caching or schema memoization yet.
79
- * Expressions evaluate with BigInt math. That’s correct but slower than precomputed constants.
80
-
81
- * **Instruction Decoding**
82
- * Current focus is account data. Instruction decoding isn’t implemented yet (needs call-site context + discriminants).
83
-
84
- * **Union Heuristics**
85
- * Unlike Rust (which requires external hints), we decode *every* variant. That’s user-friendly but does not pick a “canonical” variant automatically. Upstream UI must choose how to present ambiguous unions.
43
+ ## Public API
86
44
 
87
- * **Float16 Conversion**
88
- * We currently return the raw `u16` bits. A helper to convert to IEEE-754 half floats can be added later if needed for display accuracy.
45
+ | Function | Description |
46
+ | --- | --- |
47
+ | `reflect(abi: string, typeName: string, payload: { type: "binary", value: BinaryLike } \| { type: "hex", value: string })` | Reflects binary data (or hex) and returns the parsed JSON payload. |
48
+ | `formatReflection(raw: JsonValue)` | Delegates to the WASM formatter to collapse verbose JSON trees. Requires `ensureWasmLoaded()` (or any prior call to `reflect`) before use. |
49
+ | `buildLayoutIr(abi: string)` | Runs the shared Layout IR builder and returns the serialized IR document (schema version, per-type expressions, parameters). |
50
+ | `ensureWasmLoaded()` | Preloads the WASM bindings for callers that want to pay the initialization cost up-front. `reflect` calls it lazily. |
89
51
 
90
- * **Error Surfacing**
91
- * Errors throw `AbiParseError`, `AbiValidationError`, or `AbiDecodeError`. Explorer code should catch and surface the message. There’s no structured warning channel yet (e.g., partial decodes).
92
-
93
- * **Security / Untrusted ABIs**
94
- * The parser enforces flattened files and forbids unknown fields, but we still assume ABIs come from trusted sources. Malicious ABIs could attempt to trick UIs (e.g., wrong type names). A future enhancement could add allow-lists or signatures.
52
+ All helpers are async, because loading + instantiating the WASM module can touch
53
+ the filesystem (Node) or issue dynamic imports (bundlers).
95
54
 
96
55
  ---
97
56
 
98
- ## How Decoding Works (Step-by-step)
99
-
100
- 1. **Parse & Validate**
101
- * `parseAbiDocument` -> `AbiDocument`.
102
- * `buildTypeRegistry` indexes types, verifies refs, catches cycles.
57
+ ## WASM workflow
103
58
 
104
- 2. **Prepare State**
105
- * `decodeData` creates `DecodeState` (`Uint8Array`, `DataView`, root scope).
59
+ The generated artifacts live under `web/packages/abi/wasm/{bundler,node}` and are
60
+ checked in so the package works without a local Rust toolchain. When
61
+ `abi_reflect` or the shared IR changes, rebuild everything with:
106
62
 
107
- 3. **Walk Type Tree**
108
- * `decodeKind` dispatches by `kind` (primitive, struct, array, etc.).
109
- * Each decoder carries a byte budget so flexible members can live mid-struct while respecting trailing fixed-size fields.
110
-
111
- 4. **Field Scope & Expressions**
112
- * After each field decode, `addFieldToScope` records the result so later `field-ref`s can use it.
113
- * Expressions are evaluated lazily during decoding (e.g., array lengths, enum tags).
63
+ ```bash
64
+ # From repo root
65
+ pnpm --filter @thru/abi run build:wasm
66
+ ```
114
67
 
115
- 5. **Alignment Rules**
116
- * Structs are emitted and decoded as `packed: true`, so offsets advance exactly by the previous field’s byte length.
117
- * Alignment metadata is currently ignored because thru-net ABIs never request padding.
68
+ That script runs `wasm-pack build` twice (bundler + node targets) inside
69
+ `abi/abi_reflect_wasm`, then copies the fresh outputs into
70
+ `web/packages/abi/wasm`. The regular `pnpm --filter @thru/abi build` step runs
71
+ `tsup` and copies those WASM folders into `dist/wasm` so published packages
72
+ resolve the dynamic imports automatically.
118
73
 
119
- 6. **Result Assembly**
120
- * Every decoded chunk captures offset, size, and raw hex slice so UIs can show byte-level views alongside structured data.
74
+ When developing inside the monorepo, Vitest loads the TypeScript sources
75
+ directly. The runtime detects when it is executing from `src/` and reaches for
76
+ `../wasm`, so make sure the synced artifacts exist before running the tests.
121
77
 
122
78
  ---
123
79
 
124
- ## CLI / Development Workflow
80
+ ## Testing
125
81
 
126
82
  ```bash
127
- # Install deps
128
- pnpm install
129
-
130
- # Build ESM bundle + type declarations
131
- pnpm --filter @thru/abi build
132
-
133
- # Run test suite (vitest)
134
83
  pnpm --filter @thru/abi test
135
-
136
- # One-off verification script example
137
- npx tsx web/packages/abi/test/verify-rectangle.ts
138
84
  ```
139
85
 
140
- CI typically runs `build` and `test`. The package outputs ESM suitable for modern bundlers (Vite, Next.js, etc.).
86
+ Vitest exercises both `reflectHex` and `reflectBinary` against the
87
+ `SimpleStruct` compliance ABI plus `buildLayoutIr` to ensure the WASM bridge is
88
+ wired correctly. If you tweak the Rust runtime, rerun `pnpm build:wasm` so the
89
+ tests pick up the updated binaries.
141
90
 
142
91
  ---
143
92
 
144
- ## Integration Guidance
93
+ ## Development notes
145
94
 
146
- * **Explorer**
147
- * Keep ABIs as `.yaml` + `.bin` fixtures or embed them using `?raw`.
148
- * Wrap `decodeData` in a try/catch; surface errors in the UI.
149
- * Use `fieldOrder` to render tables with deterministic ordering.
150
- * Use `rawHex` for fallback views when a field type is `opaque`.
151
-
152
- * **Other Consumers**
153
- * Libraries/tools can reuse `parseAbiDocument` + `TypeRegistry` to build higher-level abstractions (e.g., caching parsed schemas, precomputing footprints).
154
-
155
- ---
156
-
157
- ## Future Roadmap Ideas
158
-
159
- 1. **Schema Caching** – hash YAML text and reuse parsed `AbiDocument`/`TypeRegistry`.
160
- 2. **Instruction Decoding** – need discriminants + context (program ID, variant mapping).
161
- 3. **Float16 Conversion Helper** – convert raw u16 to JS number with proper rounding.
162
- 4. **Diagnostics API** – return warnings for ambiguous unions, unsupported expressions, etc., instead of throwing.
163
- 5. **Web Worker Integration** – offload heavy decodes to worker threads for large accounts.
95
+ * The TypeScript surface intentionally stays tiny; we no longer export the old
96
+ decoder/resolver classes. Future code should talk to the WASM bridge instead
97
+ of re-implementing reflection logic in JS.
98
+ * Browser vs. Node detection happens in `src/wasmBridge.ts`. Node loads the
99
+ `wasm/node` build via `createRequire`, while bundlers dynamically import the
100
+ `wasm/bundler` module.
101
+ * The JSON shape returned by `reflect*` matches `abi_reflect`'s CLI output, so
102
+ parity debugging can use `abi/scripts/show_reflection.py`.
103
+ * Layout IR consumers can feed `buildLayoutIr` into caches or ship a prebuilt
104
+ snapshot alongside the WASM runtime to guard against future schema changes.
164
105
 
165
106
  ---
166
107
 
167
- ## Support / Contact
168
-
169
- * Code owner: thru-net Explorer team.
170
- * If something decodes differently from the Rust validator, cross-check against the reference implementation under `abi/abi_reflect` and open an issue referencing the specific ABI + binary pair.
171
- * When adding new ABI features, update both this README and `GAPS_AND_PLAN.md` so future iterations know the exact capability boundaries.
172
-
173
- Happy decoding! 🎯
174
-
108
+ Questions? Ping the thru-net ABI team (same folks maintaining
109
+ `abi/abi_reflect`). Whenever you extend the Rust reflection engine or shared IR,
110
+ regenerate the WASM artifacts and mention the change in `enums-fam-impl.md` so
111
+ tooling consumers know which version to depend on.
package/dist/index.d.ts CHANGED
@@ -1,185 +1,71 @@
1
- type PrimitiveName = "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f16" | "f32" | "f64";
2
- interface ContainerAttributes {
3
- packed: boolean;
4
- aligned: number;
5
- comment?: string;
6
- }
7
- interface AbiMetadata {
8
- package: string;
9
- abiVersion: number;
10
- packageVersion?: string;
11
- description?: string;
12
- }
13
- interface AbiDocument {
14
- metadata: AbiMetadata;
15
- types: TypeDefinition[];
16
- }
17
- interface TypeDefinition {
18
- name: string;
19
- kind: TypeKind;
20
- }
21
- type TypeKind = PrimitiveType | StructType | ArrayType | EnumType | UnionType | SizeDiscriminatedUnionType | TypeRefType;
22
- interface PrimitiveType {
23
- kind: "primitive";
24
- primitive: PrimitiveName;
25
- }
26
- interface StructField {
27
- name: string;
28
- type: TypeKind;
29
- }
30
- interface StructType {
31
- kind: "struct";
32
- attributes: ContainerAttributes;
33
- fields: StructField[];
34
- }
35
- interface ArrayType {
36
- kind: "array";
37
- attributes: ContainerAttributes;
38
- elementType: TypeKind;
39
- size: Expression;
40
- }
41
- interface EnumVariant {
42
- name: string;
43
- tagValue: number;
44
- type: TypeKind;
45
- }
46
- interface EnumType {
47
- kind: "enum";
48
- attributes: ContainerAttributes;
49
- tagExpression: Expression;
50
- variants: EnumVariant[];
51
- }
52
- interface UnionVariant {
53
- name: string;
54
- type: TypeKind;
55
- }
56
- interface UnionType {
57
- kind: "union";
58
- attributes: ContainerAttributes;
59
- variants: UnionVariant[];
60
- }
61
- interface SizeDiscriminatedVariant {
62
- name: string;
63
- expectedSize: number;
64
- type: TypeKind;
65
- }
66
- interface SizeDiscriminatedUnionType {
67
- kind: "size-discriminated-union";
68
- attributes: ContainerAttributes;
69
- variants: SizeDiscriminatedVariant[];
70
- }
71
- interface TypeRefType {
72
- kind: "type-ref";
73
- name: string;
74
- }
75
- type Expression = LiteralExpression | FieldRefExpression | BinaryExpression | UnaryExpression | SizeOfExpression | AlignOfExpression;
76
- interface LiteralExpression {
77
- type: "literal";
78
- literalType: PrimitiveName;
79
- value: bigint;
80
- }
81
- interface FieldRefExpression {
82
- type: "field-ref";
83
- path: string[];
84
- }
85
- type BinaryOperator = "add" | "sub" | "mul" | "div" | "mod" | "bit-and" | "bit-or" | "bit-xor" | "left-shift" | "right-shift";
86
- interface BinaryExpression {
87
- type: "binary";
88
- op: BinaryOperator;
89
- left: Expression;
90
- right: Expression;
91
- }
92
- type UnaryOperator = "bit-not";
93
- interface UnaryExpression {
94
- type: "unary";
95
- op: UnaryOperator;
96
- operand: Expression;
97
- }
98
- interface SizeOfExpression {
99
- type: "sizeof";
1
+ interface ByteRange {
2
+ offset: number;
3
+ size: number;
4
+ }
5
+ type FormattedValue = null | string | number | boolean | bigint | FormattedValue[] | {
6
+ [key: string]: FormattedValue;
7
+ } | {
8
+ variant: string;
9
+ value: FormattedValue | null;
10
+ };
11
+ type FormattedValueWithByteRange = {
12
+ value: FormattedValue;
13
+ _byteRange: ByteRange;
14
+ } | {
15
+ hex: string;
16
+ _byteRange: ByteRange;
17
+ } | {
18
+ variant: string;
19
+ value: FormattedValue | null;
20
+ _byteRange: ByteRange;
21
+ };
22
+ interface FormattedReflection {
100
23
  typeName: string;
24
+ kind: string | null | undefined;
25
+ value: FormattedValue;
26
+ byteRange?: ByteRange;
101
27
  }
102
- interface AlignOfExpression {
103
- type: "alignof";
104
- typeName: string;
105
- }
106
- declare function parseAbiDocument(yamlText: string): AbiDocument;
107
28
 
108
- type DecodedValueKind = "primitive" | "struct" | "array" | "enum" | "union" | "size-discriminated-union" | "opaque";
109
- interface BaseDecodedValue {
110
- kind: DecodedValueKind;
111
- typeName?: string;
112
- byteOffset: number;
113
- byteLength: number;
114
- rawHex: string;
115
- }
116
- interface DecodedPrimitiveValue extends BaseDecodedValue {
117
- kind: "primitive";
118
- primitiveType: PrimitiveName;
119
- value: number | bigint;
120
- }
121
- interface DecodedField {
122
- name: string;
123
- value: DecodedValue;
124
- }
125
- interface DecodedStructValue extends BaseDecodedValue {
126
- kind: "struct";
127
- fields: Record<string, DecodedValue>;
128
- fieldOrder: DecodedField[];
129
- }
130
- interface DecodedArrayValue extends BaseDecodedValue {
131
- kind: "array";
132
- length: number;
133
- elements: DecodedValue[];
134
- }
135
- interface DecodedEnumValue extends BaseDecodedValue {
136
- kind: "enum";
137
- tagValue: number;
138
- variantName: string;
139
- value: DecodedValue | null;
140
- }
141
- interface DecodedUnionValue extends BaseDecodedValue {
142
- kind: "union";
143
- variants: DecodedField[];
144
- note?: string;
145
- }
146
- interface DecodedSizeDiscriminatedUnionValue extends BaseDecodedValue {
147
- kind: "size-discriminated-union";
148
- variantName: string;
149
- expectedSize: number;
150
- value: DecodedValue;
151
- }
152
- interface DecodedOpaqueValue extends BaseDecodedValue {
153
- kind: "opaque";
154
- description: string;
155
- }
156
- type DecodedValue = DecodedPrimitiveValue | DecodedStructValue | DecodedArrayValue | DecodedEnumValue | DecodedUnionValue | DecodedSizeDiscriminatedUnionValue | DecodedOpaqueValue;
157
-
158
- declare class TypeRegistry {
159
- private readonly types;
160
- constructor(definitions: Iterable<TypeDefinition>);
161
- get(typeName: string): TypeDefinition;
162
- has(typeName: string): boolean;
163
- entries(): IterableIterator<[string, TypeDefinition]>;
164
- }
165
- declare function buildTypeRegistry(document: AbiDocument): TypeRegistry;
166
-
167
- declare function decodeData(yamlText: string, typeName: string, data: Uint8Array): DecodedValue;
168
-
169
- type AbiErrorCode = "PARSE_ERROR" | "VALIDATION_ERROR" | "DECODE_ERROR";
170
- declare class AbiError extends Error {
171
- readonly code: AbiErrorCode;
172
- readonly details?: Record<string, unknown>;
173
- constructor(code: AbiErrorCode, message: string, details?: Record<string, unknown>);
174
- }
175
- declare class AbiParseError extends AbiError {
176
- constructor(message: string, details?: Record<string, unknown>);
177
- }
178
- declare class AbiValidationError extends AbiError {
179
- constructor(message: string, details?: Record<string, unknown>);
180
- }
181
- declare class AbiDecodeError extends AbiError {
182
- constructor(message: string, details?: Record<string, unknown>);
183
- }
29
+ type JsonValue = unknown;
30
+ /**
31
+ * Configure the URL from which to load the WASM file.
32
+ * Must be called before any reflection functions are used.
33
+ *
34
+ * This is useful for environments like Next.js where bundler-based WASM loading
35
+ * doesn't work. Instead, copy the WASM file to your public directory and call:
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { configureWasm } from "@thru/abi";
40
+ * configureWasm("/wasm/abi_reflect_wasm_bg.wasm");
41
+ * ```
42
+ *
43
+ * @param url - URL or path to the WASM file (e.g., "/wasm/abi_reflect_wasm_bg.wasm")
44
+ */
45
+ declare function configureWasm(url: string): void;
46
+ type BinaryLike = Uint8Array | ArrayBuffer | ArrayBufferView | number[];
47
+ declare function reflect(abiYaml: string, typeName: string, payload: {
48
+ type: 'binary';
49
+ value: BinaryLike;
50
+ } | {
51
+ type: 'hex';
52
+ value: string;
53
+ }): Promise<JsonValue>;
54
+ declare function buildLayoutIr(abiYaml: string): Promise<JsonValue>;
55
+ type ReflectRootPayload = {
56
+ type: 'binary';
57
+ value: BinaryLike;
58
+ } | {
59
+ type: 'hex';
60
+ value: string;
61
+ };
62
+ declare function reflectInstruction(abiYaml: string, payload: ReflectRootPayload): Promise<JsonValue>;
63
+ declare function reflectAccount(abiYaml: string, payload: ReflectRootPayload): Promise<JsonValue>;
64
+ declare function reflectEvent(abiYaml: string, payload: ReflectRootPayload): Promise<JsonValue>;
65
+ declare function ensureWasmLoaded(): Promise<void>;
66
+ interface FormatOptions {
67
+ includeByteOffsets?: boolean;
68
+ }
69
+ declare function formatReflection(raw: JsonValue, options?: FormatOptions): FormattedReflection;
184
70
 
185
- export { AbiDecodeError, AbiError, AbiParseError, AbiValidationError, type DecodedArrayValue, type DecodedEnumValue, type DecodedField, type DecodedPrimitiveValue, type DecodedSizeDiscriminatedUnionValue, type DecodedStructValue, type DecodedUnionValue, type DecodedValue, TypeRegistry, buildTypeRegistry, decodeData, parseAbiDocument };
71
+ export { type ByteRange, type FormatOptions, type FormattedReflection, type FormattedValue, type FormattedValueWithByteRange, buildLayoutIr, configureWasm, ensureWasmLoaded, formatReflection, reflect, reflectAccount, reflectEvent, reflectInstruction };