@black-market/sdk 0.13.0 → 0.14.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 (53) hide show
  1. package/README.md +4 -4
  2. package/dist/lifecycle/index.d.ts +3 -1
  3. package/dist/lifecycle/index.d.ts.map +1 -1
  4. package/dist/lifecycle/index.js +3 -1
  5. package/dist/lifecycle/index.js.map +1 -1
  6. package/dist/lifecycle/lens.d.ts +409 -0
  7. package/dist/lifecycle/lens.d.ts.map +1 -0
  8. package/dist/lifecycle/lens.js +30 -0
  9. package/dist/lifecycle/lens.js.map +1 -0
  10. package/dist/lifecycle/mining.d.ts +47 -0
  11. package/dist/lifecycle/mining.d.ts.map +1 -0
  12. package/dist/lifecycle/mining.js +89 -0
  13. package/dist/lifecycle/mining.js.map +1 -0
  14. package/dist/lifecycle/planner.d.ts.map +1 -1
  15. package/dist/lifecycle/planner.js +70 -62
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +3 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +1 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +48 -0
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +31 -20
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +6 -2
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +47 -44
  28. package/dist/lifecycle/rpc.js.map +1 -1
  29. package/dist/lifecycle/simulation.d.ts +5 -3
  30. package/dist/lifecycle/simulation.d.ts.map +1 -1
  31. package/dist/lifecycle/simulation.js +130 -109
  32. package/dist/lifecycle/simulation.js.map +1 -1
  33. package/dist/lifecycle/snapshot.d.ts +8 -0
  34. package/dist/lifecycle/snapshot.d.ts.map +1 -0
  35. package/dist/lifecycle/snapshot.js +79 -0
  36. package/dist/lifecycle/snapshot.js.map +1 -0
  37. package/dist/lifecycle/types.d.ts +15 -5
  38. package/dist/lifecycle/types.d.ts.map +1 -1
  39. package/dist/lifecycle/types.js.map +1 -1
  40. package/docs/launch-lifecycle.md +42 -20
  41. package/docs/reference.md +39 -6
  42. package/examples/README.md +2 -2
  43. package/package.json +1 -1
  44. package/src/lifecycle/index.ts +5 -1
  45. package/src/lifecycle/lens.ts +42 -0
  46. package/src/lifecycle/mining.ts +80 -0
  47. package/src/lifecycle/planner.ts +72 -58
  48. package/src/lifecycle/presets.ts +4 -0
  49. package/src/lifecycle/progress.ts +26 -16
  50. package/src/lifecycle/rpc.ts +45 -43
  51. package/src/lifecycle/simulation.ts +126 -100
  52. package/src/lifecycle/snapshot.ts +72 -0
  53. package/src/lifecycle/types.ts +15 -5
package/docs/reference.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use the [launch guide](launch-lifecycle.md) for the end-to-end flow and the
4
4
  [runner reference](../examples/README.md) for environment and recovery artifacts.
5
- This page covers SDK **0.13.0** addresses, limits, wire formats, fees, and diagnostics.
5
+ This page covers SDK **0.14.0** addresses, limits, wire formats, fees, and diagnostics.
6
6
 
7
7
  ## Deployed infrastructure
8
8
 
@@ -165,9 +165,28 @@ Failed getters cannot fall back to the large header gas limit or a hard-coded 32
165
165
 
166
166
  `transactionGasCeiling`, when set, is the minimum complete-envelope chain/RPC/account
167
167
  restriction. It is separate from compute. NodeInterface (`0xc8`)
