@black-market/sdk 0.14.0 → 0.15.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 (73) hide show
  1. package/README.md +4 -3
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/gas-profile.d.ts +35 -0
  5. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  6. package/dist/lifecycle/gas-profile.js +183 -0
  7. package/dist/lifecycle/gas-profile.js.map +1 -0
  8. package/dist/lifecycle/index.d.ts +3 -1
  9. package/dist/lifecycle/index.d.ts.map +1 -1
  10. package/dist/lifecycle/index.js +3 -1
  11. package/dist/lifecycle/index.js.map +1 -1
  12. package/dist/lifecycle/lens.d.ts +1 -1
  13. package/dist/lifecycle/lens.d.ts.map +1 -1
  14. package/dist/lifecycle/lens.js +1 -1
  15. package/dist/lifecycle/lens.js.map +1 -1
  16. package/dist/lifecycle/opening-buys.d.ts +13 -0
  17. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  18. package/dist/lifecycle/opening-buys.js +171 -0
  19. package/dist/lifecycle/opening-buys.js.map +1 -0
  20. package/dist/lifecycle/planner.d.ts +2 -2
  21. package/dist/lifecycle/planner.d.ts.map +1 -1
  22. package/dist/lifecycle/planner.js +178 -163
  23. package/dist/lifecycle/planner.js.map +1 -1
  24. package/dist/lifecycle/presets.d.ts +6 -1
  25. package/dist/lifecycle/presets.d.ts.map +1 -1
  26. package/dist/lifecycle/presets.js +7 -0
  27. package/dist/lifecycle/presets.js.map +1 -1
  28. package/dist/lifecycle/progress.d.ts +1 -1
  29. package/dist/lifecycle/progress.d.ts.map +1 -1
  30. package/dist/lifecycle/progress.js +21 -20
  31. package/dist/lifecycle/progress.js.map +1 -1
  32. package/dist/lifecycle/rpc.d.ts +24 -19
  33. package/dist/lifecycle/rpc.d.ts.map +1 -1
  34. package/dist/lifecycle/rpc.js +58 -50
  35. package/dist/lifecycle/rpc.js.map +1 -1
  36. package/dist/lifecycle/simulation.d.ts +3 -4
  37. package/dist/lifecycle/simulation.d.ts.map +1 -1
  38. package/dist/lifecycle/simulation.js +98 -247
  39. package/dist/lifecycle/simulation.js.map +1 -1
  40. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  41. package/dist/lifecycle/snapshot.js +1 -2
  42. package/dist/lifecycle/snapshot.js.map +1 -1
  43. package/dist/lifecycle/transactions.d.ts +19 -3
  44. package/dist/lifecycle/transactions.d.ts.map +1 -1
  45. package/dist/lifecycle/transactions.js +25 -20
  46. package/dist/lifecycle/transactions.js.map +1 -1
  47. package/dist/lifecycle/types.d.ts +53 -45
  48. package/dist/lifecycle/types.d.ts.map +1 -1
  49. package/dist/lifecycle/types.js.map +1 -1
  50. package/dist/lifecycle/ufg.d.ts.map +1 -1
  51. package/dist/lifecycle/ufg.js +3 -7
  52. package/dist/lifecycle/ufg.js.map +1 -1
  53. package/docs/launch-lifecycle.md +71 -82
  54. package/docs/lifecycle-gas-profiles.md +90 -0
  55. package/docs/reference.md +53 -51
  56. package/examples/README.md +13 -10
  57. package/examples/launch-example.ts +35 -25
  58. package/examples/launch-lifecycle-smoke.ts +5 -3
  59. package/package.json +1 -1
  60. package/src/lifecycle/authors.ts +2 -2
  61. package/src/lifecycle/gas-profile.ts +175 -0
  62. package/src/lifecycle/index.ts +3 -1
  63. package/src/lifecycle/lens.ts +1 -1
  64. package/src/lifecycle/opening-buys.ts +157 -0
  65. package/src/lifecycle/planner.ts +164 -151
  66. package/src/lifecycle/presets.ts +12 -1
  67. package/src/lifecycle/progress.ts +19 -20
  68. package/src/lifecycle/rpc.ts +56 -44
  69. package/src/lifecycle/simulation.ts +85 -231
  70. package/src/lifecycle/snapshot.ts +1 -2
  71. package/src/lifecycle/transactions.ts +33 -21
  72. package/src/lifecycle/types.ts +52 -42
  73. package/src/lifecycle/ufg.ts +3 -7
