@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.
- package/README.md +96 -96
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/lens.d.ts +409 -0
- package/dist/lifecycle/lens.d.ts.map +1 -0
- package/dist/lifecycle/lens.js +30 -0
- package/dist/lifecycle/lens.js.map +1 -0
- package/dist/lifecycle/mining.d.ts +47 -0
- package/dist/lifecycle/mining.d.ts.map +1 -0
- package/dist/lifecycle/mining.js +89 -0
- package/dist/lifecycle/mining.js.map +1 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +121 -78
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +1 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +51 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +149 -73
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +14 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +171 -12
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +5 -3
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +137 -115
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts +8 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.js +79 -0
- package/dist/lifecycle/snapshot.js.map +1 -0
- package/dist/lifecycle/types.d.ts +27 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +627 -1495
- package/docs/reference.md +365 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/package.json +1 -1
- package/src/lifecycle/index.ts +5 -1
- package/src/lifecycle/lens.ts +42 -0
- package/src/lifecycle/mining.ts +80 -0
- package/src/lifecycle/planner.ts +122 -73
- package/src/lifecycle/presets.ts +4 -0
- package/src/lifecycle/progress.ts +132 -59
- package/src/lifecycle/rpc.ts +163 -13
- package/src/lifecycle/simulation.ts +134 -106
- package/src/lifecycle/snapshot.ts +72 -0
- 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
|
|
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
package/src/lifecycle/index.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/lifecycle/planner.ts
CHANGED
|
@@ -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,
|
|
8
|
-
import {
|
|
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
|
-
|
|
286
|
-
const
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
417
|
+
|
|
393
418
|
return { ...planned, hookDeployments, simulation: waiting };
|
|
394
419
|
}
|
|
395
420
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
396
|
-
|
|
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 =
|
|
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 (
|
|
423
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
437
|
-
//
|
|
438
|
-
while (
|
|
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?.
|
|
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
|
-
|
|
508
|
-
const block = await
|
|
509
|
-
const
|
|
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
|
-
|
|
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
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
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
|