@black-market/sdk 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +9 -8
  2. package/dist/lifecycle/auto-compound.d.ts +21 -0
  3. package/dist/lifecycle/auto-compound.d.ts.map +1 -0
  4. package/dist/lifecycle/auto-compound.js +34 -0
  5. package/dist/lifecycle/auto-compound.js.map +1 -0
  6. package/dist/lifecycle/calibration.d.ts +2 -4
  7. package/dist/lifecycle/calibration.d.ts.map +1 -1
  8. package/dist/lifecycle/calibration.js +1 -26
  9. package/dist/lifecycle/calibration.js.map +1 -1
  10. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
  11. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
  12. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
  13. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
  14. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
  15. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
  16. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
  17. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
  18. package/dist/lifecycle/gas-profile.d.ts +17 -13
  19. package/dist/lifecycle/gas-profile.d.ts.map +1 -1
  20. package/dist/lifecycle/gas-profile.js +125 -115
  21. package/dist/lifecycle/gas-profile.js.map +1 -1
  22. package/dist/lifecycle/index.d.ts +4 -4
  23. package/dist/lifecycle/index.d.ts.map +1 -1
  24. package/dist/lifecycle/index.js +4 -4
  25. package/dist/lifecycle/index.js.map +1 -1
  26. package/dist/lifecycle/markets.d.ts +2 -0
  27. package/dist/lifecycle/markets.d.ts.map +1 -1
  28. package/dist/lifecycle/markets.js +30 -3
  29. package/dist/lifecycle/markets.js.map +1 -1
  30. package/dist/lifecycle/opening-buys.js +1 -1
  31. package/dist/lifecycle/opening-buys.js.map +1 -1
  32. package/dist/lifecycle/planner.d.ts +6 -9
  33. package/dist/lifecycle/planner.d.ts.map +1 -1
  34. package/dist/lifecycle/planner.js +102 -207
  35. package/dist/lifecycle/planner.js.map +1 -1
  36. package/dist/lifecycle/presets.d.ts +7 -3
  37. package/dist/lifecycle/presets.d.ts.map +1 -1
  38. package/dist/lifecycle/presets.js +10 -1
  39. package/dist/lifecycle/presets.js.map +1 -1
  40. package/dist/lifecycle/rpc.d.ts +3 -13
  41. package/dist/lifecycle/rpc.d.ts.map +1 -1
  42. package/dist/lifecycle/rpc.js +11 -37
  43. package/dist/lifecycle/rpc.js.map +1 -1
  44. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  45. package/dist/lifecycle/snapshot.js +1 -2
  46. package/dist/lifecycle/snapshot.js.map +1 -1
  47. package/dist/lifecycle/transactions.d.ts +10 -3
  48. package/dist/lifecycle/transactions.d.ts.map +1 -1
  49. package/dist/lifecycle/transactions.js +40 -10
  50. package/dist/lifecycle/transactions.js.map +1 -1
  51. package/dist/lifecycle/types.d.ts +32 -107
  52. package/dist/lifecycle/types.d.ts.map +1 -1
  53. package/dist/lifecycle/types.js +3 -3
  54. package/dist/lifecycle/types.js.map +1 -1
  55. package/dist/lifecycle/ufg.d.ts +1 -1
  56. package/dist/lifecycle/ufg.d.ts.map +1 -1
  57. package/dist/lifecycle/ufg.js +1 -1
  58. package/dist/lifecycle/ufg.js.map +1 -1
  59. package/docs/launch-lifecycle.md +43 -0
  60. package/docs/lifecycle-gas-profiles.md +1 -1
  61. package/docs/reference.md +56 -2
  62. package/examples/.env.example +0 -3
  63. package/examples/README.md +10 -10
  64. package/examples/launch-example-support.ts +4 -9
  65. package/examples/launch-example.ts +12 -28
  66. package/examples/launch-lifecycle-smoke.ts +10 -22
  67. package/examples/launch.ts +8 -11
  68. package/package.json +1 -1
  69. package/src/lifecycle/auto-compound.ts +38 -0
  70. package/src/lifecycle/calibration.ts +3 -23
  71. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  72. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  73. package/src/lifecycle/gas-profile.ts +117 -106
  74. package/src/lifecycle/index.ts +4 -4
  75. package/src/lifecycle/markets.ts +25 -3
  76. package/src/lifecycle/opening-buys.ts +1 -1
  77. package/src/lifecycle/planner.ts +98 -190
  78. package/src/lifecycle/presets.ts +10 -1
  79. package/src/lifecycle/rpc.ts +10 -32
  80. package/src/lifecycle/snapshot.ts +1 -2
  81. package/src/lifecycle/transactions.ts +39 -9
  82. package/src/lifecycle/types.ts +38 -87
  83. package/src/lifecycle/ufg.ts +1 -1
  84. package/dist/lifecycle/fork.d.ts +0 -15
  85. package/dist/lifecycle/fork.d.ts.map +0 -1
  86. package/dist/lifecycle/fork.js +0 -36
  87. package/dist/lifecycle/fork.js.map +0 -1
  88. package/dist/lifecycle/simulation.d.ts +0 -17
  89. package/dist/lifecycle/simulation.d.ts.map +0 -1
  90. package/dist/lifecycle/simulation.js +0 -605
  91. package/dist/lifecycle/simulation.js.map +0 -1
  92. package/src/lifecycle/fork.ts +0 -36
  93. package/src/lifecycle/simulation.ts +0 -543
