@black-market/sdk 0.15.0 → 0.16.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 (113) hide show
  1. package/README.md +10 -8
  2. package/dist/lifecycle/auto-compound.d.ts +21 -0
  3. package/dist/lifecycle/auto-compound.d.ts.map +1 -0
  4. package/dist/lifecycle/auto-compound.js +34 -0
  5. package/dist/lifecycle/auto-compound.js.map +1 -0
  6. package/dist/lifecycle/calibration.d.ts +2 -4
  7. package/dist/lifecycle/calibration.d.ts.map +1 -1
  8. package/dist/lifecycle/calibration.js +1 -26
  9. package/dist/lifecycle/calibration.js.map +1 -1
  10. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
  11. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
  12. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
  13. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
  14. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
  15. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
  16. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
  17. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
  18. package/dist/lifecycle/deployments/raeto-dynamic-pool-v1-20261029.d.ts +71 -0
  19. package/dist/lifecycle/deployments/raeto-dynamic-pool-v1-20261029.d.ts.map +1 -0
  20. package/dist/lifecycle/deployments/raeto-dynamic-pool-v1-20261029.js +73 -0
  21. package/dist/lifecycle/deployments/raeto-dynamic-pool-v1-20261029.js.map +1 -0
  22. package/dist/lifecycle/deployments/raeto-fixed-pool-v1-20261029.d.ts +71 -0
  23. package/dist/lifecycle/deployments/raeto-fixed-pool-v1-20261029.d.ts.map +1 -0
  24. package/dist/lifecycle/deployments/raeto-fixed-pool-v1-20261029.js +73 -0
  25. package/dist/lifecycle/deployments/raeto-fixed-pool-v1-20261029.js.map +1 -0
  26. package/dist/lifecycle/gas-profile.d.ts +17 -13
  27. package/dist/lifecycle/gas-profile.d.ts.map +1 -1
  28. package/dist/lifecycle/gas-profile.js +133 -113
  29. package/dist/lifecycle/gas-profile.js.map +1 -1
  30. package/dist/lifecycle/index.d.ts +6 -4
  31. package/dist/lifecycle/index.d.ts.map +1 -1
  32. package/dist/lifecycle/index.js +6 -4
  33. package/dist/lifecycle/index.js.map +1 -1
  34. package/dist/lifecycle/markets.d.ts +4 -0
  35. package/dist/lifecycle/markets.d.ts.map +1 -1
  36. package/dist/lifecycle/markets.js +58 -3
  37. package/dist/lifecycle/markets.js.map +1 -1
  38. package/dist/lifecycle/opening-buys.js +3 -3
  39. package/dist/lifecycle/opening-buys.js.map +1 -1
  40. package/dist/lifecycle/planner.d.ts +6 -9
  41. package/dist/lifecycle/planner.d.ts.map +1 -1
  42. package/dist/lifecycle/planner.js +122 -208
  43. package/dist/lifecycle/planner.js.map +1 -1
  44. package/dist/lifecycle/presets.d.ts +9 -3
  45. package/dist/lifecycle/presets.d.ts.map +1 -1
  46. package/dist/lifecycle/presets.js +17 -1
  47. package/dist/lifecycle/presets.js.map +1 -1
  48. package/dist/lifecycle/raeto-abi.d.ts +175 -0
  49. package/dist/lifecycle/raeto-abi.d.ts.map +1 -0
  50. package/dist/lifecycle/raeto-abi.js +224 -0
  51. package/dist/lifecycle/raeto-abi.js.map +1 -0
  52. package/dist/lifecycle/raeto.d.ts +21 -0
  53. package/dist/lifecycle/raeto.d.ts.map +1 -0
  54. package/dist/lifecycle/raeto.js +59 -0
  55. package/dist/lifecycle/raeto.js.map +1 -0
  56. package/dist/lifecycle/rpc.d.ts +3 -13
  57. package/dist/lifecycle/rpc.d.ts.map +1 -1
  58. package/dist/lifecycle/rpc.js +11 -37
  59. package/dist/lifecycle/rpc.js.map +1 -1
  60. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  61. package/dist/lifecycle/snapshot.js +1 -2
  62. package/dist/lifecycle/snapshot.js.map +1 -1
  63. package/dist/lifecycle/transactions.d.ts +10 -3
  64. package/dist/lifecycle/transactions.d.ts.map +1 -1
  65. package/dist/lifecycle/transactions.js +47 -10
  66. package/dist/lifecycle/transactions.js.map +1 -1
  67. package/dist/lifecycle/types.d.ts +32 -107
  68. package/dist/lifecycle/types.d.ts.map +1 -1
  69. package/dist/lifecycle/types.js +3 -3
  70. package/dist/lifecycle/types.js.map +1 -1
  71. package/dist/lifecycle/ufg.d.ts +1 -1
  72. package/dist/lifecycle/ufg.d.ts.map +1 -1
  73. package/dist/lifecycle/ufg.js +1 -1
  74. package/dist/lifecycle/ufg.js.map +1 -1
  75. package/docs/launch-lifecycle.md +113 -1
  76. package/docs/lifecycle-gas-profiles.md +141 -24
  77. package/docs/reference.md +112 -3
  78. package/examples/.env.example +0 -3
  79. package/examples/README.md +11 -11
  80. package/examples/launch-example-support.ts +4 -9
  81. package/examples/launch-example.ts +12 -28
  82. package/examples/launch-lifecycle-smoke.ts +10 -22
  83. package/examples/launch.ts +8 -11
  84. package/package.json +1 -1
  85. package/src/lifecycle/auto-compound.ts +38 -0
  86. package/src/lifecycle/calibration.ts +3 -23
  87. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  88. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  89. package/src/lifecycle/deployments/raeto-dynamic-pool-v1-20261029.ts +74 -0
  90. package/src/lifecycle/deployments/raeto-fixed-pool-v1-20261029.ts +74 -0
  91. package/src/lifecycle/gas-profile.ts +125 -104
  92. package/src/lifecycle/index.ts +6 -4
  93. package/src/lifecycle/markets.ts +47 -3
  94. package/src/lifecycle/opening-buys.ts +3 -3
  95. package/src/lifecycle/planner.ts +116 -191
  96. package/src/lifecycle/presets.ts +17 -1
  97. package/src/lifecycle/raeto-abi.ts +223 -0
  98. package/src/lifecycle/raeto.ts +61 -0
  99. package/src/lifecycle/rpc.ts +10 -32
  100. package/src/lifecycle/snapshot.ts +1 -2
  101. package/src/lifecycle/transactions.ts +46 -9
  102. package/src/lifecycle/types.ts +38 -87
  103. package/src/lifecycle/ufg.ts +1 -1
  104. package/dist/lifecycle/fork.d.ts +0 -15
  105. package/dist/lifecycle/fork.d.ts.map +0 -1
  106. package/dist/lifecycle/fork.js +0 -36
  107. package/dist/lifecycle/fork.js.map +0 -1
  108. package/dist/lifecycle/simulation.d.ts +0 -17
  109. package/dist/lifecycle/simulation.d.ts.map +0 -1
  110. package/dist/lifecycle/simulation.js +0 -605
  111. package/dist/lifecycle/simulation.js.map +0 -1
  112. package/src/lifecycle/fork.ts +0 -36
  113. package/src/lifecycle/simulation.ts +0 -543
