@black-market/sdk 0.12.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 (57) hide show
  1. package/README.md +96 -96
  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 +121 -78
  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 +51 -1
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +149 -73
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +14 -4
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +171 -12
  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 +137 -115
  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 +27 -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 +627 -1495
  41. package/docs/reference.md +365 -0
  42. package/examples/.env.example +3 -2
  43. package/examples/README.md +171 -552
  44. package/examples/launch-example-support.ts +11 -3
  45. package/examples/launch-example.ts +1 -1
  46. package/examples/launch-lifecycle-smoke.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/lifecycle/index.ts +5 -1
  49. package/src/lifecycle/lens.ts +42 -0
  50. package/src/lifecycle/mining.ts +80 -0
  51. package/src/lifecycle/planner.ts +122 -73
  52. package/src/lifecycle/presets.ts +4 -0
  53. package/src/lifecycle/progress.ts +132 -59
  54. package/src/lifecycle/rpc.ts +163 -13
  55. package/src/lifecycle/simulation.ts +134 -106
  56. package/src/lifecycle/snapshot.ts +72 -0
  57. package/src/lifecycle/types.ts +24 -6
@@ -607,9 +607,7 @@ export function createLocalLaunchWallet({ privateKey, creator, chainId, orchestr
607
607
  if (request[field] < 0n) throw exampleError("INVALID_ENVELOPE", `${field} must be unsigned`);
608
608
  }
609
609
  }
610
- request.nonce = envelope.nonce === undefined
611
- ? exactNumber(await client.request({ method: "eth_getTransactionCount", params: [creator, "pending"] }), "nonce")
612
- : exactNumber(envelope.nonce, "nonce");
610
+ if (envelope.nonce !== undefined) request.nonce = exactNumber(envelope.nonce, "nonce");
613
611
  if (request.type !== undefined) {
614
612
  request.type = transactionTypes[request.type];
615
613
  if (!request.type) throw exampleError("INVALID_ENVELOPE", "Only legacy/EIP-2930/EIP-1559 envelopes are supported");
@@ -622,6 +620,16 @@ export function createLocalLaunchWallet({ privateKey, creator, chainId, orchestr
622
620
  request.maxFeePerGas ??= fees.maxFeePerGas;
623
621
  request.maxPriorityFeePerGas ??= fees.maxPriorityFeePerGas;
624
622
  }
623
+ const [nonceValue, balanceValue] = await Promise.all([
624
+ client.request({ method: "eth_getTransactionCount", params: [creator, "pending"] }),
625
+ client.request({ method: "eth_getBalance", params: [creator, "pending"] }),
626
+ ]);
627
+ const pendingNonce = exactNumber(nonceValue, "pending nonce");
628
+ if (request.nonce !== undefined && request.nonce !== pendingNonce) throw exampleError("PENDING_NONCE_CHANGED", "Pending account nonce differs from the exact reviewed envelope");
629
+ request.nonce = pendingNonce;
630
+ const maximumFee = request.gas * (request.gasPrice ?? request.maxFeePerGas);
631
+ if (BigInt(balanceValue) < request.value + maximumFee) throw exampleError("INSUFFICIENT_PENDING_BALANCE", "Pending balance cannot fund the exact transaction value and maximum gas envelope");
632
+ await assertChain();
625
633
  const rawTransaction = await account.signTransaction(request);
626
634
  redactor.addSecret(rawTransaction);
627
635
  const wire = { ...envelope, chainId: toHex(id), nonce: toHex(request.nonce) };
@@ -494,7 +494,7 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
494
494
  const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client, reviewedTransaction, signal });
495
495
  if (!next) return { references, activation };
496
496
  assert.ok(reviewedTransaction, "No transaction outside the saved review may be submitted");
