@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,17 +5,39 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./ab
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
6
6
  import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
7
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
8
- import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
- import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
8
+ import { lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
10
9
  import { LifecyclePlanningError } from "./types.js";
11
- import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
10
+ import { deriveKnownPoolBoundHookDeployment, validateBuySlippageBps } from "./calibration.js";
12
11
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
- import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
12
+ import { buildLifecycleCommand, packPreparationBatches } 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 } from "./gas-profile.js";
20
+ const defaultPlanReadContext = {};
21
+ function observedFields(value) {
22
+ const keys = Object.keys(value);
23
+ const values = keys.map((key) => value[key]);
24
+ return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
25
+ }
26
+ async function selectLaunchMode(requested, plan, capacity, limits, client, block) {
27
+ if (requested !== "atomic" || plan.markets.length !== 1 || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
28
+ capacity?.atomic === undefined || capacity.bounds.atomicComputeGasUpperBound === undefined)
29
+ return "staged";
30
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(capacity.atomic, await readNitroPrices(client, block)).posterGas : 0n;
31
+ return capacity.bounds.atomicComputeGasUpperBound + poster <= limits.executionGasCeiling ? "atomic" : "staged";
32
+ }
33
+ async function preparationBatches(plan, prepared, ufg, limits, client, block) {
34
+ if (prepared === plan.markets.length)
35
+ return prepared === 0 ? [] : [prepared];
36
+ const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
37
+ const sample = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }, route);
38
+ const poster = limits.protocol === "nitro" ? nitroPosterGasBound(sample, await readNitroPrices(client, block)).posterGas : 0n;
39
+ return packPreparationBatches(plan, prepared, limits.executionGasCeiling - poster, limits.headroomBps);
40
+ }
19
41
  function validatePlanShape(plan) {
20
42
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
21
43
  const commitment = hashLaunchPlan(plan);
@@ -152,28 +174,42 @@ async function readPlanMetadata(client, plan, token, block) {
152
174
  }))).filter((deployment) => deployment !== undefined);
153
175
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
154
176
  }
155
- function orderedTransactions(plan, mode, progress, approvals, batches, ufg) {
177
+ function orderedTransactions(plan, mode, progress, approvals, batches, ufg, capacity) {
156
178
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
157
179
  return [];
158
- const prepared = progress.canonical.preparedMarkets;
159
- const compiled = ufg === undefined ? buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined })
160
- : buildUfgLaunchTransactions({ plan, config: ufg.config, mode, preparationBatches: mode === "staged" ? batches : undefined });
180
+ const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
181
+ const compiled = [];
182
+ if (mode === "atomic")
183
+ compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
184
+ else {
185
+ compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
186
+ let first = 0;
187
+ for (const count of batches) {
188
+ compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route));
189
+ first += count;
190
+ }
191
+ compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
192
+ }
161
193
  const commands = compiled.filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
162
194
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
163
195
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
164
- return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
196
+ return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
165
197
  }