@@ -2,13 +2,12 @@ import { encodeFunctionData, keccak256, toHex, zeroAddress } from "viem";
2
2
  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
- import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
5
+ import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
6
6
  import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
7
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
8
- import { assertLifecycleReadClientOpen, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
- import { observedFields, prepareLaunchSimulation } from "./simulation.js";
8
+ import { lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
10
9
  import { LifecyclePlanningError } from "./types.js";
11
- import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
10
+ import { deriveKnownPoolBoundHookDeployment, validateBuySlippageBps } from "./calibration.js";
12
11
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
12
  import { buildLifecycleCommand, packPreparationBatches } from "./transactions.js";
14
13
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
@@ -17,8 +16,43 @@ import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
16
  import { readLifecyclePlanningBlock } from "./snapshot.js";
18
17
  import { mineLifecycleHooks } from "./mining.js";
19
18
  import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
20
- import { deriveLifecycleLaunchCapacity, profileLifecycleTransactions } from "./gas-profile.js";
19
+ import { assessLifecycleTransactions, deriveLifecycleLaunchCapacity } from "./gas-profile.js";
20
+ import { isRaetoProfile, RAETO_QUOTE_ASSETS } from "./raeto.js";
21
21
  const defaultPlanReadContext = {};
22
+ function observedFields(value) {
23
+ const keys = Object.keys(value);
24
+ const values = keys.map((key) => value[key]);
25
+ return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
26
+ }
27
+ async function selectLaunchMode(requested, plan, capacity, limits, client, block) {
28
+ if (requested !== "atomic" || plan.markets.length !== 1 || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
29
+ capacity?.atomic === undefined || capacity.bounds.atomicComputeGasUpperBound === undefined)
30
+ return "staged";
31
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(capacity.atomic, await readNitroPrices(client, block)).posterGas : 0n;
32
+ return capacity.bounds.atomicComputeGasUpperBound + poster <= limits.executionGasCeiling ? "atomic" : "staged";
33
+ }
34
+ async function preparationBatches(plan, prepared, ufg, limits, client, block) {
35
+ if (prepared === plan.markets.length)
36
+ return prepared === 0 ? [] : [prepared];
37
+ const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
38
+ const sample = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }, route);
39
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(sample, await readNitroPrices(client, block)).posterGas : 0n;
40
+ return packPreparationBatches(plan, prepared, limits.executionGasCeiling - poster, limits.headroomBps);
41
+ }
42
+ /** Preserve Raeto's outer quote/profile binding even on known-preset recovery. */
43
+ function validateRaetoMarketContext(market, config) {
44
+ if (!isRaetoProfile(market.profileId))
45
+ return;
46
+ if (market.configVersion !== 6 || !RAETO_QUOTE_ASSETS.some((quote) => quote.toLowerCase() === market.quoteAsset.toLowerCase())) {
47
+ throw new LifecyclePlanningError("INVALID_RAETO_CONFIG", "Raeto requires config6 and a WETH or USDG quote");
48
+ }
49
+ // Normal planning supplies its already decoded config. Continuation must also
50
+ // reject malformed fee/share tails before reading or building its next command.
51
+ const decoded = config ?? decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
52
+ if (decoded.version !== 6 || decoded.profileId.toLowerCase() !== market.profileId.toLowerCase()) {
53
+ throw new LifecyclePlanningError("INVALID_RAETO_CONFIG", "Raeto config must bind its market's Raeto profile");
54
+ }
55
+ }
22
56
  function validatePlanShape(plan) {
23
57
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
24
58
  const commitment = hashLaunchPlan(plan);
@@ -64,6 +98,7 @@ function validatePlanShape(plan) {
64
98
  config = decodeV4LifecycleMarketConfig(market.config);
65
99
  else if (isPoolBoundV4ConfigVersion(market.configVersion))
66
100
  config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
101
+ validateRaetoMarketContext(market, config);
67
102
  if (config !== undefined) {
68
103
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase())
69
104
  throw new LifecyclePlanningError("PROFILE_TERMS_MISMATCH", "Inner config profile differs from the committed market profile");
@@ -176,16 +211,14 @@ function orderedTransactions(plan, mode, progress, approvals, batches, ufg, capa
176
211
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
177
212
  return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
178
213
  }
179
- function attachEstimates(transactions, simulation) {
214
+ function attachEstimates(transactions, assessment) {
180
215
  return transactions.map((transaction, index) => {
181
216
  if (index !== 0)
182
217
  return transaction;
183
- const admission = { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit,
184
- blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits };
185
218
  if (transaction.kind === "approve" || transaction.kind === "approve-reset")
186
- return { ...transaction, admission };
187
- const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate ?? simulation.estimate;
188
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission };
219
+ return { ...transaction, assessment };
220
+ const estimate = assessment.estimate;
221
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, assessment };
189
222
  });
190
223
  }
191
224
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"];
@@ -215,16 +248,16 @@ function assertReviewedTransactionUnchanged(options, reviewed, next) {
215
248
  if (options.planned.ufg !== undefined)
216
249
  assertUfgLaunchAccount(options.planned.plan, options.planned.account);
217
250
  if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
218
- throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
251
+ throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
219
252
  }
220
253
  if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
221
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
254
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
222
255
  }