168
- `gasEstimateL1Component` estimates poster allocation using the exact sender/target/
169
- calldata/value at the same block. Returned transaction gas can exceed compute gas
170
- because it includes poster allocation. Exact native replay, not the estimate, proves fit.
168
+ `gasEstimateL1Component` estimates poster allocation once for the next final
169
+ transaction, using its exact sender/target/calldata/value at the same block.
170
+ Compute-only calibration and fresh poster allocation size the immediate envelope
171
+ before native validation. `step.computeGasRequired` retains a separable compute
172
+ observation, never the old poster charge or provisional budget. A fresh poster
173
+ quote is combined once when that command becomes next. Both unvalidated sizing
174
+ and the final exact pass use affordable pinned envelopes; validation:false does
175
+ not disable Nitro's upfront funds check. The final pass uses exactly returned gas.
176
+ Sizing starts with per-call ceilings because unused gas is refunded between calls.
177
+ Only a genuine upfront-funds refusal permits one second unvalidated sizing attempt
178
+ with the budget shared across the sequence. Other failures do not retry; the final
179
+ exact validated pass is never replayed.
180
+
181
+ A tight payer balance can cover smaller exact staged envelopes yet not the
182
+ discovery margin. This may return `failureCategory: "affordability"` even though
183
+ the underlying launch could fit. The refused RPC exposes no successful-prefix
184
+ results; the SDK does not invent them or add more metering attempts.
185
+
186
+ Suffix steps are marked `provisional: true` and have no transaction estimate:
187
+ their conservative complete budgets check sequence feasibility, bounded across
188
+ the pinned affordable remaining balance. `buildNextTransaction` never reuses
189
+ those budgets as estimates; it freshly quotes and proves only the next command.
171
190
 
172
191
  On Nitro, `estimate.posterGas` / `posterFee` are buffered allocations **inside**
173
192
  `gasLimit`; `dataFeeIncludedInGas` is true, `dataFee` is zero, and `totalFee` equals
@@ -187,14 +206,28 @@ fixed transaction fees or proof that every permitted topology fits.
187
206
  | `LifecycleRpcClient` option | Behavior |
188
207
  | --- | --- |
189
208
  | `supportsReadBatching: true` | Opt into at most eight concurrent SDK reads per invocation; set only when your transport batches concurrent requests. |
190
- | `sessionCache: createLifecycleSessionCache()` | Caller-owned reuse of SDK-selected static reads at one canonical chain/block; default has no cross-invocation cache. |
209
+ | `sessionCache: createLifecycleSessionCache()` | Caller-owned reuse of SDK-selected static reads at one pin; successful deployment/backend observations also survive across blocks. Without it, those observations belong to the supplied client object. |
191
210
  | `onDiagnostic(event)` | Local opt-in stage/RPC timing listener; no automatic console output or telemetry. |
192
211
 
193
212
  Call `sessionCache.invalidate()` on wallet/chain switches and after each confirmed
194
- transaction. `sessionCache.block` is readonly `{ number, hash }` or undefined.
213
+ transaction to discard pinned static values. Session deployment/backend facts are
214
+ keyed by chain and account where applicable and live until the session owner is
215
+ discarded. `sessionCache.block` is readonly `{ number, hash }` or undefined.
216
+ Planning pins retain the actual RPC L2 block number and header hash. EVM
217
+ `BLOCKNUMBER`/`BLOCKHASH`, including Multicall3's metadata getters, expose L1 values
218
+ on Nitro and are not L2 pin sources.
195
219
  Balances, allowances, current progress, receipts, nonces, policy callbacks, and
196
220
  stateful execution proof stay fresh. Session caching does not extend admission.
197
221
 
222
+ One RPC L2 header supplies number, hash, timestamp, gas limit and base fee.
223
+ The initial Multicall3 call uses that explicit L2 block and includes native balance,
224
+ funding/progress and Nitro getters. Independent getters are aggregated with no
225
+ minimum-count threshold. Runtime code is attested once per
226
+ session. The optional preset `launchLens` remains unset until deployment; once set,
227
+ one `launchLensAbi.snapshot` call replaces dependent token/hook/market/funding reads
228
+ inside that aggregate. The deployed preset `hookSaltMiner` replaces local mining
229
+ during combined preparation; every returned winner is verified locally.
230
+
198
231
  Diagnostic events carry `stage`, `phase`, optional `durationMs`, `queueMs`,
199
232
  `requestId`, allowlisted RPC `method`, and cache `hits` / `misses` / `reuses`.
200
233
  Queue time is separate from request time; request IDs are local sequences.
@@ -1,6 +1,6 @@
1
1
  # Run the examples
2
2
 
3
- These commands use the checkout's built `@black-market/sdk` **0.13.0** and viem.
3
+ These commands use the checkout's built `@black-market/sdk` **0.14.0** and viem.
4
4
  Use **Node 24+** for direct TypeScript execution. Start with the one-market