@@ -5,17 +5,20 @@ 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, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
- import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.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";
10
10
  import { LifecyclePlanningError } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
12
12
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
- import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
13
+ import { buildLifecycleCommand, packPreparationBatches } from "./transactions.js";
14
14
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
15
- import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
16
- import { buildUfgLaunchTransactions, buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
15
+ import { assertUfgLaunchAccount, hashUfgLaunchConfig, ufgPlanPrincipal, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
16
+ import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
17
  import { readLifecyclePlanningBlock } from "./snapshot.js";
18
18
  import { mineLifecycleHooks } from "./mining.js";
19
+ import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
20
+ import { deriveLifecycleLaunchCapacity, profileLifecycleTransactions } from "./gas-profile.js";
21
+ const defaultPlanReadContext = {};
19
22
  function validatePlanShape(plan) {
20
23
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
21
24
  const commitment = hashLaunchPlan(plan);
@@ -152,28 +155,44 @@ async function readPlanMetadata(client, plan, token, block) {
152
155
  }))).filter((deployment) => deployment !== undefined);
153
156
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
154
157
  }
155
- function orderedTransactions(plan, mode, progress, approvals, batches, ufg) {
158
+ function orderedTransactions(plan, mode, progress, approvals, batches, ufg, capacity) {
156
159
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
157
160
  return [];
158
- const prepared = progress.canonical.preparedMarkets;
159
- const compiled = ufg === undefined ? buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined })
160
- : buildUfgLaunchTransactions({ plan, config: ufg.config, mode, preparationBatches: mode === "staged" ? batches : undefined });
161
+ const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
162
+ const compiled = [];
163
+ if (mode === "atomic")
164
+ compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
165
+ else {
166
+ compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
167
+ let first = 0;
168
+ for (const count of batches) {
169
+ compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route));
170
+ first += count;
171
+ }
172
+ compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
173
+ }
161
174
  const commands = compiled.filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
162
175
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
163
176
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
164
- return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
177
+ return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
165
178
  }
