@black-market/sdk 0.15.0 → 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 -8
- 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 +17 -13
- package/dist/lifecycle/gas-profile.d.ts.map +1 -1
- package/dist/lifecycle/gas-profile.js +125 -115
- package/dist/lifecycle/gas-profile.js.map +1 -1
- package/dist/lifecycle/index.d.ts +4 -4
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -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.js +1 -1
- package/dist/lifecycle/opening-buys.js.map +1 -1
- package/dist/lifecycle/planner.d.ts +6 -9
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +102 -207
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +7 -3
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +10 -1
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +3 -13
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +11 -37
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +1 -2
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +10 -3
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +40 -10
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +32 -107
- 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 +1 -1
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +43 -0
- package/docs/lifecycle-gas-profiles.md +1 -1
- package/docs/reference.md +56 -2
- package/examples/.env.example +0 -3
- package/examples/README.md +10 -10
- package/examples/launch-example-support.ts +4 -9
- package/examples/launch-example.ts +12 -28
- package/examples/launch-lifecycle-smoke.ts +10 -22
- package/examples/launch.ts +8 -11
- package/package.json +1 -1
- 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 +117 -106
- package/src/lifecycle/index.ts +4 -4
- package/src/lifecycle/markets.ts +25 -3
- package/src/lifecycle/opening-buys.ts +1 -1
- package/src/lifecycle/planner.ts +98 -190
- package/src/lifecycle/presets.ts +10 -1
- package/src/lifecycle/rpc.ts +10 -32
- package/src/lifecycle/snapshot.ts +1 -2
- package/src/lifecycle/transactions.ts +39 -9
- package/src/lifecycle/types.ts +38 -87
- package/src/lifecycle/ufg.ts +1 -1
- 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 -17
- package/dist/lifecycle/simulation.d.ts.map +0 -1
- package/dist/lifecycle/simulation.js +0 -605
- package/dist/lifecycle/simulation.js.map +0 -1
- package/src/lifecycle/fork.ts +0 -36
- package/src/lifecycle/simulation.ts +0 -543
package/src/lifecycle/markets.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { MarketConfigV1 } from "./schema.js";
|
|
|
3
3
|
import { LifecyclePlanningError, type LifecycleProfile, type PoolBoundHookSaltProgress } from "./types.js";
|
|
4
4
|
import { decodeHmmLaunchTail, encodeHmmLaunchTail, isHmmProfile, type HmmLaunchConfig } from "./hmm.js";
|
|
5
5
|
import { decodeUfgLaunchConfig, encodeUfgLaunchConfig, isUfgProfile, type UfgLaunchConfig } from "./ufg-config.js";
|
|
6
|
+
import { decodeAutoCompoundTail, encodeAutoCompoundTail, isAutoCompoundProfile, validateAutoCompoundConfig, type AutoCompoundConfigV1 } from "./auto-compound.js";
|
|
6
7
|
|
|
7
8
|
export type V4LifecyclePositionConfig = {
|
|
8
9
|
tickLower: number; tickUpper: number; liquidity: bigint; salt: Hex; maxTokenAmount: bigint;
|
|
@@ -46,6 +47,7 @@ export type V4PoolBoundLifecycleMarketConfigV6 = Omit<V4LifecycleMarketConfig, "
|
|
|
46
47
|
version: 6; minimumHookFeePips: number; feeSensitivityPipsSecondsPerTick: number; hookSalt: Hex;
|
|
47
48
|
hmmLaunchConfig?: HmmLaunchConfig;
|
|
48
49
|
ufgLaunchConfig?: UfgLaunchConfig;
|
|
50
|
+
autoCompoundConfig?: AutoCompoundConfigV1;
|
|
49
51
|
};
|
|
50
52
|
export type V4PoolBoundLifecycleMarketConfig = V4PoolBoundLifecycleMarketConfigV5 | V4PoolBoundLifecycleMarketConfigV6;
|
|
51
53
|
export const V4_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint8,uint8,address,bool,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
|
|
@@ -92,9 +94,22 @@ function assertV1HookFeeShape(config: object): void {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function validateAutoCompoundMarket(config: V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig): asserts config is V4PoolBoundLifecycleMarketConfigV6 & { autoCompoundConfig: AutoCompoundConfigV1 } {
|
|
98
|
+
if (config.version !== 6 || config.autoCompoundConfig === undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles require config6 and an explicit autoCompoundConfig");
|
|
99
|
+
if (config.hmmLaunchConfig !== undefined || config.ufgLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles cannot carry HMM or UFG tails");
|
|
100
|
+
if (config.lpFeePips !== 0) throw new LifecyclePlanningError("INVALID_HOOK_FEES", "Auto-compounding profiles require zero LP fee");
|
|
101
|
+
if (config.developerFeeBps !== 0) throw new LifecyclePlanningError("DEVELOPER_FEE_CEILING", "Auto-compounding profiles require zero developer fee");
|
|
102
|
+
validateAutoCompoundConfig(config.autoCompoundConfig);
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
/** Auxiliary launch profiles require their typed tail and append it automatically. */
|
|
96
106
|
export function encodePoolBoundV4LifecycleMarketConfig(config: V4PoolBoundLifecycleMarketConfig): Hex {
|
|
97
107
|
const prefix = encodePoolBoundV4Prefix(config);
|
|
108
|
+
if (isAutoCompoundProfile(config.profileId)) {
|
|
109
|
+
validateAutoCompoundMarket(config);
|
|
110
|
+
return concatHex([prefix, encodeAutoCompoundTail(config.autoCompoundConfig)]);
|
|
111
|
+
}
|
|
112
|
+
if (config.version === 6 && config.autoCompoundConfig !== undefined) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "An auto-compounding tail requires an auto-compounding profile");
|
|
98
113
|
if (isUfgProfile(config.profileId)) {
|
|
99
114
|
if (config.version !== 6 || config.ufgLaunchConfig === undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles require config6 and an explicit ufgLaunchConfig");
|
|
100
115
|
if (config.hmmLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles cannot carry an HMM tail");
|
|
@@ -130,6 +145,10 @@ export function decodePoolBoundV4LifecycleMarketConfig(encoded: Hex, expectedVer
|
|
|
130
145
|
const config = version === 5
|
|
131
146
|
? decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV5 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV5
|
|
132
147
|
: decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV6 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV6;
|
|
148
|
+
if (isAutoCompoundProfile(config.profileId)) {
|
|
149
|
+
if (config.version !== 6) throw new LifecyclePlanningError("INVALID_AUTO_COMPOUND_CONFIG", "Auto-compounding profiles require config6");
|
|
150
|
+
config.autoCompoundConfig = decodeAutoCompoundTail(`0x${encoded.slice(encodePoolBoundV4Prefix(config).length)}`);
|
|
151
|
+
}
|
|
133
152
|
if (isHmmProfile(config.profileId)) {
|
|
134
153
|
if (config.version !== 6) throw new LifecyclePlanningError("INVALID_HMM_CONFIG", "HMM profiles require config6");
|
|
135
154
|
const prefixLength = encodePoolBoundV4Prefix(config).length;
|
|
@@ -160,6 +179,7 @@ export function validateV4LifecycleMarket(options: {
|
|
|
160
179
|
if (config.version !== 4 && !isPoolBoundV4ConfigVersion(config.version)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Reviewed V4 config version must be 4, 5 or 6");
|
|
161
180
|
if (config.version === 6) validatePoolBoundHookFees(config);
|
|
162
181
|
else if (config.version === 5) assertV1HookFeeShape(config);
|
|
182
|
+
if (isAutoCompoundProfile(config.profileId)) validateAutoCompoundMarket(config);
|
|
163
183
|
const { envelope, developerTerms: terms, protocolMaximumDeveloperFeeBps: protocolCeiling } = profile;
|
|
164
184
|
if (envelope === undefined || terms === undefined || protocolCeiling === undefined || !terms.enabled ||
|
|
165
185
|
profile.topology.hookTopology !== (config.version === 4 ? 1 : 2) || market.configVersion !== config.version ||
|
|
@@ -258,9 +278,11 @@ export function hashPoolBoundV4MarketCommitment(options: { chainId: bigint; core
|
|
|
258
278
|
if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound V4 market requires explicit config version 5 or 6");
|
|
259
279
|
const config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
|
|
260
280
|
if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Config profile differs from the committed market profile");
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
const
|
|
281
|
+
// Auto-compounding binds its tail into constructor economics; older factories
|
|
282
|
+
// bind only the prefix. Every full plan commitment also binds every tail byte.
|
|
283
|
+
const normalized = { ...config, hookSalt: zeroHash };
|
|
284
|
+
const configHash = keccak256(isAutoCompoundProfile(config.profileId)
|
|
285
|
+
? encodePoolBoundV4LifecycleMarketConfig(normalized) : encodePoolBoundV4Prefix(normalized));
|
|
264
286
|
return keccak256(encodeAbiParameters([
|
|
265
287
|
{ type: "bytes32" }, { type: "uint256" }, { type: "address" }, { type: "address" }, { type: "address" },
|
|
266
288
|
{ type: "bytes32" }, { type: "bytes32" }, { type: "address" }, { type: "uint256" }, { type: "uint32" }, { type: "bytes32" },
|
|
@@ -52,7 +52,7 @@ function openingState(plan: LaunchPlanV1, marketIndex: number): OpeningState {
|
|
|
52
52
|
const config = decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
|
|
53
53
|
if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_MARKET", "V4 opening config differs from the selected profile");
|
|
54
54
|
price = config.sqrtPriceX96; spacing = config.tickSpacing; lpFee = BigInt(config.lpFeePips); positions = config.positions;
|
|
55
|
-
const dynamic = preset.key === KnownLifecycleProfile.V4DynamicFeePool || preset.key === KnownLifecycleProfile.V4HmmDynamicPool || preset.key === KnownLifecycleProfile.V4UfgDynamicPool;
|
|
55
|
+
const dynamic = preset.key === KnownLifecycleProfile.V4DynamicFeePool || preset.key === KnownLifecycleProfile.V4HmmDynamicPool || preset.key === KnownLifecycleProfile.V4UfgDynamicPool || preset.key === KnownLifecycleProfile.V4AutoCompoundDynamicPool;
|
|
56
56
|
hookFee = BigInt(dynamic ? config.minimumHookFeePips! : config.hookFeePips);
|
|
57
57
|
}
|
|
58
58
|
const tick = getTickAtSqrtRatio(price), liquidityNet = new Map<number, bigint>();
|
package/src/lifecycle/planner.ts
CHANGED
|
@@ -5,10 +5,9 @@ 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
12
|
import { buildLifecycleCommand, packPreparationBatches, type LifecycleCommand } from "./transactions.js";
|
|
14
13
|
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
@@ -17,7 +16,7 @@ import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
|
|
|
17
16
|
import { readLifecyclePlanningBlock } from "./snapshot.js";
|
|
18
17
|
import { mineLifecycleHooks } from "./mining.js";
|
|
19
18
|
import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
|
|
20
|
-
import { deriveLifecycleLaunchCapacity,
|
|
19
|
+
import { assessLifecycleTransactions, deriveLifecycleLaunchCapacity, type NextTransactionEnvelope, type ProfiledLaunchCapacity } from "./gas-profile.js";
|
|
21
20
|
import type { LifecycleOpeningBuyQuote } from "./types.js";
|
|
22
21
|
|
|
23
22
|
type ValidatedPlanShape = {
|
|
@@ -26,12 +25,34 @@ type ValidatedPlanShape = {
|
|
|
26
25
|
marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
|
|
27
26
|
};
|
|
28
27
|
type PlanReadContext = {
|
|
29
|
-
block?: LifecycleBlock;
|
|
30
|
-
|
|
31
|
-
openingBuys?: readonly LifecycleOpeningBuyQuote[]; forceSimulation?: boolean; immediate?: boolean;
|
|
28
|
+
block?: LifecycleBlock;
|
|
29
|
+
openingBuys?: readonly LifecycleOpeningBuyQuote[];
|
|
32
30
|
};
|
|
33
31
|
const defaultPlanReadContext: PlanReadContext = {};
|
|
34
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
|
+
}
|
|
55
|
+
|
|
35
56
|
function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
|
|
36
57
|
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
37
58
|
const commitment = hashLaunchPlan(plan);
|
|
@@ -164,14 +185,12 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
|
|
|
164
185
|
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
165
186
|
}
|
|
166
187
|
|
|
167
|
-
function attachEstimates(transactions: readonly LifecycleTransaction[],
|
|
188
|
+
function attachEstimates(transactions: readonly LifecycleTransaction[], assessment: LifecyclePlanningAssessment): LifecycleTransaction[] {
|
|
168
189
|
return transactions.map((transaction, index) => {
|
|
169
190
|
if (index !== 0) return transaction;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate ?? simulation.estimate;
|
|
174
|
-
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission };
|
|
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 };
|
|
175
194
|
});
|
|
176
195
|
}
|
|
177
196
|
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
|
|
@@ -203,16 +222,16 @@ function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNex
|
|
|
203
222
|
function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
|
|
204
223
|
if (options.planned.ufg !== undefined) assertUfgLaunchAccount(options.planned.plan, options.planned.account);
|
|
205
224
|
if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
|
|
206
|
-
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed
|
|
225
|
+
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed while preparing the reviewed transaction");
|
|
207
226
|
}
|
|
208
227
|
if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
|
|
209
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed
|
|
228
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed while preparing the reviewed transaction");
|
|
210
229
|
}
|
|
211
230
|
const current = readReviewedNextTransaction(options.reviewedTransaction);
|
|
212
231
|
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
|
|
213
232
|
reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
|
|
214
233
|
(current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
|
|
215
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed
|
|
234
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed while preparing it");
|
|
216
235
|
}
|
|
217
236
|
}
|
|
218
237
|
|
|
@@ -227,16 +246,6 @@ async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator:
|
|
|
227
246
|
return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
|
|
228
247
|
}
|
|
229
248
|
|
|
230
|
-
function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
|
|
231
|
-
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");
|
|
232
|
-
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");
|
|
233
|
-
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");
|
|
234
|
-
if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
|
|
235
|
-
if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
|
|
236
|
-
const fresh = current.hookDeployments[index];
|
|
237
|
-
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();
|
|
238
|
-
})) throw new LifecyclePlanningError("HOOK_DEPLOYMENT_CHANGED", "Final bound-hook deployer, initcode, salt or prediction changed before wallet execution");
|
|
239
|
-
}
|
|
240
249
|
|
|
241
250
|
function assertPlanningActive(signal?: AbortSignal): void {
|
|
242
251
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle launch planning cancelled"), { name: "AbortError" });
|
|
@@ -289,18 +298,9 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
|
|
|
289
298
|
if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
|
|
290
299
|
}
|
|
291
300
|
|
|
292
|
-
async function evaluateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], block: LifecycleBlock, controls: PlanReadContext = defaultPlanReadContext): Promise<LifecycleSimulation> {
|
|
293
|
-
const profile = controls.diagnostic ? undefined : await profileLifecycleTransactions(options, transactions, block, controls.finalEnvelope, controls.openingBuys, controls.capacity);
|
|
294
|
-
if (!controls.forceSimulation && profile?.profileOnly) return profile.simulation;
|
|
295
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, controls.immediate);
|
|
296
|
-
try {
|
|
297
|
-
const simulation = controls.diagnostic ? await prepared.measure(transactions) : await prepared(transactions, controls.finalEnvelope);
|
|
298
|
-
return profile === undefined ? simulation : { ...simulation, gasProfile: profile.simulation.gasProfile };
|
|
299
|
-
} finally { prepared.close(); }
|
|
300
|
-
}
|
|
301
301
|
|
|
302
|
-
/** One owner
|
|
303
|
-
*
|
|
302
|
+
/** One owner mines constructor-bound salts and protects ordered buys with exact
|
|
303
|
+
* local curve math. Launch calldata is never executed during planning. */
|
|
304
304
|
export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
|
|
305
305
|
const { commitment, ufgConfigHash } = validatePlanningIntent(options);
|
|
306
306
|
if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
|
|
@@ -334,9 +334,11 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
334
334
|
}
|
|
335
335
|
assertDraft();
|
|
336
336
|
let plan: LaunchPlanV1 = { ...draft, markets };
|
|
337
|
-
let ownedProgress: CanonicalLaunchProgress | undefined;
|
|
338
337
|
let openingBuys: PlanReadContext["openingBuys"];
|
|
339
|
-
if (needsProtection
|
|
338
|
+
if (needsProtection) {
|
|
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");
|
|
340
342
|
const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
|
|
341
343
|
openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
|
|
342
344
|
const factor = BigInt(10000 - options.buySlippageBps!);
|
|
@@ -345,19 +347,8 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
345
347
|
if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
|
|
346
348
|
return { ...buy, minTokenOut };
|
|
347
349
|
}) };
|
|
348
|
-
} else if (needsProtection) {
|
|
349
|
-
// A diagnostic minimum is never returned as an admitted or executable plan.
|
|
350
|
-
// Price guards, funding minima, recipients and all ordered budgets stay exact.
|
|
351
|
-
const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
|
|
352
|
-
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
|
|
353
|
-
assertDraft();
|
|
354
|
-
if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
|
|
355
|
-
plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
|
|
356
|
-
// This is the real SDK-owned None observation, not an assumed new-launch
|
|
357
|
-
// state. No onchain commitment existed at this exact snapshot to replace.
|
|
358
|
-
ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
|
|
359
350
|
}
|
|
360
|
-
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block,
|
|
351
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, openingBuys }));
|
|
361
352
|
assertDraft();
|
|
362
353
|
assertFinalPlan(planned, options.signal);
|
|
363
354
|
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
@@ -366,7 +357,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
366
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");
|
|
367
358
|
}
|
|
368
359
|
}
|
|
369
|
-
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);
|
|
370
361
|
return planned;
|
|
371
362
|
});
|
|
372
363
|
assertDraft();
|
|
@@ -374,7 +365,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
374
365
|
return result;
|
|
375
366
|
}
|
|
376
367
|
|
|
377
|
-
/**
|
|
368
|
+
/** Fresh pinned input and deterministic gas-budget checks. No simulation or broadcasts. */
|
|
378
369
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
379
370
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
380
371
|
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
@@ -386,44 +377,34 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
386
377
|
}
|
|
387
378
|
|
|
388
379
|
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, readContext: PlanReadContext = defaultPlanReadContext): Promise<PlannedLaunch> {
|
|
389
|
-
const { block: pinnedBlock,
|
|
390
|
-
const { plan, account,
|
|
391
|
-
let mode = requestedMode;
|
|
380
|
+
const { block: pinnedBlock, openingBuys } = readContext;
|
|
381
|
+
const { plan, account, ufg } = options;
|
|
392
382
|
const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
|
|
393
383
|
const launchId = hashLaunchIdentity(plan);
|
|
394
384
|
const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
395
385
|
pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
|
|
396
386
|
]));
|
|
397
387
|
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
398
|
-
// Only preparation/build-next can supply freshly observed progress from this
|
|
399
|
-
// still-open SDK invocation. Stored/query DTOs never enter this continuation.
|
|
400
|
-
if (ownedProgress !== undefined) {
|
|
401
|
-
if (!assertLifecycleReadClientOpen(client) || pinnedBlock !== block || ownedProgress.blockNumber !== block.number ||
|
|
402
|
-
ownedProgress.blockHash.toLowerCase() !== block.hash.toLowerCase() || ownedProgress.chainId !== plan.chainId ||
|
|
403
|
-
ownedProgress.confirmationDepth !== (options.confirmations ?? 1)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Owned progress differs from its active planning snapshot");
|
|
404
|
-
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");
|
|
405
|
-
}
|
|
406
388
|
assertPlanningActive(options.signal);
|
|
407
389
|
const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
408
390
|
const [{ tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
|
|
409
391
|
preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
410
392
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
411
393
|
lifecycleStage(client, "plan.progress", async () => {
|
|
412
|
-
const [predictedToken] = await Promise.all([
|
|
413
|
-
readLifecycleTokenAtBlock(client, plan, block),
|
|
414
|
-
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),
|
|
415
396
|
]);
|
|
397
|
+
const mode = head.phase === LifecyclePhase.None ? options.mode : head.mode === 1 ? "staged" as const : "atomic" as const;
|
|
416
398
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
|
|
417
399
|
...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
|
|
418
400
|
if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
|
|
419
|
-
const progress =
|
|
420
|
-
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);
|
|
421
402
|
return { identity, progress };
|
|
422
403
|
}),
|
|
423
404
|
]));
|
|
424
405
|
assertPlanningActive(options.signal);
|
|
425
|
-
const placeholder:
|
|
426
|
-
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,
|
|
427
408
|
allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
|
|
428
409
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
429
410
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
@@ -434,72 +415,35 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
434
415
|
const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
|
|
435
416
|
hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
|
|
436
417
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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." } };
|
|
440
421
|
}
|
|
441
422
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
442
|
-
|
|
443
|
-
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 } };
|
|
444
424
|
}
|
|
445
|
-
// Topology is a creation policy, not a retroactive obstacle to recovery of
|
|
446
|
-
// immutable onchain commitments. Waiting and terminal launches returned above.
|
|
447
425
|
if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None) validatePlanTopology(options, marketConfigs, identity.predictedToken);
|
|
448
426
|
const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
449
427
|
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block, ufg === undefined ? plan.creator : account, ufg !== undefined) : { prerequisites: [], approvals: [] },
|
|
450
428
|
readPlanMetadata(client, plan, identity.predictedToken, block),
|
|
451
429
|
]));
|
|
452
430
|
assertFinalPlan(planned, options.signal);
|
|
453
|
-
const capacity =
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const prices = await readNitroPrices(client, block);
|
|
457
|
-
if (capacity.bounds.atomicComputeGasUpperBound + nitroPosterGasBound(capacity.atomic, prices).posterGas > limits.executionGasCeiling) mode = "staged";
|
|
458
|
-
}
|
|
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;
|
|
459
434
|
planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
let
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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" } };
|
|
466
442
|
}
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
{ diagnostic, openingBuys: quotes, capacity, forceSimulation });
|
|
470
|
-
let simulation = await execute(transactions);
|
|
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);
|
|
471
445
|
assertFinalPlan(planned, options.signal);
|
|
472
|
-
|
|
473
|
-
simulation.executionProof === "failed" && simulation.failureCategory === "capacity" && simulation.capacityConstraint === "compute") {
|
|
474
|
-
mode = "staged"; planned = { ...planned, mode };
|
|
475
|
-
batches = packPreparationBatches(plan, prepared, computeCap);
|
|
476
|
-
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
477
|
-
simulation = await execute(transactions);
|
|
478
|
-
assertFinalPlan(planned, options.signal);
|
|
479
|
-
}
|
|
480
|
-
if (mode === "staged") {
|
|
481
|
-
// Only pressure at the native ceiling can invalidate packing. A smaller
|
|
482
|
-
// affordability/RPC/account envelope is not evidence for another wallet write.
|
|
483
|
-
while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
|
|
484
|
-
const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
|
|
485
|
-
const failedTransaction = transactions[failedIndex];
|
|
486
|
-
const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
|
|
487
|
-
const count = failedTransaction?.marketCount ?? 0;
|
|
488
|
-
if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
|
|
489
|
-
let first = 0;
|
|
490
|
-
const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
|
|
491
|
-
const left = Math.ceil(count / 2);
|
|
492
|
-
batches.splice(batchIndex, 1, left, count - left);
|
|
493
|
-
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
494
|
-
simulation = await execute(transactions, simulation.gasProfile?.initialBuys);
|
|
495
|
-
assertFinalPlan(planned, options.signal);
|
|
496
|
-
}
|
|
497
|
-
if (!simulation.admitted && simulation.failedTransactionId === "activate" && simulation.failureCategory === "capacity") simulation = {
|
|
498
|
-
...simulation, capacityConstraint: "activation", reason: "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." };
|
|
499
|
-
} else if (!diagnostic && !simulation.admitted) {
|
|
500
|
-
simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
|
|
501
|
-
}
|
|
502
|
-
return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
446
|
+
return { ...planned, assessment, transactions: attachEstimates(transactions, assessment), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
503
447
|
}
|
|
504
448
|
|
|
505
449
|
function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
|
|
@@ -508,63 +452,48 @@ function finalizeNextTransaction(options: BuildNextTransactionOptions, next: Lif
|
|
|
508
452
|
return { ...next, dependencies: [] };
|
|
509
453
|
}
|
|
510
454
|
|
|
511
|
-
/** Recovery observes
|
|
512
|
-
*
|
|
455
|
+
/** Recovery observes actual canonical receipts, then constructs and budgets only
|
|
456
|
+
* the next command. No launch transaction is executed during planning. */
|
|
513
457
|
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
514
458
|
const original = options.planned, matches = observedFields(original);
|
|
515
|
-
const fullReviewAdmitted = original.simulation?.admitted === true;
|
|
516
459
|
const assertInput = () => {
|
|
517
460
|
if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
|
|
518
461
|
assertFinalPlan(original, options.signal);
|
|
519
462
|
};
|
|
520
463
|
const supplied = readReviewedNextTransaction(options.reviewedTransaction);
|
|
521
|
-
// Snapshot only the reviewed wire fields; caller proofs and commands are never
|
|
522
|
-
// reused, and final checks compare the mutable input with this one local copy.
|
|
523
464
|
const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
|
|
524
465
|
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
525
466
|
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
|
|
526
467
|
const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
|
|
527
468
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
528
469
|
registerLifecycleFinalCheck(client, assertInput);
|
|
529
|
-
const block = await readLifecyclePlanningBlock(client,
|
|
530
|
-
const limitSource = options.limits === undefined ?
|
|
531
|
-
if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
|
|
532
|
-
await Promise.all([
|
|
533
|
-
readLifecycleTokenAtBlock(client, options.planned.plan, block),
|
|
534
|
-
readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
|
|
535
|
-
resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
|
|
536
|
-
]);
|
|
537
|
-
}
|
|
470
|
+
const block = await readLifecyclePlanningBlock(client, original.plan, original.account, original.ufg !== undefined);
|
|
471
|
+
const limitSource = options.limits === undefined ? original.limits : options.limits;
|
|
538
472
|
const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
|
|
539
|
-
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// policy below remains in force for every nonterminal case.
|
|
543
|
-
|
|
544
|
-
return undefined;
|
|
545
|
-
}
|
|
546
|
-
if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
|
|
547
|
-
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.");
|
|
548
476
|
assertPlanningActive(options.signal);
|
|
477
|
+
const { limits } = await resolveLifecycleLimits({ client, account: original.account, orchestrator: original.plan.orchestrator, block, chainId: original.chainId }, limitSource);
|
|
549
478
|
if (options.action === "cancel") {
|
|
550
479
|
if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
551
|
-
const {
|
|
552
|
-
assertFinalPlan(planned, options.signal);
|
|
480
|
+
const planned = { ...original, progress };
|
|
553
481
|
const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
|
|
554
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 }] };
|
|
555
|
-
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current
|
|
556
|
-
const
|
|
557
|
-
if (!
|
|
558
|
-
|
|
559
|
-
if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
560
|
-
return finalizeNextTransaction(options, next, 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);
|
|
561
487
|
}
|
|
562
|
-
const { planned } = options;
|
|
563
|
-
assertFinalPlan(planned, options.signal);
|
|
564
|
-
if (progress.canonical.phase === LifecyclePhase.None && !fullReviewAdmitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", "Complete an admitted full launch review before starting or approving funding; next-only construction does not replan the remaining sequence");
|
|
565
488
|
const funding = progress.canonical.phase === LifecyclePhase.None
|
|
566
|
-
? await readFunding(client,
|
|
489
|
+
? await readFunding(client, original.plan, block, original.account, original.ufg !== undefined)
|
|
567
490
|
: { prerequisites: [], approvals: [] };
|
|
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
|
+
}
|
|
568
497
|
let candidate = funding.approvals[0];
|
|
569
498
|
if (candidate === undefined) {
|
|
570
499
|
const prepared = progress.canonical.preparedMarkets;
|
|
@@ -574,46 +503,25 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
574
503
|
let count: number;
|
|
575
504
|
if (reviewed?.kind === "prepare") {
|
|
576
505
|
const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
577
|
-
|
|
506
|
+
if (Number(span?.[1]) !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the unfinished market");
|
|
578
507
|
count = Number(span?.[2]);
|
|
579
|
-
if (first !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
|
|
580
508
|
} else {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
else {
|
|
584
|
-
const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
|
|
585
|
-
const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
586
|
-
count = packPreparationBatches(planned.plan, prepared, cap)[prepared > 0 ? 1 : 0]!;
|
|
587
|
-
}
|
|
509
|
+
const batches = await preparationBatches(planned.plan, prepared, planned.ufg, limits, client, block);
|
|
510
|
+
count = batches[prepared > 0 ? 1 : 0]!;
|
|
588
511
|
}
|
|
589
|
-
if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must
|
|
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");
|
|
590
513
|
command = { kind: "prepare", first: prepared, count };
|
|
591
514
|
} else command = { kind: "activate" };
|
|
592
515
|
candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
|
|
593
516
|
}
|
|
594
|
-
if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current
|
|
595
|
-
const
|
|
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);
|
|
596
519
|
assertFinalPlan(planned, options.signal);
|
|
597
|
-
if (!
|
|
598
|
-
|
|
599
|
-
return finalizeNextTransaction(options, next, 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);
|
|
600
522
|
});
|
|
601
523
|
assertInput();
|
|
602
524
|
if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
|
|
603
525
|
return result;
|
|
604
526
|
}
|
|
605
527
|
|
|
606
|
-
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
607
|
-
* evidence and confirmation depth as build-next, never silently dropping context. */
|
|
608
|
-
export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
|
|
609
|
-
const { client, planned } = options;
|
|
610
|
-
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");
|
|
611
|
-
const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
|
|
612
|
-
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
613
|
-
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
614
|
-
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
|
|
615
|
-
ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
|
|
616
|
-
}, readClient, { forceSimulation: true }));
|
|
617
|
-
assertPlannedDeploymentBindings(planned, current);
|
|
618
|
-
return current.simulation;
|
|
619
|
-
}
|