@black-market/sdk 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +9 -7
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/auto-compound.d.ts +21 -0
  5. package/dist/lifecycle/auto-compound.d.ts.map +1 -0
  6. package/dist/lifecycle/auto-compound.js +34 -0
  7. package/dist/lifecycle/auto-compound.js.map +1 -0
  8. package/dist/lifecycle/calibration.d.ts +2 -4
  9. package/dist/lifecycle/calibration.d.ts.map +1 -1
  10. package/dist/lifecycle/calibration.js +1 -26
  11. package/dist/lifecycle/calibration.js.map +1 -1
  12. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
  13. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
  14. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
  15. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
  16. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
  17. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
  18. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
  19. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
  20. package/dist/lifecycle/gas-profile.d.ts +39 -0
  21. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  22. package/dist/lifecycle/gas-profile.js +193 -0
  23. package/dist/lifecycle/gas-profile.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +6 -4
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +6 -4
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +2 -0
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +30 -3
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/opening-buys.d.ts +13 -0
  33. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  34. package/dist/lifecycle/opening-buys.js +171 -0
  35. package/dist/lifecycle/opening-buys.js.map +1 -0
  36. package/dist/lifecycle/planner.d.ts +6 -9
  37. package/dist/lifecycle/planner.d.ts.map +1 -1
  38. package/dist/lifecycle/planner.js +175 -265
  39. package/dist/lifecycle/planner.js.map +1 -1
  40. package/dist/lifecycle/presets.d.ts +10 -3
  41. package/dist/lifecycle/presets.d.ts.map +1 -1
  42. package/dist/lifecycle/presets.js +13 -1
  43. package/dist/lifecycle/presets.js.map +1 -1
  44. package/dist/lifecycle/progress.d.ts +1 -1
  45. package/dist/lifecycle/progress.d.ts.map +1 -1
  46. package/dist/lifecycle/progress.js +21 -20
  47. package/dist/lifecycle/progress.js.map +1 -1
  48. package/dist/lifecycle/rpc.d.ts +25 -30
  49. package/dist/lifecycle/rpc.d.ts.map +1 -1
  50. package/dist/lifecycle/rpc.js +63 -81
  51. package/dist/lifecycle/rpc.js.map +1 -1
  52. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  53. package/dist/lifecycle/snapshot.js +2 -4
  54. package/dist/lifecycle/snapshot.js.map +1 -1
  55. package/dist/lifecycle/transactions.d.ts +29 -6
  56. package/dist/lifecycle/transactions.d.ts.map +1 -1
  57. package/dist/lifecycle/transactions.js +62 -27
  58. package/dist/lifecycle/transactions.js.map +1 -1
  59. package/dist/lifecycle/types.d.ts +55 -122
  60. package/dist/lifecycle/types.d.ts.map +1 -1
  61. package/dist/lifecycle/types.js +3 -3
  62. package/dist/lifecycle/types.js.map +1 -1
  63. package/dist/lifecycle/ufg.d.ts +1 -1
  64. package/dist/lifecycle/ufg.d.ts.map +1 -1
  65. package/dist/lifecycle/ufg.js +4 -8
  66. package/dist/lifecycle/ufg.js.map +1 -1
  67. package/docs/launch-lifecycle.md +110 -78
  68. package/docs/lifecycle-gas-profiles.md +90 -0
  69. package/docs/reference.md +99 -47
  70. package/examples/.env.example +0 -3
  71. package/examples/README.md +14 -11
  72. package/examples/launch-example-support.ts +4 -9
  73. package/examples/launch-example.ts +30 -36
  74. package/examples/launch-lifecycle-smoke.ts +11 -21
  75. package/examples/launch.ts +8 -11
  76. package/package.json +1 -1
  77. package/src/lifecycle/authors.ts +2 -2
  78. package/src/lifecycle/auto-compound.ts +38 -0
  79. package/src/lifecycle/calibration.ts +3 -23
  80. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  81. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  82. package/src/lifecycle/gas-profile.ts +186 -0
  83. package/src/lifecycle/index.ts +6 -4
  84. package/src/lifecycle/markets.ts +25 -3
  85. package/src/lifecycle/opening-buys.ts +157 -0
  86. package/src/lifecycle/planner.ts +168 -247
  87. package/src/lifecycle/presets.ts +15 -1
  88. package/src/lifecycle/progress.ts +19 -20
  89. package/src/lifecycle/rpc.ts +58 -68
  90. package/src/lifecycle/snapshot.ts +2 -4
  91. package/src/lifecycle/transactions.ts +69 -27
  92. package/src/lifecycle/types.ts +60 -99
  93. package/src/lifecycle/ufg.ts +4 -8
  94. package/dist/lifecycle/fork.d.ts +0 -15
  95. package/dist/lifecycle/fork.d.ts.map +0 -1
  96. package/dist/lifecycle/fork.js +0 -36
  97. package/dist/lifecycle/fork.js.map +0 -1
  98. package/dist/lifecycle/simulation.d.ts +0 -18
  99. package/dist/lifecycle/simulation.d.ts.map +0 -1
  100. package/dist/lifecycle/simulation.js +0 -754
  101. package/dist/lifecycle/simulation.js.map +0 -1
  102. package/src/lifecycle/fork.ts +0 -36
  103. package/src/lifecycle/simulation.ts +0 -689
