@thru/abi 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Import Resolver
3
+ *
4
+ * Resolves ABI imports and builds a manifest for WASM consumption.
5
+ */
6
+
7
+ import * as yaml from "yaml";
8
+ import type {
9
+ ImportSource,
10
+ ImportSourceYaml,
11
+ PackageId,
12
+ ResolvedPackage,
13
+ ResolutionResult,
14
+ AbiMetadata,
15
+ AbiFile,
16
+ RpcEndpoints,
17
+ RevisionSpec,
18
+ } from "./types";
19
+ import { ResolutionError, DEFAULT_RPC_ENDPOINTS } from "./types";
20
+ import { OnchainFetcher, type OnchainFetcherConfig } from "./onchainFetcher";
21
+
22
+ export interface ResolverConfig {
23
+ onchainFetcher?: OnchainFetcher;
24
+ rpcEndpoints?: RpcEndpoints;
25
+ maxDepth?: number;
26
+ }
27
+
28
+ /**
29
+ * Resolve all imports for an ABI and return a manifest.
30
+ *
31
+ * The resolver only supports on-chain imports for browser environments.
32
+ * For path imports, use the CLI `bundle` command.
33
+ */
34
+ export async function resolveImports(
35
+ rootAbiYaml: string,
36
+ config: ResolverConfig = {}
37
+ ): Promise<ResolutionResult> {
38
+ const resolver = new ImportResolver(config);
39
+ return resolver.resolve(rootAbiYaml);
40
+ }
41
+
42
+ /**
43
+ * Create a manifest from an ABI and its resolved imports.
44
+ *
45
+ * The manifest maps package names to ABI YAML content.
46
+ */
47
+ export function createManifest(result: ResolutionResult): Record<string, string> {
48
+ return result.manifest;
49
+ }
50
+
51
+ class ImportResolver {
52
+ private onchainFetcher: OnchainFetcher;
53
+ private maxDepth: number;
54
+ private visited: Map<string, ResolvedPackage>;
55
+ private inProgress: Set<string>;
56
+
57
+ constructor(config: ResolverConfig = {}) {
58
+ this.onchainFetcher =
59
+ config.onchainFetcher ??
60
+ new OnchainFetcher({
61
+ rpcEndpoints: { ...DEFAULT_RPC_ENDPOINTS, ...config.rpcEndpoints },
62
+ });
63
+ this.maxDepth = config.maxDepth ?? 10;
64
+ this.visited = new Map();
65
+ this.inProgress = new Set();
66
+ }
67
+
68
+ async resolve(rootAbiYaml: string): Promise<ResolutionResult> {
69
+ this.visited.clear();
70
+ this.inProgress.clear();
71
+
72
+ const rootPackage = await this.resolvePackage(rootAbiYaml, false, 0);
73
+ const allPackages = Array.from(this.visited.values());
74
+ const manifest: Record<string, string> = {};
75
+
76
+ for (const pkg of allPackages) {
77
+ manifest[pkg.id.packageName] = pkg.abiYaml;
78
+ }
79
+
80
+ return {
81
+ root: rootPackage,
82
+ allPackages,
83
+ manifest,
84
+ };
85
+ }
86
+
87
+ private async resolvePackage(
88
+ abiYaml: string,
89
+ isRemote: boolean,
90
+ depth: number
91
+ ): Promise<ResolvedPackage> {
92
+ if (depth > this.maxDepth) {
93
+ throw new ResolutionError(
94
+ "CYCLIC_DEPENDENCY",
95
+ `Maximum resolution depth (${this.maxDepth}) exceeded`
96
+ );
97
+ }
98
+
99
+ const abiFile = this.parseAbiYaml(abiYaml);
100
+ const packageId = this.extractPackageId(abiFile);
101
+ const canonicalKey = `${packageId.packageName}@${packageId.version}`;
102
+
103
+ /* Check for cycle */
104
+ if (this.inProgress.has(canonicalKey)) {
105
+ throw new ResolutionError(
106
+ "CYCLIC_DEPENDENCY",
107
+ `Cyclic dependency detected: ${canonicalKey}`
108
+ );
109
+ }
110
+
111
+ /* Check if already resolved (keyed by name@version to match inProgress) */
112
+ const existing = this.visited.get(canonicalKey);
113
+ if (existing) {
114
+ return existing;
115
+ }
116
+
117
+ /* Check for version conflict: same package name, different version */
118
+ for (const [key, pkg] of this.visited) {
119
+ if (pkg.id.packageName === packageId.packageName && pkg.id.version !== packageId.version) {
120
+ throw new ResolutionError(
121
+ "VERSION_CONFLICT",
122
+ `Version conflict for ${packageId.packageName}: ` +
123
+ `${pkg.id.version} vs ${packageId.version}`,
124
+ { existing: pkg.id, conflicting: packageId }
125
+ );
126
+ }
127
+ }
128
+
129
+ this.inProgress.add(canonicalKey);
130
+
131
+ const imports = abiFile.abi.imports ?? [];
132
+ const dependencies: PackageId[] = [];
133
+
134
+ for (const importYaml of imports) {
135
+ const importSource = this.normalizeImportSource(importYaml);
136
+
137
+ /* Enforce local import restriction */
138
+ if (isRemote && importSource.type === "path") {
139
+ throw new ResolutionError(
140
+ "UNSUPPORTED_IMPORT_TYPE",
141
+ `Remote package ${packageId.packageName} cannot import local path: ${importSource.path}`
142
+ );
143
+ }
144
+
145
+ /* Determine child remoteness from the import source type, not the parent.
146
+ Non-path imports (onchain, git, http) are always remote. */
147
+ const childIsRemote = importSource.type !== "path";
148
+ const depPackage = await this.resolveImport(importSource, childIsRemote, depth + 1);
149
+ dependencies.push(depPackage.id);
150
+ }
151
+
152
+ this.inProgress.delete(canonicalKey);
153
+
154
+ const resolvedPackage: ResolvedPackage = {
155
+ id: packageId,
156
+ source: { type: "path", path: "<root>" },
157
+ abiYaml,
158
+ dependencies,
159
+ isRemote,
160
+ };
161
+
162
+ this.visited.set(canonicalKey, resolvedPackage);
163
+ return resolvedPackage;
164
+ }
165
+
166
+ private async resolveImport(
167
+ source: ImportSource,
168
+ parentIsRemote: boolean,
169
+ depth: number
170
+ ): Promise<ResolvedPackage> {
171
+ switch (source.type) {
172
+ case "path":
173
+ throw new ResolutionError(
174
+ "UNSUPPORTED_IMPORT_TYPE",
175
+ `Path imports are not supported in browser. Use CLI 'bundle' command. Path: ${source.path}`
176
+ );
177
+
178
+ case "git":
179
+ throw new ResolutionError(
180
+ "UNSUPPORTED_IMPORT_TYPE",
181
+ `Git imports are not supported in browser. Use CLI 'bundle' command. URL: ${source.url}`
182
+ );
183
+
184
+ case "http":
185
+ throw new ResolutionError(
186
+ "UNSUPPORTED_IMPORT_TYPE",
187
+ `HTTP imports are not supported in browser. Use CLI 'bundle' command. URL: ${source.url}`
188
+ );
189
+
190
+ case "onchain":
191
+ return this.resolveOnchainImport(source, depth);
192
+
193
+ default:
194
+ throw new ResolutionError(
195
+ "UNSUPPORTED_IMPORT_TYPE",
196
+ `Unknown import type: ${(source as ImportSource).type}`
197
+ );
198
+ }
199
+ }
200
+
201
+ private async resolveOnchainImport(
202
+ source: Extract<ImportSource, { type: "onchain" }>,
203
+ depth: number
204
+ ): Promise<ResolvedPackage> {
205
+ const result = await this.onchainFetcher.fetch(
206
+ source.address,
207
+ source.target,
208
+ source.network,
209
+ source.revision
210
+ );
211
+
212
+ const resolved = await this.resolvePackage(result.abiYaml, true, depth);
213
+ resolved.source = source;
214
+ resolved.isRemote = true;
215
+
216
+ return resolved;
217
+ }
218
+
219
+ private parseAbiYaml(yamlContent: string): AbiFile {
220
+ try {
221
+ const parsed = yaml.parse(yamlContent);
222
+ if (!parsed || typeof parsed !== "object") {
223
+ throw new Error("Invalid ABI YAML: not an object");
224
+ }
225
+ if (!parsed.abi || typeof parsed.abi !== "object") {
226
+ throw new Error("Invalid ABI YAML: missing 'abi' section");
227
+ }
228
+ return parsed as AbiFile;
229
+ } catch (error) {
230
+ throw new ResolutionError(
231
+ "PARSE_ERROR",
232
+ `Failed to parse ABI YAML: ${error instanceof Error ? error.message : String(error)}`,
233
+ error
234
+ );
235
+ }
236
+ }
237
+
238
+ private extractPackageId(abiFile: AbiFile): PackageId {
239
+ const metadata = abiFile.abi;
240
+ if (!metadata.package) {
241
+ throw new ResolutionError("PARSE_ERROR", "ABI missing 'package' field");
242
+ }
243
+ return {
244
+ packageName: metadata.package,
245
+ version: metadata["package-version"] ?? "0.0.0",
246
+ };
247
+ }
248
+
249
+ private normalizeImportSource(source: ImportSourceYaml): ImportSource {
250
+ switch (source.type) {
251
+ case "path":
252
+ return { type: "path", path: source.path };
253
+
254
+ case "git":
255
+ return {
256
+ type: "git",
257
+ url: source.url,
258
+ ref: source.ref,
259
+ path: source.path,
260
+ };
261
+
262
+ case "http":
263
+ return { type: "http", url: source.url };
264
+
265
+ case "onchain": {
266
+ const revision = this.parseRevisionSpec(source.revision);
267
+ return {
268
+ type: "onchain",
269
+ address: source.address,
270
+ target: source.target ?? "program",
271
+ network: source.network,
272
+ revision,
273
+ };
274
+ }
275
+
276
+ default:
277
+ throw new ResolutionError(
278
+ "UNSUPPORTED_IMPORT_TYPE",
279
+ `Unknown import type: ${(source as ImportSourceYaml).type}`
280
+ );
281
+ }
282
+ }
283
+
284
+ private parseRevisionSpec(revision: number | string | undefined): RevisionSpec {
285
+ if (revision === undefined || revision === "latest") {
286
+ return { type: "latest" };
287
+ }
288
+ if (typeof revision === "number") {
289
+ return { type: "exact", value: revision };
290
+ }
291
+ if (typeof revision === "string") {
292
+ if (revision.startsWith(">=")) {
293
+ const value = parseInt(revision.slice(2), 10);
294
+ if (isNaN(value)) {
295
+ throw new Error(`Invalid minimum revision: ${revision}`);
296
+ }
297
+ return { type: "minimum", value };
298
+ }
299
+ const value = parseInt(revision, 10);
300
+ if (!isNaN(value)) {
301
+ return { type: "exact", value };
302
+ }
303
+ }
304
+ throw new Error(`Invalid revision specification: ${revision}`);
305
+ }
306
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Import Source Types
3
+ *
4
+ * These types mirror the Rust ImportSource enum and related types.
5
+ */
6
+
7
+ /* Target type for on-chain ABI imports */
8
+ export type OnchainTarget = "program" | "abi-meta" | "abi";
9
+
10
+ /* Revision specifier for on-chain imports */
11
+ export type RevisionSpec =
12
+ | { type: "exact"; value: number }
13
+ | { type: "minimum"; value: number }
14
+ | { type: "latest" };
15
+
16
+ /* Import source specification */
17
+ export type ImportSource =
18
+ | { type: "path"; path: string }
19
+ | { type: "git"; url: string; ref: string; path: string }
20
+ | { type: "http"; url: string }
21
+ | {
22
+ type: "onchain";
23
+ address: string;
24
+ target: OnchainTarget;
25
+ network: string;
26
+ revision: RevisionSpec;
27
+ };
28
+
29
+ /* Package identifier */
30
+ export interface PackageId {
31
+ packageName: string;
32
+ version: string;
33
+ }
34
+
35
+ /* Resolved package information */
36
+ export interface ResolvedPackage {
37
+ id: PackageId;
38
+ source: ImportSource;
39
+ abiYaml: string;
40
+ dependencies: PackageId[];
41
+ isRemote: boolean;
42
+ }
43
+
44
+ /* Resolution result */
45
+ export interface ResolutionResult {
46
+ root: ResolvedPackage;
47
+ allPackages: ResolvedPackage[];
48
+ manifest: Record<string, string>;
49
+ }
50
+
51
+ /* Resolution error */
52
+ export class ResolutionError extends Error {
53
+ constructor(
54
+ public code:
55
+ | "CYCLIC_DEPENDENCY"
56
+ | "VERSION_CONFLICT"
57
+ | "FETCH_ERROR"
58
+ | "PARSE_ERROR"
59
+ | "NOT_FOUND"
60
+ | "UNSUPPORTED_IMPORT_TYPE",
61
+ message: string,
62
+ public details?: unknown
63
+ ) {
64
+ super(message);
65
+ this.name = "ResolutionError";
66
+ }
67
+ }
68
+
69
+ /* ABI account header constants (matches Rust) */
70
+ export const ABI_ACCOUNT_HEADER_SIZE = 45;
71
+ export const ABI_STATE_OPEN = 0x00;
72
+ export const ABI_STATE_FINALIZED = 0x01;
73
+
74
+ /* Parsed ABI account data */
75
+ export interface AbiAccountData {
76
+ abiMetaAccount: Uint8Array;
77
+ revision: bigint;
78
+ state: number;
79
+ content: string;
80
+ }
81
+
82
+ /* RPC endpoint configuration */
83
+ export interface RpcEndpoints {
84
+ [network: string]: string;
85
+ }
86
+
87
+ /* Default RPC endpoints */
88
+ export const DEFAULT_RPC_ENDPOINTS: RpcEndpoints = {
89
+ mainnet: "https://rpc.thru.network",
90
+ testnet: "https://rpc-testnet.thru.network",
91
+ };
92
+
93
+ /* ABI metadata parsed from YAML */
94
+ export interface AbiMetadata {
95
+ package: string;
96
+ name?: string;
97
+ "abi-version": number;
98
+ "package-version": string;
99
+ description: string;
100
+ imports?: ImportSourceYaml[];
101
+ }
102
+
103
+ /* Import source as it appears in YAML */
104
+ export type ImportSourceYaml =
105
+ | { type: "path"; path: string }
106
+ | { type: "git"; url: string; ref: string; path: string }
107
+ | { type: "http"; url: string }
108
+ | {
109
+ type: "onchain";
110
+ address: string;
111
+ target?: OnchainTarget;
112
+ network: string;
113
+ revision?: number | string;
114
+ };
115
+
116
+ /* Parsed ABI file */
117
+ export interface AbiFile {
118
+ abi: AbiMetadata;
119
+ types: unknown[];
120
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,18 @@ export {
6
6
  reflectInstruction,
7
7
  reflectAccount,
8
8
  reflectEvent,
9
- formatReflection
9
+ formatReflection,
10
+ /* Manifest-based functions for ABIs with imports */
11
+ reflectWithManifest,
12
+ reflectInstructionWithManifest,
13
+ reflectAccountWithManifest,
14
+ reflectEventWithManifest,
15
+ buildLayoutIrWithManifest,
16
+ getManifestPackages,
17
+ validateManifest,
10
18
  } from "./wasmBridge";
11
- export type { FormatOptions } from "./wasmBridge";
19
+ export type { FormatOptions, Manifest, ManifestPackageInfo } from "./wasmBridge";
12
20
  export type { FormattedReflection, FormattedValue, FormattedValueWithByteRange, ByteRange } from "./types";
21
+
22
+ /* Import resolver module */
23
+ export * from "./imports";
package/src/wasmBridge.ts CHANGED
@@ -10,6 +10,14 @@ type WasmReflectBindings = {
10
10
  build_layout_ir: (abi: string) => string;
11
11
  format_reflection: (raw: string) => string;
12
12
  format_reflection_with_options: (raw: string, options: string) => string;
13
+ /* Manifest-based functions for ABIs with imports */
14
+ reflect_with_manifest: (manifest: string, rootPackage: string, typeName: string, buffer: Uint8Array) => string;
15
+ reflect_instruction_with_manifest: (manifest: string, rootPackage: string, buffer: Uint8Array) => string;
16
+ reflect_account_with_manifest: (manifest: string, rootPackage: string, buffer: Uint8Array) => string;
17
+ reflect_event_with_manifest: (manifest: string, rootPackage: string, buffer: Uint8Array) => string;
18
+ build_layout_ir_with_manifest: (manifest: string, rootPackage: string) => string;
19
+ get_manifest_packages: (manifest: string) => string;
20
+ validate_manifest: (manifest: string) => string;
13
21
  wasm_start?: () => void;
14
22
  };
15
23
 
@@ -243,3 +251,172 @@ export function formatReflection(raw: JsonValue, options?: FormatOptions): Forma
243
251
  const result = bindings.format_reflection(serialized);
244
252
  return JSON.parse(result) as FormattedReflection;
245
253
  }
254
+
255
+ /* ============================================================================
256
+ Manifest-based Functions
257
+
258
+ These functions support ABIs with imports by accepting a pre-resolved manifest
259
+ (a map of package names to their ABI YAML content).
260
+ ============================================================================ */
261
+
262
+ export type Manifest = Record<string, string>;
263
+
264
+ async function callReflectWithManifest(
265
+ manifest: Manifest,
266
+ rootPackage: string,
267
+ typeName: string,
268
+ buffer: Uint8Array,
269
+ ): Promise<JsonValue> {
270
+ const bindings = await loadBindings();
271
+ const manifestJson = JSON.stringify(manifest);
272
+ const result = bindings.reflect_with_manifest(manifestJson, rootPackage, typeName, buffer);
273
+ return JSON.parse(result);
274
+ }
275
+
276
+ async function callReflectInstructionWithManifest(
277
+ manifest: Manifest,
278
+ rootPackage: string,
279
+ buffer: Uint8Array,
280
+ ): Promise<JsonValue> {
281
+ const bindings = await loadBindings();
282
+ const manifestJson = JSON.stringify(manifest);
283
+ const result = bindings.reflect_instruction_with_manifest(manifestJson, rootPackage, buffer);
284
+ return JSON.parse(result);
285
+ }
286
+
287
+ async function callReflectAccountWithManifest(
288
+ manifest: Manifest,
289
+ rootPackage: string,
290
+ buffer: Uint8Array,
291
+ ): Promise<JsonValue> {
292
+ const bindings = await loadBindings();
293
+ const manifestJson = JSON.stringify(manifest);
294
+ const result = bindings.reflect_account_with_manifest(manifestJson, rootPackage, buffer);
295
+ return JSON.parse(result);
296
+ }
297
+
298
+ async function callReflectEventWithManifest(
299
+ manifest: Manifest,
300
+ rootPackage: string,
301
+ buffer: Uint8Array,
302
+ ): Promise<JsonValue> {
303
+ const bindings = await loadBindings();
304
+ const manifestJson = JSON.stringify(manifest);
305
+ const result = bindings.reflect_event_with_manifest(manifestJson, rootPackage, buffer);
306
+ return JSON.parse(result);
307
+ }
308
+
309
+ /**
310
+ * Reflect a binary buffer using a pre-resolved manifest.
311
+ *
312
+ * @param manifest - Map of package names to ABI YAML content
313
+ * @param rootPackage - The package containing the target type
314
+ * @param typeName - The type name to parse
315
+ * @param payload - Binary data to reflect
316
+ */
317
+ export async function reflectWithManifest(
318
+ manifest: Manifest,
319
+ rootPackage: string,
320
+ typeName: string,
321
+ payload: ReflectRootPayload,
322
+ ): Promise<JsonValue> {
323
+ if (payload.type === 'binary') {
324
+ return callReflectWithManifest(manifest, rootPackage, typeName, toUint8Array(payload.value));
325
+ }
326
+ if (payload.type === 'hex') {
327
+ return callReflectWithManifest(manifest, rootPackage, typeName, hexToBytes(payload.value));
328
+ }
329
+ throw new Error(`Invalid payload type`);
330
+ }
331
+
332
+ /**
333
+ * Reflect an instruction using a pre-resolved manifest.
334
+ */
335
+ export async function reflectInstructionWithManifest(
336
+ manifest: Manifest,
337
+ rootPackage: string,
338
+ payload: ReflectRootPayload,
339
+ ): Promise<JsonValue> {
340
+ if (payload.type === 'binary') {
341
+ return callReflectInstructionWithManifest(manifest, rootPackage, toUint8Array(payload.value));
342
+ }
343
+ if (payload.type === 'hex') {
344
+ return callReflectInstructionWithManifest(manifest, rootPackage, hexToBytes(payload.value));
345
+ }
346
+ throw new Error(`Invalid payload type`);
347
+ }
348
+
349
+ /**
350
+ * Reflect an account using a pre-resolved manifest.
351
+ */
352
+ export async function reflectAccountWithManifest(
353
+ manifest: Manifest,
354
+ rootPackage: string,
355
+ payload: ReflectRootPayload,
356
+ ): Promise<JsonValue> {
357
+ if (payload.type === 'binary') {
358
+ return callReflectAccountWithManifest(manifest, rootPackage, toUint8Array(payload.value));
359
+ }
360
+ if (payload.type === 'hex') {
361
+ return callReflectAccountWithManifest(manifest, rootPackage, hexToBytes(payload.value));
362
+ }
363
+ throw new Error(`Invalid payload type`);
364
+ }
365
+
366
+ /**
367
+ * Reflect an event using a pre-resolved manifest.
368
+ */
369
+ export async function reflectEventWithManifest(
370
+ manifest: Manifest,
371
+ rootPackage: string,
372
+ payload: ReflectRootPayload,
373
+ ): Promise<JsonValue> {
374
+ if (payload.type === 'binary') {
375
+ return callReflectEventWithManifest(manifest, rootPackage, toUint8Array(payload.value));
376
+ }
377
+ if (payload.type === 'hex') {
378
+ return callReflectEventWithManifest(manifest, rootPackage, hexToBytes(payload.value));
379
+ }
380
+ throw new Error(`Invalid payload type`);
381
+ }
382
+
383
+ /**
384
+ * Build layout IR using a pre-resolved manifest.
385
+ */
386
+ export async function buildLayoutIrWithManifest(
387
+ manifest: Manifest,
388
+ rootPackage: string,
389
+ ): Promise<JsonValue> {
390
+ const bindings = await loadBindings();
391
+ const manifestJson = JSON.stringify(manifest);
392
+ const result = bindings.build_layout_ir_with_manifest(manifestJson, rootPackage);
393
+ return JSON.parse(result);
394
+ }
395
+
396
+ /**
397
+ * Get the list of package names in a manifest.
398
+ */
399
+ export async function getManifestPackages(manifest: Manifest): Promise<string[]> {
400
+ const bindings = await loadBindings();
401
+ const manifestJson = JSON.stringify(manifest);
402
+ const result = bindings.get_manifest_packages(manifestJson);
403
+ return JSON.parse(result);
404
+ }
405
+
406
+ export interface ManifestPackageInfo {
407
+ name: string;
408
+ package: string;
409
+ version: number;
410
+ type_count: number;
411
+ has_root_types: boolean;
412
+ }
413
+
414
+ /**
415
+ * Validate a manifest and return information about its contents.
416
+ */
417
+ export async function validateManifest(manifest: Manifest): Promise<ManifestPackageInfo[]> {
418
+ const bindings = await loadBindings();
419
+ const manifestJson = JSON.stringify(manifest);
420
+ const result = bindings.validate_manifest(manifestJson);
421
+ return JSON.parse(result);
422
+ }