5
5
  `launch-erc20-abyss.ts`; larger cases can exceed the RPC's simulation budget.
6
6
  For valuation, token ordering, ranges, and buy protection, read the
@@ -20,7 +20,7 @@ chmod 600 .env
20
20
  Launch wrappers load `.env` from the working directory with `process.loadEnvFile`.
21
21
  Existing environment values take precedence. Values are literal; shell interpolation
22
22
  is not applied. Use the matching SDK version when copying wrappers into a consumer project:
23
- `pnpm add @black-market/sdk@0.13.0 viem`.
23
+ `pnpm add @black-market/sdk@0.14.0 viem`.
24
24
  Copy `launch-example.ts`, `launch-example-support.ts`, and `launch-example-kyber.ts` with the wrapper.
25
25
 
26
26
  ### Configure the environment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@black-market/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "TypeScript SDK for Abyss token launches, swaps, and lending.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,11 +5,15 @@ export * from "./abi.js";
5
5
  export * from "./markets.js";
6
6
  export * from "./hmm.js";
7
7
  export * from "./ufg.js";
8
- export * from "./progress.js";
8
+ export { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLifecycleTokenAtBlock, readLifecycleHeadProgress, predictLifecycleToken,
9
+ validateV4LifecycleOracle, readLifecycleProfiles, readPoolBoundHookDeployment,
10
+ buildPoolBoundHookDeploymentTransaction, validateLifecycleMarketIdentity, readLaunchProgress } from "./progress.js";
9
11
  export * from "./fork.js";
10
12
  export * from "./authors.js";
11
13
  export * from "./presets.js";
12
14
  export * from "./transactions.js";
13
15
  export { createLifecycleSessionCache } from "./rpc.js";
16
+ export { launchLensAbi } from "./lens.js";
17
+ export { hookSaltMinerAbi } from "./mining.js";
14
18
  export { planLaunch, simulateLaunchPlan, buildNextTransaction, prepareAndPlanLifecycleLaunch } from "./planner.js";
15
19
  export { assertLaunchTopologyCoverage, getTickAtSqrtRatio, type LaunchTopologyCoverageInput, type LaunchTopologyPosition } from "../launch.js";