@@ -5,23 +5,53 @@ 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";
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
- import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
8
+ import { lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
+ import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecyclePlanningAssessment, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type ResolvedLifecycleLimits } from "./types.js";
10
+ import { deriveKnownPoolBoundHookDeployment, validateBuySlippageBps } from "./calibration.js";
12
11
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
- import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
12
+ import { buildLifecycleCommand, packPreparationBatches, type LifecycleCommand } from "./transactions.js";
14
13
  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";
14
+ import { assertUfgLaunchAccount, hashUfgLaunchConfig, ufgPlanPrincipal, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
15
+ import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
16
  import { readLifecyclePlanningBlock } from "./snapshot.js";
18
17
  import { mineLifecycleHooks } from "./mining.js";
18
+ import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
19
+ import { assessLifecycleTransactions, deriveLifecycleLaunchCapacity, type NextTransactionEnvelope, type ProfiledLaunchCapacity } from "./gas-profile.js";
20
+ import type { LifecycleOpeningBuyQuote } from "./types.js";
19
21
 
20
22
  type ValidatedPlanShape = {
21
23
  commitment: Hex;
22
24
  ufgConfigHash?: Hex;
23
25
  marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
24
26
  };
27
+ type PlanReadContext = {
28
+ block?: LifecycleBlock;
29
+ openingBuys?: readonly LifecycleOpeningBuyQuote[];
30
+ };
31
+ const defaultPlanReadContext: PlanReadContext = {};
32
+
33
+ function observedFields<T extends object>(value: T): () => boolean {
34
+ const keys = Object.keys(value) as (keyof T)[];
35
+ const values = keys.map((key) => value[key]);
36
+ return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
37
+ }
38
+
39
+ async function selectLaunchMode(requested: "atomic" | "staged", plan: LaunchPlanV1, capacity: ProfiledLaunchCapacity | undefined,
40
+ limits: ResolvedLifecycleLimits, client: LifecycleRpcClient, block: LifecycleBlock): Promise<"atomic" | "staged"> {
41
+ if (requested !== "atomic" || plan.markets.length !== 1 || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
42
+ capacity?.atomic === undefined || capacity.bounds.atomicComputeGasUpperBound === undefined) return "staged";
43
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(capacity.atomic, await readNitroPrices(client, block)).posterGas : 0n;
44
+ return capacity.bounds.atomicComputeGasUpperBound + poster <= limits.executionGasCeiling ? "atomic" : "staged";
45
+ }
46
+
47
+ async function preparationBatches(plan: LaunchPlanV1, prepared: number, ufg: PlanLaunchOptions["ufg"], limits: ResolvedLifecycleLimits,
48
+ client: LifecycleRpcClient, block: LifecycleBlock): Promise<number[]> {
49
+ if (prepared === plan.markets.length) return prepared === 0 ? [] : [prepared];
50
+ const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
51
+ const sample = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }, route);
52
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(sample, await readNitroPrices(client, block)).posterGas : 0n;
53
+ return packPreparationBatches(plan, prepared, limits.executionGasCeiling - poster, limits.headroomBps);
54
+ }
25
55
 
26
56
  function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