@@ -3,6 +3,7 @@ import type { MarketConfigV1 } from "./schema.js";
3
3
  import { LifecyclePlanningError, type LifecycleProfile, type PoolBoundHookSaltProgress } from "./types.js";
4
4
  import { decodeHmmLaunchTail, encodeHmmLaunchTail, isHmmProfile, type HmmLaunchConfig } from "./hmm.js";
5
5
  import { decodeUfgLaunchConfig, encodeUfgLaunchConfig, isUfgProfile, type UfgLaunchConfig } from "./ufg-config.js";
6
+ import { decodeAutoCompoundTail, encodeAutoCompoundTail, isAutoCompoundProfile, validateAutoCompoundConfig, type AutoCompoundConfigV1 } from "./auto-compound.js";
6
7
 
7
8
  export type V4LifecyclePositionConfig = {
8
9
  tickLower: number; tickUpper: number; liquidity: bigint; salt: Hex; maxTokenAmount: bigint;
@@ -46,6 +47,7 @@ export type V4PoolBoundLifecycleMarketConfigV6 = Omit<V4LifecycleMarketConfig, "
46
47
  version: 6; minimumHookFeePips: number; feeSensitivityPipsSecondsPerTick: number; hookSalt: Hex;
47
48
  hmmLaunchConfig?: HmmLaunchConfig;
48
49
  ufgLaunchConfig?: UfgLaunchConfig;
50
+ autoCompoundConfig?: AutoCompoundConfigV1;
49
51
  };
50
52
  export type V4PoolBoundLifecycleMarketConfig = V4PoolBoundLifecycleMarketConfigV5 | V4PoolBoundLifecycleMarketConfigV6;
51
53
  export const V4_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint8,uint8,address,bool,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
@@ -92,9 +94,22 @@ function assertV1HookFeeShape(config: object): void {
92
94
  }
93
95
  }
94
96
 