166
- function attachEstimates(transactions, simulation) {
167
- return transactions.map((transaction) => {
168
- const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
169
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
170
- admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
198
+ function attachEstimates(transactions, assessment) {
199
+ return transactions.map((transaction, index) => {
200
+ if (index !== 0)
201
+ return transaction;
202
+ if (transaction.kind === "approve" || transaction.kind === "approve-reset")
203
+ return { ...transaction, assessment };
204
+ const estimate = assessment.estimate;
205
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, assessment };
171
206
  });
172
207
  }
173
208
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"];
174
209
  function readReviewedNextTransaction(transaction) {
175
210
  if (transaction === undefined)
176
211
  return undefined;
212
+ const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
177
213
  if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
178
214
  !reviewedTransactionKinds.includes(transaction.kind) ||
179
215
  !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
@@ -181,9 +217,8 @@ function readReviewedNextTransaction(transaction) {
181
217
  typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
182
218
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
183
219
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
184
- typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
185
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
186
- transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
220
+ !walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
221
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
187
222
  throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
188
223
  }
189
224
  return transaction;
@@ -191,23 +226,22 @@ function readReviewedNextTransaction(transaction) {
191
226
  function matchesReviewedNextTransaction(next, reviewed) {
192
227
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
193
228
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
194
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
195
- (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
229
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
196
230
  }
197
231
  function assertReviewedTransactionUnchanged(options, reviewed, next) {
198
232
  if (options.planned.ufg !== undefined)
199
233
  assertUfgLaunchAccount(options.planned.plan, options.planned.account);
200
234
  if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
201
- throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
235
+ throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
202
236
  }
203
237
  if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
204
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
238
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
205
239
  }
206
240
  const current = readReviewedNextTransaction(options.reviewedTransaction);
207
- if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
208
- current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || !matchesReviewedNextTransaction(next, reviewed) ||
209
- next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
210
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
241
+ if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
242
+ reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
243
+ (current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
244
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
211
245
  }
212
246
  }
213
247
  async function readTokenFactoryBinding(client, orchestrator, block) {
@@ -221,21 +255,6 @@ async function readTokenFactoryBinding(client, orchestrator, block) {
221
255
  throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
222
256
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
223
257
  }
224
- function assertPlannedDeploymentBindings(stored, current) {
225
- if (hashLaunchPlan(stored.plan).toLowerCase() !== stored.planHash.toLowerCase() || current.planHash.toLowerCase() !== stored.planHash.toLowerCase() || current.launchId.toLowerCase() !== stored.launchId.toLowerCase())
226
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its finalized commitment");
227
- if (stored.ufg === undefined ? current.ufg !== undefined : current.ufg === undefined || hashUfgLaunchConfig(stored.ufg.config) !== stored.ufg.configHash || current.ufg.configHash !== stored.ufg.configHash)
228
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored UFG route or treasury config no longer matches its finalized commitment");
229
- if (current.predictedToken.toLowerCase() !== stored.predictedToken.toLowerCase() || current.tokenFactory.toLowerCase() !== stored.tokenFactory.toLowerCase() || current.tokenFactoryCodeHash.toLowerCase() !== stored.tokenFactoryCodeHash.toLowerCase())
230
- throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Final token prediction or its immutable factory code binding changed before execution");
231
- if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled)
232
- return;
233
- if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
234
- const fresh = current.hookDeployments[index];
235
- 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();
236
- }))
237
- throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
238
- }
239
258
  function assertPlanningActive(signal) {
240
259
  if (signal?.aborted)
241
260
  throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
@@ -244,7 +263,7 @@ function validatePlanningIntent(options) {
244
263
  const { plan, account, mode } = options;
245
264
  assertPlanningActive(options.signal);
246
265
  if (mode !== "atomic" && mode !== "staged")
247
- throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
266
+ throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
248
267
  if (options.ufg !== undefined)
249
268
  assertUfgLaunchAccount(plan, account);
250
269
  else if (account.toLowerCase() !== plan.creator.toLowerCase())
@@ -295,21 +314,15 @@ function assertFinalPlan(planned, signal) {
295
314
  if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash)
296
315
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
297
316
  }
298
- /** One owner constructs and mines, optionally measures actual ordered buy outputs,
299
- * then proves only the final protected commitment. No cached execution certificates. */
317
+ /** One owner mines constructor-bound salts and protects ordered buys with exact
318
+ * local curve math. Launch calldata is never executed during planning. */
300
319
  export async function prepareAndPlanLifecycleLaunch(options) {
301
320
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
302
321
  if (options.buySlippageBps !== undefined)
303
322
  validateBuySlippageBps(options.buySlippageBps);
304
- const fixed = options.fixedFinalEnvelope;
305
- if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
306
- fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
307
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
308
- }
309
- const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
310
323
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
311
324
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
312
- if (!needsMining && !needsProtection && fixed === undefined)
325
+ if (!needsMining && !needsProtection)
313
326
  return planLaunch(options);
314
327
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
315
328
  const assertDraft = () => {
@@ -318,9 +331,6 @@ export async function prepareAndPlanLifecycleLaunch(options) {
318
331
  throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
319
332
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash)
320
333
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
321
- if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
322
- fixed.gas !== heldEnvelope.gas || fixed.gasPrice !== heldEnvelope.gasPrice)
323
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
324
334
  };