@@ -0,0 +1,42 @@
1
+ import { type Address, type Hex } from "viem";
2
+ import { launchPlanV1Components, launchProgressV1Components, preparedMarketV1Components, marketLiveStateV1Components, type LaunchProgressV1, type PreparedMarketV1, type MarketLiveStateV1 } from "./schema.js";
3
+ import type { LifecycleBlock, LifecycleRpcClient } from "./types.js";
4
+
5
+ /** ILaunchLens.sol, versioned planning snapshot. No preset lens is deployed by this release. */
6
+ export const launchLensAbi = [{ type: "function", name: "snapshot", stateMutability: "view",
7
+ inputs: [{ name: "plan", type: "tuple", components: launchPlanV1Components }, { name: "owner", type: "address" },
8
+ { name: "fundingEscrow", type: "address" }, { name: "tokens", type: "address[]" }, { name: "maxAttempts", type: "uint256" }],
9
+ outputs: [{ name: "result", type: "tuple", components: [
10
+ { name: "chainId", type: "uint256" }, { name: "blockNumber", type: "uint256" },
11
+ { name: "parentBlockHash", type: "bytes32" }, { name: "timestamp", type: "uint256" }, { name: "nativeBalance", type: "uint256" },
12
+ { name: "tokenBalances", type: "tuple[]", components: [{ name: "token", type: "address" }, { name: "balance", type: "uint256" }, { name: "allowance", type: "uint256" }] },
13
+ { name: "predictedToken", type: "address" }, { name: "launchId", type: "bytes32" },
14
+ { name: "progress", type: "tuple", components: launchProgressV1Components },
15
+ { name: "preparedMarkets", type: "tuple[]", components: [
16
+ { name: "index", type: "uint32" }, { name: "adapter", type: "address" }, { name: "prepared", type: "tuple", components: preparedMarketV1Components },
17
+ { name: "liveStateSuccess", type: "bool" }, { name: "live", type: "tuple", components: marketLiveStateV1Components }, { name: "liveStateError", type: "bytes" },
18
+ ] },
19
+ { name: "hookDeployments", type: "tuple[]", components: [
20
+ { name: "marketIndex", type: "uint32" }, { name: "adapter", type: "address" }, { name: "deployer", type: "address" },
21
+ { name: "constructorParameters", type: "bytes" }, { name: "initCodeHash", type: "bytes32" }, { name: "salt", type: "bytes32" }, { name: "hook", type: "address" }, { name: "found", type: "bool" },
22
+ ] },
23
+ ] }],
24
+ }] as const;
25
+
26
+ export type LifecycleLensSnapshot = {
27
+ chainId: bigint; blockNumber: bigint; parentBlockHash: Hex; timestamp: bigint; nativeBalance: bigint;
28
+ tokenBalances: readonly { token: Address; balance: bigint; allowance: bigint }[];
29
+ predictedToken: Address; launchId: Hex; progress: LaunchProgressV1;
30
+ preparedMarkets: readonly { index: number; adapter: Address; prepared: PreparedMarketV1; liveStateSuccess: boolean; live: MarketLiveStateV1; liveStateError: Hex }[];
31
+ hookDeployments: readonly { marketIndex: number; adapter: Address; deployer: Address; constructorParameters: Hex; initCodeHash: Hex; salt: Hex; hook: Address; found: boolean }[];
32
+ };
33
+ const snapshots = new WeakMap<LifecycleRpcClient, { blockHash: Hex; snapshot: LifecycleLensSnapshot }>();
34
+
35
+ export function observeLifecycleLens(client: LifecycleRpcClient, block: LifecycleBlock, snapshot: LifecycleLensSnapshot): void {
36
+ snapshots.set(client, { blockHash: block.hash.toLowerCase() as Hex, snapshot });
37
+ }
38
+
39
+ export function lifecycleLensSnapshot(client: LifecycleRpcClient, block: LifecycleBlock): LifecycleLensSnapshot | undefined {
40
+ const observation = snapshots.get(client);
41
+ return observation?.snapshot.blockNumber === block.number && observation.blockHash === block.hash.toLowerCase() ? observation.snapshot : undefined;
42
+ }
@@ -0,0 +1,80 @@
1
+ import { parseAbi, type Address, type Hex } from "viem";
2
+ import { hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress } from "./markets.js";
3
+ import { readLifecycleContract } from "./rpc.js";
4
+ import { LifecyclePlanningError, type LifecycleBlock, type LifecyclePoolBoundHookDeployment, type LifecycleRpcClient, type PoolBoundLifecyclePreparationProgress } from "./types.js";
5
+ import { lifecycleLensSnapshot } from "./lens.js";
6
+
7
+ export const hookSaltMinerAbi = parseAbi(["function findMany((address deployer, bytes32 initCodeHash, uint256 startSalt, uint160 flags)[] searches, uint256 maxAttempts) pure returns (bytes32[] salts, address[] hooks, bool[] found)"]);
8
+
9
+ /** Read-only bounded EVM mining; every returned winner is checked with local CREATE2. */
10
+ export async function mineLifecycleHooks(options: {
11
+ client: LifecycleRpcClient; block: LifecycleBlock; miner?: Address;
12
+ deployments: readonly LifecyclePoolBoundHookDeployment[]; signal?: AbortSignal;
13
+ onProgress?: (progress: PoolBoundLifecyclePreparationProgress) => void;
14
+ }): Promise<LifecyclePoolBoundHookDeployment[]> {
15
+ const { deployments, signal, onProgress, miner, client, block } = options;
16
+ const checkActive = () => { if (signal?.aborted) throw Object.assign(new Error("Lifecycle hook mining cancelled"), { name: "AbortError" }); };
17
+ checkActive();
18
+ if (miner === undefined) return Promise.all(deployments.map(async (deployment) => ({ ...deployment,
19
+ ...await minePoolBoundHookSalt({ ...deployment, startSalt: BigInt(deployment.salt), signal,
20
+ onProgress: onProgress === undefined ? undefined : (progress) => onProgress({ marketIndex: deployment.marketIndex, ...progress }) }),
21
+ })));
22
+ const results = deployments.map((deployment) => ({ ...deployment }));
23
+ const lens = lifecycleLensSnapshot(client, block);
24
+ let pending: number[] = [];
25
+ for (const [index, deployment] of deployments.entries()) {
26
+ const observed = lens?.hookDeployments.find((hook) => hook.marketIndex === deployment.marketIndex);
27
+ if (observed === undefined) { pending.push(index); continue; }
28
+ const predictedHook = predictPoolBoundHookAddress({ ...deployment, salt: observed.salt });
29
+ if (observed.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || observed.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() ||
30
+ BigInt(observed.salt) < BigInt(deployment.salt) || observed.found && (observed.hook.toLowerCase() !== predictedHook.toLowerCase() || !hasLifecycleV4HookPermissions(predictedHook))) {
31
+ throw new LifecyclePlanningError("INVALID_MINER_RESULT", "LaunchLens hook result differs from the locally derived constructor or CREATE2 address");
32
+ }
33
+ results[index] = { ...deployment, salt: observed.salt, predictedHook };
34
+ if (!observed.found) pending.push(index);
35
+ else onProgress?.({ marketIndex: deployment.marketIndex, salt: observed.salt, predictedHook, attempts: BigInt(observed.salt) - BigInt(deployment.salt) + 1n });
36
+ }
37
+ while (pending.length > 0) {
38
+ checkActive();
39
+ let maxAttempts = BigInt(Math.floor(150_000 / pending.length));
40
+ for (const index of pending) {
41
+ const available = (1n << 256n) - 1n - BigInt(results[index]!.salt);
42
+ if (available < maxAttempts) maxAttempts = available;
43
+ }
44
+ if (maxAttempts === 0n) {
45
+ pending = pending.filter((index) => {
46
+ const deployment = results[index]!;
47
+ if (BigInt(deployment.salt) !== (1n << 256n) - 1n) return true;
48
+ const predictedHook = predictPoolBoundHookAddress(deployment);
49
+ if (!hasLifecycleV4HookPermissions(predictedHook)) throw new LifecyclePlanningError("HOOK_SALT_EXHAUSTED", "No valid hook address remains in the uint256 salt range");
50
+ results[index] = { ...deployment, predictedHook };
51
+ onProgress?.({ marketIndex: deployment.marketIndex, salt: deployment.salt, predictedHook, attempts: BigInt(deployment.salt) - BigInt(deployments[index]!.salt) + 1n });
52
+ return false;
53
+ });
54
+ continue;
55
+ }
56
+ const searches = pending.map((index) => ({ deployer: results[index]!.deployer, initCodeHash: results[index]!.initCodeHash, startSalt: BigInt(results[index]!.salt), flags: 0x1afcn }));
57
+ const [salts, hooks, found] = await readLifecycleContract<readonly [readonly Hex[], readonly Address[], readonly boolean[]]>(client, miner, hookSaltMinerAbi, "findMany", [searches, maxAttempts], block);
58
+ checkActive();
59
+ if (salts.length !== pending.length || hooks.length !== pending.length || found.length !== pending.length) throw new LifecyclePlanningError("INVALID_MINER_RESULT", "Hook miner omitted a search result");
60
+ const unfinished: number[] = [];
61
+ for (const [ordinal, index] of pending.entries()) {
62
+ const deployment = results[index]!;
63
+ const salt = salts[ordinal]!;
64
+ const end = searches[ordinal]!.startSalt + maxAttempts;
65
+ const predictedHook = predictPoolBoundHookAddress({ ...deployment, salt });
66
+ if (found[ordinal]) {
67
+ if (BigInt(salt) < searches[ordinal]!.startSalt || BigInt(salt) >= end || hooks[ordinal]!.toLowerCase() !== predictedHook.toLowerCase() || !hasLifecycleV4HookPermissions(predictedHook)) throw new LifecyclePlanningError("INVALID_MINER_RESULT", "Hook miner returned an invalid CREATE2 salt or permission mask");
68
+ } else {
69
+ if (BigInt(salt) !== end) throw new LifecyclePlanningError("INVALID_MINER_RESULT", "Hook miner did not advance to the next unsearched salt");
70
+ unfinished.push(index);
71
+ }
72
+ results[index] = { ...deployment, salt, predictedHook };
73
+ onProgress?.({ marketIndex: deployment.marketIndex, salt, predictedHook, attempts: BigInt(salt) - BigInt(deployments[index]!.salt) + (found[ordinal] ? 1n : 0n) });
74
+ checkActive();
75
+ }
76
+ pending = unfinished;
77
+ }
78
+ checkActive();
79
+ return results;
80
+ }
@@ -3,10 +3,10 @@ import { ABYSS_FEE_TIERS } from "../abyss.js";
3
3
  import { assertLaunchTopologyCoverage, getTickAtSqrtRatio } from "../launch.js";
