@black-market/sdk 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +28 -4
  2. package/dist/addresses.d.ts +1 -1
  3. package/dist/addresses.js +4 -4
  4. package/dist/launch.d.ts +61 -0
  5. package/dist/launch.d.ts.map +1 -1
  6. package/dist/launch.js +171 -6
  7. package/dist/launch.js.map +1 -1
  8. package/dist/lifecycle/abi.d.ts +1999 -253
  9. package/dist/lifecycle/abi.d.ts.map +1 -1
  10. package/dist/lifecycle/abi.js +43 -14
  11. package/dist/lifecycle/abi.js.map +1 -1
  12. package/dist/lifecycle/allocations.d.ts +11 -0
  13. package/dist/lifecycle/allocations.d.ts.map +1 -0
  14. package/dist/lifecycle/allocations.js +36 -0
  15. package/dist/lifecycle/allocations.js.map +1 -0
  16. package/dist/lifecycle/calibration.d.ts +19 -0
  17. package/dist/lifecycle/calibration.d.ts.map +1 -0
  18. package/dist/lifecycle/calibration.js +65 -0
  19. package/dist/lifecycle/calibration.js.map +1 -0
  20. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts +11 -0
  21. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts.map +1 -0
  22. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js +10 -0
  23. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +4 -2
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +4 -2
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +170 -6
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +164 -37
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/planner.d.ts +7 -14
  33. package/dist/lifecycle/planner.d.ts.map +1 -1
  34. package/dist/lifecycle/planner.js +354 -400
  35. package/dist/lifecycle/planner.js.map +1 -1
  36. package/dist/lifecycle/presets.d.ts +62 -0
  37. package/dist/lifecycle/presets.d.ts.map +1 -0
  38. package/dist/lifecycle/presets.js +195 -0
  39. package/dist/lifecycle/presets.js.map +1 -0
  40. package/dist/lifecycle/progress.d.ts +5 -1
  41. package/dist/lifecycle/progress.d.ts.map +1 -1
  42. package/dist/lifecycle/progress.js +212 -53
  43. package/dist/lifecycle/progress.js.map +1 -1
  44. package/dist/lifecycle/rpc.d.ts +13 -2
  45. package/dist/lifecycle/rpc.d.ts.map +1 -1
  46. package/dist/lifecycle/rpc.js +92 -8
  47. package/dist/lifecycle/rpc.js.map +1 -1
  48. package/dist/lifecycle/simulation.d.ts +14 -4
  49. package/dist/lifecycle/simulation.d.ts.map +1 -1
  50. package/dist/lifecycle/simulation.js +383 -116
  51. package/dist/lifecycle/simulation.js.map +1 -1
  52. package/dist/lifecycle/transactions.d.ts +6 -0
  53. package/dist/lifecycle/transactions.d.ts.map +1 -0
  54. package/dist/lifecycle/transactions.js +40 -0
  55. package/dist/lifecycle/transactions.js.map +1 -0
  56. package/dist/lifecycle/types.d.ts +39 -2
  57. package/dist/lifecycle/types.d.ts.map +1 -1
  58. package/dist/lifecycle/types.js.map +1 -1
  59. package/docs/launch-lifecycle.md +228 -54
  60. package/examples/README.md +108 -18
  61. package/examples/generate-lifecycle-commitment-fixture.mjs +3 -1
  62. package/examples/launch-example-support.mjs +1 -1
  63. package/examples/launch-example.mjs +73 -26
  64. package/examples/launch-lifecycle-smoke.mjs +13 -11
  65. package/examples/launch.mjs +12 -11
  66. package/examples/quickstart.mjs +1 -1
  67. package/package.json +1 -1
  68. package/src/addresses.ts +4 -4
  69. package/src/launch.ts +213 -6
  70. package/src/lifecycle/abi.ts +43 -14
  71. package/src/lifecycle/allocations.ts +35 -0
  72. package/src/lifecycle/calibration.ts +59 -0
  73. package/src/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.ts +11 -0
  74. package/src/lifecycle/index.ts +4 -2
  75. package/src/lifecycle/markets.ts +169 -35
  76. package/src/lifecycle/planner.ts +326 -330
  77. package/src/lifecycle/presets.ts +243 -0
  78. package/src/lifecycle/progress.ts +193 -50
  79. package/src/lifecycle/rpc.ts +97 -9
  80. package/src/lifecycle/simulation.ts +370 -109
  81. package/src/lifecycle/transactions.ts +34 -0
  82. package/src/lifecycle/types.ts +35 -3
@@ -1,14 +1,18 @@
1
- import { encodeAbiParameters, encodeFunctionData, keccak256, toHex, zeroAddress } from "viem";
2
- import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFundingEscrowAbi, lifecycleRegistryAbi } from "./abi.js";
3
- import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecycleMode, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LifecycleVenue, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MULTI_POSITION_CAPABILITY, LIFECYCLE_REQUIRED_CAPABILITIES, marketIdentityV1Components, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
4
- import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
5
- import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA } from "./markets.js";
6
- import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
1
+ import { encodeFunctionData, keccak256, toHex, zeroAddress } from "viem";
2
+ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
3
+ import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
4
+ import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
5
+ import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt } from "./markets.js";
6
+ import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
7
7
  import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
8
8
  import { LifecyclePlanningError } from "./types.js";
