@black-market/sdk 0.6.4 → 0.7.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 +28 -4
- package/dist/addresses.d.ts +1 -1
- package/dist/addresses.js +4 -4
- package/dist/launch.d.ts +61 -0
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +171 -6
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +1999 -253
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +43 -14
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/allocations.d.ts +11 -0
- package/dist/lifecycle/allocations.d.ts.map +1 -0
- package/dist/lifecycle/allocations.js +36 -0
- package/dist/lifecycle/allocations.js.map +1 -0
- package/dist/lifecycle/calibration.d.ts +19 -0
- package/dist/lifecycle/calibration.d.ts.map +1 -0
- package/dist/lifecycle/calibration.js +65 -0
- package/dist/lifecycle/calibration.js.map +1 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts +11 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts.map +1 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js +10 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js.map +1 -0
- package/dist/lifecycle/index.d.ts +4 -2
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -2
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +170 -6
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +164 -37
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/planner.d.ts +7 -14
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +354 -400
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +62 -0
- package/dist/lifecycle/presets.d.ts.map +1 -0
- package/dist/lifecycle/presets.js +195 -0
- package/dist/lifecycle/presets.js.map +1 -0
- package/dist/lifecycle/progress.d.ts +5 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +212 -53
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +13 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +92 -8
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +14 -4
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +383 -116
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +6 -0
- package/dist/lifecycle/transactions.d.ts.map +1 -0
- package/dist/lifecycle/transactions.js +40 -0
- package/dist/lifecycle/transactions.js.map +1 -0
- package/dist/lifecycle/types.d.ts +39 -2
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +228 -54
- package/examples/README.md +108 -18
- package/examples/generate-lifecycle-commitment-fixture.mjs +3 -1
- package/examples/launch-example-support.mjs +1 -1
- package/examples/launch-example.mjs +73 -26
- package/examples/launch-lifecycle-smoke.mjs +13 -11
- package/examples/launch.mjs +12 -11
- package/examples/quickstart.mjs +1 -1
- package/package.json +1 -1
- package/src/addresses.ts +4 -4
- package/src/launch.ts +213 -6
- package/src/lifecycle/abi.ts +43 -14
- package/src/lifecycle/allocations.ts +35 -0
- package/src/lifecycle/calibration.ts +59 -0
- package/src/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.ts +11 -0
- package/src/lifecycle/index.ts +4 -2
- package/src/lifecycle/markets.ts +169 -35
- package/src/lifecycle/planner.ts +326 -330
- package/src/lifecycle/presets.ts +243 -0
- package/src/lifecycle/progress.ts +193 -50
- package/src/lifecycle/rpc.ts +97 -9
- package/src/lifecycle/simulation.ts +370 -109
- package/src/lifecycle/transactions.ts +34 -0
- package/src/lifecycle/types.ts +35 -3
package/src/lifecycle/planner.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi
|
|
3
|
-
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind,
|
|
4
|
-
import {
|
|
5
|
-
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig,
|
|
6
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc,
|
|
7
|
-
import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
|
|
8
|
-
import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type
|
|
1
|
+
import { encodeFunctionData, keccak256, toHex, zeroAddress, type Address, type Hex } from "viem";
|
|
2
|
+
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
+
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1 } from "./schema.js";
|
|
4
|
+
import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
5
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
6
|
+
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
7
|
+
import { prepareLaunchSimulation, simulateLaunchTransactions, type NextTransactionEnvelope } from "./simulation.js";
|
|
8
|
+
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";
|
|
9
|
+
import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
|
|
10
|
+
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
11
|
+
import { buildLaunchTransactions } from "./transactions.js";
|
|
12
|
+
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
9
13
|
|
|
10
|
-
function validatePlanShape(plan: LaunchPlanV1):
|
|
14
|
+
function validatePlanShape(plan: LaunchPlanV1): Hex {
|
|
11
15
|
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
12
|
-
hashLaunchPlan(plan);
|
|
16
|
+
const commitment = hashLaunchPlan(plan);
|
|
13
17
|
if (plan.markets.length === 0 || plan.markets.length > 16 || plan.buys.length > 64 || plan.funding.length > 8 || plan.feeAssets.length === 0 || plan.feeAssets.length > 8 || plan.token.supply <= 0n || plan.token.inventoryRecipient === zeroAddress || plan.token.inventoryRecipient.toLowerCase() === plan.orchestrator.toLowerCase() || plan.executorFeeBps > 1000) throw new LifecyclePlanningError("INVALID_PLAN", "Plan exceeds lifecycle bounds or lacks positive token economics");
|
|
14
18
|
if ((plan.token.kind === LifecycleTokenKind.ERC404 && plan.token.supply > LIFECYCLE_MAX_ERC404_SUPPLY) || (plan.token.kind === LifecycleTokenKind.ERC20 && plan.token.rewardMode !== LifecycleRewardMode.None && plan.token.supply > LIFECYCLE_MAX_REWARD_ERC20_SUPPLY)) throw new LifecyclePlanningError("INVALID_TOKEN_SUPPLY", "Reward-enabled ERC20 supply must not exceed 10^77; ERC404 supply must fit uint96; reward-free ERC20 retains uint256");
|
|
15
19
|
const encoder = new TextEncoder();
|
|
@@ -27,7 +31,6 @@ function validatePlanShape(plan: LaunchPlanV1): void {
|
|
|
27
31
|
if (funding.kind === LifecycleFundingKind.Swap && (funding.inputAsset.toLowerCase() === funding.asset.toLowerCase() || funding.data === "0x" || (funding.data.length - 2) / 2 > 16384)) throw new LifecyclePlanningError("INVALID_FUNDING", "Conversion funding must change assets and commit bounded nonempty calldata");
|
|
28
32
|
previous = BigInt(funding.asset);
|
|
29
33
|
}
|
|
30
|
-
if (plan.markets.reduce((sum, market) => sum + market.tokenBudget, 0n) > plan.token.supply) throw new LifecyclePlanningError("INVALID_TOKEN_BUDGET", "Market token budgets exceed committed supply");
|
|
31
34
|
for (const buy of plan.buys) {
|
|
32
35
|
if (buy.marketIndex >= plan.markets.length || buy.quoteAmountIn <= 0n || buy.minTokenOut <= 0n || buy.recipient === zeroAddress || buy.recipient.toLowerCase() === plan.orchestrator.toLowerCase()) throw new LifecyclePlanningError("INVALID_BUY", "Every ordered buy requires a committed market, positive budget/minimum, and external recipient");
|
|
33
36
|
}
|
|
@@ -36,214 +39,143 @@ function validatePlanShape(plan: LaunchPlanV1): void {
|
|
|
36
39
|
for (const market of plan.markets) {
|
|
37
40
|
if (market.tokenBudget <= 0n || market.config === "0x" || (market.config.length - 2) / 2 > 16384) throw new LifecyclePlanningError("INVALID_MARKET", "Each market needs a positive token budget and bounded versioned config");
|
|
38
41
|
let config: V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig | undefined;
|
|
39
|
-
if (market.configVersion
|
|
42
|
+
if (market.configVersion !== 1 && market.configVersion !== 4 && !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Encode reviewed SharedV4 version 4 or pool-bound version 5/6 only on its exact matching deployment");
|
|
40
43
|
if (market.configVersion === 4) config = decodeV4LifecycleMarketConfig(market.config);
|
|
41
|
-
else if (market.configVersion
|
|
44
|
+
else if (isPoolBoundV4ConfigVersion(market.configVersion)) config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
|
|
42
45
|
if (config !== undefined) {
|
|
43
46
|
if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("PROFILE_TERMS_MISMATCH", "Inner config profile differs from the committed market profile");
|
|
44
47
|
const quote = market.quoteAsset.toLowerCase();
|
|
45
48
|
if (v4Quotes.has(quote)) throw new LifecyclePlanningError("DUPLICATE_V4_QUOTE", "Only one V4 market per quote asset is allowed across shared and pool-bound offerings; put multiple positions in that market");
|
|
46
49
|
v4Quotes.add(quote);
|
|
47
|
-
if (config.
|
|
48
|
-
positionCount += config.positions
|
|
50
|
+
if (config.lpFeePips >= 1000000 || (config.version === 6 ? config.hookFeePips > 1000000 : config.hookFeePips >= 1000000) || config.feeMode > 1 || config.tickSpacing < 1 || config.tickSpacing > 32767 || config.sqrtPriceX96 < 4295128739n || config.sqrtPriceX96 >= 1461446703485210103287273052203988822378723970342n || (config.protocolFeeDenominator !== 0 && (config.protocolFeeDenominator < 4 || config.protocolFeeDenominator > 10 || config.treasury === zeroAddress))) throw new LifecyclePlanningError("INVALID_MARKET", "V4 market violates lifecycle fee, price, treasury or position bounds");
|
|
51
|
+
positionCount += assertLifecycleMarketAllocation(market, config.positions);
|
|
49
52
|
}
|
|
53
|
+
else positionCount += assertLifecycleMarketAllocation(market, decodeAbyssLifecycleMarketConfig(market.config).positions);
|
|
50
54
|
if (positionCount > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 total positions across every venue");
|
|
51
55
|
}
|
|
56
|
+
assertLifecycleSupplyAllocation(plan);
|
|
52
57
|
for (const market of plan.markets) {
|
|
53
58
|
const required = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === market.quoteAsset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
|
|
54
59
|
const funding = plan.funding.find((item) => item.asset.toLowerCase() === market.quoteAsset.toLowerCase());
|
|
55
60
|
if (required > (funding?.amount ?? 0n)) throw new LifecyclePlanningError("INVALID_FUNDING", "Quote funding does not cover all ordered buys for its asset");
|
|
56
61
|
}
|
|
62
|
+
return commitment;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
|
|
73
|
-
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
|
|
74
|
-
]),
|
|
75
|
-
funding.kind === LifecycleFundingKind.NativeWrap ? readLifecycleContract<Address>(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block) : undefined,
|
|
76
|
-
funding.kind === LifecycleFundingKind.Swap ? Promise.all([
|
|
77
|
-
readLifecycleContract<readonly [Address, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block),
|
|
78
|
-
lifecyclePinnedRpc(client, "eth_getCode", [funding.target, toHex(block.number)], block),
|
|
79
|
-
]) : undefined,
|
|
65
|
+
async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[] }> {
|
|
66
|
+
if (plan.funding.length === 0) return { prerequisites: [], approvals: [] };
|
|
67
|
+
const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
68
|
+
const escrow = preset?.fundingEscrow ?? await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
|
|
69
|
+
// Only allowance observations construct approval commands. Core execution
|
|
70
|
+
// enforces input/output asset, funding target, balance and conversion predicates.
|
|
71
|
+
const prerequisites = await Promise.all(plan.funding.map(async (funding): Promise<LifecycleFundingPrerequisite> => {
|
|
72
|
+
const native = funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress;
|
|
73
|
+
const [inputBalance, allowance] = native
|
|
74
|
+
? [rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block), "creator native balance"), undefined] as const
|
|
75
|
+
: await Promise.all([
|
|
76
|
+
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
|
|
77
|
+
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
|
|
80
78
|
]);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
]);
|
|
92
|
-
if (authority.toLowerCase() !== plan.orchestrator.toLowerCase()) throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
|
|
93
|
-
const totals = new Map<string, { asset: Address; amount: bigint; balance: bigint; allowance: bigint }>();
|
|
94
|
-
const prerequisites: LifecycleFundingPrerequisite[] = [];
|
|
95
|
-
const approvals: LifecycleTransaction[] = [];
|
|
96
|
-
const reasons: string[] = [];
|
|
97
|
-
let nativeValue = 0n;
|
|
98
|
-
for (const { prerequisite, reason } of observations) {
|
|
99
|
-
prerequisites.push(prerequisite);
|
|
100
|
-
if (reason !== undefined) reasons.push(reason);
|
|
101
|
-
nativeValue += prerequisite.nativeValue;
|
|
102
|
-
if (prerequisite.allowance !== undefined) {
|
|
103
|
-
const { funding } = prerequisite;
|
|
104
|
-
const key = funding.inputAsset.toLowerCase();
|
|
105
|
-
const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, balance: prerequisite.inputBalance, allowance: prerequisite.allowance };
|
|
106
|
-
total.amount += funding.inputAmount;
|
|
107
|
-
totals.set(key, total);
|
|
108
|
-
}
|
|
79
|
+
return { funding, inputBalance, requiredInput: funding.inputAmount, allowance, spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" };
|
|
80
|
+
}));
|
|
81
|
+
const totals = new Map<string, { asset: Address; amount: bigint; allowance: bigint }>();
|
|
82
|
+
for (const prerequisite of prerequisites) {
|
|
83
|
+
if (prerequisite.allowance === undefined) continue;
|
|
84
|
+
const { funding } = prerequisite;
|
|
85
|
+
const key = funding.inputAsset.toLowerCase();
|
|
86
|
+
const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, allowance: prerequisite.allowance };
|
|
87
|
+
total.amount += funding.inputAmount;
|
|
88
|
+
totals.set(key, total);
|
|
109
89
|
}
|
|
110
|
-
|
|
90
|
+
const approvals: LifecycleTransaction[] = [];
|
|
111
91
|
for (const total of totals.values()) {
|
|
112
|
-
if (total.balance < total.amount) reasons.push(`Creator balance is below combined funding requirements for ${total.asset}`);
|
|
113
92
|
if (total.allowance >= total.amount) continue;
|
|
114
93
|
if (total.allowance > 0n) approvals.push({ id: `approve-reset:${total.asset}`, kind: "approve-reset", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, 0n] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: 0n, exact: 0n }] });
|
|
115
94
|
approvals.push({ id: `approve:${total.asset}`, kind: "approve", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, total.amount] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: total.amount }] });
|
|
116
95
|
}
|
|
117
|
-
return { prerequisites, approvals
|
|
96
|
+
return { prerequisites, approvals };
|
|
118
97
|
}
|
|
119
98
|
|
|
120
|
-
async function
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
|
|
131
|
-
registryRead.then((registry) => Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
|
|
132
|
-
? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true))),
|
|
133
|
-
progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
|
|
134
|
-
]);
|
|
135
|
-
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
136
|
-
if (block.timestamp > plan.deadline) reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
137
|
-
for (const policy of plan.feeAssets) if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase()) reasons.push("Only the launch token may have a burn fee share");
|
|
138
|
-
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("The launch token must have a committed fee policy");
|
|
139
|
-
for (const { asset, code } of assetCodes) if (code === "0x") reasons.push(`Asset ${asset} has no contract code`);
|
|
140
|
-
const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
|
|
141
|
-
const markets = await Promise.all(plan.markets.map(async (market, index) => {
|
|
142
|
-
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
143
|
-
let positions = 0;
|
|
144
|
-
let deployment: LifecyclePoolBoundHookDeployment | undefined;
|
|
145
|
-
let identity: MarketIdentityV1 | undefined;
|
|
146
|
-
let reason: string | undefined;
|
|
147
|
-
if (profile === undefined || !profile.admitted) return { positions, deployment, identity, reason };
|
|
148
|
-
try {
|
|
149
|
-
if (market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() || market.configVersion !== profile.topology.configVersion) throw new LifecyclePlanningError("INVALID_MARKET", "Market adapter or version differs from exact certified profile");
|
|
150
|
-
if (market.quoteAsset.toLowerCase() === planned.predictedToken.toLowerCase() || !plan.feeAssets.some((policy) => policy.asset.toLowerCase() === market.quoteAsset.toLowerCase())) throw new LifecyclePlanningError("INVALID_MARKET", "Each distinct quote requires its committed fee policy");
|
|
151
|
-
const schema = profile.registration.configSchema.toLowerCase();
|
|
152
|
-
const config = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeV4LifecycleMarketConfig(market.config) : schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodePoolBoundV4LifecycleMarketConfig(market.config) : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeAbyssLifecycleMarketConfig(market.config) : undefined;
|
|
153
|
-
if (config === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
|
|
154
|
-
if ("developerFeeBps" in config) {
|
|
155
|
-
validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
|
|
156
|
-
}
|
|
157
|
-
if (config.positions.length === 0 || config.positions.length > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
158
|
-
positions = config.positions.length;
|
|
159
|
-
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
160
|
-
if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
|
|
161
|
-
const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
162
|
-
const [adapter, boundDeployment, resolvedIdentity] = await Promise.all([
|
|
163
|
-
readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
|
|
164
|
-
profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
|
|
165
|
-
readLifecycleContract<MarketIdentityV1>(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block),
|
|
166
|
-
"developerFeeBps" in config && config.version === 4 ? validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block }) : undefined,
|
|
167
|
-
]);
|
|
168
|
-
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
169
|
-
if (boundDeployment !== undefined) {
|
|
170
|
-
if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
171
|
-
deployment = { marketIndex: index, ...boundDeployment };
|
|
172
|
-
}
|
|
173
|
-
identity = resolvedIdentity;
|
|
174
|
-
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
175
|
-
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
176
|
-
if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0))) throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
|
|
177
|
-
const prepared = progress.markets.find((item) => item.index === index);
|
|
178
|
-
if (prepared !== undefined) {
|
|
179
|
-
const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
|
|
180
|
-
const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
|
|
181
|
-
if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96) throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
|
|
182
|
-
await readLifecycleContract<undefined>(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
|
|
183
|
-
}
|
|
184
|
-
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
185
|
-
return { positions, deployment, identity, reason };
|
|
186
|
-
}));
|
|
187
|
-
for (const [index, funding] of plan.funding.entries()) {
|
|
188
|
-
if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
|
|
189
|
-
if (!allowedInputs[index]) reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
|
|
190
|
-
}
|
|
191
|
-
const hookDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
192
|
-
const identities = new Set<string>();
|
|
193
|
-
const v4Quotes = new Set<string>();
|
|
194
|
-
let positionCount = 0;
|
|
195
|
-
for (const [index, result] of markets.entries()) {
|
|
196
|
-
if (positionCount + result.positions > 32) { reasons.push("A launch commits at most 32 positive positions across every market/venue"); continue; }
|
|
197
|
-
positionCount += result.positions;
|
|
198
|
-
if (result.deployment !== undefined) hookDeployments.push(result.deployment);
|
|
199
|
-
if (result.reason !== undefined) { reasons.push(result.reason); continue; }
|
|
200
|
-
if (result.identity === undefined) continue;
|
|
201
|
-
if (result.identity.venue === LifecycleVenue.UniswapV4) {
|
|
202
|
-
const quote = plan.markets[index]!.quoteAsset.toLowerCase();
|
|
203
|
-
if (v4Quotes.has(quote)) { reasons.push("Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt"); continue; }
|
|
204
|
-
v4Quotes.add(quote);
|
|
99
|
+
async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1, token: Address, block: LifecycleBlock): Promise<{ profiles: (LifecycleProfile | LifecycleConstructionProfile)[]; hookDeployments: LifecyclePoolBoundHookDeployment[]; reasons: string[] }> {
|
|
100
|
+
const ids = [...new Set(plan.markets.map((market) => market.profileId.toLowerCase() as Hex))];
|
|
101
|
+
const known = ids.map((profileId) => getKnownLifecycleProfile({ chainId: plan.chainId, orchestrator: plan.orchestrator, profileId }));
|
|
102
|
+
const unknownIds = ids.filter((_id, index) => known[index] === undefined);
|
|
103
|
+
const live = unknownIds.length === 0 ? [] : await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: unknownIds }, block);
|
|
104
|
+
const profiles = ids.map((id, index): LifecycleProfile | LifecycleConstructionProfile => {
|
|
105
|
+
const preset = known[index];
|
|
106
|
+
if (preset !== undefined) {
|
|
107
|
+
if (!preset.supported) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", preset.unavailableReason ?? "Historical preset does not support the current launch ABI");
|
|
108
|
+
return { ...preset.profile, metadataSource: "preset" };
|
|
205
109
|
}
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
return { profiles, hookDeployments, reasons };
|
|
110
|
+
const profile = live.find((candidate) => candidate.id.toLowerCase() === id);
|
|
111
|
+
if (profile === undefined) throw new LifecyclePlanningError("PROFILE_NOT_FOUND", "Unlisted profile discovery omitted the committed profile");
|
|
112
|
+
return profile;
|
|
113
|
+
});
|
|
114
|
+
const hookDeployments = (await Promise.all(plan.markets.map(async (market, marketIndex): Promise<LifecyclePoolBoundHookDeployment | undefined> => {
|
|
115
|
+
if (!isPoolBoundV4ConfigVersion(market.configVersion)) return undefined;
|
|
116
|
+
const known = deriveKnownPoolBoundHookDeployment({ plan, token, marketIndex });
|
|
117
|
+
return { marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) };
|
|
118
|
+
}))).filter((deployment): deployment is LifecyclePoolBoundHookDeployment => deployment !== undefined);
|
|
119
|
+
return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
|
|
216
120
|
}
|
|
217
121
|
|
|
218
|
-
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[],
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (mode === "staged") {
|
|
231
|
-
for (let first = progress.canonical.preparedMarkets; first < plan.markets.length; first += batchSize) {
|
|
232
|
-
const count = Math.min(batchSize, plan.markets.length - first);
|
|
233
|
-
const completed = first + count;
|
|
234
|
-
transactions.push({ ...base, id: `prepare:${first}:${count}`, kind: "prepare", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "prepareMarkets", args: [plan, first, count] }), value: 0n, marketStart: first, marketCount: count, postconditions: [{ kind: "launch", phase: completed === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: completed }] });
|
|
235
|
-
}
|
|
236
|
-
transactions.push({ ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "activateLaunch", args: [plan] }), value: 0n, postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
|
|
237
|
-
}
|
|
238
|
-
return transactions.map((transaction, index) => ({ ...transaction, dependencies: index === 0 ? [] : [transactions[index - 1]?.id ?? ""] }));
|
|
122
|
+
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batchSize: number): LifecycleTransaction[] {
|
|
123
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
|
|
124
|
+
const prepared = progress.canonical.preparedMarkets;
|
|
125
|
+
const batches: number[] = prepared > 0 ? [prepared] : [];
|
|
126
|
+
for (let first = prepared; first < plan.markets.length; first += batchSize) batches.push(Math.min(batchSize, plan.markets.length - first));
|
|
127
|
+
const commands = buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined }).filter((transaction) =>
|
|
128
|
+
(transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
129
|
+
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
130
|
+
const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
|
|
131
|
+
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
239
132
|
}
|
|
240
133
|
|
|
241
134
|
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
242
135
|
return transactions.map((transaction) => {
|
|
243
136
|
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
|
|
244
|
-
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { 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 } };
|
|
137
|
+
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { 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 } };
|
|
245
138
|
});
|
|
246
139
|
}
|
|
140
|
+
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
|
|
141
|
+
const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
|
|
142
|
+
|
|
143
|
+
function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
|
|
144
|
+
if (transaction === undefined) return undefined;
|
|
145
|
+
if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
|
|
146
|
+
!reviewedTransactionKinds.includes(transaction.kind) ||
|
|
147
|
+
!Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
|
|
148
|
+
typeof transaction.from !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.from) ||
|
|
149
|
+
typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
|
|
150
|
+
typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
|
|
151
|
+
typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
|
|
152
|
+
typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
153
|
+
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n) {
|
|
154
|
+
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");
|
|
155
|
+
}
|
|
156
|
+
return transaction as ReviewedNextTransaction;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
|
|
160
|
+
return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
|
|
161
|
+
next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
|
|
162
|
+
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
|
|
166
|
+
if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
|
|
167
|
+
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
|
|
168
|
+
}
|
|
169
|
+
if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
|
|
170
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
|
|
171
|
+
}
|
|
172
|
+
const current = readReviewedNextTransaction(options.reviewedTransaction);
|
|
173
|
+
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
|
|
174
|
+
current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || !matchesReviewedNextTransaction(next, reviewed) ||
|
|
175
|
+
next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
|
|
176
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
247
179
|
|
|
248
180
|
async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator: Address, block: LifecycleBlock): Promise<Pick<PlannedLaunch, "tokenFactory" | "tokenFactoryCodeHash">> {
|
|
249
181
|
const tokenFactory = await readLifecycleContract<Address>(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
|
|
@@ -266,163 +198,209 @@ function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: Planned
|
|
|
266
198
|
})) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
|
|
267
199
|
}
|
|
268
200
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
client: LifecycleRpcClient; plan: LaunchPlanV1; signal?: AbortSignal;
|
|
272
|
-
onProgress?: (progress: PoolBoundLifecyclePreparationProgress) => void;
|
|
273
|
-
}): Promise<{ plan: LaunchPlanV1; deployments: readonly LifecyclePoolBoundHookDeployment[] }> {
|
|
274
|
-
const { client, signal } = options;
|
|
275
|
-
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
276
|
-
validatePlanShape(options.plan);
|
|
277
|
-
const commitment = hashLaunchPlan(options.plan);
|
|
278
|
-
let plan = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
279
|
-
const initialBlock = await readLifecycleBlock(client);
|
|
280
|
-
if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
|
|
281
|
-
const initialFactory = await readTokenFactoryBinding(client, plan.orchestrator, initialBlock);
|
|
282
|
-
const initialToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], initialBlock);
|
|
283
|
-
const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, initialBlock);
|
|
284
|
-
const deployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
285
|
-
for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
|
|
286
|
-
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
287
|
-
const market = plan.markets[marketIndex];
|
|
288
|
-
if (market === undefined) throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
|
|
289
|
-
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
290
|
-
if (profile === undefined || !profile.admitted) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", profile?.reason ?? "Market is not an admitted lifecycle profile");
|
|
291
|
-
if (profile.topology.hookTopology !== 2) continue;
|
|
292
|
-
const metadata = await readPoolBoundHookDeployment({ client, plan, marketIndex }, initialBlock);
|
|
293
|
-
const mined = await minePoolBoundHookSalt({
|
|
294
|
-
deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, startSalt: BigInt(metadata.salt), signal,
|
|
295
|
-
onProgress: (progress) => options.onProgress?.({ marketIndex, ...progress }),
|
|
296
|
-
});
|
|
297
|
-
plan = { ...plan, markets: plan.markets.map((item, index) => index === marketIndex ? { ...item, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(item.config), hookSalt: mined.salt }) } : item) };
|
|
298
|
-
deployments.push({ marketIndex, deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, ...mined });
|
|
299
|
-
}
|
|
300
|
-
await assertLifecycleBlock(client, initialBlock);
|
|
301
|
-
const finalBlock = await readLifecycleBlock(client);
|
|
302
|
-
const finalFactory = await readTokenFactoryBinding(client, plan.orchestrator, finalBlock);
|
|
303
|
-
if (finalFactory.tokenFactory.toLowerCase() !== initialFactory.tokenFactory.toLowerCase() || finalFactory.tokenFactoryCodeHash.toLowerCase() !== initialFactory.tokenFactoryCodeHash.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory address or runtime code changed while mining the market-bound deployment");
|
|
304
|
-
const finalToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], finalBlock);
|
|
305
|
-
if (finalToken.toLowerCase() !== initialToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Token prediction changed while mining the market-bound deployment");
|
|
306
|
-
const finalProfiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(deployments.map((deployment) => plan.markets[deployment.marketIndex]?.profileId).filter((id): id is Hex => id !== undefined))] }, finalBlock);
|
|
307
|
-
for (const deployment of deployments) {
|
|
308
|
-
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
309
|
-
const market = plan.markets[deployment.marketIndex];
|
|
310
|
-
const profile = finalProfiles.find((item) => item.id.toLowerCase() === market?.profileId.toLowerCase());
|
|
311
|
-
if (market === undefined || profile === undefined || !profile.admitted || profile.topology.hookTopology !== 2) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", profile?.reason ?? "Final bound profile is no longer admitted");
|
|
312
|
-
const fresh = await readPoolBoundHookDeployment({ client, plan, marketIndex: deployment.marketIndex }, finalBlock);
|
|
313
|
-
if (fresh.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || fresh.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() || fresh.salt.toLowerCase() !== deployment.salt.toLowerCase() || fresh.predictedHook.toLowerCase() !== deployment.predictedHook.toLowerCase() || !hasLifecycleV4HookPermissions(fresh.predictedHook) || predictPoolBoundHookAddress(fresh).toLowerCase() !== fresh.predictedHook.toLowerCase()) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Mined deployment no longer matches the exact final economic configuration and certified factory");
|
|
314
|
-
const identity = await readLifecycleContract<MarketIdentityV1>(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [hashLaunchIdentity(plan), finalToken, market], finalBlock);
|
|
315
|
-
validateLifecycleMarketIdentity(identity, plan, finalToken, deployment.marketIndex);
|
|
316
|
-
if (identity.hook.toLowerCase() !== fresh.predictedHook.toLowerCase() || identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase()) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final resolved PoolKey does not use the exact mined hook/manager");
|
|
317
|
-
}
|
|
318
|
-
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous salt mining; previous mining is invalid");
|
|
319
|
-
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
320
|
-
await assertLifecycleBlock(client, finalBlock);
|
|
321
|
-
return { plan, deployments };
|
|
201
|
+
function assertPlanningActive(signal?: AbortSignal): void {
|
|
202
|
+
if (signal?.aborted) throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
|
|
322
203
|
}
|
|
323
204
|
|
|
324
|
-
|
|
205
|
+
function validatePlanningIntent(options: PlanLaunchOptions): Hex {
|
|
206
|
+
const { plan, account, mode } = options;
|
|
207
|
+
assertPlanningActive(options.signal);
|
|
208
|
+
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
209
|
+
if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
210
|
+
if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
211
|
+
return validatePlanShape(plan);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
|
|
215
|
+
assertPlanningActive(signal);
|
|
216
|
+
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** One owner constructs and mines, optionally measures actual ordered buy outputs,
|
|
220
|
+
* then proves only the final protected commitment. No certificates or skip flags. */
|
|
221
|
+
export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
|
|
222
|
+
const commitment = validatePlanningIntent(options);
|
|
223
|
+
if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
|
|
224
|
+
const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
|
|
225
|
+
const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
|
|
226
|
+
if (!needsMining && !needsProtection) return planLaunch(options);
|
|
227
|
+
const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
228
|
+
const assertDraft = () => {
|
|
229
|
+
assertPlanningActive(options.signal);
|
|
230
|
+
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
|
|
231
|
+
};
|
|
232
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
233
|
+
const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
|
|
234
|
+
if (chainId !== draft.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
|
|
235
|
+
const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
|
|
236
|
+
? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
|
|
237
|
+
assertDraft();
|
|
238
|
+
const markets = [...draft.markets];
|
|
239
|
+
const minedDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
240
|
+
for (const [marketIndex, deployment] of metadata.entries()) {
|
|
241
|
+
if (deployment === undefined) continue;
|
|
242
|
+
assertDraft();
|
|
243
|
+
const mined = await minePoolBoundHookSalt({
|
|
244
|
+
deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
|
|
245
|
+
onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
|
|
246
|
+
});
|
|
247
|
+
const market = markets[marketIndex]!;
|
|
248
|
+
if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
|
|
249
|
+
markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
|
|
250
|
+
minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
|
|
251
|
+
}
|
|
252
|
+
assertDraft();
|
|
253
|
+
// Mining is local construction, not a state admission. Economic observations
|
|
254
|
+
// start at a fresh post-mining snapshot and share it through final protection.
|
|
255
|
+
const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
|
|
256
|
+
assertDraft();
|
|
257
|
+
let plan: LaunchPlanV1 = { ...draft, markets };
|
|
258
|
+
let ownedProgress: CanonicalLaunchProgress | undefined;
|
|
259
|
+
if (needsProtection) {
|
|
260
|
+
// A diagnostic minimum is never returned as an admitted or executable plan.
|
|
261
|
+
// Price guards, funding minima, recipients and all ordered budgets stay exact.
|
|
262
|
+
const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
|
|
263
|
+
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
|
|
264
|
+
assertDraft();
|
|
265
|
+
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");
|
|
266
|
+
plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
|
|
267
|
+
// This is the real SDK-owned None observation, not an assumed new-launch
|
|
268
|
+
// state. No onchain commitment existed at this exact snapshot to replace.
|
|
269
|
+
ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
|
|
270
|
+
}
|
|
271
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
|
|
272
|
+
assertDraft();
|
|
273
|
+
assertFinalPlan(planned, options.signal);
|
|
274
|
+
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
275
|
+
for (const mined of minedDeployments) {
|
|
276
|
+
const final = planned.hookDeployments.find((deployment) => deployment.marketIndex === mined.marketIndex);
|
|
277
|
+
if (final === undefined || final.deployer.toLowerCase() !== mined.deployer.toLowerCase() || final.initCodeHash.toLowerCase() !== mined.initCodeHash.toLowerCase() || final.salt.toLowerCase() !== mined.salt.toLowerCase() || final.predictedHook.toLowerCase() !== mined.predictedHook.toLowerCase()) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
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);
|
|
281
|
+
return planned;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
|
|
325
286
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
326
287
|
return withLifecycleReadClient(options.client, async (client) => {
|
|
327
288
|
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
328
|
-
|
|
289
|
+
assertFinalPlan(planned, options.signal);
|
|
329
290
|
return planned;
|
|
330
291
|
});
|
|
331
292
|
}
|
|
332
293
|
|
|
333
|
-
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient): Promise<PlannedLaunch> {
|
|
294
|
+
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction): Promise<PlannedLaunch> {
|
|
334
295
|
const { plan, account, mode } = options;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const planHash = hashLaunchPlan(plan); const launchId = hashLaunchIdentity(plan);
|
|
340
|
-
const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
341
|
-
readLifecycleBlock(client),
|
|
342
|
-
lifecycleRpc(client, "eth_chainId"),
|
|
296
|
+
const planHash = validatePlanningIntent(options);
|
|
297
|
+
const launchId = hashLaunchIdentity(plan);
|
|
298
|
+
const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
299
|
+
pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
|
|
343
300
|
]));
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
301
|
+
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
302
|
+
// Only preparation/build-next can supply freshly observed progress from this
|
|
303
|
+
// still-open SDK invocation. Stored/query DTOs never enter this continuation.
|
|
304
|
+
if (ownedProgress !== undefined) {
|
|
305
|
+
if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
|
|
306
|
+
ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
|
|
307
|
+
ownedProgress.confirmationDepth !== (options.confirmations ?? 1)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
|
|
308
|
+
if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
|
|
309
|
+
}
|
|
310
|
+
assertPlanningActive(options.signal);
|
|
311
|
+
const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
312
|
+
const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
|
|
313
|
+
preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
350
314
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
351
315
|
lifecycleStage(client, "plan.progress", async () => {
|
|
352
|
-
const predictedToken = await
|
|
316
|
+
const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
|
|
353
317
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
354
|
-
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
318
|
+
const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
|
|
319
|
+
if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
|
|
355
320
|
return { identity, progress };
|
|
356
321
|
}),
|
|
357
322
|
]));
|
|
358
|
-
|
|
323
|
+
assertPlanningActive(options.signal);
|
|
359
324
|
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
360
|
-
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder,
|
|
325
|
+
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
|
|
361
326
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
362
327
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
363
328
|
const hookDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
364
329
|
for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
|
|
365
|
-
|
|
330
|
+
const market = plan.markets[marketIndex]!;
|
|
331
|
+
if (!isPoolBoundV4ConfigVersion(market.configVersion)) continue;
|
|
332
|
+
const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
|
|
333
|
+
hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
|
|
366
334
|
}
|
|
367
|
-
// Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
|
|
368
335
|
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" };
|
|
369
336
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
370
|
-
return { ...planned, hookDeployments, simulation: waiting
|
|
337
|
+
return { ...planned, hookDeployments, simulation: waiting };
|
|
371
338
|
}
|
|
372
339
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
373
340
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
374
341
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
375
342
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
? prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork }, block)
|
|
380
|
-
: undefined;
|
|
381
|
-
const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
382
|
-
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
|
|
383
|
-
validatePendingInputs(client, planned, progress, block),
|
|
343
|
+
const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
344
|
+
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [] },
|
|
345
|
+
readPlanMetadata(client, plan, identity.predictedToken, block),
|
|
384
346
|
]));
|
|
385
|
-
|
|
386
|
-
planned = { ...planned, prerequisites:
|
|
387
|
-
|
|
388
|
-
|
|
347
|
+
assertFinalPlan(planned, options.signal);
|
|
348
|
+
planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
|
|
349
|
+
let preparationBatchSize = plan.markets.length;
|
|
350
|
+
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
|
|
351
|
+
if (metadata.reasons.length > 0) {
|
|
389
352
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
390
|
-
return { ...planned, simulation:
|
|
353
|
+
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) };
|
|
391
354
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
355
|
+
const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block);
|
|
356
|
+
try {
|
|
357
|
+
const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => {
|
|
358
|
+
if (reviewed === undefined) return preparedSimulation(candidate);
|
|
359
|
+
if (!matchesReviewedNextTransaction(candidate[0], reviewed)) {
|
|
360
|
+
// A fresh prepare batch may still require SDK-owned capacity discovery.
|
|
361
|
+
// Never use held fees for a different step or caller-selected calldata.
|
|
362
|
+
if (candidate[0]?.kind === "prepare" && reviewed.kind === "prepare") return preparedSimulation(candidate);
|
|
363
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
364
|
+
}
|
|
365
|
+
return preparedSimulation(candidate, reviewed);
|
|
366
|
+
};
|
|
367
|
+
let simulation = await execute(transactions);
|
|
368
|
+
assertFinalPlan(planned, options.signal);
|
|
369
|
+
if (mode === "staged") {
|
|
370
|
+
// Only explicit gas/compute evidence can partition empty preparations.
|
|
371
|
+
// Byte caps cannot shrink: each prepare still carries the entire plan.
|
|
372
|
+
while (!simulation.admitted && simulation.failureCategory === "capacity" &&
|
|
373
|
+
(simulation.capacityConstraint === "compute" || simulation.capacityConstraint === "gas-envelope") &&
|
|
374
|
+
!(reviewed !== undefined && simulation.capacityConstraint === "gas-envelope" && simulation.failedTransactionId === transactions[0]?.id && matchesReviewedNextTransaction(transactions[0], reviewed)) &&
|
|
375
|
+
simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
|
|
376
|
+
const failedMarketCount = transactions.find((transaction) => transaction.id === simulation.failedTransactionId)?.marketCount ?? 0;
|
|
377
|
+
if (failedMarketCount <= 1) break;
|
|
378
|
+
preparationBatchSize = Math.ceil(failedMarketCount / 2);
|
|
379
|
+
transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
|
|
380
|
+
simulation = await execute(transactions);
|
|
381
|
+
assertFinalPlan(planned, options.signal);
|
|
382
|
+
}
|
|
383
|
+
if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
|
|
384
|
+
} else if (!diagnostic && !simulation.admitted) {
|
|
385
|
+
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` };
|
|
402
386
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
: preparedSimulation(transactions));
|
|
409
|
-
while (!simulation.admitted && simulation.confidence === "stateful" && simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
|
|
410
|
-
preparationBatchSize = Math.ceil(preparationBatchSize / 2);
|
|
411
|
-
transactions = orderedTransactions(plan, "staged", progress, prerequisites.approvals, prerequisites.nativeValue, preparationBatchSize);
|
|
412
|
-
simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, transactions, block);
|
|
413
|
-
}
|
|
414
|
-
if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
|
|
415
|
-
return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
|
|
387
|
+
if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
|
|
388
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
389
|
+
}
|
|
390
|
+
return { ...planned, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
391
|
+
} finally { preparedSimulation.close(); }
|
|
416
392
|
}
|
|
417
393
|
|
|
418
|
-
async function preflightNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, simulation: LifecycleSimulation): Promise<LifecycleTransaction> {
|
|
394
|
+
async function preflightNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, simulation: LifecycleSimulation, reviewed?: ReviewedNextTransaction): Promise<LifecycleTransaction> {
|
|
419
395
|
if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
|
|
420
396
|
simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
|
|
421
397
|
next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
|
|
422
398
|
simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
|
|
423
399
|
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
|
|
424
400
|
}
|
|
401
|
+
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
425
402
|
const submissionClient = options.submissionClient;
|
|
403
|
+
assertPlanningActive(options.signal);
|
|
426
404
|
if (submissionClient === undefined) return { ...next, dependencies: [] };
|
|
427
405
|
try {
|
|
428
406
|
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
@@ -431,46 +409,64 @@ async function preflightNextTransaction(options: BuildNextTransactionOptions, ne
|
|
|
431
409
|
}, "latest"]), "submission gas estimate");
|
|
432
410
|
if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
433
411
|
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
|
|
412
|
+
assertPlanningActive(options.signal);
|
|
434
413
|
} catch (failure) {
|
|
435
|
-
|
|
414
|
+
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
415
|
+
const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
|
|
436
416
|
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
437
417
|
}
|
|
438
|
-
const canonical = await
|
|
418
|
+
const [canonical, chainId] = await Promise.all([
|
|
419
|
+
readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
|
|
420
|
+
lifecycleRpc(options.client, "eth_chainId"),
|
|
421
|
+
]);
|
|
439
422
|
if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
|
|
440
|
-
if (rpcQuantity(
|
|
423
|
+
if (rpcQuantity(chainId, "chain ID") !== simulation.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
|
|
441
424
|
if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
425
|
+
assertPlanningActive(options.signal);
|
|
426
|
+
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
442
427
|
return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
|
|
443
428
|
}
|
|
444
429
|
|
|
445
|
-
/**
|
|
430
|
+
/** Recovery observes real canonical receipts/nonces, then proves only unfinished
|
|
431
|
+
* execution; known construction metadata is not recertified through getter mirrors. */
|
|
446
432
|
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
await
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
433
|
+
const supplied = readReviewedNextTransaction(options.reviewedTransaction);
|
|
434
|
+
// Snapshot only the reviewed wire fields; caller proofs and commands are never
|
|
435
|
+
// reused, and final checks compare the mutable input with this one local copy.
|
|
436
|
+
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
437
|
+
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
|
|
438
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
439
|
+
const block = await readLifecycleBlock(client);
|
|
440
|
+
const progress = await readLaunchProgress({ ...options, client }, block);
|
|
441
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
442
|
+
// Only the confirmed canonical terminal phase is terminal; a head-only
|
|
443
|
+
// Active observation is not, and the confirmation/nonce/receipt gates below
|
|
444
|
+
// remain in force for every nonterminal case.
|
|
445
|
+
await assertLifecycleBlock(client, block, options.planned.chainId);
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
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");
|
|
449
|
+
if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed")) throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
|
|
450
|
+
assertPlanningActive(options.signal);
|
|
451
|
+
if (options.action === "cancel") {
|
|
452
|
+
if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
453
|
+
const { planned } = options;
|
|
454
|
+
const transaction: LifecycleTransaction = { 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 }] };
|
|
455
|
+
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
|
|
456
|
+
const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
|
|
457
|
+
if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
458
|
+
const next = attachEstimates([transaction], simulation)[0];
|
|
459
|
+
if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
460
|
+
return preflightNextTransaction(options, next, simulation, reviewed);
|
|
461
|
+
}
|
|
462
|
+
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 }, client, block, progress, false, reviewed));
|
|
463
|
+
assertFinalPlan(refreshed, options.signal);
|
|
464
|
+
if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
|
|
465
|
+
assertPlannedDeploymentBindings(options.planned, refreshed);
|
|
466
|
+
const next = refreshed.transactions[0];
|
|
467
|
+
if (next === undefined) return undefined;
|
|
468
|
+
return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
|
|
469
|
+
});
|
|
474
470
|
}
|
|
475
471
|
|
|
476
472
|
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
@@ -481,7 +477,7 @@ export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Pr
|
|
|
481
477
|
const current = await planLaunch({
|
|
482
478
|
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
483
479
|
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
484
|
-
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
|
|
480
|
+
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
|
|
485
481
|
});
|
|
486
482
|
assertPlannedDeploymentBindings(planned, current);
|
|
487
483
|
return current.simulation;
|