@black-market/sdk 0.14.1 → 0.15.1
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 +9 -7
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/auto-compound.d.ts +21 -0
- package/dist/lifecycle/auto-compound.d.ts.map +1 -0
- package/dist/lifecycle/auto-compound.js +34 -0
- package/dist/lifecycle/auto-compound.js.map +1 -0
- package/dist/lifecycle/calibration.d.ts +2 -4
- package/dist/lifecycle/calibration.d.ts.map +1 -1
- package/dist/lifecycle/calibration.js +1 -26
- package/dist/lifecycle/calibration.js.map +1 -1
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/gas-profile.d.ts +39 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +193 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- package/dist/lifecycle/index.d.ts +6 -4
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +6 -4
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +2 -0
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +30 -3
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/opening-buys.d.ts +13 -0
- package/dist/lifecycle/opening-buys.d.ts.map +1 -0
- package/dist/lifecycle/opening-buys.js +171 -0
- package/dist/lifecycle/opening-buys.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +6 -9
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +175 -265
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +10 -3
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +13 -1
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +21 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +25 -30
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +63 -81
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +2 -4
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +29 -6
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +62 -27
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +55 -122
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js +3 -3
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +4 -8
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +110 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +99 -47
- package/examples/.env.example +0 -3
- package/examples/README.md +14 -11
- package/examples/launch-example-support.ts +4 -9
- package/examples/launch-example.ts +30 -36
- package/examples/launch-lifecycle-smoke.ts +11 -21
- package/examples/launch.ts +8 -11
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/auto-compound.ts +38 -0
- package/src/lifecycle/calibration.ts +3 -23
- package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/gas-profile.ts +186 -0
- package/src/lifecycle/index.ts +6 -4
- package/src/lifecycle/markets.ts +25 -3
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +168 -247
- package/src/lifecycle/presets.ts +15 -1
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +58 -68
- package/src/lifecycle/snapshot.ts +2 -4
- package/src/lifecycle/transactions.ts +69 -27
- package/src/lifecycle/types.ts +60 -99
- package/src/lifecycle/ufg.ts +4 -8
- package/dist/lifecycle/fork.d.ts +0 -15
- package/dist/lifecycle/fork.d.ts.map +0 -1
- package/dist/lifecycle/fork.js +0 -36
- package/dist/lifecycle/fork.js.map +0 -1
- package/dist/lifecycle/simulation.d.ts +0 -18
- package/dist/lifecycle/simulation.d.ts.map +0 -1
- package/dist/lifecycle/simulation.js +0 -754
- package/dist/lifecycle/simulation.js.map +0 -1
- package/src/lifecycle/fork.ts +0 -36
- package/src/lifecycle/simulation.ts +0 -689
package/src/lifecycle/planner.ts
CHANGED
|
@@ -5,23 +5,53 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./ab
|
|
|
5
5
|
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1 } from "./schema.js";
|
|
6
6
|
import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
7
7
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
|
|
8
|
+
import { lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
9
|
+
import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecyclePlanningAssessment, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type ResolvedLifecycleLimits } from "./types.js";
|
|
10
|
+
import { deriveKnownPoolBoundHookDeployment, validateBuySlippageBps } from "./calibration.js";
|
|
12
11
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
13
|
-
import {
|
|
12
|
+
import { buildLifecycleCommand, packPreparationBatches, type LifecycleCommand } from "./transactions.js";
|
|
14
13
|
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
15
|
-
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
16
|
-
import {
|
|
14
|
+
import { assertUfgLaunchAccount, hashUfgLaunchConfig, ufgPlanPrincipal, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
15
|
+
import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
|
|
17
16
|
import { readLifecyclePlanningBlock } from "./snapshot.js";
|
|
18
17
|
import { mineLifecycleHooks } from "./mining.js";
|
|
18
|
+
import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
|
|
19
|
+
import { assessLifecycleTransactions, deriveLifecycleLaunchCapacity, type NextTransactionEnvelope, type ProfiledLaunchCapacity } from "./gas-profile.js";
|
|
20
|
+
import type { LifecycleOpeningBuyQuote } from "./types.js";
|
|
19
21
|
|
|
20
22
|
type ValidatedPlanShape = {
|
|
21
23
|
commitment: Hex;
|
|
22
24
|
ufgConfigHash?: Hex;
|
|
23
25
|
marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
|
|
24
26
|
};
|
|
27
|
+
type PlanReadContext = {
|
|
28
|
+
block?: LifecycleBlock;
|
|
29
|
+
openingBuys?: readonly LifecycleOpeningBuyQuote[];
|
|
30
|
+
};
|
|
31
|
+
const defaultPlanReadContext: PlanReadContext = {};
|
|
32
|
+
|
|
33
|
+
function observedFields<T extends object>(value: T): () => boolean {
|
|
34
|
+
const keys = Object.keys(value) as (keyof T)[];
|
|
35
|
+
const values = keys.map((key) => value[key]);
|
|
36
|
+
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function selectLaunchMode(requested: "atomic" | "staged", plan: LaunchPlanV1, capacity: ProfiledLaunchCapacity | undefined,
|
|
40
|
+
limits: ResolvedLifecycleLimits, client: LifecycleRpcClient, block: LifecycleBlock): Promise<"atomic" | "staged"> {
|
|
41
|
+
if (requested !== "atomic" || plan.markets.length !== 1 || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
|
|
42
|
+
capacity?.atomic === undefined || capacity.bounds.atomicComputeGasUpperBound === undefined) return "staged";
|
|
43
|
+
const poster = limits.protocol === "nitro" ? nitroPosterGasBound(capacity.atomic, await readNitroPrices(client, block)).posterGas : 0n;
|
|
44
|
+
return capacity.bounds.atomicComputeGasUpperBound + poster <= limits.executionGasCeiling ? "atomic" : "staged";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function preparationBatches(plan: LaunchPlanV1, prepared: number, ufg: PlanLaunchOptions["ufg"], limits: ResolvedLifecycleLimits,
|
|
48
|
+
client: LifecycleRpcClient, block: LifecycleBlock): Promise<number[]> {
|
|
49
|
+
if (prepared === plan.markets.length) return prepared === 0 ? [] : [prepared];
|
|
50
|
+
const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
|
|
51
|
+
const sample = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }, route);
|
|
52
|
+
const poster = limits.protocol === "nitro" ? nitroPosterGasBound(sample, await readNitroPrices(client, block)).posterGas : 0n;
|
|
53
|
+
return packPreparationBatches(plan, prepared, limits.executionGasCeiling - poster, limits.headroomBps);
|
|
54
|
+
}
|
|
25
55
|
|
|
26
56
|
function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
|
|
27
57
|
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
@@ -137,30 +167,38 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
|
|
|
137
167
|
return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"]): LifecycleTransaction[] {
|
|
170
|
+
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"], capacity?: ProfiledLaunchCapacity): LifecycleTransaction[] {
|
|
141
171
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
|
|
142
|
-
const prepared = progress.canonical.preparedMarkets;
|
|
143
|
-
const compiled
|
|
144
|
-
|
|
172
|
+
const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
|
|
173
|
+
const compiled: LifecycleTransaction[] = [];
|
|
174
|
+
if (mode === "atomic") compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
|
|
175
|
+
else {
|
|
176
|
+
compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
|
|
177
|
+
let first = 0;
|
|
178
|
+
for (const count of batches) { compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route)); first += count; }
|
|
179
|
+
compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
|
|
180
|
+
}
|
|
145
181
|
const commands = compiled.filter((transaction) =>
|
|
146
182
|
(transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
147
183
|
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
148
184
|
const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
|
|
149
|
-
return transactions.map((transaction, index) => ({ ...transaction,
|
|
185
|
+
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
150
186
|
}
|
|
151
187
|
|
|
152
|
-
function attachEstimates(transactions: readonly LifecycleTransaction[],
|
|
153
|
-
return transactions.map((transaction) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
function attachEstimates(transactions: readonly LifecycleTransaction[], assessment: LifecyclePlanningAssessment): LifecycleTransaction[] {
|
|
189
|
+
return transactions.map((transaction, index) => {
|
|
190
|
+
if (index !== 0) return transaction;
|
|
191
|
+
if (transaction.kind === "approve" || transaction.kind === "approve-reset") return { ...transaction, assessment };
|
|
192
|
+
const estimate = assessment.estimate;
|
|
193
|
+
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, assessment };
|
|
157
194
|
});
|
|
158
195
|
}
|
|
159
|
-
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"
|
|
196
|
+
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
|
|
160
197
|
const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
|
|
161
198
|
|
|
162
199
|
function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
|
|
163
200
|
if (transaction === undefined) return undefined;
|
|
201
|
+
const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
|
|
164
202
|
if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
|
|
165
203
|
!reviewedTransactionKinds.includes(transaction.kind) ||
|
|
166
204
|
!Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
|
|
@@ -168,9 +206,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
168
206
|
typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
|
|
169
207
|
typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
|
|
170
208
|
typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
|
|
171
|
-
typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
172
|
-
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n
|
|
173
|
-
transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
|
|
209
|
+
!walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
210
|
+
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
|
|
174
211
|
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");
|
|
175
212
|
}
|
|
176
213
|
return transaction as ReviewedNextTransaction;
|
|
@@ -179,23 +216,22 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
179
216
|
function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
|
|
180
217
|
return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
|
|
181
218
|
next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
|
|
182
|
-
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value
|
|
183
|
-
(reviewed.nonce === undefined || next.nonce === reviewed.nonce);
|
|
219
|
+
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
|
|
184
220
|
}
|
|
185
221
|
|
|
186
222
|
function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
|
|
187
223
|
if (options.planned.ufg !== undefined) assertUfgLaunchAccount(options.planned.plan, options.planned.account);
|
|
188
224
|
if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
|
|
189
|
-
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed
|
|
225
|
+
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
|
|
190
226
|
}
|
|
191
227
|
if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
|
|
192
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed
|
|
228
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
|
|
193
229
|
}
|
|
194
230
|
const current = readReviewedNextTransaction(options.reviewedTransaction);
|
|
195
|
-
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
|
|
196
|
-
|
|
197
|
-
next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
|
|
198
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed
|
|
231
|
+
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
|
|
232
|
+
reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
|
|
233
|
+
(current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
|
|
234
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
|
|
199
235
|
}
|
|
200
236
|
}
|
|
201
237
|
|
|
@@ -210,16 +246,6 @@ async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator:
|
|
|
210
246
|
return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
|
|
211
247
|
}
|
|
212
248
|
|
|
213
|
-
function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
|
|
214
|
-
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");
|
|
215
|
-
if (stored.ufg === undefined ? current.ufg !== undefined : current.ufg === undefined || hashUfgLaunchConfig(stored.ufg.config) !== stored.ufg.configHash || current.ufg.configHash !== stored.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored UFG route or treasury config no longer matches its finalized commitment");
|
|
216
|
-
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");
|
|
217
|
-
if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
|
|
218
|
-
if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
|
|
219
|
-
const fresh = current.hookDeployments[index];
|
|
220
|
-
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();
|
|
221
|
-
})) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
|
|
222
|
-
}
|
|
223
249
|
|
|
224
250
|
function assertPlanningActive(signal?: AbortSignal): void {
|
|
225
251
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
|
|
@@ -228,7 +254,7 @@ function assertPlanningActive(signal?: AbortSignal): void {
|
|
|
228
254
|
function validatePlanningIntent(options: PlanLaunchOptions): ValidatedPlanShape {
|
|
229
255
|
const { plan, account, mode } = options;
|
|
230
256
|
assertPlanningActive(options.signal);
|
|
231
|
-
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or
|
|
257
|
+
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
|
|
232
258
|
if (options.ufg !== undefined) assertUfgLaunchAccount(plan, account);
|
|
233
259
|
else if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
234
260
|
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");
|
|
@@ -272,27 +298,20 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
|
|
|
272
298
|
if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
|
|
273
299
|
}
|
|
274
300
|
|
|
275
|
-
|
|
276
|
-
|
|
301
|
+
|
|
302
|
+
/** One owner mines constructor-bound salts and protects ordered buys with exact
|
|
303
|
+
* local curve math. Launch calldata is never executed during planning. */
|
|
277
304
|
export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
|
|
278
305
|
const { commitment, ufgConfigHash } = validatePlanningIntent(options);
|
|
279
306
|
if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
|
|
280
|
-
const fixed = options.fixedFinalEnvelope;
|
|
281
|
-
if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
|
|
282
|
-
fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
|
|
283
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
|
|
284
|
-
}
|
|
285
|
-
const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
|
|
286
307
|
const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
|
|
287
308
|
const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
|
|
288
|
-
if (!needsMining && !needsProtection
|
|
309
|
+
if (!needsMining && !needsProtection) return planLaunch(options);
|
|
289
310
|
const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
290
311
|
const assertDraft = () => {
|
|
291
312
|
assertPlanningActive(options.signal);
|
|
292
313
|
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
|
|
293
314
|
if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
|
|
294
|
-
if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
|
|
295
|
-
fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
|
|
296
315
|
};
|
|
297
316
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
298
317
|
const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
|
|
@@ -314,31 +333,22 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
314
333
|
markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
|
|
315
334
|
}
|
|
316
335
|
assertDraft();
|
|
317
|
-
const executionBlock = block;
|
|
318
|
-
assertDraft();
|
|
319
|
-
const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
|
|
320
|
-
const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
|
|
321
|
-
const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
|
|
322
|
-
gas: heldEnvelope.gas,
|
|
323
|
-
gasPrice: heldEnvelope.gasPrice ?? (price! > baseFeeEnvelope ? price! : baseFeeEnvelope),
|
|
324
|
-
});
|
|
325
336
|
let plan: LaunchPlanV1 = { ...draft, markets };
|
|
326
|
-
let
|
|
327
|
-
let sizing: GasSizingHints | undefined;
|
|
337
|
+
let openingBuys: PlanReadContext["openingBuys"];
|
|
328
338
|
if (needsProtection) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
plan =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
339
|
+
if (!hasProfiledOpeningMath(plan)) throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Exact opening-buy math is unavailable for this profile. Use a supported profile or update the SDK.");
|
|
340
|
+
const head = await readLifecycleHeadProgress(client, plan, options.account, block);
|
|
341
|
+
if (head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
|
|
342
|
+
const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
|
|
343
|
+
openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
|
|
344
|
+
const factor = BigInt(10000 - options.buySlippageBps!);
|
|
345
|
+
plan = { ...plan, buys: plan.buys.map((buy, index) => {
|
|
346
|
+
const minTokenOut = openingBuys![index]!.tokenOut * factor / 10000n;
|
|
347
|
+
if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
|
|
348
|
+
return { ...buy, minTokenOut };
|
|
349
|
+
}) };
|
|
340
350
|
}
|
|
341
|
-
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client,
|
|
351
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
|
|
342
352
|
assertDraft();
|
|
343
353
|
assertFinalPlan(planned, options.signal);
|
|
344
354
|
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
@@ -347,7 +357,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
347
357
|
if (final === undefined || final.deployer.toLowerCase() !== mined.deployer.toLowerCase() || final.initCodeHash.toLowerCase() !== mined.initCodeHash.toLowerCase() || final.salt.toLowerCase() !== mined.salt.toLowerCase() || final.predictedHook.toLowerCase() !== mined.predictedHook.toLowerCase()) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final exact constructor binding differs from the one locally mined candidate");
|
|
348
358
|
}
|
|
349
359
|
}
|
|
350
|
-
if (needsProtection && !planned.
|
|
360
|
+
if (needsProtection && !planned.assessment.ready) throw new LifecyclePlanningError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "The launch could not be prepared. Refresh the review before continuing.", planned.assessment);
|
|
351
361
|
return planned;
|
|
352
362
|
});
|
|
353
363
|
assertDraft();
|
|
@@ -355,7 +365,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
355
365
|
return result;
|
|
356
366
|
}
|
|
357
367
|
|
|
358
|
-
/**
|
|
368
|
+
/** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
|
|
359
369
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
360
370
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
361
371
|
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
@@ -366,43 +376,35 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
366
376
|
return result;
|
|
367
377
|
}
|
|
368
378
|
|
|
369
|
-
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient,
|
|
370
|
-
const {
|
|
379
|
+
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, readContext: PlanReadContext = defaultPlanReadContext): Promise<PlannedLaunch> {
|
|
380
|
+
const { block: pinnedBlock, openingBuys } = readContext;
|
|
381
|
+
const { plan, account, ufg } = options;
|
|
371
382
|
const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
|
|
372
383
|
const launchId = hashLaunchIdentity(plan);
|
|
373
384
|
const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
374
385
|
pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
|
|
375
386
|
]));
|
|
376
387
|
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
377
|
-
// Only preparation/build-next can supply freshly observed progress from this
|
|
378
|
-
// still-open SDK invocation. Stored/query DTOs never enter this continuation.
|
|
379
|
-
if (ownedProgress !== undefined) {
|
|
380
|
-
if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
|
|
381
|
-
ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
|
|
382
|
-
ownedProgress.confirmationDepth !== (options.confirmations ?? 1)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
|
|
383
|
-
if (ownedProgress.planHash.toLowerCase() !== planHash.toLowerCase() || ownedProgress.launchId.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its canonical progress was read");
|
|
384
|
-
}
|
|
385
388
|
assertPlanningActive(options.signal);
|
|
386
389
|
const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
387
390
|
const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
|
|
388
391
|
preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
389
392
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
390
393
|
lifecycleStage(client, "plan.progress", async () => {
|
|
391
|
-
const [predictedToken] = await Promise.all([
|
|
392
|
-
readLifecycleTokenAtBlock(client, plan, block),
|
|
393
|
-
ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
|
|
394
|
+
const [predictedToken, head] = await Promise.all([
|
|
395
|
+
readLifecycleTokenAtBlock(client, plan, block), readLifecycleHeadProgress(client, plan, account, block),
|
|
394
396
|
]);
|
|
397
|
+
const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" as const : "atomic" as const;
|
|
395
398
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
|
|
396
399
|
...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
|
|
397
400
|
if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
|
|
398
|
-
const progress =
|
|
399
|
-
if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
|
|
401
|
+
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
|
|
400
402
|
return { identity, progress };
|
|
401
403
|
}),
|
|
402
404
|
]));
|
|
403
405
|
assertPlanningActive(options.signal);
|
|
404
|
-
const placeholder:
|
|
405
|
-
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress,
|
|
406
|
+
const placeholder: LifecyclePlanningAssessment = { ready: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits };
|
|
407
|
+
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, assessment: placeholder, limits: options.limits,
|
|
406
408
|
allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
|
|
407
409
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
408
410
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
@@ -413,194 +415,113 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
413
415
|
const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
|
|
414
416
|
hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
|
|
415
417
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
418
|
+
return { ...planned, hookDeployments, assessment: { ...placeholder,
|
|
419
|
+
code: !progress.confirmationSafe ? "UNCONFIRMED_STATE" : "RECEIPT_PENDING",
|
|
420
|
+
reason: !progress.confirmationSafe ? "Wait for the selected launch confirmation depth before continuing." : "Wait for the pending transaction to confirm or resolve its replacement." } };
|
|
419
421
|
}
|
|
420
422
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
421
|
-
|
|
422
|
-
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
423
|
+
return { ...planned, assessment: { ...placeholder, ready: true } };
|
|
423
424
|
}
|
|
424
|
-
// Topology is a creation policy, not a retroactive obstacle to recovery of
|
|
425
|
-
// immutable onchain commitments. Waiting and terminal launches returned above.
|
|
426
425
|
if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None) validatePlanTopology(options, marketConfigs, identity.predictedToken);
|
|
427
426
|
const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
428
427
|
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block, ufg === undefined ? plan.creator : account, ufg !== undefined) : { prerequisites: [], approvals: [] },
|
|
429
428
|
readPlanMetadata(client, plan, identity.predictedToken, block),
|
|
430
429
|
]));
|
|
431
430
|
assertFinalPlan(planned, options.signal);
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
431
|
+
const capacity = deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys);
|
|
432
|
+
const mode = progress.canonical.phase === LifecyclePhase.None
|
|
433
|
+
? await selectLaunchMode(options.mode, plan, capacity, limits, client, block) : identity.mode;
|
|
434
|
+
planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
|
|
435
|
+
if (metadata.reasons.length > 0) return { ...planned, assessment: { ...placeholder, code: "PROFILE_UNAVAILABLE", failureCategory: "semantic",
|
|
436
|
+
reason: "The selected launch profile could not be read or is unavailable. Refresh the review before continuing." } };
|
|
437
|
+
let batches: number[];
|
|
438
|
+
try { batches = mode === "staged" ? await preparationBatches(plan, progress.canonical.preparedMarkets, ufg, limits, client, block) : []; }
|
|
439
|
+
catch (failure) {
|
|
440
|
+
if (!(failure instanceof LifecyclePlanningError) || failure.code !== "PREPARATION_GAS_LIMIT") throw failure;
|
|
441
|
+
return { ...planned, assessment: { ...placeholder, code: failure.code, reason: failure.message, failureCategory: "capacity", capacityConstraint: "preparation" } };
|
|
444
442
|
}
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let simulation = await execute(transactions);
|
|
450
|
-
assertFinalPlan(planned, options.signal);
|
|
451
|
-
if (mode === "staged") {
|
|
452
|
-
// Only pressure at the native ceiling can invalidate packing. A smaller
|
|
453
|
-
// affordability/RPC/account envelope is not evidence for another wallet write.
|
|
454
|
-
while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
|
|
455
|
-
const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
|
|
456
|
-
const failedTransaction = transactions[failedIndex];
|
|
457
|
-
const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
|
|
458
|
-
const count = failedTransaction?.marketCount ?? 0;
|
|
459
|
-
if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
|
|
460
|
-
let first = 0;
|
|
461
|
-
const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
|
|
462
|
-
const left = Math.ceil(count / 2);
|
|
463
|
-
batches.splice(batchIndex, 1, left, count - left);
|
|
464
|
-
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
|
|
465
|
-
simulation = await execute(transactions);
|
|
466
|
-
assertFinalPlan(planned, options.signal);
|
|
467
|
-
}
|
|
468
|
-
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` };
|
|
469
|
-
} else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
|
|
470
|
-
simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
|
|
471
|
-
}
|
|
472
|
-
return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
473
|
-
} finally { preparedSimulation.close(); }
|
|
443
|
+
const transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
444
|
+
const assessment = await assessLifecycleTransactions({ client, planned, limits: options.limits, signal: options.signal }, transactions, block, undefined, openingBuys, capacity);
|
|
445
|
+
assertFinalPlan(planned, options.signal);
|
|
446
|
+
return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
474
447
|
}
|
|
475
448
|
|
|
476
|
-
|
|
477
|
-
if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
|
|
478
|
-
simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
|
|
479
|
-
next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
|
|
480
|
-
simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
|
|
481
|
-
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
|
|
482
|
-
}
|
|
483
|
-
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
484
|
-
const submissionClient = options.submissionClient;
|
|
485
|
-
assertPlanningActive(options.signal);
|
|
486
|
-
if (submissionClient === undefined) return { ...next, dependencies: [] };
|
|
487
|
-
try {
|
|
488
|
-
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
489
|
-
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
490
|
-
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
491
|
-
...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
|
|
492
|
-
}, "latest"]), "submission gas estimate");
|
|
493
|
-
if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
494
|
-
assertPlanningActive(options.signal);
|
|
495
|
-
} catch (failure) {
|
|
496
|
-
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
497
|
-
const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
|
|
498
|
-
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
499
|
-
}
|
|
500
|
-
if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
449
|
+
function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
|
|
501
450
|
assertPlanningActive(options.signal);
|
|
502
451
|
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
503
|
-
return { ...next, dependencies: []
|
|
452
|
+
return { ...next, dependencies: [] };
|
|
504
453
|
}
|
|
505
454
|
|
|
506
|
-
/** Recovery observes
|
|
507
|
-
*
|
|
455
|
+
/** Recovery observes actual canonical receipts, then constructs and budgets only
|
|
456
|
+
* the next command. No launch transaction is executed during planning. */
|
|
508
457
|
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
458
|
+
const original = options.planned, matches = observedFields(original);
|
|
459
|
+
const assertInput = () => {
|
|
460
|
+
if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
|
|
461
|
+
assertFinalPlan(original, options.signal);
|
|
462
|
+
};
|
|
509
463
|
const supplied = readReviewedNextTransaction(options.reviewedTransaction);
|
|
510
|
-
|
|
511
|
-
// reused, and final checks compare the mutable input with this one local copy.
|
|
464
|
+
const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
|
|
512
465
|
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
513
|
-
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice
|
|
466
|
+
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
|
|
467
|
+
const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
|
|
514
468
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
await Promise.all([
|
|
519
|
-
readLifecycleTokenAtBlock(client, options.planned.plan, block),
|
|
520
|
-
readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
|
|
521
|
-
resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
|
|
522
|
-
]);
|
|
523
|
-
}
|
|
469
|
+
registerLifecycleFinalCheck(client, assertInput);
|
|
470
|
+
const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
|
|
471
|
+
const limitSource = options.limits === undefined ? original.limits : options.limits;
|
|
524
472
|
const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
|
|
525
|
-
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// remain in force for every nonterminal case.
|
|
529
|
-
|
|
530
|
-
return undefined;
|
|
531
|
-
}
|
|
532
|
-
if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for confirmed canonical progress and account nonce, including untracked approvals/replacements, before constructing the next transaction");
|
|
533
|
-
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");
|
|
473
|
+
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return undefined;
|
|
474
|
+
if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected launch confirmation depth before continuing.");
|
|
475
|
+
if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed")) throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for the pending transaction to confirm or resolve its replacement.");
|
|
534
476
|
assertPlanningActive(options.signal);
|
|
477
|
+
const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
|
|
535
478
|
if (options.action === "cancel") {
|
|
536
479
|
if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
537
|
-
const {
|
|
538
|
-
assertFinalPlan(planned, options.signal);
|
|
480
|
+
const planned = { ...original, progress };
|
|
539
481
|
const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
|
|
540
482
|
: { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
|
|
541
|
-
transaction
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const next = attachEstimates([transaction], simulation)[0];
|
|
546
|
-
if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
547
|
-
return preflightNextTransaction(options, next, simulation, reviewed);
|
|
483
|
+
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current cancellation");
|
|
484
|
+
const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [transaction], block, envelope);
|
|
485
|
+
if (!assessment.ready) throw new LifecyclePlanningError(assessment.code ?? "CANCEL_NOT_READY", assessment.reason ?? "Cancellation could not be prepared. Refresh before continuing.", assessment);
|
|
486
|
+
return finalizeNextTransaction(options, attachEstimates([transaction], assessment)[0]!, reviewed);
|
|
548
487
|
}
|
|
549
|
-
const { planned } = options;
|
|
550
|
-
assertFinalPlan(planned, options.signal);
|
|
551
488
|
const funding = progress.canonical.phase === LifecyclePhase.None
|
|
552
|
-
? await readFunding(client,
|
|
489
|
+
? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
|
|
553
490
|
: { prerequisites: [], approvals: [] };
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
491
|
+
const planned = { ...original, progress, prerequisites: funding.prerequisites };
|
|
492
|
+
const capacity = deriveLifecycleLaunchCapacity({ plan: planned.plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps });
|
|
493
|
+
if (progress.canonical.phase === LifecyclePhase.None &&
|
|
494
|
+
await selectLaunchMode(planned.mode, planned.plan, capacity, limits, client, block) !== planned.mode) {
|
|
495
|
+
throw new LifecyclePlanningError("MODE_REVIEW_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.");
|
|
496
|
+
}
|
|
497
|
+
let candidate = funding.approvals[0];
|
|
498
|
+
if (candidate === undefined) {
|
|
499
|
+
const prepared = progress.canonical.preparedMarkets;
|
|
500
|
+
let command: LifecycleCommand;
|
|
501
|
+
if (progress.canonical.phase === LifecyclePhase.None) command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
|
|
502
|
+
else if (prepared < planned.plan.markets.length) {
|
|
503
|
+
let count: number;
|
|
504
|
+
if (reviewed?.kind === "prepare") {
|
|
505
|
+
const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
506
|
+
if (Number(span?.[1]) !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
|
|
507
|
+
count = Number(span?.[2]);
|
|
508
|
+
} else {
|
|
509
|
+
const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
|
|
510
|
+
count = batches[prepared > 0 ? 1 : 0]!;
|
|
511
|
+
}
|
|
512
|
+
if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must cover a positive contiguous unfinished market span");
|
|
513
|
+
command = { kind: "prepare", first: prepared, count };
|
|
514
|
+
} else command = { kind: "activate" };
|
|
515
|
+
candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
|
|
575
516
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
579
|
-
const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
|
|
580
|
-
const compute = observed?.computeGasRequired;
|
|
581
|
-
const sizing: GasSizingHints | undefined = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
|
|
582
|
-
const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
|
|
517
|
+
if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current next step");
|
|
518
|
+
const assessment = await assessLifecycleTransactions({ client, planned, limits: limitSource, signal: options.signal }, [candidate], block, envelope, undefined, capacity);
|
|
583
519
|
assertFinalPlan(planned, options.signal);
|
|
584
|
-
if (!
|
|
585
|
-
|
|
586
|
-
return preflightNextTransaction(options, next, simulation, reviewed);
|
|
520
|
+
if (!assessment.ready) throw new LifecyclePlanningError(assessment.code ?? "PLAN_NOT_READY", assessment.reason ?? "The next launch step could not be prepared. Refresh before continuing.", assessment);
|
|
521
|
+
return finalizeNextTransaction(options, attachEstimates([candidate], assessment)[0]!, reviewed);
|
|
587
522
|
});
|
|
588
|
-
|
|
523
|
+
assertInput();
|
|
589
524
|
if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
|
|
590
525
|
return result;
|
|
591
526
|
}
|
|
592
527
|
|
|
593
|
-
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
594
|
-
* evidence and confirmation depth as build-next, never silently dropping context. */
|
|
595
|
-
export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
|
|
596
|
-
const { client, planned } = options;
|
|
597
|
-
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");
|
|
598
|
-
const current = await planLaunch({
|
|
599
|
-
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
600
|
-
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
601
|
-
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
|
|
602
|
-
ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
|
|
603
|
-
});
|
|
604
|
-
assertPlannedDeploymentBindings(planned, current);
|
|
605
|
-
return current.simulation;
|
|
606
|
-
}
|