@black-market/sdk 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +58 -22
  2. package/dist/launch.d.ts +31 -5
  3. package/dist/launch.d.ts.map +1 -1
  4. package/dist/launch.js +71 -11
  5. package/dist/launch.js.map +1 -1
  6. package/dist/lifecycle/abi.d.ts +18 -0
  7. package/dist/lifecycle/abi.d.ts.map +1 -1
  8. package/dist/lifecycle/abi.js +4 -0
  9. package/dist/lifecycle/abi.js.map +1 -1
  10. package/dist/lifecycle/index.d.ts +2 -0
  11. package/dist/lifecycle/index.d.ts.map +1 -1
  12. package/dist/lifecycle/index.js +2 -0
  13. package/dist/lifecycle/index.js.map +1 -1
  14. package/dist/lifecycle/planner.d.ts +1 -1
  15. package/dist/lifecycle/planner.d.ts.map +1 -1
  16. package/dist/lifecycle/planner.js +58 -10
  17. package/dist/lifecycle/planner.js.map +1 -1
  18. package/dist/lifecycle/progress.js +35 -35
  19. package/dist/lifecycle/progress.js.map +1 -1
  20. package/dist/lifecycle/rpc.d.ts +10 -4
  21. package/dist/lifecycle/rpc.d.ts.map +1 -1
  22. package/dist/lifecycle/rpc.js +137 -16
  23. package/dist/lifecycle/rpc.js.map +1 -1
  24. package/dist/lifecycle/types.d.ts +20 -1
  25. package/dist/lifecycle/types.d.ts.map +1 -1
  26. package/dist/lifecycle/types.js.map +1 -1
  27. package/docs/launch-lifecycle.md +396 -64
  28. package/examples/.env.example +22 -2
  29. package/examples/README.md +315 -87
  30. package/examples/{launch-erc20-abyss.mjs → launch-erc20-abyss.ts} +1 -1
  31. package/examples/{launch-erc20-burn-mixed.mjs → launch-erc20-burn-mixed.ts} +1 -1
  32. package/examples/launch-erc20-depth-v4.ts +7 -0
  33. package/examples/{launch-erc20-dividends-abyss.mjs → launch-erc20-dividends-abyss.ts} +1 -1
  34. package/examples/launch-erc20-dividends-multiquote-mixed.ts +8 -0
  35. package/examples/launch-erc20-multi-quote.ts +8 -0
  36. package/examples/{launch-erc20-staking-v4.mjs → launch-erc20-staking-v4.ts} +1 -1
  37. package/examples/{launch-erc20-v4.mjs → launch-erc20-v4.ts} +1 -1
  38. package/examples/{launch-erc404-abyss.mjs → launch-erc404-abyss.ts} +1 -1
  39. package/examples/{launch-erc404-dividends-mixed.mjs → launch-erc404-dividends-mixed.ts} +1 -1
  40. package/examples/{launch-erc404-v4.mjs → launch-erc404-v4.ts} +1 -1
  41. package/examples/launch-example-kyber.ts +204 -0
  42. package/examples/launch-example-support.ts +876 -0
  43. package/examples/launch-example.ts +1163 -0
  44. package/examples/{launch-lifecycle-smoke.mjs → launch-lifecycle-smoke.ts} +1 -1
  45. package/examples/{launch.mjs → launch.ts} +1 -1
  46. package/package.json +2 -2
  47. package/src/launch.ts +81 -14
  48. package/src/lifecycle/abi.ts +4 -0
  49. package/src/lifecycle/index.ts +2 -0
  50. package/src/lifecycle/planner.ts +61 -12
  51. package/src/lifecycle/progress.ts +35 -35
  52. package/src/lifecycle/rpc.ts +140 -17
  53. package/src/lifecycle/types.ts +19 -1
  54. package/examples/launch-example-support.mjs +0 -393
  55. package/examples/launch-example.mjs +0 -633
  56. /package/examples/{generate-lifecycle-commitment-fixture.mjs → generate-lifecycle-commitment-fixture.ts} +0 -0
  57. /package/examples/{quickstart.mjs → quickstart.ts} +0 -0
