@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.
- package/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/accounts.d.ts +12 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +38 -0
- package/dist/accounts.js.map +1 -0
- package/dist/connection.d.ts +121 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +1064 -0
- package/dist/connection.js.map +1 -0
- package/dist/devnode-backend.d.ts +53 -0
- package/dist/devnode-backend.d.ts.map +1 -0
- package/dist/devnode-backend.js +121 -0
- package/dist/devnode-backend.js.map +1 -0
- package/dist/devnode-manager.d.ts +130 -0
- package/dist/devnode-manager.d.ts.map +1 -0
- package/dist/devnode-manager.js +546 -0
- package/dist/devnode-manager.js.map +1 -0
- package/dist/execution-key-cache.d.ts +71 -0
- package/dist/execution-key-cache.d.ts.map +1 -0
- package/dist/execution-key-cache.js +286 -0
- package/dist/execution-key-cache.js.map +1 -0
- package/dist/file-io.d.ts +2 -0
- package/dist/file-io.d.ts.map +1 -0
- package/dist/file-io.js +9 -0
- package/dist/file-io.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/named-account-manager.d.ts +40 -0
- package/dist/named-account-manager.d.ts.map +1 -0
- package/dist/named-account-manager.js +116 -0
- package/dist/named-account-manager.js.map +1 -0
- package/dist/network-manager.d.ts +36 -0
- package/dist/network-manager.d.ts.map +1 -0
- package/dist/network-manager.js +226 -0
- package/dist/network-manager.js.map +1 -0
- package/dist/sdk-adapter.d.ts +271 -0
- package/dist/sdk-adapter.d.ts.map +1 -0
- package/dist/sdk-adapter.js +997 -0
- package/dist/sdk-adapter.js.map +1 -0
- package/dist/sdk-diagnostics.d.ts +113 -0
- package/dist/sdk-diagnostics.d.ts.map +1 -0
- package/dist/sdk-diagnostics.js +241 -0
- package/dist/sdk-diagnostics.js.map +1 -0
- package/dist/transition-selector.d.ts +15 -0
- package/dist/transition-selector.d.ts.map +1 -0
- package/dist/transition-selector.js +45 -0
- package/dist/transition-selector.js.map +1 -0
- package/dist/types.d.ts +484 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +129 -0
- package/dist/types.js.map +1 -0
- 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
|