@black-market/sdk 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,589 @@
1
+ // Running a fixed launch example deliberately signs, broadcasts and publishes.
2
+ import assert from "node:assert/strict";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
+ import { mkdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import {
7
+ decodeEventLog, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, keccak256,
8
+ recoverTypedDataAddress, toHex, zeroAddress, zeroHash,
9
+ } from "viem";
10
+ import {
11
+ createLocalLaunchWallet, errorEvidence, findRevertData, launchApiUrl,
12
+ loadExampleEnvironment, readExampleConfiguration, pause, publishUntilIndexed,
13
+ Redactor, rpcRequestSignal, RunArtifacts, exampleError,
14
+ submitSignedRecorded, transactionEnvelope,
15
+ } from "./launch-example-support.mjs";
16
+ const UNIT = 10n ** 18n;
17
+ const OPENING_PRICE = 2n ** 96n;
18
+ const MIN_SQRT_RATIO = 4295128739n;
19
+ const MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342n;
20
+ const same = (left, right) => typeof left === "string" && typeof right === "string" && left.toLowerCase() === right.toLowerCase();
21
+ const addressEqual = (actual, expected, label) => assert.ok(same(actual, expected), `${label}: expected ${expected}, got ${actual}`);
22
+
23
+
24
+ function validateCase(scenario) {
25
+ if (![0, 1].includes(scenario.tokenKind) || ![0, 1, 2].includes(scenario.rewardMode) || !["atomic", "staged"].includes(scenario.mode)) throw exampleError("UNSUPPORTED_CASE", "Case token kind, reward mode and execution mode must be explicit supported values");
26
+ const allowed = new Set(["id", "description", "tokenKind", "rewardMode", "mode", "markets", "buysPerMarket", "burnBps"]);
27
+ for (const key of Object.keys(scenario)) if (!allowed.has(key)) throw exampleError("UNSUPPORTED_CASE", `Unsupported case economic field ${key}; it cannot be silently ignored`);
28
+ if (!Array.isArray(scenario.markets) || scenario.markets.length < 1 || scenario.markets.length > 16 || !Number.isSafeInteger(scenario.buysPerMarket) || scenario.buysPerMarket < 1 || scenario.buysPerMarket * scenario.markets.length > 64) throw exampleError("UNSUPPORTED_CASE", "Case market/buy counts exceed lifecycle bounds");
29
+ if (scenario.burnBps !== undefined && (!Number.isSafeInteger(scenario.burnBps) || scenario.burnBps < 0 || scenario.burnBps > 10000 || scenario.rewardMode !== 0)) throw exampleError("UNSUPPORTED_CASE", "Token-only burn policy requires an explicit reward-free 0..10000 share");
30
+ let positions = 0;
31
+ for (const market of scenario.markets) {
32
+ if (!["v4", "abyss"].includes(market.venue) || !Number.isSafeInteger(market.positions) || market.positions < 1 || market.positions > 32) throw exampleError("UNSUPPORTED_CASE", "Case requires a supported venue and 1..32 exact positions per market");
33
+ for (const key of Object.keys(market)) if (!["venue", "positions", "feeMode"].includes(key)) throw exampleError("UNSUPPORTED_CASE", `Unsupported market economic field ${key}`);
34
+ if (market.feeMode !== undefined && (market.venue !== "v4" || ![0, 1].includes(market.feeMode))) throw exampleError("UNSUPPORTED_CASE", "Only V4 markets support explicit feeMode 0/1");
35
+ positions += market.positions;
36
+ }
37
+ if (positions > 64) throw exampleError("UNSUPPORTED_CASE", "Case exceeds 64 permanent positions");
38
+ }
39
+
40
+ function rpcClient(url, artifacts, signal, label) {
41
+ let id = 0;
42
+ const endpoint = new URL(url);
43
+ const headers = { "Content-Type": "application/json" };
44
+ if (endpoint.username || endpoint.password) {
45
+ const authorization = `Basic ${Buffer.from(`${decodeURIComponent(endpoint.username)}:${decodeURIComponent(endpoint.password)}`).toString("base64")}`;
46
+ artifacts.redactor.addSecret(authorization);
47
+ headers.Authorization = authorization;
48
+ endpoint.username = ""; endpoint.password = "";
49
+ }
50
+ return { async request({ method, params = [] }) {
51
+ const requestSignal = rpcRequestSignal(signal, label, method);
52
+ const started = Date.now();
53
+ try {
54
+ const response = await fetch(endpoint, {
55
+ method: "POST", redirect: "error", headers,
56
+ body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
57
+ signal: requestSignal,
58
+ });
59
+ if (!response.ok) throw exampleError("RPC_HTTP_ERROR", `${label} ${method}: HTTP ${response.status}`, { status: response.status });
60
+ const body = await response.json();
61
+ if (body.error) throw exampleError(body.error.code ?? "RPC_ERROR", `${label} ${method}: ${body.error.message}`, { data: body.error.data, cause: body.error });
62
+ if (!("result" in body)) throw exampleError("INVALID_RPC_RESPONSE", `${label} ${method}: result was omitted`, { data: body });
63
+ if (method === "eth_simulateV1" || label === "simulation" && ["eth_sendTransaction", "anvil_reset", "evm_snapshot", "evm_revert"].includes(method)) artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
64
+ return body.result;
65
+ } catch (error) {
66
+ artifacts.event("rpc-error", { backend: label, method, params, elapsedMs: Date.now() - started, error: errorEvidence(error) });
67
+ throw error;
68
+ }
69
+ } };
70
+ }
71
+
72
+ function contractReader(client) {
73
+ return async (to, abi, functionName, args = [], tag = "latest") => decodeFunctionResult({
74
+ abi, functionName,
75
+ data: await client.request({ method: "eth_call", params: [{ to, data: encodeFunctionData({ abi, functionName, args }) }, tag] }),
76
+ });
77
+ }
78
+
79
+ function receiptEvents(receipt, address, abi, eventName) {
80
+ return receipt.logs.filter((log) => same(log.address, address)).flatMap((log) => {
81
+ let decoded;
82
+ try { decoded = decodeEventLog({ abi, data: log.data, topics: log.topics, strict: true }); }
83
+ catch { return []; } // A different event at the same contract is not the expected event.
84
+ return decoded.eventName === eventName ? [decoded.args] : [];
85
+ });
86
+ }
87
+
88
+ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal }) {
89
+ const read = contractReader(client);
90
+ artifacts.stage("profile-discovery");
91
+ const profiles = await sdk.readLifecycleProfiles({ client, orchestrator: fixture.orchestrator });
92
+ artifacts.save("profiles.json", profiles);
93
+ artifacts.event("registry-profiles", { profiles });
94
+ const selected = {};
95
+ if (scenario.markets.some((row) => row.venue === "v4")) {
96
+ const bound = profiles.filter((row) => row.venueKind === "uniswap-v4" && same(row.registration.configSchema, sdk.V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA) && row.topology.hookTopology === 2 && row.topology.configVersion === 5);
97
+ if (bound.length !== 1 || !bound[0].admitted || !bound[0].envelope || !bound[0].developerTerms) throw exampleError("PROFILE_NOT_ADMITTED", "An unambiguous admitted certified pool-bound V4/schema5 profile with frozen terms is required", { data: bound });
98
+ selected.v4 = bound[0];
99
+ }
100
+ if (scenario.markets.some((row) => row.venue === "abyss")) {
101
+ const canonical = [];
102
+ const adapters = new Set(profiles.filter((row) => row.venueKind === "abyss").map((row) => row.adapter.implementation.toLowerCase()));
103
+ for (const adapter of adapters) {
104
+ const canonicalId = await read(adapter, sdk.abyssLifecycleAdapterAbi, "profileId", [3]);
105
+ const profile = profiles.find((row) => same(row.id, canonicalId) && same(row.adapter.implementation, adapter));
106
+ if (profile) canonical.push(profile);
107
+ }
108
+ if (canonical.length !== 1 || !canonical[0].admitted || canonical[0].topology.configVersion !== 1 || !same(canonical[0].registration.configSchema, sdk.ABYSS_LIFECYCLE_CONFIG_SCHEMA)) throw exampleError("PROFILE_NOT_ADMITTED", "The registry-discovered canonical Abyss profile3 QUOTE_ORACLE/schema1 is required", { data: canonical });
109
+ selected.abyss = canonical[0];
110
+ }
111
+ const escrow = await read(fixture.orchestrator, sdk.launchLifecycleAbi, "fundingEscrow");
112
+ addressEqual(await read(escrow, sdk.lifecycleFundingEscrowAbi, "wrappedNative"), fixture.quoteAsset, "Actual native-wrap funding binding");
113
+ assert.equal(await read(fixture.quoteAsset, sdk.erc20Abi, "decimals"), fixture.quoteDecimals, "Actual quote decimals");
114
+ if (selected.v4) addressEqual(await read(selected.v4.adapter.implementation, sdk.poolMarketAdapterV1Abi, "oracleFactory"), fixture.oracleFactory, "V4 registered oracle factory");
115
+ if (selected.abyss) addressEqual(await read(selected.abyss.adapter.implementation, sdk.abyssLifecycleAdapterAbi, "factory"), fixture.oracleFactory, "Abyss registered factory");
116
+ fixture.oracleConfigId = keccak256(encodeAbiParameters([{ type: "uint24" }, { type: "uint16" }], [1, 4096]));
117
+ const oracle = await read(fixture.oracleFactory, sdk.abyssFactoryAbi, "oracleConfigs", [fixture.oracleConfigId]);
118
+ assert.equal(Number(oracle[0]), 1, "Canonical P1 oracle must be registered");
119
+ assert.equal(Number(oracle[1]), 4096, "Canonical P1 oracle cardinality");
120
+ artifacts.event("quote-oracle-discovered", { quoteAsset: fixture.quoteAsset, oracleFactory: fixture.oracleFactory, oracleConfigId: fixture.oracleConfigId, oracle });
121
+ const block = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
122
+ const identity = { nonce: BigInt(`0x${randomBytes(32).toString("hex")}`), tokenSalt: `0x${randomBytes(32).toString("hex")}` };
123
+ artifacts.save("identity.json", { ...identity, caseId: scenario.id, creator: fixture.creator });
124
+ const token = {
125
+ kind: scenario.tokenKind, rewardMode: scenario.rewardMode, name: `Example ${scenario.id}`, symbol: "EXAMPLE",
126
+ supply: (scenario.tokenKind === 1 ? 10_000n : 1_000_000n) * UNIT,
127
+ nftUnit: scenario.tokenKind === 1 ? 100n * UNIT : 0n,
128
+ metadataURI: scenario.tokenKind === 1 ? fixture.nftBaseUri : "", salt: identity.tokenSalt,
129
+ inventoryRecipient: fixture.creator, burnOnCancel: false,
130
+ };
131
+ const draft = {
132
+ chainId: BigInt(fixture.chainId), orchestrator: fixture.orchestrator, creator: fixture.creator, nonce: identity.nonce,
133
+ token, funding: [], feeAssets: [], markets: [], buys: [], deadline: BigInt(block.timestamp) + 3600n, executorFeeBps: 275,
134
+ };
135
+ // CREATE2 token identity is independent of finalized market config/hook salts.
136
+ const predictedToken = await sdk.predictLifecycleToken({ client, plan: draft });
137
+ const token0 = BigInt(predictedToken) < BigInt(fixture.quoteAsset);
138
+ const markets = scenario.markets.map((spec, marketIndex) => {
139
+ const profile = selected[spec.venue];
140
+ const positions = Array.from({ length: spec.positions }, (_unused, index) => ({
141
+ tickLower: token0 ? index * 120 : index === spec.positions - 1 ? -887220 : -(index + 1) * 120,
142
+ tickUpper: token0 ? index === spec.positions - 1 ? 887220 : (index + 1) * 120 : -index * 120,
143
+ liquidity: 1000n * UNIT,
144
+ }));
145
+ let config;
146
+ if (spec.venue === "v4") {
147
+ const envelope = profile.envelope;
148
+ const feeMode = spec.feeMode ?? 0;
149
+ if (spec.positions > envelope.bounds.maximumPositions || 60 < envelope.bounds.minimumTickSpacing || 60 > envelope.bounds.maximumTickSpacing || !(envelope.bounds.feeModeFlags & 1 << feeMode)) throw exampleError("UNSUPPORTED_CASE", "Exact V4 positions, spacing or fee mode exceed the actual certified envelope", { data: { spec, bounds: envelope.bounds } });
150
+ config = sdk.encodePoolBoundV4LifecycleMarketConfig({
151
+ version: 5, lpFeePips: 3000, hookFeePips: 10000, tickSpacing: 60, sqrtPriceX96: OPENING_PRICE,
152
+ feeMode, protocolFeeDenominator: envelope.protocolFeeDenominator, treasury: envelope.protocolTreasury,
153
+ externalLiquidityDisabled: true, oracleConfigId: fixture.oracleConfigId, profileId: profile.id,
154
+ termsDigest: envelope.termsDigest, developerBeneficiary: envelope.beneficiary, developerFeeBps: 0, hookSalt: zeroHash,
155
+ positions: positions.map((position, index) => ({ ...position, salt: toHex(BigInt(marketIndex * 32 + index + 1), { size: 32 }), maxTokenAmount: 1100n * UNIT })),
156
+ });
157
+ } else config = sdk.encodeAbyssLifecycleMarketConfig({
158
+ profile: 3, fee: 3000, oracleConfigId: fixture.oracleConfigId, openingSqrtPriceX96: OPENING_PRICE,
159
+ positions: positions.map((position) => ({ ...position, tokenAmountMaximum: 1100n * UNIT })),
160
+ });
161
+ return { adapterId: profile.registration.adapterId, profileId: profile.id, quoteAsset: fixture.quoteAsset, tokenBudget: 1100n * UNIT, configVersion: spec.venue === "v4" ? 5 : 1, config };
162
+ });
163
+ const buys = markets.flatMap((_market, marketIndex) => Array.from({ length: scenario.buysPerMarket }, () => ({
164
+ marketIndex, quoteAmountIn: 10n ** 15n, minTokenOut: 1n, recipient: fixture.creator,
165
+ sqrtPriceLimitX96: token0 ? MAX_SQRT_RATIO - 1n : MIN_SQRT_RATIO + 1n,
166
+ })));
167
+ const amount = buys.reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
168
+ const feeAssets = [predictedToken, fixture.quoteAsset].sort((left, right) => BigInt(left) < BigInt(right) ? -1 : 1).map((asset) => {
169
+ const rewardsBps = scenario.rewardMode === 0 ? 0 : 4000;
170
+ const burnBps = same(asset, predictedToken) ? scenario.burnBps ?? 0 : 0;
171
+ return { asset, ownerBps: 10000 - rewardsBps - burnBps, rewardsBps, burnBps };
172
+ });
173
+ const plan = { ...draft, markets, buys, feeAssets, funding: [{ asset: fixture.quoteAsset, amount, kind: sdk.LifecycleFundingKind.NativeWrap, inputAsset: fixture.quoteAsset, inputAmount: amount, target: zeroAddress, data: "0x" }] };
174
+ artifacts.save("draft-plan.json", plan);
175
+ artifacts.result.chain.predictedToken = predictedToken;
176
+ artifacts.stage("salt-finalization", { predictedToken, selectedProfiles: Object.fromEntries(Object.entries(selected).map(([key, profile]) => [key, profile.id])) });
177
+ const prepared = await sdk.preparePoolBoundLifecyclePlan({ client, plan, signal,
178
+ onProgress: (progress) => artifacts.event("salt-mining", progress),
179
+ });
180
+ artifacts.save("plan.json", JSON.parse(sdk.serializeLaunchPlan(prepared.plan)));
181
+ artifacts.save("hook-deployments.json", prepared.deployments);
182
+ const actualPrediction = await sdk.predictLifecycleToken({ client, plan: prepared.plan });
183
+ addressEqual(actualPrediction, predictedToken, "Finalized token prediction");
184
+ artifacts.result.chain.planHash = sdk.hashLaunchPlan(prepared.plan);
185
+ artifacts.result.chain.launchId = sdk.hashLaunchIdentity(prepared.plan);
186
+ artifacts.event("plan-finalized", { predictedToken, planHash: artifacts.result.chain.planHash, launchId: artifacts.result.chain.launchId, mode: scenario.mode, markets: markets.length, positions: scenario.markets.reduce((sum, market) => sum + market.positions, 0), buys: buys.length, deployments: prepared.deployments });
187
+ return { plan: prepared.plan, predictedToken };
188
+ }
189
+
190
+ function apiTransport({ artifacts, signal, apiUrl }) {
191
+ let requestTimeoutMs = 30_000;
192
+ return {
193
+ setTimeoutMs(ms) { requestTimeoutMs = Math.max(1, Math.min(30_000, ms)); },
194
+ async fetch(url, init = {}) {
195
+ if (new URL(url).origin !== new URL(apiUrl).origin) throw exampleError("UNSAFE_ENDPOINT", "API request changed the configured origin");
196
+ launchApiUrl(new URL(url).origin);
197
+ const started = Date.now();
198
+ artifacts.event("api-request", { method: init.method ?? "GET", url: String(url), headers: init.headers, body: init.body ? JSON.parse(init.body) : undefined });
199
+ try {
200
+ const response = await fetch(url, { ...init, redirect: "error", signal: AbortSignal.any([signal, AbortSignal.timeout(requestTimeoutMs)]) });
201
+ const text = await response.clone().text();
202
+ let body; try { body = JSON.parse(text); } catch { body = text; }
203
+ if (body?.capability) {
204
+ artifacts.redactor.addSecret(body.capability);
205
+ artifacts.privateRecovery({ ...artifacts.recovery, session: body, capability: body.capability });
206
+ }
207
+ artifacts.event("api-response", { method: init.method ?? "GET", url: String(url), status: response.status, retryAfter: response.headers.get("Retry-After"), body, elapsedMs: Date.now() - started });
208
+ return response;
209
+ } catch (error) {
210
+ artifacts.event("api-error", { url: String(url), elapsedMs: Date.now() - started, error: errorEvidence(error) });
211
+ throw error;
212
+ }
213
+ },
214
+ };
215
+ }
216
+
217
+ async function stageMetadata({ fixture, plan, sdk, artifacts, transport, wallet }) {
218
+ artifacts.stage("api-metadata");
219
+ artifacts.result.api.status = "staging";
220
+ delete artifacts.result.api.reason;
221
+ const metadata = { name: plan.token.name, symbol: plan.token.symbol, description: `Actual Node SDK launch example: ${artifacts.result.caseId}` };
222
+ const idempotencyKey = `node-example-${randomUUID()}`;
223
+ const nonce = `0x${randomBytes(32).toString("hex")}`;
224
+ const deadline = BigInt(Math.floor(Date.now() / 1000)) + 600n;
225
+ const typedData = {
226
+ domain: { name: "Abyss Launch Attribution", version: "1", chainId: Number(plan.chainId), verifyingContract: plan.orchestrator },
227
+ types: { EIP712Domain: [{ name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }], ...sdk.launchAttributionTypes },
228
+ primaryType: "LaunchAttribution",
229
+ message: { chainId: plan.chainId.toString(), wallet: plan.creator, metadataHash: sdk.canonicalLaunchMetadataHash(metadata), imageSha256: zeroHash, imageContentType: "", imageContentLength: "0", idempotencyKey, nonce, deadline: deadline.toString() },
230
+ };
231
+ const signature = await wallet.signTypedData(typedData);
232
+ artifacts.redactor.addSecret(signature);
233
+ const request = { chainId: Number(plan.chainId), wallet: plan.creator, metadata, authorization: { nonce, deadline: deadline.toString(), signature } };
234
+ const recovery = { ...artifacts.recovery, schema: "black-market.launch-example-recovery.v1", sdk: "node", caseId: artifacts.result.caseId, apiUrl: fixture.apiUrl, idempotencyKey, typedData, request };
235
+ artifacts.privateRecovery(recovery);
236
+ const signer = await recoverTypedDataAddress({ ...typedData, signature });
237
+ addressEqual(signer, plan.creator, "Attribution signer");
238
+ const api = new sdk.LaunchApiClient({ baseUrl: fixture.apiUrl, retries: 1, fetch: transport.fetch.bind(transport) });
239
+ const session = await api.createUploadSession(request, idempotencyKey);
240
+ if (session.capability) artifacts.redactor.addSecret(session.capability);
241
+ // Persist the one-time capability before even validating the response.
242
+ artifacts.privateRecovery({ ...recovery, session, capability: session.capability });
243
+ artifacts.result.api.session = session;
244
+ artifacts.result.api.sessionId = session.sessionId;
245
+ artifacts.event("metadata-session-created", { session });
246
+ assert.ok(typeof session.capability === "string" && session.capability.length > 0, "Actual API must issue the one-time session capability");
247
+ assert.equal(session.chainId, Number(plan.chainId)); addressEqual(session.wallet, plan.creator, "Session wallet");
248
+ assert.equal(session.status, "ready_to_launch"); assert.equal(session.metadata.name, plan.token.name); assert.equal(session.metadata.symbol, plan.token.symbol);
249
+ assert.equal(session.image, null, "This example stages metadata only");
250
+ artifacts.result.api.status = "session-ready";
251
+ return { api, sessionId: session.sessionId, capability: session.capability };
252
+ }
253
+
254
+ function limitSource(fixture, artifacts) {
255
+ const policy = fixture.executionLimits;
256
+ return async ({ block, chainId, account, orchestrator }) => {
257
+ const limits = {
258
+ chainId, account, orchestrator, observedBlockNumber: block.number, observedBlockHash: block.hash,
259
+ chainGasLimit: BigInt(policy.chainTransactionGasLimit) < block.gasLimit ? BigInt(policy.chainTransactionGasLimit) : block.gasLimit, rpcGasLimit: BigInt(policy.rpcTransactionGasLimit),
260
+ accountGasLimit: BigInt(policy.accountTransactionGasLimit), maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
261
+ };
262
+ artifacts.event("execution-limits", { limits, provenance: policy.provenance });
263
+ return limits;
264
+ };
265
+ }
266
+
267
+ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, fork, wallet }) {
268
+ const references = [];
269
+ let activation;
270
+ for (let step = 0; step < 64; step += 1) {
271
+ artifacts.stage("build-next", { step });
272
+ const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, fork });
273
+ if (!next) return { references, activation };
274
+ artifacts.event("next-admitted", { transaction: next });
275
+ assert.equal(next.chainId, Number(planned.chainId));
276
+ assert.equal(BigInt(await client.request({ method: "eth_chainId" })), planned.chainId, "Recheck execution chain before every send");
277
+ addressEqual(next.from, planned.plan.creator, "Exact transaction creator");
278
+ addressEqual(next.to, planned.plan.orchestrator, "Native-wrap launch transaction destination");
279
+ const functions = { atomic: "launchAtomic", begin: "beginLaunch", prepare: "prepareMarkets", activate: "activateLaunch" };
280
+ const functionName = functions[next.kind];
281
+ assert.ok(functionName, "These native-wrap cases do not permit unrelated approval/cancellation writes");
282
+ const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
283
+ assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
284
+ assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.admission?.admitted === true, "Actual next transaction needs stateful gas/envelope admission");
285
+ const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
286
+ const ceiling = [BigInt(head.gasLimit), ...["chainTransactionGasLimit", "rpcTransactionGasLimit", "accountTransactionGasLimit"].map((key) => BigInt(fixture.executionLimits[key]))].reduce((left, right) => left < right ? left : right);
287
+ assert.ok(next.gas > 0n && next.gas <= ceiling, "Admitted gas must fit all explicit caps");
288
+ assert.ok((next.data.length - 2) / 2 <= fixture.executionLimits.maxCalldataBytes, "Exact calldata cap");
289
+ const envelope = transactionEnvelope(next);
290
+ const wire = { ...envelope };
291
+ for (const key of ["chainId", "value", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce"]) if (wire[key] !== undefined) wire[key] = toHex(BigInt(wire[key]));
292
+ artifacts.stage(`submit-${next.kind}`, { id: next.id });
293
+ artifacts.result.execution = "running"; artifacts.result.chain.status = "executing";
294
+ const submitted = await submitSignedRecorded({
295
+ transaction: { ...next, wire }, artifacts, signal, wallet, client,
296
+ wait: async (hash) => {
297
+ const deadline = Date.now() + 30_000;
298
+ while (Date.now() < deadline) {
299
+ const receipt = await client.request({ method: "eth_getTransactionReceipt", params: [hash] });
300
+ if (receipt) return receipt;
301
+ await pause(100, signal);
302
+ }
303
+ throw exampleError("RECEIPT_TIMEOUT", `Submitted transaction was not included within 30 seconds: ${hash}`, { transactionHash: hash });
304
+ },
305
+ });
306
+ const { row, receipt } = submitted;
307
+ addressEqual(receipt.transactionHash, row.transactionHash, "Receipt transaction hash");
308
+ if (BigInt(receipt.status) !== 1n) {
309
+ let trace;
310
+ try { trace = await client.request({ method: "debug_traceTransaction", params: [row.transactionHash, { tracer: "callTracer" }] }); }
311
+ catch (error) { artifacts.event("revert-trace-unavailable", { transactionHash: row.transactionHash, error: errorEvidence(error) }); }
312
+ if (trace) artifacts.event("transaction-revert-trace", { transactionHash: row.transactionHash, trace });
313
+ row.status = "reverted"; artifacts.saveReceipts();
314
+ throw exampleError("TRANSACTION_REVERTED", `Actual source-chain transaction reverted: ${row.transactionHash}`, { transactionHash: row.transactionHash, revertData: findRevertData(trace), receipt });
315
+ }
316
+ const includedBlock = await client.request({ method: "eth_getBlockByNumber", params: [receipt.blockNumber, false] });
317
+ addressEqual(includedBlock?.hash, receipt.blockHash, "Canonical included block");
318
+ const latest = BigInt(await client.request({ method: "eth_blockNumber" }));
319
+ assert.ok(latest >= BigInt(receipt.blockNumber), "Required one confirmation must be included canonically");
320
+ const reference = { transactionHash: row.transactionHash, observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: 1 };
321
+ row.status = "confirmed"; row.reference = reference; artifacts.saveReceipts();
322
+ references.push(reference);
323
+ if (["atomic", "activate"].includes(next.kind)) {
324
+ assert.equal(activation, undefined, "Exactly one activation transaction is allowed");
325
+ activation = { transactionHash: row.transactionHash, receipt };
326
+ artifacts.result.chain.activationTransactionHash = row.transactionHash;
327
+ }
328
+ artifacts.event("transaction-confirmed", { reference, gasUsed: receipt.gasUsed, transactionKind: next.kind });
329
+ }
330
+ throw exampleError("EXECUTION_STEP_LIMIT", "Lifecycle did not terminate within 64 exact SDK transactions; state/hashes are retained");
331
+ }
332
+
333
+ async function verifyChain({ client, planned, references, activation, fixture, scenario, sdk, artifacts }) {
334
+ artifacts.stage("chain-verification");
335
+ assert.ok(activation, "A real atomic/activate receipt is required");
336
+ const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: 1 });
337
+ artifacts.result.chain.progress = progress;
338
+ artifacts.save("progress.json", progress);
339
+ artifacts.event("canonical-progress", { progress });
340
+ const expectedPositions = scenario.markets.reduce((sum, market) => sum + market.positions, 0);
341
+ assert.equal(progress.canonical.phase, sdk.LifecyclePhase.Active);
342
+ addressEqual(progress.canonical.token, planned.predictedToken, "Actual canonical token");
343
+ assert.equal(progress.canonical.marketCount, scenario.markets.length);
344
+ assert.equal(progress.canonical.preparedMarkets, scenario.markets.length);
345
+ assert.equal(progress.canonical.positionCount, expectedPositions);
346
+ assert.equal(progress.canonical.buyCount, planned.plan.buys.length);
347
+ assert.equal(progress.markets.length, scenario.markets.length);
348
+ assert.ok(progress.confirmationSafe); assert.ok(progress.receipts.every((row) => row.status === "confirmed"));
349
+ const tag = toHex(progress.confirmedBlockNumber);
350
+ const read = (to, abi, name, args = []) => contractReader(client)(to, abi, name, args, tag);
351
+ const token = planned.predictedToken;
352
+ const runtime = await client.request({ method: "eth_getCode", params: [token, tag] });
353
+ assert.ok(runtime !== "0x", "Actual deployed token runtime is required");
354
+ const factoryRuntime = await client.request({ method: "eth_getCode", params: [planned.tokenFactory, tag] });
355
+ assert.equal(keccak256(factoryRuntime).toLowerCase(), planned.tokenFactoryCodeHash.toLowerCase(), "Current immutable token-factory runtime binding");
356
+ const deployments = artifacts.transactions.flatMap((row) => receiptEvents(row.receipt, planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "TokenDeployed"));
357
+ assert.equal(deployments.length, 1, "Actual token-factory deployment event");
358
+ addressEqual(deployments[0].token, token, "Factory deployed token"); addressEqual(deployments[0].launchId, planned.launchId, "Factory launch ID");
359
+ assert.equal(deployments[0].kind, scenario.tokenKind, "Actual factory token kind");
360
+ addressEqual(await read(planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "tokenOfLaunch", [planned.launchId]), token, "Factory token binding");
361
+ addressEqual(await read(planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "launchOfToken", [token]), planned.launchId, "Factory reverse launch binding");
362
+ const tokenState = {};
363
+ for (const name of ["authority", "tokenFactory", "launchId", "rewardMode", "initialSupply", "burnOnCancel", "active", "cancelled", "exclusionsFinalized", "feeHub", "rewardModule"]) tokenState[name] = await read(token, sdk.lifecycleTokenContextAbi, name);
364
+ for (const name of ["name", "symbol", "decimals", "totalSupply"]) tokenState[name] = await read(token, sdk.erc20Abi, name);
365
+ tokenState.runtimeCodeHash = keccak256(runtime); tokenState.kind = deployments[0].kind;
366
+ artifacts.result.chain.token = token;
367
+ artifacts.result.chain.tokenState = tokenState;
368
+ artifacts.event("actual-token", { token, tokenState });
369
+ assert.equal(tokenState.name, planned.plan.token.name); assert.equal(tokenState.symbol, planned.plan.token.symbol); assert.equal(tokenState.decimals, 18);
370
+ assert.equal(tokenState.initialSupply, planned.plan.token.supply); assert.equal(tokenState.totalSupply, planned.plan.token.supply);
371
+ assert.equal(tokenState.rewardMode, scenario.rewardMode); assert.equal(tokenState.burnOnCancel, false);
372
+ assert.equal(tokenState.active, true); assert.equal(tokenState.cancelled, false); assert.equal(tokenState.exclusionsFinalized, true);
373
+ addressEqual(tokenState.authority, fixture.orchestrator, "Token authority"); addressEqual(tokenState.tokenFactory, planned.tokenFactory, "Token factory"); addressEqual(tokenState.launchId, planned.launchId, "Token launch ID");
374
+ addressEqual(tokenState.feeHub, progress.canonical.feeHub, "Token fee hub"); addressEqual(tokenState.rewardModule, progress.canonical.rewards, "Token rewards binding");
375
+ if (scenario.rewardMode === 0) addressEqual(tokenState.rewardModule, zeroAddress, "Reward-free module");
376
+ else if (scenario.rewardMode === 2) addressEqual(tokenState.rewardModule, token, "Embedded holder dividends");
377
+ else {
378
+ assert.ok(!same(tokenState.rewardModule, token) && !same(tokenState.rewardModule, zeroAddress), "Actual distinct staking module");
379
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [tokenState.rewardModule, tag] }), "0x", "Staking runtime");
380
+ }
381
+ if (scenario.tokenKind === 1) {
382
+ tokenState.nftUnit = await read(token, sdk.lifecycleErc404Abi, "unit");
383
+ tokenState.metadataURI = await read(token, sdk.lifecycleErc404Abi, "baseURI");
384
+ tokenState.mirror = await read(token, sdk.lifecycleErc404Abi, "mirrorERC721");
385
+ artifacts.event("actual-erc404", { nftUnit: tokenState.nftUnit, metadataURI: tokenState.metadataURI, mirror: tokenState.mirror });
386
+ assert.equal(tokenState.nftUnit, planned.plan.token.nftUnit); assert.equal(tokenState.metadataURI, planned.plan.token.metadataURI);
387
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [tokenState.mirror, tag] }), "0x", "Real ERC404 mirror runtime");
388
+ addressEqual(await read(tokenState.mirror, sdk.lifecycleErc404MirrorAbi, "baseERC20"), token, "ERC404 mirror link");
389
+ }
390
+ const hub = tokenState.feeHub;
391
+ addressEqual(await read(hub, sdk.lifecycleFeeHubAbi, "launchToken"), token, "Hub token");
392
+ addressEqual(await read(hub, sdk.lifecycleFeeHubAbi, "rewards"), tokenState.rewardModule, "Hub reward module");
393
+ assert.equal(await read(hub, sdk.lifecycleFeeHubAbi, "executorFeeBps"), 275);
394
+ assert.equal(await read(hub, sdk.lifecycleFeeHubAbi, "finalized"), true);
395
+ const assets = await read(hub, sdk.lifecycleFeeHubAbi, "assets");
396
+ assert.deepEqual(assets.map((value) => value.toLowerCase()), planned.plan.feeAssets.map((policy) => policy.asset.toLowerCase()));
397
+ const expectedRewardAssets = planned.plan.feeAssets.filter((policy) => policy.rewardsBps > 0).map((policy) => policy.asset.toLowerCase());
398
+ const rewardAssets = await read(hub, sdk.lifecycleFeeHubAbi, "rewardAssets");
399
+ assert.deepEqual(rewardAssets.map((value) => value.toLowerCase()), expectedRewardAssets);
400
+ if (scenario.rewardMode !== 0) {
401
+ const moduleAssets = await read(tokenState.rewardModule, sdk.lifecycleRewardsAbi, "rewardAssets");
402
+ assert.deepEqual(moduleAssets.map((value) => value.toLowerCase()), expectedRewardAssets, "Actual reward-module assets");
403
+ }
404
+ artifacts.event("actual-reward-binding", { mode: scenario.rewardMode, module: tokenState.rewardModule, rewardAssets });
405
+ for (const policy of planned.plan.feeAssets) {
406
+ const actual = await read(hub, sdk.lifecycleFeeHubAbi, "policy", [policy.asset]);
407
+ artifacts.event("actual-fee-policy", { policy: actual });
408
+ addressEqual(actual.asset, policy.asset, "Actual fee asset");
409
+ for (const key of ["ownerBps", "rewardsBps", "burnBps"]) assert.equal(actual[key], policy[key], `Actual ${key}`);
410
+ }
411
+ const activated = receiptEvents(activation.receipt, fixture.orchestrator, sdk.launchLifecycleAbi, "LaunchActivated");
412
+ assert.equal(activated.length, 1); addressEqual(activated[0].launchId, planned.launchId, "Activation launch ID"); addressEqual(activated[0].planHash, planned.planHash, "Activation commitment"); addressEqual(activated[0].token, token, "Activation token");
413
+ assert.equal(activated[0].marketCount, scenario.markets.length); assert.equal(activated[0].positionCount, expectedPositions);
414
+ const buys = receiptEvents(activation.receipt, fixture.orchestrator, sdk.launchLifecycleAbi, "InitialBuyExecuted");
415
+ artifacts.result.chain.buys = buys;
416
+ artifacts.event("actual-ordered-buys", { buys });
417
+ assert.equal(buys.length, planned.plan.buys.length);
418
+ for (let index = 0; index < buys.length; index += 1) {
419
+ const actual = buys[index]; const expected = planned.plan.buys[index];
420
+ assert.equal(actual.buyIndex, index); assert.equal(actual.marketIndex, expected.marketIndex); addressEqual(actual.launchId, planned.launchId, "Buy launch ID");
421
+ addressEqual(actual.quoteAsset, fixture.quoteAsset, "Buy quote"); addressEqual(actual.recipient, expected.recipient, "Buy recipient");
422
+ assert.ok(actual.tokenOut >= expected.minTokenOut); assert.ok(actual.quoteSpent > 0n && actual.quoteSpent <= expected.quoteAmountIn);
423
+ }
424
+ const directory = await read(fixture.orchestrator, sdk.launchLifecycleAbi, "directory");
425
+ const canonicalMarkets = [];
426
+ artifacts.result.chain.markets = canonicalMarkets;
427
+ for (const market of progress.markets) {
428
+ const spec = scenario.markets[market.index];
429
+ const identity = market.prepared.identity;
430
+ const observation = { ...market, positions: [] };
431
+ canonicalMarkets.push(observation);
432
+ artifacts.event("actual-market", { market });
433
+ assert.equal(market.live?.publicTrading, true, market.error ?? "Actual public trading must be open");
434
+ assert.equal(identity.openingSqrtPriceX96, OPENING_PRICE); assert.equal(identity.fee, 3000); assert.equal(identity.tickSpacing, 60);
435
+ assert.equal(identity.venue, spec.venue === "v4" ? sdk.LifecycleVenue.UniswapV4 : sdk.LifecycleVenue.Abyss);
436
+ assert.equal(market.prepared.positionCount, spec.positions); assert.ok(!same(market.prepared.custody, zeroAddress));
437
+ if (spec.venue === "v4") {
438
+ const sealed = await read(market.prepared.custody, sdk.lifecycleV4LockerAbi, "isSealed", [identity.poolId]);
439
+ const count = await read(market.prepared.custody, sdk.lifecycleV4LockerAbi, "positionCount", [identity.poolId]);
440
+ observation.permanentCustody = { sealed, count };
441
+ assert.equal(sealed, true); assert.equal(count, BigInt(spec.positions));
442
+ }
443
+ const positions = await read(directory, sdk.lifecycleDirectoryAbi, "positions", [planned.launchId, market.index, 0n, BigInt(spec.positions)]);
444
+ assert.equal(positions.length, spec.positions);
445
+ const profile = planned.profiles.find((row) => same(row.id, identity.profileId));
446
+ assert.ok(profile, "Actual market must use the planned registered profile");
447
+ const config = spec.venue === "v4" ? sdk.decodePoolBoundV4LifecycleMarketConfig(planned.plan.markets[market.index].config) : sdk.decodeAbyssLifecycleMarketConfig(planned.plan.markets[market.index].config);
448
+ for (let index = 0; index < positions.length; index += 1) {
449
+ const position = positions[index]; const expected = config.positions[index];
450
+ const [liquidity, custody] = await read(profile.adapter.implementation, sdk.lifecycleAdapterAbi, "readPosition", [position]);
451
+ const actual = { ...position, actualLiquidity: liquidity, actualCustody: custody };
452
+ observation.positions.push(actual);
453
+ addressEqual(position.marketId, identity.canonicalId, "Position canonical market"); addressEqual(position.custody, market.prepared.custody, "Position committed custody");
454
+ assert.equal(position.tickLower, expected.tickLower); assert.equal(position.tickUpper, expected.tickUpper);
455
+ assert.equal(position.liquidity, expected.liquidity); assert.equal(liquidity, expected.liquidity); addressEqual(custody, market.prepared.custody, "Actual permanent position custody");
456
+ if (spec.venue === "v4") addressEqual(position.salt, expected.salt, "Committed V4 position salt");
457
+ else {
458
+ const lock = await read(custody, sdk.abyssPositionLockerAbi, "locks", [position.tokenId]);
459
+ actual.permanentLock = lock;
460
+ for (let term = 0; term < 3; term += 1) addressEqual(lock[term], market.prepared.feeSource, "Abyss collector-owned permanent lock");
461
+ assert.equal(lock[3], 0n); assert.equal(lock[4], false);
462
+ }
463
+ artifacts.event("actual-position-custody", { marketIndex: market.index, position: actual });
464
+ }
465
+ }
466
+ assert.equal(await sdk.buildNextTransaction({ client, planned, receipts: references }), undefined, "Active is terminal; no implicit launch retry");
467
+ artifacts.result.chain.status = "passed"; artifacts.result.execution = "completed";
468
+ artifacts.event("chain-verified", { token, activationTransactionHash: activation.transactionHash, marketCount: canonicalMarkets.length, positionCount: expectedPositions, buyCount: buys.length });
469
+ }
470
+
471
+ async function publishAndVerify({ fixture, planned, activation, session, sdk, artifacts, transport, signal, timeoutSeconds }) {
472
+ artifacts.stage("api-publication", { transactionHash: activation.transactionHash, timeoutSeconds });
473
+ artifacts.result.api.activationTransactionHash = activation.transactionHash;
474
+ const published = await publishUntilIndexed({
475
+ timeoutMs: timeoutSeconds * 1000, artifacts, signal,
476
+ isPending: (error) => error instanceof sdk.LaunchPublishPending,
477
+ publish: async (remainingMs) => {
478
+ transport.setTimeoutMs(remainingMs);
479
+ try { return await session.api.publishUploadSession(Number(planned.chainId), session.sessionId, session.capability, { chainId: Number(planned.chainId), transactionHash: activation.transactionHash }); }
480
+ catch (error) {
481
+ if (error instanceof sdk.LaunchPublishPending && error.session.token != null) addressEqual(error.session.token, planned.predictedToken, "Pending API token");
482
+ throw error;
483
+ }
484
+ },
485
+ });
486
+ artifacts.result.api.session = published;
487
+ artifacts.event("actual-published-session", { session: published });
488
+ addressEqual(published.token, planned.predictedToken, "Published actual token");
489
+ addressEqual(published.transactionHash, activation.transactionHash, "Published activation transaction");
490
+ assert.equal(published.chainId, Number(planned.chainId)); addressEqual(published.wallet, planned.plan.creator, "Published wallet");
491
+ assert.ok(["optimistic", "final"].includes(published.status)); assert.ok(["optimistic", "final"].includes(published.canonicalStatus));
492
+ assert.equal(published.metadata.name, planned.plan.token.name); assert.equal(published.metadata.symbol, planned.plan.token.symbol);
493
+ artifacts.stage("api-token-verification");
494
+ transport.setTimeoutMs(30_000);
495
+ const url = new URL(`/api/v1/launches/${planned.predictedToken}`, fixture.apiUrl);
496
+ url.searchParams.set("chainId", String(planned.chainId));
497
+ const response = await transport.fetch(url, { method: "GET" });
498
+ const representation = await response.json();
499
+ artifacts.result.api.representation = representation;
500
+ artifacts.save("published-token.json", representation);
501
+ artifacts.event("actual-api-token", { status: response.status, representation });
502
+ if (!response.ok) throw exampleError(representation.code ?? "API_TOKEN_HTTP_ERROR", representation.error ?? `Actual token GET failed: HTTP ${response.status}`, { status: response.status, data: representation });
503
+ assert.equal(representation.chainId, Number(planned.chainId));
504
+ const item = representation.item;
505
+ assert.ok(item && typeof item === "object", "Actual API launch detail must contain item");
506
+ assert.equal(item.chainId, Number(planned.chainId)); addressEqual(item.token, planned.predictedToken, "Indexed actual token");
507
+ addressEqual(item.transactionHash, activation.transactionHash, "Indexed activation provenance"); addressEqual(item.creator, planned.plan.creator, "Indexed creator");
508
+ assert.ok(["optimistic", "final"].includes(item.canonicalStatus), "Indexed canonical status");
509
+ assert.equal(item.metadata?.document?.name, planned.plan.token.name); assert.equal(item.metadata?.document?.symbol, planned.plan.token.symbol);
510
+ assert.equal(item.launchedToken?.name, planned.plan.token.name); assert.equal(item.launchedToken?.symbol, planned.plan.token.symbol);
511
+ artifacts.result.api.status = "passed";
512
+ }
513
+
514
+ async function executeExample({ fixture, scenario, sdk, artifacts, signal }) {
515
+ validateCase(scenario);
516
+ artifacts.result.network = "chain-4663";
517
+ artifacts.result.chain.chainId = fixture.chainId; artifacts.result.chain.rpcUrl = fixture.rpcUrl;
518
+ artifacts.result.chain.orchestrator = fixture.orchestrator; artifacts.result.chain.creator = fixture.creator;
519
+ artifacts.result.api = { status: "not-run", endpoint: fixture.apiUrl };
520
+ artifacts.save("run-config.json", { configuration: fixture, case: scenario });
521
+ const client = rpcClient(fixture.rpcUrl, artifacts, signal, "execution");
522
+ artifacts.stage("deployment-provenance");
523
+ const chainId = BigInt(await client.request({ method: "eth_chainId" }));
524
+ assert.equal(chainId, 4663n, "Examples require Robinhood chain 4663");
525
+ assert.equal(await client.request({ method: "eth_getCode", params: [fixture.creator, "latest"] }), "0x", "Creator must be an EOA");
526
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [fixture.orchestrator, "latest"] }), "0x", "Current orchestrator must be deployed");
527
+ const wallet = createLocalLaunchWallet({ privateKey: process.env.PRIVATE_KEY, creator: fixture.creator, chainId, orchestrator: fixture.orchestrator, client, redactor: artifacts.redactor });
528
+ artifacts.event("deployment-verified", { chainId, creator: fixture.creator, orchestrator: fixture.orchestrator });
529
+ const { plan, predictedToken } = await constructPlan({ client, fixture, scenario, sdk, artifacts, signal });
530
+ artifacts.stage("execution-planning");
531
+ const limits = limitSource(fixture, artifacts);
532
+ let fork;
533
+ if (fixture.forkRpcUrl !== undefined) {
534
+ const baseFork = sdk.createControlledLifecycleFork({ sourceRpcUrl: fixture.rpcUrl, forkRpcUrl: fixture.forkRpcUrl, allowTransactions: true, impersonation: "anvil", receiptTimeoutMs: 30_000 });
535
+ // Only the SDK's separate owned simulation fork is reset/impersonated/reverted.
536
+ fork = { ...baseFork, client: rpcClient(fixture.forkRpcUrl, artifacts, signal, "simulation") };
537
+ assert.match(await fork.client.request({ method: "web3_clientVersion" }), /anvil/i);
538
+ }
539
+ const planned = await sdk.planLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, fork, confirmations: 1 });
540
+ artifacts.save("planning.json", planned);
541
+ artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, atomicAttempt: planned.atomicAttempt, prerequisites: planned.prerequisites, transactions: planned.transactions });
542
+ assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
543
+ if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
544
+ const transport = apiTransport({ artifacts, signal, apiUrl: fixture.apiUrl });
545
+ const session = await stageMetadata({ fixture, plan, sdk, artifacts, transport, wallet });
546
+ const executed = await executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, fork, wallet });
547
+ await verifyChain({ client, planned, ...executed, fixture, scenario, sdk, artifacts });
548
+ await publishAndVerify({ fixture, planned, activation: executed.activation, session, sdk, artifacts, transport, signal, timeoutSeconds: 90 });
549
+ artifacts.stage("completed");
550
+ }
551
+
552
+ export async function runLaunchExample(caseObject) {
553
+ const redactor = new Redactor();
554
+ let artifacts;
555
+ const controller = new AbortController();
556
+ const interrupted = () => controller.abort(exampleError("INTERRUPTED", "Launch example interrupted; submitted hashes and chain state are retained"));
557
+ process.on("SIGINT", interrupted); process.on("SIGTERM", interrupted);
558
+ try {
559
+ const caseId = caseObject.id;
560
+ assert.match(caseId, /^[a-z0-9-]+$/, "Example case ID must be a safe directory label");
561
+ mkdirSync("launch-results", { recursive: true, mode: 0o700 });
562
+ artifacts = new RunArtifacts(join("launch-results", `${new Date().toISOString().replaceAll(":", "-")}-${caseId}-${randomUUID()}`), caseId, redactor);
563
+ artifacts.initialize();
564
+ loadExampleEnvironment();
565
+ redactor.addSecret(process.env.PRIVATE_KEY);
566
+ if (typeof process.env.PRIVATE_KEY === "string") redactor.addSecret(process.env.PRIVATE_KEY.slice(2));
567
+ // Import after .env loading: the public SDK resolves address overrides on import.
568
+ const sdk = await import("@black-market/sdk");
569
+ const fixture = readExampleConfiguration(sdk, redactor);
570
+ await executeExample({ fixture, scenario: caseObject, sdk, artifacts, signal: controller.signal });
571
+ artifacts.finish("passed");
572
+ console.log(`Actual chain launch and API publication/indexing passed\nArtifacts: ${artifacts.directory}\nToken: ${artifacts.result.chain.token}`);
573
+ return artifacts.result;
574
+ } catch (failure) {
575
+ const error = controller.signal.aborted ? controller.signal.reason : failure;
576
+ if (error !== failure && error instanceof Error) error.cause = failure;
577
+ if (artifacts) {
578
+ if (artifacts.result.stage.startsWith("api-")) artifacts.result.api.status = "failed";
579
+ else if (artifacts.result.execution === "running") artifacts.result.chain.status = "failed";
580
+ try { artifacts.finish("failed", error); }
581
+ catch (storageError) { console.error(JSON.stringify(redactor.value({ artifactWriteError: errorEvidence(storageError), originalError: errorEvidence(error) }))); }
582
+ }
583
+ console.error(JSON.stringify(redactor.value({ sdk: "node", caseId: artifacts?.result.caseId, status: "failed", stage: artifacts?.result.stage ?? "configuration", error: errorEvidence(error), artifacts: artifacts?.directory }), null, 2));
584
+ process.exitCode = 1;
585
+ return artifacts?.result;
586
+ } finally {
587
+ process.removeListener("SIGINT", interrupted); process.removeListener("SIGTERM", interrupted);
588
+ }
589
+ }