4
4
  import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1 } from "./schema.js";
6
- import { readLaunchProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
- import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
8
- import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
- import { prepareLaunchSimulation, simulateLaunchTransactions, type NextTransactionEnvelope } from "./simulation.js";
6
+ import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
+ import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
8
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
+ import { prepareLaunchSimulation, simulateLaunchTransactions, type GasSizingHints, type NextTransactionEnvelope } from "./simulation.js";
10
10
  import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
12
12
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
@@ -14,6 +14,8 @@ import { buildLaunchTransactions, packPreparationBatches } from "./transactions.
14
14
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
15
15
  import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
16
16
  import { buildUfgLaunchTransactions, buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
17
+ import { readLifecyclePlanningBlock } from "./snapshot.js";
18
+ import { mineLifecycleHooks } from "./mining.js";
17
19
 
18
20
  type ValidatedPlanShape = {
19
21
  commitment: Hex;
@@ -293,29 +295,26 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
293
295
  fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
294
296
  };
295
297
  const result = await withLifecycleReadClient(options.client, async (client) => {
296
- const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
298
+ const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
299
+ const chainId = await readLifecycleChainId(client);
297
300
  if (chainId !== draft.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
298
301
  const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
299
302
  ? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
300
303
  assertDraft();
301
304
  const markets = [...draft.markets];
302
- const minedDeployments: LifecyclePoolBoundHookDeployment[] = [];
303
- for (const [marketIndex, deployment] of metadata.entries()) {
304
- if (deployment === undefined) continue;
305
+ const minedDeployments = await mineLifecycleHooks({ client, block,
306
+ miner: getKnownLifecycleDeployment({ chainId, orchestrator: draft.orchestrator })?.hookSaltMiner,
307
+ deployments: metadata.flatMap((deployment, marketIndex) => deployment === undefined ? [] : [{ marketIndex, ...deployment }]),
308
+ signal: options.signal, onProgress: options.onProgress });
309
+ for (const mined of minedDeployments) {
305
310
  assertDraft();
306
- const mined = await minePoolBoundHookSalt({
307
- deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
308
- onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
309
- });
311
+ const marketIndex = mined.marketIndex;
310
312
  const market = markets[marketIndex]!;
311
313
  if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
312
314
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
313
- minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
314
315
  }
315
316
  assertDraft();
316
- // Mining is local construction, not a state admission. Economic observations
317
- // start at a fresh post-mining snapshot and share it through final protection.
318
- const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
317
+ const executionBlock = block;
319
318
  assertDraft();
320
319
  const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
321
320
  const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
@@ -325,19 +324,21 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
325
324
  });
326
325
  let plan: LaunchPlanV1 = { ...draft, markets };
327
326
  let ownedProgress: CanonicalLaunchProgress | undefined;
327
+ let sizing: GasSizingHints | undefined;
328
328
  if (needsProtection) {
329
329
  // A diagnostic minimum is never returned as an admitted or executable plan.
330
330
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
331
331
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
332
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, undefined, finalEnvelope));
332
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
333
333
  assertDraft();
334
334
  if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
335
335
  plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
336
336
  // This is the real SDK-owned None observation, not an assumed new-launch
337
337
  // state. No onchain commitment existed at this exact snapshot to replace.
338
338
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
339
+ sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed!]));
339
340
  }