9
+ import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
10
+ import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
11
+ import { buildLaunchTransactions } from "./transactions.js";
12
+ import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
9
13
  function validatePlanShape(plan) {
10
14
  // Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
11
- hashLaunchPlan(plan);
15
+ const commitment = hashLaunchPlan(plan);
12
16
  if (plan.markets.length === 0 || plan.markets.length > 16 || plan.buys.length > 64 || plan.funding.length > 8 || plan.feeAssets.length === 0 || plan.feeAssets.length > 8 || plan.token.supply <= 0n || plan.token.inventoryRecipient === zeroAddress || plan.token.inventoryRecipient.toLowerCase() === plan.orchestrator.toLowerCase() || plan.executorFeeBps > 1000)
13
17
  throw new LifecyclePlanningError("INVALID_PLAN", "Plan exceeds lifecycle bounds or lacks positive token economics");
14
18
  if ((plan.token.kind === LifecycleTokenKind.ERC404 && plan.token.supply > LIFECYCLE_MAX_ERC404_SUPPLY) || (plan.token.kind === LifecycleTokenKind.ERC20 && plan.token.rewardMode !== LifecycleRewardMode.None && plan.token.supply > LIFECYCLE_MAX_REWARD_ERC20_SUPPLY))
@@ -34,8 +38,6 @@ function validatePlanShape(plan) {
34
38
  throw new LifecyclePlanningError("INVALID_FUNDING", "Conversion funding must change assets and commit bounded nonempty calldata");
35
39
  previous = BigInt(funding.asset);
36
40
  }
37
- if (plan.markets.reduce((sum, market) => sum + market.tokenBudget, 0n) > plan.token.supply)
38
- throw new LifecyclePlanningError("INVALID_TOKEN_BUDGET", "Market token budgets exceed committed supply");
39
41
  for (const buy of plan.buys) {
40
42
  if (buy.marketIndex >= plan.markets.length || buy.quoteAmountIn <= 0n || buy.minTokenOut <= 0n || buy.recipient === zeroAddress || buy.recipient.toLowerCase() === plan.orchestrator.toLowerCase())
41
43
  throw new LifecyclePlanningError("INVALID_BUY", "Every ordered buy requires a committed market, positive budget/minimum, and external recipient");
@@ -46,12 +48,12 @@ function validatePlanShape(plan) {
46
48
  if (market.tokenBudget <= 0n || market.config === "0x" || (market.config.length - 2) / 2 > 16384)
47
49
  throw new LifecyclePlanningError("INVALID_MARKET", "Each market needs a positive token budget and bounded versioned config");
48
50
  let config;
49
- if (market.configVersion === 2 || market.configVersion === 3)
50
- throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Old V4 config versions 2 and 3 are not supported; encode reviewed terms explicitly using version 4 or 5");
51
+ if (market.configVersion !== 1 && market.configVersion !== 4 && !isPoolBoundV4ConfigVersion(market.configVersion))
52
+ throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Encode reviewed SharedV4 version 4 or pool-bound version 5/6 only on its exact matching deployment");
51
53
  if (market.configVersion === 4)
52
54
  config = decodeV4LifecycleMarketConfig(market.config);
53
- else if (market.configVersion === 5)
54
- config = decodePoolBoundV4LifecycleMarketConfig(market.config);
55
+ else if (isPoolBoundV4ConfigVersion(market.configVersion))
56
+ config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
55
57
  if (config !== undefined) {
56
58
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase())
57
59
  throw new LifecyclePlanningError("PROFILE_TERMS_MISMATCH", "Inner config profile differs from the committed market profile");
@@ -59,258 +61,140 @@ function validatePlanShape(plan) {
59
61
  if (v4Quotes.has(quote))
60
62
  throw new LifecyclePlanningError("DUPLICATE_V4_QUOTE", "Only one V4 market per quote asset is allowed across shared and pool-bound offerings; put multiple positions in that market");
61
63
  v4Quotes.add(quote);
62
- if (config.positions.length === 0 || config.positions.length > 32 || config.positions.reduce((total, position) => total + position.maxTokenAmount, 0n) > market.tokenBudget || config.lpFeePips >= 1000000 || config.hookFeePips >= 1000000 || config.feeMode > 1 || config.tickSpacing < 1 || config.tickSpacing > 32767 || config.sqrtPriceX96 < 4295128739n || config.sqrtPriceX96 >= 1461446703485210103287273052203988822378723970342n || (config.protocolFeeDenominator !== 0 && (config.protocolFeeDenominator < 4 || config.protocolFeeDenominator > 10 || config.treasury === zeroAddress)))
63
- throw new LifecyclePlanningError("INVALID_MARKET", "V4 market violates lifecycle fee, price, treasury, position or budget bounds");
64
- positionCount += config.positions.length;
64
+ if (config.lpFeePips >= 1000000 || (config.version === 6 ? config.hookFeePips > 1000000 : config.hookFeePips >= 1000000) || config.feeMode > 1 || config.tickSpacing < 1 || config.tickSpacing > 32767 || config.sqrtPriceX96 < 4295128739n || config.sqrtPriceX96 >= 1461446703485210103287273052203988822378723970342n || (config.protocolFeeDenominator !== 0 && (config.protocolFeeDenominator < 4 || config.protocolFeeDenominator > 10 || config.treasury === zeroAddress)))
65
+ throw new LifecyclePlanningError("INVALID_MARKET", "V4 market violates lifecycle fee, price, treasury or position bounds");
66
+ positionCount += assertLifecycleMarketAllocation(market, config.positions);
65
67
  }
68
+ else
69
+ positionCount += assertLifecycleMarketAllocation(market, decodeAbyssLifecycleMarketConfig(market.config).positions);
66
70
  if (positionCount > 32)
67
71
  throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 total positions across every venue");
68
72
  }
73
+ assertLifecycleSupplyAllocation(plan);
69
74
  for (const market of plan.markets) {
70
75
  const required = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === market.quoteAsset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
71
76
  const funding = plan.funding.find((item) => item.asset.toLowerCase() === market.quoteAsset.toLowerCase());
72
77
  if (required > (funding?.amount ?? 0n))
73
78
  throw new LifecyclePlanningError("INVALID_FUNDING", "Quote funding does not cover all ordered buys for its asset");
74
79
  }
80
+ return commitment;
75
81
  }
76
82
  async function readFunding(client, plan, block) {
77
- const [escrow, registry, balanceValue] = await Promise.all([
78
- readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block),
79
- readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
80
- lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block),
81
- ]);
82
- const nativeBalance = rpcQuantity(balanceValue, "creator native balance");
83
- const [authority, observations] = await Promise.all([
84
- readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "core", [], block),
85
- Promise.all(plan.funding.map(async (funding) => {
86
- const native = funding.kind === LifecycleFundingKind.NativeWrap || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress);
87
- const [input, wrapped, target] = await Promise.all([
88
- native ? [nativeBalance, undefined] : Promise.all([
89
- readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
90
- readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
91
- ]),
92
- funding.kind === LifecycleFundingKind.NativeWrap ? readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block) : undefined,
93
- funding.kind === LifecycleFundingKind.Swap ? Promise.all([
94
- readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block),
95
- lifecyclePinnedRpc(client, "eth_getCode", [funding.target, toHex(block.number)], block),
96
- ]) : undefined,
83
+ if (plan.funding.length === 0)
84
+ return { prerequisites: [], approvals: [] };
85
+ const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
86
+ const escrow = preset?.fundingEscrow ?? await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
87
+ // Only allowance observations construct approval commands. Core execution
88
+ // enforces input/output asset, funding target, balance and conversion predicates.
89
+ const prerequisites = await Promise.all(plan.funding.map(async (funding) => {
90
+ const native = funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress;
91
+ const [inputBalance, allowance] = native
92
+ ? [rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block), "creator native balance"), undefined]
93
+ : await Promise.all([
94
+ readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
95
+ readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
97
96
  ]);
98
- let reason;
99
- if (wrapped !== undefined && (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase()))
100
- reason = "Committed native-wrap output is not the escrow's supported wrapped native asset";
101
- if (target !== undefined) {
102
- const [[spender, codeHash, enabled], codeValue] = target;
103
- const code = rpcHex(codeValue, "funding target code");
104
- if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase())
105
- reason = "Committed funding conversion target is not currently allowlisted with its immutable code hash";
106
- }
107
- const prerequisite = { funding, inputBalance: input[0], requiredInput: funding.inputAmount, allowance: input[1], spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" };
108
- return { prerequisite, reason };
109
- })),
110
- ]);
111
- if (authority.toLowerCase() !== plan.orchestrator.toLowerCase())
112
- throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
97
+ return { funding, inputBalance, requiredInput: funding.inputAmount, allowance, spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" };
98
+ }));
113
99
  const totals = new Map();
