@lionden/network 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/accounts.d.ts +12 -0
  4. package/dist/accounts.d.ts.map +1 -0
  5. package/dist/accounts.js +38 -0
  6. package/dist/accounts.js.map +1 -0
  7. package/dist/connection.d.ts +121 -0
  8. package/dist/connection.d.ts.map +1 -0
  9. package/dist/connection.js +1064 -0
  10. package/dist/connection.js.map +1 -0
  11. package/dist/devnode-backend.d.ts +53 -0
  12. package/dist/devnode-backend.d.ts.map +1 -0
  13. package/dist/devnode-backend.js +121 -0
  14. package/dist/devnode-backend.js.map +1 -0
  15. package/dist/devnode-manager.d.ts +130 -0
  16. package/dist/devnode-manager.d.ts.map +1 -0
  17. package/dist/devnode-manager.js +546 -0
  18. package/dist/devnode-manager.js.map +1 -0
  19. package/dist/execution-key-cache.d.ts +71 -0
  20. package/dist/execution-key-cache.d.ts.map +1 -0
  21. package/dist/execution-key-cache.js +286 -0
  22. package/dist/execution-key-cache.js.map +1 -0
  23. package/dist/file-io.d.ts +2 -0
  24. package/dist/file-io.d.ts.map +1 -0
  25. package/dist/file-io.js +9 -0
  26. package/dist/file-io.js.map +1 -0
  27. package/dist/index.d.ts +12 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/named-account-manager.d.ts +40 -0
  32. package/dist/named-account-manager.d.ts.map +1 -0
  33. package/dist/named-account-manager.js +116 -0
  34. package/dist/named-account-manager.js.map +1 -0
  35. package/dist/network-manager.d.ts +36 -0
  36. package/dist/network-manager.d.ts.map +1 -0
  37. package/dist/network-manager.js +226 -0
  38. package/dist/network-manager.js.map +1 -0
  39. package/dist/sdk-adapter.d.ts +271 -0
  40. package/dist/sdk-adapter.d.ts.map +1 -0
  41. package/dist/sdk-adapter.js +997 -0
  42. package/dist/sdk-adapter.js.map +1 -0
  43. package/dist/sdk-diagnostics.d.ts +113 -0
  44. package/dist/sdk-diagnostics.d.ts.map +1 -0
  45. package/dist/sdk-diagnostics.js +241 -0
  46. package/dist/sdk-diagnostics.js.map +1 -0
  47. package/dist/transition-selector.d.ts +15 -0
  48. package/dist/transition-selector.d.ts.map +1 -0
  49. package/dist/transition-selector.js +45 -0
  50. package/dist/transition-selector.js.map +1 -0
  51. package/dist/types.d.ts +484 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +129 -0
  54. package/dist/types.js.map +1 -0
  55. package/package.json +35 -0
