@black-market/sdk 0.6.0 → 0.6.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyX,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAK9b,OAAO,EAA0B,KAAK,2BAA2B,EAAwF,KAAK,gCAAgC,EAAyB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,qCAAqC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAiOhb,0GAA0G;AAC1G,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,MAAM,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,qCAAqC,KAAK,IAAI,CAAC;CACxE,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,SAAS,gCAAgC,EAAE,CAAA;CAAE,CAAC,CAiD5F;AAED,yGAAyG;AACzG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+DnF;AA6BD,yGAAyG;AACzG,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CA4B1H;AAED;qFACqF;AACrF,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAUzG"}
1
+ {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyX,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAK9b,OAAO,EAA0B,KAAK,2BAA2B,EAAwF,KAAK,gCAAgC,EAAyB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,qCAAqC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAqQhb,0GAA0G;AAC1G,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,MAAM,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,qCAAqC,KAAK,IAAI,CAAC;CACxE,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,SAAS,gCAAgC,EAAE,CAAA;CAAE,CAAC,CAiD5F;AAED,yGAAyG;AACzG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAMnF;AAyGD,yGAAyG;AACzG,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CA4B1H;AAED;qFACqF;AACrF,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAUzG"}
@@ -3,7 +3,7 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFu
3
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
4
  import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
5
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, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity } from "./rpc.js";
6
+ import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
7
7
  import { simulateLaunchTransactions } from "./simulation.js";
8
8
  import { LifecyclePlanningError } from "./types.js";
9
9
  function validatePlanShape(plan) {
@@ -74,47 +74,59 @@ function validatePlanShape(plan) {
74
74
  }
75
75
  }
76
76
  async function readFunding(client, plan, block) {
77
- const escrow = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
78
- if ((await readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "core", [], block)).toLowerCase() !== plan.orchestrator.toLowerCase())
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,
97
+ ]);
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())
79
112
  throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
80
- const registry = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
81
- const nativeBalance = rpcQuantity(await lifecycleRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)]), "creator native balance");
82
113
  const totals = new Map();
83
114
  const prerequisites = [];
84
115
  const approvals = [];
85
116
  const reasons = [];
86
117
  let nativeValue = 0n;
87
- for (const funding of plan.funding) {
88
- const native = funding.kind === LifecycleFundingKind.NativeWrap || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress);
89
- let inputBalance = nativeBalance;
90
- let allowance;
91
- if (native)
92
- nativeValue += funding.inputAmount;
93
- else {
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;
94
125
  const key = funding.inputAsset.toLowerCase();
95
- let total = totals.get(key);
96
- if (total === undefined) {
97
- inputBalance = await readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block);
98
- allowance = await readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block);
99
- total = { asset: funding.inputAsset, amount: 0n, balance: inputBalance, allowance };
100
- totals.set(key, total);
101
- }
126
+ const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, balance: prerequisite.inputBalance, allowance: prerequisite.allowance };
102
127
  total.amount += funding.inputAmount;
103
- inputBalance = total.balance;
104
- allowance = total.allowance;
128
+ totals.set(key, total);
105
129
  }
106
- if (funding.kind === LifecycleFundingKind.NativeWrap) {
107
- const wrapped = await readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block);
108
- if (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase())
109
- reasons.push("Committed native-wrap output is not the escrow's supported wrapped native asset");
110
- }
111
- if (funding.kind === LifecycleFundingKind.Swap) {
112
- const [spender, codeHash, enabled] = await readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block);
113
- const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [funding.target, toHex(block.number)]), "funding target code");
114
- if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase())
115
- reasons.push("Committed funding conversion target is not currently allowlisted with its immutable code hash");
116
- }
117
- prerequisites.push({ 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" });
118
130
  }
119
131
  if (nativeBalance < nativeValue)
120
132
  reasons.push("Creator native balance does not cover committed native funding inputs");
@@ -131,17 +143,6 @@ async function readFunding(client, plan, block) {
131
143
  }
132
144
  async function validatePendingInputs(client, planned, progress, block) {
133
145
  const { plan } = planned;
134
- const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block);
135
- const registry = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
136
- const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
137
- if (block.timestamp > plan.deadline)
138
- reasons.push("The immutable launch deadline has expired; cancellation remains available");
139
- for (const policy of plan.feeAssets)
140
- if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
141
- reasons.push("Only the launch token may have a burn fee share");
142
- if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase()))
143
- reasons.push("The launch token must have a committed fee policy");
144
- const hookDeployments = [];
145
146
  const assets = new Set();