340
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, undefined, finalEnvelope));
341
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
341
342
  assertDraft();
342
343
  assertFinalPlan(planned, options.signal);
343
344
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -365,12 +366,12 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
365
366
  return result;
366
367
  }
367
368
 
368
- async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction, fixedFinalEnvelope?: NextTransactionEnvelope): Promise<PlannedLaunch> {
369
+ async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, fixedFinalEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<PlannedLaunch> {
369
370
  const { plan, account, mode, ufg } = options;
370
371
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
371
372
  const launchId = hashLaunchIdentity(plan);
372
373
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
373
- pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
374
+ pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
374
375
  ]));
375
376
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
376
377
  // Only preparation/build-next can supply freshly observed progress from this
@@ -413,11 +414,11 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
413
414
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
414
415
  }
415
416
  const waiting = { ...placeholder, confidence: "stateful" as const, reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
416
- await assertLifecycleBlock(client, block, plan.chainId);
417
+
417
418
  return { ...planned, hookDeployments, simulation: waiting };
418
419
  }
419
420
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
420
- await assertLifecycleBlock(client, block, plan.chainId);
421
+
421
422
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
422
423
  }
423
424
  // Topology is a creation policy, not a retroactive obstacle to recovery of
@@ -432,43 +433,30 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
432
433
  const prepared = progress.canonical.preparedMarkets;