114
- const prerequisites = [];
115
- const approvals = [];
116
- const reasons = [];
117
- let nativeValue = 0n;
118
- for (const { prerequisite, reason } of observations) {
119
- prerequisites.push(prerequisite);
120
- if (reason !== undefined)
121
- reasons.push(reason);
122
- nativeValue += prerequisite.nativeValue;
123
- if (prerequisite.allowance !== undefined) {
124
- const { funding } = prerequisite;
125
- const key = funding.inputAsset.toLowerCase();
126
- const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, balance: prerequisite.inputBalance, allowance: prerequisite.allowance };
127
- total.amount += funding.inputAmount;
128
- totals.set(key, total);
129
- }
100
+ for (const prerequisite of prerequisites) {
101
+ if (prerequisite.allowance === undefined)
102
+ continue;
103
+ const { funding } = prerequisite;
104
+ const key = funding.inputAsset.toLowerCase();
105
+ const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, allowance: prerequisite.allowance };
106
+ total.amount += funding.inputAmount;
107
+ totals.set(key, total);
130
108
  }
131
- if (nativeBalance < nativeValue)
132
- reasons.push("Creator native balance does not cover committed native funding inputs");
109
+ const approvals = [];
133
110
  for (const total of totals.values()) {
134
- if (total.balance < total.amount)
135
- reasons.push(`Creator balance is below combined funding requirements for ${total.asset}`);
136
111
  if (total.allowance >= total.amount)
137
112
  continue;
138
113
  if (total.allowance > 0n)
139
114
  approvals.push({ id: `approve-reset:${total.asset}`, kind: "approve-reset", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, 0n] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: 0n, exact: 0n }] });
140
115
  approvals.push({ id: `approve:${total.asset}`, kind: "approve", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, total.amount] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: total.amount }] });
141
116
  }
142
- return { prerequisites, approvals, reasons, nativeValue };
117
+ return { prerequisites, approvals };
143
118
  }
