@black-market/sdk 0.12.0 → 0.14.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 +96 -96
  2. package/dist/lifecycle/index.d.ts +3 -1
  3. package/dist/lifecycle/index.d.ts.map +1 -1
  4. package/dist/lifecycle/index.js +3 -1
  5. package/dist/lifecycle/index.js.map +1 -1
  6. package/dist/lifecycle/lens.d.ts +409 -0
  7. package/dist/lifecycle/lens.d.ts.map +1 -0
  8. package/dist/lifecycle/lens.js +30 -0
  9. package/dist/lifecycle/lens.js.map +1 -0
  10. package/dist/lifecycle/mining.d.ts +47 -0
  11. package/dist/lifecycle/mining.d.ts.map +1 -0
  12. package/dist/lifecycle/mining.js +89 -0
  13. package/dist/lifecycle/mining.js.map +1 -0
  14. package/dist/lifecycle/planner.d.ts.map +1 -1
  15. package/dist/lifecycle/planner.js +121 -78
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +3 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +1 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +51 -1
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +149 -73
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +14 -4
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +171 -12
  28. package/dist/lifecycle/rpc.js.map +1 -1
  29. package/dist/lifecycle/simulation.d.ts +5 -3
  30. package/dist/lifecycle/simulation.d.ts.map +1 -1
  31. package/dist/lifecycle/simulation.js +137 -115
  32. package/dist/lifecycle/simulation.js.map +1 -1
  33. package/dist/lifecycle/snapshot.d.ts +8 -0
  34. package/dist/lifecycle/snapshot.d.ts.map +1 -0
  35. package/dist/lifecycle/snapshot.js +79 -0
  36. package/dist/lifecycle/snapshot.js.map +1 -0
  37. package/dist/lifecycle/types.d.ts +27 -5
  38. package/dist/lifecycle/types.d.ts.map +1 -1
  39. package/dist/lifecycle/types.js.map +1 -1
  40. package/docs/launch-lifecycle.md +627 -1495
  41. package/docs/reference.md +365 -0
  42. package/examples/.env.example +3 -2
  43. package/examples/README.md +171 -552
  44. package/examples/launch-example-support.ts +11 -3
  45. package/examples/launch-example.ts +1 -1
  46. package/examples/launch-lifecycle-smoke.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/lifecycle/index.ts +5 -1
  49. package/src/lifecycle/lens.ts +42 -0
  50. package/src/lifecycle/mining.ts +80 -0
  51. package/src/lifecycle/planner.ts +122 -73
  52. package/src/lifecycle/presets.ts +4 -0
  53. package/src/lifecycle/progress.ts +132 -59
  54. package/src/lifecycle/rpc.ts +163 -13
  55. package/src/lifecycle/simulation.ts +134 -106
  56. package/src/lifecycle/snapshot.ts +72 -0
  57. package/src/lifecycle/types.ts +24 -6
@@ -3,9 +3,9 @@ import { ABYSS_FEE_TIERS } from "../abyss.js";
3
3
  import { assertLaunchTopologyCoverage, getTickAtSqrtRatio } from "../launch.js";
4
4
  import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
6
- import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
- import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt } from "./markets.js";
8
- import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
6
+ import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
+ import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
8
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
9
  import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
10
10
  import { LifecyclePlanningError } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
@@ -14,6 +14,8 @@ import { buildLaunchTransactions, packPreparationBatches } from "./transactions.
14
14
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
15
15
  import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