146
147
  for (const policy of plan.feeAssets)
147
148
  if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
@@ -153,90 +154,131 @@ async function validatePendingInputs(client, planned, progress, block) {
153
154
  if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress)
154
155
  assets.add(funding.inputAsset);
155
156
  }
156
- for (const asset of assets)
157
- if (rpcHex(await lifecycleRpc(client, "eth_getCode", [asset, toHex(block.number)]), "asset code") === "0x")
157
+ const [profiles, registry, assetCodes] = await Promise.all([
158
+ readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
159
+ readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
160
+ Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
161
+ ]);
162
+ const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
163
+ if (block.timestamp > plan.deadline)
164
+ reasons.push("The immutable launch deadline has expired; cancellation remains available");
165
+ for (const policy of plan.feeAssets)
166
+ if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
167
+ reasons.push("Only the launch token may have a burn fee share");
168
+ if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase()))
169
+ reasons.push("The launch token must have a committed fee policy");
170
+ for (const { asset, code } of assetCodes)
171
+ if (code === "0x")
158
172
  reasons.push(`Asset ${asset} has no contract code`);
159
- for (const funding of plan.funding) {
173
+ const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
174
+ const [allowedInputs, markets, escrowBalances] = await Promise.all([
175
+ Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
176
+ ? readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true)),
177
+ Promise.all(plan.markets.map(async (market, index) => {
178
+ const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
179
+ let positions = 0;
180
+ let deployment;
181
+ let identity;
182
+ let reason;
183
+ if (profile === undefined || !profile.admitted)
184
+ return { positions, deployment, identity, reason };
185
+ try {
186
+ if (market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() || market.configVersion !== profile.topology.configVersion)
187
+ throw new LifecyclePlanningError("INVALID_MARKET", "Market adapter or version differs from exact certified profile");
188
+ if (market.quoteAsset.toLowerCase() === planned.predictedToken.toLowerCase() || !plan.feeAssets.some((policy) => policy.asset.toLowerCase() === market.quoteAsset.toLowerCase()))
189
+ throw new LifecyclePlanningError("INVALID_MARKET", "Each distinct quote requires its committed fee policy");
190
+ const schema = profile.registration.configSchema.toLowerCase();
191
+ 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;
192
+ if (config === undefined)
193
+ throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
194
+ if ("developerFeeBps" in config) {
195
+ validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
196
+ if (config.version === 4)
197
+ await validateV4LifecycleOracle({ client, envelope: profile.envelope, oracleConfigId: config.oracleConfigId, block });
198
+ }
199
+ if (config.positions.length === 0 || config.positions.length > 32)
200
+ throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
201
+ positions = config.positions.length;
202
+ const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
203
+ if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0)
204
+ throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
205
+ const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
206
+ const [adapter, boundDeployment] = await Promise.all([
207
+ readLifecycleContract(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
208
+ profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
209
+ ]);
210
+ if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase())
211
+ throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
212
+ if (boundDeployment !== undefined) {
213
+ if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook))
214
+ throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
215
+ deployment = { marketIndex: index, ...boundDeployment };
216
+ }
217
+ identity = await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
218
+ validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
219
+ const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
220
+ 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)))
221
+ throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
222
+ const prepared = progress.markets.find((item) => item.index === index);
223
+ if (prepared !== undefined) {
224
+ const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
225
+ const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
226
+ if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96)
227
+ throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
228
+ await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
229
+ }
230
+ }
231
+ catch (failure) {
232
+ reason = failure instanceof Error ? failure.message : String(failure);
233
+ }
234
+ return { positions, deployment, identity, reason };
235
+ })),
236
+ progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
237
+ ]);
238
+ for (const [index, funding] of plan.funding.entries()) {
160
239
  if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase()))
161
240
  reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
162
- if (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress && !await readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block))
241
+ if (!allowedInputs[index])
163
242
  reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
164
243
  }
165
- const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
244
+ const hookDeployments = [];
166
245
  const identities = new Set();
167
246
  const v4Quotes = new Set();
168
247
  let positionCount = 0;