166
179
  function attachEstimates(transactions, simulation) {
167
- return transactions.map((transaction) => {
168
- const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
169
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
170
- admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
180
+ return transactions.map((transaction, index) => {
181
+ if (index !== 0)
182
+ 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
+ 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 };
171
189
  });
172
190
  }
173
191
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"];
174
192
  function readReviewedNextTransaction(transaction) {
175
193
  if (transaction === undefined)
176
194
  return undefined;
195
+ const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
177
196
  if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
178
197
  !reviewedTransactionKinds.includes(transaction.kind) ||
179
198
  !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
@@ -181,9 +200,8 @@ function readReviewedNextTransaction(transaction) {
181
200
  typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
182
201
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
183
202
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
184
- typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
185
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
186
- transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
203
+ !walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
204
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
187
205
  throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
188
206
  }
189
207
  return transaction;
@@ -191,8 +209,7 @@ function readReviewedNextTransaction(transaction) {
191
209
  function matchesReviewedNextTransaction(next, reviewed) {
192
210
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
193
211
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
194
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
195
- (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
212
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
196
213
  }
197
214
  function assertReviewedTransactionUnchanged(options, reviewed, next) {
198
215
  if (options.planned.ufg !== undefined)
@@ -204,9 +221,9 @@ function assertReviewedTransactionUnchanged(options, reviewed, next) {
204
221
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
205
222
  }
206
223
  const current = readReviewedNextTransaction(options.reviewedTransaction);
207
- if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
208
- current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || !matchesReviewedNextTransaction(next, reviewed) ||
209
- next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
224
+ if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
225
+ reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
226
+ (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
210
227
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
211
228
  }
212
229
  }
@@ -244,7 +261,7 @@ function validatePlanningIntent(options) {
244
261
  const { plan, account, mode } = options;
245
262
  assertPlanningActive(options.signal);
246
263
  if (mode !== "atomic" && mode !== "staged")
247
- throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
264
+ throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
248
265
  if (options.ufg !== undefined)
249
266
  assertUfgLaunchAccount(plan, account);
250
267
  else if (account.toLowerCase() !== plan.creator.toLowerCase())
@@ -295,21 +312,28 @@ function assertFinalPlan(planned, signal) {
295
312
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash)
296
313
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
297
314
  }
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
+ }
298
328
  /** One owner constructs and mines, optionally measures actual ordered buy outputs,
299
329
  * then proves only the final protected commitment. No cached execution certificates. */
300
330
  export async function prepareAndPlanLifecycleLaunch(options) {
301
331
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
302
332
  if (options.buySlippageBps !== undefined)
303
333
  validateBuySlippageBps(options.buySlippageBps);
304
- const fixed = options.fixedFinalEnvelope;
305
- if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
306
- fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
307
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
308
- }
309
- const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
310
334
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
311
335
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
312
- if (!needsMining && !needsProtection && fixed === undefined)
336
+ if (!needsMining && !needsProtection)
313
337
  return planLaunch(options);
314
338
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
315
339
  const assertDraft = () => {
@@ -318,9 +342,6 @@ export async function prepareAndPlanLifecycleLaunch(options) {
318
342
  throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
319
343
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash)
320
344
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
321
- if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
322
- fixed.gas !== heldEnvelope.gas || fixed.gasPrice !== heldEnvelope.gasPrice)
323
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
324
345
  };
325
346
  const result = await withLifecycleReadClient(options.client, async (client) => {
326
347
  const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
@@ -344,22 +365,25 @@ export async function prepareAndPlanLifecycleLaunch(options) {
344
365
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
345
366
  }
346
367
  assertDraft();
347
- const executionBlock = block;
348
- assertDraft();
349
- const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
350
- const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
351
- const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
352
- gas: heldEnvelope.gas,
353
- gasPrice: heldEnvelope.gasPrice ?? (price > baseFeeEnvelope ? price : baseFeeEnvelope),
354
- });
355
368
  let plan = { ...draft, markets };
356
369
  let ownedProgress;
357
- let sizing;
358
- if (needsProtection) {
370
+ let openingBuys;
371
+ if (needsProtection && hasProfiledOpeningMath(plan)) {
372
+ const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
373
+ openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
374
+ const factor = BigInt(10000 - options.buySlippageBps);
375
+ plan = { ...plan, buys: plan.buys.map((buy, index) => {
376
+ const minTokenOut = openingBuys[index].tokenOut * factor / 10000n;
377
+ if (minTokenOut <= 0n)
378
+ throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
379
+ return { ...buy, minTokenOut };
380
+ }) };
381
+ }
382
+ else if (needsProtection) {
359
383
  // A diagnostic minimum is never returned as an admitted or executable plan.
360
384
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
361
385
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
362
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
386
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
363
387
  assertDraft();
364
388
  if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
365
389
  throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
@@ -367,9 +391,8 @@ export async function prepareAndPlanLifecycleLaunch(options) {
367
391
  // This is the real SDK-owned None observation, not an assumed new-launch
368
392
  // state. No onchain commitment existed at this exact snapshot to replace.
369
393
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
370
- sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed]));
371
394
  }
372
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
395
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, progress: ownedProgress, openingBuys }));
373
396
  assertDraft();
374
397
  assertFinalPlan(planned, options.signal);
375
398
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -397,8 +420,10 @@ export async function planLaunch(options) {
397
420
  assertFinalPlan(result, options.signal);
398
421
  return result;
399
422
  }
400
- async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, fixedFinalEnvelope, sizing) {
401
- const { plan, account, mode, ufg } = options;
423
+ 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;
402
427
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
403
428
  const launchId = hashLaunchIdentity(plan);
404
429
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
@@ -437,7 +462,7 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
437
462
  }),
438
463
  ]));
439
464
  assertPlanningActive(options.signal);
440
- const placeholder = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
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: [] };
441
466
  let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
442
467
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
443
468
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
@@ -450,7 +475,7 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
450
475
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
451
476
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
452
477
  }
453
- const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
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" };
454
479
  return { ...planned, hookDeployments, simulation: waiting };
455
480
  }
456
481
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
@@ -465,99 +490,84 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
465
490
  readPlanMetadata(client, plan, identity.predictedToken, block),
466
491
  ]));
467
492
  assertFinalPlan(planned, options.signal);