27
57
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
@@ -137,30 +167,38 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
137
167
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
138
168
  }
139
169
 
140
- function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"]): LifecycleTransaction[] {
170
+ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"], capacity?: ProfiledLaunchCapacity): LifecycleTransaction[] {
141
171
  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 });
172
+ const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
173
+ const compiled: LifecycleTransaction[] = [];
174
+ if (mode === "atomic") compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
175
+ else {
176
+ compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
177
+ let first = 0;
178
+ for (const count of batches) { compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route)); first += count; }
179
+ compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
180
+ }
145
181
  const commands = compiled.filter((transaction) =>
146
182
  (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
147
183
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
148
184
  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] }));
185
+ return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
150
186
  }
151
187
 
152
- 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 } };
188
+ function attachEstimates(transactions: readonly LifecycleTransaction[], assessment: LifecyclePlanningAssessment): LifecycleTransaction[] {
189
+ return transactions.map((transaction, index) => {
190
+ if (index !== 0) return transaction;
191
+ if (transaction.kind === "approve" || transaction.kind === "approve-reset") return { ...transaction, assessment };
192
+ const estimate = assessment.estimate;
193
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, assessment };
157
194
  });
158
195
  }
159
- type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value" | "nonce"> & NextTransactionEnvelope;
196
+ type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
160
197
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
161
198
 
162
199
  function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
163
200
  if (transaction === undefined) return undefined;
201
+ const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
164
202
  if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
165
203
  !reviewedTransactionKinds.includes(transaction.kind) ||
166
204
  !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
@@ -168,9 +206,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
168
206
  typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
169
207
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
170
208
  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)) {
209
+ !walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
210
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
174
211
  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
212
  }
176
213
  return transaction as ReviewedNextTransaction;
@@ -179,23 +216,22 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
179
216
  function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
180
217
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
181
218
  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);
219
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
184
220
  }
185
221
 
186
222
  function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
187
223
  if (options.planned.ufg !== undefined) assertUfgLaunchAccount(options.planned.plan, options.planned.account);
188
224
  if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
189
- throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
225
+ throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
190
226
  }
191
227
  if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
192
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
228
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
193
229
  }
194
230
  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) {
198
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
231
+ if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
232
+ reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
233
+ (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
234
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
199
235
  }
200
236
  }
201
237
 
@@ -210,16 +246,6 @@ async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator:
210
246
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
211
247
  }
212
248
 
213
- function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
214
- if (hashLaunchPlan(stored.plan).toLowerCase() !== stored.planHash.toLowerCase() || current.planHash.toLowerCase() !== stored.planHash.toLowerCase() || current.launchId.toLowerCase() !== stored.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its finalized commitment");
215
- if (stored.ufg === undefined ? current.ufg !== undefined : current.ufg === undefined || hashUfgLaunchConfig(stored.ufg.config) !== stored.ufg.configHash || current.ufg.configHash !== stored.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored UFG route or treasury config no longer matches its finalized commitment");
216
- if (current.predictedToken.toLowerCase() !== stored.predictedToken.toLowerCase() || current.tokenFactory.toLowerCase() !== stored.tokenFactory.toLowerCase() || current.tokenFactoryCodeHash.toLowerCase() !== stored.tokenFactoryCodeHash.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Final token prediction or its immutable factory code binding changed before execution");
217
- if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
218
- if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
219
- const fresh = current.hookDeployments[index];
220
- return fresh === undefined || fresh.marketIndex !== deployment.marketIndex || fresh.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || fresh.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() || fresh.salt.toLowerCase() !== deployment.salt.toLowerCase() || fresh.predictedHook.toLowerCase() !== deployment.predictedHook.toLowerCase();
221
- })) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
222
- }
223
249
 