433
434
  const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
434
435
  let batches: number[] = [];
435
- if (mode === "staged") batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
436
- : [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
437
- if (reviewed !== undefined && reviewed.kind === "prepare") {
438
- // Canonical progress fixes the start; the exact held calldata fixes its span.
439
- // Never rediscover a batch or enlarge a reviewed transaction.
440
- const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
441
- const start = Number(match?.[1]), count = Number(match?.[2]);
442
- if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
443
- batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
444
- }
436
+ if (mode === "staged") batches = packPreparationBatches(plan, prepared, computeCap);
445
437
  let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
446
438
  if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0]!.kind !== "atomic")) {
447
439
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
448
440
  }
449
- if (reviewed !== undefined) {
450
- if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
451
- transactions = transactions.slice(0, 1);
452
- }
453
441
  if (metadata.reasons.length > 0) {
454
- await assertLifecycleBlock(client, block, plan.chainId);
442
+
455
443
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
456
444
  }
457
- const envelope = diagnostic ? undefined : reviewed ?? fixedFinalEnvelope;
458
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice);
445
+ const envelope = diagnostic ? undefined : fixedFinalEnvelope;
446
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
459
447
  try {
460
448
  const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, envelope);
461
449
  let simulation = await execute(transactions);
462
450
  assertFinalPlan(planned, options.signal);
463
451
  if (mode === "staged") {
464
- // Only a measurement that consumed almost its entire legal envelope
465
- // invalidates packing estimates, including gas hidden by CREATE wrappers.
466
- while (reviewed === undefined && !simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
452
+ // Only pressure at the native ceiling can invalidate packing. A smaller
453
+ // affordability/RPC/account envelope is not evidence for another wallet write.
454
+ while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
467
455
  const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
468
456
  const failedTransaction = transactions[failedIndex];
469
457
  const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
470
458
  const count = failedTransaction?.marketCount ?? 0;
471
- if (count <= 1 || step?.estimate !== undefined || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasUsed * 100n < step.gasLimit * 95n) break;
459
+ if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
472
460
  let first = 0;
473
461
  const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
474
462
  const left = Math.ceil(count / 2);
@@ -481,9 +469,6 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
481
469
  } else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
482
470
  simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
483
471
  }
484
- if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
485
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
486
- }
487
472
  return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
488
473
  } finally { preparedSimulation.close(); }
489
474
  }
@@ -506,7 +491,6 @@ async function preflightNextTransaction(options: BuildNextTransactionOptions, ne
506
491
  ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
507
492
  }, "latest"]), "submission gas estimate");
508
493
  if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
509
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
510
494
  assertPlanningActive(options.signal);