497
- for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
497
+ for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "nonce", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
498
498
  if (retainedReferences.length) {
499
499
  const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
500
500
  assertRetainedExampleReceipts({ progress, references, artifacts });
@@ -46,7 +46,7 @@ async function send(transaction) {
46
46
  const gasPrice = transaction.gasPrice ?? BigInt(await client.request({ method: "eth_gasPrice" }));
47
47
  const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
48
48
  const ceiling = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
49
- const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) };
49
+ const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) };
50
50
  const estimated = transaction.gas ?? ((BigInt(await client.request({ method: "eth_estimateGas", params: [{ ...wire, gas: toHex(ceiling) }] })) * BigInt(10000 + Number(policy.headroomBps)) + 9999n) / 10000n);
51
51
  assert.ok(estimated <= ceiling, "Unsigned helper must still fit explicit local execution admission");
52
52
  const transactionHash = await client.request({ method: "eth_sendTransaction", params: [{ ...wire, gas: toHex(estimated) }] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@black-market/sdk",
3
- "version": "0.12.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, 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, 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;
@@ -144,7 +146,7 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
144
146
  (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
145
147
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
146
148
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
147
- return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
149
+ return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
148
150
  }
149
151
 
150
152
  function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
@@ -154,7 +156,7 @@ function attachEstimates(transactions: readonly LifecycleTransaction[], simulati
154
156
  admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
155
157
  });
156
158
  }
157
- type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
159
+ type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value" | "nonce"> & NextTransactionEnvelope;
158
160
  const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
159
161
 
160
162
  function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
@@ -167,7 +169,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
167
169
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
168
170
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
169
171
  typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
170
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n) {
172
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
173
+ transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
171
174
  throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
172
175
  }
173
176
  return transaction as ReviewedNextTransaction;
@@ -176,7 +179,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
176
179
  function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
177
180
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
178
181
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
179
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
182
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
183
+ (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
180
184
  }
181
185
 
182
186
  function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
@@ -273,55 +277,68 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
273
277
  export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
274
278
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
275
279
  if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
280
+ const fixed = options.fixedFinalEnvelope;
281
+ if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
282
+ fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
283
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
284
+ }
285
+ const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
276
286
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
277
287
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
278
- if (!needsMining && !needsProtection) return planLaunch(options);
288
+ if (!needsMining && !needsProtection && fixed === undefined) return planLaunch(options);
279
289
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
280
290
  const assertDraft = () => {
281
291
  assertPlanningActive(options.signal);
282
292
  if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
283
293
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
294
+ if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
295
+ fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
284
296
  };
285
- return withLifecycleReadClient(options.client, async (client) => {
286
- const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
297
+ const result = await withLifecycleReadClient(options.client, async (client) => {
298
+ const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
299
+ const chainId = await readLifecycleChainId(client);
287
300
  if (chainId !== draft.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
288
301
  const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
289
302
  ? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
290
303
  assertDraft();
291
304
  const markets = [...draft.markets];
292
- const minedDeployments: LifecyclePoolBoundHookDeployment[] = [];
293
- for (const [marketIndex, deployment] of metadata.entries()) {
294
- 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) {
295
310
  assertDraft();
296
- const mined = await minePoolBoundHookSalt({
297
- deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
298
- onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
299
- });
311
+ const marketIndex = mined.marketIndex;
300
312
  const market = markets[marketIndex]!;
301
313
  if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
302
314
  markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
303
- minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
304
315
  }
305
316
  assertDraft();
306
- // Mining is local construction, not a state admission. Economic observations
307
- // start at a fresh post-mining snapshot and share it through final protection.
308
- const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
317
+ const executionBlock = block;
309
318
  assertDraft();
319
+ const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
320
+ const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
321
+ const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
322
+ gas: heldEnvelope.gas,
323
+ gasPrice: heldEnvelope.gasPrice ?? (price! > baseFeeEnvelope ? price! : baseFeeEnvelope),
324
+ });
310
325
  let plan: LaunchPlanV1 = { ...draft, markets };
311
326
  let ownedProgress: CanonicalLaunchProgress | undefined;
327
+ let sizing: GasSizingHints | undefined;
312
328
  if (needsProtection) {
313
329
  // A diagnostic minimum is never returned as an admitted or executable plan.
314
330
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
315
331
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
316
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
332
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
317
333
  assertDraft();
318
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");
319
335
  plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
320
336
  // This is the real SDK-owned None observation, not an assumed new-launch
321
337
  // state. No onchain commitment existed at this exact snapshot to replace.
322
338
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
339
+ sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed!]));
323
340
  }