325
335
  const result = await withLifecycleReadClient(options.client, async (client) => {
326
336
  const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
@@ -344,32 +354,25 @@ export async function prepareAndPlanLifecycleLaunch(options) {
344
354
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
345
355
  }
346
356
  assertDraft();
347
- const executionBlock = block;
348
- assertDraft();
349
- const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
350
- const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
351
- const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
352
- gas: heldEnvelope.gas,
353
- gasPrice: heldEnvelope.gasPrice ?? (price > baseFeeEnvelope ? price : baseFeeEnvelope),
354
- });
355
357
  let plan = { ...draft, markets };
356
- let ownedProgress;
357
- let sizing;
358
+ let openingBuys;
358
359
  if (needsProtection) {
359
- // A diagnostic minimum is never returned as an admitted or executable plan.
360
- // Price guards, funding minima, recipients and all ordered budgets stay exact.
361
- const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
362
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
363
- assertDraft();
364
- if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
360
+ if (!hasProfiledOpeningMath(plan))
361
+ throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Exact opening-buy math is unavailable for this profile. Use a supported profile or update the SDK.");
362
+ const head = await readLifecycleHeadProgress(client, plan, options.account, block);
363
+ if (head.phase !== LifecyclePhase.None)
365
364
  throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
366
- plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps);
367
- // This is the real SDK-owned None observation, not an assumed new-launch
368
- // state. No onchain commitment existed at this exact snapshot to replace.
369
- ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
370
- sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed]));
365
+ const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
366
+ openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
367
+ const factor = BigInt(10000 - options.buySlippageBps);
368
+ plan = { ...plan, buys: plan.buys.map((buy, index) => {
369
+ const minTokenOut = openingBuys[index].tokenOut * factor / 10000n;
370
+ if (minTokenOut <= 0n)
371
+ throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
372
+ return { ...buy, minTokenOut };
373
+ }) };
371
374
  }
372
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
375
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
373
376
  assertDraft();
374
377
  assertFinalPlan(planned, options.signal);
375
378
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -379,15 +382,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
379
382
  throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
380
383
  }
381
384
  }
382
- if (needsProtection && !planned.simulation.admitted)
383
- throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
385
+ if (needsProtection && !planned.assessment.ready)
386
+ throw new LifecyclePlanningError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "The launch could not be prepared. Refresh the review before continuing.", planned.assessment);
384
387
  return planned;
385
388
  });
386
389
  assertDraft();
387
390
  assertFinalPlan(result, options.signal);
388
391
  return result;
389
392
  }
