@black-market/sdk 0.15.0 → 0.15.1

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 (93) hide show
  1. package/README.md +9 -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/gas-profile.d.ts +17 -13
  19. package/dist/lifecycle/gas-profile.d.ts.map +1 -1
  20. package/dist/lifecycle/gas-profile.js +125 -115
  21. package/dist/lifecycle/gas-profile.js.map +1 -1
  22. package/dist/lifecycle/index.d.ts +4 -4
  23. package/dist/lifecycle/index.d.ts.map +1 -1
  24. package/dist/lifecycle/index.js +4 -4
  25. package/dist/lifecycle/index.js.map +1 -1
  26. package/dist/lifecycle/markets.d.ts +2 -0
  27. package/dist/lifecycle/markets.d.ts.map +1 -1
  28. package/dist/lifecycle/markets.js +30 -3
  29. package/dist/lifecycle/markets.js.map +1 -1
  30. package/dist/lifecycle/opening-buys.js +1 -1
  31. package/dist/lifecycle/opening-buys.js.map +1 -1
  32. package/dist/lifecycle/planner.d.ts +6 -9
  33. package/dist/lifecycle/planner.d.ts.map +1 -1
  34. package/dist/lifecycle/planner.js +102 -207
  35. package/dist/lifecycle/planner.js.map +1 -1
  36. package/dist/lifecycle/presets.d.ts +7 -3
  37. package/dist/lifecycle/presets.d.ts.map +1 -1
  38. package/dist/lifecycle/presets.js +10 -1
  39. package/dist/lifecycle/presets.js.map +1 -1
  40. package/dist/lifecycle/rpc.d.ts +3 -13
  41. package/dist/lifecycle/rpc.d.ts.map +1 -1
  42. package/dist/lifecycle/rpc.js +11 -37
  43. package/dist/lifecycle/rpc.js.map +1 -1
  44. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  45. package/dist/lifecycle/snapshot.js +1 -2
  46. package/dist/lifecycle/snapshot.js.map +1 -1
  47. package/dist/lifecycle/transactions.d.ts +10 -3
  48. package/dist/lifecycle/transactions.d.ts.map +1 -1
  49. package/dist/lifecycle/transactions.js +40 -10
  50. package/dist/lifecycle/transactions.js.map +1 -1
  51. package/dist/lifecycle/types.d.ts +32 -107
  52. package/dist/lifecycle/types.d.ts.map +1 -1
  53. package/dist/lifecycle/types.js +3 -3
  54. package/dist/lifecycle/types.js.map +1 -1
  55. package/dist/lifecycle/ufg.d.ts +1 -1
  56. package/dist/lifecycle/ufg.d.ts.map +1 -1
  57. package/dist/lifecycle/ufg.js +1 -1
  58. package/dist/lifecycle/ufg.js.map +1 -1
  59. package/docs/launch-lifecycle.md +43 -0
  60. package/docs/lifecycle-gas-profiles.md +1 -1
  61. package/docs/reference.md +56 -2
  62. package/examples/.env.example +0 -3
  63. package/examples/README.md +10 -10
  64. package/examples/launch-example-support.ts +4 -9
  65. package/examples/launch-example.ts +12 -28
  66. package/examples/launch-lifecycle-smoke.ts +10 -22
  67. package/examples/launch.ts +8 -11
  68. package/package.json +1 -1
  69. package/src/lifecycle/auto-compound.ts +38 -0
  70. package/src/lifecycle/calibration.ts +3 -23
  71. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  72. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  73. package/src/lifecycle/gas-profile.ts +117 -106
  74. package/src/lifecycle/index.ts +4 -4
  75. package/src/lifecycle/markets.ts +25 -3
  76. package/src/lifecycle/opening-buys.ts +1 -1
  77. package/src/lifecycle/planner.ts +98 -190
  78. package/src/lifecycle/presets.ts +10 -1
  79. package/src/lifecycle/rpc.ts +10 -32
  80. package/src/lifecycle/snapshot.ts +1 -2
  81. package/src/lifecycle/transactions.ts +39 -9
  82. package/src/lifecycle/types.ts +38 -87
  83. package/src/lifecycle/ufg.ts +1 -1
  84. package/dist/lifecycle/fork.d.ts +0 -15
  85. package/dist/lifecycle/fork.d.ts.map +0 -1
  86. package/dist/lifecycle/fork.js +0 -36
  87. package/dist/lifecycle/fork.js.map +0 -1
  88. package/dist/lifecycle/simulation.d.ts +0 -17
  89. package/dist/lifecycle/simulation.d.ts.map +0 -1
  90. package/dist/lifecycle/simulation.js +0 -605
  91. package/dist/lifecycle/simulation.js.map +0 -1
  92. package/src/lifecycle/fork.ts +0 -36
  93. package/src/lifecycle/simulation.ts +0 -543