169
- for (let index = 0; index < plan.markets.length; index += 1) {
170
- const market = plan.markets[index];
171
- if (market === undefined)
172
- throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
173
- const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
174
- if (profile === undefined || !profile.admitted)
248
+ for (const [index, result] of markets.entries()) {
249
+ if (positionCount + result.positions > 32) {
250
+ reasons.push("A launch commits at most 32 positive positions across every market/venue");
175
251
  continue;
176
- try {
177
- if (market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() || market.configVersion !== profile.topology.configVersion)
178
- throw new LifecyclePlanningError("INVALID_MARKET", "Market adapter or version differs from exact certified profile");
179
- if (market.quoteAsset.toLowerCase() === planned.predictedToken.toLowerCase() || !plan.feeAssets.some((policy) => policy.asset.toLowerCase() === market.quoteAsset.toLowerCase()))
180
- throw new LifecyclePlanningError("INVALID_MARKET", "Each distinct quote requires its committed fee policy");
181
- const schema = profile.registration.configSchema.toLowerCase();
182
- 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;
183
- if (config === undefined)
184
- throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
185
- if ("developerFeeBps" in config) {
186
- validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
187
- if (config.version === 4)
188
- await validateV4LifecycleOracle({ client, envelope: profile.envelope, oracleConfigId: config.oracleConfigId, block });
189
- }
190
- if (config.positions.length === 0 || config.positions.length > 32 || positionCount + config.positions.length > 32)
191
- throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
192
- positionCount += config.positions.length;
193
- const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
194
- if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0)
195
- throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
196
- const capabilities = required | (config.positions.length > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
197
- const adapter = await readLifecycleContract(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block);
198
- if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase())
199
- throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
200
- const deployment = profile.topology.hookTopology === 2 ? await readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined;
201
- if (deployment !== undefined) {
202
- if (!hasLifecycleV4HookPermissions(deployment.predictedHook))
203
- throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
204
- hookDeployments.push({ marketIndex: index, ...deployment });
205
- }
206
- const identity = await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
207
- validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
208
- const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
209
- 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)))
210
- throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
211
- if (identity.venue === LifecycleVenue.UniswapV4) {
212
- const quote = market.quoteAsset.toLowerCase();
213
- if (v4Quotes.has(quote))
214
- throw new LifecyclePlanningError("DUPLICATE_V4_QUOTE", "Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
215
- v4Quotes.add(quote);
216
- }
217
- if (identities.has(identity.canonicalId.toLowerCase()))
218
- throw new LifecyclePlanningError("DUPLICATE_MARKET", "Two committed markets resolve to the same canonical pool");
219
- identities.add(identity.canonicalId.toLowerCase());
220
- const prepared = progress.markets.find((item) => item.index === index);
221
- if (prepared !== undefined) {
222
- const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
223
- const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
224
- if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96)
225
- throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
226
- await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
252
+ }
253
+ positionCount += result.positions;
254
+ if (result.deployment !== undefined)
255
+ hookDeployments.push(result.deployment);
256
+ if (result.reason !== undefined) {
257
+ reasons.push(result.reason);
258
+ continue;
259
+ }
260
+ if (result.identity === undefined)
261
+ continue;
262
+ if (result.identity.venue === LifecycleVenue.UniswapV4) {
263
+ const quote = plan.markets[index].quoteAsset.toLowerCase();
264
+ if (v4Quotes.has(quote)) {
265
+ reasons.push("Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
266
+ continue;
227
267
  }
268
+ v4Quotes.add(quote);
228
269
  }
229
- catch (failure) {
230
- reasons.push(failure instanceof Error ? failure.message : String(failure));
270
+ const identity = result.identity.canonicalId.toLowerCase();
271
+ if (identities.has(identity)) {
272
+ reasons.push("Two committed markets resolve to the same canonical pool");
273
+ continue;
231
274
  }
275
+ identities.add(identity);
232
276
  }
233
- if (progress.canonical.phase !== LifecyclePhase.None) {
234
- for (const funding of plan.funding) {
235
- const balance = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block);
236
- const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
237
- if (balance < needed)
238
- reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
239
- }
277
+ for (const [index, balance] of escrowBalances.entries()) {
278
+ const funding = plan.funding[index];
279
+ const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
280
+ if (balance < needed)
281
+ reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
240
282
  }
241
283
  return { profiles, hookDeployments, reasons };
242
284
  }
@@ -272,8 +314,12 @@ function attachEstimates(transactions, simulation) {
272
314
  }