224
250
  function assertPlanningActive(signal?: AbortSignal): void {
225
251
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
@@ -228,7 +254,7 @@ function assertPlanningActive(signal?: AbortSignal): void {
228
254
  function validatePlanningIntent(options: PlanLaunchOptions): ValidatedPlanShape {
229
255
  const { plan, account, mode } = options;
230
256
  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");
257
+ if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
232
258
  if (options.ufg !== undefined) assertUfgLaunchAccount(plan, account);
233
259
  else if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
234
260
  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 +298,20 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
272
298
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
273
299
  }
274
300
 
275
- /** One owner constructs and mines, optionally measures actual ordered buy outputs,
276
- * then proves only the final protected commitment. No cached execution certificates. */
301
+
302
+ /** One owner mines constructor-bound salts and protects ordered buys with exact
303
+ * local curve math. Launch calldata is never executed during planning. */
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,22 @@ 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
- let ownedProgress: CanonicalLaunchProgress | undefined;
327
- let sizing: GasSizingHints | undefined;
337
+ let openingBuys: PlanReadContext["openingBuys"];
328
338
  if (needsProtection) {
329
- // A diagnostic minimum is never returned as an admitted or executable plan.
330
- // Price guards, funding minima, recipients and all ordered budgets stay exact.
331
- 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));
333
- assertDraft();
334
- 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
- plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
336
- // This is the real SDK-owned None observation, not an assumed new-launch
337
- // state. No onchain commitment existed at this exact snapshot to replace.
338
- ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
339
- sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed!]));
339
+ if (!hasProfiledOpeningMath(plan)) throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Exact opening-buy math is unavailable for this profile. Use a supported profile or update the SDK.");
340
+ const head = await readLifecycleHeadProgress(client, plan, options.account, block);
341
+ if (head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
342
+ const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
343
+ openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
344
+ const factor = BigInt(10000 - options.buySlippageBps!);
345
+ plan = { ...plan, buys: plan.buys.map((buy, index) => {
346
+ const minTokenOut = openingBuys![index]!.tokenOut * factor / 10000n;
347
+ if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
348
+ return { ...buy, minTokenOut };
349
+ }) };
340
350
  }
341
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
351
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
342
352
  assertDraft();
343
353
  assertFinalPlan(planned, options.signal);
344
354
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -347,7 +357,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
347
357
  if (final === undefined || final.deployer.toLowerCase() !== mined.deployer.toLowerCase() || final.initCodeHash.toLowerCase() !== mined.initCodeHash.toLowerCase() || final.salt.toLowerCase() !== mined.salt.toLowerCase() || final.predictedHook.toLowerCase() !== mined.predictedHook.toLowerCase()) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
348
358
  }
349
359
  }
350
- if (needsProtection && !planned.simulation.admitted) throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
360
+ if (needsProtection && !planned.assessment.ready) throw new LifecyclePlanningError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "The launch could not be prepared. Refresh the review before continuing.", planned.assessment);
351
361
  return planned;
352
362
  });
353
363
  assertDraft();
@@ -355,7 +365,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
355
365
  return result;
356
366
  }
357
367
 
