@black-market/sdk 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +5 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +5 -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 +43 -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 +10 -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.1** addresses, limits, wire formats, fees, and diagnostics.
6
6
 
7
7
  ## Deployed infrastructure
8
8
 
@@ -19,6 +19,7 @@ start historical log queries there, in chunks of at most **10M blocks**.
19
19
  | `UFG_LAUNCH_PROXY_ADDRESS` | `0xf6673d859DE03eB74A0b01696Edb97f941dFD0CA` |
20
20
  | `UFG_TREASURY_FACTORY_ADDRESS` | `0x26Aa5D52134c8E6c4C01E9fb53A7568c0C64901F` |
21
21
  | `UFG_UPSTREAM_FACTORY_ADDRESS` | `0xA170b5A58C111334A7237F2Ea2c428ec5022f68E` |
22
+ | Known deployment `launchLens` | `0xcb4bb3caca71fe52eace8170cf83e901418646f8` |
22
23
 
23
24
  `getKnownLifecycleDeployment`, `getKnownLifecycleProfile`, and
24
25
  `listKnownLifecycleProfiles` are pure selectors keyed by chain/core. Unknown
@@ -165,9 +166,28 @@ Failed getters cannot fall back to the large header gas limit or a hard-coded 32
165
166
 
166
167
  `transactionGasCeiling`, when set, is the minimum complete-envelope chain/RPC/account
167
168
  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.
169
+ `gasEstimateL1Component` estimates poster allocation once for the next final
170
+ transaction, using its exact sender/target/calldata/value at the same block.
171
+ Compute-only calibration and fresh poster allocation size the immediate envelope
172
+ before native validation. `step.computeGasRequired` retains a separable compute
173
+ observation, never the old poster charge or provisional budget. A fresh poster
174
+ quote is combined once when that command becomes next. Both unvalidated sizing
175
+ and the final exact pass use affordable pinned envelopes; validation:false does
176
+ not disable Nitro's upfront funds check. The final pass uses exactly returned gas.
177
+ Sizing starts with per-call ceilings because unused gas is refunded between calls.
178
+ Only a genuine upfront-funds refusal permits one second unvalidated sizing attempt
179
+ with the budget shared across the sequence. Other failures do not retry; the final
180
+ exact validated pass is never replayed.
181
+
182
+ A tight payer balance can cover smaller exact staged envelopes yet not the
183
+ discovery margin. This may return `failureCategory: "affordability"` even though
184
+ the underlying launch could fit. The refused RPC exposes no successful-prefix
185
+ results; the SDK does not invent them or add more metering attempts.
186
+
187
+ Suffix steps are marked `provisional: true` and have no transaction estimate:
188
+ their conservative complete budgets check sequence feasibility, bounded across
189
+ the pinned affordable remaining balance. `buildNextTransaction` never reuses
190
+ those budgets as estimates; it freshly quotes and proves only the next command.
171
191
 
172
192
  On Nitro, `estimate.posterGas` / `posterFee` are buffered allocations **inside**
173
193
  `gasLimit`; `dataFeeIncludedInGas` is true, `dataFee` is zero, and `totalFee` equals
@@ -187,14 +207,31 @@ fixed transaction fees or proof that every permitted topology fits.
187
207
  | `LifecycleRpcClient` option | Behavior |
188
208
  | --- | --- |
189
209
  | `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. |
210
+ | `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
211
  | `onDiagnostic(event)` | Local opt-in stage/RPC timing listener; no automatic console output or telemetry. |
192
212
 
193
213
  Call `sessionCache.invalidate()` on wallet/chain switches and after each confirmed
194
- transaction. `sessionCache.block` is readonly `{ number, hash }` or undefined.
214
+ transaction to discard pinned static values. Session deployment/backend facts are
215
+ keyed by chain and account where applicable and live until the session owner is
216
+ discarded. `sessionCache.block` is readonly `{ number, hash }` or undefined.
217
+ Planning pins retain the actual RPC L2 block number and header hash. EVM
218
+ `BLOCKNUMBER`/`BLOCKHASH`, including Multicall3's metadata getters, expose L1 values
219
+ on Nitro and are not L2 pin sources.
195
220
  Balances, allowances, current progress, receipts, nonces, policy callbacks, and
196
221
  stateful execution proof stay fresh. Session caching does not extend admission.
197
222
 
223
+ One RPC L2 header supplies number, hash, timestamp, gas limit and base fee.
224
+ The initial Multicall3 call uses that explicit L2 block and includes native balance,
225
+ funding/progress and Nitro getters. Independent getters are aggregated with no
226
+ minimum-count threshold. Multicall3 runtime code is attested once per session.
227
+ The 4663 preset enables `launchLensAbi.snapshot` without caller configuration:
228
+ one call replaces dependent token/hook/market/funding reads inside that aggregate.
229
+ Its `launchLensCodeHash` pins the reviewed deployed runtime as frozen release
230
+ metadata, like `tokenFactoryCodeHash`, without another guard-only RPC.
231
+ The deployed preset `hookSaltMiner` replaces local mining during combined
232
+ preparation; every returned winner is verified locally.
233
+ Lens failures surface directly; planning does not retry through legacy reads.
234
+
198
235
  Diagnostic events carry `stage`, `phase`, optional `durationMs`, `queueMs`,
199
236
  `requestId`, allowlisted RPC `method`, and cache `hits` / `misses` / `reuses`.
200
237
  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.1** 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.1 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.1",
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. Its address is owned by the deployment preset. */
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,11 @@ export type LifecycleDeploymentPreset = {
61
61
  readonly tokenFactory: Address;
62
62
  readonly tokenFactoryCodeHash: Hex;
63
63
  readonly fundingEscrow: Address;
64
+ /** Optional SDK-owned read-only acceleration contracts. */
65
+ readonly hookSaltMiner?: Address;
66
+ readonly launchLens?: Address;
67
+ /** Frozen reviewed runtime identity, like tokenFactoryCodeHash; not live admission. */
68
+ readonly launchLensCodeHash?: Hex;
64
69
  readonly profiles: readonly LifecycleProfilePreset[];
65
70
  readonly provenance: {
66
71
  readonly sourceCommit: string;
@@ -325,6 +330,11 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
325
330
  tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
326
331
  tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
327
332
  fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
333
+ hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
334
+ // LaunchLens/871f9385, L2 deployment block 100095353.
335
+ // tx 0xafdf35fa50d2c3fe08f1ab231a4e5d5e4dd5629c57f14dfe0a3aff5b0b8404ff
336
+ launchLens: "0xcb4bb3caca71fe52eace8170cf83e901418646f8",
337
+ launchLensCodeHash: "0xc19e86bfed47afbb6ff719293edd8fb8bb3399d5d017b3f533d90cc72e58f1d3",
328
338
  profiles: Object.freeze([
329
339
  abyssPreset(KnownLifecycleProfile.Abyss0, "0x49b2ed65e2021247951193dd2c671b40867b6e064af65a2d98a70f57556412fc"),
330
340
  abyssPreset(KnownLifecycleProfile.Abyss1, "0x2f6f494f8810473b3c0ac6b03b1a1353d067ffa5c86d3cba16b04c6b61f73c05"),