@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,23 +5,32 @@ 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, type LaunchPlanV1 } from "./schema.js";
6
6
  import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
7
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } 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, type GasSizingHints, type NextTransactionEnvelope } 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, type NextTransactionEnvelope } from "./simulation.js";
10
10
  import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } 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, type LifecycleCommand } 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, type ProfiledLaunchCapacity } from "./gas-profile.js";
21
+ import type { LifecycleOpeningBuyQuote } from "./types.js";
19
22
 
20
23
  type ValidatedPlanShape = {
21
24
  commitment: Hex;
22
25
  ufgConfigHash?: Hex;
23
26
  marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
24
27
  };
28
+ type PlanReadContext = {
29
+ block?: LifecycleBlock; progress?: CanonicalLaunchProgress; diagnostic?: boolean;
30
+ finalEnvelope?: NextTransactionEnvelope; capacity?: ProfiledLaunchCapacity;
31
+ openingBuys?: readonly LifecycleOpeningBuyQuote[]; forceSimulation?: boolean; immediate?: boolean;
32
+ };
33
+ const defaultPlanReadContext: PlanReadContext = {};
25
34
 
26
35
  function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
27
36
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
@@ -137,30 +146,40 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
137
146
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
138
147
  }
139
148
 
140
- function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"]): LifecycleTransaction[] {
149
+ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"], capacity?: ProfiledLaunchCapacity): LifecycleTransaction[] {
141
150
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
142
- const prepared = progress.canonical.preparedMarkets;
143
- const compiled = ufg === undefined ? buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined })
144
- : buildUfgLaunchTransactions({ plan, config: ufg.config, mode, preparationBatches: mode === "staged" ? batches : undefined });
151
+ const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
152
+ const compiled: LifecycleTransaction[] = [];
153
+ if (mode === "atomic") compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
154
+ else {
155
+ compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
156
+ let first = 0;
157
+ for (const count of batches) { compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route)); first += count; }
158
+ compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
159
+ }
145
160
  const commands = compiled.filter((transaction) =>
146
161
  (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
147
162
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
148
163
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
149
- return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
164
+ return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
150
165
  }
151
166
 
152
167
  function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
153
- return transactions.map((transaction) => {
154
- const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
155
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
156
- 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 } };
168
+ return transactions.map((transaction, index) => {
169
+ if (index !== 0) return transaction;
170
+ const admission = { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit,
171
+ blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits };
172
+ if (transaction.kind === "approve" || transaction.kind === "approve-reset") return { ...transaction, admission };
173
+ const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate ?? simulation.estimate;
174
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission };
157
175
  });
158
176
  }
159
- type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value" | "nonce"> & NextTransactionEnvelope;
177
+ type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
160
178
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
161
179
 
162
180
  function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
163
181
  if (transaction === undefined) return undefined;
182
+ const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
164
183
  if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
165
184
  !reviewedTransactionKinds.includes(transaction.kind) ||
166
185
  !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
@@ -168,9 +187,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
168
187
  typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
169
188
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
170
189
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
171
- typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
172
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
173
- transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
190
+ !walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
191
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
174
192
  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");
175
193
  }
176
194
  return transaction as ReviewedNextTransaction;
@@ -179,8 +197,7 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
179
197
  function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
180
198
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
181
199
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
182
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
183
- (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
200
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
184
201
  }
185
202
 
186
203
  function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
@@ -192,9 +209,9 @@ function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions
192
209
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
193
210
  }
194
211
  const current = readReviewedNextTransaction(options.reviewedTransaction);
195
- if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
196
- current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || !matchesReviewedNextTransaction(next, reviewed) ||
197
- next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
212
+ if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
213
+ reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
214
+ (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
198
215
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
199
216
  }
200
217
  }
@@ -228,7 +245,7 @@ function assertPlanningActive(signal?: AbortSignal): void {
228
245
  function validatePlanningIntent(options: PlanLaunchOptions): ValidatedPlanShape {
229
246
  const { plan, account, mode } = options;
230
247
  assertPlanningActive(options.signal);
231
- if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
248
+ if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
232
249
  if (options.ufg !== undefined) assertUfgLaunchAccount(plan, account);
233
250
  else if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
234
251
  if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
@@ -272,27 +289,29 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
272
289
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
273
290
  }
274
291
 
292
+ async function evaluateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], block: LifecycleBlock, controls: PlanReadContext = defaultPlanReadContext): Promise<LifecycleSimulation> {
293
+ const profile = controls.diagnostic ? undefined : await profileLifecycleTransactions(options, transactions, block, controls.finalEnvelope, controls.openingBuys, controls.capacity);
294
+ if (!controls.forceSimulation && profile?.profileOnly) return profile.simulation;
295
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, controls.immediate);
296
+ try {
297
+ const simulation = controls.diagnostic ? await prepared.measure(transactions) : await prepared(transactions, controls.finalEnvelope);
298
+ return profile === undefined ? simulation : { ...simulation, gasProfile: profile.simulation.gasProfile };
299
+ } finally { prepared.close(); }
300
+ }
301
+
275
302
  /** One owner constructs and mines, optionally measures actual ordered buy outputs,
276
303
  * then proves only the final protected commitment. No cached execution certificates. */