@@ -0,0 +1,1163 @@
1
+ // Launch cases default to unsigned planning; --broadcast explicitly signs, broadcasts and publishes.
2
+ import assert from "node:assert/strict";
3
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
4
+ import { mkdirSync } from "node:fs";
5
+ import { join, relative } from "node:path";
6
+ import {
7
+ decodeEventLog, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, erc20Abi, 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, registerEndpointSecrets,
14
+ submitSignedRecorded, transactionEnvelope, readExampleQuoteUsdSeed, assertUnchangedEconomicQuote,
15
+ resolveExampleQuotes, buildExampleBuysAndFunding, assertFutureLaunchDeadline, resolveExampleIdentity, readExampleImage,
16
+ exampleImageUrl, readExamplePublishedImage,
17
+ allocateMarketBudgets, deriveExampleMarketEconomics, economicReviewHash, guardEconomicReviewWallet,
18
+ } from "./launch-example-support.ts";
19
+ import { buildExampleKyberConversions, assertExampleConversionAdmission, assertExampleConversionBindings } from "./launch-example-kyber.ts";
20
+ const UNIT = 10n ** 18n;
21
+ const BUY_SLIPPAGE_BPS = 50;
22
+ const same = (left, right) => typeof left === "string" && typeof right === "string" && left.toLowerCase() === right.toLowerCase();
23
+ const addressEqual = (actual, expected, label) => assert.ok(same(actual, expected), `${label}: expected ${expected}, got ${actual}`);
24
+ // Canonical AbyssFactory event; the SDK exposes its mapping getter, not enumeration.
25
+ export const exampleOracleRegistrationAbi = [{
26
+ type: "event", name: "OracleConfigRegistered", anonymous: false,
27
+ inputs: [{ name: "oracleConfigId", type: "bytes32", indexed: true },
28
+ { name: "maxAbsTickMove", type: "uint24", indexed: false }, { name: "cardinality", type: "uint16", indexed: false }],
29
+ }];
30
+
31
+ // Receipt accounting, not a creator reserve or an unchanged-total-supply assumption.
32
+ export function verifyLaunchInventory({ supply, totalSupply, orchestrator, creator, transfers, buys, orchestratorBalance, creatorBalance }) {
33
+ const minted = transfers.filter((transfer) => same(transfer.from, zeroAddress));
34
+ assert.equal(minted.reduce((sum, transfer) => sum + transfer.value, 0n), supply, "Exact minted supply");
35
+ assert.ok(minted.every((transfer) => same(transfer.to, orchestrator)), "Initial inventory belongs only to the launch core");
36
+ const burns = transfers.filter((transfer) => same(transfer.to, zeroAddress));
37
+ assert.ok(burns.every((transfer) => same(transfer.from, orchestrator)), "Launch residual burns belong only to the launch core");
38
+ const burned = burns.reduce((sum, transfer) => sum + transfer.value, 0n);
39
+ assert.ok(burned <= supply, "Residual inventory cannot exceed minted supply");
40
+ assert.equal(totalSupply, supply - burned, "Actual remaining supply accounts for residual burns");
41
+ assert.equal(orchestratorBalance, 0n, "No launch inventory remains in the core");
42
+ const bought = buys.filter((buy) => same(buy.recipient, creator)).reduce((sum, buy) => sum + buy.tokenOut, 0n);
43
+ assert.equal(creatorBalance, bought, "Creator receives only paid opening buys, never residual inventory");
44
+ return { minted: supply, burned, remainingSupply: totalSupply, creatorBought: bought };
45
+ }
46
+
47
+
48
+ export function validateCase(scenario) {
49
+ 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");
50
+ const allowed = new Set(["id", "description", "tokenKind", "rewardMode", "mode", "markets", "buysPerMarket", "burnBps"]);
51
+ 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`);
52
+ 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");
53
+ 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");
54
+ let positions = 0;
55
+ for (const market of scenario.markets) {
56
+ if (!["v4", "abyss"].includes(market.venue) || !Number.isSafeInteger(market.positions) || market.positions < 1 || market.positions > 16) throw exampleError("UNSUPPORTED_CASE", "Case requires a supported venue and 1..16 exact recipe positions per market");
57
+ for (const key of Object.keys(market)) if (!["venue", "positions", "feeMode", "quote", "distributionPreset"].includes(key)) throw exampleError("UNSUPPORTED_CASE", `Unsupported market economic field ${key}`);
58
+ if (market.feeMode !== undefined && (market.venue !== "v4" || ![0, 1].includes(market.feeMode))) throw exampleError("UNSUPPORTED_CASE", "Only V4 markets support explicit feeMode 0/1");
59
+ if (market.quote !== undefined && !["WETH", "USDG"].includes(market.quote)) throw exampleError("UNSUPPORTED_CASE", "Market quote must be WETH or USDG");
60
+ if (market.distributionPreset !== undefined && (market.positions < 2 || !["staircase", "early-scarcity", "smooth-ramp"].includes(market.distributionPreset))) throw exampleError("UNSUPPORTED_CASE", "Distribution preset requires supported multi-position source economics");
61
+ positions += market.positions;
62
+ }
63
+ if (positions > 32) throw exampleError("UNSUPPORTED_CASE", "Case exceeds 32 permanent positions");
64
+ }
65
+
66
+ function rpcClient(url, artifacts, signal, label) {
67
+ let id = 0;
68
+ const endpoint = new URL(url);
69
+ const headers = { "Content-Type": "application/json" };
70
+ if (endpoint.username || endpoint.password) {
71
+ const authorization = `Basic ${Buffer.from(`${decodeURIComponent(endpoint.username)}:${decodeURIComponent(endpoint.password)}`).toString("base64")}`;
72
+ artifacts.redactor.addSecret(authorization);
73
+ headers.Authorization = authorization;
74
+ endpoint.username = ""; endpoint.password = "";
75
+ }
76
+ // The official chain-4663 endpoint filters bare non-browser requests at the
77
+ // transport (403 without an Origin). Mirror the launch app's public origin so
78
+ // the example RPC client is admitted identically to the UI it validates.
79
+ if (endpoint.hostname === "rpc.mainnet.chain.robinhood.com") headers.Origin = "https://abyss.trading";
80
+ return { async request({ method, params = [] }) {
81
+ const requestSignal = rpcRequestSignal(signal, label, method);
82
+ const started = Date.now();
83
+ try {
84
+ const response = await fetch(endpoint, {
85
+ method: "POST", redirect: "error", headers,
86
+ body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
87
+ signal: requestSignal,
88
+ });
89
+ if (!response.ok) throw exampleError("RPC_HTTP_ERROR", `${label} ${method}: HTTP ${response.status}`, { status: response.status });
90
+ const body = await response.json();
91
+ if (body.error) throw exampleError(body.error.code ?? "RPC_ERROR", `${label} ${method}: ${body.error.message}`, { data: body.error.data, cause: body.error });
92
+ if (!("result" in body)) throw exampleError("INVALID_RPC_RESPONSE", `${label} ${method}: result was omitted`, { data: body });
93
+ if (method === "eth_simulateV1") artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
94
+ return body.result;
95
+ } catch (error) {
96
+ artifacts.event("rpc-error", { backend: label, method, params, elapsedMs: Date.now() - started, error: errorEvidence(error) });
97
+ throw error;
98
+ }
99
+ } };
100
+ }
101
+
102
+ function contractReader(client) {
103
+ return async (to, abi, functionName, args = [], tag = "latest") => decodeFunctionResult({
104
+ abi, functionName,
105
+ data: await client.request({ method: "eth_call", params: [{ to, data: encodeFunctionData({ abi, functionName, args }) }, tag] }),
106
+ });
107
+ }
108
+
109
+ function receiptEvents(receipt, address, abi, eventName) {
110
+ return receipt.logs.filter((log) => same(log.address, address)).flatMap((log) => {
111
+ let decoded;
112
+ try { decoded = decodeEventLog({ abi, data: log.data, topics: log.topics, strict: true }); }
113
+ catch { return []; } // A different event at the same contract is not the expected event.
114
+ return decoded.eventName === eventName ? [decoded.args] : [];
115
+ });
116
+ }
117
+
118
+ export async function discoverExampleQuoteOracles({ client, fixture, scenario, quotes, selected, sdk, block, artifacts }) {
119
+ const read = (name, args) => contractReader(client)(fixture.oracleFactory, sdk.abyssFactoryAbi, name, args, block.number);
120
+ let discovered;
121
+ if (quotes.some((quote) => quote.symbol === "USDG")) {
122
+ const topic = keccak256(toHex("OracleConfigRegistered(bytes32,uint24,uint16)"));
123
+ // The RPC bounds eth_getLogs to 10,000,000 blocks per request; page the full
124
+ // canonical factory history in compliant windows rather than query from 0.
125
+ const window = 9_999_999n;
126
+ const head = BigInt(block.number);
127
+ const ranges = [];
128
+ for (let from = 0n; from <= head; from += window + 1n) {
129
+ ranges.push([from, from + window > head ? head : from + window]);
130
+ }
131
+ // Independent windows query in parallel; registration events are append-only
132
+ // and immutable, so order is restored by sorting on block/logIndex after merge.
133
+ const pages = await Promise.all(ranges.map(([from, to]) =>
134
+ client.request({ method: "eth_getLogs", params: [{
135
+ address: fixture.oracleFactory, topics: [topic], fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}`,
136
+ }] }).then((page) => {
137
+ if (!Array.isArray(page)) throw exampleError("INVALID_RPC_RESPONSE", "Oracle factory registration logs must be an array");
138
+ return page;
139
+ })));
140
+ const logs = pages.flat().sort((a, b) =>
141
+ Number(BigInt(a.blockNumber) - BigInt(b.blockNumber)) ||
142
+ Number(BigInt(a.logIndex ?? "0x0") - BigInt(b.logIndex ?? "0x0")));
143
+ const registrations = new Map();
144
+ for (const log of logs) {
145
+ if (log.removed || !same(log.address, fixture.oracleFactory) || BigInt(log.blockNumber) > BigInt(block.number)) {
146
+ throw exampleError("INVALID_ORACLE_CONFIG", "Oracle registration must belong to the pinned canonical factory history");
147
+ }
148
+ const { args } = decodeEventLog({ abi: exampleOracleRegistrationAbi, topics: log.topics, data: log.data, strict: true });
149
+ const id = keccak256(encodeAbiParameters([{ type: "uint24" }, { type: "uint16" }], [args.maxAbsTickMove, args.cardinality]));
150
+ addressEqual(id, args.oracleConfigId, "Registered oracle config digest");
151
+ registrations.set(id.toLowerCase(), { oracleConfigId: id, maxAbsTickMove: Number(args.maxAbsTickMove), cardinality: Number(args.cardinality) });
152
+ }
153
+ discovered = [];
154
+ for (const registration of registrations.values()) {
155
+ const actual = await read("oracleConfigs", [registration.oracleConfigId]);
156
+ assert.equal(Number(actual[0]), registration.maxAbsTickMove, "Pinned registered oracle move");
157
+ assert.equal(Number(actual[1]), registration.cardinality, "Pinned registered oracle cardinality");
158
+ discovered.push(registration);
159
+ }
160
+ discovered.sort((left, right) => left.maxAbsTickMove - right.maxAbsTickMove || right.cardinality - left.cardinality ||
161
+ left.oracleConfigId.localeCompare(right.oracleConfigId));
162
+ }
163
+ for (const quote of quotes) {
164
+ let oracle;
165
+ if (quote.symbol === "WETH") {
166
+ const oracleConfigId = keccak256(encodeAbiParameters([{ type: "uint24" }, { type: "uint16" }], [1, 4096]));
167
+ const actual = await read("oracleConfigs", [oracleConfigId]);
168
+ assert.equal(Number(actual[0]), 1, "Canonical WETH P1 oracle must be registered");
169
+ assert.equal(Number(actual[1]), 4096, "Canonical WETH P1 oracle cardinality");
170
+ oracle = { oracleConfigId, maxAbsTickMove: 1, cardinality: 4096 };
171
+ } else {
172
+ const needsV4 = scenario.markets.some((market) => market.venue === "v4" && (market.quote ?? "WETH") === quote.symbol);
173
+ const maximumCardinality = needsV4 ? Math.min(4096, selected.v4.envelope.bounds.maximumOracleCardinality) : 4096;
174
+ oracle = discovered.find((row) => row.maxAbsTickMove > 0 && row.maxAbsTickMove <= 887272 && row.cardinality >= 2 && row.cardinality <= maximumCardinality);
175
+ if (!oracle) throw exampleError("QUOTE_ORACLE_NOT_REGISTERED", `No registered compatible oracle configuration for ${quote.symbol} at the pinned block`,
176
+ { quoteAsset: quote.tokenAddress, oracleFactory: fixture.oracleFactory, blockNumber: block.number, blockHash: block.hash });
177
+ }
178
+ quote.oracle = { ...oracle, factory: fixture.oracleFactory, observedBlockNumber: BigInt(block.number), observedBlockHash: block.hash };
179
+ artifacts.event("quote-oracle-discovered", { quote });
180
+ }
181
+ const canonical = await client.request({ method: "eth_getBlockByNumber", params: [block.number, false] });
182
+ addressEqual(canonical?.hash, block.hash, "Pinned quote/oracle block remains canonical");
183
+ }
184
+
185
+
186
+ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal, limits, metadata, image }) {
187
+ const read = contractReader(client);
188
+ artifacts.stage("profile-discovery");
189
+ const profiles = await sdk.readLifecycleProfiles({ client, orchestrator: fixture.orchestrator });
190
+ artifacts.save("profiles.json", profiles);
191
+ artifacts.event("registry-profiles", { profiles });
192
+ const selected = {};
193
+ if (scenario.markets.some((row) => row.venue === "v4")) {
194
+ const bound = profiles.filter((row) => row.venueKind === "uniswap-v4" && row.topology.hookTopology === 2 &&
195
+ row.topology.configVersion === 6 && same(row.registration.configSchema, sdk.V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6) &&
196
+ row.adapter.configVersion === 6 && row.envelope?.configVersion === 6);
197
+ if (bound.length !== 1 || !bound[0].admitted || !bound[0].envelope || !bound[0].developerTerms) throw exampleError("PROFILE_NOT_ADMITTED", "An unambiguous admitted pool-bound V4 config6/V2 profile with its exact schema and frozen terms is required", { data: bound });
198
+ selected.v4 = bound[0];
199
+ }
200
+ if (scenario.markets.some((row) => row.venue === "abyss")) {
201
+ const canonical = [];
202
+ const adapters = new Set(profiles.filter((row) => row.venueKind === "abyss").map((row) => row.adapter.implementation.toLowerCase()));
203
+ for (const adapter of adapters) {
204
+ const canonicalId = await read(adapter, sdk.abyssLifecycleAdapterAbi, "profileId", [3]);
205
+ const profile = profiles.find((row) => same(row.id, canonicalId) && same(row.adapter.implementation, adapter));
206
+ if (profile) canonical.push(profile);
207
+ }
208
+ 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 });
209
+ selected.abyss = canonical[0];
210
+ }
211
+ const block = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
212
+ const pinnedRead = (to, abi, name, args = []) => read(to, abi, name, args, block.number);
213
+ const escrow = await pinnedRead(fixture.orchestrator, sdk.launchLifecycleAbi, "fundingEscrow");
214
+ addressEqual(await pinnedRead(escrow, sdk.lifecycleFundingEscrowAbi, "core"), fixture.orchestrator, "Funding escrow core");
215
+ addressEqual(await pinnedRead(escrow, sdk.lifecycleFundingEscrowAbi, "wrappedNative"), fixture.quoteAsset, "Actual native-wrap funding binding");
216
+ const quotes = resolveExampleQuotes({ sdk, fixture, scenario });
217
+ for (const quote of quotes) assert.equal(await pinnedRead(quote.tokenAddress, sdk.erc20Abi, "decimals"), quote.tokenDecimals, `Actual ${quote.symbol} quote decimals`);
218
+ if (selected.v4) addressEqual(await pinnedRead(selected.v4.adapter.implementation, sdk.poolMarketAdapterV1Abi, "oracleFactory"), fixture.oracleFactory, "V4 registered oracle factory");
219
+ if (selected.abyss) addressEqual(await pinnedRead(selected.abyss.adapter.implementation, sdk.abyssLifecycleAdapterAbi, "factory"), fixture.oracleFactory, "Abyss registered factory");
220
+ await discoverExampleQuoteOracles({ client, fixture, scenario, quotes, selected, sdk, block, artifacts });
221
+ const identity = { nonce: BigInt(`0x${randomBytes(32).toString("hex")}`), tokenSalt: `0x${randomBytes(32).toString("hex")}` };
222
+ artifacts.save("identity.json", { ...identity, caseId: scenario.id, creator: fixture.creator });
223
+ const token = {
224
+ kind: scenario.tokenKind, rewardMode: scenario.rewardMode, name: metadata.name, symbol: metadata.symbol,
225
+ supply: (scenario.tokenKind === 1 ? 10_000n : 1_000_000n) * UNIT,
226
+ nftUnit: scenario.tokenKind === 1 ? 100n * UNIT : 0n,
227
+ metadataURI: scenario.tokenKind === 1 ? fixture.nftBaseUri : "", salt: identity.tokenSalt,
228
+ inventoryRecipient: fixture.creator, burnOnCancel: false,
229
+ };
230
+ const draft = {
231
+ chainId: BigInt(fixture.chainId), orchestrator: fixture.orchestrator, creator: fixture.creator, nonce: identity.nonce,
232
+ // One-hour plan deadline: a transport failure plus resume must never strand a
233
+ // begun launch inside a too-tight window.
234
+ token, funding: [], feeAssets: [], markets: [], buys: [], deadline: BigInt(Math.floor(Date.now() / 1000)) + 3600n, executorFeeBps: 275,
235
+ };
236
+ // CREATE2 token identity is independent of finalized market config/hook salts.
237
+ const predictedToken = await sdk.predictLifecycleToken({ client, plan: draft });
238
+ for (const quote of quotes) {
239
+ quote.usdSeed = await readExampleQuoteUsdSeed({ sdk, fixture, quote, signal });
240
+ artifacts.event("quote-usd-seed", { quote });
241
+ }
242
+ const budgets = allocateMarketBudgets(token.supply, scenario.markets.length);
243
+ const marketEconomics = [];
244
+ const markets = scenario.markets.map((spec, marketIndex) => {
245
+ const profile = selected[spec.venue];
246
+ const tokenBudget = budgets[marketIndex];
247
+ const quote = quotes.find((row) => row.symbol === (spec.quote ?? "WETH"));
248
+ const token0 = BigInt(predictedToken) < BigInt(quote.tokenAddress);
249
+ const economics = deriveExampleMarketEconomics({ sdk, supply: token.supply, tokenBudget, tokenIsCurrency0: token0,
250
+ positions: spec.positions, venue: spec.venue, quoteUsdX18: quote.usdSeed.priceUsdX18,
251
+ targetUsdX18: fixture.targetMarketCapUsdX18, quoteDecimals: quote.tokenDecimals, distributionPreset: spec.distributionPreset });
252
+ marketEconomics.push({ marketIndex, quoteSymbol: quote.symbol, quoteAsset: quote.tokenAddress,
253
+ quoteDecimals: quote.tokenDecimals, quoteUsdX18: quote.usdSeed.priceUsdX18, oracleConfigId: quote.oracle.oracleConfigId, ...economics });
254
+ const positions = economics.positions.map(({ tickLower, tickUpper, liquidity }) => ({ tickLower, tickUpper, liquidity }));
255
+ let config;
256
+ if (spec.venue === "v4") {
257
+ const envelope = profile.envelope;
258
+ const feeMode = spec.feeMode ?? 0;
259
+ 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 } });
260
+ config = sdk.encodePoolBoundV4LifecycleMarketConfig({
261
+ version: 6, lpFeePips: 3000, hookFeePips: 10000,
262
+ minimumHookFeePips: 1000, feeSensitivityPipsSecondsPerTick: 7654321,
263
+ tickSpacing: economics.tickSpacing, sqrtPriceX96: economics.sqrtPriceX96,
264
+ feeMode, protocolFeeDenominator: envelope.protocolFeeDenominator, treasury: envelope.protocolTreasury,
265
+ externalLiquidityDisabled: true, oracleConfigId: quote.oracle.oracleConfigId, profileId: profile.id,
266
+ termsDigest: envelope.termsDigest, developerBeneficiary: envelope.beneficiary, developerFeeBps: 0, hookSalt: zeroHash,
267
+ positions: positions.map((position, index) => ({ ...position, salt: toHex(BigInt(marketIndex * 32 + index + 1), { size: 32 }), maxTokenAmount: economics.positions[index].maxTokenAmount })),
268
+ });
269
+ } else config = sdk.encodeAbyssLifecycleMarketConfig({
270
+ profile: 3, fee: 3000, oracleConfigId: quote.oracle.oracleConfigId, openingSqrtPriceX96: economics.sqrtPriceX96,
271
+ positions: positions.map((position, index) => ({ ...position, tokenAmountMaximum: economics.positions[index].maxTokenAmount })),
272
+ });
273
+ return { adapterId: profile.registration.adapterId, profileId: profile.id, quoteAsset: quote.tokenAddress, tokenBudget, configVersion: spec.venue === "v4" ? profile.topology.configVersion : 1, config };
274
+ });
275
+ // The Kyber conversion carries its own near-term swap expiry (matching the app's
276
+ // launch-purpose window), decoupled from the plan's longer execution deadline.
277
+ const conversionDeadline = BigInt(Math.floor(Date.now() / 1000)) + 300n;
278
+ await buildExampleKyberConversions({ client, fixture, scenario, markets, quotes, escrow, deadline: conversionDeadline, sdk, block, artifacts, signal });
279
+ const { buys, funding } = buildExampleBuysAndFunding({ sdk, fixture, scenario, markets, quotes, predictedToken });
280
+ const feeAssets = [predictedToken, ...quotes.map((quote) => quote.tokenAddress)].sort((left, right) => BigInt(left) < BigInt(right) ? -1 : 1).map((asset) => {
281
+ const rewardsBps = scenario.rewardMode === 0 ? 0 : 4000;
282
+ const burnBps = same(asset, predictedToken) ? scenario.burnBps ?? 0 : 0;
283
+ return { asset, ownerBps: 10000 - rewardsBps - burnBps, rewardsBps, burnBps };
284
+ });
285
+ const plan = { ...draft, markets, buys, feeAssets, funding };
286
+ assertExampleConversionBindings({ plan, quotes });
287
+ artifacts.save("draft-plan.json", plan);
288
+ artifacts.result.chain.predictedToken = predictedToken;
289
+ artifacts.stage("preparation-and-planning", { predictedToken, selectedProfiles: Object.fromEntries(Object.entries(selected).map(([key, profile]) => [key, profile.id])) });
290
+ const confirmations = artifacts.result.chain.confirmations;
291
+ const planned = await sdk.prepareAndPlanLifecycleLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, confirmations, buySlippageBps: BUY_SLIPPAGE_BPS, signal,
292
+ onProgress: (progress) => artifacts.event("salt-mining", progress),
293
+ });
294
+ const finalizedPlan = JSON.parse(sdk.serializeLaunchPlan(planned.plan));
295
+ const roundingBurn = marketEconomics.reduce((sum, market) => sum + market.roundingBurn, 0n);
296
+ const retainedSupply = token.supply - roundingBurn;
297
+ for (const market of marketEconomics) {
298
+ const ratio = market.rawQuotePerRawToken;
299
+ market.retainedSupplyOpeningValuationUsdX18 = ratio.numerator * retainedSupply * market.quoteUsdX18 /
300
+ (ratio.denominator * 10n ** BigInt(market.quoteDecimals));
301
+ }
302
+ const activationTransaction = planned.transactions.find((transaction) => ["atomic", "activate"].includes(transaction.kind));
303
+ const activationStep = planned.simulation.steps.find((step) => step.transactionId === activationTransaction?.id);
304
+ if (!activationStep?.success || !activationStep.returnData) throw exampleError("MISSING_ECONOMIC_OUTPUTS", "Economic review requires the admitted activation receipt and every ordered buy output");
305
+ const simulatedReceipt = decodeFunctionResult({ abi: sdk.launchLifecycleAbi,
306
+ functionName: activationTransaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", data: activationStep.returnData });
307
+ assert.equal(simulatedReceipt.planHash.toLowerCase(), planned.planHash.toLowerCase(), "Economic receipt exact plan");
308
+ assert.equal(simulatedReceipt.tokenOut.length, planned.plan.buys.length, "Economic receipt ordered outputs");
309
+ const simulatedBuys = planned.plan.buys.map((buy, index) => {
310
+ const tokenOut = simulatedReceipt.tokenOut[index];
311
+ const quoteSpent = simulatedReceipt.quoteSpent[index];
312
+ assert.ok(tokenOut >= buy.minTokenOut && tokenOut > 0n && quoteSpent <= buy.quoteAmountIn, "Exact protected simulated buy");
313
+ const market = marketEconomics[buy.marketIndex];
314
+ const ratio = market.rawQuotePerRawToken;
315
+ return { buyIndex: index, marketIndex: buy.marketIndex, quoteAsset: market.quoteAsset, quoteSymbol: market.quoteSymbol, quoteSpent, tokenOut,
316
+ allInUnitPriceUsdX18: quoteSpent * UNIT * market.quoteUsdX18 / (tokenOut * 10n ** BigInt(market.quoteDecimals)),
317
+ feeAndPriceImpactPremiumBps: (quoteSpent * ratio.denominator - tokenOut * ratio.numerator) * 10000n /
318
+ (tokenOut * ratio.numerator),
319
+ // Effective execution price includes fees and path impact, not post-buy marginal spot.
320
+ priceBasis: "admitted-simulation-all-in-average-including-fees-and-price-impact" };
321
+ });
322
+ const economicReview = {
323
+ schema: "black-market.launch-economic-review.v1", caseId: scenario.id, chainId: fixture.chainId,
324
+ targetMarketCapUsdX18: fixture.targetMarketCapUsdX18, quotes, metadata, image: image?.descriptor ?? null,
325
+ actualMintedSupply: token.supply, nftUnit: token.nftUnit, tokenDecimals: 18,
326
+ predictedToken, markets: marketEconomics,
327
+ expectedUnusedBurn: roundingBurn, expectedSupplyAfterMintRounding: token.supply - roundingBurn,
328
+ fees: { lpFeePips: 3000, v4HookMaximumPips: 10000, v4HookMinimumPips: 1000, v4HookSensitivityPipsSecondsPerTick: 7654321,
329
+ executorFeeBps: plan.executorFeeBps, feeAssets, v4Markets: scenario.markets.flatMap((spec, marketIndex) => spec.venue === "v4" ?
330
+ [{ marketIndex, feeMode: spec.feeMode ?? 0, protocolFeeDenominator: selected.v4.envelope.protocolFeeDenominator,
331
+ treasury: selected.v4.envelope.protocolTreasury, developerBeneficiary: selected.v4.envelope.beneficiary, developerFeeBps: 0 }] : []) },
332
+ initialBuy: { slippageBps: BUY_SLIPPAGE_BPS, funding: planned.plan.funding, orderedBuys: planned.plan.buys, simulatedBuys,
333
+ observedBlockNumber: planned.simulation.blockNumber, observedBlockHash: planned.simulation.blockHash },
334
+ planHash: planned.planHash, finalizedPlan: finalizedPlan,
335
+ };
336
+ const reviewHash = economicReviewHash(economicReview);
337
+ artifacts.save("economic-review.json", { ...economicReview, reviewHash });
338
+ // Complete exact intent is owner-only and durable before attribution or chain signing.
339
+ artifacts.privateRecovery({
340
+ schema: "black-market.launch-example-recovery.v1", sdk: "node", caseId: artifacts.result.caseId,
341
+ plan: finalizedPlan, mode: scenario.mode, confirmations, executionLimits: fixture.executionLimits, economicReview: { ...economicReview, reviewHash },
342
+ });
343
+ artifacts.save("plan.json", finalizedPlan);
344
+ artifacts.save("hook-deployments.json", planned.hookDeployments);
345
+ addressEqual(planned.predictedToken, predictedToken, "Finalized token prediction");
346
+ artifacts.result.chain.planHash = planned.planHash;
347
+ artifacts.result.chain.launchId = planned.launchId;
348
+ artifacts.event("plan-finalized", { predictedToken, planHash: planned.planHash, launchId: planned.launchId, mode: planned.mode, markets: markets.length, positions: scenario.markets.reduce((sum, market) => sum + market.positions, 0), buys: buys.length, deployments: planned.hookDeployments });
349
+ return { planned, predictedToken, economicReview, reviewHash };
350
+ }
351
+
352
+ export function apiTransport({ artifacts, signal, apiUrl, fetchImpl = fetch }) {
353
+ let requestTimeoutMs = 30_000;
354
+ let imageUpload;
355
+ return {
356
+ setTimeoutMs(ms) { requestTimeoutMs = Math.max(1, Math.min(30_000, ms)); },
357
+ authorizeImageUpload(upload, descriptor) {
358
+ assert.equal(upload.method, "PUT", "Only the exact issued image PUT is authorized");
359
+ registerEndpointSecrets(artifacts.redactor, upload.url);
360
+ imageUpload = { url: exampleImageUrl(upload.url), headers: { ...upload.headers }, descriptor };
361
+ },
362
+ fetchPublishedImage(url, descriptor) {
363
+ return readExamplePublishedImage({ url, descriptor, artifacts, signal, timeoutMs: requestTimeoutMs, fetchImpl });
364
+ },
365
+ async fetch(url, init = {}) {
366
+ const endpoint = new URL(url);
367
+ const isImageUpload = imageUpload && init.method === "PUT" && endpoint.href === imageUpload.url;
368
+ if (isImageUpload) {
369
+ assert.deepEqual(init.headers, imageUpload.headers, "Exact API-issued image-upload headers");
370
+ assert.ok(init.body instanceof Blob && init.body.type === imageUpload.descriptor.contentType &&
371
+ init.body.size === imageUpload.descriptor.contentLength, "Exact frozen image-upload body");
372
+ assert.equal(`0x${createHash("sha256").update(Buffer.from(await init.body.arrayBuffer())).digest("hex")}`,
373
+ imageUpload.descriptor.sha256, "Exact signed image-upload digest");
374
+ } else if (endpoint.origin !== new URL(apiUrl).origin) throw exampleError("UNSAFE_ENDPOINT", "API request changed the configured origin");
375
+ launchApiUrl(endpoint.origin);
376
+ const started = Date.now();
377
+ artifacts.event("api-request", { method: init.method ?? "GET", url: String(url), headers: init.headers,
378
+ body: isImageUpload ? { image: imageUpload.descriptor } : init.body ? JSON.parse(init.body) : undefined });
379
+ try {
380
+ const response = await fetchImpl(url, { ...init, redirect: "error", signal: AbortSignal.any([signal, AbortSignal.timeout(requestTimeoutMs)].filter(Boolean)) });
381
+ const text = await response.clone().text();
382
+ let body; try { body = JSON.parse(text); } catch { body = text; }
383
+ if (body?.capability) {
384
+ artifacts.redactor.addSecret(body.capability);
385
+ artifacts.privateRecovery({ ...artifacts.recovery, session: body, capability: body.capability });
386
+ }
387
+ 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 });
388
+ return response;
389
+ } catch (error) {
390
+ artifacts.event("api-error", { url: String(url), elapsedMs: Date.now() - started, error: errorEvidence(error) });
391
+ throw error;
392
+ }
393
+ },
394
+ };
395
+ }
396
+
397
+ export async function stageMetadata({ fixture, plan, sdk, artifacts, transport, wallet, metadata, image }) {
398
+ artifacts.stage("api-metadata");
399
+ artifacts.result.api.status = "staging";
400
+ delete artifacts.result.api.reason;
401
+ assertFutureLaunchDeadline(plan.deadline);
402
+ assert.equal(metadata.name, plan.token.name); assert.equal(metadata.symbol, plan.token.symbol);
403
+ const idempotencyKey = `node-example-${randomUUID()}`;
404
+ const nonce = `0x${randomBytes(32).toString("hex")}`;
405
+ const deadline = BigInt(Math.floor(Date.now() / 1000)) + 600n;
406
+ const typedData = {
407
+ domain: { name: "Abyss Launch Attribution", version: "1", chainId: Number(plan.chainId), verifyingContract: plan.orchestrator },
408
+ types: { EIP712Domain: [{ name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }], ...sdk.launchAttributionTypes },
409
+ primaryType: "LaunchAttribution",
410
+ message: { chainId: plan.chainId.toString(), wallet: plan.creator, metadataHash: sdk.canonicalLaunchMetadataHash(metadata),
411
+ imageSha256: image?.descriptor.sha256 ?? zeroHash, imageContentType: image?.descriptor.contentType ?? "",
412
+ imageContentLength: String(image?.descriptor.contentLength ?? 0), idempotencyKey, nonce, deadline: deadline.toString() },
413
+ };
414
+ const signature = await wallet.signTypedData(typedData);
415
+ artifacts.redactor.addSecret(signature);
416
+ const request = { chainId: Number(plan.chainId), wallet: plan.creator, metadata,
417
+ ...(image ? { image: image.descriptor } : {}), authorization: { nonce, deadline: deadline.toString(), signature } };
418
+ const recovery = { ...artifacts.recovery, schema: "black-market.launch-example-recovery.v1", sdk: "node", caseId: artifacts.result.caseId, apiUrl: fixture.apiUrl, idempotencyKey, typedData, request };
419
+ artifacts.privateRecovery(recovery);
420
+ const signer = await recoverTypedDataAddress({ ...typedData, signature });
421
+ addressEqual(signer, plan.creator, "Attribution signer");
422
+ assertFutureLaunchDeadline(plan.deadline);
423
+ const api = new sdk.LaunchApiClient({ baseUrl: fixture.apiUrl, retries: 1, fetch: transport.fetch.bind(transport) });
424
+ let session = await api.createUploadSession(request, idempotencyKey);
425
+ if (session.capability) artifacts.redactor.addSecret(session.capability);
426
+ // Persist the one-time capability before even validating the response.
427
+ artifacts.privateRecovery({ ...recovery, session, capability: session.capability });
428
+ artifacts.result.api.session = session;
429
+ artifacts.result.api.sessionId = session.sessionId;
430
+ artifacts.event("metadata-session-created", { session });
431
+ assert.ok(typeof session.capability === "string" && session.capability.length > 0, "Actual API must issue the one-time session capability");
432
+ assert.equal(session.chainId, Number(plan.chainId)); addressEqual(session.wallet, plan.creator, "Session wallet");
433
+ assert.equal(session.metadata.name, plan.token.name); assert.equal(session.metadata.symbol, plan.token.symbol);
434
+ assert.equal(session.metadata.description, metadata.description || null); assert.ok(session.transactionHash == null, "New metadata session cannot already bind a transaction");
435
+ const sessionId = session.sessionId;
436
+ const capability = session.capability;
437
+ if (image) {
438
+ assert.equal(session.status, "awaiting_upload");
439
+ for (const field of ["sha256", "contentType", "contentLength"]) assert.equal(session.image?.[field], image.descriptor[field], `Signed image ${field}`);
440
+ assert.ok(session.upload, "The image session must issue a direct upload URL");
441
+ assertFutureLaunchDeadline(plan.deadline);
442
+ transport.authorizeImageUpload(session.upload, image.descriptor);
443
+ await api.putImage(session.upload, new Blob([image.bytes], { type: image.descriptor.contentType }));
444
+ assertFutureLaunchDeadline(plan.deadline);
445
+ session = await api.completeUpload(Number(plan.chainId), sessionId, capability);
446
+ artifacts.privateRecovery({ session, capability });
447
+ artifacts.result.api.session = session;
448
+ artifacts.event("image-upload-completed", { session });
449
+ assert.equal(session.sessionId, sessionId); assert.equal(session.chainId, Number(plan.chainId));
450
+ addressEqual(session.wallet, plan.creator, "Completed image session wallet");
451
+ assert.equal(session.metadata.name, metadata.name); assert.equal(session.metadata.symbol, metadata.symbol);
452
+ assert.equal(session.metadata.description, metadata.description || null);
453
+ assert.ok(session.transactionHash == null, "Image completion cannot bind an activation");
454
+ for (const field of ["sha256", "contentType", "contentLength"]) assert.equal(session.image?.[field], image.descriptor[field], `Verified image ${field}`);
455
+ assert.equal(session.imageStatus, "ready");
456
+ for (const field of ["width", "height"]) assert.ok(Number.isSafeInteger(session.image[field]) && session.image[field] > 0 && session.image[field] <= 4096, `Verified image ${field}`);
457
+ } else assert.equal(session.image, null, "Metadata-only session retains no image");
458
+ assert.equal(session.status, "ready_to_launch");
459
+ assertFutureLaunchDeadline(plan.deadline);
460
+ artifacts.result.api.status = "session-ready";
461
+ return { api, sessionId, capability, metadata, image: image?.descriptor };
462
+ }
463
+
464
+ function limitSource(fixture, artifacts) {
465
+ const policy = fixture.executionLimits;
466
+ return async ({ block, chainId, account, orchestrator }) => {
467
+ const limits = {
468
+ chainId, account, orchestrator, observedBlockNumber: block.number, observedBlockHash: block.hash,
469
+ chainGasLimit: policy.chainTransactionGasLimit === undefined ? undefined : BigInt(policy.chainTransactionGasLimit),
470
+ rpcGasLimit: policy.rpcTransactionGasLimit === undefined ? undefined : BigInt(policy.rpcTransactionGasLimit),
471
+ accountGasLimit: policy.accountTransactionGasLimit === undefined ? undefined : BigInt(policy.accountTransactionGasLimit),
472
+ maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
473
+ };
474
+ artifacts.event("execution-limits", { limits, provenance: policy.provenance });
475
+ return limits;
476
+ };
477
+ }
478
+
479
+ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, wallet, references: retainedReferences = [] }) {
480
+ const references = [...retainedReferences];
481
+ let activation;
482
+ for (let step = 0; step < 64; step += 1) {
483
+ artifacts.stage("build-next", { step });
484
+ const reviewedTransaction = planned.transactions[step];
485
+ const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client, reviewedTransaction, signal });
486
+ if (!next) return { references, activation };
487
+ assert.ok(reviewedTransaction, "No transaction outside the saved review may be submitted");
488
+ for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
489
+ if (retainedReferences.length) {
490
+ const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
491
+ assertRetainedExampleReceipts({ progress, references, artifacts });
492
+ }
493
+ artifacts.event("next-admitted", { transaction: next });
494
+ assert.equal(next.chainId, Number(planned.chainId));
495
+ assert.equal(BigInt(await client.request({ method: "eth_chainId" })), planned.chainId, "Recheck execution chain before every send");
496
+ addressEqual(next.from, planned.plan.creator, "Exact transaction creator");
497
+ addressEqual(next.to, planned.plan.orchestrator, "Native-input conversion launch transaction destination");
498
+ const functions = { atomic: "launchAtomic", begin: "beginLaunch", prepare: "prepareMarkets", activate: "activateLaunch" };
499
+ const functionName = functions[next.kind];
500
+ assert.ok(functionName, "Native-input conversion cases do not permit unrelated approval/cancellation writes");
501
+ const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
502
+ assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
503
+ 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");
504
+ assert.equal(next.admission.executionProof, "proved"); assert.equal(next.admission.protocolFit, "proved");
505
+ assert.equal(next.admission.transportPreflight, "passed", "Exact immediate-next submission RPC preflight is required before signing");
506
+ const ceiling = next.admission.limits.transactionGasCeiling;
507
+ assert.ok(next.gas > 0n && (ceiling === undefined || next.gas <= ceiling), "Complete gas envelope must fit supplied restrictions; Nitro compute is separately proved");
508
+ const calldataCap = next.admission.limits.maxCalldataBytes;
509
+ assert.ok(calldataCap === undefined || (next.data.length - 2) / 2 <= calldataCap, "Exact calldata must fit any supplied cap");
510
+ const envelope = transactionEnvelope(next);
511
+ const wire = { ...envelope };
512
+ for (const key of ["chainId", "value", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce"]) if (wire[key] !== undefined) wire[key] = toHex(BigInt(wire[key]));
513
+ artifacts.stage(`submit-${next.kind}`, { id: next.id });
514
+ artifacts.result.execution = "running"; artifacts.result.chain.status = "executing";
515
+ const submitted = await submitSignedRecorded({
516
+ transaction: { ...next, wire }, artifacts, signal, wallet, client,
517
+ wait: async (hash) => {
518
+ const deadline = Date.now() + 30_000;
519
+ while (Date.now() < deadline) {
520
+ const receipt = await client.request({ method: "eth_getTransactionReceipt", params: [hash] });
521
+ if (receipt) return receipt;
522
+ await pause(100, signal);
523
+ }
524
+ throw exampleError("RECEIPT_TIMEOUT", `Submitted transaction was not included within 30 seconds: ${hash}`, { transactionHash: hash });
525
+ },
526
+ });
527
+ const { row, receipt } = submitted;
528
+ addressEqual(receipt.transactionHash, row.transactionHash, "Receipt transaction hash");
529
+ addressEqual(receipt.from, next.from, "Receipt sender");
530
+ addressEqual(receipt.to, next.to, "Receipt destination");
531
+ if (BigInt(receipt.status) !== 1n) {
532
+ let trace;
533
+ try { trace = await client.request({ method: "debug_traceTransaction", params: [row.transactionHash, { tracer: "callTracer" }] }); }
534
+ catch (error) { artifacts.event("revert-trace-unavailable", { transactionHash: row.transactionHash, error: errorEvidence(error) }); }
535
+ if (trace) artifacts.event("transaction-revert-trace", { transactionHash: row.transactionHash, trace });
536
+ row.status = "reverted"; artifacts.saveReceipts();
537
+ throw exampleError("TRANSACTION_REVERTED", `Actual source-chain transaction reverted: ${row.transactionHash}`, { transactionHash: row.transactionHash, revertData: findRevertData(trace), receipt });
538
+ }
539
+ const includedBlock = await client.request({ method: "eth_getBlockByNumber", params: [receipt.blockNumber, false] });
540
+ addressEqual(includedBlock?.hash, receipt.blockHash, "Canonical included block");
541
+ const latest = BigInt(await client.request({ method: "eth_blockNumber" }));
542
+ assert.ok(latest >= BigInt(receipt.blockNumber) + BigInt(planned.confirmations) - 1n, "Required confirmations must be included canonically");
543
+ const reference = { transactionHash: row.transactionHash, observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: planned.confirmations };
544
+ row.status = "confirmed"; row.reference = reference; artifacts.saveReceipts();
545
+ references.push(reference);
546
+ if (["atomic", "activate"].includes(next.kind)) {
547
+ assert.equal(activation, undefined, "Exactly one activation transaction is allowed");
548
+ activation = { transactionHash: row.transactionHash, receipt };
549
+ artifacts.result.chain.activationTransactionHash = row.transactionHash;
550
+ }
551
+ artifacts.privateRecovery({ receipts: references, activation });
552
+ artifacts.event("transaction-confirmed", { reference, gasUsed: receipt.gasUsed, transactionKind: next.kind });
553
+ }
554
+ throw exampleError("EXECUTION_STEP_LIMIT", "Lifecycle did not terminate within 64 exact SDK transactions; state/hashes are retained");
555
+ }
556
+
557
+ async function verifyChain({ client, planned, references, activation, fixture, scenario, sdk, artifacts }) {
558
+ artifacts.stage("chain-verification");
559
+ assert.ok(activation, "A real atomic/activate receipt is required");
560
+ const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations });
561
+ artifacts.result.chain.progress = progress;
562
+ artifacts.save("progress.json", progress);
563
+ artifacts.event("canonical-progress", { progress });
564
+ const expectedPositions = scenario.markets.reduce((sum, market) => sum + market.positions, 0);
565
+ assert.equal(progress.canonical.phase, sdk.LifecyclePhase.Active);
566
+ addressEqual(progress.canonical.token, planned.predictedToken, "Actual canonical token");
567
+ assert.equal(progress.canonical.marketCount, scenario.markets.length);
568
+ assert.equal(progress.canonical.preparedMarkets, scenario.markets.length);
569
+ assert.equal(progress.canonical.positionCount, expectedPositions);
570
+ assert.equal(progress.canonical.buyCount, planned.plan.buys.length);
571
+ assert.equal(progress.markets.length, scenario.markets.length);
572
+ assert.ok(progress.confirmationSafe); assert.ok(progress.receipts.every((row) => row.status === "confirmed"));
573
+ const tag = toHex(progress.confirmedBlockNumber);
574
+ const read = (to, abi, name, args = []) => contractReader(client)(to, abi, name, args, tag);
575
+ const token = planned.predictedToken;
576
+ const runtime = await client.request({ method: "eth_getCode", params: [token, tag] });
577
+ assert.ok(runtime !== "0x", "Actual deployed token runtime is required");
578
+ const factoryRuntime = await client.request({ method: "eth_getCode", params: [planned.tokenFactory, tag] });
579
+ assert.equal(keccak256(factoryRuntime).toLowerCase(), planned.tokenFactoryCodeHash.toLowerCase(), "Current immutable token-factory runtime binding");
580
+ const deployments = artifacts.transactions.flatMap((row) => receiptEvents(row.receipt, planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "TokenDeployed"));
581
+ assert.equal(deployments.length, 1, "Actual token-factory deployment event");
582
+ addressEqual(deployments[0].token, token, "Factory deployed token"); addressEqual(deployments[0].launchId, planned.launchId, "Factory launch ID");
583
+ assert.equal(deployments[0].kind, scenario.tokenKind, "Actual factory token kind");
584
+ addressEqual(await read(planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "tokenOfLaunch", [planned.launchId]), token, "Factory token binding");
585
+ addressEqual(await read(planned.tokenFactory, sdk.lifecycleTokenFactoryAbi, "launchOfToken", [token]), planned.launchId, "Factory reverse launch binding");
586
+ const tokenState = {};
587
+ for (const name of ["authority", "tokenFactory", "launchId", "rewardMode", "initialSupply", "burnOnCancel", "active", "cancelled", "exclusionsFinalized", "feeHub", "rewardModule"]) tokenState[name] = await read(token, sdk.lifecycleTokenContextAbi, name);
588
+ for (const name of ["name", "symbol", "decimals", "totalSupply"]) tokenState[name] = await read(token, sdk.erc20Abi, name);
589
+ tokenState.runtimeCodeHash = keccak256(runtime); tokenState.kind = deployments[0].kind;
590
+ artifacts.result.chain.token = token;
591
+ artifacts.result.chain.tokenState = tokenState;
592
+ artifacts.event("actual-token", { token, tokenState });
593
+ assert.equal(tokenState.name, planned.plan.token.name); assert.equal(tokenState.symbol, planned.plan.token.symbol); assert.equal(tokenState.decimals, 18);
594
+ assert.equal(tokenState.initialSupply, planned.plan.token.supply);
595
+ assert.equal(tokenState.rewardMode, scenario.rewardMode); assert.equal(tokenState.burnOnCancel, false);
596
+ assert.equal(tokenState.active, true); assert.equal(tokenState.cancelled, false); assert.equal(tokenState.exclusionsFinalized, true);
597
+ addressEqual(tokenState.authority, fixture.orchestrator, "Token authority"); addressEqual(tokenState.tokenFactory, planned.tokenFactory, "Token factory"); addressEqual(tokenState.launchId, planned.launchId, "Token launch ID");
598
+ addressEqual(tokenState.feeHub, progress.canonical.feeHub, "Token fee hub"); addressEqual(tokenState.rewardModule, progress.canonical.rewards, "Token rewards binding");
599
+ if (scenario.rewardMode === 0) addressEqual(tokenState.rewardModule, zeroAddress, "Reward-free module");
600
+ else if (scenario.rewardMode === 2) addressEqual(tokenState.rewardModule, token, "Embedded holder dividends");
601
+ else {
602
+ assert.ok(!same(tokenState.rewardModule, token) && !same(tokenState.rewardModule, zeroAddress), "Actual distinct staking module");
603
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [tokenState.rewardModule, tag] }), "0x", "Staking runtime");
604
+ }
605
+ if (scenario.tokenKind === 1) {
606
+ tokenState.nftUnit = await read(token, sdk.lifecycleErc404Abi, "unit");
607
+ tokenState.metadataURI = await read(token, sdk.lifecycleErc404Abi, "baseURI");
608
+ tokenState.mirror = await read(token, sdk.lifecycleErc404Abi, "mirrorERC721");
609
+ artifacts.event("actual-erc404", { nftUnit: tokenState.nftUnit, metadataURI: tokenState.metadataURI, mirror: tokenState.mirror });
610
+ assert.equal(tokenState.nftUnit, planned.plan.token.nftUnit); assert.equal(tokenState.metadataURI, planned.plan.token.metadataURI);
611
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [tokenState.mirror, tag] }), "0x", "Real ERC404 mirror runtime");
612
+ addressEqual(await read(tokenState.mirror, sdk.lifecycleErc404MirrorAbi, "baseERC20"), token, "ERC404 mirror link");
613
+ }
614
+ const hub = tokenState.feeHub;
615
+ addressEqual(await read(hub, sdk.lifecycleFeeHubAbi, "launchToken"), token, "Hub token");
616
+ addressEqual(await read(hub, sdk.lifecycleFeeHubAbi, "rewards"), tokenState.rewardModule, "Hub reward module");
617
+ assert.equal(await read(hub, sdk.lifecycleFeeHubAbi, "executorFeeBps"), 275);
618
+ assert.equal(await read(hub, sdk.lifecycleFeeHubAbi, "finalized"), true);
619
+ const assets = await read(hub, sdk.lifecycleFeeHubAbi, "assets");
620
+ assert.deepEqual(assets.map((value) => value.toLowerCase()), planned.plan.feeAssets.map((policy) => policy.asset.toLowerCase()));
621
+ const expectedRewardAssets = planned.plan.feeAssets.filter((policy) => policy.rewardsBps > 0).map((policy) => policy.asset.toLowerCase());
622
+ const rewardAssets = await read(hub, sdk.lifecycleFeeHubAbi, "rewardAssets");
623
+ assert.deepEqual(rewardAssets.map((value) => value.toLowerCase()), expectedRewardAssets);
624
+ if (scenario.rewardMode !== 0) {
625
+ const moduleAssets = await read(tokenState.rewardModule, sdk.lifecycleRewardsAbi, "rewardAssets");
626
+ assert.deepEqual(moduleAssets.map((value) => value.toLowerCase()), expectedRewardAssets, "Actual reward-module assets");
627
+ }
628
+ artifacts.event("actual-reward-binding", { mode: scenario.rewardMode, module: tokenState.rewardModule, rewardAssets });
629
+ for (const policy of planned.plan.feeAssets) {
630
+ const actual = await read(hub, sdk.lifecycleFeeHubAbi, "policy", [policy.asset]);
631
+ artifacts.event("actual-fee-policy", { policy: actual });
632
+ addressEqual(actual.asset, policy.asset, "Actual fee asset");
633
+ for (const key of ["ownerBps", "rewardsBps", "burnBps"]) assert.equal(actual[key], policy[key], `Actual ${key}`);
634
+ }
635
+ const activated = receiptEvents(activation.receipt, fixture.orchestrator, sdk.launchLifecycleAbi, "LaunchActivated");
636
+ 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");
637
+ assert.equal(activated[0].marketCount, scenario.markets.length); assert.equal(activated[0].positionCount, expectedPositions);
638
+ const buys = receiptEvents(activation.receipt, fixture.orchestrator, sdk.launchLifecycleAbi, "InitialBuyExecuted");
639
+ const transfers = artifacts.transactions.flatMap((row) => receiptEvents(row.receipt, token, erc20Abi, "Transfer"));
640
+ tokenState.inventory = verifyLaunchInventory({
641
+ supply: planned.plan.token.supply, totalSupply: tokenState.totalSupply,
642
+ orchestrator: fixture.orchestrator, creator: fixture.creator, transfers, buys,
643
+ orchestratorBalance: await read(token, sdk.lifecycleErc20Abi, "balanceOf", [fixture.orchestrator]),
644
+ creatorBalance: await read(token, sdk.lifecycleErc20Abi, "balanceOf", [fixture.creator]),
645
+ });
646
+ artifacts.event("actual-inventory-accounting", tokenState.inventory);
647
+ artifacts.result.chain.buys = buys;
648
+ artifacts.event("actual-ordered-buys", { buys });
649
+ assert.equal(buys.length, planned.plan.buys.length);
650
+ for (let index = 0; index < buys.length; index += 1) {
651
+ const actual = buys[index]; const expected = planned.plan.buys[index];
652
+ assert.equal(actual.buyIndex, index); assert.equal(actual.marketIndex, expected.marketIndex); addressEqual(actual.launchId, planned.launchId, "Buy launch ID");
653
+ addressEqual(actual.quoteAsset, planned.plan.markets[expected.marketIndex].quoteAsset, "Market-specific buy quote"); addressEqual(actual.recipient, expected.recipient, "Buy recipient");
654
+ assert.ok(actual.tokenOut >= expected.minTokenOut); assert.ok(actual.quoteSpent > 0n && actual.quoteSpent <= expected.quoteAmountIn);
655
+ }
656
+ const directory = await read(fixture.orchestrator, sdk.launchLifecycleAbi, "directory");
657
+ const canonicalMarkets = [];
658
+ artifacts.result.chain.markets = canonicalMarkets;
659
+ for (const market of progress.markets) {
660
+ const spec = scenario.markets[market.index];
661
+ const config = spec.venue === "v4" ? sdk.decodePoolBoundV4LifecycleMarketConfig(planned.plan.markets[market.index].config) : sdk.decodeAbyssLifecycleMarketConfig(planned.plan.markets[market.index].config);
662
+ const identity = market.prepared.identity;
663
+ const quoteAsset = planned.plan.markets[market.index].quoteAsset;
664
+ const currencies = [token, quoteAsset].sort((left, right) => BigInt(left) < BigInt(right) ? -1 : 1);
665
+ addressEqual(identity.currency0, currencies[0], "Actual market currency0");
666
+ addressEqual(identity.currency1, currencies[1], "Actual market currency1");
667
+ const observation = { ...market, positions: [] };
668
+ canonicalMarkets.push(observation);
669
+ artifacts.event("actual-market", { market });
670
+ assert.equal(market.live?.publicTrading, true, market.error ?? "Actual public trading must be open");
671
+ assert.equal(identity.openingSqrtPriceX96, spec.venue === "v4" ? config.sqrtPriceX96 : config.openingSqrtPriceX96);
672
+ assert.equal(identity.fee, 3000); assert.equal(identity.tickSpacing, 60);
673
+ assert.equal(identity.venue, spec.venue === "v4" ? sdk.LifecycleVenue.UniswapV4 : sdk.LifecycleVenue.Abyss);
674
+ assert.equal(market.prepared.positionCount, spec.positions); assert.ok(!same(market.prepared.custody, zeroAddress));
675
+ if (spec.venue === "v4") {
676
+ const sealed = await read(market.prepared.custody, sdk.lifecycleV4LockerAbi, "isSealed", [identity.poolId]);
677
+ const count = await read(market.prepared.custody, sdk.lifecycleV4LockerAbi, "positionCount", [identity.poolId]);
678
+ observation.permanentCustody = { sealed, count };
679
+ assert.equal(sealed, true); assert.equal(count, BigInt(spec.positions));
680
+ }
681
+ const positions = await read(directory, sdk.lifecycleDirectoryAbi, "positions", [planned.launchId, market.index, 0n, BigInt(spec.positions)]);
682
+ assert.equal(positions.length, spec.positions);
683
+ const profile = planned.profiles.find((row) => same(row.id, identity.profileId));
684
+ assert.ok(profile, "Actual market must use the planned registered profile");
685
+ for (let index = 0; index < positions.length; index += 1) {
686
+ const position = positions[index]; const expected = config.positions[index];
687
+ const [liquidity, custody] = await read(profile.adapter.implementation, sdk.lifecycleAdapterAbi, "readPosition", [position]);
688
+ const actual = { ...position, actualLiquidity: liquidity, actualCustody: custody };
689
+ observation.positions.push(actual);
690
+ addressEqual(position.marketId, identity.canonicalId, "Position canonical market"); addressEqual(position.custody, market.prepared.custody, "Position committed custody");
691
+ assert.equal(position.tickLower, expected.tickLower); assert.equal(position.tickUpper, expected.tickUpper);
692
+ assert.equal(position.liquidity, expected.liquidity); assert.equal(liquidity, expected.liquidity); addressEqual(custody, market.prepared.custody, "Actual permanent position custody");
693
+ if (spec.venue === "v4") addressEqual(position.salt, expected.salt, "Committed V4 position salt");
694
+ else {
695
+ const lock = await read(custody, sdk.abyssPositionLockerAbi, "locks", [position.tokenId]);
696
+ actual.permanentLock = lock;
697
+ for (let term = 0; term < 3; term += 1) addressEqual(lock[term], market.prepared.feeSource, "Abyss collector-owned permanent lock");
698
+ assert.equal(lock[3], 0n); assert.equal(lock[4], false);
699
+ }
700
+ artifacts.event("actual-position-custody", { marketIndex: market.index, position: actual });
701
+ }
702
+ }
703
+ assert.equal(await sdk.buildNextTransaction({ client, planned, receipts: references }), undefined, "Active is terminal; no implicit launch retry");
704
+ artifacts.result.chain.status = "passed"; artifacts.result.execution = "completed";
705
+ artifacts.event("chain-verified", { token, activationTransactionHash: activation.transactionHash, marketCount: canonicalMarkets.length, positionCount: expectedPositions, buyCount: buys.length });
706
+ }
707
+
708
+ export async function assertCanonicalExampleActivation({ client, planned, references, activation, sdk, artifacts, signal }) {
709
+ try {
710
+ assert.ok(activation?.receipt, "Retained original activation receipt is required");
711
+ const original = references.find((reference) => same(reference.transactionHash, activation.transactionHash));
712
+ assert.ok(original && original.observedBlockNumber === BigInt(activation.receipt.blockNumber) &&
713
+ same(original.observedBlockHash, activation.receipt.blockHash), "Original typed receipt inclusion must remain retained");
714
+ const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
715
+ const evidence = { progress, references, originalActivation: {
716
+ transactionHash: activation.transactionHash, blockNumber: original.observedBlockNumber, blockHash: original.observedBlockHash,
717
+ } };
718
+ artifacts.result.chain.progress = progress;
719
+ artifacts.save("publication-progress.json", evidence);
720
+ artifacts.event("publication-canonical-progress", evidence);
721
+ artifacts.privateRecovery({ publicationEvidence: evidence });
722
+ const receipt = progress.receipts.find((row) => same(row.transactionHash, activation.transactionHash));
723
+ if (!progress.confirmationSafe || progress.canonical.phase !== sdk.LifecyclePhase.Active ||
724
+ !same(progress.canonical.token, planned.predictedToken) || progress.receipts.length !== references.length ||
725
+ !progress.receipts.every((row) => row.status === "confirmed") || receipt?.status !== "confirmed" ||
726
+ !same(receipt.effectiveHash, activation.transactionHash) || receipt.blockNumber !== original.observedBlockNumber ||
727
+ !same(receipt.blockHash, original.observedBlockHash)) {
728
+ throw exampleError("ACTIVATION_CANONICALITY_CHANGED",
729
+ "Original activation is no longer confirmation-safe canonical Active at its retained block number/hash; stop and reconcile the saved recovery evidence, never republish or relaunch",
730
+ { data: evidence });
731
+ }
732
+ return progress;
733
+ } catch (error) {
734
+ artifacts.result.chain.status = "canonicality-failed";
735
+ artifacts.result.execution = "reconciliation-required";
736
+ artifacts.event("publication-canonicality-failed", { error: errorEvidence(error) });
737
+ throw error;
738
+ }
739
+ }
740
+
741
+ export async function publishAndVerify({ client, fixture, planned, references, activation, session, sdk, artifacts, transport, signal, timeoutSeconds }) {
742
+ artifacts.stage("api-publication", { transactionHash: activation.transactionHash, timeoutSeconds });
743
+ artifacts.result.api.activationTransactionHash = activation.transactionHash;
744
+ const published = await publishUntilIndexed({
745
+ timeoutMs: timeoutSeconds * 1000, artifacts, signal,
746
+ isPending: (error) => error instanceof sdk.LaunchPublishPending,
747
+ publish: async (remainingMs) => {
748
+ const started = Date.now();
749
+ await assertCanonicalExampleActivation({ client, planned, references, activation, sdk, artifacts, signal });
750
+ const availableMs = remainingMs - (Date.now() - started);
751
+ if (availableMs <= 0) throw exampleError("PUBLISH_TIMEOUT", "Canonical publication recheck exhausted the indexing deadline");
752
+ transport.setTimeoutMs(availableMs);
753
+ try { return await session.api.publishUploadSession(Number(planned.chainId), session.sessionId, session.capability, { chainId: Number(planned.chainId), transactionHash: activation.transactionHash }); }
754
+ catch (error) {
755
+ if (error instanceof sdk.LaunchPublishPending) {
756
+ assert.equal(error.session.chainId, Number(planned.chainId));
757
+ addressEqual(error.session.wallet, planned.plan.creator, "Pending API wallet");
758
+ if (error.session.token != null) addressEqual(error.session.token, planned.predictedToken, "Pending API token");
759
+ if (error.session.transactionHash != null) addressEqual(error.session.transactionHash, activation.transactionHash, "Pending API activation");
760
+ }
761
+ throw error;
762
+ }
763
+ },
764
+ });
765
+ artifacts.result.api.session = published;
766
+ artifacts.event("actual-published-session", { session: published });
767
+ addressEqual(published.token, planned.predictedToken, "Published actual token");
768
+ addressEqual(published.transactionHash, activation.transactionHash, "Published activation transaction");
769
+ assert.equal(published.chainId, Number(planned.chainId)); addressEqual(published.wallet, planned.plan.creator, "Published wallet");
770
+ assert.ok(["optimistic", "final"].includes(published.status)); assert.ok(["optimistic", "final"].includes(published.canonicalStatus));
771
+ assert.equal(published.metadataStatus, "ready", "Publication must actually make owner metadata available");
772
+ assert.equal(published.metadata.name, planned.plan.token.name); assert.equal(published.metadata.symbol, planned.plan.token.symbol);
773
+ assert.equal(published.metadata.description, session.metadata.description || null);
774
+ if (session.image) {
775
+ // The publish response proves the staged image via its descriptor; the public
776
+ // imageUrl is only carried on the indexed DTO (verified below), not here.
777
+ for (const field of ["sha256", "contentType", "contentLength"]) assert.equal(published.image?.[field], session.image[field], `Published image ${field}`);
778
+ assert.equal(published.imageStatus, "ready");
779
+ }
780
+ artifacts.stage("api-token-verification");
781
+ transport.setTimeoutMs(30_000);
782
+ const url = new URL(`/api/v1/launches/${planned.predictedToken}`, fixture.apiUrl);
783
+ url.searchParams.set("chainId", String(planned.chainId));
784
+ const response = await transport.fetch(url, { method: "GET" });
785
+ const representation = await response.json();
786
+ artifacts.result.api.representation = representation;
787
+ artifacts.save("published-token.json", representation);
788
+ artifacts.event("actual-api-token", { status: response.status, representation });
789
+ 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 });
790
+ assert.equal(representation.chainId, Number(planned.chainId));
791
+ const item = representation.item;
792
+ assert.ok(item && typeof item === "object", "Actual API launch detail must contain item");
793
+ assert.equal(item.chainId, Number(planned.chainId)); addressEqual(item.token, planned.predictedToken, "Indexed actual token");
794
+ addressEqual(item.transactionHash, activation.transactionHash, "Indexed activation provenance"); addressEqual(item.creator, planned.plan.creator, "Indexed creator");
795
+ assert.ok(["optimistic", "final"].includes(item.canonicalStatus), "Indexed canonical status");
796
+ assert.equal(item.metadata?.document?.name, planned.plan.token.name); assert.equal(item.metadata?.document?.symbol, planned.plan.token.symbol);
797
+ assert.equal(item.launchedToken?.name, planned.plan.token.name); assert.equal(item.launchedToken?.symbol, planned.plan.token.symbol);
798
+ assert.equal(item.metadata?.document?.description ?? "", session.metadata.description);
799
+ if (session.image) {
800
+ const imageKey = item.metadata?.document?.imageKey;
801
+ assert.ok(imageKey?.startsWith(`launches/${planned.chainId}/${planned.predictedToken.toLowerCase()}/`), "Published token-bound image association");
802
+ assert.equal(item.metadata.imageContentType, session.image.contentType);
803
+ assert.equal(item.metadata.imageContentLength, session.image.contentLength);
804
+ // The public image URL lives on the indexed DTO; assert it directly (the
805
+ // publish response carries no imageUrl field on LaunchSessionResponse).
806
+ assert.ok(typeof item.imageUrl === "string" && item.imageUrl.length > 0, "Indexed non-null public image URL");
807
+ assert.ok(new URL(exampleImageUrl(item.imageUrl)).pathname.endsWith(`/${imageKey}`), "Public URL references the actual token-bound upload key");
808
+ artifacts.stage("api-image-verification", { url: item.imageUrl, image: session.image });
809
+ artifacts.result.api.image = await transport.fetchPublishedImage(item.imageUrl, session.image);
810
+ }
811
+ await assertCanonicalExampleActivation({ client, planned, references, activation, sdk, artifacts, signal });
812
+ artifacts.result.api.status = "passed";
813
+ }
814
+
815
+ export async function assertExampleEconomicAuthority({ client, fixture, planned, economicReview, reviewHash, sdk, artifacts, signal, image, fetchImpl = fetch }) {
816
+ signal?.throwIfAborted();
817
+ assertFutureLaunchDeadline(planned.plan.deadline);
818
+ assert.equal(economicReviewHash(economicReview), reviewHash, "Saved economic review must not change");
819
+ assert.equal(fixture.targetMarketCapUsdX18, economicReview.targetMarketCapUsdX18, "Reviewed target valuation");
820
+ assert.equal(planned.plan.token.supply, economicReview.actualMintedSupply, "Actual reviewed total minted supply");
821
+ assert.deepEqual(JSON.parse(sdk.serializeLaunchPlan(planned.plan)), economicReview.finalizedPlan, "Exact reviewed finalized intent");
822
+ assert.equal(planned.planHash, economicReview.planHash, "Exact reviewed plan hash");
823
+ if (image) assert.equal(`0x${createHash("sha256").update(image.bytes).digest("hex")}`, economicReview.image.sha256, "Frozen signed image remains unchanged");
824
+ assertExampleConversionBindings({ plan: planned.plan, quotes: economicReview.quotes });
825
+ addressEqual(await sdk.predictLifecycleToken({ client, plan: planned.plan }), economicReview.predictedToken, "Fresh reviewed token orientation");
826
+ const block = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
827
+ // Only quotes that actually used Kyber need router re-admission; native-wrap
828
+ // quotes are validated by planner admission and carry no router evidence.
829
+ const kyberQuotes = economicReview.quotes.filter((quote) => quote.conversion.nativeWrap !== true);
830
+ if (kyberQuotes.length > 0) {
831
+ await assertExampleConversionAdmission({ client, fixture, escrow: kyberQuotes[0].conversion.escrow, sdk, block, artifacts });
832
+ }
833
+ const currentQuotes = [];
834
+ for (const quote of economicReview.quotes) {
835
+ const current = await readExampleQuoteUsdSeed({ sdk, fixture, quote, signal, fetchImpl });
836
+ assertUnchangedEconomicQuote(quote.usdSeed, current);
837
+ currentQuotes.push(current);
838
+ }
839
+ // An async API/registry/prediction reread cannot carry authority past the saved deadline.
840
+ signal?.throwIfAborted();
841
+ assertFutureLaunchDeadline(planned.plan.deadline);
842
+ assertExampleConversionBindings({ plan: planned.plan, quotes: economicReview.quotes });
843
+ artifacts.event("economic-authority-rechecked", { reviewHash, currentQuotes });
844
+ }
845
+
846
+ async function verifyExampleDeployment({ client, fixture, sdk, artifacts, signal }) {
847
+ signal?.throwIfAborted();
848
+ artifacts.stage("deployment-provenance");
849
+ const chainId = BigInt(await client.request({ method: "eth_chainId" }));
850
+ assert.equal(chainId, 4663n, "Examples require Robinhood chain 4663");
851
+ assert.equal(await client.request({ method: "eth_getCode", params: [fixture.creator, "latest"] }), "0x", "Creator must be an EOA");
852
+ assert.notEqual(await client.request({ method: "eth_getCode", params: [fixture.orchestrator, "latest"] }), "0x", "Current orchestrator must be deployed");
853
+ addressEqual(await contractReader(client)(fixture.orchestrator, sdk.launchLifecycleAbi, "registry"), fixture.implementationRegistry, "Configured deployment registry");
854
+ artifacts.event("deployment-verified", { chainId, creator: fixture.creator, orchestrator: fixture.orchestrator });
855
+ return chainId;
856
+ }
857
+
858
+ async function executeExample({ fixture, privateKey, scenario, sdk, artifacts, signal, broadcast = false,
859
+ client = rpcClient(fixture.rpcUrl, artifacts, signal, "execution"),
860
+ createWallet = createLocalLaunchWallet, stage = stageMetadata,
861
+ execute = executePlan, verify = verifyChain, publish = publishAndVerify }) {
862
+ validateCase(scenario);
863
+ const metadata = resolveExampleIdentity(scenario, fixture.identityOverrides);
864
+ const image = readExampleImage({ sdk, path: fixture.imagePath });
865
+ artifacts.result.network = "chain-4663";
866
+ artifacts.result.chain.chainId = fixture.chainId; artifacts.result.chain.rpcUrl = fixture.rpcUrl;
867
+ artifacts.result.chain.orchestrator = fixture.orchestrator; artifacts.result.chain.creator = fixture.creator;
868
+ artifacts.result.api = { status: "not-run", endpoint: fixture.apiUrl };
869
+ artifacts.save("run-config.json", { configuration: fixture, case: scenario });
870
+ const chainId = await verifyExampleDeployment({ client, fixture, sdk, artifacts, signal });
871
+ // Planning and review need only the public creator address, never a wallet signer.
872
+ const limits = limitSource(fixture, artifacts);
873
+ const { planned, predictedToken, economicReview, reviewHash } = await constructPlan({ client, fixture, scenario, sdk, artifacts, signal, limits, metadata, image });
874
+ const { plan } = planned;
875
+ artifacts.save("planning.json", planned);
876
+ artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, prerequisites: planned.prerequisites, transactions: planned.transactions });
877
+ assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
878
+ if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
879
+ const usd = (value) => `${value / UNIT}.${(value % UNIT).toString().padStart(18, "0")}`;
880
+ const fundingSummary = economicReview.quotes.every((quote) => quote.conversion.nativeWrap === true)
881
+ ? "payable native wrap"
882
+ : economicReview.quotes.some((quote) => quote.conversion.nativeWrap === true)
883
+ ? "native wrap for the wrapped-native quote, Kyber conversion for the rest"
884
+ : "Kyber conversion";
885
+ console.log(`Economic review ${reviewHash}\n${metadata.name} (${metadata.symbol}); target USD ${usd(economicReview.targetMarketCapUsdX18)}; minted supply ${usd(economicReview.actualMintedSupply)}\n` +
886
+ economicReview.markets.map((market) => `Market ${market.marketIndex} ${market.venue}/${market.quoteSymbol}: retained-supply opening valuation USD ${usd(market.retainedSupplyOpeningValuationUsdX18)} (minted-supply USD ${usd(market.openingValuationUsdX18)}), token USD ${usd(market.unitPriceUsdX18)}, token ${market.quoteSymbol} ${usd(market.unitPriceQuoteX18)}, ${market.positions.length} positions`).join("\n") +
887
+ `\nExpected rounding burn ${economicReview.expectedUnusedBurn} raw token units; ${planned.plan.buys.length} ordered buys, native input ${fixture.buyAmountWei} wei each (${fundingSummary}); 50 bps conversion and ordered-buy protection.\nLP fee 3000 pips; V4 hook max 10000/min 1000 pips (fixed maximum schedule); executor 275 bps.\nImage ${image ? `${image.descriptor.contentType}, ${image.descriptor.contentLength} bytes, ${image.descriptor.sha256}` : "none (metadata only)"}.\nReview allocations, fees and exact protected buys: ${artifacts.directory}/economic-review.json`);
888
+ console.log(economicReview.initialBuy.simulatedBuys.map((buy) => `Buy ${buy.buyIndex} market ${buy.marketIndex}/${buy.quoteSymbol}: quote spent ${buy.quoteSpent} raw units; simulated all-in USD/token ${usd(buy.allInUnitPriceUsdX18)}; fees + price-impact premium ${buy.feeAndPriceImpactPremiumBps} bps vs opening`).join("\n"));
889
+ for (const quote of economicReview.quotes) {
890
+ const seed = quote.usdSeed;
891
+ console.log(`${quote.symbol}/USD ${usd(seed.priceUsdX18)}; source ${seed.source}; chain ${seed.chainId}; quote ${seed.tokenAddress} (${seed.tokenDecimals} decimals); retrieved ${seed.retrievedAt}.\n` +
892
+ (seed.source === "abyss-api" ? `API ${seed.apiOrigin}, indexed block ${seed.indexedBlock}; graph block ${seed.blockNumber}; WETH graph block ${seed.graphBlockNumber}; ${seed.anchorKind} anchor ${seed.anchorAggregator}; upstream ${seed.sourceTimestamp} (${seed.upstreamAgeSeconds} seconds old at retrieval). Indicative valuation, not execution proof.\n` : "Reviewed SDK USDG dollar peg; no WETH API-price substitution.\n") +
893
+ (quote.conversion.nativeWrap === true
894
+ ? `Native-wrap payable funding; input ${quote.conversion.nativeInputPerBuy} wei per buy across ${quote.conversion.buyCount} buy(s); oracle ${quote.oracle.oracleConfigId}.`
895
+ : `Kyber ${quote.conversion.apiOrigin}; input ${quote.conversion.response.amountIn} native wei; protected quote output ${quote.conversion.response.amountOutMinimum} raw ${quote.symbol}; conversion price impact ${quote.conversion.response.priceImpactBps ?? "unknown"} bps; expires ${quote.conversion.response.expiresAt}; oracle ${quote.oracle.oracleConfigId}.`));
896
+ }
897
+ if (!broadcast) {
898
+ artifacts.result.execution = "dry-run";
899
+ artifacts.stage("dry-run-completed");
900
+ return;
901
+ }
902
+ // --broadcast is the authorization; the hash is retained only as provenance.
903
+ const economicAdmission = { authorization: "--broadcast", reviewHash, admittedAt: new Date().toISOString(),
904
+ policy: "unchanged-exact-api-price-and-source; upstream-age-preserved" };
905
+ artifacts.privateRecovery({ economicAdmission });
906
+ artifacts.save("economic-admission.json", economicAdmission);
907
+ const assertEconomicAuthority = () => assertExampleEconomicAuthority({ client, fixture, planned, economicReview, reviewHash, sdk, artifacts, signal, image });
908
+ const localWallet = createWallet({ privateKey, creator: fixture.creator, chainId, orchestrator: fixture.orchestrator, client, redactor: artifacts.redactor });
909
+ const wallet = guardEconomicReviewWallet(localWallet, assertEconomicAuthority);
910
+ const transport = apiTransport({ artifacts, signal, apiUrl: fixture.apiUrl });
911
+ const session = await stage({ fixture, plan, sdk, artifacts, transport, wallet, metadata, image });
912
+ const executed = await execute({ client, fixture, planned, sdk, artifacts, signal, limits, wallet });
913
+ await verify({ client, planned, ...executed, fixture, scenario, sdk, artifacts });
914
+ await publish({ client, fixture, planned, ...executed, session, sdk, artifacts, transport, signal, timeoutSeconds: 90 });
915
+ artifacts.stage("completed");
916
+ }
917
+
918
+ export function readLaunchExampleArguments(argv) {
919
+ const usage = "Usage: node examples/<case>.ts [--broadcast | --resume <recovery-dir>]\nOmit flags for a dry run; --broadcast authorizes signing, transactions and publication.";
920
+ if (!argv.length) return { mode: "dry-run" };
921
+ if (argv.length === 1 && argv[0] === "--broadcast") return { mode: "broadcast" };
922
+ if (argv[0] !== "--resume") throw exampleError("INVALID_ARGUMENTS", usage);
923
+ if (argv.length !== 2 || typeof argv[1] !== "string" || !argv[1].trim() || argv[1].startsWith("--")) {
924
+ throw exampleError("MISSING_RECOVERY", `--resume requires exactly one recovery directory.\n${usage}`);
925
+ }
926
+ return { mode: "resume", resumeDirectory: argv[1] };
927
+ }
928
+
929
+ export function launchExampleBroadcastCommand({ script = process.argv[1], cwd = process.cwd(), execArgv = process.execArgv } = {}) {
930
+ const quote = (value) => /^[\w./:-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
931
+ return ["node", ...execArgv, relative(cwd, script), "--broadcast"].map(quote).join(" ");
932
+ }
933
+
934
+ function assertRetainedExampleReceipts({ progress, references, artifacts }) {
935
+ if (!progress.confirmationSafe || progress.receipts.length !== references.length || references.some((reference) => {
936
+ const receipt = progress.receipts.find((row) => same(row.transactionHash, reference.transactionHash));
937
+ return receipt?.status !== "confirmed" || !same(receipt.effectiveHash, reference.transactionHash) ||
938
+ receipt.blockNumber !== reference.observedBlockNumber || !same(receipt.blockHash, reference.observedBlockHash);
939
+ })) {
940
+ artifacts.result.execution = "reconciliation-required";
941
+ artifacts.result.chain.status = "canonicality-failed";
942
+ artifacts.save("resume-progress.json", { progress, references });
943
+ throw exampleError("RECEIPT_RECONCILIATION_REQUIRED",
944
+ "Retained receipts are no longer confirmation-safe at their original block number/hash; reconcile read-only, never replay or relaunch");
945
+ }
946
+ }
947
+
948
+ export async function refreshExampleUploadSession({ fixture, planned, recovery, sdk, artifacts, transport, signal }) {
949
+ artifacts.stage("api-resume-session");
950
+ const api = new sdk.LaunchApiClient({ baseUrl: fixture.apiUrl, retries: 1, fetch: transport.fetch.bind(transport) });
951
+ const sessionId = recovery.session?.sessionId;
952
+ const capability = recovery.capability ?? recovery.session?.capability;
953
+ assert.ok(typeof sessionId === "string" && sessionId.length && typeof capability === "string" && capability.length,
954
+ "Recovery requires the original API session and one-time capability");
955
+ if (recovery.session.capability) assert.equal(recovery.session.capability, capability, "Original one-time capability");
956
+ artifacts.redactor.addSecret(capability);
957
+ const metadata = recovery.economicReview.metadata;
958
+ const image = recovery.economicReview.image ?? undefined;
959
+ assert.deepEqual(recovery.request.metadata, metadata, "Original signed metadata");
960
+ assert.deepEqual(recovery.request.image ?? null, image ?? null, "Original signed image descriptor");
961
+ const retain = (current) => {
962
+ // Read/renew/complete responses never echo the one-time capability.
963
+ const session = { ...current, capability };
964
+ artifacts.privateRecovery({ session, capability });
965
+ artifacts.result.api.session = session;
966
+ artifacts.result.api.sessionId = sessionId;
967
+ assert.equal(session.sessionId, sessionId, "Original API session"); assert.equal(session.chainId, Number(planned.chainId));
968
+ addressEqual(session.wallet, planned.plan.creator, "Resumed session wallet");
969
+ assert.equal(session.metadata.name, metadata.name); assert.equal(session.metadata.symbol, metadata.symbol);
970
+ assert.equal(session.metadata.description, metadata.description || null);
971
+ if (session.transactionHash != null) {
972
+ addressEqual(session.transactionHash, artifacts.recovery.activation?.transactionHash, "Original session activation");
973
+ }
974
+ if (session.token != null) addressEqual(session.token, planned.predictedToken, "Original session token");
975
+ if (image) for (const field of ["sha256", "contentType", "contentLength"]) assert.equal(session.image?.[field], image[field], `Resumed signed image ${field}`);
976
+ else assert.equal(session.image, null, "Original metadata-only session");
977
+ return session;
978
+ };
979
+ signal?.throwIfAborted();
980
+ let session = retain(await api.getUploadSession(Number(planned.chainId), sessionId, capability));
981
+ if (image && session.imageStatus !== "ready") {
982
+ assert.ok(session.status === "awaiting_upload" && session.upload, "Live session must await the original image upload");
983
+ if (!fixture.imagePath) throw exampleError("IMAGE_INVALID", "Resume with an unready image requires EXAMPLE_IMAGE_PATH for the original signed bytes");
984
+ const reloaded = readExampleImage({ sdk, path: fixture.imagePath });
985
+ assert.deepEqual(reloaded.descriptor, image, "Reloaded image bytes must match the original signed SHA-256, content type and length");
986
+ const put = async () => {
987
+ signal?.throwIfAborted();
988
+ transport.authorizeImageUpload(session.upload, image);
989
+ await api.putImage(session.upload, new Blob([reloaded.bytes], { type: image.contentType }));
990
+ };
991
+ try { await put(); }
992
+ catch (error) {
993
+ if (!(error instanceof sdk.LaunchApiError) || ![403, 410, 412].includes(error.status) || typeof api.renewUploadUrl !== "function") throw error;
994
+ artifacts.event("image-upload-url-expired", { status: error.status });
995
+ session = retain(await api.renewUploadUrl(Number(planned.chainId), sessionId, capability));
996
+ assert.ok(session.status === "awaiting_upload" && session.upload, "Renewed session must issue the original image's fresh PUT URL");
997
+ await put(); // Only one renewal/retry, always the same signed bytes.
998
+ }
999
+ signal?.throwIfAborted();
1000
+ session = retain(await api.completeUpload(Number(planned.chainId), sessionId, capability));
1001
+ assert.equal(session.imageStatus, "ready", "Original image upload must be verified");
1002
+ for (const field of ["width", "height"]) assert.ok(Number.isSafeInteger(session.image[field]) && session.image[field] > 0 && session.image[field] <= 4096, `Verified image ${field}`);
1003
+ artifacts.event("image-upload-completed", { session });
1004
+ }
1005
+ assert.ok(["ready_to_launch", "transaction_submitted", "awaiting_indexer", "optimistic", "final"].includes(session.status), "Resumed session must be reusable for publication");
1006
+ artifacts.result.api.status = "session-ready";
1007
+ return { api, sessionId, capability, metadata, image };
1008
+ }
1009
+
1010
+ export async function resumeLaunchExample({ fixture, privateKey, sdk, artifacts, signal,
1011
+ client = rpcClient(fixture.rpcUrl, artifacts, signal, "execution"),
1012
+ transport = apiTransport({ artifacts, signal, apiUrl: fixture.apiUrl }),
1013
+ execute = executePlan, verify = verifyChain, publish = publishAndVerify }) {
1014
+ const recovery = artifacts.recovery;
1015
+ assert.ok(recovery && ["atomic", "staged"].includes(recovery.mode) && recovery.plan, "Recovery requires the original committed plan and mode");
1016
+ const plan = sdk.parseLaunchPlan(JSON.stringify(recovery.plan));
1017
+ const confirmations = recovery.confirmations;
1018
+ assert.ok(Number.isSafeInteger(confirmations) && confirmations > 0, "Retained confirmation policy");
1019
+ assert.ok(recovery.economicReview?.reviewHash && Array.isArray(recovery.economicReview.markets), "Recovery requires the retained economic review");
1020
+ const { reviewHash, ...economicReview } = recovery.economicReview;
1021
+ assert.equal(economicReviewHash(economicReview), reviewHash, "Retained economic review hash");
1022
+ assert.equal(recovery.economicAdmission?.reviewHash, reviewHash, "Retained explicit economic admission");
1023
+ assert.deepEqual(JSON.parse(sdk.serializeLaunchPlan(plan)), economicReview.finalizedPlan, "Original reviewed finalized plan");
1024
+ assert.equal(sdk.hashLaunchPlan(plan), economicReview.planHash, "Original reviewed plan commitment");
1025
+ // Rehydrate only the wide review values read by the existing signing guard.
1026
+ economicReview.targetMarketCapUsdX18 = BigInt(economicReview.targetMarketCapUsdX18);
1027
+ economicReview.actualMintedSupply = BigInt(economicReview.actualMintedSupply);
1028
+ for (const quote of economicReview.quotes) {
1029
+ quote.conversion = { ...quote.conversion, nativeInputPerBuy: BigInt(quote.conversion.nativeInputPerBuy),
1030
+ ...(quote.conversion.deadline === undefined ? {} : { deadline: BigInt(quote.conversion.deadline) }) };
1031
+ }
1032
+ const scenario = { id: recovery.caseId ?? artifacts.result.caseId, mode: recovery.mode,
1033
+ tokenKind: plan.token.kind, rewardMode: plan.token.rewardMode,
1034
+ markets: economicReview.markets.map((market) => ({ venue: market.venue, positions: market.positions.length })) };
1035
+ assert.equal(scenario.markets.length, plan.markets.length, "Retained review market count");
1036
+ assert.ok(scenario.markets.every((market) => ["v4", "abyss"].includes(market.venue) && market.positions > 0), "Retained review market topology");
1037
+ assert.equal(plan.chainId, BigInt(fixture.chainId), "Original execution chain");
1038
+ addressEqual(fixture.creator, plan.creator, "Configured signer must be the retained creator");
1039
+ addressEqual(fixture.orchestrator, plan.orchestrator, "Original orchestrator");
1040
+ assert.equal(launchApiUrl(recovery.apiUrl), launchApiUrl(fixture.apiUrl), "Original API endpoint");
1041
+ fixture = { ...fixture, targetMarketCapUsdX18: economicReview.targetMarketCapUsdX18,
1042
+ executionLimits: recovery.executionLimits ?? artifacts.configuration?.executionLimits ?? fixture.executionLimits };
1043
+ assert.ok(Array.isArray(recovery.receipts) && recovery.receipts.length, "Resume requires retained confirmed receipt evidence");
1044
+ const references = recovery.receipts.map((reference) => {
1045
+ assert.match(reference.transactionHash, /^0x[\da-fA-F]{64}$/, "Original receipt hash");
1046
+ assert.match(reference.observedBlockHash, /^0x[\da-fA-F]{64}$/, "Original receipt block hash");
1047
+ assert.ok(reference.observedBlockNumber !== undefined && reference.observedBlockNumber !== null, "Original receipt block number");
1048
+ return { ...reference, observedBlockNumber: BigInt(reference.observedBlockNumber), confirmations: reference.confirmations ?? confirmations };
1049
+ });
1050
+ const retainedHashes = new Set(references.map((reference) => reference.transactionHash.toLowerCase()));
1051
+ if (references.some((reference) => reference.replacementHash && !same(reference.replacementHash, reference.transactionHash)) ||
1052
+ [...(recovery.signedTransactions ?? []), ...artifacts.transactions].some((row) => !retainedHashes.has(row.transactionHash.toLowerCase()))) {
1053
+ artifacts.result.execution = "reconciliation-required";
1054
+ throw exampleError("RECEIPT_RECONCILIATION_REQUIRED", "A signed or replaced hash lacks original confirmed inclusion evidence; reconcile read-only before resuming, never resend");
1055
+ }
1056
+ artifacts.result.network = "chain-4663";
1057
+ Object.assign(artifacts.result.chain, { chainId: fixture.chainId, rpcUrl: fixture.rpcUrl, orchestrator: plan.orchestrator, creator: plan.creator, confirmations });
1058
+ artifacts.result.api.endpoint = fixture.apiUrl;
1059
+ const chainId = await verifyExampleDeployment({ client, fixture, sdk, artifacts, signal });
1060
+ const limits = limitSource(fixture, artifacts);
1061
+ artifacts.stage("resume-planning");
1062
+ const planned = await sdk.planLaunch({ client, account: plan.creator, plan, mode: recovery.mode, limits, confirmations, receipts: references, signal });
1063
+ assert.deepEqual(JSON.parse(sdk.serializeLaunchPlan(planned.plan)), recovery.plan, "Resume cannot remine or change the committed plan");
1064
+ assert.equal(planned.mode, recovery.mode, "Original admitted mode");
1065
+ assert.equal(planned.planHash, economicReview.planHash, "Original admitted commitment");
1066
+ assert.equal(planned.launchId, sdk.hashLaunchIdentity(plan), "Original launch identity");
1067
+ addressEqual(planned.predictedToken, economicReview.predictedToken, "Original predicted token");
1068
+ const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations, signal });
1069
+ assertRetainedExampleReceipts({ progress, references, artifacts });
1070
+ if (![sdk.LifecyclePhase.Preparing, sdk.LifecyclePhase.Ready, sdk.LifecyclePhase.Active].includes(progress.canonical.phase)) {
1071
+ throw exampleError("RESUME_PHASE_UNSUPPORTED", "Resume requires canonical Preparing, Ready or Active; an unstarted or cancelled launch must never be relaunched");
1072
+ }
1073
+ if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "Remaining committed launch work was not admitted", { simulation: planned.simulation });
1074
+ Object.assign(artifacts.result.chain, { planHash: planned.planHash, launchId: planned.launchId, predictedToken: planned.predictedToken });
1075
+ // Canonical receipts restore the begin/deployment logs needed by verifyChain.
1076
+ for (const reference of references) {
1077
+ const receipt = await client.request({ method: "eth_getTransactionReceipt", params: [reference.transactionHash] });
1078
+ if (!receipt || BigInt(receipt.status) !== 1n || !same(receipt.transactionHash, reference.transactionHash) ||
1079
+ BigInt(receipt.blockNumber) !== reference.observedBlockNumber || !same(receipt.blockHash, reference.observedBlockHash)) {
1080
+ artifacts.result.execution = "reconciliation-required";
1081
+ throw exampleError("RECEIPT_RECONCILIATION_REQUIRED", "Retained receipt inclusion changed; reconcile read-only, never replay");
1082
+ }
1083
+ let row = artifacts.transactions.find((transaction) => same(transaction.transactionHash, reference.transactionHash));
1084
+ if (!row) { row = { transactionHash: reference.transactionHash }; artifacts.transactions.push(row); }
1085
+ Object.assign(row, { status: "confirmed", reference, receipt });
1086
+ }
1087
+ artifacts.saveReceipts();
1088
+ let executed;
1089
+ if (progress.canonical.phase === sdk.LifecyclePhase.Active) {
1090
+ const activationHash = recovery.activation?.transactionHash ?? references.at(-1).transactionHash;
1091
+ const row = artifacts.transactions.find((transaction) => same(transaction.transactionHash, activationHash));
1092
+ assert.ok(row && retainedHashes.has(activationHash.toLowerCase()), "Active resume requires the retained activation receipt");
1093
+ executed = { references, activation: { transactionHash: activationHash, receipt: row.receipt } };
1094
+ artifacts.result.chain.activationTransactionHash = activationHash;
1095
+ artifacts.privateRecovery({ activation: executed.activation });
1096
+ await assertCanonicalExampleActivation({ client, planned, ...executed, sdk, artifacts, signal });
1097
+ // Terminal planLaunch deliberately omits execution metadata; verifyChain still
1098
+ // needs the committed profiles' adapters to check permanent position custody.
1099
+ planned.profiles = await sdk.readLifecycleProfiles({ client, orchestrator: plan.orchestrator,
1100
+ profileIds: [...new Set(plan.markets.map((market) => market.profileId))] });
1101
+ }
1102
+ const session = await refreshExampleUploadSession({ fixture, planned, recovery: artifacts.recovery, sdk, artifacts, transport, signal });
1103
+ if (!executed) {
1104
+ const localWallet = createLocalLaunchWallet({ privateKey, creator: plan.creator, chainId, orchestrator: plan.orchestrator, client, redactor: artifacts.redactor });
1105
+ const wallet = guardEconomicReviewWallet(localWallet, () => assertExampleEconomicAuthority({ client, fixture, planned, economicReview, reviewHash, sdk, artifacts, signal }));
1106
+ executed = await execute({ client, fixture, planned, sdk, artifacts, signal, limits, wallet, references });
1107
+ }
1108
+ await verify({ client, planned, ...executed, fixture, scenario, sdk, artifacts });
1109
+ await publish({ client, fixture, planned, ...executed, session, sdk, artifacts, transport, signal, timeoutSeconds: 90 });
1110
+ artifacts.stage("completed");
1111
+ }
1112
+
1113
+ export async function runLaunchExample(caseObject, argv = process.argv.slice(2), { sdk: suppliedSdk, env = process.env, ...runtime } = {}) {
1114
+ const redactor = new Redactor();
1115
+ let artifacts;
1116
+ let resumeDirectory;
1117
+ let mode;
1118
+ const controller = new AbortController();
1119
+ const interrupted = () => controller.abort(exampleError("INTERRUPTED", "Launch example interrupted; submitted hashes and chain state are retained"));
1120
+ process.on("SIGINT", interrupted); process.on("SIGTERM", interrupted);
1121
+ try {
1122
+ ({ mode, resumeDirectory } = readLaunchExampleArguments(argv));
1123
+ if (mode === "dry-run") console.log("DRY RUN — no transactions signed or broadcast.");
1124
+ const caseId = caseObject.id;
1125
+ assert.match(caseId, /^[a-z0-9-]+$/, "Example case ID must be a safe directory label");
1126
+ if (resumeDirectory) artifacts = new RunArtifacts(resumeDirectory, caseId, redactor, { resume: true });
1127
+ else {
1128
+ mkdirSync("launch-results", { recursive: true, mode: 0o700 });
1129
+ artifacts = new RunArtifacts(join("launch-results", `${new Date().toISOString().replaceAll(":", "-")}-${caseId}-${randomUUID()}`), caseId, redactor);
1130
+ }
1131
+ artifacts.initialize();
1132
+ artifacts.result.runMode = mode;
1133
+ if (mode === "dry-run") artifacts.result.scope = "planning";
1134
+ loadExampleEnvironment();
1135
+ redactor.addSecret(env.PRIVATE_KEY);
1136
+ if (typeof env.PRIVATE_KEY === "string") redactor.addSecret(env.PRIVATE_KEY.slice(2));
1137
+ // A static SDK import would resolve address overrides before .env loading.
1138
+ const sdk = suppliedSdk ?? await import("@black-market/sdk");
1139
+ const { fixture, privateKey } = await readExampleConfiguration(sdk, redactor, env);
1140
+ const options = { ...runtime, fixture, privateKey, sdk, artifacts, signal: controller.signal };
1141
+ if (resumeDirectory) await resumeLaunchExample(options);
1142
+ else await executeExample({ ...options, scenario: caseObject, broadcast: mode === "broadcast" });
1143
+ artifacts.finish("passed");
1144
+ if (mode === "dry-run") {
1145
+ console.log(`Dry-run planning and economic review passed.\nArtifacts: ${artifacts.directory}\nTo sign, broadcast and publish, re-run with --broadcast:\n${launchExampleBroadcastCommand()}`);
1146
+ } else console.log(`Actual chain launch and API publication/indexing passed${resumeDirectory ? " (resumed)" : ""}\nArtifacts: ${artifacts.directory}\nToken: ${artifacts.result.chain.token}`);
1147
+ return artifacts.result;
1148
+ } catch (failure) {
1149
+ const error = controller.signal.aborted ? controller.signal.reason : failure;
1150
+ if (error !== failure && error instanceof Error) error.cause = failure;
1151
+ if (artifacts) {
1152
+ if (artifacts.result.stage.startsWith("api-")) artifacts.result.api.status = "failed";
1153
+ else if (artifacts.result.execution === "running") artifacts.result.chain.status = "failed";
1154
+ try { artifacts.finish("failed", error); }
1155
+ catch (storageError) { console.error(JSON.stringify(redactor.value({ artifactWriteError: errorEvidence(storageError), originalError: errorEvidence(error) }))); }
1156
+ }
1157
+ console.error(JSON.stringify(redactor.value({ sdk: "node", caseId: artifacts?.result.caseId, status: "failed", stage: artifacts?.result.stage ?? (argv[0] === "--resume" ? "resume" : "configuration"), error: errorEvidence(error), artifacts: artifacts?.directory ?? resumeDirectory }), null, 2));
1158
+ process.exitCode = 1;
1159
+ return artifacts?.result;
1160
+ } finally {
1161
+ process.removeListener("SIGINT", interrupted); process.removeListener("SIGTERM", interrupted);
1162
+ }
1163
+ }