273
315
  async function readTokenFactoryBinding(client, orchestrator, block) {
274
316
  const tokenFactory = await readLifecycleContract(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
275
- const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [tokenFactory, toHex(block.number)]), "token factory code");
276
- if (tokenFactory === zeroAddress || code === "0x" || (await readLifecycleContract(client, tokenFactory, lifecycleAdapterAbi, "core", [], block)).toLowerCase() !== orchestrator.toLowerCase())
317
+ const [codeValue, authority] = await Promise.all([
318
+ lifecyclePinnedRpc(client, "eth_getCode", [tokenFactory, toHex(block.number)], block),
319
+ readLifecycleContract(client, tokenFactory, lifecycleAdapterAbi, "core", [], block),
320
+ ]);
321
+ const code = rpcHex(codeValue, "token factory code");
322
+ if (tokenFactory === zeroAddress || code === "0x" || authority.toLowerCase() !== orchestrator.toLowerCase())
277
323
  throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
278
324
  return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
279
325
  }
@@ -357,7 +403,15 @@ export async function preparePoolBoundLifecyclePlan(options) {
357
403
  }
358
404
  /** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
359
405
  export async function planLaunch(options) {
360
- const { client, plan, account, mode } = options;
406
+ return withLifecycleReadClient(options.client, async (client) => {
407
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
408
+ if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
409
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
410
+ return planned;
411
+ });
412
+ }
413
+ async function planLaunchWithReads(options, client) {
414
+ const { plan, account, mode } = options;
361
415
  if (mode !== "atomic" && mode !== "staged")
362
416
  throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
363
417
  if (account.toLowerCase() !== plan.creator.toLowerCase())
@@ -365,21 +419,30 @@ export async function planLaunch(options) {
365
419
  if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n)
366
420
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
367
421
  validatePlanShape(plan);
368
- const block = await readLifecycleBlock(client);
369
- if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== plan.chainId)
370
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
371
422
  const planHash = hashLaunchPlan(plan);
372
423
  const launchId = hashLaunchIdentity(plan);
373
- const remoteHash = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block);
374
- const remoteIdentity = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block);
424
+ const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
425
+ readLifecycleBlock(client),
426
+ lifecycleRpc(client, "eth_chainId"),
427
+ ]));
428
+ if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId)
429
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
430
+ const prediction = readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
431
+ const [remoteHash, remoteIdentity, { tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
432
+ readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
433
+ readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
434
+ readTokenFactoryBinding(client, plan.orchestrator, block),
435
+ resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
436
+ lifecycleStage(client, "plan.progress", async () => {
437
+ const predictedToken = await prediction;
438
+ const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
439
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
440
+ return { identity, progress };
441
+ }),
442
+ ]));
375
443
  if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase())
376
444
  throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
377
- const predictedToken = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
378
- const { tokenFactory, tokenFactoryCodeHash } = await readTokenFactoryBinding(client, plan.orchestrator, block);
379
- const { limits } = await resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits);
380
445
  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: [] };
381
- const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
382
- const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
383
446
  let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
384
447
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
385
448
  if (!progress.confirmationSafe || receiptsPending) {
@@ -390,20 +453,22 @@ export async function planLaunch(options) {
390
453
  }
391
454
  // Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
392
455
  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" };
393
- await assertLifecycleBlock(client, block);
456
+ await assertLifecycleBlock(client, block, plan.chainId);
394
457
  return { ...planned, hookDeployments, simulation: waiting, atomicAttempt: waiting };
395
458
  }
396
459
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
397
- await assertLifecycleBlock(client, block);
460
+ await assertLifecycleBlock(client, block, plan.chainId);
398
461
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
399
462
  }
400
- const prerequisites = progress.canonical.phase === LifecyclePhase.None ? await readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] };
401
- const validation = await validatePendingInputs(client, planned, progress, block);
463
+ const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
464
+ progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
465
+ validatePendingInputs(client, planned, progress, block),
466
+ ]));
402
467
  const reasons = [...prerequisites.reasons, ...validation.reasons];
403
468
  planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
404
469
  if (reasons.length > 0) {
405
470
  const rejected = { ...placeholder, confidence: "stateful", reason: reasons.join("; ") };
406
- await assertLifecycleBlock(client, block);
471
+ await assertLifecycleBlock(client, block, plan.chainId);
407
472
  return { ...planned, simulation: rejected, atomicAttempt: rejected, transactions: orderedTransactions(plan, mode, progress, prerequisites.approvals, prerequisites.nativeValue, planned.preparationBatchSize) };
408
473
  }
409
474
  let atomicAttempt = { ...placeholder, reason: "Atomic execution cannot be evaluated from an already begun staged launch" };