97
+ function validateAutoCompoundMarket(config: V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig): asserts config is V4PoolBoundLifecycleMarketConfigV6 & { autoCompoundConfig: AutoCompoundConfigV1 } {
98
+ if (config.version !== 6 || config.autoCompoundConfig === undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles require config6 and an explicit autoCompoundConfig");
99
+ if (config.hmmLaunchConfig !== undefined || config.ufgLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles cannot carry HMM or UFG tails");
100
+ if (config.lpFeePips !== 0) throw new LifecyclePlanningError("INVALID_HOOK_FEES", "Auto-compounding profiles require zero LP fee");
101
+ if (config.developerFeeBps !== 0) throw new LifecyclePlanningError("DEVELOPER_FEE_CEILING", "Auto-compounding profiles require zero developer fee");
102
+ validateAutoCompoundConfig(config.autoCompoundConfig);
103
+ }
104
+
95
105
  /** Auxiliary launch profiles require their typed tail and append it automatically. */
96
106
  export function encodePoolBoundV4LifecycleMarketConfig(config: V4PoolBoundLifecycleMarketConfig): Hex {
97
107
  const prefix = encodePoolBoundV4Prefix(config);
108
+ if (isAutoCompoundProfile(config.profileId)) {
109
+ validateAutoCompoundMarket(config);
110
+ return concatHex([prefix, encodeAutoCompoundTail(config.autoCompoundConfig)]);
111
+ }
112
+ if (config.version === 6 && config.autoCompoundConfig !== undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "An auto-compounding tail requires an auto-compounding profile");
98
113
  if (isUfgProfile(config.profileId)) {
99
114
  if (config.version !== 6 || config.ufgLaunchConfig === undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles require config6 and an explicit ufgLaunchConfig");
100
115
  if (config.hmmLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles cannot carry an HMM tail");
@@ -130,6 +145,10 @@ export function decodePoolBoundV4LifecycleMarketConfig(encoded: Hex, expectedVer
130
145
  const config = version === 5
131
146
  ? decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV5 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV5
132
147
  : decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV6 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV6;
148
+ if (isAutoCompoundProfile(config.profileId)) {
149
+ if (config.version !== 6) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles require config6");
150
+ config.autoCompoundConfig = decodeAutoCompoundTail(`0x${encoded.slice(encodePoolBoundV4Prefix(config).length)}`);
151
+ }
133
152
  if (isHmmProfile(config.profileId)) {
134
153
  if (config.version !== 6) throw new LifecyclePlanningError("INVALID_HMM_CONFIG", "HMM profiles require config6");
135
154
  const prefixLength = encodePoolBoundV4Prefix(config).length;
@@ -160,6 +179,7 @@ export function validateV4LifecycleMarket(options: {
160
179
  if (config.version !== 4 && !isPoolBoundV4ConfigVersion(config.version)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Reviewed V4 config version must be 4, 5 or 6");
161
180
  if (config.version === 6) validatePoolBoundHookFees(config);
162
181
  else if (config.version === 5) assertV1HookFeeShape(config);
182
+ if (isAutoCompoundProfile(config.profileId)) validateAutoCompoundMarket(config);
163
183
  const { envelope, developerTerms: terms, protocolMaximumDeveloperFeeBps: protocolCeiling } = profile;
164
184
  if (envelope === undefined || terms === undefined || protocolCeiling === undefined || !terms.enabled ||
165
185
  profile.topology.hookTopology !== (config.version === 4 ? 1 : 2) || market.configVersion !== config.version ||
@@ -258,9 +278,11 @@ export function hashPoolBoundV4MarketCommitment(options: { chainId: bigint; core
258
278
  if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound V4 market requires explicit config version 5 or 6");
259
279
  const config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
260
280
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Config profile differs from the committed market profile");
261
- // Collector factories commit only the V6 prefix, not auxiliary launch tails.
262
- // The full plan commitment separately binds every byte of those tails.
263
- const configHash = keccak256(encodePoolBoundV4Prefix({ ...config, hookSalt: zeroHash }));
281
+ // Auto-compounding binds its tail into constructor economics; older factories
282
+ // bind only the prefix. Every full plan commitment also binds every tail byte.
283
+ const normalized = { ...config, hookSalt: zeroHash };
284
+ const configHash = keccak256(isAutoCompoundProfile(config.profileId)
285
+ ? encodePoolBoundV4LifecycleMarketConfig(normalized) : encodePoolBoundV4Prefix(normalized));
264
286
  return keccak256(encodeAbiParameters([
265
287
  { type: "bytes32" }, { type: "uint256" }, { type: "address" }, { type: "address" }, { type: "address" },
266
288
  { type: "bytes32" }, { type: "bytes32" }, { type: "address" }, { type: "uint256" }, { type: "uint32" }, { type: "bytes32" },
@@ -52,7 +52,7 @@ function openingState(plan: LaunchPlanV1, marketIndex: number): OpeningState {
52
52
  const config = decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
53
53
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_MARKET", "V4 opening config differs from the selected profile");
54
54
  price = config.sqrtPriceX96; spacing = config.tickSpacing; lpFee = BigInt(config.lpFeePips); positions = config.positions;
55
- const dynamic = preset.key === KnownLifecycleProfile.V4DynamicFeePool || preset.key === KnownLifecycleProfile.V4HmmDynamicPool || preset.key === KnownLifecycleProfile.V4UfgDynamicPool;
55
+ const dynamic = preset.key === KnownLifecycleProfile.V4DynamicFeePool || preset.key === KnownLifecycleProfile.V4HmmDynamicPool || preset.key === KnownLifecycleProfile.V4UfgDynamicPool || preset.key === KnownLifecycleProfile.V4AutoCompoundDynamicPool;
56
56
  hookFee = BigInt(dynamic ? config.minimumHookFeePips! : config.hookFeePips);
57
57
  }
58
58
  const tick = getTickAtSqrtRatio(price), liquidityNet = new Map<number, bigint>();
@@ -5,10 +5,9 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./ab
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan, 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, 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
- 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
12
  import { buildLifecycleCommand, packPreparationBatches, type LifecycleCommand } from "./transactions.js";
14
13
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
@@ -17,7 +16,7 @@ import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
16
  import { readLifecyclePlanningBlock } from "./snapshot.js";
18
17
  import { mineLifecycleHooks } from "./mining.js";
19
18
  import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
20
- import { deriveLifecycleLaunchCapacity, profileLifecycleTransactions, type ProfiledLaunchCapacity } from "./gas-profile.js";
19
+ import { assessLifecycleTransactions, deriveLifecycleLaunchCapacity, type NextTransactionEnvelope, type ProfiledLaunchCapacity } from "./gas-profile.js";
21
20
  import type { LifecycleOpeningBuyQuote } from "./types.js";
22
21
 
23
22
  type ValidatedPlanShape = {
@@ -26,12 +25,34 @@ type ValidatedPlanShape = {
26
25
  marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
27
26
  };
28
27
  type PlanReadContext = {
29
- block?: LifecycleBlock; progress?: CanonicalLaunchProgress; diagnostic?: boolean;
30
- finalEnvelope?: NextTransactionEnvelope; capacity?: ProfiledLaunchCapacity;
31
- openingBuys?: readonly LifecycleOpeningBuyQuote[]; forceSimulation?: boolean; immediate?: boolean;
28
+ block?: LifecycleBlock;
29
+ openingBuys?: readonly LifecycleOpeningBuyQuote[];
32
30
  };
33
31
  const defaultPlanReadContext: PlanReadContext = {};
34
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
+ }
55
+
35
56
  function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
36
57
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
37
58
  const commitment = hashLaunchPlan(plan);
@@ -164,14 +185,12 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
164
185
  return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
165
186
  }
166
187
 
167
- function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
188
+ function attachEstimates(transactions: readonly LifecycleTransaction[], assessment: LifecyclePlanningAssessment): LifecycleTransaction[] {
168
189
  return transactions.map((transaction, index) => {
169
190
  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 };
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 };
175
194
  });
176
195
  }
177
196
  type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
@@ -203,16 +222,16 @@ function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNex
203
222
  function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
204
223
  if (options.planned.ufg !== undefined) assertUfgLaunchAccount(options.planned.plan, options.planned.account);
205
224
  if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
206
- 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");
207
226
  }
208
227
  if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
209
- 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");
210
229
  }
211
230
  const current = readReviewedNextTransaction(options.reviewedTransaction);
212
231
  if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
213
232
  reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
214
233
  (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
215
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
234
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
216
235
  }
217
236
  }
218
237
 
@@ -227,16 +246,6 @@ async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator:
227
246
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
228
247
  }
229
248
 
230
- function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
231
- 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");
232
- 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");
233
- 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");
234
- if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
235
- if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
236
- const fresh = current.hookDeployments[index];
237
- 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();
238
- })) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
239
- }
240
249
 
241
250
  function assertPlanningActive(signal?: AbortSignal): void {
242
251
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
@@ -289,18 +298,9 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
289
298
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
290
299
  }
291
300
 
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
301
 
302
- /** One owner constructs and mines, optionally measures actual ordered buy outputs,
303
- * then proves only the final protected commitment. No cached execution certificates. */
302
+ /** One owner mines constructor-bound salts and protects ordered buys with exact
303
+ * local curve math. Launch calldata is never executed during planning. */
304
304
  export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
305
305
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
306
306
  if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
@@ -334,9 +334,11 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
334
334
  }
335
335
  assertDraft();
336
336
  let plan: LaunchPlanV1 = { ...draft, markets };
337
- let ownedProgress: CanonicalLaunchProgress | undefined;
338
337
  let openingBuys: PlanReadContext["openingBuys"];
339
- if (needsProtection && hasProfiledOpeningMath(plan)) {
338
+ if (needsProtection) {
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");
340
342
  const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
341
343
  openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
342
344
  const factor = BigInt(10000 - options.buySlippageBps!);
@@ -345,19 +347,8 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
345
347
  if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
346
348
  return { ...buy, minTokenOut };
347
349
  }) };
348
- } else if (needsProtection) {
349
- // A diagnostic minimum is never returned as an admitted or executable plan.
350
- // Price guards, funding minima, recipients and all ordered budgets stay exact.
351
- const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
352
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
353
- assertDraft();
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");
355
- plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
356
- // This is the real SDK-owned None observation, not an assumed new-launch
357
- // state. No onchain commitment existed at this exact snapshot to replace.
358
- ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
359
350
  }
360
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, progress: ownedProgress, openingBuys }));
351
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
361
352
  assertDraft();
