@black-market/sdk 0.1.0 → 0.4.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 +92 -20
- package/dist/abyss.d.ts +4 -2748
- package/dist/abyss.d.ts.map +1 -1
- package/dist/abyss.js +3 -1029
- package/dist/abyss.js.map +1 -1
- package/dist/addresses.d.ts +3 -7
- package/dist/addresses.d.ts.map +1 -1
- package/dist/addresses.js +6 -21
- package/dist/addresses.js.map +1 -1
- package/dist/auction.d.ts +2 -2
- package/dist/auction.d.ts.map +1 -1
- package/dist/auction.js +2 -2
- package/dist/auction.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/launch-api.d.ts +151 -0
- package/dist/launch-api.d.ts.map +1 -0
- package/dist/launch-api.js +117 -0
- package/dist/launch-api.js.map +1 -0
- package/dist/launch.d.ts +23 -185
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +35 -272
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +6391 -0
- package/dist/lifecycle/abi.d.ts.map +1 -0
- package/dist/lifecycle/abi.js +335 -0
- package/dist/lifecycle/abi.js.map +1 -0
- package/dist/lifecycle/authors.d.ts +133 -0
- package/dist/lifecycle/authors.d.ts.map +1 -0
- package/dist/lifecycle/authors.js +217 -0
- package/dist/lifecycle/authors.js.map +1 -0
- package/dist/lifecycle/fork.d.ts +15 -0
- package/dist/lifecycle/fork.d.ts.map +1 -0
- package/dist/lifecycle/fork.js +36 -0
- package/dist/lifecycle/fork.js.map +1 -0
- package/dist/lifecycle/index.d.ts +10 -0
- package/dist/lifecycle/index.d.ts.map +1 -0
- package/dist/lifecycle/index.js +10 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/markets.d.ts +349 -0
- package/dist/lifecycle/markets.d.ts.map +1 -0
- package/dist/lifecycle/markets.js +186 -0
- package/dist/lifecycle/markets.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +20 -0
- package/dist/lifecycle/planner.d.ts.map +1 -0
- package/dist/lifecycle/planner.js +481 -0
- package/dist/lifecycle/planner.js.map +1 -0
- package/dist/lifecycle/progress.d.ts +43 -0
- package/dist/lifecycle/progress.d.ts.map +1 -0
- package/dist/lifecycle/progress.js +449 -0
- package/dist/lifecycle/progress.js.map +1 -0
- package/dist/lifecycle/rpc.d.ts +17 -0
- package/dist/lifecycle/rpc.d.ts.map +1 -0
- package/dist/lifecycle/rpc.js +100 -0
- package/dist/lifecycle/rpc.js.map +1 -0
- package/dist/lifecycle/schema.d.ts +1068 -0
- package/dist/lifecycle/schema.d.ts.map +1 -0
- package/dist/lifecycle/schema.js +335 -0
- package/dist/lifecycle/schema.js.map +1 -0
- package/dist/lifecycle/simulation.d.ts +3 -0
- package/dist/lifecycle/simulation.d.ts.map +1 -0
- package/dist/lifecycle/simulation.js +393 -0
- package/dist/lifecycle/simulation.js.map +1 -0
- package/dist/lifecycle/types.d.ts +273 -0
- package/dist/lifecycle/types.d.ts.map +1 -0
- package/dist/lifecycle/types.js +11 -0
- package/dist/lifecycle/types.js.map +1 -0
- package/docs/launch-lifecycle.md +243 -0
- package/package.json +12 -3
- package/src/abyss.ts +3 -1059
- package/src/addresses.ts +9 -26
- package/src/auction.ts +2 -2
- package/src/index.ts +2 -0
- package/src/launch-api.ts +110 -0
- package/src/launch.ts +43 -411
- package/src/lifecycle/abi.ts +343 -0
- package/src/lifecycle/authors.ts +203 -0
- package/src/lifecycle/fork.ts +36 -0
- package/src/lifecycle/index.ts +9 -0
- package/src/lifecycle/markets.ts +205 -0
- package/src/lifecycle/planner.ts +396 -0
- package/src/lifecycle/progress.ts +378 -0
- package/src/lifecycle/rpc.ts +88 -0
- package/src/lifecycle/schema.ts +387 -0
- package/src/lifecycle/simulation.ts +329 -0
- package/src/lifecycle/types.ts +132 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { encodeAbiParameters, encodeFunctionData, keccak256, toHex, zeroAddress, type Address, type Hex } from "viem";
|
|
2
|
+
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFundingEscrowAbi, lifecycleRegistryAbi } from "./abi.js";
|
|
3
|
+
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecycleMode, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LifecycleVenue, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MULTI_POSITION_CAPABILITY, LIFECYCLE_REQUIRED_CAPABILITIES, marketIdentityV1Components, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1, type MarketIdentityV1 } from "./schema.js";
|
|
4
|
+
import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
|
|
5
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
6
|
+
import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity } from "./rpc.js";
|
|
7
|
+
import { 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 PoolBoundLifecyclePreparationProgress, type SimulateLaunchPlanOptions } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function validatePlanShape(plan: LaunchPlanV1): void {
|
|
11
|
+
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
12
|
+
hashLaunchPlan(plan);
|
|
13
|
+
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
|
+
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
|
+
const encoder = new TextEncoder();
|
|
16
|
+
if (encoder.encode(plan.token.name).length === 0 || encoder.encode(plan.token.name).length > 128 || encoder.encode(plan.token.symbol).length === 0 || encoder.encode(plan.token.symbol).length > 32 || encoder.encode(plan.token.metadataURI).length > 2048) throw new LifecyclePlanningError("INVALID_PLAN", "Token metadata exceeds the committed byte bounds");
|
|
17
|
+
let previous = 0n;
|
|
18
|
+
for (const policy of plan.feeAssets) {
|
|
19
|
+
if (BigInt(policy.asset) <= previous || policy.ownerBps + policy.rewardsBps + policy.burnBps !== 10000) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Fee assets must be unique ascending addresses with fractions summing to 10000");
|
|
20
|
+
previous = BigInt(policy.asset);
|
|
21
|
+
}
|
|
22
|
+
if (plan.feeAssets.some((policy) => policy.rewardsBps !== 0) !== (plan.token.rewardMode !== LifecycleRewardMode.None)) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Reward-enabled tokens require a rewards share; reward-free tokens cannot commit rewards shares");
|
|
23
|
+
previous = 0n;
|
|
24
|
+
for (const funding of plan.funding) {
|
|
25
|
+
if (BigInt(funding.asset) <= previous || funding.amount <= 0n || funding.inputAmount <= 0n) throw new LifecyclePlanningError("INVALID_FUNDING", "Funding output assets must be positive, unique and ascending");
|
|
26
|
+
if (funding.kind !== LifecycleFundingKind.Swap && (funding.inputAsset.toLowerCase() !== funding.asset.toLowerCase() || funding.inputAmount !== funding.amount || funding.target !== zeroAddress || funding.data !== "0x")) throw new LifecyclePlanningError("INVALID_FUNDING", "ERC20 and native-wrap funding must use the exact output asset/amount and no target call");
|
|
27
|
+
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
|
+
previous = BigInt(funding.asset);
|
|
29
|
+
}
|
|
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
|
+
for (const buy of plan.buys) {
|
|
32
|
+
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
|
+
}
|
|
34
|
+
const v4Quotes = new Set<string>();
|
|
35
|
+
let positionCount = 0;
|
|
36
|
+
for (const market of plan.markets) {
|
|
37
|
+
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
|
+
let config: V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig | undefined;
|
|
39
|
+
if (market.configVersion === 2 || market.configVersion === 3) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Old V4 config versions 2 and 3 are not supported; encode reviewed terms explicitly using version 4 or 5");
|
|
40
|
+
if (market.configVersion === 4) config = decodeV4LifecycleMarketConfig(market.config);
|
|
41
|
+
else if (market.configVersion === 5) config = decodePoolBoundV4LifecycleMarketConfig(market.config);
|
|
42
|
+
if (config !== undefined) {
|
|
43
|
+
if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("PROFILE_TERMS_MISMATCH", "Inner config profile differs from the committed market profile");
|
|
44
|
+
const quote = market.quoteAsset.toLowerCase();
|
|
45
|
+
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
|
+
v4Quotes.add(quote);
|
|
47
|
+
if (config.positions.length === 0 || config.positions.length > 32 || config.positions.reduce((total, position) => total + position.maxTokenAmount, 0n) > market.tokenBudget || config.lpFeePips >= 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, position or budget bounds");
|
|
48
|
+
positionCount += config.positions.length;
|
|
49
|
+
}
|
|
50
|
+
if (positionCount > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 total positions across every venue");
|
|
51
|
+
}
|
|
52
|
+
for (const market of plan.markets) {
|
|
53
|
+
const required = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === market.quoteAsset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
|
|
54
|
+
const funding = plan.funding.find((item) => item.asset.toLowerCase() === market.quoteAsset.toLowerCase());
|
|
55
|
+
if (required > (funding?.amount ?? 0n)) throw new LifecyclePlanningError("INVALID_FUNDING", "Quote funding does not cover all ordered buys for its asset");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[]; reasons: string[]; nativeValue: bigint }> {
|
|
60
|
+
const escrow = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
|
|
61
|
+
if ((await readLifecycleContract<Address>(client, escrow, lifecycleFundingEscrowAbi, "core", [], block)).toLowerCase() !== plan.orchestrator.toLowerCase()) throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
|
|
62
|
+
const registry = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
63
|
+
const nativeBalance = rpcQuantity(await lifecycleRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)]), "creator native balance");
|
|
64
|
+
const totals = new Map<string, { asset: Address; amount: bigint; balance: bigint; allowance: bigint }>();
|
|
65
|
+
const prerequisites: LifecycleFundingPrerequisite[] = [];
|
|
66
|
+
const approvals: LifecycleTransaction[] = [];
|
|
67
|
+
const reasons: string[] = [];
|
|
68
|
+
let nativeValue = 0n;
|
|
69
|
+
for (const funding of plan.funding) {
|
|
70
|
+
const native = funding.kind === LifecycleFundingKind.NativeWrap || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress);
|
|
71
|
+
let inputBalance = nativeBalance;
|
|
72
|
+
let allowance: bigint | undefined;
|
|
73
|
+
if (native) nativeValue += funding.inputAmount;
|
|
74
|
+
else {
|
|
75
|
+
const key = funding.inputAsset.toLowerCase();
|
|
76
|
+
let total = totals.get(key);
|
|
77
|
+
if (total === undefined) {
|
|
78
|
+
inputBalance = await readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block);
|
|
79
|
+
allowance = await readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block);
|
|
80
|
+
total = { asset: funding.inputAsset, amount: 0n, balance: inputBalance, allowance };
|
|
81
|
+
totals.set(key, total);
|
|
82
|
+
}
|
|
83
|
+
total.amount += funding.inputAmount;
|
|
84
|
+
inputBalance = total.balance; allowance = total.allowance;
|
|
85
|
+
}
|
|
86
|
+
if (funding.kind === LifecycleFundingKind.NativeWrap) {
|
|
87
|
+
const wrapped = await readLifecycleContract<Address>(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block);
|
|
88
|
+
if (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase()) reasons.push("Committed native-wrap output is not the escrow's supported wrapped native asset");
|
|
89
|
+
}
|
|
90
|
+
if (funding.kind === LifecycleFundingKind.Swap) {
|
|
91
|
+
const [spender, codeHash, enabled] = await readLifecycleContract<readonly [Address, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block);
|
|
92
|
+
const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [funding.target, toHex(block.number)]), "funding target code");
|
|
93
|
+
if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase()) reasons.push("Committed funding conversion target is not currently allowlisted with its immutable code hash");
|
|
94
|
+
}
|
|
95
|
+
prerequisites.push({ 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" });
|
|
96
|
+
}
|
|
97
|
+
if (nativeBalance < nativeValue) reasons.push("Creator native balance does not cover committed native funding inputs");
|
|
98
|
+
for (const total of totals.values()) {
|
|
99
|
+
if (total.balance < total.amount) reasons.push(`Creator balance is below combined funding requirements for ${total.asset}`);
|
|
100
|
+
if (total.allowance >= total.amount) continue;
|
|
101
|
+
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 }] });
|
|
102
|
+
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 }] });
|
|
103
|
+
}
|
|
104
|
+
return { prerequisites, approvals, reasons, nativeValue };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function validatePendingInputs(client: LifecycleRpcClient, planned: PlannedLaunch, progress: CanonicalLaunchProgress, block: LifecycleBlock): Promise<{ profiles: LifecycleProfile[]; hookDeployments: LifecyclePoolBoundHookDeployment[]; reasons: string[] }> {
|
|
108
|
+
const { plan } = planned;
|
|
109
|
+
const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block);
|
|
110
|
+
const registry = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
111
|
+
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
112
|
+
if (block.timestamp > plan.deadline) reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
113
|
+
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");
|
|
114
|
+
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("The launch token must have a committed fee policy");
|
|
115
|
+
const hookDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
116
|
+
const assets = new Set< Address >();
|
|
117
|
+
for (const policy of plan.feeAssets) if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase()) assets.add(policy.asset);
|
|
118
|
+
for (const market of plan.markets) assets.add(market.quoteAsset);
|
|
119
|
+
for (const funding of plan.funding) { assets.add(funding.asset); if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress) assets.add(funding.inputAsset); }
|
|
120
|
+
for (const asset of assets) if (rpcHex(await lifecycleRpc(client, "eth_getCode", [asset, toHex(block.number)]), "asset code") === "0x") reasons.push(`Asset ${asset} has no contract code`);
|
|
121
|
+
for (const funding of plan.funding) {
|
|
122
|
+
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");
|
|
123
|
+
if (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress && !await readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block)) reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
|
|
124
|
+
}
|
|
125
|
+
const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
|
|
126
|
+
const identities = new Set<string>();
|
|
127
|
+
const v4Quotes = new Set<string>();
|
|
128
|
+
let positionCount = 0;
|
|
129
|
+
for (let index = 0; index < plan.markets.length; index += 1) {
|
|
130
|
+
const market = plan.markets[index];
|
|
131
|
+
if (market === undefined) throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
|
|
132
|
+
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
133
|
+
if (profile === undefined || !profile.admitted) continue;
|
|
134
|
+
try {
|
|
135
|
+
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");
|
|
136
|
+
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");
|
|
137
|
+
const schema = profile.registration.configSchema.toLowerCase();
|
|
138
|
+
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;
|
|
139
|
+
if (config === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
|
|
140
|
+
if ("developerFeeBps" in config) {
|
|
141
|
+
validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
|
|
142
|
+
if (config.version === 4) await validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block });
|
|
143
|
+
}
|
|
144
|
+
if (config.positions.length === 0 || config.positions.length > 32 || positionCount + config.positions.length > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
145
|
+
positionCount += config.positions.length;
|
|
146
|
+
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
147
|
+
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");
|
|
148
|
+
const capabilities = required | (config.positions.length > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
149
|
+
const adapter = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block);
|
|
150
|
+
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
151
|
+
const deployment = profile.topology.hookTopology === 2 ? await readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined;
|
|
152
|
+
if (deployment !== undefined) {
|
|
153
|
+
if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
154
|
+
hookDeployments.push({ marketIndex: index, ...deployment });
|
|
155
|
+
}
|
|
156
|
+
const identity = await readLifecycleContract<MarketIdentityV1>(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
|
|
157
|
+
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
158
|
+
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
159
|
+
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");
|
|
160
|
+
if (identity.venue === LifecycleVenue.UniswapV4) {
|
|
161
|
+
const quote = market.quoteAsset.toLowerCase();
|
|
162
|
+
if (v4Quotes.has(quote)) throw new LifecyclePlanningError("DUPLICATE_V4_QUOTE", "Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
|
|
163
|
+
v4Quotes.add(quote);
|
|
164
|
+
}
|
|
165
|
+
if (identities.has(identity.canonicalId.toLowerCase())) throw new LifecyclePlanningError("DUPLICATE_MARKET", "Two committed markets resolve to the same canonical pool");
|
|
166
|
+
identities.add(identity.canonicalId.toLowerCase());
|
|
167
|
+
const prepared = progress.markets.find((item) => item.index === index);
|
|
168
|
+
if (prepared !== undefined) {
|
|
169
|
+
const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
|
|
170
|
+
const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
|
|
171
|
+
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");
|
|
172
|
+
await readLifecycleContract<undefined>(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
|
|
173
|
+
}
|
|
174
|
+
} catch (failure) { reasons.push(failure instanceof Error ? failure.message : String(failure)); }
|
|
175
|
+
}
|
|
176
|
+
if (progress.canonical.phase !== LifecyclePhase.None) {
|
|
177
|
+
for (const funding of plan.funding) {
|
|
178
|
+
const balance = await readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block);
|
|
179
|
+
const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
|
|
180
|
+
if (balance < needed) reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { profiles, hookDeployments, reasons };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], nativeValue: bigint, batchSize: number): LifecycleTransaction[] {
|
|
187
|
+
const transactions: LifecycleTransaction[] = [];
|
|
188
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return transactions;
|
|
189
|
+
const base = { chainId: Number(plan.chainId), from: plan.creator, to: plan.orchestrator, dependencies: [] as string[] };
|
|
190
|
+
if (progress.canonical.phase === LifecyclePhase.None) {
|
|
191
|
+
transactions.push(...approvals);
|
|
192
|
+
if (mode === "atomic") {
|
|
193
|
+
transactions.push({ ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "launchAtomic", args: [plan] }), value: nativeValue, postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
|
|
194
|
+
} else {
|
|
195
|
+
transactions.push({ ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "beginLaunch", args: [plan, LifecycleMode.Staged] }), value: nativeValue, postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (mode === "staged") {
|
|
199
|
+
for (let first = progress.canonical.preparedMarkets; first < plan.markets.length; first += batchSize) {
|
|
200
|
+
const count = Math.min(batchSize, plan.markets.length - first);
|
|
201
|
+
const completed = first + count;
|
|
202
|
+
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 }] });
|
|
203
|
+
}
|
|
204
|
+
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 }] });
|
|
205
|
+
}
|
|
206
|
+
return transactions.map((transaction, index) => ({ ...transaction, dependencies: index === 0 ? [] : [transactions[index - 1]?.id ?? ""] }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
210
|
+
return transactions.map((transaction) => {
|
|
211
|
+
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
|
|
212
|
+
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, reason: simulation.reason, limits: simulation.limits } };
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator: Address, block: LifecycleBlock): Promise<Pick<PlannedLaunch, "tokenFactory" | "tokenFactoryCodeHash">> {
|
|
217
|
+
const tokenFactory = await readLifecycleContract<Address>(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
|
|
218
|
+
const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [tokenFactory, toHex(block.number)]), "token factory code");
|
|
219
|
+
if (tokenFactory === zeroAddress || code === "0x" || (await readLifecycleContract<Address>(client, tokenFactory, lifecycleAdapterAbi, "core", [], block)).toLowerCase() !== orchestrator.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
|
|
220
|
+
return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
|
|
224
|
+
if (hashLaunchPlan(stored.plan).toLowerCase() !== stored.planHash.toLowerCase() || current.planHash.toLowerCase() !== stored.planHash.toLowerCase() || current.launchId.toLowerCase() !== stored.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its finalized commitment");
|
|
225
|
+
if (current.predictedToken.toLowerCase() !== stored.predictedToken.toLowerCase() || current.tokenFactory.toLowerCase() !== stored.tokenFactory.toLowerCase() || current.tokenFactoryCodeHash.toLowerCase() !== stored.tokenFactoryCodeHash.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Final token prediction or its immutable factory code binding changed before execution");
|
|
226
|
+
if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
|
|
227
|
+
if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
|
|
228
|
+
const fresh = current.hookDeployments[index];
|
|
229
|
+
return fresh === undefined || fresh.marketIndex !== deployment.marketIndex || fresh.deployer.toLowerCase() !== deployment.deployer.toLowerCase() || fresh.initCodeHash.toLowerCase() !== deployment.initCodeHash.toLowerCase() || fresh.salt.toLowerCase() !== deployment.salt.toLowerCase() || fresh.predictedHook.toLowerCase() !== deployment.predictedHook.toLowerCase();
|
|
230
|
+
})) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Mines only certified bound markets, then re-reads the exact final economics and derived identities. */
|
|
234
|
+
export async function preparePoolBoundLifecyclePlan(options: {
|
|
235
|
+
client: LifecycleRpcClient; plan: LaunchPlanV1; signal?: AbortSignal;
|
|
236
|
+
onProgress?: (progress: PoolBoundLifecyclePreparationProgress) => void;
|
|
237
|
+
}): Promise<{ plan: LaunchPlanV1; deployments: readonly LifecyclePoolBoundHookDeployment[] }> {
|
|
238
|
+
const { client, signal } = options;
|
|
239
|
+
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
240
|
+
validatePlanShape(options.plan);
|
|
241
|
+
const commitment = hashLaunchPlan(options.plan);
|
|
242
|
+
let plan = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
243
|
+
const initialBlock = await readLifecycleBlock(client);
|
|
244
|
+
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");
|
|
245
|
+
const initialFactory = await readTokenFactoryBinding(client, plan.orchestrator, initialBlock);
|
|
246
|
+
const initialToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], initialBlock);
|
|
247
|
+
const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, initialBlock);
|
|
248
|
+
const deployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
249
|
+
for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
|
|
250
|
+
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
251
|
+
const market = plan.markets[marketIndex];
|
|
252
|
+
if (market === undefined) throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
|
|
253
|
+
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
254
|
+
if (profile === undefined || !profile.admitted) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", profile?.reason ?? "Market is not an admitted lifecycle profile");
|
|
255
|
+
if (profile.topology.hookTopology !== 2) continue;
|
|
256
|
+
const metadata = await readPoolBoundHookDeployment({ client, plan, marketIndex }, initialBlock);
|
|
257
|
+
const mined = await minePoolBoundHookSalt({
|
|
258
|
+
deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, startSalt: BigInt(metadata.salt), signal,
|
|
259
|
+
onProgress: (progress) => options.onProgress?.({ marketIndex, ...progress }),
|
|
260
|
+
});
|
|
261
|
+
plan = { ...plan, markets: plan.markets.map((item, index) => index === marketIndex ? { ...item, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(item.config), hookSalt: mined.salt }) } : item) };
|
|
262
|
+
deployments.push({ marketIndex, deployer: metadata.deployer, initCodeHash: metadata.initCodeHash, ...mined });
|
|
263
|
+
}
|
|
264
|
+
await assertLifecycleBlock(client, initialBlock);
|
|
265
|
+
const finalBlock = await readLifecycleBlock(client);
|
|
266
|
+
const finalFactory = await readTokenFactoryBinding(client, plan.orchestrator, finalBlock);
|
|
267
|
+
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");
|
|
268
|
+
const finalToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], finalBlock);
|
|
269
|
+
if (finalToken.toLowerCase() !== initialToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Token prediction changed while mining the market-bound deployment");
|
|
270
|
+
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);
|
|
271
|
+
for (const deployment of deployments) {
|
|
272
|
+
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
273
|
+
const market = plan.markets[deployment.marketIndex];
|
|
274
|
+
const profile = finalProfiles.find((item) => item.id.toLowerCase() === market?.profileId.toLowerCase());
|
|
275
|
+
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");
|
|
276
|
+
const fresh = await readPoolBoundHookDeployment({ client, plan, marketIndex: deployment.marketIndex }, finalBlock);
|
|
277
|
+
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");
|
|
278
|
+
const identity = await readLifecycleContract<MarketIdentityV1>(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [hashLaunchIdentity(plan), finalToken, market], finalBlock);
|
|
279
|
+
validateLifecycleMarketIdentity(identity, plan, finalToken, deployment.marketIndex);
|
|
280
|
+
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");
|
|
281
|
+
}
|
|
282
|
+
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous salt mining; previous mining is invalid");
|
|
283
|
+
if (signal?.aborted) throw Object.assign(new Error("Pool-bound lifecycle preparation cancelled"), { name: "AbortError" });
|
|
284
|
+
await assertLifecycleBlock(client, finalBlock);
|
|
285
|
+
return { plan, deployments };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
|
|
289
|
+
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
290
|
+
const { client, plan, account, mode } = options;
|
|
291
|
+
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
292
|
+
if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
293
|
+
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");
|
|
294
|
+
validatePlanShape(plan);
|
|
295
|
+
const block = await readLifecycleBlock(client);
|
|
296
|
+
if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
297
|
+
const planHash = hashLaunchPlan(plan); const launchId = hashLaunchIdentity(plan);
|
|
298
|
+
const remoteHash = await readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block);
|
|
299
|
+
const remoteIdentity = await readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block);
|
|
300
|
+
if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
|
|
301
|
+
const predictedToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
302
|
+
const { tokenFactory, tokenFactoryCodeHash } = await readTokenFactoryBinding(client, plan.orchestrator, block);
|
|
303
|
+
const { limits } = await resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits);
|
|
304
|
+
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
305
|
+
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
306
|
+
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
307
|
+
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
|
|
308
|
+
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
309
|
+
if (!progress.confirmationSafe || receiptsPending) {
|
|
310
|
+
const hookDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
311
|
+
for (let marketIndex = 0; marketIndex < plan.markets.length; marketIndex += 1) {
|
|
312
|
+
if (plan.markets[marketIndex]?.configVersion === 5) hookDeployments.push({ marketIndex, ...await readPoolBoundHookDeployment({ client, plan, marketIndex }, block) });
|
|
313
|
+
}
|
|
314
|
+
// Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
|
|
315
|
+
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" };
|
|
316
|
+
await assertLifecycleBlock(client, block);
|
|
317
|
+
return { ...planned, hookDeployments, simulation: waiting, atomicAttempt: waiting };
|
|
318
|
+
}
|
|
319
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
320
|
+
await assertLifecycleBlock(client, block);
|
|
321
|
+
return { ...planned, simulation: { ...placeholder, confidence: "stateful", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
322
|
+
}
|
|
323
|
+
const prerequisites = progress.canonical.phase === LifecyclePhase.None ? await readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] };
|
|
324
|
+
const validation = await validatePendingInputs(client, planned, progress, block);
|
|
325
|
+
const reasons = [...prerequisites.reasons, ...validation.reasons];
|
|
326
|
+
planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
|
|
327
|
+
if (reasons.length > 0) {
|
|
328
|
+
const rejected = { ...placeholder, confidence: "stateful" as const, reason: reasons.join("; ") };
|
|
329
|
+
await assertLifecycleBlock(client, block);
|
|
330
|
+
return { ...planned, simulation: rejected, atomicAttempt: rejected, transactions: orderedTransactions(plan, mode, progress, prerequisites.approvals, prerequisites.nativeValue, planned.preparationBatchSize) };
|
|
331
|
+
}
|
|
332
|
+
let atomicAttempt: LifecycleSimulation = { ...placeholder, reason: "Atomic execution cannot be evaluated from an already begun staged launch" };
|
|
333
|
+
if (progress.canonical.phase === LifecyclePhase.None) {
|
|
334
|
+
const atomicTransactions = orderedTransactions(plan, "atomic", progress, prerequisites.approvals, prerequisites.nativeValue, plan.markets.length);
|
|
335
|
+
atomicAttempt = await simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, atomicTransactions, block);
|
|
336
|
+
if (mode === "atomic") {
|
|
337
|
+
const reason = atomicAttempt.admitted ? undefined : `${atomicAttempt.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits`;
|
|
338
|
+
const simulation = { ...atomicAttempt, reason };
|
|
339
|
+
return { ...planned, atomicAttempt, simulation, transactions: attachEstimates(atomicTransactions, simulation) };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let preparationBatchSize = plan.markets.length;
|
|
343
|
+
let transactions = orderedTransactions(plan, "staged", progress, prerequisites.approvals, prerequisites.nativeValue, preparationBatchSize);
|
|
344
|
+
let simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, transactions, block);
|
|
345
|
+
while (!simulation.admitted && simulation.confidence === "stateful" && simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
|
|
346
|
+
preparationBatchSize = Math.ceil(preparationBatchSize / 2);
|
|
347
|
+
transactions = orderedTransactions(plan, "staged", progress, prerequisites.approvals, prerequisites.nativeValue, preparationBatchSize);
|
|
348
|
+
simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits, fork: options.fork }, transactions, block);
|
|
349
|
+
}
|
|
350
|
+
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` };
|
|
351
|
+
return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Re-reads canonical state, revalidates all prerequisites, and proves the remaining sequence afresh. */
|
|
355
|
+
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
356
|
+
const block = await readLifecycleBlock(options.client);
|
|
357
|
+
const progress = await readLaunchProgress(options, block);
|
|
358
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
359
|
+
// Only the confirmed canonical terminal phase is terminal; a head-only
|
|
360
|
+
// Active observation is not, and the confirmation/nonce/receipt gates below
|
|
361
|
+
// remain in force for every nonterminal case.
|
|
362
|
+
await assertLifecycleBlock(options.client, block);
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
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");
|
|
366
|
+
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");
|
|
367
|
+
if (options.action === "cancel") {
|
|
368
|
+
if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
369
|
+
const { planned } = options;
|
|
370
|
+
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 }] };
|
|
371
|
+
const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits ?? planned.limits, fork: options.fork }, [transaction], block);
|
|
372
|
+
if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
373
|
+
return attachEstimates([transaction], simulation)[0];
|
|
374
|
+
}
|
|
375
|
+
const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits ?? options.planned.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
|
|
376
|
+
if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
|
|
377
|
+
assertPlannedDeploymentBindings(options.planned, refreshed);
|
|
378
|
+
const next = refreshed.transactions[0];
|
|
379
|
+
if (next === undefined) return undefined;
|
|
380
|
+
if (next.gas === undefined || next.gasPrice === undefined || next.gas > refreshed.simulation.limits.executionGasCeiling) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof");
|
|
381
|
+
return { ...next, dependencies: [] };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
385
|
+
* evidence and confirmation depth as build-next, never silently dropping context. */
|
|
386
|
+
export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
|
|
387
|
+
const { client, planned } = options;
|
|
388
|
+
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
|
|
389
|
+
const current = await planLaunch({
|
|
390
|
+
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
391
|
+
limits: options.limits ?? planned.limits, fork: options.fork,
|
|
392
|
+
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
|
|
393
|
+
});
|
|
394
|
+
assertPlannedDeploymentBindings(planned, current);
|
|
395
|
+
return current.simulation;
|
|
396
|
+
}
|