277
304
  export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
278
305
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
279
306
  if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
280
- const fixed = options.fixedFinalEnvelope;
281
- if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
282
- fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
283
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
284
- }
285
- const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
286
307
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
287
308
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
288
- if (!needsMining && !needsProtection && fixed === undefined) return planLaunch(options);
309
+ if (!needsMining && !needsProtection) return planLaunch(options);
289
310
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
290
311
  const assertDraft = () => {
291
312
  assertPlanningActive(options.signal);
292
313
  if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
293
314
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
294
- if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
295
- fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
296
315
  };
297
316
  const result = await withLifecycleReadClient(options.client, async (client) => {
298
317
  const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
@@ -314,31 +333,31 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
314
333
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
315
334
  }
316
335
  assertDraft();
317
- const executionBlock = block;
318
- assertDraft();
319
- const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
320
- const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
321
- const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
322
- gas: heldEnvelope.gas,
323
- gasPrice: heldEnvelope.gasPrice ?? (price! > baseFeeEnvelope ? price! : baseFeeEnvelope),
324
- });
325
336
  let plan: LaunchPlanV1 = { ...draft, markets };
326
337
  let ownedProgress: CanonicalLaunchProgress | undefined;
327
- let sizing: GasSizingHints | undefined;
328
- if (needsProtection) {
338
+ let openingBuys: PlanReadContext["openingBuys"];
339
+ if (needsProtection && hasProfiledOpeningMath(plan)) {
340
+ const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
341
+ openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
342
+ const factor = BigInt(10000 - options.buySlippageBps!);
343
+ plan = { ...plan, buys: plan.buys.map((buy, index) => {
344
+ const minTokenOut = openingBuys![index]!.tokenOut * factor / 10000n;
345
+ if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
346
+ return { ...buy, minTokenOut };
347
+ }) };
348
+ } else if (needsProtection) {
329
349
  // A diagnostic minimum is never returned as an admitted or executable plan.
330
350
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
331
351
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
332
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
352
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
333
353
  assertDraft();
334
354
  if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
335
355
  plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
336
356
  // This is the real SDK-owned None observation, not an assumed new-launch
337
357
  // state. No onchain commitment existed at this exact snapshot to replace.
338
358
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
339
- sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed!]));
340
359
  }
341
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
360
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, progress: ownedProgress, openingBuys }));
342
361
  assertDraft();
343
362
  assertFinalPlan(planned, options.signal);
344
363
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -366,8 +385,10 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
366
385
  return result;
367
386
  }
368
387
 
369
- async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, fixedFinalEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<PlannedLaunch> {
370
- const { plan, account, mode, ufg } = options;
388
+ async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, readContext: PlanReadContext = defaultPlanReadContext): Promise<PlannedLaunch> {
389
+ const { block: pinnedBlock, progress: ownedProgress, diagnostic = false, openingBuys, forceSimulation = false } = readContext;
390
+ const { plan, account, mode: requestedMode, ufg } = options;
391
+ let mode = requestedMode;
371
392
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
372
393
  const launchId = hashLaunchIdentity(plan);
373
394
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
@@ -401,7 +422,7 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
401
422
  }),
402
423
  ]));
403
424
  assertPlanningActive(options.signal);
404
- const placeholder: LifecycleSimulation = { 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: [] };
425
+ const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
405
426
  let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
406
427
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
407
428
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
@@ -413,7 +434,7 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
413
434
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
414
435
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
415
436
  }
416
- const waiting = { ...placeholder, confidence: "stateful" as const, 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" };
437
+ const waiting = { ...placeholder, confidence: "stateful" as const, 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" };
417
438
 
418
439
  return { ...planned, hookDeployments, simulation: waiting };
419
440
  }
@@ -429,89 +450,82 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
429
450
  readPlanMetadata(client, plan, identity.predictedToken, block),
430
451
  ]));