223
256
  const current = readReviewedNextTransaction(options.reviewedTransaction);
224
257
  if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
225
258
  reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
226
259
  (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
227
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
260
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
228
261
  }
229
262
  }
230
263
  async function readTokenFactoryBinding(client, orchestrator, block) {
@@ -238,21 +271,6 @@ async function readTokenFactoryBinding(client, orchestrator, block) {
238
271
  throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
239
272
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
240
273
  }
241
- function assertPlannedDeploymentBindings(stored, current) {
242
- if (hashLaunchPlan(stored.plan).toLowerCase() !== stored.planHash.toLowerCase() || current.planHash.toLowerCase() !== stored.planHash.toLowerCase() || current.launchId.toLowerCase() !== stored.launchId.toLowerCase())
243
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its finalized commitment");
244
- if (stored.ufg === undefined ? current.ufg !== undefined : current.ufg === undefined || hashUfgLaunchConfig(stored.ufg.config) !== stored.ufg.configHash || current.ufg.configHash !== stored.ufg.configHash)
245
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored UFG route or treasury config no longer matches its finalized commitment");
246
- if (current.predictedToken.toLowerCase() !== stored.predictedToken.toLowerCase() || current.tokenFactory.toLowerCase() !== stored.tokenFactory.toLowerCase() || current.tokenFactoryCodeHash.toLowerCase() !== stored.tokenFactoryCodeHash.toLowerCase())
247
- throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Final token prediction or its immutable factory code binding changed before execution");
248
- if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled)
249
- return;
250
- if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
251
- const fresh = current.hookDeployments[index];
252
- return fresh === undefined || fresh.marketIndex !== deployment.marketIndex || fresh.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || fresh.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() || fresh.salt.toLowerCase() !== deployment.salt.toLowerCase() || fresh.predictedHook.toLowerCase() !== deployment.predictedHook.toLowerCase();
253
- }))
254
- throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
255
- }
256
274
  function assertPlanningActive(signal) {
257
275
  if (signal?.aborted)
258
276
  throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
@@ -312,21 +330,8 @@ function assertFinalPlan(planned, signal) {
312
330
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash)
313
331
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
314
332
  }