@@ -5,10 +5,9 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./ab
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_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,28 @@ 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";
21
20
  const defaultPlanReadContext = {};
21
+ function observedFields(value) {
22
+ const keys = Object.keys(value);
23
+ const values = keys.map((key) => value[key]);
24
+ return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
25
+ }
26
+ async function selectLaunchMode(requested, plan, capacity, limits, client, block) {
27
+ if (requested !== "atomic" || plan.markets.length !== 1 || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
28
+ capacity?.atomic === undefined || capacity.bounds.atomicComputeGasUpperBound === undefined)
29
+ return "staged";
30
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(capacity.atomic, await readNitroPrices(client, block)).posterGas : 0n;
31
+ return capacity.bounds.atomicComputeGasUpperBound + poster <= limits.executionGasCeiling ? "atomic" : "staged";
32
+ }
33
+ async function preparationBatches(plan, prepared, ufg, limits, client, block) {
34
+ if (prepared === plan.markets.length)
35
+ return prepared === 0 ? [] : [prepared];
36
+ const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
37
+ const sample = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }, route);
38
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(sample, await readNitroPrices(client, block)).posterGas : 0n;
39
+ return packPreparationBatches(plan, prepared, limits.executionGasCeiling - poster, limits.headroomBps);
40
+ }
22
41
  function validatePlanShape(plan) {
23
42
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
24
43
  const commitment = hashLaunchPlan(plan);
@@ -176,16 +195,14 @@ function orderedTransactions(plan, mode, progress, approvals, batches, ufg, capa
176
195
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
177
196
  return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
178
197
  }
179
- function attachEstimates(transactions, simulation) {
198
+ function attachEstimates(transactions, assessment) {
180
199
  return transactions.map((transaction, index) => {
181
200
  if (index !== 0)
182
201
  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
202
  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 };
203
+ return { ...transaction, assessment };
204
+ const estimate = assessment.estimate;
205
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, assessment };
189
206
  });
190
207
  }
191
208
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"];
@@ -215,16 +232,16 @@ function assertReviewedTransactionUnchanged(options, reviewed, next) {
215
232
  if (options.planned.ufg !== undefined)
216
233
  assertUfgLaunchAccount(options.planned.plan, options.planned.account);
217
234
  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");
235
+ throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
219
236
  }
220
237
  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");
238
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
222
239
  }
223
240
  const current = readReviewedNextTransaction(options.reviewedTransaction);
224
241
  if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
225
242
  reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
226
243
  (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");
244
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
228
245
  }
229
246
  }
230
247
  async function readTokenFactoryBinding(client, orchestrator, block) {
@@ -238,21 +255,6 @@ async function readTokenFactoryBinding(client, orchestrator, block) {
238
255
  throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
239
256
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
240
257
  }
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
258
  function assertPlanningActive(signal) {
257
259
  if (signal?.aborted)
258
260
  throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
@@ -312,21 +314,8 @@ function assertFinalPlan(planned, signal) {
312
314
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash)
313
315
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
314
316
  }
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. */
317
+ /** One owner mines constructor-bound salts and protects ordered buys with exact
318
+ * local curve math. Launch calldata is never executed during planning. */
330
319
  export async function prepareAndPlanLifecycleLaunch(options) {
331
320
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
332
321
  if (options.buySlippageBps !== undefined)
@@ -366,9 +355,13 @@ export async function prepareAndPlanLifecycleLaunch(options) {
366
355
  }
367
356
  assertDraft();
368
357
  let plan = { ...draft, markets };
369
- let ownedProgress;
370
358
  let openingBuys;
371
- if (needsProtection && hasProfiledOpeningMath(plan)) {
359
+ if (needsProtection) {
360
+ if (!hasProfiledOpeningMath(plan))
361
+ throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Exact opening-buy math is unavailable for this profile. Use a supported profile or update the SDK.");
362
+ const head = await readLifecycleHeadProgress(client, plan, options.account, block);
363
+ if (head.phase !== LifecyclePhase.None)
364
+ throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
372
365
  const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
373
366
  openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
374
367
  const factor = BigInt(10000 - options.buySlippageBps);
@@ -379,20 +372,7 @@ export async function prepareAndPlanLifecycleLaunch(options) {
379
372
  return { ...buy, minTokenOut };
380
373
  }) };
381
374
  }
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 }));
375
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
396
376
  assertDraft();
