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