144
- async function validatePendingInputs(client, planned, progress, block) {
145
- const { plan } = planned;
146
- const assets = new Set();
147
- for (const policy of plan.feeAssets)
148
- if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
149
- assets.add(policy.asset);
150
- for (const market of plan.markets)
151
- assets.add(market.quoteAsset);
152
- for (const funding of plan.funding) {
153
- assets.add(funding.asset);
154
- if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress)
155
- assets.add(funding.inputAsset);
156
- }
157
- const registryRead = readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
158
- const [profiles, registry, assetCodes, allowedInputs, escrowBalances] = await Promise.all([
159
- readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
160
- registryRead,
161
- Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
162
- registryRead.then((registry) => Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
163
- ? readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true))),
164
- progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
165
- ]);
166
- const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
167
- if (block.timestamp > plan.deadline)
168
- reasons.push("The immutable launch deadline has expired; cancellation remains available");
169
- for (const policy of plan.feeAssets)
170
- if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
171
- reasons.push("Only the launch token may have a burn fee share");
172
- if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase()))
173
- reasons.push("The launch token must have a committed fee policy");
174
- for (const { asset, code } of assetCodes)
175
- if (code === "0x")
176
- reasons.push(`Asset ${asset} has no contract code`);
177
- const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
178
- const markets = await Promise.all(plan.markets.map(async (market, index) => {
179
- const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
180
- let positions = 0;
181
- let deployment;
182
- let identity;
183
- let reason;
184
- if (profile === undefined || !profile.admitted)
185
- return { positions, deployment, identity, reason };
186
- try {
187
- if (market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() || market.configVersion !== profile.topology.configVersion)
188
- throw new LifecyclePlanningError("INVALID_MARKET", "Market adapter or version differs from exact certified profile");
189
- if (market.quoteAsset.toLowerCase() === planned.predictedToken.toLowerCase() || !plan.feeAssets.some((policy) => policy.asset.toLowerCase() === market.quoteAsset.toLowerCase()))
190
- throw new LifecyclePlanningError("INVALID_MARKET", "Each distinct quote requires its committed fee policy");
191
- const schema = profile.registration.configSchema.toLowerCase();
192
- const config = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeV4LifecycleMarketConfig(market.config) : schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodePoolBoundV4LifecycleMarketConfig(market.config) : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeAbyssLifecycleMarketConfig(market.config) : undefined;
193
- if (config === undefined)
194
- throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
195
- if ("developerFeeBps" in config) {
196
- validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
197
- }
198
- if (config.positions.length === 0 || config.positions.length > 32)
199
- throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
200
- positions = config.positions.length;
201
- const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
202
- if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0)
203
- throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
204
- const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
205
- const [adapter, boundDeployment, resolvedIdentity] = await Promise.all([
206
- readLifecycleContract(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
207
- profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
208
- readLifecycleContract(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block),
209
- "developerFeeBps" in config && config.version === 4 ? validateV4LifecycleOracle({ client, envelope: profile.envelope, oracleConfigId: config.oracleConfigId, block }) : undefined,
210
- ]);
211
- if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase())
212
- throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
213
- if (boundDeployment !== undefined) {
214
- if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook))
215
- throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
216
- deployment = { marketIndex: index, ...boundDeployment };
217
- }
218
- identity = resolvedIdentity;
219
- validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
220
- const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
221
- if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0)))
222
- throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
223
- const prepared = progress.markets.find((item) => item.index === index);
224
- if (prepared !== undefined) {
225
- const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
226
- const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
227
- if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96)
228
- throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
229
- await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
230
- }
231
- }
232
- catch (failure) {
233
- reason = failure instanceof Error ? failure.message : String(failure);
234
- }
235
- return { positions, deployment, identity, reason };
236
- }));
237
- for (const [index, funding] of plan.funding.entries()) {
238
- if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase()))
239
- reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
240
- if (!allowedInputs[index])
241
- reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
242
- }
243
- const hookDeployments = [];
244
- const identities = new Set();
245
- const v4Quotes = new Set();
246
- let positionCount = 0;
247
- for (const [index, result] of markets.entries()) {
248
- if (positionCount + result.positions > 32) {
249
- reasons.push("A launch commits at most 32 positive positions across every market/venue");
250
- continue;
251
- }
252
- positionCount += result.positions;
253
- if (result.deployment !== undefined)
254
- hookDeployments.push(result.deployment);
255
- if (result.reason !== undefined) {
256
- reasons.push(result.reason);
257
- continue;
119
+ async function readPlanMetadata(client, plan, token, block) {
120
+ const ids = [...new Set(plan.markets.map((market) => market.profileId.toLowerCase()))];
121
+ const known = ids.map((profileId) => getKnownLifecycleProfile({ chainId: plan.chainId, orchestrator: plan.orchestrator, profileId }));
122
+ const unknownIds = ids.filter((_id, index) => known[index] === undefined);
123
+ const live = unknownIds.length === 0 ? [] : await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: unknownIds }, block);
124
+ const profiles = ids.map((id, index) => {
125
+ const preset = known[index];
126
+ if (preset !== undefined) {
127
+ if (!preset.supported)
128
+ throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", preset.unavailableReason ?? "Historical preset does not support the current launch ABI");
129
+ return { ...preset.profile, metadataSource: "preset" };
258
130
  }
259
- if (result.identity === undefined)
260
- continue;
261
- if (result.identity.venue === LifecycleVenue.UniswapV4) {
262
- const quote = plan.markets[index].quoteAsset.toLowerCase();
263
- if (v4Quotes.has(quote)) {
264
- reasons.push("Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
265
- continue;
266
- }
267
- v4Quotes.add(quote);
268
- }
269
- const identity = result.identity.canonicalId.toLowerCase();
270
- if (identities.has(identity)) {
271
- reasons.push("Two committed markets resolve to the same canonical pool");
272
- continue;
273
- }
274
- identities.add(identity);
275
- }
276
- for (const [index, balance] of escrowBalances.entries()) {
277
- const funding = plan.funding[index];
278
- const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
279
- if (balance < needed)
280
- reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
281
- }
282
- return { profiles, hookDeployments, reasons };
131
+ const profile = live.find((candidate) => candidate.id.toLowerCase() === id);
132
+ if (profile === undefined)
133
+ throw new LifecyclePlanningError("PROFILE_NOT_FOUND", "Unlisted profile discovery omitted the committed profile");
134
+ return profile;
135
+ });
136
+ const hookDeployments = (await Promise.all(plan.markets.map(async (market, marketIndex) => {
137
+ if (!isPoolBoundV4ConfigVersion(market.configVersion))
138
+ return undefined;
139
+ const known = deriveKnownPoolBoundHookDeployment({ plan, token, marketIndex });
140
+ return { marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) };
141
+ }))).filter((deployment) => deployment !== undefined);
142
+ return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
283
143
  }
284
- function orderedTransactions(plan, mode, progress, approvals, nativeValue, batchSize) {
285
- const transactions = [];
144
+ function orderedTransactions(plan, mode, progress, approvals, batchSize) {
286
145
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
287
- return transactions;
288
- const base = { chainId: Number(plan.chainId), from: plan.creator, to: plan.orchestrator, dependencies: [] };
289
- if (progress.canonical.phase === LifecyclePhase.None) {
290
- transactions.push(...approvals);
291
- if (mode === "atomic") {
292
- transactions.push({ ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "launchAtomic", args: [plan] }), value: nativeValue, postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
293
- }
294
- else {
295
- transactions.push({ ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "beginLaunch", args: [plan, LifecycleMode.Staged] }), value: nativeValue, postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] });
296
- }
297
- }
298
- if (mode === "staged") {
299
- for (let first = progress.canonical.preparedMarkets; first < plan.markets.length; first += batchSize) {
300
- const count = Math.min(batchSize, plan.markets.length - first);
301
- const completed = first + count;
302
- transactions.push({ ...base, id: `prepare:${first}:${count}`, kind: "prepare", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "prepareMarkets", args: [plan, first, count] }), value: 0n, marketStart: first, marketCount: count, postconditions: [{ kind: "launch", phase: completed === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: completed }] });
303
- }
304
- transactions.push({ ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "activateLaunch", args: [plan] }), value: 0n, postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
305
- }
306
- return transactions.map((transaction, index) => ({ ...transaction, dependencies: index === 0 ? [] : [transactions[index - 1]?.id ?? ""] }));
146
+ return [];
147
+ const prepared = progress.canonical.preparedMarkets;
148
+ const batches = prepared > 0 ? [prepared] : [];
149
+ for (let first = prepared; first < plan.markets.length; first += batchSize)
150
+ batches.push(Math.min(batchSize, plan.markets.length - first));
151
+ const commands = buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined }).filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
152
+ (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
153
+ const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
154
+ return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
307
155
  }