468
- planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
469
- const prepared = progress.canonical.preparedMarkets;
470
- const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
471
- let batches = [];
472
- if (mode === "staged")
473
- batches = packPreparationBatches(plan, prepared, computeCap);
474
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
475
- if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0].kind !== "atomic")) {
476
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
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";
477
499
  }
500
+ 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);
478
504
  if (metadata.reasons.length > 0) {
479
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) };
480
506
  }
481
- const envelope = diagnostic ? undefined : fixedFinalEnvelope;
482
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
483
- try {
484
- const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, envelope);
485
- let simulation = await execute(transactions);
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);
486
517
  assertFinalPlan(planned, options.signal);
487
- if (mode === "staged") {
488
- // Only pressure at the native ceiling can invalidate packing. A smaller
489
- // affordability/RPC/account envelope is not evidence for another wallet write.
490
- while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
491
- const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
492
- const failedTransaction = transactions[failedIndex];
493
- const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
494
- const count = failedTransaction?.marketCount ?? 0;
495
- if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n)
496
- break;
497
- let first = 0;
498
- const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
499
- const left = Math.ceil(count / 2);
500
- batches.splice(batchIndex, 1, left, count - left);
501
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
502
- simulation = await execute(transactions);
503
- assertFinalPlan(planned, options.signal);
504
- }
505
- if (!simulation.admitted && simulation.failedTransactionId === "activate")
506
- simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
507
- }
508
- else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
509
- simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
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);
510
536
  }
511
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
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
+ };
512
541
  }
513
- finally {
514
- preparedSimulation.close();
542
+ else if (!diagnostic && !simulation.admitted) {
543
+ simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
515
544
  }
545
+ return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
516
546
  }
517
- async function preflightNextTransaction(options, next, simulation, reviewed) {
518
- if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
519
- simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
520
- next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
521
- simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
522
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
523
- }
524
- assertReviewedTransactionUnchanged(options, reviewed, next);
525
- const submissionClient = options.submissionClient;
526
- assertPlanningActive(options.signal);
527
- if (submissionClient === undefined)
528
- return { ...next, dependencies: [] };
529
- try {
530
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
531
- throw new Error("Active submission RPC chain differs from the reviewed transaction");
532
- const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
533
- from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
534
- ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
535
- }, "latest"]), "submission gas estimate");
536
- if (estimated <= 0n || estimated > next.gas)
537
- throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
538
- assertPlanningActive(options.signal);
539
- }
540
- catch (failure) {
541
- if (failure instanceof Error && failure.name === "AbortError")
542
- throw failure;
543
- const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
544
- throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
545
- }
546
- if (next.admission === undefined)
547
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
547
+ function finalizeNextTransaction(options, next, reviewed) {
548
548
  assertPlanningActive(options.signal);
549
549
  assertReviewedTransactionUnchanged(options, reviewed, next);
550
- return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
550
+ return { ...next, dependencies: [] };
551
551
  }