16
16
  import { buildUfgLaunchTransactions, buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
+ import { readLifecyclePlanningBlock } from "./snapshot.js";
18
+ import { mineLifecycleHooks } from "./mining.js";
17
19
  function validatePlanShape(plan) {
18
20
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
19
21
  const commitment = hashLaunchPlan(plan);
@@ -159,7 +161,7 @@ function orderedTransactions(plan, mode, progress, approvals, batches, ufg) {
159
161
  const commands = compiled.filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
160
162
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
161
163
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
162
- return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
164
+ return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
163
165
  }
164
166
  function attachEstimates(transactions, simulation) {
165
167
  return transactions.map((transaction) => {
@@ -180,7 +182,8 @@ function readReviewedNextTransaction(transaction) {
180
182
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
181
183
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
182
184
  typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
183
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n) {
185
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
186
+ transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
184
187
  throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
185
188
  }
186
189
  return transaction;
@@ -188,7 +191,8 @@ function readReviewedNextTransaction(transaction) {
188
191
  function matchesReviewedNextTransaction(next, reviewed) {
189
192
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
190
193
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
191
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
194
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
195
+ (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
192
196
  }
193
197
  function assertReviewedTransactionUnchanged(options, reviewed, next) {
194
198
  if (options.planned.ufg !== undefined)
@@ -297,9 +301,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
297
301
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
298
302
  if (options.buySlippageBps !== undefined)
299
303
  validateBuySlippageBps(options.buySlippageBps);
304
+ const fixed = options.fixedFinalEnvelope;
305
+ if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
306
+ fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
307
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
308
+ }
309
+ const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
300
310
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
301
311
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
302
- if (!needsMining && !needsProtection)
312
+ if (!needsMining && !needsProtection && fixed === undefined)
303
313
  return planLaunch(options);
304
314
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
305
315
  const assertDraft = () => {
@@ -308,42 +318,48 @@ export async function prepareAndPlanLifecycleLaunch(options) {
308
318
  throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
309
319
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash)
310
320
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
321
+ if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
322
+ fixed.gas !== heldEnvelope.gas || fixed.gasPrice !== heldEnvelope.gasPrice)
323
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
311
324
  };
312
- return withLifecycleReadClient(options.client, async (client) => {
313
- const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
325
+ const result = await withLifecycleReadClient(options.client, async (client) => {
326
+ const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
327
+ const chainId = await readLifecycleChainId(client);
314
328
  if (chainId !== draft.chainId)
315
329
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
316
330
  const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
317
331
  ? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
318
332
  assertDraft();
319
333
  const markets = [...draft.markets];
320
- const minedDeployments = [];
321
- for (const [marketIndex, deployment] of metadata.entries()) {
322
- if (deployment === undefined)
323
- continue;
334
+ const minedDeployments = await mineLifecycleHooks({ client, block,
335
+ miner: getKnownLifecycleDeployment({ chainId, orchestrator: draft.orchestrator })?.hookSaltMiner,
336
+ deployments: metadata.flatMap((deployment, marketIndex) => deployment === undefined ? [] : [{ marketIndex, ...deployment }]),
337
+ signal: options.signal, onProgress: options.onProgress });
338
+ for (const mined of minedDeployments) {
324
339
  assertDraft();
325
- const mined = await minePoolBoundHookSalt({
326
- deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
327
- onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
328
- });
340
+ const marketIndex = mined.marketIndex;
329
341
  const market = markets[marketIndex];
330
342
  if (!isPoolBoundV4ConfigVersion(market.configVersion))
331
343
  throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
332
344
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
333
- minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
334
345
  }
335
346
  assertDraft();
336
- // Mining is local construction, not a state admission. Economic observations
337
- // start at a fresh post-mining snapshot and share it through final protection.
338
- const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
347
+ const executionBlock = block;
339
348
  assertDraft();
349
+ const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
350
+ const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
351
+ const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
352
+ gas: heldEnvelope.gas,
353
+ gasPrice: heldEnvelope.gasPrice ?? (price > baseFeeEnvelope ? price : baseFeeEnvelope),
354
+ });
340
355
  let plan = { ...draft, markets };
341
356
  let ownedProgress;
357
+ let sizing;
342
358
  if (needsProtection) {
343
359
  // A diagnostic minimum is never returned as an admitted or executable plan.
344
360
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
345
361
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
346
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
362
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
347
363
  assertDraft();
348
364
  if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
349
365
  throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
@@ -351,8 +367,9 @@ export async function prepareAndPlanLifecycleLaunch(options) {
351
367
  // This is the real SDK-owned None observation, not an assumed new-launch
352
368
  // state. No onchain commitment existed at this exact snapshot to replace.
353
369
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
370
+ sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed]));
354
371
  }
355
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
372
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
356
373
  assertDraft();
357
374
  assertFinalPlan(planned, options.signal);
358
375
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -366,21 +383,26 @@ export async function prepareAndPlanLifecycleLaunch(options) {
366
383
  throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
367
384
  return planned;
368
385
  });
386
+ assertDraft();
387
+ assertFinalPlan(result, options.signal);
388
+ return result;
369
389
  }
370
390
  /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
371
391
  export async function planLaunch(options) {
372
- return withLifecycleReadClient(options.client, async (client) => {
392
+ const result = await withLifecycleReadClient(options.client, async (client) => {
373
393
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
374
394
  assertFinalPlan(planned, options.signal);
375
395
  return planned;
376
396
  });
397
+ assertFinalPlan(result, options.signal);
398
+ return result;
377
399
  }
378
- async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, reviewed) {
400
+ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, fixedFinalEnvelope, sizing) {
379
401
  const { plan, account, mode, ufg } = options;
380
402
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
381
403
  const launchId = hashLaunchIdentity(plan);
382
404
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
383
- pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
405
+ pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
384
406
  ]));
385
407
  if (chainId !== plan.chainId)
386
408
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
@@ -400,7 +422,10 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
400
422
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
401
423
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
402
424
  lifecycleStage(client, "plan.progress", async () => {
403
- const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
425
+ const [predictedToken] = await Promise.all([
426
+ readLifecycleTokenAtBlock(client, plan, block),
427
+ ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
428
+ ]);
404
429
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
405
430
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
406
431
  if (ufg !== undefined)
@@ -426,11 +451,9 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
426
451
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
427
452
  }
428
453
  const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
429
- await assertLifecycleBlock(client, block, plan.chainId);
430
454
  return { ...planned, hookDeployments, simulation: waiting };
431
455
  }
432
456
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
433
- await assertLifecycleBlock(client, block, plan.chainId);
434
457
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
435
458
  }
436
459
  // Topology is a creation policy, not a retroactive obstacle to recovery of
@@ -447,41 +470,29 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
447
470
  const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
448
471
  let batches = [];
449
472
  if (mode === "staged")
450
- batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
451
- : [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
452
- if (reviewed !== undefined && reviewed.kind === "prepare") {
453
- // Canonical progress fixes the start; the exact held calldata fixes its span.
454
- // Never rediscover a batch or enlarge a reviewed transaction.
455
- const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
456
- const start = Number(match?.[1]), count = Number(match?.[2]);
457
- if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length)
458
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
459
- batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
460
- }
473
+ batches = packPreparationBatches(plan, prepared, computeCap);
461
474
  let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
462
- if (reviewed !== undefined) {
463
- if (!matchesReviewedNextTransaction(transactions[0], reviewed))
464
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
465
- transactions = transactions.slice(0, 1);
475
+ if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0].kind !== "atomic")) {
476
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
466
477
  }
467
478
  if (metadata.reasons.length > 0) {
468
- await assertLifecycleBlock(client, block, plan.chainId);
469
479
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
470
480
  }
471
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
481
+ const envelope = diagnostic ? undefined : fixedFinalEnvelope;
482
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
472
483
  try {
473
- const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, reviewed);
484
+ const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, envelope);
474
485
  let simulation = await execute(transactions);
