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