362
353
  assertFinalPlan(planned, options.signal);
363
354
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -366,7 +357,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
366
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");
367
358
  }
368
359
  }
369
- 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);
370
361
  return planned;
371
362
  });
372
363
  assertDraft();
@@ -374,7 +365,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
374
365
  return result;
375
366
  }
376
367
 
377
- /** 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. */
378
369
  export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
379
370
  const result = await withLifecycleReadClient(options.client, async (client) => {
380
371
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
@@ -386,44 +377,34 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
386
377
  }
387
378
 
388
379
  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;
380
+ const { block: pinnedBlock, openingBuys } = readContext;
381
+ const { plan, account, ufg } = options;
392
382
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
393
383
  const launchId = hashLaunchIdentity(plan);
394
384
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
395
385
  pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
396
386
  ]));
397
387
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
398
- // Only preparation/build-next can supply freshly observed progress from this
399
- // still-open SDK invocation. Stored/query DTOs never enter this continuation.
400
- if (ownedProgress !== undefined) {
401
- if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
402
- ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
403
- ownedProgress.confirmationDepth !== (options.confirmations ?? 1)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
404
- 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");
405
- }
406
388
  assertPlanningActive(options.signal);
407
389
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
408
390
  const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
409
391
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
410
392
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
411
393
  lifecycleStage(client, "plan.progress", async () => {
412
- const [predictedToken] = await Promise.all([
413
- readLifecycleTokenAtBlock(client, plan, block),
414
- 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),
415
396
  ]);
397
+ const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" as const : "atomic" as const;
416
398
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
417
399
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
418
400
  if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
419
- const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
420
- 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);
421
402
  return { identity, progress };