431
452
  assertFinalPlan(planned, options.signal);
432
- planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
433
- const prepared = progress.canonical.preparedMarkets;
434
- const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
435
- let batches: number[] = [];
436
- if (mode === "staged") batches = packPreparationBatches(plan, prepared, computeCap);
437
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
438
- if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0]!.kind !== "atomic")) {
439
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
453
+ const capacity = limits.protocol === "nitro" && limits.arbOSVersion === 61n
454
+ ? deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys) : undefined;
455
+ if (!forceSimulation && progress.canonical.phase === LifecyclePhase.None && mode === "atomic" && capacity !== undefined) {
456
+ const prices = await readNitroPrices(client, block);
457
+ if (capacity.bounds.atomicComputeGasUpperBound + nitroPosterGasBound(capacity.atomic, prices).posterGas > limits.executionGasCeiling) mode = "staged";
440
458
  }
459
+ planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
460
+ const prepared = progress.canonical.preparedMarkets, computeCap = limits.executionGasCeiling;
461
+ let batches: number[] = mode === "staged" ? packPreparationBatches(plan, prepared, computeCap) : [];
462
+ let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
441
463
  if (metadata.reasons.length > 0) {
442
464
 
443
465
  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) };
444
466
  }
445
- const envelope = diagnostic ? undefined : fixedFinalEnvelope;
446
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
447
- try {
448
- const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, envelope);
449
- let simulation = await execute(transactions);
467
+ const execute = (candidate: readonly LifecycleTransaction[], quotes = openingBuys) => evaluateLaunchTransactions(
468
+ { client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, candidate, block,
469
+ { diagnostic, openingBuys: quotes, capacity, forceSimulation });
470
+ let simulation = await execute(transactions);
471
+ assertFinalPlan(planned, options.signal);
472
+ if (!diagnostic && !forceSimulation && requestedMode === "atomic" && mode === "atomic" && progress.canonical.phase === LifecyclePhase.None &&
473
+ simulation.executionProof === "failed" && simulation.failureCategory === "capacity" && simulation.capacityConstraint === "compute") {
474
+ mode = "staged"; planned = { ...planned, mode };
475
+ batches = packPreparationBatches(plan, prepared, computeCap);
476
+ transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
477
+ simulation = await execute(transactions);
450
478
  assertFinalPlan(planned, options.signal);
451
- if (mode === "staged") {
452
- // Only pressure at the native ceiling can invalidate packing. A smaller
453
- // affordability/RPC/account envelope is not evidence for another wallet write.
454
- while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
455
- const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
456
- const failedTransaction = transactions[failedIndex];
457
- const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
458
- const count = failedTransaction?.marketCount ?? 0;
459
- if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
460
- let first = 0;
461
- const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
462
- const left = Math.ceil(count / 2);
463
- batches.splice(batchIndex, 1, left, count - left);
464
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
465
- simulation = await execute(transactions);
466
- assertFinalPlan(planned, options.signal);
467
- }
468
- if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
469
- } else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
470
- 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` };
479
+ }
480
+ if (mode === "staged") {
481
+ // Only pressure at the native ceiling can invalidate packing. A smaller
482
+ // affordability/RPC/account envelope is not evidence for another wallet write.
483
+ while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
484
+ const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
485
+ const failedTransaction = transactions[failedIndex];
486
+ const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
487
+ const count = failedTransaction?.marketCount ?? 0;
488
+ if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
489
+ let first = 0;
490
+ const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
491
+ const left = Math.ceil(count / 2);
492
+ batches.splice(batchIndex, 1, left, count - left);
493
+ transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
494
+ simulation = await execute(transactions, simulation.gasProfile?.initialBuys);
495
+ assertFinalPlan(planned, options.signal);
471
496
  }
472
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
473
- } finally { preparedSimulation.close(); }
497
+ if (!simulation.admitted && simulation.failedTransactionId === "activate" && simulation.failureCategory === "capacity") simulation = {
498
+ ...simulation, capacityConstraint: "activation", reason: "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." };
499
+ } else if (!diagnostic && !simulation.admitted) {
500
+ simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
501
+ }
502
+ return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
474
503
  }
475
504
 
476
- async function preflightNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, simulation: LifecycleSimulation, reviewed?: ReviewedNextTransaction): Promise<LifecycleTransaction> {
477
- if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
478
- simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
479
- next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
480
- simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
481
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
482
- }
483
- assertReviewedTransactionUnchanged(options, reviewed, next);
484
- const submissionClient = options.submissionClient;
485
- assertPlanningActive(options.signal);
486
- if (submissionClient === undefined) return { ...next, dependencies: [] };
487
- try {
488
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
489
- const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
490
- from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
491
- ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
492
- }, "latest"]), "submission gas estimate");
493
- if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
494
- assertPlanningActive(options.signal);
495
- } catch (failure) {
496
- if (failure instanceof Error && failure.name === "AbortError") throw failure;
497
- const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
498
- throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
499
- }
500
- if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
505
+ function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
501
506
  assertPlanningActive(options.signal);
502
507
  assertReviewedTransactionUnchanged(options, reviewed, next);
503
- return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
508
+ return { ...next, dependencies: [] };
504
509
  }
505
510
 
506
- /** Recovery observes real canonical receipts/nonces, then proves only unfinished
507
- * execution; known construction metadata is not recertified through getter mirrors. */
511
+ /** Recovery observes real canonical receipts, then evaluates only unfinished
512
+ * execution; the wallet owns its nonce and assignment at send time. */
508
513
  export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
514
+ const original = options.planned, matches = observedFields(original);
515
+ const fullReviewAdmitted = original.simulation?.admitted === true;
516
+ const assertInput = () => {
517
+ if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
518
+ assertFinalPlan(original, options.signal);
519
+ };
509
520
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
510
521
  // Snapshot only the reviewed wire fields; caller proofs and commands are never
511
522
  // reused, and final checks compare the mutable input with this one local copy.
523
+ const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
512
524
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
513
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
525
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
526
+ const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
514
527
  const result = await withLifecycleReadClient(options.client, async (client) => {
528
+ registerLifecycleFinalCheck(client, assertInput);
515
529
  const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
516
530
  const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
517
531
  if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
@@ -524,12 +538,12 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
524
538
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
525
539
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
526
540
  // Only the confirmed canonical terminal phase is terminal; a head-only
527
- // Active observation is not, and the confirmation/nonce/receipt gates below
528
- // remain in force for every nonterminal case.
541
+ // Active observation is not, and the launch-state/receipt confirmation
542
+ // policy below remains in force for every nonterminal case.
529
543
 
530
544
  return undefined;
531
545
  }
532
- if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
546
+ if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
533
547
  if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed")) throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
534
548
  assertPlanningActive(options.signal);
535
549
  if (options.action === "cancel") {
@@ -538,54 +552,53 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
538
552
  assertFinalPlan(planned, options.signal);
539
553
  const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
540
554
  : { 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 }] };
541
- transaction.nonce = progress.pendingAccountNonce;
542
555
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
543
- const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
556
+ 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 });
544
557
  if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
545
558
  const next = attachEstimates([transaction], simulation)[0];
546
559
  if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
547
- return preflightNextTransaction(options, next, simulation, reviewed);
560
+ return finalizeNextTransaction(options, next, reviewed);
548
561
  }
549
562
  const { planned } = options;
550
563
  assertFinalPlan(planned, options.signal);
564
+ if (progress.canonical.phase === LifecyclePhase.None && !fullReviewAdmitted) 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");
551
565
  const funding = progress.canonical.phase === LifecyclePhase.None
552
566
  ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
553
567
  : { prerequisites: [], approvals: [] };
554
- const prepared = progress.canonical.preparedMarkets;
555
- let batches: number[] = [];
556
- if (planned.mode === "staged") {
557
- let suffixStart = prepared;
558
- const prefix = prepared > 0 ? [prepared] : [];
559
- if (reviewed?.kind === "prepare") {
560
- const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
561
- const first = Number(span?.[1]), count = Number(span?.[2]);
562
- if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
563
- prefix.push(count);
564
- suffixStart += count;
565
- }
566
- const suffix = planned.transactions
567
- .filter((transaction) => transaction.kind === "prepare" && transaction.marketStart! + transaction.marketCount! > suffixStart)
568
- .map((transaction) => transaction.marketStart! + transaction.marketCount! - Math.max(transaction.marketStart!, suffixStart));
569
- if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
570
- const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
571
- const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
572
- suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
573
- }
574
- batches = [...prefix, ...suffix];
568
+ let candidate = funding.approvals[0];
569
+ if (candidate === undefined) {
570
+ const prepared = progress.canonical.preparedMarkets;
571
+ let command: LifecycleCommand;
572
+ if (progress.canonical.phase === LifecyclePhase.None) command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
573
+ else if (prepared < planned.plan.markets.length) {
574
+ let count: number;
575
+ if (reviewed?.kind === "prepare") {
576
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
577
+ const first = Number(span?.[1]);
578
+ count = Number(span?.[2]);
579
+ if (first !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
580
+ } else {
581
+ const stored = planned.transactions.find((transaction) => transaction.kind === "prepare" && transaction.marketStart! <= prepared && transaction.marketStart! + transaction.marketCount! > prepared);
582
+ if (stored !== undefined) count = stored.marketStart! + stored.marketCount! - prepared;
583
+ else {
584
+ const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
585
+ const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
586
+ count = packPreparationBatches(planned.plan, prepared, cap)[prepared > 0 ? 1 : 0]!;
587
+ }
588
+ }
589
+ if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must be a positive contiguous canonical market span");
590
+ command = { kind: "prepare", first: prepared, count };
591
+ } else command = { kind: "activate" };
592
+ candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
575
593
  }
576
- const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
577
- if (candidate === undefined) return undefined;
578
594
  if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
579
- const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
580
- const compute = observed?.computeGasRequired;
581
- const sizing: GasSizingHints | undefined = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
582
- const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
595
+ const simulation = await evaluateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, { finalEnvelope: envelope, immediate: true });
583
596
  assertFinalPlan(planned, options.signal);
584
597
  if (!simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
585
598
  const next = attachEstimates([candidate], simulation)[0]!;
586
- return preflightNextTransaction(options, next, simulation, reviewed);
599
+ return finalizeNextTransaction(options, next, reviewed);
587
600
  });
588
- assertFinalPlan(options.planned, options.signal);
601
+ assertInput();
589
602
  if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
590
603
  return result;
591
604
  }
@@ -595,12 +608,12 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
595
608
  export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
596
609
  const { client, planned } = options;
597
610
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
598
- const current = await planLaunch({
611
+ const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
599
612
  client, plan: planned.plan, account: planned.account, mode: planned.mode,
600
613
  limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
601
614
  receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
602
615
  ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
603
- });
616
+ }, readClient, { forceSimulation: true }));
604
617
  assertPlannedDeploymentBindings(planned, current);
605
618
  return current.simulation;
606
619
  }
@@ -35,6 +35,9 @@ export enum KnownLifecycleHook {
35
35
  Object.freeze(KnownLifecycleProfile);
36
36
  Object.freeze(KnownLifecycleHook);
37
37
 
38
+ /** Total launch-transaction gas limit, including Nitro poster gas; unused gas is refunded. */
39
+ export const ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT = 32_000_000n;
40
+
38
41
  /** Frozen construction metadata, not a current eligibility or execution observation. */
39
42
  export type LifecycleProfilePreset = {
40
43
  readonly key: KnownLifecycleProfile;
@@ -61,9 +64,12 @@ export type LifecycleDeploymentPreset = {
61
64
  readonly tokenFactory: Address;
62
65
  readonly tokenFactoryCodeHash: Hex;
63
66
  readonly fundingEscrow: Address;
64
- /** Optional read-only acceleration contracts. An unset lens is not a deployed feature. */
67
+ readonly nativeTransactionGasLimit?: bigint;
68
+ /** Optional SDK-owned read-only acceleration contracts. */
65
69
  readonly hookSaltMiner?: Address;
66
70
  readonly launchLens?: Address;
71
+ /** Frozen reviewed runtime identity, like tokenFactoryCodeHash; not live admission. */
72
+ readonly launchLensCodeHash?: Hex;
67
73
  readonly profiles: readonly LifecycleProfilePreset[];
68
74
  readonly provenance: {
69
75
  readonly sourceCommit: string;
@@ -328,7 +334,12 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
328
334
  tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
329
335
  tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
330
336
  fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
337
+ nativeTransactionGasLimit: ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT,
331
338
  hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
339
+ // LaunchLens/871f9385, L2 deployment block 100095353.
340
+ // tx 0xafdf35fa50d2c3fe08f1ab231a4e5d5e4dd5629c57f14dfe0a3aff5b0b8404ff
341
+ launchLens: "0xcb4bb3caca71fe52eace8170cf83e901418646f8",
342
+ launchLensCodeHash: "0xc19e86bfed47afbb6ff719293edd8fb8bb3399d5d017b3f533d90cc72e58f1d3",
332
343
  profiles: Object.freeze([
333
344
  abyssPreset(KnownLifecycleProfile.Abyss0, "0x49b2ed65e2021247951193dd2c671b40867b6e064af65a2d98a70f57556412fc"),
334
345
  abyssPreset(KnownLifecycleProfile.Abyss1, "0x2f6f494f8810473b3c0ac6b03b1a1353d067ffa5c86d3cba16b04c6b61f73c05"),