552
- /** Recovery observes real canonical receipts/nonces, then proves only unfinished
553
- * execution; known construction metadata is not recertified through getter mirrors. */
552
+ /** Recovery observes real canonical receipts, then evaluates only unfinished
553
+ * execution; the wallet owns its nonce and assignment at send time. */
554
554
  export async function buildNextTransaction(options) {
555
+ const original = options.planned, matches = observedFields(original);
556
+ const fullReviewAdmitted = original.simulation?.admitted === true;
557
+ const assertInput = () => {
558
+ if (options.planned !== original || !matches())
559
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
560
+ assertFinalPlan(original, options.signal);
561
+ };
555
562
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
556
563
  // Snapshot only the reviewed wire fields; caller proofs and commands are never
557
564
  // reused, and final checks compare the mutable input with this one local copy.
565
+ const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
558
566
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
559
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
567
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
568
+ const envelope = reviewed === undefined || walletApproval ? undefined : reviewed;
560
569
  const result = await withLifecycleReadClient(options.client, async (client) => {
570
+ registerLifecycleFinalCheck(client, assertInput);
561
571
  const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
562
572
  const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
563
573
  if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
@@ -570,12 +580,12 @@ export async function buildNextTransaction(options) {
570
580
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
571
581
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
572
582
  // Only the confirmed canonical terminal phase is terminal; a head-only
573
- // Active observation is not, and the confirmation/nonce/receipt gates below
574
- // remain in force for every nonterminal case.
583
+ // Active observation is not, and the launch-state/receipt confirmation
584
+ // policy below remains in force for every nonterminal case.
575
585
  return undefined;
576
586
  }
577
587
  if (!progress.confirmationSafe)
578
- throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
588
+ throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
579
589
  if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed"))
580
590
  throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
581
591
  assertPlanningActive(options.signal);
@@ -586,61 +596,66 @@ export async function buildNextTransaction(options) {
586
596
  assertFinalPlan(planned, options.signal);
587
597
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
588
598
  : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
589
- transaction.nonce = progress.pendingAccountNonce;
590
599
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
591
600
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
592
- const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
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 });
593
602
  if (!simulation.admitted)
594
603
  throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
595
604
  const next = attachEstimates([transaction], simulation)[0];
596
605
  if (next === undefined)
597
606
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
598
- return preflightNextTransaction(options, next, simulation, reviewed);
607
+ return finalizeNextTransaction(options, next, reviewed);
599
608
  }
600
609
  const { planned } = options;
601
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");
602
613
  const funding = progress.canonical.phase === LifecyclePhase.None
603
614
  ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
604
615
  : { prerequisites: [], approvals: [] };
605
- const prepared = progress.canonical.preparedMarkets;
606
- let batches = [];
607
- if (planned.mode === "staged") {
608
- let suffixStart = prepared;
609
- const prefix = prepared > 0 ? [prepared] : [];
610
- if (reviewed?.kind === "prepare") {
611
- const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
612
- const first = Number(span?.[1]), count = Number(span?.[2]);
613
- if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length)
614
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
615
- prefix.push(count);
616
- suffixStart += count;
617
- }
618
- const suffix = planned.transactions
619
- .filter((transaction) => transaction.kind === "prepare" && transaction.marketStart + transaction.marketCount > suffixStart)
620
- .map((transaction) => transaction.marketStart + transaction.marketCount - Math.max(transaction.marketStart, suffixStart));
621
- if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
622
- const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
623
- const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
624
- suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
616
+ let candidate = funding.approvals[0];
617
+ if (candidate === undefined) {
618
+ const prepared = progress.canonical.preparedMarkets;
619
+ let command;
620
+ if (progress.canonical.phase === LifecyclePhase.None)
621
+ command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
622
+ else if (prepared < planned.plan.markets.length) {
623
+ let count;
624
+ if (reviewed?.kind === "prepare") {
625
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
626
+ const first = Number(span?.[1]);
627
+ 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
+ }
631
+ 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
+ }
640
+ }
641
+ 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");
643
+ command = { kind: "prepare", first: prepared, count };
625
644
  }
626
- batches = [...prefix, ...suffix];
645
+ else
646
+ command = { kind: "activate" };
647
+ candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
627
648
  }
628
- const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
629
- if (candidate === undefined)
630
- return undefined;
631
649
  if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed))
632
650
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
633
- const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
634
- const compute = observed?.computeGasRequired;
635
- const sizing = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
636
- const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
651
+ const simulation = await evaluateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, { finalEnvelope: envelope, immediate: true });
637
652
  assertFinalPlan(planned, options.signal);
638
653
  if (!simulation.admitted)
639
654
  throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
640
655
  const next = attachEstimates([candidate], simulation)[0];
641
- return preflightNextTransaction(options, next, simulation, reviewed);
656
+ return finalizeNextTransaction(options, next, reviewed);
642
657
  });
643
- assertFinalPlan(options.planned, options.signal);
658
+ assertInput();
644
659
  if (result !== undefined)
645
660
  assertReviewedTransactionUnchanged(options, reviewed, result);
646
661
  return result;
@@ -651,12 +666,12 @@ export async function simulateLaunchPlan(options) {
651
666
  const { client, planned } = options;
652
667
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
653
668
  throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
654
- const current = await planLaunch({
669
+ const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
655
670
  client, plan: planned.plan, account: planned.account, mode: planned.mode,
656
671
  limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
657
672
  receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
658
673
  ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
659
- });
674
+ }, readClient, { forceSimulation: true }));
660
675
  assertPlannedDeploymentBindings(planned, current);
661
676
  return current.simulation;
662
677
  }