422
403
  }),
423
404
  ]));
424
405
  assertPlanningActive(options.signal);
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: [] };
426
- 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,
427
408
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
428
409
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
429
410
  if (!progress.confirmationSafe || receiptsPending) {
@@ -434,72 +415,35 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
434
415
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
435
416
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
436
417
  }
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" };
438
-
439
- 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." } };
440
421
  }
441
422
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
442
-
443
- 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 } };
444
424
  }
445
- // Topology is a creation policy, not a retroactive obstacle to recovery of
446
- // immutable onchain commitments. Waiting and terminal launches returned above.
447
425
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None) validatePlanTopology(options, marketConfigs, identity.predictedToken);
448
426
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
449
427
  progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block, ufg === undefined ? plan.creator : account, ufg !== undefined) : { prerequisites: [], approvals: [] },
450
428
  readPlanMetadata(client, plan, identity.predictedToken, block),
451
429
  ]));
452
430
  assertFinalPlan(planned, options.signal);
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";
458
- }
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;
459
434
  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);
463
- if (metadata.reasons.length > 0) {
464
-
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) };
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" } };
466
442
  }
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);
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);
471
445
  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);
478
- assertFinalPlan(planned, options.signal);
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);
496
- }
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) };
446
+ return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
503
447
  }
504
448
 