308
156
  function attachEstimates(transactions, simulation) {
309
157
  return transactions.map((transaction) => {
310
158
  const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
311
- return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { 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 } };
159
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { 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 } };
312
160
  });
313
161
  }
162
+ const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"];
163
+ function readReviewedNextTransaction(transaction) {
164
+ if (transaction === undefined)
165
+ return undefined;
166
+ if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
167
+ !reviewedTransactionKinds.includes(transaction.kind) ||
168
+ !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
169
+ typeof transaction.from !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.from) ||
170
+ typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
171
+ typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
172
+ typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
173
+ typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
174
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n) {
175
+ 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");
176
+ }
177
+ return transaction;
178
+ }
179
+ function matchesReviewedNextTransaction(next, reviewed) {
180
+ return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
181
+ next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
182
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
183
+ }
184
+ function assertReviewedTransactionUnchanged(options, reviewed, next) {
185
+ if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
186
+ throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
187
+ }
188
+ if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
189
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
190
+ }
191
+ const current = readReviewedNextTransaction(options.reviewedTransaction);
192
+ if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
193
+ current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || !matchesReviewedNextTransaction(next, reviewed) ||
194
+ next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
195
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
196
+ }
197
+ }
314
198
  async function readTokenFactoryBinding(client, orchestrator, block) {
315
199
  const tokenFactory = await readLifecycleContract(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
316
200
  const [codeValue, authority] = await Promise.all([
@@ -335,180 +219,230 @@ function assertPlannedDeploymentBindings(stored, current) {
335
219
  }))
336
220
  throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
337
221
  }
338
- /** Mines only certified bound markets, then re-reads the exact final economics and derived identities. */
339
- export async function preparePoolBoundLifecyclePlan(options) {
340
- const { client, signal } = options;
222
+ function assertPlanningActive(signal) {
341
223
  if (signal?.aborted)
342
- throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
343
- validatePlanShape(options.plan);
344
- const commitment = hashLaunchPlan(options.plan);
345
- let plan = parseLaunchPlan(serializeLaunchPlan(options.plan));
346
- const initialBlock = await readLifecycleBlock(client);
347
- if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== plan.chainId)
348
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
349
- const initialFactory = await readTokenFactoryBinding(client, plan.orchestrator, initialBlock);
350
- const initialToken = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], initialBlock);
351
- const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, initialBlock);
352
- const deployments = [];
353
- for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
354
- if (signal?.aborted)
355
- throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
356
- const market = plan.markets[marketIndex];
357
- if (market === undefined)
358
- throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
359
- const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
360
- if (profile === undefined || !profile.admitted)
361
- throw new LifecyclePlanningError("INELIGIBLE_PROFILE", profile?.reason ?? "Market is not an admitted lifecycle profile");
362
- if (profile.topology.hookTopology !== 2)
363
- continue;
364
- const metadata = await readPoolBoundHookDeployment({ client, plan, marketIndex }, initialBlock);
365
- const mined = await minePoolBoundHookSalt({
366
- deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, startSalt: BigInt(metadata.salt), signal,
367
- onProgress: (progress) => options.onProgress?.({ marketIndex, ...progress }),
368
- });
369
- plan = { ...plan, markets: plan.markets.map((item, index) => index === marketIndex ? { ...item, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(item.config), hookSalt: mined.salt }) } : item) };
370
- deployments.push({ marketIndex, deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, ...mined });
371
- }
372
- await assertLifecycleBlock(client, initialBlock);
373
- const finalBlock = await readLifecycleBlock(client);
374
- const finalFactory = await readTokenFactoryBinding(client, plan.orchestrator, finalBlock);
375
- if (finalFactory.tokenFactory.toLowerCase() !== initialFactory.tokenFactory.toLowerCase() || finalFactory.tokenFactoryCodeHash.toLowerCase() !== initialFactory.tokenFactoryCodeHash.toLowerCase())
376
- throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory address or runtime code changed while mining the market-bound deployment");
377
- const finalToken = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], finalBlock);
378
- if (finalToken.toLowerCase() !== initialToken.toLowerCase())
379
- throw new LifecyclePlanningError("TOKEN_IDENTITY", "Token prediction changed while mining the market-bound deployment");
380
- const finalProfiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(deployments.map((deployment) => plan.markets[deployment.marketIndex]?.profileId).filter((id) => id !== undefined))] }, finalBlock);
381
- for (const deployment of deployments) {
382
- if (signal?.aborted)
383
- throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
384
- const market = plan.markets[deployment.marketIndex];
385
- const profile = finalProfiles.find((item) => item.id.toLowerCase() === market?.profileId.toLowerCase());
386
- if (market === undefined || profile === undefined || !profile.admitted || profile.topology.hookTopology !== 2)
387
- throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", profile?.reason ?? "Final bound profile is no longer admitted");
388
- const fresh = await readPoolBoundHookDeployment({ client, plan, marketIndex: deployment.marketIndex }, finalBlock);
389
- if (fresh.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || fresh.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() || fresh.salt.toLowerCase() !== deployment.salt.toLowerCase() || fresh.predictedHook.toLowerCase() !== deployment.predictedHook.toLowerCase() || !hasLifecycleV4HookPermissions(fresh.predictedHook) || predictPoolBoundHookAddress(fresh).toLowerCase() !== fresh.predictedHook.toLowerCase())
390
- throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Mined deployment no longer matches the exact final economic configuration and certified factory");
391
- const identity = await readLifecycleContract(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [hashLaunchIdentity(plan), finalToken, market], finalBlock);
392
- validateLifecycleMarketIdentity(identity, plan, finalToken, deployment.marketIndex);
393
- if (identity.hook.toLowerCase() !== fresh.predictedHook.toLowerCase() || identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase())
394
- throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final resolved PoolKey does not use the exact mined hook/manager");
395
- }
396
- if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase())
397
- throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous salt mining; previous mining is invalid");
398
- if (signal?.aborted)
399
- throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
400
- await assertLifecycleBlock(client, finalBlock);
401
- return { plan, deployments };
402
- }
403
- /** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
404
- export async function planLaunch(options) {
405
- return withLifecycleReadClient(options.client, async (client) => {
406
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
407
- if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
408
- throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
409
- return planned;
410
- });
224
+ throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
411
225
  }
412
- async function planLaunchWithReads(options, client) {
226
+ function validatePlanningIntent(options) {
413
227
  const { plan, account, mode } = options;
228
+ assertPlanningActive(options.signal);
414
229
  if (mode !== "atomic" && mode !== "staged")
415
230
  throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
416
231
  if (account.toLowerCase() !== plan.creator.toLowerCase())
417
232
  throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
418
233
  if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n)
419
234
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
420
- validatePlanShape(plan);
421
- const planHash = hashLaunchPlan(plan);
235
+ return validatePlanShape(plan);
236
+ }
237
+ function assertFinalPlan(planned, signal) {
238
+ assertPlanningActive(signal);
239
+ if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
240
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
241
+ }
242
+ /** One owner constructs and mines, optionally measures actual ordered buy outputs,
243
+ * then proves only the final protected commitment. No certificates or skip flags. */
244
+ export async function prepareAndPlanLifecycleLaunch(options) {
245
+ const commitment = validatePlanningIntent(options);
246
+ if (options.buySlippageBps !== undefined)
247
+ validateBuySlippageBps(options.buySlippageBps);
248
+ const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
249
+ const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
250
+ if (!needsMining && !needsProtection)
251
+ return planLaunch(options);
252
+ const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
253
+ const assertDraft = () => {
254
+ assertPlanningActive(options.signal);
255
+ if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase())
256
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
257
+ };
258
+ return withLifecycleReadClient(options.client, async (client) => {
259
+ const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
260
+ if (chainId !== draft.chainId)
261
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
262
+ const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
263
+ ? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
264
+ assertDraft();
265
+ const markets = [...draft.markets];
266
+ const minedDeployments = [];
267
+ for (const [marketIndex, deployment] of metadata.entries()) {
268
+ if (deployment === undefined)
269
+ continue;
270
+ assertDraft();
271
+ const mined = await minePoolBoundHookSalt({
272
+ deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
273
+ onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
274
+ });
275
+ const market = markets[marketIndex];
276
+ if (!isPoolBoundV4ConfigVersion(market.configVersion))
277
+ throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
278
+ markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
279
+ minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
280
+ }
281
+ assertDraft();
282
+ // Mining is local construction, not a state admission. Economic observations
283
+ // start at a fresh post-mining snapshot and share it through final protection.
284
+ const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
285
+ assertDraft();
286
+ let plan = { ...draft, markets };
287
+ let ownedProgress;
288
+ if (needsProtection) {
289
+ // A diagnostic minimum is never returned as an admitted or executable plan.
290
+ // Price guards, funding minima, recipients and all ordered budgets stay exact.
291
+ const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
292
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
293
+ assertDraft();
294
+ if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
295
+ throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
296
+ plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps);
297
+ // This is the real SDK-owned None observation, not an assumed new-launch
298
+ // state. No onchain commitment existed at this exact snapshot to replace.
299
+ ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
300
+ }
301
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
302
+ assertDraft();
303
+ assertFinalPlan(planned, options.signal);
304
+ if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
305
+ for (const mined of minedDeployments) {
306
+ const final = planned.hookDeployments.find((deployment) => deployment.marketIndex === mined.marketIndex);
307
+ 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())
308
+ throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
309
+ }
310
+ }
311
+ if (needsProtection && !planned.simulation.admitted)
312
+ throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
313
+ return planned;
314
+ });
315
+ }
316
+ /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
317
+ export async function planLaunch(options) {
318
+ return withLifecycleReadClient(options.client, async (client) => {
319
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
320
+ assertFinalPlan(planned, options.signal);
321
+ return planned;
322
+ });
323
+ }
324
+ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, reviewed) {
325
+ const { plan, account, mode } = options;
326
+ const planHash = validatePlanningIntent(options);
422
327
  const launchId = hashLaunchIdentity(plan);
423
- const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
424
- readLifecycleBlock(client),
425
- lifecycleRpc(client, "eth_chainId"),
328
+ const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
329
+ pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
426
330
  ]));