315
- async function evaluateLaunchTransactions(options, transactions, block, controls = defaultPlanReadContext) {
316
- const profile = controls.diagnostic ? undefined : await profileLifecycleTransactions(options, transactions, block, controls.finalEnvelope, controls.openingBuys, controls.capacity);
317
- if (!controls.forceSimulation && profile?.profileOnly)
318
- return profile.simulation;
319
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, controls.immediate);
320
- try {
321
- const simulation = controls.diagnostic ? await prepared.measure(transactions) : await prepared(transactions, controls.finalEnvelope);
322
- return profile === undefined ? simulation : { ...simulation, gasProfile: profile.simulation.gasProfile };
323
- }
324
- finally {
325
- prepared.close();
326
- }
327
- }
328
- /** One owner constructs and mines, optionally measures actual ordered buy outputs,
329
- * then proves only the final protected commitment. No cached execution certificates. */
333
+ /** One owner mines constructor-bound salts and protects ordered buys with exact
334
+ * local curve math. Launch calldata is never executed during planning. */
330
335
  export async function prepareAndPlanLifecycleLaunch(options) {
331
336
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
332
337
  if (options.buySlippageBps !== undefined)
@@ -366,9 +371,13 @@ export async function prepareAndPlanLifecycleLaunch(options) {
366
371
  }
367
372
  assertDraft();
368
373
  let plan = { ...draft, markets };
369
- let ownedProgress;
370
374
  let openingBuys;
371
- if (needsProtection && hasProfiledOpeningMath(plan)) {
375
+ if (needsProtection) {
376
+ if (!hasProfiledOpeningMath(plan))
377
+ throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Exact opening-buy math is unavailable for this profile. Use a supported profile or update the SDK.");
378
+ const head = await readLifecycleHeadProgress(client, plan, options.account, block);
379
+ if (head.phase !== LifecyclePhase.None)
380
+ throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
372
381
  const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
373
382
  openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
374
383
  const factor = BigInt(10000 - options.buySlippageBps);
@@ -379,20 +388,7 @@ export async function prepareAndPlanLifecycleLaunch(options) {
379
388
  return { ...buy, minTokenOut };
380
389
  }) };
381
390
  }