390
- /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
393
+ /** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
391
394
  export async function planLaunch(options) {
392
395
  const result = await withLifecycleReadClient(options.client, async (client) => {
393
396
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
@@ -397,8 +400,9 @@ export async function planLaunch(options) {
397
400
  assertFinalPlan(result, options.signal);
398
401
  return result;
399
402
  }
400
- async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, fixedFinalEnvelope, sizing) {
401
- const { plan, account, mode, ufg } = options;
403
+ async function planLaunchWithReads(options, client, readContext = defaultPlanReadContext) {
404
+ const { block: pinnedBlock, openingBuys } = readContext;
405
+ const { plan, account, ufg } = options;
402
406
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
403
407
  const launchId = hashLaunchIdentity(plan);
404
408
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
@@ -406,39 +410,27 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
406
410
  ]));
407
411
  if (chainId !== plan.chainId)
408
412
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
409
- // Only preparation/build-next can supply freshly observed progress from this
410
- // still-open SDK invocation. Stored/query DTOs never enter this continuation.
411
- if (ownedProgress !== undefined) {
412
- if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
413
- ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
414
- ownedProgress.confirmationDepth !== (options.confirmations ?? 1))
415
- throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
416
- if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase())
417
- throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
418
- }
419
413
  assertPlanningActive(options.signal);
420
414
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
421
415
  const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
422
416
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
423
417
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
424
418
  lifecycleStage(client, "plan.progress", async () => {
425
- const [predictedToken] = await Promise.all([
426
- readLifecycleTokenAtBlock(client, plan, block),
427
- ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
419
+ const [predictedToken, head] = await Promise.all([
420
+ readLifecycleTokenAtBlock(client, plan, block), readLifecycleHeadProgress(client, plan, account, block),
428
421
  ]);
422
+ const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" : "atomic";
429
423
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
430
424
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
431
425
  if (ufg !== undefined)
432
426
  validateUfgLaunchPlan(plan, ufg.config, predictedToken);
433
- const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
434
- if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase())
435
- throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
427
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
436
428
  return { identity, progress };
437
429
  }),
438
430
  ]));
439
431
  assertPlanningActive(options.signal);
440
- const placeholder = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
441
- let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
432
+ const placeholder = { ready: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits };
433
+ let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, assessment: placeholder, limits: options.limits,
442
434
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
443
435
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
444
436
  if (!progress.confirmationSafe || receiptsPending) {
@@ -450,14 +442,13 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
450
442
  const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
451
443
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
452
444
  }
453
- const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
454
- return { ...planned, hookDeployments, simulation: waiting };
445
+ return { ...planned, hookDeployments, assessment: { ...placeholder,
446
+ code: !progress.confirmationSafe ? "UNCONFIRMED_STATE" : "RECEIPT_PENDING",
447
+ reason: !progress.confirmationSafe ? "Wait for the selected launch confirmation depth before continuing." : "Wait for the pending transaction to confirm or resolve its replacement." } };
455
448
  }
456
449
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
457
- return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
450
+ return { ...planned, assessment: { ...placeholder, ready: true } };
458
451
  }
459
- // Topology is a creation policy, not a retroactive obstacle to recovery of
460
- // immutable onchain commitments. Waiting and terminal launches returned above.
461
452
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None)
462
453
  validatePlanTopology(options, marketConfigs, identity.predictedToken);
463
454
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
@@ -465,199 +456,118 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
465
456
  readPlanMetadata(client, plan, identity.predictedToken, block),
466
457
  ]));
467
458
  assertFinalPlan(planned, options.signal);
468
- planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
469
- const prepared = progress.canonical.preparedMarkets;
470
- const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
471
- let batches = [];
472
- if (mode === "staged")
473
- batches = packPreparationBatches(plan, prepared, computeCap);
474
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
475
- if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0].kind !== "atomic")) {
476
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
477
- }
478
- if (metadata.reasons.length > 0) {
479
- return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
480
- }
481
- const envelope = diagnostic ? undefined : fixedFinalEnvelope;
482
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
483
- try {
484
- const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, envelope);
485
- let simulation = await execute(transactions);
486
- assertFinalPlan(planned, options.signal);
487
- if (mode === "staged") {
488
- // Only pressure at the native ceiling can invalidate packing. A smaller
489
- // affordability/RPC/account envelope is not evidence for another wallet write.
490
- while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
491
- const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
492
- const failedTransaction = transactions[failedIndex];
493
- const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
494
- const count = failedTransaction?.marketCount ?? 0;
495
- if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n)
496
- break;
497
- let first = 0;
498
- const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
499
- const left = Math.ceil(count / 2);
500
- batches.splice(batchIndex, 1, left, count - left);
501
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
502
- simulation = await execute(transactions);
503
- assertFinalPlan(planned, options.signal);
504
- }
505
- if (!simulation.admitted && simulation.failedTransactionId === "activate")
506
- simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
507
- }
508
- else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
509
- simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
510
- }
511
- return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
512
- }
513
- finally {
514
- preparedSimulation.close();
515
- }
516
- }
517
- async function preflightNextTransaction(options, next, simulation, reviewed) {
518
- if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
519
- simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
520
- next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
521
- simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
522
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
523
- }
524
- assertReviewedTransactionUnchanged(options, reviewed, next);
525
- const submissionClient = options.submissionClient;
526
- assertPlanningActive(options.signal);
527
- if (submissionClient === undefined)
528
- return { ...next, dependencies: [] };
459
+ const capacity = deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys);
460
+ const mode = progress.canonical.phase === LifecyclePhase.None
461
+ ? await selectLaunchMode(options.mode, plan, capacity, limits, client, block) : identity.mode;
462
+ planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
463
+ if (metadata.reasons.length > 0)
464
+ return { ...planned, assessment: { ...placeholder, code: "PROFILE_UNAVAILABLE", failureCategory: "semantic",
465
+ reason: "The selected launch profile could not be read or is unavailable. Refresh the review before continuing." } };
466
+ let batches;
529
467
  try {
530
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
531
- throw new Error("Active submission RPC chain differs from the reviewed transaction");
532
- const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
533
- from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
534
- ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
535
- }, "latest"]), "submission gas estimate");
536
- if (estimated <= 0n || estimated > next.gas)
537
- throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
538
- assertPlanningActive(options.signal);
468
+ batches = mode === "staged" ? await preparationBatches(plan, progress.canonical.preparedMarkets, ufg, limits, client, block) : [];
539
469
  }
540
470
  catch (failure) {
541
- if (failure instanceof Error && failure.name === "AbortError")
471
+ if (!(failure instanceof LifecyclePlanningError) || failure.code !== "PREPARATION_GAS_LIMIT")
542
472
  throw failure;
543
- const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
544
- throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
473
+ return { ...planned, assessment: { ...placeholder, code: failure.code, reason: failure.message, failureCategory: "capacity", capacityConstraint: "preparation" } };
545
474
  }
546
- if (next.admission === undefined)
547
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
475
+ const transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
476
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: options.limits, signal: options.signal }, transactions, block, undefined, openingBuys, capacity);
477
+ assertFinalPlan(planned, options.signal);
478
+ return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
479
+ }
480
+ function finalizeNextTransaction(options, next, reviewed) {
548
481
  assertPlanningActive(options.signal);
549
482
  assertReviewedTransactionUnchanged(options, reviewed, next);
550
- return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
483
+ return { ...next, dependencies: [] };
551
484
  }
552
- /** Recovery observes real canonical receipts/nonces, then proves only unfinished
553
- * execution; known construction metadata is not recertified through getter mirrors. */
485
+ /** Recovery observes actual canonical receipts, then constructs and budgets only
486
+ * the next command. No launch transaction is executed during planning. */
554
487
  export async function buildNextTransaction(options) {
488
+ const original = options.planned, matches = observedFields(original);
489
+ const assertInput = () => {
490
+ if (options.planned !== original || !matches())
491
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
492
+ assertFinalPlan(original, options.signal);
493
+ };
555
494
  const supplied = readReviewedNextTransaction(options.reviewedTransaction);
556
- // Snapshot only the reviewed wire fields; caller proofs and commands are never
557
- // reused, and final checks compare the mutable input with this one local copy.
495
+ const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
558
496
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
559
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
497
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
498
+ const envelope = reviewed === undefined || walletApproval ? undefined : reviewed;
560
499
  const result = await withLifecycleReadClient(options.client, async (client) => {
561
- const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
562
- const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
563
- if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
564
- await Promise.all([
565
- readLifecycleTokenAtBlock(client, options.planned.plan, block),
566
- readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
567
- resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
568
- ]);
569
- }
500
+ registerLifecycleFinalCheck(client, assertInput);
501
+ const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
502
+ const limitSource = options.limits === undefined ? original.limits : options.limits;
570
503
  const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
571
- if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
572
- // Only the confirmed canonical terminal phase is terminal; a head-only
573
- // Active observation is not, and the confirmation/nonce/receipt gates below
574
- // remain in force for every nonterminal case.
504
+ if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
575
505
  return undefined;
576
- }
577
506
  if (!progress.confirmationSafe)
578
- throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
507
+ throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected launch confirmation depth before continuing.");
579
508
  if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed"))
580
- throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
509
+ throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for the pending transaction to confirm or resolve its replacement.");
581
510
  assertPlanningActive(options.signal);
511
+ const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
582
512
  if (options.action === "cancel") {
583
513
  if (progress.canonical.phase === LifecyclePhase.None)
584
514
  throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
585
- const { planned } = options;
586
- assertFinalPlan(planned, options.signal);
515
+ const planned = { ...original, progress };
587
516
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
588
517
  : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
589
- transaction.nonce = progress.pendingAccountNonce;
590
518
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
591
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
592
- const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
593
- if (!simulation.admitted)
594
- throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
595
- const next = attachEstimates([transaction], simulation)[0];
596
- if (next === undefined)
597
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
598
- return preflightNextTransaction(options, next, simulation, reviewed);
519
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current cancellation");
520
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [transaction], block, envelope);
521
+ if (!assessment.ready)
522
+ throw new LifecyclePlanningError(assessment.code ?? "CANCEL_NOT_READY", assessment.reason ?? "Cancellation could not be prepared. Refresh before continuing.", assessment);
523
+ return finalizeNextTransaction(options, attachEstimates([transaction], assessment)[0], reviewed);
599
524
  }