505
449
  function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
@@ -508,63 +452,48 @@ function finalizeNextTransaction(options: BuildNextTransactionOptions, next: Lif
508
452
  return { ...next, dependencies: [] };
509
453
  }
510
454
 
511
- /** Recovery observes real canonical receipts, then evaluates only unfinished
512
- * execution; the wallet owns its nonce and assignment at send time. */
455
+ /** Recovery observes actual canonical receipts, then constructs and budgets only
456
+ * the next command. No launch transaction is executed during planning. */
513
457
  export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
514
458
  const original = options.planned, matches = observedFields(original);
515
- const fullReviewAdmitted = original.simulation?.admitted === true;
516
459
  const assertInput = () => {
517
460
  if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
518
461
  assertFinalPlan(original, options.signal);
519
462
  };
520
463
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
521
- // Snapshot only the reviewed wire fields; caller proofs and commands are never
522
- // reused, and final checks compare the mutable input with this one local copy.
523
464
  const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
524
465
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
525
466
  from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
526
467
  const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
527
468
  const result = await withLifecycleReadClient(options.client, async (client) => {
528
469
  registerLifecycleFinalCheck(client, assertInput);
529
- const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
530
- const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
531
- if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
532
- await Promise.all([
533
- readLifecycleTokenAtBlock(client, options.planned.plan, block),
534
- readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
535
- resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
536
- ]);
537
- }
470
+ const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
471
+ const limitSource = options.limits === undefined ? original.limits : options.limits;
538
472
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
539
- if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
540
- // Only the confirmed canonical terminal phase is terminal; a head-only
541
- // Active observation is not, and the launch-state/receipt confirmation
542
- // policy below remains in force for every nonterminal case.
543
-
544
- return undefined;
545
- }
546
- if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
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");
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.");
548
476
  assertPlanningActive(options.signal);
477
+ const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
549
478
  if (options.action === "cancel") {
550
479
  if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
551
- const { planned } = options;
552
- assertFinalPlan(planned, options.signal);
480
+ const planned = { ...original, progress };
553
481
  const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
554
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 }] };
555
- if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
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 });
557
- if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
558
- const next = attachEstimates([transaction], simulation)[0];
559
- if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
560
- return finalizeNextTransaction(options, next, 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);
561
487
  }
562
- const { planned } = options;
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");
565
488
  const funding = progress.canonical.phase === LifecyclePhase.None
566
- ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
489
+ ? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
567
490
  : { prerequisites: [], approvals: [] };
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
+ }
568
497
  let candidate = funding.approvals[0];
569
498
  if (candidate === undefined) {
570
499
  const prepared = progress.canonical.preparedMarkets;
@@ -574,46 +503,25 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
574
503
  let count: number;
575
504
  if (reviewed?.kind === "prepare") {
576
505
  const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
577
- const first = Number(span?.[1]);
506
+ if (Number(span?.[1]) !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
578
507
  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
508
  } 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
- }
509
+ const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
510
+ count = batches[prepared > 0 ? 1 : 0]!;
588
511
  }
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");
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");
590
513
  command = { kind: "prepare", first: prepared, count };
591
514
  } else command = { kind: "activate" };
592
515
  candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
593
516
  }
594
- if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
595
- const simulation = await evaluateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, { finalEnvelope: envelope, immediate: true });
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);
596
519
  assertFinalPlan(planned, options.signal);
597
- if (!simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
598
- const next = attachEstimates([candidate], simulation)[0]!;
599
- return finalizeNextTransaction(options, next, 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);
600
522
  });
601
523
  assertInput();
602
524
  if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
603
525
  return result;
604
526
  }
605
527
 
606
- /** Re-simulates only canonical unfinished work with the same submitted receipt
607
- * evidence and confirmation depth as build-next, never silently dropping context. */
608
- export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
609
- const { client, planned } = options;
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");
611
- const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
612
- client, plan: planned.plan, account: planned.account, mode: planned.mode,
613
- limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
614
- receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
615
- ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
616
- }, readClient, { forceSimulation: true }));
617
- assertPlannedDeploymentBindings(planned, current);
618
- return current.simulation;
619
- }