382
- else if (needsProtection) {
383
- // A diagnostic minimum is never returned as an admitted or executable plan.
384
- // Price guards, funding minima, recipients and all ordered budgets stay exact.
385
- const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
386
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
387
- assertDraft();
388
- if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
389
- throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
390
- plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps);
391
- // This is the real SDK-owned None observation, not an assumed new-launch
392
- // state. No onchain commitment existed at this exact snapshot to replace.
393
- ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
394
- }
395
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, progress: ownedProgress, openingBuys }));
391
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
396
392
  assertDraft();
397
393
  assertFinalPlan(planned, options.signal);
398
394
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -402,15 +398,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
402
398
  throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
403
399
  }
404
400
  }
405
- if (needsProtection && !planned.simulation.admitted)
406
- throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
401
+ if (needsProtection && !planned.assessment.ready)
402
+ throw new LifecyclePlanningError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "The launch could not be prepared. Refresh the review before continuing.", planned.assessment);
407
403
  return planned;
408
404
  });
409
405
  assertDraft();
410
406
  assertFinalPlan(result, options.signal);
411
407
  return result;
412
408
  }
413
- /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
409
+ /** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
414
410
  export async function planLaunch(options) {
415
411
  const result = await withLifecycleReadClient(options.client, async (client) => {
416
412
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
@@ -421,9 +417,8 @@ export async function planLaunch(options) {
421
417
  return result;
422
418
  }
423
419
  async function planLaunchWithReads(options, client, readContext = defaultPlanReadContext) {
424
- const { block: pinnedBlock, progress: ownedProgress, diagnostic = false, openingBuys, forceSimulation = false } = readContext;
425
- const { plan, account, mode: requestedMode, ufg } = options;
426
- let mode = requestedMode;
420
+ const { block: pinnedBlock, openingBuys } = readContext;
421
+ const { plan, account, ufg } = options;
427
422
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
428
423
  const launchId = hashLaunchIdentity(plan);
429
424
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
@@ -431,39 +426,27 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
431
426
  ]));
432
427
  if (chainId !== plan.chainId)
433
428
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
434
- // Only preparation/build-next can supply freshly observed progress from this
435
- // still-open SDK invocation. Stored/query DTOs never enter this continuation.
436
- if (ownedProgress !== undefined) {
437
- if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
438
- ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
439
- ownedProgress.confirmationDepth !== (options.confirmations ?? 1))
440
- throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
441
- if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase())
442
- throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
443
- }
444
429
  assertPlanningActive(options.signal);
445
430
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
446
431
  const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
447
432
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
448
433
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
449
434
  lifecycleStage(client, "plan.progress", async () => {
450
- const [predictedToken] = await Promise.all([
451
- readLifecycleTokenAtBlock(client, plan, block),
452
- ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
435
+ const [predictedToken, head] = await Promise.all([
436
+ readLifecycleTokenAtBlock(client, plan, block), readLifecycleHeadProgress(client, plan, account, block),
453
437
  ]);
438
+ const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" : "atomic";
454
439
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
455
440
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
456
441
  if (ufg !== undefined)
457
442
  validateUfgLaunchPlan(plan, ufg.config, predictedToken);
458
- const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
459
- if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase())
460
- throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
443
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
461
444
  return { identity, progress };
462
445
  }),
463
446
  ]));
464
447
  assertPlanningActive(options.signal);
465
- const placeholder = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
466
- let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
448
+ const placeholder = { ready: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits };
449
+ let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, assessment: placeholder, limits: options.limits,
467
450
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
468
451
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
469
452
  if (!progress.confirmationSafe || receiptsPending) {
@@ -475,14 +458,13 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
475
458
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
476
459
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
477
460
  }
478
- const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical launch progress has not reached the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
479
- return { ...planned, hookDeployments, simulation: waiting };
461
+ return { ...planned, hookDeployments, assessment: { ...placeholder,
462
+ code: !progress.confirmationSafe ? "UNCONFIRMED_STATE" : "RECEIPT_PENDING",
463
+ reason: !progress.confirmationSafe ? "Wait for the selected launch confirmation depth before continuing." : "Wait for the pending transaction to confirm or resolve its replacement." } };
480
464
  }
481
465
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
482
- return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
466
+ return { ...planned, assessment: { ...placeholder, ready: true } };
483
467
  }
484
- // Topology is a creation policy, not a retroactive obstacle to recovery of
485
- // immutable onchain commitments. Waiting and terminal launches returned above.
486
468
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None)
487
469
  validatePlanTopology(options, marketConfigs, identity.predictedToken);
488
470
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
@@ -490,129 +472,84 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
490
472
  readPlanMetadata(client, plan, identity.predictedToken, block),
491
473
  ]));
492
474
  assertFinalPlan(planned, options.signal);
493
- const capacity = limits.protocol === "nitro" && limits.arbOSVersion === 61n
494
- ? deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys) : undefined;
495
- if (!forceSimulation && progress.canonical.phase === LifecyclePhase.None && mode === "atomic" && capacity !== undefined) {
496
- const prices = await readNitroPrices(client, block);
497
- if (capacity.bounds.atomicComputeGasUpperBound + nitroPosterGasBound(capacity.atomic, prices).posterGas > limits.executionGasCeiling)
498
- mode = "staged";
499
- }
475
+ const capacity = deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys);
476
+ const mode = progress.canonical.phase === LifecyclePhase.None
477
+ ? await selectLaunchMode(options.mode, plan, capacity, limits, client, block) : identity.mode;
500
478
  planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
501
- const prepared = progress.canonical.preparedMarkets, computeCap = limits.executionGasCeiling;
502
- let batches = mode === "staged" ? packPreparationBatches(plan, prepared, computeCap) : [];
503
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
504
- if (metadata.reasons.length > 0) {
505
- 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) };
506
- }
507
- const execute = (candidate, quotes = openingBuys) => evaluateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, candidate, block, { diagnostic, openingBuys: quotes, capacity, forceSimulation });
508
- let simulation = await execute(transactions);
509
- assertFinalPlan(planned, options.signal);
510
- if (!diagnostic && !forceSimulation && requestedMode === "atomic" && mode === "atomic" && progress.canonical.phase === LifecyclePhase.None &&
511
- simulation.executionProof === "failed" && simulation.failureCategory === "capacity" && simulation.capacityConstraint === "compute") {
512
- mode = "staged";
513
- planned = { ...planned, mode };
514
- batches = packPreparationBatches(plan, prepared, computeCap);
515
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
516
- simulation = await execute(transactions);
517
- assertFinalPlan(planned, options.signal);
518
- }
519
- if (mode === "staged") {
520
- // Only pressure at the native ceiling can invalidate packing. A smaller
521
- // affordability/RPC/account envelope is not evidence for another wallet write.
522
- while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
523
- const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
524
- const failedTransaction = transactions[failedIndex];
525
- const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
526
- const count = failedTransaction?.marketCount ?? 0;
527
- if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n)
528
- break;
529
- let first = 0;
530
- const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
531
- const left = Math.ceil(count / 2);
532
- batches.splice(batchIndex, 1, left, count - left);
533
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
534
- simulation = await execute(transactions, simulation.gasProfile?.initialBuys);
535
- assertFinalPlan(planned, options.signal);
536
- }
537
- if (!simulation.admitted && simulation.failedTransactionId === "activate" && simulation.failureCategory === "capacity")
538
- simulation = {
539
- ...simulation, capacityConstraint: "activation", reason: "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling."
540
- };
479
+ if (metadata.reasons.length > 0)
480
+ return { ...planned, assessment: { ...placeholder, code: "PROFILE_UNAVAILABLE", failureCategory: "semantic",
481
+ reason: "The selected launch profile could not be read or is unavailable. Refresh the review before continuing." } };
482
+ let batches;
483
+ try {
484
+ batches = mode === "staged" ? await preparationBatches(plan, progress.canonical.preparedMarkets, ufg, limits, client, block) : [];
541
485
  }
542
- else if (!diagnostic && !simulation.admitted) {
543
- simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
486
+ catch (failure) {
487
+ if (!(failure instanceof LifecyclePlanningError) || failure.code !== "PREPARATION_GAS_LIMIT")
488
+ throw failure;
489
+ return { ...planned, assessment: { ...placeholder, code: failure.code, reason: failure.message, failureCategory: "capacity", capacityConstraint: "preparation" } };
544
490
  }
545
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
491
+ const transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
492
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: options.limits, signal: options.signal }, transactions, block, undefined, openingBuys, capacity);
493
+ assertFinalPlan(planned, options.signal);
494
+ return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
546
495
  }
547
496
  function finalizeNextTransaction(options, next, reviewed) {
548
497
  assertPlanningActive(options.signal);
549
498
  assertReviewedTransactionUnchanged(options, reviewed, next);
550
499
  return { ...next, dependencies: [] };
551
500
  }
552
- /** Recovery observes real canonical receipts, then evaluates only unfinished
553
- * execution; the wallet owns its nonce and assignment at send time. */
501
+ /** Recovery observes actual canonical receipts, then constructs and budgets only
502
+ * the next command. No launch transaction is executed during planning. */
554
503
  export async function buildNextTransaction(options) {
504
+ if (options.action !== "cancel")
505
+ for (const market of options.planned.plan.markets)
506
+ validateRaetoMarketContext(market);
555
507
  const original = options.planned, matches = observedFields(original);
556
- const fullReviewAdmitted = original.simulation?.admitted === true;
557
508
  const assertInput = () => {
558
509
  if (options.planned !== original || !matches())
559
510
  throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
560
511
  assertFinalPlan(original, options.signal);
561
512
  };
562
513
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
563
- // Snapshot only the reviewed wire fields; caller proofs and commands are never
564
- // reused, and final checks compare the mutable input with this one local copy.
565
514
  const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
566
515
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
567
516
  from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
568
517
  const envelope = reviewed === undefined || walletApproval ? undefined : reviewed;
569
518
  const result = await withLifecycleReadClient(options.client, async (client) => {
570
519
  registerLifecycleFinalCheck(client, assertInput);
571
- const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
572
- const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
573
- if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
574
- await Promise.all([
575
- readLifecycleTokenAtBlock(client, options.planned.plan, block),
576
- readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
577
- resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
578
- ]);
579
- }
520
+ const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
521
+ const limitSource = options.limits === undefined ? original.limits : options.limits;
580
522
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
581
- if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
582
- // Only the confirmed canonical terminal phase is terminal; a head-only
583
- // Active observation is not, and the launch-state/receipt confirmation
584
- // policy below remains in force for every nonterminal case.
523
+ if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
585
524
  return undefined;
586
- }
587
525
  if (!progress.confirmationSafe)
588
- throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
526
+ throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected launch confirmation depth before continuing.");
589
527
  if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed"))
590
- throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
528
+ throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for the pending transaction to confirm or resolve its replacement.");
591
529
  assertPlanningActive(options.signal);
530
+ const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
592
531
  if (options.action === "cancel") {
593
532
  if (progress.canonical.phase === LifecyclePhase.None)
594
533
  throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
595
- const { planned } = options;
596
- assertFinalPlan(planned, options.signal);
534
+ const planned = { ...original, progress };
597
535
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
598
536
  : { 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 }] };
