@black-market/sdk 0.6.0 → 0.6.2
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/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +198 -133
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +204 -143
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -1
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +149 -10
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +23 -14
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +15 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +107 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +164 -109
- package/src/lifecycle/progress.ts +187 -126
- package/src/lifecycle/rpc.ts +146 -11
- package/src/lifecycle/simulation.ts +23 -13
- package/src/lifecycle/types.ts +19 -0
package/src/lifecycle/planner.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFu
|
|
|
3
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
4
|
import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
|
|
5
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";
|
|
6
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
7
7
|
import { simulateLaunchTransactions } from "./simulation.js";
|
|
8
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
9
|
|
|
@@ -57,42 +57,55 @@ function validatePlanShape(plan: LaunchPlanV1): void {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[]; reasons: string[]; nativeValue: bigint }> {
|
|
60
|
-
const escrow = await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const [escrow, registry, balanceValue] = await Promise.all([
|
|
61
|
+
readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block),
|
|
62
|
+
readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
|
|
63
|
+
lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block),
|
|
64
|
+
]);
|
|
65
|
+
const nativeBalance = rpcQuantity(balanceValue, "creator native balance");
|
|
66
|
+
const [authority, observations] = await Promise.all([
|
|
67
|
+
readLifecycleContract<Address>(client, escrow, lifecycleFundingEscrowAbi, "core", [], block),
|
|
68
|
+
Promise.all(plan.funding.map(async (funding) => {
|
|
69
|
+
const native = funding.kind === LifecycleFundingKind.NativeWrap || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress);
|
|
70
|
+
const [input, wrapped, target] = await Promise.all([
|
|
71
|
+
native ? [nativeBalance, undefined] as const : Promise.all([
|
|
72
|
+
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
|
|
73
|
+
readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
|
|
74
|
+
]),
|
|
75
|
+
funding.kind === LifecycleFundingKind.NativeWrap ? readLifecycleContract<Address>(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block) : undefined,
|
|
76
|
+
funding.kind === LifecycleFundingKind.Swap ? Promise.all([
|
|
77
|
+
readLifecycleContract<readonly [Address, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block),
|
|
78
|
+
lifecyclePinnedRpc(client, "eth_getCode", [funding.target, toHex(block.number)], block),
|
|
79
|
+
]) : undefined,
|
|
80
|
+
]);
|
|
81
|
+
let reason: string | undefined;
|
|
82
|
+
if (wrapped !== undefined && (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase())) reason = "Committed native-wrap output is not the escrow's supported wrapped native asset";
|
|
83
|
+
if (target !== undefined) {
|
|
84
|
+
const [[spender, codeHash, enabled], codeValue] = target;
|
|
85
|
+
const code = rpcHex(codeValue, "funding target code");
|
|
86
|
+
if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase()) reason = "Committed funding conversion target is not currently allowlisted with its immutable code hash";
|
|
87
|
+
}
|
|
88
|
+
const prerequisite: LifecycleFundingPrerequisite = { 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" };
|
|
89
|
+
return { prerequisite, reason };
|
|
90
|
+
})),
|
|
91
|
+
]);
|
|
92
|
+
if (authority.toLowerCase() !== plan.orchestrator.toLowerCase()) throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
|
|
64
93
|
const totals = new Map<string, { asset: Address; amount: bigint; balance: bigint; allowance: bigint }>();
|
|
65
94
|
const prerequisites: LifecycleFundingPrerequisite[] = [];
|
|
66
95
|
const approvals: LifecycleTransaction[] = [];
|
|
67
96
|
const reasons: string[] = [];
|
|
68
97
|
let nativeValue = 0n;
|
|
69
|
-
for (const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
98
|
+
for (const { prerequisite, reason } of observations) {
|
|
99
|
+
prerequisites.push(prerequisite);
|
|
100
|
+
if (reason !== undefined) reasons.push(reason);
|
|
101
|
+
nativeValue += prerequisite.nativeValue;
|
|
102
|
+
if (prerequisite.allowance !== undefined) {
|
|
103
|
+
const { funding } = prerequisite;
|
|
75
104
|
const key = funding.inputAsset.toLowerCase();
|
|
76
|
-
|
|
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
|
-
}
|
|
105
|
+
const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, balance: prerequisite.inputBalance, allowance: prerequisite.allowance };
|
|
83
106
|
total.amount += funding.inputAmount;
|
|
84
|
-
|
|
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");
|
|
107
|
+
totals.set(key, total);
|
|
89
108
|
}
|
|
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
109
|
}
|
|
97
110
|
if (nativeBalance < nativeValue) reasons.push("Creator native balance does not cover committed native funding inputs");
|
|
98
111
|
for (const total of totals.values()) {
|
|
@@ -106,79 +119,98 @@ async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block
|
|
|
106
119
|
|
|
107
120
|
async function validatePendingInputs(client: LifecycleRpcClient, planned: PlannedLaunch, progress: CanonicalLaunchProgress, block: LifecycleBlock): Promise<{ profiles: LifecycleProfile[]; hookDeployments: LifecyclePoolBoundHookDeployment[]; reasons: string[] }> {
|
|
108
121
|
const { plan } = planned;
|
|
109
|
-
const
|
|
110
|
-
const
|
|
122
|
+
const assets = new Set<Address>();
|
|
123
|
+
for (const policy of plan.feeAssets) if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase()) assets.add(policy.asset);
|
|
124
|
+
for (const market of plan.markets) assets.add(market.quoteAsset);
|
|
125
|
+
for (const funding of plan.funding) { assets.add(funding.asset); if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress) assets.add(funding.inputAsset); }
|
|
126
|
+
const [profiles, registry, assetCodes] = await Promise.all([
|
|
127
|
+
readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
|
|
128
|
+
readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
|
|
129
|
+
Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
|
|
130
|
+
]);
|
|
111
131
|
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
112
132
|
if (block.timestamp > plan.deadline) reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
113
133
|
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
134
|
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("The launch token must have a committed fee policy");
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
for (const { asset, code } of assetCodes) if (code === "0x") reasons.push(`Asset ${asset} has no contract code`);
|
|
136
|
+
const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
|
|
137
|
+
const [allowedInputs, markets, escrowBalances] = await Promise.all([
|
|
138
|
+
Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
|
|
139
|
+
? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true)),
|
|
140
|
+
Promise.all(plan.markets.map(async (market, index) => {
|
|
141
|
+
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
142
|
+
let positions = 0;
|
|
143
|
+
let deployment: LifecyclePoolBoundHookDeployment | undefined;
|
|
144
|
+
let identity: MarketIdentityV1 | undefined;
|
|
145
|
+
let reason: string | undefined;
|
|
146
|
+
if (profile === undefined || !profile.admitted) return { positions, deployment, identity, reason };
|
|
147
|
+
try {
|
|
148
|
+
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");
|
|
149
|
+
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");
|
|
150
|
+
const schema = profile.registration.configSchema.toLowerCase();
|
|
151
|
+
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;
|
|
152
|
+
if (config === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
|
|
153
|
+
if ("developerFeeBps" in config) {
|
|
154
|
+
validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
|
|
155
|
+
if (config.version === 4) await validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block });
|
|
156
|
+
}
|
|
157
|
+
if (config.positions.length === 0 || config.positions.length > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
158
|
+
positions = config.positions.length;
|
|
159
|
+
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
160
|
+
if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
|
|
161
|
+
const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
162
|
+
const [adapter, boundDeployment] = await Promise.all([
|
|
163
|
+
readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
|
|
164
|
+
profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
|
|
165
|
+
]);
|
|
166
|
+
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
167
|
+
if (boundDeployment !== undefined) {
|
|
168
|
+
if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
169
|
+
deployment = { marketIndex: index, ...boundDeployment };
|
|
170
|
+
}
|
|
171
|
+
identity = await readLifecycleContract<MarketIdentityV1>(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
|
|
172
|
+
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
173
|
+
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
174
|
+
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");
|
|
175
|
+
const prepared = progress.markets.find((item) => item.index === index);
|
|
176
|
+
if (prepared !== undefined) {
|
|
177
|
+
const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
|
|
178
|
+
const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
|
|
179
|
+
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");
|
|
180
|
+
await readLifecycleContract<undefined>(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
|
|
181
|
+
}
|
|
182
|
+
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
183
|
+
return { positions, deployment, identity, reason };
|
|
184
|
+
})),
|
|
185
|
+
progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
|
|
186
|
+
]);
|
|
187
|
+
for (const [index, funding] of plan.funding.entries()) {
|
|
122
188
|
if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
|
|
123
|
-
if (
|
|
189
|
+
if (!allowedInputs[index]) reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
|
|
124
190
|
}
|
|
125
|
-
const
|
|
191
|
+
const hookDeployments: LifecyclePoolBoundHookDeployment[] = [];
|
|
126
192
|
const identities = new Set<string>();
|
|
127
193
|
const v4Quotes = new Set<string>();
|
|
128
194
|
let positionCount = 0;
|
|
129
|
-
for (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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`);
|
|
195
|
+
for (const [index, result] of markets.entries()) {
|
|
196
|
+
if (positionCount + result.positions > 32) { reasons.push("A launch commits at most 32 positive positions across every market/venue"); continue; }
|
|
197
|
+
positionCount += result.positions;
|
|
198
|
+
if (result.deployment !== undefined) hookDeployments.push(result.deployment);
|
|
199
|
+
if (result.reason !== undefined) { reasons.push(result.reason); continue; }
|
|
200
|
+
if (result.identity === undefined) continue;
|
|
201
|
+
if (result.identity.venue === LifecycleVenue.UniswapV4) {
|
|
202
|
+
const quote = plan.markets[index]!.quoteAsset.toLowerCase();
|
|
203
|
+
if (v4Quotes.has(quote)) { reasons.push("Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt"); continue; }
|
|
204
|
+
v4Quotes.add(quote);
|
|
181
205
|
}
|
|
206
|
+
const identity = result.identity.canonicalId.toLowerCase();
|
|
207
|
+
if (identities.has(identity)) { reasons.push("Two committed markets resolve to the same canonical pool"); continue; }
|
|
208
|
+
identities.add(identity);
|
|
209
|
+
}
|
|
210
|
+
for (const [index, balance] of escrowBalances.entries()) {
|
|
211
|
+
const funding = plan.funding[index]!;
|
|
212
|
+
const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
|
|
213
|
+
if (balance < needed) reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
|
|
182
214
|
}
|
|
183
215
|
return { profiles, hookDeployments, reasons };
|
|
184
216
|
}
|
|
@@ -215,8 +247,12 @@ function attachEstimates(transactions: readonly LifecycleTransaction[], simulati
|
|
|
215
247
|
|
|
216
248
|
async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator: Address, block: LifecycleBlock): Promise<Pick<PlannedLaunch, "tokenFactory" | "tokenFactoryCodeHash">> {
|
|
217
249
|
const tokenFactory = await readLifecycleContract<Address>(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
|
|
218
|
-
const
|
|
219
|
-
|
|
250
|
+
const [codeValue, authority] = await Promise.all([
|
|
251
|
+
lifecyclePinnedRpc(client, "eth_getCode", [tokenFactory, toHex(block.number)], block),
|
|
252
|
+
readLifecycleContract<Address>(client, tokenFactory, lifecycleAdapterAbi, "core", [], block),
|
|
253
|
+
]);
|
|
254
|
+
const code = rpcHex(codeValue, "token factory code");
|
|
255
|
+
if (tokenFactory === zeroAddress || code === "0x" || authority.toLowerCase() !== orchestrator.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
|
|
220
256
|
return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
|
|
221
257
|
}
|
|
222
258
|
|
|
@@ -287,23 +323,40 @@ export async function preparePoolBoundLifecyclePlan(options: {
|
|
|
287
323
|
|
|
288
324
|
/** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
|
|
289
325
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
290
|
-
|
|
326
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
327
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
328
|
+
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
|
|
329
|
+
return planned;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient): Promise<PlannedLaunch> {
|
|
334
|
+
const { plan, account, mode } = options;
|
|
291
335
|
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
292
336
|
if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
293
337
|
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
338
|
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
339
|
const planHash = hashLaunchPlan(plan); const launchId = hashLaunchIdentity(plan);
|
|
298
|
-
const
|
|
299
|
-
|
|
340
|
+
const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
341
|
+
readLifecycleBlock(client),
|
|
342
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
343
|
+
]));
|
|
344
|
+
if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
345
|
+
const prediction = readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
346
|
+
const [remoteHash, remoteIdentity, { tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
|
|
347
|
+
readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
|
|
348
|
+
readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
|
|
349
|
+
readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
350
|
+
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
351
|
+
lifecycleStage(client, "plan.progress", async () => {
|
|
352
|
+
const predictedToken = await prediction;
|
|
353
|
+
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
354
|
+
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
355
|
+
return { identity, progress };
|
|
356
|
+
}),
|
|
357
|
+
]));
|
|
300
358
|
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
359
|
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
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
360
|
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
361
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
309
362
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
@@ -313,20 +366,22 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
313
366
|
}
|
|
314
367
|
// Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
|
|
315
368
|
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);
|
|
369
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
317
370
|
return { ...planned, hookDeployments, simulation: waiting, atomicAttempt: waiting };
|
|
318
371
|
}
|
|
319
372
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
320
|
-
await assertLifecycleBlock(client, block);
|
|
373
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
321
374
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
322
375
|
}
|
|
323
|
-
const prerequisites =
|
|
324
|
-
|
|
376
|
+
const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
377
|
+
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
|
|
378
|
+
validatePendingInputs(client, planned, progress, block),
|
|
379
|
+
]));
|
|
325
380
|
const reasons = [...prerequisites.reasons, ...validation.reasons];
|
|
326
381
|
planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
|
|
327
382
|
if (reasons.length > 0) {
|
|
328
383
|
const rejected = { ...placeholder, confidence: "stateful" as const, reason: reasons.join("; ") };
|
|
329
|
-
await assertLifecycleBlock(client, block);
|
|
384
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
330
385
|
return { ...planned, simulation: rejected, atomicAttempt: rejected, transactions: orderedTransactions(plan, mode, progress, prerequisites.approvals, prerequisites.nativeValue, planned.preparationBatchSize) };
|
|
331
386
|
}
|
|
332
387
|
let atomicAttempt: LifecycleSimulation = { ...placeholder, reason: "Atomic execution cannot be evaluated from an already begun staged launch" };
|