600
- const { planned } = options;
601
- assertFinalPlan(planned, options.signal);
602
525
  const funding = progress.canonical.phase === LifecyclePhase.None
603
- ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
526
+ ? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
604
527
  : { prerequisites: [], approvals: [] };
605
- const prepared = progress.canonical.preparedMarkets;
606
- let batches = [];
607
- if (planned.mode === "staged") {
608
- let suffixStart = prepared;
609
- const prefix = prepared > 0 ? [prepared] : [];
610
- if (reviewed?.kind === "prepare") {
611
- const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
612
- const first = Number(span?.[1]), count = Number(span?.[2]);
613
- if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length)
614
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
615
- prefix.push(count);
616
- suffixStart += count;
617
- }
618
- const suffix = planned.transactions
619
- .filter((transaction) => transaction.kind === "prepare" && transaction.marketStart + transaction.marketCount > suffixStart)
620
- .map((transaction) => transaction.marketStart + transaction.marketCount - Math.max(transaction.marketStart, suffixStart));
621
- if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
622
- const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
623
- const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
624
- suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
528
+ const planned = { ...original, progress, prerequisites: funding.prerequisites };
529
+ const capacity = deriveLifecycleLaunchCapacity({ plan: planned.plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps });
530
+ if (progress.canonical.phase === LifecyclePhase.None &&
531
+ await selectLaunchMode(planned.mode, planned.plan, capacity, limits, client, block) !== planned.mode) {
532
+ throw new LifecyclePlanningError("MODE_REVIEW_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.");
533
+ }
534
+ let candidate = funding.approvals[0];
535
+ if (candidate === undefined) {
536
+ const prepared = progress.canonical.preparedMarkets;
537
+ let command;
538
+ if (progress.canonical.phase === LifecyclePhase.None)
539
+ command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
540
+ else if (prepared < planned.plan.markets.length) {
541
+ let count;
542
+ if (reviewed?.kind === "prepare") {
543
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
544
+ if (Number(span?.[1]) !== prepared)
545
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
546
+ count = Number(span?.[2]);
547
+ }
548
+ else {
549
+ const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
550
+ count = batches[prepared > 0 ? 1 : 0];
551
+ }
552
+ if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length)
553
+ throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must cover a positive contiguous unfinished market span");
554
+ command = { kind: "prepare", first: prepared, count };
625
555
  }