599
537
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
600
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
601
- const simulation = await evaluateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, { finalEnvelope: envelope, immediate: true });
602
- if (!simulation.admitted)
603
- throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
604
- const next = attachEstimates([transaction], simulation)[0];
605
- if (next === undefined)
606
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
607
- return finalizeNextTransaction(options, next, reviewed);
538
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current cancellation");
539
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [transaction], block, envelope);
540
+ if (!assessment.ready)
541
+ throw new LifecyclePlanningError(assessment.code ?? "CANCEL_NOT_READY", assessment.reason ?? "Cancellation could not be prepared. Refresh before continuing.", assessment);
542
+ return finalizeNextTransaction(options, attachEstimates([transaction], assessment)[0], reviewed);
608
543
  }
609
- const { planned } = options;
610
- assertFinalPlan(planned, options.signal);
611
- if (progress.canonical.phase === LifecyclePhase.None && !fullReviewAdmitted)
612
- throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", "Complete an admitted full launch review before starting or approving funding; next-only construction does not replan the remaining sequence");
613
544
  const funding = progress.canonical.phase === LifecyclePhase.None
614
- ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
545
+ ? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
615
546
  : { prerequisites: [], approvals: [] };
547
+ const planned = { ...original, progress, prerequisites: funding.prerequisites };
548
+ const capacity = deriveLifecycleLaunchCapacity({ plan: planned.plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps });
549
+ if (progress.canonical.phase === LifecyclePhase.None &&
550
+ await selectLaunchMode(planned.mode, planned.plan, capacity, limits, client, block) !== planned.mode) {
551
+ throw new LifecyclePlanningError("MODE_REVIEW_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.");
552
+ }
616
553
  let candidate = funding.approvals[0];