397
377
  assertFinalPlan(planned, options.signal);
398
378
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -402,15 +382,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
402
382
  throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
403
383
  }
404
384
  }
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);
385
+ if (needsProtection && !planned.assessment.ready)
386
+ 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
387
  return planned;
408
388
  });
409
389
  assertDraft();
410
390
  assertFinalPlan(result, options.signal);
411
391
  return result;
412
392
  }
413
- /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
393
+ /** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
414
394
  export async function planLaunch(options) {
415
395
  const result = await withLifecycleReadClient(options.client, async (client) => {
416
396
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
@@ -421,9 +401,8 @@ export async function planLaunch(options) {
421
401
  return result;
422
402
  }
423
403
  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;
404
+ const { block: pinnedBlock, openingBuys } = readContext;
405
+ const { plan, account, ufg } = options;
427
406
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
428
407
  const launchId = hashLaunchIdentity(plan);
429
408
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
@@ -431,39 +410,27 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
431
410
  ]));
432
411
  if (chainId !== plan.chainId)
433
412
  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
413
  assertPlanningActive(options.signal);
445
414
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
446
415
  const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
447
416
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
448
417
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
449
418
  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,
419
+ const [predictedToken, head] = await Promise.all([
420
+ readLifecycleTokenAtBlock(client, plan, block), readLifecycleHeadProgress(client, plan, account, block),
453
421
  ]);
422
+ const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" : "atomic";
454
423
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
455
424
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
456
425
  if (ufg !== undefined)
457
426
  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");
427
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
461
428
  return { identity, progress };
462
429
  }),
463
430
  ]));
464
431
  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,
432
+ const placeholder = { ready: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits };
433
+ let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, assessment: placeholder, limits: options.limits,
467
434
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
468
435
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
469
436
  if (!progress.confirmationSafe || receiptsPending) {
@@ -475,14 +442,13 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
475
442
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
476
443
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
477
444
  }
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 };
445
+ return { ...planned, hookDeployments, assessment: { ...placeholder,
446
+ code: !progress.confirmationSafe ? "UNCONFIRMED_STATE" : "RECEIPT_PENDING",
447
+ reason: !progress.confirmationSafe ? "Wait for the selected launch confirmation depth before continuing." : "Wait for the pending transaction to confirm or resolve its replacement." } };
480
448
  }
481
449
  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" } };
450
+ return { ...planned, assessment: { ...placeholder, ready: true } };
483
451
  }
484
- // Topology is a creation policy, not a retroactive obstacle to recovery of
485
- // immutable onchain commitments. Waiting and terminal launches returned above.
486
452
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None)
487
453
  validatePlanTopology(options, marketConfigs, identity.predictedToken);
488
454
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
@@ -490,129 +456,81 @@ async function planLaunchWithReads(options, client, readContext = defaultPlanRea
490
456
  readPlanMetadata(client, plan, identity.predictedToken, block),
491
457
  ]));
492
458
  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
- }
459
+ const capacity = deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys);
460
+ const mode = progress.canonical.phase === LifecyclePhase.None
461
+ ? await selectLaunchMode(options.mode, plan, capacity, limits, client, block) : identity.mode;
500
462
  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
- };
463
+ if (metadata.reasons.length > 0)
464
+ return { ...planned, assessment: { ...placeholder, code: "PROFILE_UNAVAILABLE", failureCategory: "semantic",
465
+ reason: "The selected launch profile could not be read or is unavailable. Refresh the review before continuing." } };
466
+ let batches;
467
+ try {
468
+ batches = mode === "staged" ? await preparationBatches(plan, progress.canonical.preparedMarkets, ufg, limits, client, block) : [];
541
469
  }
542
- else if (!diagnostic && !simulation.admitted) {
543
- simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
470
+ catch (failure) {
471
+ if (!(failure instanceof LifecyclePlanningError) || failure.code !== "PREPARATION_GAS_LIMIT")
472
+ throw failure;
473
+ return { ...planned, assessment: { ...placeholder, code: failure.code, reason: failure.message, failureCategory: "capacity", capacityConstraint: "preparation" } };
544
474
  }
545
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
475
+ const transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
476
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: options.limits, signal: options.signal }, transactions, block, undefined, openingBuys, capacity);
477
+ assertFinalPlan(planned, options.signal);
478
+ return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
546
479
  }
547
480
  function finalizeNextTransaction(options, next, reviewed) {
548
481
  assertPlanningActive(options.signal);
549
482
  assertReviewedTransactionUnchanged(options, reviewed, next);
550
483
  return { ...next, dependencies: [] };
551
484
  }
552
- /** Recovery observes real canonical receipts, then evaluates only unfinished
553
- * execution; the wallet owns its nonce and assignment at send time. */
485
+ /** Recovery observes actual canonical receipts, then constructs and budgets only
486
+ * the next command. No launch transaction is executed during planning. */
554
487
  export async function buildNextTransaction(options) {
555
488
  const original = options.planned, matches = observedFields(original);
556
- const fullReviewAdmitted = original.simulation?.admitted === true;
557
489
  const assertInput = () => {
558
490
  if (options.planned !== original || !matches())
559
491
  throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
560
492
  assertFinalPlan(original, options.signal);
561
493
  };
562
494
  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
495
  const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
566
496
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
567
497
  from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
568
498
  const envelope = reviewed === undefined || walletApproval ? undefined : reviewed;
569
499
  const result = await withLifecycleReadClient(options.client, async (client) => {
570
500
  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
- }
501
+ const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
502
+ const limitSource = options.limits === undefined ? original.limits : options.limits;
580
503
  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.
504
+ if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
585
505
  return undefined;
586
- }
587
506
  if (!progress.confirmationSafe)
588
- throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
507
+ throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected launch confirmation depth before continuing.");
589
508
  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");
509
+ throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for the pending transaction to confirm or resolve its replacement.");
591
510
  assertPlanningActive(options.signal);
511
+ const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
592
512
  if (options.action === "cancel") {
593
513
  if (progress.canonical.phase === LifecyclePhase.None)
594
514
  throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
595
- const { planned } = options;
596
- assertFinalPlan(planned, options.signal);
515
+ const planned = { ...original, progress };
597
516
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
598
517
  : { 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
518
  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);
519
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current cancellation");
520
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [transaction], block, envelope);
521
+ if (!assessment.ready)
522
+ throw new LifecyclePlanningError(assessment.code ?? "CANCEL_NOT_READY", assessment.reason ?? "Cancellation could not be prepared. Refresh before continuing.", assessment);
523
+ return finalizeNextTransaction(options, attachEstimates([transaction], assessment)[0], reviewed);
608
524
  }
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
525
  const funding = progress.canonical.phase === LifecyclePhase.None