427
- if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId)
331
+ if (chainId !== plan.chainId)
428
332
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
429
- const prediction = readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
430
- const [remoteHash, remoteIdentity, { tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
431
- readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
432
- readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
433
- readTokenFactoryBinding(client, plan.orchestrator, block),
333
+ // Only preparation/build-next can supply freshly observed progress from this
334
+ // still-open SDK invocation. Stored/query DTOs never enter this continuation.
335
+ if (ownedProgress !== undefined) {
336
+ if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
337
+ ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
338
+ ownedProgress.confirmationDepth !== (options.confirmations ?? 1))
339
+ throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
340
+ if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase())
341
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
342
+ }
343
+ assertPlanningActive(options.signal);
344
+ const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
345
+ const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
346
+ preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
434
347
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
435
348
  lifecycleStage(client, "plan.progress", async () => {
436
- const predictedToken = await prediction;
349
+ const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
437
350
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
438
- const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
351
+ const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
352
+ if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase())
353
+ throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
439
354
  return { identity, progress };
440
355
  }),
441
356
  ]));
442
- if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase())
443
- throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
357
+ assertPlanningActive(options.signal);
444
358
  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: [] };
445
- let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
359
+ let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
446
360
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
447
361
  if (!progress.confirmationSafe || receiptsPending) {
448
362
  const hookDeployments = [];
449
363
  for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
450
- if (plan.markets[marketIndex]?.configVersion === 5)
451
- hookDeployments.push({ marketIndex, ...await readPoolBoundHookDeployment({ client, plan, marketIndex }, block) });
364
+ const market = plan.markets[marketIndex];
365
+ if (!isPoolBoundV4ConfigVersion(market.configVersion))
366
+ continue;
367
+ const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
368
+ hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
452
369
  }