358
- /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
368
+ /** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
359
369
  export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
360
370
  const result = await withLifecycleReadClient(options.client, async (client) => {
361
371
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
@@ -366,43 +376,35 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
366
376
  return result;
367
377
  }
368
378
 
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;
379
+ async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, readContext: PlanReadContext = defaultPlanReadContext): Promise<PlannedLaunch> {
380
+ const { block: pinnedBlock, openingBuys } = readContext;
381
+ const { plan, account, ufg } = options;
371
382
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
372
383
  const launchId = hashLaunchIdentity(plan);
373
384
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
374
385
  pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
375
386
  ]));
376
387
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
377
- // Only preparation/build-next can supply freshly observed progress from this
378
- // still-open SDK invocation. Stored/query DTOs never enter this continuation.
379
- if (ownedProgress !== undefined) {
380
- if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
381
- ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
382
- ownedProgress.confirmationDepth !== (options.confirmations ?? 1)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
383
- if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
384
- }
385
388
  assertPlanningActive(options.signal);
386
389
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
387
390
  const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
388
391
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
389
392
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
390
393
  lifecycleStage(client, "plan.progress", async () => {
391
- const [predictedToken] = await Promise.all([
392
- readLifecycleTokenAtBlock(client, plan, block),
393
- ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
394
+ const [predictedToken, head] = await Promise.all([
395
+ readLifecycleTokenAtBlock(client, plan, block), readLifecycleHeadProgress(client, plan, account, block),
394
396
  ]);
397
+ const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" as const : "atomic" as const;
395
398
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
396
399
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
397
400
  if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
398
- const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
399
- if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
401
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
400
402
  return { identity, progress };
401
403
  }),
402
404
  ]));
403
405
  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: [] };
405
- let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
406
+ const placeholder: LifecyclePlanningAssessment = { ready: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits };
407
+ let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, assessment: placeholder, limits: options.limits,
406
408
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
407
409
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
408
410
  if (!progress.confirmationSafe || receiptsPending) {
@@ -413,194 +415,113 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
413
415
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
414
416
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
415
417
  }
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" };
417
-
418
- return { ...planned, hookDeployments, simulation: waiting };
418
+ return { ...planned, hookDeployments, assessment: { ...placeholder,
419
+ code: !progress.confirmationSafe ? "UNCONFIRMED_STATE" : "RECEIPT_PENDING",
420
+ reason: !progress.confirmationSafe ? "Wait for the selected launch confirmation depth before continuing." : "Wait for the pending transaction to confirm or resolve its replacement." } };
419
421
  }
420
422
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
421
-
422
- return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
423
+ return { ...planned, assessment: { ...placeholder, ready: true } };
423
424
  }
424
- // Topology is a creation policy, not a retroactive obstacle to recovery of
425
- // immutable onchain commitments. Waiting and terminal launches returned above.
426
425
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None) validatePlanTopology(options, marketConfigs, identity.predictedToken);
427
426
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
428
427
  progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block, ufg === undefined ? plan.creator : account, ufg !== undefined) : { prerequisites: [], approvals: [] },
429
428
  readPlanMetadata(client, plan, identity.predictedToken, block),
430
429
  ]));
431
430
  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");
440
- }
441
- if (metadata.reasons.length > 0) {
442
-
443
- 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) };
431
+ const capacity = deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys);
432
+ const mode = progress.canonical.phase === LifecyclePhase.None
433
+ ? await selectLaunchMode(options.mode, plan, capacity, limits, client, block) : identity.mode;
434
+ planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
435
+ if (metadata.reasons.length > 0) return { ...planned, assessment: { ...placeholder, code: "PROFILE_UNAVAILABLE", failureCategory: "semantic",
436
+ reason: "The selected launch profile could not be read or is unavailable. Refresh the review before continuing." } };
437
+ let batches: number[];
438
+ try { batches = mode === "staged" ? await preparationBatches(plan, progress.canonical.preparedMarkets, ufg, limits, client, block) : []; }
439
+ catch (failure) {
440
+ if (!(failure instanceof LifecyclePlanningError) || failure.code !== "PREPARATION_GAS_LIMIT") throw failure;
441
+ return { ...planned, assessment: { ...placeholder, code: failure.code, reason: failure.message, failureCategory: "capacity", capacityConstraint: "preparation" } };
444
442
  }
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);
450
- 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` };
471
- }
472
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
473
- } finally { preparedSimulation.close(); }
443
+ const transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
444
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: options.limits, signal: options.signal }, transactions, block, undefined, openingBuys, capacity);
445
+ assertFinalPlan(planned, options.signal);
446
+ return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
474
447
  }
475
448
 
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);
449
+ function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
501
450
  assertPlanningActive(options.signal);
502
451
  assertReviewedTransactionUnchanged(options, reviewed, next);
503
- return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
452
+ return { ...next, dependencies: [] };
504
453
  }
505
454
 
506
- /** Recovery observes real canonical receipts/nonces, then proves only unfinished
507
- * execution; known construction metadata is not recertified through getter mirrors. */
455
+ /** Recovery observes actual canonical receipts, then constructs and budgets only
456
+ * the next command. No launch transaction is executed during planning. */
508
457
  export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
458
+ const original = options.planned, matches = observedFields(original);
459
+ const assertInput = () => {
460
+ if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
461
+ assertFinalPlan(original, options.signal);
462
+ };
509
463
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
510
- // Snapshot only the reviewed wire fields; caller proofs and commands are never
511
- // reused, and final checks compare the mutable input with this one local copy.
464
+ const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
512
465
  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 });