614
- ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
526
+ ? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
615
527
  : { prerequisites: [], approvals: [] };
528
+ const planned = { ...original, progress, prerequisites: funding.prerequisites };
529
+ const capacity = deriveLifecycleLaunchCapacity({ plan: planned.plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps });
530
+ if (progress.canonical.phase === LifecyclePhase.None &&
531
+ await selectLaunchMode(planned.mode, planned.plan, capacity, limits, client, block) !== planned.mode) {
532
+ throw new LifecyclePlanningError("MODE_REVIEW_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.");
533
+ }
616
534
  let candidate = funding.approvals[0];
617
535
  if (candidate === undefined) {
618
536
  const prepared = progress.canonical.preparedMarkets;
@@ -623,23 +541,16 @@ export async function buildNextTransaction(options) {
623
541
  let count;
624
542
  if (reviewed?.kind === "prepare") {
625
543
  const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
626
- const first = Number(span?.[1]);
544
+ if (Number(span?.[1]) !== prepared)
545
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
627
546
  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
547
  }
631
548
  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
- }
549
+ const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
550
+ count = batches[prepared > 0 ? 1 : 0];
640
551
  }
641
552
  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");
553
+ throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must cover a positive contiguous unfinished market span");
643
554
  command = { kind: "prepare", first: prepared, count };
644
555
  }
645
556
  else
@@ -647,32 +558,16 @@ export async function buildNextTransaction(options) {
647
558
  candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
648
559
  }
649
560
  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 });
561
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current next step");
562
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [candidate], block, envelope, undefined, capacity);
652
563
  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);
564
+ if (!assessment.ready)
565
+ throw new LifecyclePlanningError(assessment.code ?? "PLAN_NOT_READY", assessment.reason ?? "The next launch step could not be prepared. Refresh before continuing.", assessment);
566
+ return finalizeNextTransaction(options, attachEstimates([candidate], assessment)[0], reviewed);
657
567
  });
658
568
  assertInput();
659
569
  if (result !== undefined)
660
570
  assertReviewedTransactionUnchanged(options, reviewed, result);
661
571
  return result;
662
572
  }
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
573
  //# sourceMappingURL=planner.js.map