324
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
341
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
325
342
  assertDraft();
326
343
  assertFinalPlan(planned, options.signal);
327
344
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -333,23 +350,28 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
333
350
  if (needsProtection && !planned.simulation.admitted) throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
334
351
  return planned;
335
352
  });
353
+ assertDraft();
354
+ assertFinalPlan(result, options.signal);
355
+ return result;
336
356
  }
337
357
 
338
358
  /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
339
359
  export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
340
- return withLifecycleReadClient(options.client, async (client) => {
360
+ const result = await withLifecycleReadClient(options.client, async (client) => {
341
361
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
342
362
  assertFinalPlan(planned, options.signal);
343
363
  return planned;
344
364
  });
365
+ assertFinalPlan(result, options.signal);
366
+ return result;
345
367
  }
346
368
 
347
- async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction): Promise<PlannedLaunch> {
369
+ async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, fixedFinalEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<PlannedLaunch> {
348
370
  const { plan, account, mode, ufg } = options;
349
371
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
350
372
  const launchId = hashLaunchIdentity(plan);
351
373
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
352
- pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
374
+ pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
353
375
  ]));
354
376
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
355
377
  // Only preparation/build-next can supply freshly observed progress from this
@@ -366,7 +388,10 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
366
388
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
367
389
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
368
390
  lifecycleStage(client, "plan.progress", async () => {
369
- const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
391
+ const [predictedToken] = await Promise.all([
392
+ readLifecycleTokenAtBlock(client, plan, block),
393
+ ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
394
+ ]);
370
395
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
371
396
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
372
397
  if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
@@ -389,11 +414,11 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
389
414
  hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
390
415
  }
391
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" };
392
- await assertLifecycleBlock(client, block, plan.chainId);
417
+
393
418
  return { ...planned, hookDeployments, simulation: waiting };
394
419
  }
395
420
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
396
- await assertLifecycleBlock(client, block, plan.chainId);
421
+
397
422
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
398
423
  }
399
424
  // Topology is a creation policy, not a retroactive obstacle to recovery of
@@ -408,39 +433,30 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
408
433
  const prepared = progress.canonical.preparedMarkets;
409
434
  const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
410
435
  let batches: number[] = [];
411
- if (mode === "staged") batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
412
- : [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
413
- if (reviewed !== undefined && reviewed.kind === "prepare") {
414
- // Canonical progress fixes the start; the exact held calldata fixes its span.
415
- // Never rediscover a batch or enlarge a reviewed transaction.
416
- const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
417
- const start = Number(match?.[1]), count = Number(match?.[2]);
418
- 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");
419
- batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
420
- }
436
+ if (mode === "staged") batches = packPreparationBatches(plan, prepared, computeCap);
421
437
  let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
422
- if (reviewed !== undefined) {
423
- if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
424
- transactions = transactions.slice(0, 1);
438
+ if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0]!.kind !== "atomic")) {
439
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
425
440
  }
426
441
  if (metadata.reasons.length > 0) {
427
- await assertLifecycleBlock(client, block, plan.chainId);
442
+
428
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) };
429
444
  }
430
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
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);
431
447
  try {
432
- const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, reviewed);
448
+ const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, envelope);
433
449
  let simulation = await execute(transactions);
434
450
  assertFinalPlan(planned, options.signal);