475
486
  assertFinalPlan(planned, options.signal);
476
487
  if (mode === "staged") {
477
- // Only a measurement that consumed almost its entire legal envelope
478
- // invalidates packing estimates, including gas hidden by CREATE wrappers.
479
- while (reviewed === undefined && !simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
488
+ // Only pressure at the native ceiling can invalidate packing. A smaller
489
+ // affordability/RPC/account envelope is not evidence for another wallet write.
490
+ while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
480
491
  const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
481
492
  const failedTransaction = transactions[failedIndex];
482
493
  const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
483
494
  const count = failedTransaction?.marketCount ?? 0;
484
- if (count <= 1 || step?.estimate !== undefined || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasUsed * 100n < step.gasLimit * 95n)
495
+ if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n)
485
496
  break;
486
497
  let first = 0;
487
498
  const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
@@ -497,9 +508,6 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
497
508
  else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
498
509
  simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
499
510
  }
500
- if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
501
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
502
- }
503
511
  return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
504
512
  }
505
513
  finally {
@@ -523,11 +531,10 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
523
531
  throw new Error("Active submission RPC chain differs from the reviewed transaction");
524
532
  const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
525
533
  from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
534
+ ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
526
535
  }, "latest"]), "submission gas estimate");
527
536
  if (estimated <= 0n || estimated > next.gas)
528
537
  throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
529
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
530
- throw new Error("Active submission RPC chain changed during preflight");
531
538
  assertPlanningActive(options.signal);
532
539
  }
533
540
  catch (failure) {
@@ -536,14 +543,6 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
536
543
  const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
537
544
  throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
538
545
  }
539
- const [canonical, chainId] = await Promise.all([
540
- readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
541
- lifecycleRpc(options.client, "eth_chainId"),
542
- ]);
543
- if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase())
544
- throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
545
- if (rpcQuantity(chainId, "chain ID") !== simulation.chainId)
546
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
547
546
  if (next.admission === undefined)