453
- // Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
454
370
  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" };
455
371
  await assertLifecycleBlock(client, block, plan.chainId);
456
- return { ...planned, hookDeployments, simulation: waiting, atomicAttempt: waiting };
372
+ return { ...planned, hookDeployments, simulation: waiting };
457
373
  }
458
374
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
459
375
  await assertLifecycleBlock(client, block, plan.chainId);
460
376
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
461
377
  }
462
- // Only batching-capable sources overlap the first simulation's read-only
463
- // context with independent input admission. Waiting/terminal plans skip it.
464
- const preparedSimulation = lifecycleSourceClient(client).supportsReadBatching === true
465
- ? prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork }, block)
466
- : undefined;
467
- const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
468
- progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
469
- validatePendingInputs(client, planned, progress, block),
378
+ const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
379
+ progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [] },
380
+ readPlanMetadata(client, plan, identity.predictedToken, block),
470
381
  ]));
471
- const reasons = [...prerequisites.reasons, ...validation.reasons];
472
- planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
473
- if (reasons.length > 0) {
474
- const rejected = { ...placeholder, confidence: "stateful", reason: reasons.join("; ") };
382
+ assertFinalPlan(planned, options.signal);
383
+ planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
384
+ let preparationBatchSize = plan.markets.length;
385
+ let transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
386
+ if (metadata.reasons.length > 0) {
475
387
  await assertLifecycleBlock(client, block, plan.chainId);
476
- return { ...planned, simulation: rejected, atomicAttempt: rejected, transactions: orderedTransactions(plan, mode, progress, prerequisites.approvals, prerequisites.nativeValue, planned.preparationBatchSize) };
388
+ 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) };
477
389
  }