435
451
  if (mode === "staged") {
436
- // Only a measurement that consumed almost its entire legal envelope
437
- // invalidates packing estimates, including gas hidden by CREATE wrappers.
438
- 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:")) {
439
455
  const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
440
456
  const failedTransaction = transactions[failedIndex];
441
457
  const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
442
458
  const count = failedTransaction?.marketCount ?? 0;
443
- 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;
444
460
  let first = 0;
445
461
  const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
446
462
  const left = Math.ceil(count / 2);
@@ -453,9 +469,6 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
453
469
  } else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
454
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` };
455
471
  }
456
- if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
457
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
458
- }
459
472
  return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
460
473
  } finally { preparedSimulation.close(); }
461
474
  }
@@ -475,21 +488,15 @@ async function preflightNextTransaction(options: BuildNextTransactionOptions, ne
475
488
  if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
476
489
  const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
477
490
  from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
491
+ ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
478
492
  }, "latest"]), "submission gas estimate");
479
493
  if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
480
- if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
481
494
  assertPlanningActive(options.signal);
482
495
  } catch (failure) {
483
496
  if (failure instanceof Error && failure.name === "AbortError") throw failure;
484
497
  const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
485
498
  throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
486
499
  }
487
- const [canonical, chainId] = await Promise.all([
488
- readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
489
- lifecycleRpc(options.client, "eth_chainId"),
490
- ]);
491
- if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
492
- if (rpcQuantity(chainId, "chain ID") !== simulation.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
493
500
  if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
494
501
  assertPlanningActive(options.signal);
495
502
  assertReviewedTransactionUnchanged(options, reviewed, next);
@@ -503,15 +510,23 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
503
510
  // Snapshot only the reviewed wire fields; caller proofs and commands are never
504
511
  // reused, and final checks compare the mutable input with this one local copy.
505
512
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
506
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
507
- return withLifecycleReadClient(options.client, async (client) => {
508
- const block = await readLifecycleBlock(client);
509
- const progress = await readLaunchProgress({ ...options, client }, block);
513
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
514
+ const result = await withLifecycleReadClient(options.client, async (client) => {
515
+ const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
516
+ const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
517
+ if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
518
+ await Promise.all([
519
+ readLifecycleTokenAtBlock(client, options.planned.plan, block),
520
+ readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
521
+ resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
522
+ ]);
523
+ }
524
+ const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
510
525
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
511
526
  // Only the confirmed canonical terminal phase is terminal; a head-only
512
527
  // Active observation is not, and the confirmation/nonce/receipt gates below
513
528
  // remain in force for every nonterminal case.
514
- await assertLifecycleBlock(client, block, options.planned.chainId);
529
+
515
530
  return undefined;
516
531
  }
517
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");
@@ -523,6 +538,7 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
523
538
  assertFinalPlan(planned, options.signal);
524
539
  const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
525
540
  : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
541
+ transaction.nonce = progress.pendingAccountNonce;
526
542
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
527
543
  const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
528
544
  if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
@@ -530,15 +546,48 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
530
546
  if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
531
547
  return preflightNextTransaction(options, next, simulation, reviewed);
532
548
  }
533
- 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,
534
- ufg: options.planned.ufg, allowTopologyGaps: options.planned.allowTopologyGaps, allowMissingPermanentTail: options.planned.allowMissingPermanentTail }, client, block, progress, false, reviewed));
535
- assertFinalPlan(refreshed, options.signal);
536
- if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
537
- assertPlannedDeploymentBindings(options.planned, refreshed);
538
- const next = refreshed.transactions[0];
539
- if (next === undefined) return undefined;
540
- 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);
541
587
  });
588
+ assertFinalPlan(options.planned, options.signal);
589
+ if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
590
+ return result;
542
591
  }
543
592
 
544
593
  /** Re-simulates only canonical unfinished work with the same submitted receipt