548
547
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
549
548
  assertPlanningActive(options.signal);
@@ -557,15 +556,22 @@ export async function buildNextTransaction(options) {
557
556
  // Snapshot only the reviewed wire fields; caller proofs and commands are never
558
557
  // reused, and final checks compare the mutable input with this one local copy.
559
558
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
560
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
561
- return withLifecycleReadClient(options.client, async (client) => {
562
- const block = await readLifecycleBlock(client);
563
- const progress = await readLaunchProgress({ ...options, client }, block);
559
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
560
+ const result = await withLifecycleReadClient(options.client, async (client) => {
561
+ const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
562
+ const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
563
+ if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
564
+ await Promise.all([
565
+ readLifecycleTokenAtBlock(client, options.planned.plan, block),
566
+ readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
567
+ resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
568
+ ]);
569
+ }
570
+ const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
564
571
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
565
572
  // Only the confirmed canonical terminal phase is terminal; a head-only
566
573
  // Active observation is not, and the confirmation/nonce/receipt gates below
567
574
  // remain in force for every nonterminal case.
568
- await assertLifecycleBlock(client, block, options.planned.chainId);
569
575
  return undefined;
570
576
  }
571
577
  if (!progress.confirmationSafe)
@@ -580,6 +586,7 @@ export async function buildNextTransaction(options) {
580
586
  assertFinalPlan(planned, options.signal);
581
587
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
582
588
  : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
589
+ transaction.nonce = progress.pendingAccountNonce;
583
590
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
584
591
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
585
592
  const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
@@ -590,17 +597,53 @@ export async function buildNextTransaction(options) {
590
597
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
591
598
  return preflightNextTransaction(options, next, simulation, reviewed);
592
599
  }
593
- const refreshed = await lifecycleStage(client, "plan", () => planLaunchWithReads({ client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations, signal: options.signal,
594
- ufg: options.planned.ufg, allowTopologyGaps: options.planned.allowTopologyGaps, allowMissingPermanentTail: options.planned.allowMissingPermanentTail }, client, block, progress, false, reviewed));
595
- assertFinalPlan(refreshed, options.signal);
596
- if (!refreshed.simulation.admitted)
597
- throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
598
- assertPlannedDeploymentBindings(options.planned, refreshed);
599
- const next = refreshed.transactions[0];
600
- if (next === undefined)
600
+ const { planned } = options;
601
+ assertFinalPlan(planned, options.signal);
602
+ const funding = progress.canonical.phase === LifecyclePhase.None
603
+ ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
604
+ : { prerequisites: [], approvals: [] };
605
+ const prepared = progress.canonical.preparedMarkets;
606
+ let batches = [];
607
+ if (planned.mode === "staged") {
608
+ let suffixStart = prepared;
609
+ const prefix = prepared > 0 ? [prepared] : [];
610
+ if (reviewed?.kind === "prepare") {
611
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
612
+ const first = Number(span?.[1]), count = Number(span?.[2]);
613
+ if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length)
614
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
615
+ prefix.push(count);
616
+ suffixStart += count;
617
+ }
618
+ const suffix = planned.transactions
619
+ .filter((transaction) => transaction.kind === "prepare" && transaction.marketStart + transaction.marketCount > suffixStart)
620
+ .map((transaction) => transaction.marketStart + transaction.marketCount - Math.max(transaction.marketStart, suffixStart));
621
+ if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
622
+ const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
623
+ const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
624
+ suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
625
+ }
626
+ batches = [...prefix, ...suffix];
627
+ }
628
+ const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
629
+ if (candidate === undefined)
601
630
  return undefined;
602
- return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
631
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed))
632
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
633
+ const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
634
+ const compute = observed?.computeGasRequired;
635
+ const sizing = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
636
+ const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
637
+ assertFinalPlan(planned, options.signal);
638
+ if (!simulation.admitted)
639
+ throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
640
+ const next = attachEstimates([candidate], simulation)[0];
641
+ return preflightNextTransaction(options, next, simulation, reviewed);
603
642
  });
643
+ assertFinalPlan(options.planned, options.signal);
644
+ if (result !== undefined)
645
+ assertReviewedTransactionUnchanged(options, reviewed, result);
646
+ return result;
604
647
  }
605
648
  /** Re-simulates only canonical unfinished work with the same submitted receipt
606
649
  * evidence and confirmation depth as build-next, never silently dropping context. */