511
495
  } catch (failure) {
512
496
  if (failure instanceof Error && failure.name === "AbortError") throw failure;
@@ -528,7 +512,7 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
528
512
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
529
513
  from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
530
514
  const result = await withLifecycleReadClient(options.client, async (client) => {
531
- const block = await readLifecycleBlock(client);
515
+ const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
532
516
  const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
533
517
  if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
534
518
  await Promise.all([
@@ -537,12 +521,12 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
537
521
  resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
538
522
  ]);
539
523
  }
540
- const progress = await readLaunchProgress({ ...options, client }, block);
524
+ const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
541
525
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
542
526
  // Only the confirmed canonical terminal phase is terminal; a head-only
543
527
  // Active observation is not, and the confirmation/nonce/receipt gates below
544
528
  // remain in force for every nonterminal case.
545
- await assertLifecycleBlock(client, block, options.planned.chainId);
529
+
546
530
  return undefined;
547
531
  }
548
532
  if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
@@ -562,14 +546,44 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
562
546
  if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
563
547
  return preflightNextTransaction(options, next, simulation, reviewed);
564
548
  }
565
- 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,
566
- ufg: options.planned.ufg, allowTopologyGaps: options.planned.allowTopologyGaps, allowMissingPermanentTail: options.planned.allowMissingPermanentTail }, client, block, progress, false, reviewed));
567
- assertFinalPlan(refreshed, options.signal);
568
- if (!refreshed.simulation.admitted) 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) return undefined;
572
- return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
549
+ const { planned } = options;
550
+ assertFinalPlan(planned, options.signal);
551
+ const funding = progress.canonical.phase === LifecyclePhase.None
552
+ ? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
553
+ : { prerequisites: [], approvals: [] };
554
+ const prepared = progress.canonical.preparedMarkets;
555
+ let batches: number[] = [];
556
+ if (planned.mode === "staged") {
557
+ let suffixStart = prepared;
558
+ const prefix = prepared > 0 ? [prepared] : [];
559
+ if (reviewed?.kind === "prepare") {
560
+ const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
561
+ const first = Number(span?.[1]), count = Number(span?.[2]);
562
+ if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
563
+ prefix.push(count);
564
+ suffixStart += count;
565
+ }
566
+ const suffix = planned.transactions
567
+ .filter((transaction) => transaction.kind === "prepare" && transaction.marketStart! + transaction.marketCount! > suffixStart)
568
+ .map((transaction) => transaction.marketStart! + transaction.marketCount! - Math.max(transaction.marketStart!, suffixStart));
569
+ if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
570
+ const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
571
+ const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
572
+ suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
573
+ }
574
+ batches = [...prefix, ...suffix];
575
+ }
576
+ const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
577
+ if (candidate === undefined) return undefined;
578
+ if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
579
+ const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
580
+ const compute = observed?.computeGasRequired;
581
+ const sizing: GasSizingHints | undefined = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
582
+ const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
583
+ assertFinalPlan(planned, options.signal);
584
+ if (!simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
585
+ const next = attachEstimates([candidate], simulation)[0]!;
586
+ return preflightNextTransaction(options, next, simulation, reviewed);
573
587
  });
574
588
  assertFinalPlan(options.planned, options.signal);
575
589
  if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
@@ -61,6 +61,9 @@ export type LifecycleDeploymentPreset = {
61
61
  readonly tokenFactory: Address;
62
62
  readonly tokenFactoryCodeHash: Hex;
63
63
  readonly fundingEscrow: Address;
64
+ /** Optional read-only acceleration contracts. An unset lens is not a deployed feature. */
65
+ readonly hookSaltMiner?: Address;
66
+ readonly launchLens?: Address;
64
67
  readonly profiles: readonly LifecycleProfilePreset[];
65
68
  readonly provenance: {
66
69
  readonly sourceCommit: string;
@@ -325,6 +328,7 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
325
328
  tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
326
329
  tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
327
330
  fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
331
+ hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
328
332
  profiles: Object.freeze([
329
333
  abyssPreset(KnownLifecycleProfile.Abyss0, "0x49b2ed65e2021247951193dd2c671b40867b6e064af65a2d98a70f57556412fc"),
330
334
  abyssPreset(KnownLifecycleProfile.Abyss1, "0x2f6f494f8810473b3c0ac6b03b1a1353d067ffa5c86d3cba16b04c6b61f73c05"),