617
554
  if (candidate === undefined) {
618
555
  const prepared = progress.canonical.preparedMarkets;
@@ -623,23 +560,16 @@ export async function buildNextTransaction(options) {
623
560
  let count;
624
561
  if (reviewed?.kind === "prepare") {
625
562
  const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
626
- const first = Number(span?.[1]);
563
+ if (Number(span?.[1]) !== prepared)
564
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
627
565
  count = Number(span?.[2]);
628
- if (first !== prepared)
629
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
630
566
  }
631
567
  else {
632
- const stored = planned.transactions.find((transaction) => transaction.kind === "prepare" && transaction.marketStart <= prepared && transaction.marketStart + transaction.marketCount > prepared);
633
- if (stored !== undefined)
634
- count = stored.marketStart + stored.marketCount - prepared;
635
- else {
636
- const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
637
- const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
638
- count = packPreparationBatches(planned.plan, prepared, cap)[prepared > 0 ? 1 : 0];
639
- }
568
+ const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
569
+ count = batches[prepared > 0 ? 1 : 0];
640
570
  }
641
571
  if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length)
642
- throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must be a positive contiguous canonical market span");
572
+ throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must cover a positive contiguous unfinished market span");
643
573
  command = { kind: "prepare", first: prepared, count };
644
574
  }