478
- let atomicAttempt = { ...placeholder, reason: "Atomic execution cannot be evaluated from an already begun staged launch" };
479
- if (progress.canonical.phase === LifecyclePhase.None) {
480
- const atomicTransactions = orderedTransactions(plan, "atomic", progress, prerequisites.approvals, prerequisites.nativeValue, plan.markets.length);
481
- atomicAttempt = await (preparedSimulation === undefined
482
- ? simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, atomicTransactions, block)
483
- : preparedSimulation(atomicTransactions));
484
- if (mode === "atomic") {
485
- const reason = atomicAttempt.admitted ? undefined : `${atomicAttempt.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits`;
486
- const simulation = { ...atomicAttempt, reason };
487
- return { ...planned, atomicAttempt, simulation, transactions: attachEstimates(atomicTransactions, simulation) };
390
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block);
391
+ try {
392
+ const execute = diagnostic ? preparedSimulation.measure : (candidate) => {
393
+ if (reviewed === undefined)
394
+ return preparedSimulation(candidate);
395
+ if (!matchesReviewedNextTransaction(candidate[0], reviewed)) {
396
+ // A fresh prepare batch may still require SDK-owned capacity discovery.
397
+ // Never use held fees for a different step or caller-selected calldata.
398
+ if (candidate[0]?.kind === "prepare" && reviewed.kind === "prepare")
399
+ return preparedSimulation(candidate);
400
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
401
+ }
402
+ return preparedSimulation(candidate, reviewed);
403
+ };
404
+ let simulation = await execute(transactions);
405
+ assertFinalPlan(planned, options.signal);
406
+ if (mode === "staged") {
407
+ // Only explicit gas/compute evidence can partition empty preparations.
408
+ // Byte caps cannot shrink: each prepare still carries the entire plan.
409
+ while (!simulation.admitted && simulation.failureCategory === "capacity" &&
410
+ (simulation.capacityConstraint === "compute" || simulation.capacityConstraint === "gas-envelope") &&
411
+ !(reviewed !== undefined && simulation.capacityConstraint === "gas-envelope" && simulation.failedTransactionId === transactions[0]?.id && matchesReviewedNextTransaction(transactions[0], reviewed)) &&
412
+ simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
413
+ const failedMarketCount = transactions.find((transaction) => transaction.id === simulation.failedTransactionId)?.marketCount ?? 0;
414
+ if (failedMarketCount <= 1)
415
+ break;
416
+ preparationBatchSize = Math.ceil(failedMarketCount / 2);
417
+ transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
418
+ simulation = await execute(transactions);
419
+ assertFinalPlan(planned, options.signal);
420
+ }
421
+ if (!simulation.admitted && simulation.failedTransactionId === "activate")
422
+ simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
423
+ }
424
+ else if (!diagnostic && !simulation.admitted) {
425
+ 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` };
426
+ }
427
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
428
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
488
429
  }
430
+ return { ...planned, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
489
431
  }
490
- let preparationBatchSize = plan.markets.length;
491
- let transactions = orderedTransactions(plan, "staged", progress, prerequisites.approvals, prerequisites.nativeValue, preparationBatchSize);
492
- let simulation = await (preparedSimulation === undefined || progress.canonical.phase === LifecyclePhase.None
493
- ? simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, transactions, block)
494
- : preparedSimulation(transactions));
495
- while (!simulation.admitted && simulation.confidence === "stateful" && simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
496
- preparationBatchSize = Math.ceil(preparationBatchSize / 2);
497
- transactions = orderedTransactions(plan, "staged", progress, prerequisites.approvals, prerequisites.nativeValue, preparationBatchSize);
498
- simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, transactions, block);
432
+ finally {
433
+ preparedSimulation.close();
499
434
  }
500
- if (!simulation.admitted && simulation.failedTransactionId === "activate")
501
- simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
502
- return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
503
435
  }
504
- async function preflightNextTransaction(options, next, simulation) {
436
+ async function preflightNextTransaction(options, next, simulation, reviewed) {
505
437
  if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
506
438
  simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
507
439
  next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
508
440
  simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
509
441
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
510
442
  }
443
+ assertReviewedTransactionUnchanged(options, reviewed, next);
511
444
  const submissionClient = options.submissionClient;
445
+ assertPlanningActive(options.signal);
512
446
  if (submissionClient === undefined)
513
447
  return { ...next, dependencies: [] };
514
448
  try {
@@ -521,56 +455,76 @@ async function preflightNextTransaction(options, next, simulation) {
521
455
  throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
522
456
  if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
523
457
  throw new Error("Active submission RPC chain changed during preflight");
458
+ assertPlanningActive(options.signal);
524
459
  }
525
460
  catch (failure) {
526
- const reason = `Exact next-transaction submission preflight failed: ${failure instanceof Error ? failure.message : String(failure)}`;
461
+ if (failure instanceof Error && failure.name === "AbortError")
462
+ throw failure;
463
+ const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
527
464
  throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
528
465
  }
529
- const canonical = await readLifecycleBlock(options.client, toHex(simulation.blockNumber));
466
+ const [canonical, chainId] = await Promise.all([
467
+ readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
468
+ lifecycleRpc(options.client, "eth_chainId"),
469
+ ]);
530
470
  if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase())
531
471
  throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
532
- if (rpcQuantity(await lifecycleRpc(options.client, "eth_chainId"), "chain ID") !== simulation.chainId)
472
+ if (rpcQuantity(chainId, "chain ID") !== simulation.chainId)
533
473
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
534
474
  if (next.admission === undefined)
535
475
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
476
+ assertPlanningActive(options.signal);
477
+ assertReviewedTransactionUnchanged(options, reviewed, next);
536
478
  return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
537
479
  }
538
- /** Re-reads canonical state, revalidates all prerequisites, and proves the remaining sequence afresh. */
480
+ /** Recovery observes real canonical receipts/nonces, then proves only unfinished
481
+ * execution; known construction metadata is not recertified through getter mirrors. */
539
482
  export async function buildNextTransaction(options) {
540
- const block = await readLifecycleBlock(options.client);
541
- const progress = await readLaunchProgress(options, block);
542
- if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
543
- // Only the confirmed canonical terminal phase is terminal; a head-only
544
- // Active observation is not, and the confirmation/nonce/receipt gates below
545
- // remain in force for every nonterminal case.
546
- await assertLifecycleBlock(options.client, block);
547
- return undefined;
548
- }
549
- if (!progress.confirmationSafe)
550
- throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
551
- if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed"))
552
- throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
553
- if (options.action === "cancel") {
554
- if (progress.canonical.phase === LifecyclePhase.None)
555
- throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
556
- const { planned } = options;
557
- const transaction = { 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 }] };
558
- const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork }, [transaction], block);
559
- if (!simulation.admitted)
560
- throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
561
- const next = attachEstimates([transaction], simulation)[0];
483
+ const supplied = readReviewedNextTransaction(options.reviewedTransaction);
484
+ // Snapshot only the reviewed wire fields; caller proofs and commands are never
485
+ // reused, and final checks compare the mutable input with this one local copy.
486
+ const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
487
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
488
+ return withLifecycleReadClient(options.client, async (client) => {
489
+ const block = await readLifecycleBlock(client);
490
+ const progress = await readLaunchProgress({ ...options, client }, block);
491
+ if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
492
+ // Only the confirmed canonical terminal phase is terminal; a head-only
493
+ // Active observation is not, and the confirmation/nonce/receipt gates below
494
+ // remain in force for every nonterminal case.
495
+ await assertLifecycleBlock(client, block, options.planned.chainId);
496
+ return undefined;
497
+ }
498
+ if (!progress.confirmationSafe)
499
+ throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
500
+ if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed"))
501
+ throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
502
+ assertPlanningActive(options.signal);
503
+ if (options.action === "cancel") {
504
+ if (progress.canonical.phase === LifecyclePhase.None)
505
+ throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
506
+ const { planned } = options;
507
+ const transaction = { 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 }] };
508
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
509
+ throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
510
+ const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
511
+ if (!simulation.admitted)
512
+ throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
513
+ const next = attachEstimates([transaction], simulation)[0];
514
+ if (next === undefined)
515
+ throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
516
+ return preflightNextTransaction(options, next, simulation, reviewed);
517
+ }
518
+ const refreshed = await lifecycleStage(client, "plan", () => planLaunchWithReads({ client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations, signal: options.signal }, client, block, progress, false, reviewed));
519
+ assertFinalPlan(refreshed, options.signal);
520
+ if (!refreshed.simulation.admitted)
521
+ throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
522
+ assertPlannedDeploymentBindings(options.planned, refreshed);
523
+ const next = refreshed.transactions[0];
562
524
  if (next === undefined)
563
- throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
564
- return preflightNextTransaction(options, next, simulation);
565
- }
566
- const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
567
- if (!refreshed.simulation.admitted)
568
- throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
569
- assertPlannedDeploymentBindings(options.planned, refreshed);
570
- const next = refreshed.transactions[0];
571
- if (next === undefined)
572
- return undefined;
573
- return preflightNextTransaction(options, next, refreshed.simulation);
525
+ return undefined;
526
+ return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
527
+ });
574
528
  }
575
529
  /** Re-simulates only canonical unfinished work with the same submitted receipt
576
530
  * evidence and confirmation depth as build-next, never silently dropping context. */
@@ -581,7 +535,7 @@ export async function simulateLaunchPlan(options) {
581
535
  const current = await planLaunch({
582
536
  client, plan: planned.plan, account: planned.account, mode: planned.mode,
583
537
  limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
584
- receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
538
+ receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
585
539
  });
586
540
  assertPlannedDeploymentBindings(planned, current);
587
541
  return current.simulation;