626
- batches = [...prefix, ...suffix];
556
+ else
557
+ command = { kind: "activate" };
558
+ candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
627
559
  }
628
- const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
629
- if (candidate === undefined)
630
- return undefined;
631
560
  if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed))
632
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
633
- const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
634
- const compute = observed?.computeGasRequired;
635
- const sizing = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
636
- const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
561
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current next step");
562
+ const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [candidate], block, envelope, undefined, capacity);
637
563
  assertFinalPlan(planned, options.signal);
638
- if (!simulation.admitted)
639
- throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
640
- const next = attachEstimates([candidate], simulation)[0];
641
- return preflightNextTransaction(options, next, simulation, reviewed);
564
+ if (!assessment.ready)
565
+ throw new LifecyclePlanningError(assessment.code ?? "PLAN_NOT_READY", assessment.reason ?? "The next launch step could not be prepared. Refresh before continuing.", assessment);
566
+ return finalizeNextTransaction(options, attachEstimates([candidate], assessment)[0], reviewed);
642
567
  });
643
- assertFinalPlan(options.planned, options.signal);
568
+ assertInput();
644
569
  if (result !== undefined)
645
570
  assertReviewedTransactionUnchanged(options, reviewed, result);
646
571
  return result;
647
572
  }
648
- /** Re-simulates only canonical unfinished work with the same submitted receipt
649
- * evidence and confirmation depth as build-next, never silently dropping context. */
650
- export async function simulateLaunchPlan(options) {
651
- const { client, planned } = options;
652
- if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
653
- throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
654
- const current = await planLaunch({
655
- client, plan: planned.plan, account: planned.account, mode: planned.mode,
656
- limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
657
- receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
658
- ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
659
- });
660
- assertPlannedDeploymentBindings(planned, current);
661
- return current.simulation;
662
- }
663
573
  //# sourceMappingURL=planner.js.map