645
575
  else
@@ -647,32 +577,16 @@ export async function buildNextTransaction(options) {
647
577
  candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
648
578
  }
649
579
  if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed))
650
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
651
- const simulation = await evaluateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, { finalEnvelope: envelope, immediate: true });
580
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current next step");
581
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [candidate], block, envelope, undefined, capacity);
652
582
  assertFinalPlan(planned, options.signal);
653
- if (!simulation.admitted)
654
- throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
655
- const next = attachEstimates([candidate], simulation)[0];
656
- return finalizeNextTransaction(options, next, reviewed);
583
+ if (!assessment.ready)
584
+ throw new LifecyclePlanningError(assessment.code ?? "PLAN_NOT_READY", assessment.reason ?? "The next launch step could not be prepared. Refresh before continuing.", assessment);
585
+ return finalizeNextTransaction(options, attachEstimates([candidate], assessment)[0], reviewed);
657
586
  });
658
587
  assertInput();
659
588
  if (result !== undefined)
660
589
  assertReviewedTransactionUnchanged(options, reviewed, result);
661
590
  return result;
662
591
  }
663
- /** Re-simulates only canonical unfinished work with the same submitted receipt
664
- * evidence and confirmation depth as build-next, never silently dropping context. */
665
- export async function simulateLaunchPlan(options) {
666
- const { client, planned } = options;
667
- if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
668
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
669
- const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
670
- client, plan: planned.plan, account: planned.account, mode: planned.mode,
671
- limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
672
- receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
673
- ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
674
- }, readClient, { forceSimulation: true }));
675
- assertPlannedDeploymentBindings(planned, current);
676
- return current.simulation;
677
- }
678
592
  //# sourceMappingURL=planner.js.map