@@ -0,0 +1,1064 @@
1
+ /**
2
+ * AleoConnection — concrete implementation of NetworkConnection.
3
+ *
4
+ * Delegates to the Provable SDK's AleoNetworkClient for queries and
5
+ * transaction broadcasting, and to ProgramManager for transaction building.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import { normalizeRuntimeImportRef } from "@lionden/config";
9
+ import { buildRuntimeKeyIdentity, findCachedExecutionKeys, resolveProgramExecutionArtifacts, } from "./execution-key-cache.js";
10
+ import { captureSdkCall } from "./sdk-diagnostics.js";
11
+ import { selectMatchingTransition } from "./transition-selector.js";
12
+ import { LocalExecutionWasmTrapError, LocalVmExecutionError, NetworkConfirmationTimeoutError, TransitionRejectedError, } from "./types.js";
13
+ /**
14
+ * Thrown when a 2xx response body fails to match the expected confirmed-tx
15
+ * shape (missing required fields, wrong types). Distinct from transient
16
+ * network errors — the polling loop must NOT retry on this class.
17
+ */
18
+ export class TransactionShapeParseError extends Error {
19
+ kind = "TransactionShapeParseError";
20
+ txId;
21
+ field;
22
+ constructor(message, txId, field, cause) {
23
+ super(message, cause === undefined ? undefined : { cause });
24
+ this.name = "TransactionShapeParseError";
25
+ this.txId = txId;
26
+ this.field = field;
27
+ }
28
+ }
29
+ const DEFAULT_CONFIRMATION_TIMEOUT_MS = 60_000;
30
+ const CONFIRMATION_POLL_INTERVAL_MS = 1_000;
31
+ const U32_MAX = 0xffffffff;
32
+ function formatStorageVectorIndex(index) {
33
+ if (!Number.isSafeInteger(index) || index < 0 || index > U32_MAX) {
34
+ throw new Error(`Storage vector index must be a non-negative safe integer in range 0..${U32_MAX}.`);
35
+ }
36
+ return `${index}u32`;
37
+ }
38
+ function parseStorageVectorLength(value) {
39
+ const stripped = value.replace(/u32(?:\.(?:public|private))?$/i, "");
40
+ const length = Number(stripped);
41
+ if (!Number.isSafeInteger(length) || length < 0 || length > U32_MAX) {
42
+ throw new Error(`Invalid storage vector length: ${value}`);
43
+ }
44
+ return length;
45
+ }
46
+ /**
47
+ * Process-wide serialization for local-WASM trap capture.
48
+ *
49
+ * runWithLocalWasmTrapCapture installs process-global uncaughtException /
50
+ * unhandledRejection handlers. Those handler windows cannot safely overlap,
51
+ * even when the SDK operations use different connections or diagnostics sinks.
52
+ */
53
+ let localWasmTrapCaptureLock = Promise.resolve();
54
+ export class AleoConnection {
55
+ type;
56
+ name;
57
+ endpoint;
58
+ networkId;
59
+ privateKey;
60
+ apiKey;
61
+ egressPolicy;
62
+ artifactsDir;
63
+ keyCache;
64
+ logLevel;
65
+ projectRoot;
66
+ executionImports;
67
+ sdkObjects;
68
+ sdkObjectsPromise;
69
+ _closed = false;
70
+ /** Whether this connection has been permanently closed. */
71
+ get closed() {
72
+ return this._closed;
73
+ }
74
+ /** Throws if this connection has been closed. */
75
+ assertOpen() {
76
+ if (this._closed) {
77
+ throw new Error("Connection is closed.");
78
+ }
79
+ }
80
+ // Per-signer SDK object cache — isolated PM + RecordProvider + Account per key
81
+ signerSdkResolved = new Map();
82
+ signerSdkInflight = new Map();
83
+ constructor(options) {
84
+ this.type = options.type;
85
+ this.name = options.name;
86
+ this.endpoint = options.endpoint;
87
+ this.networkId = options.networkId;
88
+ this.privateKey = options.privateKey;
89
+ this.apiKey = options.apiKey;
90
+ this.egressPolicy = options.egressPolicy;
91
+ this.artifactsDir = options.artifactsDir;
92
+ this.keyCache = options.keyCache;
93
+ this.logLevel = options.logLevel;
94
+ this.projectRoot = options.projectRoot;
95
+ this.executionImports = options.executionImports ?? {};
96
+ // Devnode connections support block advancement
97
+ if (this.type === "devnode") {
98
+ this.advanceBlocks = async (count) => {
99
+ this.assertOpen();
100
+ for (let i = 0; i < count; i++) {
101
+ const url = `${this.endpoint}/${this.networkId}/block/create`;
102
+ const response = await fetch(url, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ },
107
+ body: JSON.stringify({ num_blocks: 1 }),
108
+ });
109
+ if (!response.ok) {
110
+ throw new Error(`Failed to advance block: ${response.status} ${response.statusText}`);
111
+ }
112
+ }
113
+ };
114
+ }
115
+ }
116
+ /** Lazy-init and cache SDK objects for this connection. */
117
+ async getSdkObjects() {
118
+ this.assertOpen();
119
+ if (this.sdkObjects)
120
+ return this.sdkObjects;
121
+ if (!this.sdkObjectsPromise) {
122
+ this.sdkObjectsPromise = (async () => {
123
+ const { createSdkObjects } = await import("./sdk-adapter.js");
124
+ const objects = await createSdkObjects({
125
+ network: this.networkId,
126
+ endpoint: this.endpoint,
127
+ privateKey: this.privateKey,
128
+ apiKey: this.apiKey,
129
+ keyCache: this.keyCache,
130
+ logLevel: this.logLevel,
131
+ egressPolicy: this.egressPolicy,
132
+ });
133
+ // Guard: if close() was called while we were initializing,
134
+ // destroy the account and bail out.
135
+ if (this._closed) {
136
+ tryDestroyAccount(objects.account);
137
+ throw new Error("Connection closed during SDK initialization.");
138
+ }
139
+ this.sdkObjects = objects;
140
+ return objects;
141
+ })();
142
+ }
143
+ return this.sdkObjectsPromise;
144
+ }
145
+ /**
146
+ * Get the effective ProgramManager for an execution.
147
+ * Returns the default PM when no signer override is given,
148
+ * or a per-signer isolated PM otherwise.
149
+ */
150
+ async getEffectivePm(signerPrivateKey) {
151
+ const defaultSdk = await this.getSdkObjects();
152
+ if (!signerPrivateKey || signerPrivateKey === this.privateKey) {
153
+ return {
154
+ pm: defaultSdk.programManager,
155
+ nc: defaultSdk.networkClient,
156
+ diagnostics: defaultSdk.diagnostics,
157
+ };
158
+ }
159
+ // Check resolved cache. State queries during a signer execution go through
160
+ // the signer PM's own transport, so attribute failures to the signer sink.
161
+ const resolved = this.signerSdkResolved.get(signerPrivateKey);
162
+ if (resolved) {
163
+ return {
164
+ pm: resolved.programManager,
165
+ nc: defaultSdk.networkClient,
166
+ diagnostics: resolved.diagnostics,
167
+ };
168
+ }
169
+ // Check inflight cache
170
+ let inflight = this.signerSdkInflight.get(signerPrivateKey);
171
+ if (!inflight) {
172
+ inflight = (async () => {
173
+ const { createSignerSdkObjects } = await import("./sdk-adapter.js");
174
+ const signerSdk = await createSignerSdkObjects({
175
+ privateKey: signerPrivateKey,
176
+ endpoint: this.endpoint,
177
+ network: this.networkId,
178
+ keyProvider: defaultSdk.keyProvider,
179
+ apiKey: this.apiKey,
180
+ logLevel: this.logLevel,
181
+ egressPolicy: this.egressPolicy,
182
+ });
183
+ // If close() was called while creating, destroy and bail
184
+ if (this._closed) {
185
+ tryDestroyAccount(signerSdk.account);
186
+ throw new Error("Connection closed during signer SDK initialization.");
187
+ }
188
+ this.signerSdkResolved.set(signerPrivateKey, signerSdk);
189
+ this.signerSdkInflight.delete(signerPrivateKey);
190
+ return signerSdk;
191
+ })();
192
+ this.signerSdkInflight.set(signerPrivateKey, inflight);
193
+ // Evict from inflight on rejection so retries work
194
+ inflight.catch(() => {
195
+ this.signerSdkInflight.delete(signerPrivateKey);
196
+ });
197
+ }
198
+ const signerSdk = await inflight;
199
+ return {
200
+ pm: signerSdk.programManager,
201
+ nc: defaultSdk.networkClient,
202
+ diagnostics: signerSdk.diagnostics,
203
+ };
204
+ }
205
+ async getBalance(address) {
206
+ this.assertOpen();
207
+ const addr = address ?? (await this.getDefaultAddress());
208
+ const value = await this.getMappingValue("credits.aleo", "account", addr);
209
+ if (value === null)
210
+ return 0n;
211
+ // Value looks like "123456u64" — strip the suffix
212
+ return BigInt(value.replace(/u\d+$/i, ""));
213
+ }
214
+ /**
215
+ * Shared query body for mapping/storage reads. Resolves the SDK network
216
+ * client, calls `getProgramMappingValue`, coerces undefined/null to `null`,
217
+ * treats 404/"not found" as `null`, and otherwise rethrows wrapped in a
218
+ * `Failed to query ${failureLabel}` error. Callers supply the flavored
219
+ * `failureLabel` ("mapping …" vs "storage …") so error strings stay distinct.
220
+ */
221
+ async queryProgramMappingRaw(programId, mappingName, key, failureLabel) {
222
+ this.assertOpen();
223
+ const sdk = await this.getSdkObjects();
224
+ const nc = sdk.networkClient;
225
+ try {
226
+ const value = await nc.getProgramMappingValue(programId, mappingName, key);
227
+ if (value === undefined || value === null)
228
+ return null;
229
+ return typeof value === "string" ? value : String(value);
230
+ }
231
+ catch (err) {
232
+ // SDK throws on 404 / missing key — treat as null
233
+ const message = err instanceof Error ? err.message : String(err);
234
+ if (message.includes("404") ||
235
+ message.includes("not found") ||
236
+ message.includes("Not Found")) {
237
+ return null;
238
+ }
239
+ throw new Error(`Failed to query ${failureLabel}: ${message}`);
240
+ }
241
+ }
242
+ async getMappingValue(programId, mappingName, key) {
243
+ return this.queryProgramMappingRaw(programId, mappingName, key, `mapping ${programId}/${mappingName}`);
244
+ }
245
+ async getStorageValue(programId, variableName) {
246
+ return this.queryProgramMappingRaw(programId, `${variableName}__`, "false", `storage ${programId}/${variableName}`);
247
+ }
248
+ async getStorageVectorLength(programId, variableName) {
249
+ const value = await this.getMappingValue(programId, `${variableName}__len__`, "false");
250
+ return value === null ? 0 : parseStorageVectorLength(value);
251
+ }
252
+ async getStorageVectorValue(programId, variableName, index) {
253
+ return this.getMappingValue(programId, `${variableName}__`, formatStorageVectorIndex(index));
254
+ }
255
+ async execute(programId, transitionName, args, options) {
256
+ this.assertOpen();
257
+ if (this.type === "devnode") {
258
+ const { checkDevnodeSdkSupport, initConsensusHeights } = await import("./sdk-adapter.js");
259
+ // Enforce the SDK baseline for devnode operations.
260
+ await checkDevnodeSdkSupport();
261
+ await initConsensusHeights();
262
+ }
263
+ // Resolve the effective ProgramManager — uses a per-signer isolated PM
264
+ // when options.signer is provided, otherwise the connection's default.
265
+ // `diagnostics` is the transport-failure sink for whichever PM was chosen.
266
+ const { pm: effectivePm, nc: effectiveNc, diagnostics, } = await this.getEffectivePm(options?.signer?.privateKey);
267
+ const pm = effectivePm;
268
+ const nc = effectiveNc;
269
+ const mode = options?.mode ?? "onchain";
270
+ // Resolve runtime imports (config layer + per-call layer) once.
271
+ // Path refs are absolutized via @lionden/config helpers; missing files
272
+ // fail-fast here with a clear error before we touch the SDK.
273
+ const runtimeImports = this.collectRuntimeImports(programId, options?.imports);
274
+ // Resolve program source + transitive imports once for every mode.
275
+ // includeSidecar=true is safe everywhere; it's a cheap fs read and only
276
+ // consulted when the filesystem keycache path is exercised below.
277
+ const artifacts = await resolveProgramExecutionArtifacts({
278
+ artifactsDir: this.artifactsDir,
279
+ programId,
280
+ networkClient: nc,
281
+ includeSidecar: true,
282
+ runtimeImports,
283
+ });
284
+ if (mode === "local") {
285
+ // Local execution — run without generating proofs. captureSdkCall covers
286
+ // local *rejections* and opaque-shape classification; the sink is normally
287
+ // empty for local. runWithLocalWasmTrapCapture is nested INSIDE (closest
288
+ // to pm.run) so a Leo body panic that escapes the pm.run promise as a
289
+ // process-global `RuntimeError: unreachable` trap becomes a rejection
290
+ // rather than hanging forever. Order matters: captureSdkCall must stay the
291
+ // outer wrapper so its runExclusive mutex is released when the trap rejects
292
+ // — see sdk-diagnostics.ts for the matching LocalExecutionWasmTrapError
293
+ // pass-through that keeps the trap from being re-wrapped.
294
+ const result = await captureSdkCall(diagnostics, { operation: "local", programId, transitionName }, () => runWithLocalWasmTrapCapture(() => pm.run(artifacts.source, transitionName, args, false, // proveExecution = false
295
+ artifacts.imports)));
296
+ const outputs = extractLocalExecutionOutputs(result);
297
+ return {
298
+ outputs,
299
+ };
300
+ }
301
+ // On-chain execution
302
+ let txId;
303
+ const useDevnodeFastPath = this.type === "devnode" &&
304
+ !options?.prove &&
305
+ typeof pm.buildDevnodeExecutionTransaction === "function";
306
+ const importsSlice = artifacts.imports === undefined ? {} : { imports: artifacts.imports };
307
+ if (useDevnodeFastPath) {
308
+ // Devnode fast-path — skips proof generation. Only the build is wrapped;
309
+ // broadcast already surfaces a usable HTTP error on its own. The build runs
310
+ // the transition body locally to authorize the tx, so an arithmetic body
311
+ // panic traps the same way local pm.run does — runWithLocalWasmTrapCapture
312
+ // nested INSIDE captureSdkCall converts that trap into a rejection instead
313
+ // of a hang/crash (tmp/bug-hunts/onchain-trap probe).
314
+ const tx = await captureSdkCall(diagnostics, { operation: "execute", programId, transitionName }, () => runWithLocalWasmTrapCapture(() => pm.buildDevnodeExecutionTransaction({
315
+ programName: programId,
316
+ functionName: transitionName,
317
+ inputs: args,
318
+ priorityFee: options?.fee ?? 0,
319
+ privateFee: options?.privateFee ?? false,
320
+ program: artifacts.source,
321
+ ...importsSlice,
322
+ })));
323
+ txId = await this.broadcastTransaction(tx);
324
+ }
325
+ else {
326
+ const persistentExtras = await this.getPersistentExecutionOptions(nc, programId, transitionName, artifacts);
327
+ // Standard execution via ProgramManager. This is the prove path whose
328
+ // WASM state-query callbacks surface as opaque "JS callback Promise
329
+ // rejected:" errors — captureSdkCall enriches them from the sink. The
330
+ // local authorize phase runs the transition body before proof generation,
331
+ // so an arithmetic body panic traps before any state query —
332
+ // runWithLocalWasmTrapCapture (nested INSIDE captureSdkCall) converts that
333
+ // trap into a rejection instead of a hang/crash. A normal opaque rejection
334
+ // still passes through to captureSdkCall's enrichment unchanged.
335
+ txId = await captureSdkCall(diagnostics, { operation: "execute", programId, transitionName }, () => runWithLocalWasmTrapCapture(() => pm.execute({
336
+ programName: programId,
337
+ functionName: transitionName,
338
+ inputs: args,
339
+ priorityFee: options?.fee ?? 0,
340
+ privateFee: options?.privateFee ?? false,
341
+ program: artifacts.source,
342
+ ...importsSlice,
343
+ ...(persistentExtras ?? {}),
344
+ })));
345
+ }
346
+ if (options?.awaitConfirmation !== true) {
347
+ return { outputs: [], txId };
348
+ }
349
+ return this.getTransitionOutputs(txId, programId, transitionName);
350
+ }
351
+ async checkLocalExecution(programId, transitionName, args, options) {
352
+ this.assertOpen();
353
+ if (this.type === "devnode") {
354
+ const { checkDevnodeSdkSupport, initConsensusHeights } = await import("./sdk-adapter.js");
355
+ await checkDevnodeSdkSupport();
356
+ await initConsensusHeights();
357
+ }
358
+ const { pm: effectivePm, nc: effectiveNc } = await this.getEffectivePm(options?.signer?.privateKey);
359
+ const pm = effectivePm;
360
+ const nc = effectiveNc;
361
+ if (typeof pm.buildAuthorizationUnchecked !== "function") {
362
+ throw new Error("Local failure checks require SDK ProgramManager.buildAuthorizationUnchecked().");
363
+ }
364
+ const runtimeImports = this.collectRuntimeImports(programId, options?.imports);
365
+ const artifacts = await resolveProgramExecutionArtifacts({
366
+ artifactsDir: this.artifactsDir,
367
+ programId,
368
+ networkClient: nc,
369
+ includeSidecar: true,
370
+ runtimeImports,
371
+ });
372
+ try {
373
+ await runWithLocalWasmTrapCapture(() => pm.buildAuthorizationUnchecked({
374
+ programName: programId,
375
+ functionName: transitionName,
376
+ inputs: args,
377
+ programSource: artifacts.source,
378
+ ...(artifacts.imports === undefined ? {} : { programImports: artifacts.imports }),
379
+ }));
380
+ }
381
+ catch (error) {
382
+ if (error instanceof LocalExecutionWasmTrapError || isCatchableLocalVmError(error)) {
383
+ throw new LocalVmExecutionError(`Local VM execution failed for ${programId}/${transitionName}: ${errorMessage(error)}`, {
384
+ programId,
385
+ transitionName,
386
+ cause: error,
387
+ });
388
+ }
389
+ throw error;
390
+ }
391
+ }
392
+ /**
393
+ * Await confirmation of `txId` and return the parsed outputs for the
394
+ * matching `(programId, transitionName)` transition. Used internally by
395
+ * `execute()` when `awaitConfirmation: true`, and exposed as a public
396
+ * follow-up for callers that opted into fire-and-forget broadcast.
397
+ */
398
+ async getTransitionOutputs(txId, programId, transitionName, timeout) {
399
+ const confirmed = await this.waitForConfirmation(txId, timeout);
400
+ if (confirmed.status === "rejected") {
401
+ throw new TransitionRejectedError(`Transition ${programId}/${transitionName} was rejected on inclusion (txId ${txId}); rejected execute transactions are converted to fee-only and carry no transition outputs.`, {
402
+ txId: confirmed.txId,
403
+ programId,
404
+ transitionName,
405
+ blockHeight: confirmed.blockHeight,
406
+ });
407
+ }
408
+ const transition = selectMatchingTransition(programId, transitionName, confirmed.transitions, confirmed.txId);
409
+ const outputs = transition.rawOutputs.map(toOutputString);
410
+ return {
411
+ outputs,
412
+ rawOutputs: transition.rawOutputs,
413
+ txId: confirmed.txId,
414
+ };
415
+ }
416
+ /**
417
+ * Merge config-level and per-call runtime-import refs. Per-call entries
418
+ * are normalized against `this.projectRoot`; path refs that don't exist
419
+ * fail-fast here with a config-style error. Output is sorted and deduped
420
+ * by `(kind, value)` for cache identity stability.
421
+ */
422
+ collectRuntimeImports(programId, callRefs) {
423
+ const fromConfig = this.executionImports[programId] ?? [];
424
+ const fromCall = [];
425
+ for (const raw of callRefs ?? []) {
426
+ const ref = normalizeRuntimeImportRef(raw, this.projectRoot);
427
+ if (ref.kind === "path") {
428
+ if (!fs.existsSync(ref.absolutePath)) {
429
+ throw new Error(`Runtime import path not found: ${ref.absolutePath} (from per-call options.imports ${JSON.stringify(raw)})`);
430
+ }
431
+ }
432
+ fromCall.push(ref);
433
+ }
434
+ return dedupAndSortRuntimeImports([...fromConfig, ...fromCall]);
435
+ }
436
+ async getPersistentExecutionOptions(nc, programId, transitionName, artifacts) {
437
+ if (this.keyCache?.storage !== "filesystem" || !this.keyCache.path) {
438
+ return undefined;
439
+ }
440
+ const edition = await getLatestProgramEditionIfAvailable(nc, programId);
441
+ const runtime = await import("./sdk-adapter.js");
442
+ const sdkMetadata = runtime.getSdkRuntimeMetadata(this.networkId);
443
+ const identity = buildRuntimeKeyIdentity({
444
+ network: this.networkId,
445
+ programId,
446
+ transition: transitionName,
447
+ edition,
448
+ sourceHash: artifacts.sourceHash,
449
+ importsHash: artifacts.importsHash,
450
+ wasmHash: sdkMetadata.wasmHash,
451
+ });
452
+ const cached = findCachedExecutionKeys({
453
+ cachePath: this.keyCache.path,
454
+ identity,
455
+ artifacts,
456
+ });
457
+ const keyBytes = cached;
458
+ if (!keyBytes) {
459
+ // Cache miss. Do not call the query-less WASM `synthesizeKeyPair` path:
460
+ // direct PROBE-2 evidence showed `pm.execute` lazy synthesis stays on the
461
+ // guarded CallbackQuery path, while eager synthesis cannot be transport
462
+ // guarded. Cache hits above still inject keys.
463
+ return edition === undefined ? undefined : { edition };
464
+ }
465
+ const keys = await runtime.createExecutionKeysFromBytes(this.networkId, {
466
+ provingKey: keyBytes.provingKeyBytes,
467
+ verifyingKey: keyBytes.verifyingKeyBytes,
468
+ });
469
+ return {
470
+ ...(edition === undefined ? {} : { edition }),
471
+ provingKey: keys.provingKey,
472
+ verifyingKey: keys.verifyingKey,
473
+ };
474
+ }
475
+ async waitForConfirmation(txId, timeout) {
476
+ this.assertOpen();
477
+ const effectiveTimeout = timeout ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
478
+ const deadline = Date.now() + effectiveTimeout;
479
+ const headers = {};
480
+ if (this.apiKey) {
481
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
482
+ }
483
+ const base = `${this.endpoint}/${this.networkId}`;
484
+ // Phase 1: poll /transaction/confirmed/<txId> until the tx is in a block.
485
+ // This response no longer carries block_height; the height is resolved in
486
+ // phases 2–3 via the two-step find/blockHash + block/<hash> lookup. Each
487
+ // phase delegates its per-poll verdict to a named classifier so the error
488
+ // policy is uniform and separately testable (see classify* below).
489
+ const confirmedBody = await pollPhase(`${base}/transaction/confirmed/${txId}`, headers, deadline, (response) => classifyConfirmedBody(response, txId), () => {
490
+ throw new NetworkConfirmationTimeoutError(`Transaction ${txId} not confirmed within ${effectiveTimeout}ms`, { txId, timeout: effectiveTimeout, stage: "confirmed" });
491
+ });
492
+ // In Aleo, rejected transactions are confirmed as fee-only.
493
+ // Accepted: transaction.type is "execute" or "deploy". Rejected: "fee".
494
+ const txData = confirmedBody["transaction"];
495
+ const txType = txData?.["type"] ?? confirmedBody["type"];
496
+ const status = txType === "fee" ? "rejected" : "accepted";
497
+ // Parse execute transitions. For fee-only rejected txs, the original
498
+ // execute transitions are not carried by the chain, so transitions: [].
499
+ // For accepted execute txs, missing/malformed transition data fails
500
+ // fast (TransactionShapeParseError) rather than silently returning [].
501
+ const transitions = parseConfirmedTransitions(txData, txId);
502
+ // Phase 2: resolve the containing block's hash.
503
+ // GET /<network>/find/blockHash/<txId> -> JSON-encoded block-hash string
504
+ const blockHash = await pollPhase(`${base}/find/blockHash/${txId}`, headers, deadline, (response) => classifyBlockHash(response), () => {
505
+ throw new NetworkConfirmationTimeoutError(`Transaction ${txId} confirmed but block-hash lookup did not resolve within ${effectiveTimeout}ms`, { txId, timeout: effectiveTimeout, stage: "blockHash" });
506
+ });
507
+ // Phase 3: resolve the block's height.
508
+ // GET /<network>/block/<blockHash> -> header.metadata.height (number)
509
+ const blockHeight = await pollPhase(`${base}/block/${blockHash}`, headers, deadline, (response) => classifyBlockHeight(response, txId, blockHash), () => {
510
+ throw new NetworkConfirmationTimeoutError(`Transaction ${txId} confirmed but block height could not be resolved within ${effectiveTimeout}ms`, { txId, timeout: effectiveTimeout, stage: "blockHeight" });
511
+ });
512
+ return { txId, blockHeight, status, transitions };
513
+ }
514
+ // Assigned dynamically for devnode connections only
515
+ advanceBlocks;
516
+ async getBlockHeight() {
517
+ this.assertOpen();
518
+ const sdk = await this.getSdkObjects();
519
+ const nc = sdk.networkClient;
520
+ const height = await nc.getLatestHeight();
521
+ return height;
522
+ }
523
+ async getProgramSource(programId) {
524
+ this.assertOpen();
525
+ const sdk = await this.getSdkObjects();
526
+ const nc = sdk.networkClient;
527
+ try {
528
+ const source = await nc.getProgram(programId);
529
+ if (source === undefined || source === null)
530
+ return null;
531
+ return typeof source === "string" ? source : String(source);
532
+ }
533
+ catch (err) {
534
+ const message = err instanceof Error ? err.message : String(err);
535
+ if (message.includes("404") ||
536
+ message.includes("not found") ||
537
+ message.includes("Not Found") ||
538
+ message.includes("500") // devnode returns 500 for non-existent programs
539
+ ) {
540
+ return null;
541
+ }
542
+ throw new Error(`Failed to fetch program source for "${programId}": ${message}`);
543
+ }
544
+ }
545
+ async getProgramEdition(programId) {
546
+ this.assertOpen();
547
+ const sdk = await this.getSdkObjects();
548
+ const nc = sdk.networkClient;
549
+ return (await getLatestProgramEditionIfAvailable(nc, programId)) ?? null;
550
+ }
551
+ async getProgramChecksum(programId) {
552
+ this.assertOpen();
553
+ const sdk = await this.getSdkObjects();
554
+ const nc = sdk.networkClient;
555
+ if (typeof nc.getProgram !== "function")
556
+ return null;
557
+ try {
558
+ const source = await nc.getProgram(programId);
559
+ if (source === undefined || source === null)
560
+ return null;
561
+ const program = typeof source === "string" ? source : String(source);
562
+ const { computeProgramChecksum } = await import("./sdk-adapter.js");
563
+ return await computeProgramChecksum(this.networkId, program);
564
+ }
565
+ catch {
566
+ return null;
567
+ }
568
+ }
569
+ async close() {
570
+ this._closed = true;
571
+ // Destroy cached signer accounts
572
+ for (const signerSdk of this.signerSdkResolved.values()) {
573
+ tryDestroyAccount(signerSdk.account);
574
+ }
575
+ this.signerSdkResolved.clear();
576
+ this.signerSdkInflight.clear();
577
+ // Destroy default account if resolved
578
+ if (this.sdkObjects) {
579
+ tryDestroyAccount(this.sdkObjects.account);
580
+ }
581
+ else if (this.sdkObjectsPromise !== undefined) {
582
+ // Pending — await and destroy
583
+ try {
584
+ const sdk = await this.sdkObjectsPromise;
585
+ tryDestroyAccount(sdk.account);
586
+ }
587
+ catch {
588
+ // Init failed — nothing to destroy
589
+ }
590
+ }
591
+ this.sdkObjects = undefined;
592
+ this.sdkObjectsPromise = undefined;
593
+ }
594
+ /** Broadcast a serialized transaction via the SDK network client. */
595
+ async broadcastTransaction(transaction) {
596
+ this.assertOpen();
597
+ const sdk = await this.getSdkObjects();
598
+ const nc = sdk.networkClient;
599
+ const txId = await nc.submitTransaction(transaction);
600
+ return typeof txId === "string" ? txId.replace(/"/g, "") : String(txId);
601
+ }
602
+ async getDefaultAddress() {
603
+ if (this.privateKey) {
604
+ const sdk = await this.getSdkObjects();
605
+ const account = sdk.account;
606
+ return typeof account.address === "function"
607
+ ? account.address().to_string()
608
+ : String(account.address ?? account);
609
+ }
610
+ throw new Error("No address specified and no private key configured. " +
611
+ "Pass an address or configure a private key in network config.");
612
+ }
613
+ }
614
+ function sleep(ms) {
615
+ return new Promise((resolve) => setTimeout(resolve, ms));
616
+ }
617
+ // The Provable WASM runtime can report Leo runtime panics (integer
618
+ // under/overflow, division by zero) as process-level `RuntimeError: unreachable`
619
+ // traps while leaving the SDK promise pending. Wrap this around any local-WASM
620
+ // SDK operation to convert that specific trap into the rejection callers expect:
621
+ // - local `pm.run` (mode:"local"),
622
+ // - local failure checks via `buildAuthorizationUnchecked`, and
623
+ // - the local authorize/build phase of on-chain execute
624
+ // (`buildDevnodeExecutionTransaction` fast-path and `pm.execute` prove path),
625
+ // where a transition-body arithmetic panic traps the same way before any
626
+ // broadcast — confirmed by the tmp/bug-hunts/onchain-trap probe.
627
+ // (An explicit `assert` failure is a *catchable* `Stack …` rejection, not a
628
+ // trap, so it settles the promise normally and never reaches this handler.)
629
+ //
630
+ // When LionDen owns the capture point it rethrows unrelated uncaught exceptions;
631
+ // when another capture callback is already installed, it observes traps through
632
+ // `uncaughtExceptionMonitor` because Node suppresses the `uncaughtException`
633
+ // event. The capture point and rejection listener are process-global, so every
634
+ // trap-captured SDK operation is intentionally serialized by the module-level
635
+ // mutex above. An unrelated `RuntimeError: unreachable` during the same window
636
+ // would still be attributed to this run, so keep each wrap scoped to a single
637
+ // in-flight call.
638
+ //
639
+ // The trap only carries the generic `unreachable` message; the descriptive
640
+ // snarkVM panic text (e.g. "Integer subtraction failed on: ...") is written to
641
+ // stderr by the panic hook and is not available here, so the resulting
642
+ // `LocalExecutionWasmTrapError` cannot surface the underlying failure reason.
643
+ async function runWithLocalWasmTrapCapture(operation) {
644
+ if (typeof process === "undefined")
645
+ return operation();
646
+ const previous = localWasmTrapCaptureLock;
647
+ let release;
648
+ localWasmTrapCaptureLock = new Promise((resolve) => {
649
+ release = resolve;
650
+ });
651
+ await previous;
652
+ try {
653
+ return await raceLocalOperationWithTrap(operation, (rejectTrap) => {
654
+ // The trap escapes the SDK promise through one of two process-global
655
+ // channels, depending on the call site (observed via the onchain-trap probe):
656
+ // - `uncaughtException` — local `pm.run` and the prove-path `pm.execute`
657
+ // throw the trap out of band.
658
+ // - `unhandledRejection` — the devnode fast-path
659
+ // `buildDevnodeExecutionTransaction` rejects an internal promise that is
660
+ // never awaited.
661
+ // Watch both so every confirmed site is covered; the first to fire wins
662
+ // (the trap promise's reject is idempotent).
663
+ const cleanups = [
664
+ installUncaughtExceptionTrapHandler(rejectTrap),
665
+ installUnhandledRejectionTrapHandler(rejectTrap),
666
+ ];
667
+ return () => {
668
+ for (const cleanup of cleanups)
669
+ cleanup();
670
+ };
671
+ });
672
+ }
673
+ finally {
674
+ release();
675
+ }
676
+ }
677
+ // Convert the `RuntimeError: unreachable` trap that escapes as an
678
+ // uncaughtException (local `pm.run`, prove-path `pm.execute`) into a rejection.
679
+ // Uses the capture-callback API when available — and `uncaughtExceptionMonitor`
680
+ // when another callback already owns the slot, since Node suppresses the
681
+ // `uncaughtException` event once a capture callback is installed. Unrelated
682
+ // exceptions are re-raised so Node's default crash behavior is preserved.
683
+ function installUncaughtExceptionTrapHandler(rejectTrap) {
684
+ const hasCaptureCallbackSupport = typeof process.setUncaughtExceptionCaptureCallback === "function" &&
685
+ typeof process.hasUncaughtExceptionCaptureCallback === "function";
686
+ if (hasCaptureCallbackSupport) {
687
+ if (process.hasUncaughtExceptionCaptureCallback()) {
688
+ const handler = (error) => {
689
+ if (isLocalExecutionWasmTrap(error)) {
690
+ rejectTrap(error);
691
+ }
692
+ };
693
+ process.on("uncaughtExceptionMonitor", handler);
694
+ return () => {
695
+ process.removeListener("uncaughtExceptionMonitor", handler);
696
+ };
697
+ }
698
+ process.setUncaughtExceptionCaptureCallback((error) => {
699
+ if (isLocalExecutionWasmTrap(error)) {
700
+ rejectTrap(error);
701
+ return;
702
+ }
703
+ process.setUncaughtExceptionCaptureCallback(null);
704
+ setImmediate(() => {
705
+ throw error;
706
+ });
707
+ });
708
+ return () => {
709
+ if (process.hasUncaughtExceptionCaptureCallback()) {
710
+ process.setUncaughtExceptionCaptureCallback(null);
711
+ }
712
+ };
713
+ }
714
+ const handler = (error) => {
715
+ if (isLocalExecutionWasmTrap(error)) {
716
+ rejectTrap(error);
717
+ return;
718
+ }
719
+ // Unrelated exception: detach before re-raising. A listener-based handler
720
+ // suppresses Node's default crash, so rethrowing while still attached would
721
+ // loop straight back into this handler. Detaching first lets the rethrow
722
+ // reach Node's default (crash) behavior.
723
+ process.removeListener("uncaughtException", handler);
724
+ setImmediate(() => {
725
+ throw error;
726
+ });
727
+ };
728
+ process.on("uncaughtException", handler);
729
+ return () => {
730
+ process.removeListener("uncaughtException", handler);
731
+ };
732
+ }
733
+ // Convert the same trap when it escapes as an unhandledRejection — the devnode
734
+ // `buildDevnodeExecutionTransaction` fast-path rejects an internal promise that
735
+ // LionDen never awaits, so the trap surfaces here rather than as an
736
+ // uncaughtException (confirmed by the onchain-trap probe: trapVia=unhandledRejection).
737
+ //
738
+ // Only the specific trap is intercepted; unrelated rejections are left for other
739
+ // listeners and Node's default. Merely having a listener suppresses Node's
740
+ // built-in default for the brief, serial capture window, but every other
741
+ // `unhandledRejection` listener still fires (EventEmitter semantics), so this is
742
+ // only observable if LionDen is the sole listener — an accepted process-global
743
+ // caveat that mirrors the uncaughtException path.
744
+ function installUnhandledRejectionTrapHandler(rejectTrap) {
745
+ const handler = (reason) => {
746
+ if (isLocalExecutionWasmTrap(reason)) {
747
+ rejectTrap(reason);
748
+ }
749
+ };
750
+ process.on("unhandledRejection", handler);
751
+ return () => {
752
+ process.removeListener("unhandledRejection", handler);
753
+ };
754
+ }
755
+ function raceLocalOperationWithTrap(operation, installTrapHandler) {
756
+ let cleanupTrapHandler = () => { };
757
+ const trap = new Promise((_, reject) => {
758
+ cleanupTrapHandler = installTrapHandler((error) => {
759
+ reject(new LocalExecutionWasmTrapError(error));
760
+ });
761
+ });
762
+ let operationPromise;
763
+ try {
764
+ operationPromise = operation();
765
+ }
766
+ catch (error) {
767
+ cleanupTrapHandler();
768
+ return Promise.reject(error);
769
+ }
770
+ return Promise.race([operationPromise, trap]).finally(cleanupTrapHandler);
771
+ }
772
+ function isLocalExecutionWasmTrap(error) {
773
+ if (error instanceof WebAssembly.RuntimeError && error.message === "unreachable") {
774
+ return true;
775
+ }
776
+ if (!error || typeof error !== "object")
777
+ return false;
778
+ const candidate = error;
779
+ return candidate.name === "RuntimeError" && candidate.message === "unreachable";
780
+ }
781
+ function dedupAndSortRuntimeImports(refs) {
782
+ const seen = new Set();
783
+ const out = [];
784
+ for (const ref of refs) {
785
+ const key = ref.kind === "programId" ? `id:${ref.programId}` : `path:${ref.absolutePath}`;
786
+ if (seen.has(key))
787
+ continue;
788
+ seen.add(key);
789
+ out.push(ref);
790
+ }
791
+ out.sort((a, b) => {
792
+ const aKey = a.kind === "programId" ? a.programId : a.absolutePath;
793
+ const bKey = b.kind === "programId" ? b.programId : b.absolutePath;
794
+ if (aKey === bKey)
795
+ return 0;
796
+ return aKey < bKey ? -1 : 1;
797
+ });
798
+ return out;
799
+ }
800
+ async function getLatestProgramEditionIfAvailable(nc, programId) {
801
+ if (typeof nc.getLatestProgramEdition !== "function")
802
+ return undefined;
803
+ try {
804
+ const edition = await nc.getLatestProgramEdition(programId);
805
+ return Number.isInteger(edition) && edition >= 0 ? edition : undefined;
806
+ }
807
+ catch {
808
+ return undefined;
809
+ }
810
+ }
811
+ /**
812
+ * Flatten a `RawTransitionOutput` to a string for the `outputs: string[]`
813
+ * field of `TransitionCallResult`. Plain string entries pass through;
814
+ * id-only dynamic-record outputs surface their `id`. The faithful shape is
815
+ * preserved separately in `TransitionCallResult.rawOutputs`.
816
+ */
817
+ function toOutputString(output) {
818
+ return typeof output === "string" ? output : output.id;
819
+ }
820
+ /** Best-effort destroy of an SDK Account to release WASM private-key state. */
821
+ function tryDestroyAccount(account) {
822
+ if (account && typeof account.destroy === "function") {
823
+ try {
824
+ account.destroy();
825
+ }
826
+ catch {
827
+ // Non-fatal — some SDK versions may not support destroy
828
+ }
829
+ }
830
+ }
831
+ /**
832
+ * Run a bounded poll loop for one confirmation phase. `fetch` transport errors
833
+ * are surfaced to `classify` as a `null` response (which classifiers treat as
834
+ * `retryable`). A `fatalShape` verdict throws its error immediately; exhausting
835
+ * the deadline calls `onTimeout()`, which throws the staged
836
+ * `NetworkConfirmationTimeoutError`.
837
+ */
838
+ async function pollPhase(url, headers, deadline, classify, onTimeout) {
839
+ while (Date.now() < deadline) {
840
+ let response = null;
841
+ try {
842
+ response = await fetch(url, { headers });
843
+ }
844
+ catch {
845
+ response = null; // transient transport error — classify as retryable
846
+ }
847
+ const outcome = await classify(response);
848
+ if (outcome.kind === "confirmed")
849
+ return outcome.value;
850
+ if (outcome.kind === "fatalShape")
851
+ throw outcome.error;
852
+ await sleep(CONFIRMATION_POLL_INTERVAL_MS);
853
+ }
854
+ onTimeout();
855
+ }
856
+ /**
857
+ * Phase 1 classifier — /transaction/confirmed/<txId>.
858
+ * non-ok → retryable; 2xx + valid JSON → confirmed; 2xx + unparseable JSON →
859
+ * fatalShape. A malformed JSON body after a 200 OK is a protocol-level shape
860
+ * mismatch, not a transient retry. (A 2xx with parseable JSON but wrong shape
861
+ * is caught downstream by parseConfirmedTransitions and also surfaces fast.)
862
+ */
863
+ async function classifyConfirmedBody(response, txId) {
864
+ if (!response?.ok)
865
+ return { kind: "retryable" };
866
+ let parsed;
867
+ try {
868
+ parsed = await response.json();
869
+ }
870
+ catch (cause) {
871
+ return {
872
+ kind: "fatalShape",
873
+ error: new TransactionShapeParseError(`Transaction ${txId} confirmed body is not valid JSON`, txId, "body", cause),
874
+ };
875
+ }
876
+ // Valid JSON that isn't an object (e.g. `null`, a number) is a deterministic
877
+ // 2xx shape failure — fail fast rather than confirming a value that makes the
878
+ // orchestrator throw a raw TypeError on property access downstream.
879
+ if (parsed === null || typeof parsed !== "object") {
880
+ return {
881
+ kind: "fatalShape",
882
+ error: new TransactionShapeParseError(`Transaction ${txId} confirmed body is not a JSON object`, txId, "body"),
883
+ };
884
+ }
885
+ return { kind: "confirmed", value: parsed };
886
+ }
887
+ /**
888
+ * Phase 2 classifier — /find/blockHash/<txId>.
889
+ * non-ok → retryable; 2xx → unquote the JSON-string body → confirmed. A
890
+ * read/parse failure on a 2xx body is treated as transient (retryable),
891
+ * preserving the prior bare-catch behavior — but now as an explicit verdict.
892
+ */
893
+ async function classifyBlockHash(response) {
894
+ if (!response?.ok)
895
+ return { kind: "retryable" };
896
+ try {
897
+ const raw = (await response.text()).trim();
898
+ // Body is a JSON-encoded string: `"ab1...."`
899
+ const blockHash = raw.startsWith('"') && raw.endsWith('"') ? JSON.parse(raw) : raw;
900
+ return { kind: "confirmed", value: blockHash };
901
+ }
902
+ catch {
903
+ return { kind: "retryable" };
904
+ }
905
+ }
906
+ /**
907
+ * Phase 3 classifier — /block/<blockHash>.
908
+ * non-ok → retryable; 2xx + numeric header.metadata.height → confirmed;
909
+ * 2xx + unparseable JSON OR missing/non-numeric height → fatalShape. Both are
910
+ * hard parser disagreements that won't change on retry, so fail fast rather
911
+ * than burning the deadline.
912
+ */
913
+ async function classifyBlockHeight(response, txId, blockHash) {
914
+ if (!response?.ok)
915
+ return { kind: "retryable" };
916
+ let parsed;
917
+ try {
918
+ parsed = await response.json();
919
+ }
920
+ catch (cause) {
921
+ return {
922
+ kind: "fatalShape",
923
+ error: new TransactionShapeParseError(`Transaction ${txId} confirmed at block ${blockHash} but the block body is not valid JSON`, txId, "body", cause),
924
+ };
925
+ }
926
+ // Valid JSON that isn't an object (e.g. `null`) is a deterministic shape
927
+ // failure — guard before indexing so it surfaces as a typed shape error
928
+ // rather than a raw TypeError on `block["header"]`.
929
+ if (parsed === null || typeof parsed !== "object") {
930
+ return {
931
+ kind: "fatalShape",
932
+ error: new TransactionShapeParseError(`Transaction ${txId} confirmed at block ${blockHash} but the block body is not a JSON object`, txId, "body"),
933
+ };
934
+ }
935
+ const block = parsed;
936
+ const header = block["header"];
937
+ const metadata = header?.["metadata"];
938
+ const h = metadata?.["height"];
939
+ if (typeof h === "number") {
940
+ return { kind: "confirmed", value: h };
941
+ }
942
+ return {
943
+ kind: "fatalShape",
944
+ error: new TransactionShapeParseError(`Transaction ${txId} confirmed at block ${blockHash} but header.metadata.height is missing or non-numeric`, txId, "header.metadata.height"),
945
+ };
946
+ }
947
+ /**
948
+ * Parse confirmed-transaction body into typed ConfirmedTransitionRecord[].
949
+ *
950
+ * Shape (accepted execute):
951
+ * transaction.execution.transitions[] with each entry having:
952
+ * program: "name.aleo", function: "func", outputs: [{type, id, value, ...}]
953
+ *
954
+ * Shape (rejected → fee-only / deploy):
955
+ * no `execution` field. Returns [].
956
+ *
957
+ * Throws TransactionShapeParseError on:
958
+ * - transaction.type === "execute" with missing/malformed execution or transitions
959
+ * (would silently lose typed outputs otherwise)
960
+ * - any malformed transition entry (missing program/function, non-array
961
+ * outputs, output.value present but not a string)
962
+ */
963
+ function parseConfirmedTransitions(txData, txId) {
964
+ // Strictness signal: the transaction object itself self-identifies as
965
+ // "execute". If there is no transaction object, stay tolerant — there's
966
+ // nothing to validate against.
967
+ if (!txData)
968
+ return [];
969
+ const isExecute = txData["type"] === "execute";
970
+ const execution = txData["execution"];
971
+ if (!execution) {
972
+ if (isExecute) {
973
+ throw new TransactionShapeParseError(`Transaction ${txId}: type is "execute" but transaction.execution is missing.`, txId, "transaction.execution");
974
+ }
975
+ return [];
976
+ }
977
+ const rawTransitions = execution["transitions"];
978
+ if (rawTransitions === undefined || rawTransitions === null) {
979
+ if (isExecute) {
980
+ throw new TransactionShapeParseError(`Transaction ${txId}: type is "execute" but transaction.execution.transitions is missing.`, txId, "transaction.execution.transitions");
981
+ }
982
+ return [];
983
+ }
984
+ if (!Array.isArray(rawTransitions)) {
985
+ throw new TransactionShapeParseError(`Transaction ${txId}: transaction.execution.transitions is not an array (got ${typeof rawTransitions}).`, txId, "transaction.execution.transitions");
986
+ }
987
+ return rawTransitions.map((entry, index) => parseTransition(entry, txId, index));
988
+ }
989
+ function parseTransition(entry, txId, index) {
990
+ const path = `transaction.execution.transitions[${index}]`;
991
+ if (typeof entry !== "object" || entry === null) {
992
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path} is not an object.`, txId, path);
993
+ }
994
+ const obj = entry;
995
+ const programId = obj["program"];
996
+ if (typeof programId !== "string" || programId.length === 0) {
997
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.program is missing or not a string.`, txId, `${path}.program`);
998
+ }
999
+ const transitionName = obj["function"];
1000
+ if (typeof transitionName !== "string" || transitionName.length === 0) {
1001
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.function is missing or not a string.`, txId, `${path}.function`);
1002
+ }
1003
+ const transitionPublicKey = obj["tpk"];
1004
+ if (typeof transitionPublicKey !== "string" || transitionPublicKey.length === 0) {
1005
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.tpk is missing or not a string. The transition public key is required to decrypt private inputs/outputs.`, txId, `${path}.tpk`);
1006
+ }
1007
+ const rawOutputs = obj["outputs"];
1008
+ if (rawOutputs === undefined || rawOutputs === null) {
1009
+ // Transition with no outputs is valid (some transitions return nothing).
1010
+ return { programId, transitionName, rawOutputs: [], transitionPublicKey };
1011
+ }
1012
+ if (!Array.isArray(rawOutputs)) {
1013
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.outputs is not an array.`, txId, `${path}.outputs`);
1014
+ }
1015
+ const outputValues = rawOutputs.map((output, outputIndex) => {
1016
+ if (typeof output !== "object" || output === null) {
1017
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.outputs[${outputIndex}] is not an object.`, txId, `${path}.outputs[${outputIndex}]`);
1018
+ }
1019
+ const outputObject = output;
1020
+ if (!Object.hasOwn(outputObject, "value")) {
1021
+ const id = outputObject["id"];
1022
+ if (typeof id !== "string" || id.length === 0) {
1023
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.outputs[${outputIndex}] has no value and no string id.`, txId, `${path}.outputs[${outputIndex}].id`);
1024
+ }
1025
+ const type = outputObject["type"];
1026
+ return {
1027
+ kind: "idOnly",
1028
+ id,
1029
+ type: typeof type === "string" && type.length > 0 ? type : "unknown",
1030
+ };
1031
+ }
1032
+ const value = outputObject["value"];
1033
+ if (typeof value !== "string") {
1034
+ throw new TransactionShapeParseError(`Transaction ${txId}: ${path}.outputs[${outputIndex}].value is not a string.`, txId, `${path}.outputs[${outputIndex}].value`);
1035
+ }
1036
+ return value;
1037
+ });
1038
+ return { programId, transitionName, rawOutputs: outputValues, transitionPublicKey };
1039
+ }
1040
+ function extractLocalExecutionOutputs(result) {
1041
+ if (Array.isArray(result)) {
1042
+ return result.map(String);
1043
+ }
1044
+ if (result && typeof result === "object") {
1045
+ const executionResponse = result;
1046
+ if (typeof executionResponse.getOutputs === "function") {
1047
+ const outputs = executionResponse.getOutputs();
1048
+ return Array.isArray(outputs) ? outputs.map(String) : [];
1049
+ }
1050
+ if (Array.isArray(executionResponse.outputs)) {
1051
+ return executionResponse.outputs.map(String);
1052
+ }
1053
+ }
1054
+ return [];
1055
+ }
1056
+ function isCatchableLocalVmError(error) {
1057
+ const message = errorMessage(error);
1058
+ return (message.startsWith("Stack authorization failed:") ||
1059
+ message.startsWith("Stack evaluation failed:"));
1060
+ }
1061
+ function errorMessage(error) {
1062
+ return error instanceof Error ? error.message : String(error);
1063
+ }
1064
+ //# sourceMappingURL=connection.js.map