466
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
467
+ const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
514
468
  const result = await withLifecycleReadClient(options.client, async (client) => {
515
- const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
516
- const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
517
- if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
518
- await Promise.all([
519
- readLifecycleTokenAtBlock(client, options.planned.plan, block),
520
- readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
521
- resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
522
- ]);
523
- }
469
+ registerLifecycleFinalCheck(client, assertInput);
470
+ const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
471
+ const limitSource = options.limits === undefined ? original.limits : options.limits;
524
472
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
525
- if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
526
- // 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.
529
-
530
- return undefined;
531
- }
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");
533
- 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");
473
+ if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return undefined;
474
+ if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected launch confirmation depth before continuing.");
475
+ if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed")) throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for the pending transaction to confirm or resolve its replacement.");
534
476
  assertPlanningActive(options.signal);
477
+ const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
535
478
  if (options.action === "cancel") {
536
479
  if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
537
- const { planned } = options;
538
- assertFinalPlan(planned, options.signal);
480
+ const planned = { ...original, progress };
539
481
  const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
540
482
  : { 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
- 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);
544
- if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
545
- const next = attachEstimates([transaction], simulation)[0];
546
- if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
547
- return preflightNextTransaction(options, next, simulation, reviewed);
483
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current cancellation");
484
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [transaction], block, envelope);
485
+ if (!assessment.ready) throw new LifecyclePlanningError(assessment.code ?? "CANCEL_NOT_READY", assessment.reason ?? "Cancellation could not be prepared. Refresh before continuing.", assessment);
486
+ return finalizeNextTransaction(options, attachEstimates([transaction], assessment)[0]!, reviewed);
548
487
  }
549
- const { planned } = options;
550
- assertFinalPlan(planned, options.signal);
551
488
  const funding = progress.canonical.phase === LifecyclePhase.None
552
- ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
489
+ ? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
553
490
  : { 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];
491
+ const planned = { ...original, progress, prerequisites: funding.prerequisites };
492
+ const capacity = deriveLifecycleLaunchCapacity({ plan: planned.plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps });
493
+ if (progress.canonical.phase === LifecyclePhase.None &&
494
+ await selectLaunchMode(planned.mode, planned.plan, capacity, limits, client, block) !== planned.mode) {
495
+ throw new LifecyclePlanningError("MODE_REVIEW_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.");
496
+ }
497
+ let candidate = funding.approvals[0];
498
+ if (candidate === undefined) {
499
+ const prepared = progress.canonical.preparedMarkets;
500
+ let command: LifecycleCommand;
501
+ if (progress.canonical.phase === LifecyclePhase.None) command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
502
+ else if (prepared < planned.plan.markets.length) {
503
+ let count: number;
504
+ if (reviewed?.kind === "prepare") {
505
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
506
+ if (Number(span?.[1]) !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
507
+ count = Number(span?.[2]);
508
+ } else {
509
+ const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
510
+ count = batches[prepared > 0 ? 1 : 0]!;
511
+ }
512
+ if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must cover a positive contiguous unfinished market span");
513
+ command = { kind: "prepare", first: prepared, count };
514
+ } else command = { kind: "activate" };
515
+ candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
575
516
  }
576
- const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
577
- if (candidate === undefined) return undefined;
578
- 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);
517
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current next step");
518
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [candidate], block, envelope, undefined, capacity);
583
519
  assertFinalPlan(planned, options.signal);
584
- if (!simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
585
- const next = attachEstimates([candidate], simulation)[0]!;
586
- return preflightNextTransaction(options, next, simulation, reviewed);
520
+ if (!assessment.ready) throw new LifecyclePlanningError(assessment.code ?? "PLAN_NOT_READY", assessment.reason ?? "The next launch step could not be prepared. Refresh before continuing.", assessment);
521
+ return finalizeNextTransaction(options, attachEstimates([candidate], assessment)[0]!, reviewed);
587
522
  });
588
- assertFinalPlan(options.planned, options.signal);
523
+ assertInput();
589
524
  if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
590
525
  return result;
591
526
  }
592
527
 
593
- /** Re-simulates only canonical unfinished work with the same submitted receipt
594
- * evidence and confirmation depth as build-next, never silently dropping context. */
595
- export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
596
- const { client, planned } = options;
597
- 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({
599
- client, plan: planned.plan, account: planned.account, mode: planned.mode,
600
- limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
601
- receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
602
- ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
603
- });
604
- assertPlannedDeploymentBindings(planned, current);
605
- return current.simulation;
606
- }