@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { toHex, type Address } from "viem";
|
|
2
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig } from "./markets.js";
|
|
3
|
+
import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
|
|
4
|
+
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
5
|
+
import { lifecyclePinnedRpc, lifecycleSourceClient, nitroPosterGasBound, readLifecycleGasPrice, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcQuantity } from "./rpc.js";
|
|
6
|
+
import { LifecycleFundingKind, LifecycleRewardMode, LifecycleTokenKind, type LaunchPlanV1 } from "./schema.js";
|
|
7
|
+
import { isAutoCompoundProfile } from "./auto-compound.js";
|
|
8
|
+
import { buildLifecycleCommand, LIFECYCLE_CAPACITY_PADDING_BPS, preparationCalldataGas, preparationGasEstimate } from "./transactions.js";
|
|
9
|
+
import { UFG_LAUNCH_PROXY_ADDRESS, isUfgProfile, ufgPlanPrincipal, type UfgLaunchConfig } from "./ufg-config.js";
|
|
10
|
+
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLaunchGasBounds, type LifecycleLimitSource, type LifecycleOpeningBuyQuote, type LifecyclePlanningAssessment, type LifecycleRpcClient, type LifecycleTransaction, type PlannedLaunch } from "./types.js";
|
|
11
|
+
import { isHmmProfile } from "./hmm.js";
|
|
12
|
+
|
|
13
|
+
export const LIFECYCLE_GAS_PROFILE_VERSION = "robinhood-4663-capacity-v3";
|
|
14
|
+
export type NextTransactionEnvelope = Readonly<{ gas: bigint; gasPrice: bigint }>;
|
|
15
|
+
const ceil = (value: bigint, divisor: bigint) => (value + divisor - 1n) / divisor;
|
|
16
|
+
const approval = (transaction: LifecycleTransaction) => transaction.kind === "approve" || transaction.kind === "approve-reset";
|
|
17
|
+
type MarketGas = { abyss: boolean; hmm: boolean; ufgHook: boolean; positions: number };
|
|
18
|
+
|
|
19
|
+
function marketGas(plan: LaunchPlanV1): MarketGas[] {
|
|
20
|
+
return plan.markets.map((market) => {
|
|
21
|
+
const abyss = market.configVersion === 1;
|
|
22
|
+
const config = abyss ? decodeAbyssLifecycleMarketConfig(market.config) : decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
|
|
23
|
+
return { abyss, hmm: isHmmProfile(market.profileId), ufgHook: isUfgProfile(market.profileId), positions: config.positions.length };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function utf8Bytes(value: string): number {
|
|
28
|
+
let bytes = 0;
|
|
29
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
30
|
+
const code = value.charCodeAt(index);
|
|
31
|
+
if (code < 0x80) bytes += 1;
|
|
32
|
+
else if (code < 0x800) bytes += 2;
|
|
33
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length && value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) { bytes += 4; index += 1; }
|
|
34
|
+
else bytes += 3;
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tokenCreationGas(token: LaunchPlanV1["token"]): bigint {
|
|
40
|
+
return (token.kind === LifecycleTokenKind.ERC404 ? 2_500_000n : 0n) +
|
|
41
|
+
(token.rewardMode === LifecycleRewardMode.Staking ? 2_500_000n : token.rewardMode === LifecycleRewardMode.Dividends ? 500_000n : 0n) +
|
|
42
|
+
25_000n * BigInt(Math.ceil(utf8Bytes(token.name) / 32) + Math.ceil(utf8Bytes(token.symbol) / 32) + Math.ceil(utf8Bytes(token.metadataURI) / 32));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Max observed compute: native Abyss atomic21,584,262; V4 fixed20,833,003;
|
|
46
|
+
// HMM26,395,772; UFG27,941,420 (historical-receipts.json,195 transactions).
|
|
47
|
+
// fork-matrix.json: Abyss activation8 positions11.08M/32 positions39.72M;
|
|
48
|
+
// fork-v4-matrix.json: dynamic V4 eight positions5,906,285.
|
|
49
|
+
// Native mixed HMM8/Abyss8/HMM8 activation23,883,893 (0xf05e…fe98).
|
|
50
|
+
const activationCosts = {
|
|
51
|
+
call: 1_500_000n,
|
|
52
|
+
abyss: { market: 500_000n, position: 1_250_000n },
|
|
53
|
+
v4: { market: 250_000n, position: 500_000n },
|
|
54
|
+
hmm: 1_000_000n,
|
|
55
|
+
ufg: 250_000n,
|
|
56
|
+
// Fork Abyss32: 64 buys78.44M minimum versus one buy47.49M;
|
|
57
|
+
// max observed marginal is below0.5M/buy. ERC404 pairs add below0.1M/NFT.
|
|
58
|
+
buy: 500_000n,
|
|
59
|
+
nft: 100_000n,
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/** Measured-max template budgets, never gas-limit sizing or execution evidence. */
|
|
63
|
+
function capacityBasis(plan: LaunchPlanV1, transaction: LifecycleTransaction, markets: readonly MarketGas[], quotes: readonly LifecycleOpeningBuyQuote[], ufgRoute: boolean): bigint {
|
|
64
|
+
const atomic = transaction.kind === "atomic";
|
|
65
|
+
let gas = atomic ? 22_000_000n + tokenCreationGas(plan.token) + (ufgRoute ? 6_500_000n : 0n)
|
|
66
|
+
: activationCosts.call + (ufgRoute ? activationCosts.ufg : 0n);
|
|
67
|
+
for (const market of markets) {
|
|
68
|
+
if (atomic) gas += (market.hmm ? 7_000_000n : market.ufgHook ? 1_000_000n : market.abyss ? 500_000n : 0n) +
|
|
69
|
+
(market.abyss ? 1_500_000n : 600_000n) * BigInt(market.positions - 1);
|
|
70
|
+
else {
|
|
71
|
+
const cost = market.abyss ? activationCosts.abyss : activationCosts.v4;
|
|
72
|
+
gas += cost.market + cost.position * BigInt(market.positions) + (market.hmm ? activationCosts.hmm : market.ufgHook ? activationCosts.ufg : 0n);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
gas += activationCosts.buy * BigInt(quotes.length);
|
|
76
|
+
if (plan.token.kind === LifecycleTokenKind.ERC404) for (const quote of quotes) gas += activationCosts.nft * ceil(quote.tokenOut, plan.token.nftUnit);
|
|
77
|
+
return gas + 24n * BigInt((transaction.data.length - 2) / 2) + 100_000n * BigInt(plan.feeAssets.length);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type CapacityOptions = { plan: LaunchPlanV1; predictedToken: Address; ufg?: { config: UfgLaunchConfig }; headroomBps?: number };
|
|
81
|
+
export type ProfiledLaunchCapacity = {
|
|
82
|
+
bounds: LifecycleLaunchGasBounds; initialBuys: readonly LifecycleOpeningBuyQuote[]; headroomBps: number;
|
|
83
|
+
atomic?: LifecycleTransaction; activation: LifecycleTransaction;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** No quote-token allowlist: costs and exact curve math belong to the release template. */
|
|
87
|
+
export function deriveLifecycleLaunchCapacity(options: CapacityOptions, openingBuys?: readonly LifecycleOpeningBuyQuote[]): ProfiledLaunchCapacity | undefined {
|
|
88
|
+
const { plan, ufg } = options;
|
|
89
|
+
if (!hasProfiledOpeningMath(plan) || (plan.creator.toLowerCase() === UFG_LAUNCH_PROXY_ADDRESS.toLowerCase()) !== (ufg !== undefined)) return undefined;
|
|
90
|
+
// Auto-compounding templates have no measured gas yet; never invent their budgets.
|
|
91
|
+
if (plan.markets.some((market) => isAutoCompoundProfile(market.profileId))) return undefined;
|
|
92
|
+
const headroomBps = options.headroomBps ?? 1500;
|
|
93
|
+
if (!Number.isSafeInteger(headroomBps) || headroomBps < 0 || headroomBps > 10000) throw new LifecyclePlanningError("INVALID_LIMITS", "Headroom must be between zero and 10000 basis points");
|
|
94
|
+
const initialBuys = openingBuys ?? computeLifecycleOpeningBuys(options);
|
|
95
|
+
const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
|
|
96
|
+
const atomic = plan.markets.length === 1 && !plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap)
|
|
97
|
+
? buildLifecycleCommand(plan, { kind: "atomic" }, route) : undefined;
|
|
98
|
+
const activation = buildLifecycleCommand(plan, { kind: "activate" }, route);
|
|
99
|
+
const markets = marketGas(plan), reserve = hmmV2ForwardingReserve(plan);
|
|
100
|
+
const bound = (transaction: LifecycleTransaction) => {
|
|
101
|
+
const basis = capacityBasis(plan, transaction, markets, initialBuys, ufg !== undefined);
|
|
102
|
+
const buffered = ceil(basis * BigInt(10000 + Math.max(LIFECYCLE_CAPACITY_PADDING_BPS, headroomBps)), 10000n);
|
|
103
|
+
return buffered > basis + reserve ? buffered : basis + reserve;
|
|
104
|
+
};
|
|
105
|
+
return { bounds: { ...(atomic === undefined ? {} : { atomicComputeGasUpperBound: bound(atomic) }), activationComputeGasUpperBound: bound(activation) }, initialBuys, headroomBps, atomic, activation };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Pure release-template budgets with headroom, excluding poster allocation.
|
|
109
|
+
* Missing coverage refuses planning; there is no execution fallback. */
|
|
110
|
+
export function computeLifecycleLaunchGasBounds(options: CapacityOptions): LifecycleLaunchGasBounds | undefined {
|
|
111
|
+
return deriveLifecycleLaunchCapacity(options)?.bounds;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type LifecycleBudgetOptions = { client: LifecycleRpcClient; planned: PlannedLaunch; limits?: LifecycleLimitSource; signal?: AbortSignal };
|
|
115
|
+
|
|
116
|
+
/** Checks pinned inputs and deterministic capacity only. Never executes launch calldata. */
|
|
117
|
+
export async function assessLifecycleTransactions(options: LifecycleBudgetOptions, transactions: readonly LifecycleTransaction[], block: LifecycleBlock,
|
|
118
|
+
reviewed?: NextTransactionEnvelope, openingBuys?: readonly LifecycleOpeningBuyQuote[], capacity?: ProfiledLaunchCapacity): Promise<LifecyclePlanningAssessment> {
|
|
119
|
+
const { client, planned } = options, plan = planned.plan;
|
|
120
|
+
const context = { client, account: planned.account, orchestrator: plan.orchestrator, block, chainId: planned.chainId };
|
|
121
|
+
const observed = await resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits);
|
|
122
|
+
const [recommendation, balanceValue, prices] = await Promise.all([
|
|
123
|
+
readLifecycleGasPrice(client, block, planned.chainId),
|
|
124
|
+
lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block),
|
|
125
|
+
observed.limits.protocol === "nitro" ? readNitroPrices(client, block) : undefined,
|
|
126
|
+
]);
|
|
127
|
+
options.signal?.throwIfAborted(); observed.assertCurrent(); registerLifecycleFinalCheck(client, observed.assertCurrent);
|
|
128
|
+
const { limits } = observed, baseFee = block.baseFeePerGas ?? 0n, gasLimit = limits.executionGasCeiling;
|
|
129
|
+
const requiredGasPrice = limits.protocol === "nitro" && recommendation > baseFee ? recommendation : baseFee;
|
|
130
|
+
const gasPrice = reviewed?.gasPrice ?? (recommendation > baseFee * 2n ? recommendation : baseFee * 2n);
|
|
131
|
+
if (reviewed !== undefined && reviewed.gas !== gasLimit) throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling. Refresh the review before continuing.");
|
|
132
|
+
const cancelOnly = transactions.every((transaction) => transaction.kind === "cancel");
|
|
133
|
+
const currentCapacity = cancelOnly ? undefined : capacity?.headroomBps === limits.headroomBps ? capacity
|
|
134
|
+
: deriveLifecycleLaunchCapacity({ plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps }, openingBuys);
|
|
135
|
+
const immediate = transactions[0], launch = transactions.find((transaction) => !approval(transaction));
|
|
136
|
+
const poster = (transaction: LifecycleTransaction) => prices === undefined ? 0n : nitroPosterGasBound(transaction, prices).posterGas;
|
|
137
|
+
const assessment: LifecyclePlanningAssessment = { ready: true, blockNumber: block.number, blockHash: block.hash,
|
|
138
|
+
account: planned.account, chainId: planned.chainId, limits,
|
|
139
|
+
...(currentCapacity === undefined ? {} : { gasProfile: { version: LIFECYCLE_GAS_PROFILE_VERSION, bounds: currentCapacity.bounds, initialBuys: currentCapacity.initialBuys } }) };
|
|
140
|
+
const refuse = (code: string, reason: string, failureCategory: LifecyclePlanningAssessment["failureCategory"], capacityConstraint?: LifecyclePlanningAssessment["capacityConstraint"], failedTransactionId?: string): LifecyclePlanningAssessment =>
|
|
141
|
+
({ ...assessment, ready: false, code, reason, failureCategory, capacityConstraint, failedTransactionId });
|
|
142
|
+
if (!cancelOnly && currentCapacity === undefined) return refuse("UNKNOWN_GAS_PROFILE", "Gas budgets are unavailable for this launch profile. Use a supported profile or update the SDK.", "capacity");
|
|
143
|
+
if (!cancelOnly && currentCapacity!.bounds.activationComputeGasUpperBound + poster(currentCapacity!.activation) > gasLimit) {
|
|
144
|
+
return refuse("ACTIVATION_GAS_LIMIT", "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling.", "capacity", "activation", "activate");
|
|
145
|
+
}
|
|
146
|
+
for (const transaction of transactions) {
|
|
147
|
+
if (transaction.kind === "atomic" && (currentCapacity?.bounds.atomicComputeGasUpperBound === undefined || currentCapacity.bounds.atomicComputeGasUpperBound + poster(transaction) > gasLimit))
|
|
148
|
+
return refuse("STAGED_EXECUTION_REQUIRED", "This launch requires staged execution. Refresh the review before continuing.", "capacity", "gas-envelope", transaction.id);
|
|
149
|
+
if (transaction.kind === "prepare" && ceil((preparationGasEstimate(plan, transaction.marketStart!, transaction.marketCount!) + preparationCalldataGas(transaction.data)) * BigInt(10000 + Math.max(LIFECYCLE_CAPACITY_PADDING_BPS, limits.headroomBps)), 10000n) + poster(transaction) > gasLimit)
|
|
150
|
+
return refuse("PREPARATION_GAS_LIMIT", "This preparation batch exceeds the gas budget. Refresh the review to split it.", "capacity", "preparation", transaction.id);
|
|
151
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
|
|
152
|
+
return refuse("CALLDATA_LIMIT", "The launch exceeds the transaction data limit. Reduce its positions or pools.", "capacity", "calldata", transaction.id);
|
|
153
|
+
}
|
|
154
|
+
if (launch !== undefined && limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling)
|
|
155
|
+
return refuse("GAS_POLICY_LIMIT", "The configured gas limit is below the chain's native transaction ceiling.", "capacity", "gas-envelope", launch.id);
|
|
156
|
+
if (reviewed !== undefined && gasPrice < requiredGasPrice) return refuse("REVIEWED_FEE_TOO_LOW", "The reviewed gas price is below the current required fee. Refresh the review before continuing.", "semantic", undefined, immediate?.id);
|
|
157
|
+
const quotes = currentCapacity?.initialBuys ?? [];
|
|
158
|
+
if (quotes.some((quote) => quote.tokenOut < plan.buys[quote.buyIndex]!.minTokenOut))
|
|
159
|
+
return refuse("BUY_MINIMUM_NOT_MET", "An opening buy is below its minimum output. Refresh the review before continuing.", "semantic");
|
|
160
|
+
if (block.timestamp > plan.deadline && !cancelOnly) return refuse("PLAN_EXPIRED", "The launch deadline has passed. Cancel an unfinished launch to recover its funds.", "semantic");
|
|
161
|
+
const inputs = new Map<string, { required: bigint; balance: bigint }>();
|
|
162
|
+
for (const prerequisite of planned.prerequisites) {
|
|
163
|
+
if (prerequisite.allowance === undefined) continue;
|
|
164
|
+
const key = prerequisite.funding.inputAsset.toLowerCase(), total = inputs.get(key) ?? { required: 0n, balance: prerequisite.inputBalance };
|
|
165
|
+
total.required += prerequisite.requiredInput; inputs.set(key, total);
|
|
166
|
+
}
|
|
167
|
+
if ([...inputs.values()].some((input) => input.required > input.balance)) return refuse("INSUFFICIENT_FUNDING_BALANCE", "There are not enough tokens to fund this launch. Add funds or reduce the purchase.", "affordability");
|
|
168
|
+
const balance = rpcQuantity(balanceValue, "creator native balance");
|
|
169
|
+
if (gasPrice >= 1n << 256n || launch !== undefined && launch.value + gasLimit * gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and native-ceiling fee must fit uint256");
|
|
170
|
+
if (launch !== undefined && launch.value + gasLimit * gasPrice > balance) return refuse("INSUFFICIENT_NATIVE_BALANCE", "There is not enough native currency for the launch amount and gas budget. Add funds or reduce the purchase.", "affordability", undefined, launch.id);
|
|
171
|
+
if (immediate !== undefined && !approval(immediate)) {
|
|
172
|
+
const source = lifecycleSourceClient(client);
|
|
173
|
+
const dataFee = limits.protocol === "nitro" ? 0n : observed.configuration.estimateDataFee === undefined ? undefined
|
|
174
|
+
: await observed.configuration.estimateDataFee({ ...immediate, gas: gasLimit, gasPrice }, source === client ? context : { ...context, client: source });
|
|
175
|
+
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee estimate must be a nonnegative uint256 amount");
|
|
176
|
+
const executionFee = gasLimit * gasPrice, required = immediate.value + executionFee + (dataFee ?? 0n);
|
|
177
|
+
if (required >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and gas/data fee budget must fit uint256");
|
|
178
|
+
if (required > balance) return refuse("INSUFFICIENT_NATIVE_BALANCE", "There is not enough native currency for the launch amount and gas budget. Add funds or reduce the purchase.", "affordability", undefined, immediate.id);
|
|
179
|
+
const posterGas = prices === undefined ? undefined : poster(immediate);
|
|
180
|
+
assessment.estimate = { gasLimit, gasPrice, executionFee, dataFee, totalFee: dataFee === undefined ? undefined : executionFee + dataFee,
|
|
181
|
+
posterGas, posterFee: posterGas === undefined ? undefined : posterGas * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro",
|
|
182
|
+
feeConfidence: dataFee === undefined ? "native-ceiling-only" : "native-ceiling-and-data" };
|
|
183
|
+
}
|
|
184
|
+
options.signal?.throwIfAborted(); observed.assertCurrent();
|
|
185
|
+
return assessment;
|
|
186
|
+
}
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
/** Versioned construction metadata and
|
|
1
|
+
/** Versioned construction metadata, exact local curve math and launch budgets. */
|
|
2
2
|
export * from "./schema.js";
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export * from "./abi.js";
|
|
5
5
|
export * from "./markets.js";
|
|
6
6
|
export * from "./hmm.js";
|
|
7
7
|
export * from "./ufg.js";
|
|
8
|
+
export * from "./auto-compound.js";
|
|
8
9
|
export { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLifecycleTokenAtBlock, readLifecycleHeadProgress, predictLifecycleToken,
|
|
9
10
|
validateV4LifecycleOracle, readLifecycleProfiles, readPoolBoundHookDeployment,
|
|
10
11
|
buildPoolBoundHookDeploymentTransaction, validateLifecycleMarketIdentity, readLaunchProgress } from "./progress.js";
|
|
11
|
-
export * from "./fork.js";
|
|
12
12
|
export * from "./authors.js";
|
|
13
13
|
export * from "./presets.js";
|
|
14
|
-
export
|
|
14
|
+
export { preparationGasEstimate, packPreparationBatches, buildLaunchTransactions, LIFECYCLE_CAPACITY_PADDING_BPS } from "./transactions.js";
|
|
15
15
|
export { createLifecycleSessionCache } from "./rpc.js";
|
|
16
16
|
export { launchLensAbi } from "./lens.js";
|
|
17
17
|
export { hookSaltMinerAbi } from "./mining.js";
|
|
18
|
-
export { planLaunch,
|
|
18
|
+
export { planLaunch, buildNextTransaction, prepareAndPlanLifecycleLaunch } from "./planner.js";
|
|
19
|
+
export { computeLifecycleOpeningBuys } from "./opening-buys.js";
|
|
20
|
+
export { computeLifecycleLaunchGasBounds } from "./gas-profile.js";
|
|
19
21
|
export { assertLaunchTopologyCoverage, getTickAtSqrtRatio, type LaunchTopologyCoverageInput, type LaunchTopologyPosition } from "../launch.js";
|
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" },
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
import { ABYSS_FEE_TIERS } from "../abyss.js";
|
|
3
|
+
import { getSqrtRatioAtTick, getTickAtSqrtRatio } from "../launch.js";
|
|
4
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig } from "./markets.js";
|
|
5
|
+
import { getKnownLifecycleProfile, KnownLifecycleProfile } from "./presets.js";
|
|
6
|
+
import type { LaunchPlanV1 } from "./schema.js";
|
|
7
|
+
import { LifecyclePlanningError, type LifecycleOpeningBuyQuote } from "./types.js";
|
|
8
|
+
|
|
9
|
+
// Independent integer model of the reviewed openings. Mathematical rounding and
|
|
10
|
+
// uint256 overflow branches match Uniswap v4 SqrtPriceMath/SwapMath (MIT) and
|
|
11
|
+
// the corresponding Abyss libraries. Bitmap word boundaries cannot be skipped:
|
|
12
|
+
// each step rounds input/fees up and output down, in the pool's favor.
|
|
13
|
+
const Q96 = 1n << 96n;
|
|
14
|
+
const PIPS = 1_000_000n;
|
|
15
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
16
|
+
const MIN_PRICE = 4_295_128_739n;
|
|
17
|
+
const MAX_PRICE = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342n;
|
|
18
|
+
const ceil = (numerator: bigint, denominator: bigint): bigint => (numerator + denominator - 1n) / denominator;
|
|
19
|
+
|
|
20
|
+
type OpeningState = {
|
|
21
|
+
price: bigint; tick: number; liquidity: bigint; spacing: number;
|
|
22
|
+
lpFee: bigint; hookFee: bigint;
|
|
23
|
+
liquidityNet: Map<number, bigint>; ticks: number[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Only the exact release-owned schemas/hooks have profiled opening behavior. */
|
|
27
|
+
export function hasProfiledOpeningMath(plan: LaunchPlanV1): boolean {
|
|
28
|
+
return plan.markets.every((market) => {
|
|
29
|
+
const preset = getKnownLifecycleProfile({ chainId: plan.chainId, orchestrator: plan.orchestrator, profileId: market.profileId });
|
|
30
|
+
return preset?.supported === true && preset.profile.topology.configVersion === market.configVersion &&
|
|
31
|
+
preset.profile.registration.adapterId.toLowerCase() === market.adapterId.toLowerCase();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openingState(plan: LaunchPlanV1, marketIndex: number): OpeningState {
|
|
36
|
+
const market = plan.markets[marketIndex];
|
|
37
|
+
if (market === undefined) throw new LifecyclePlanningError("INVALID_BUY", "Opening buy market index is outside the plan");
|
|
38
|
+
const preset = getKnownLifecycleProfile({ chainId: plan.chainId, orchestrator: plan.orchestrator, profileId: market.profileId });
|
|
39
|
+
if (preset?.supported !== true || preset.profile.topology.configVersion !== market.configVersion ||
|
|
40
|
+
preset.profile.registration.adapterId.toLowerCase() !== market.adapterId.toLowerCase()) throw new LifecyclePlanningError("OPENING_BUY_NOT_PROFILED", "Opening buy math requires the exact release-owned profile and schema");
|
|
41
|
+
let price: bigint, spacing: number, lpFee: bigint, hookFee = 0n;
|
|
42
|
+
let positions: readonly { tickLower: number; tickUpper: number; liquidity: bigint }[];
|
|
43
|
+
if (market.configVersion === 1) {
|
|
44
|
+
const config = decodeAbyssLifecycleMarketConfig(market.config);
|
|
45
|
+
const tier = ABYSS_FEE_TIERS.find((candidate) => candidate.feePips === config.fee);
|
|
46
|
+
if (tier === undefined || preset.key !== `abyss-${config.profile}`) throw new LifecyclePlanningError("INVALID_MARKET", "Abyss opening must use its committed profile and canonical fee tier");
|
|
47
|
+
price = config.openingSqrtPriceX96; spacing = tier.tickSpacing; lpFee = BigInt(config.fee); positions = config.positions;
|
|
48
|
+
} else {
|
|
49
|
+
// All current release-owned V4 offerings use config6. Dynamic first buys
|
|
50
|
+
// share the opening block's minimum fee; HMM strategy work starts onOpened,
|
|
51
|
+
// after the ordered initial buys, never during this unopened curve.
|
|
52
|
+
const config = decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
|
|
53
|
+
if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_MARKET", "V4 opening config differs from the selected profile");
|
|
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 || preset.key === KnownLifecycleProfile.V4AutoCompoundDynamicPool;
|
|
56
|
+
hookFee = BigInt(dynamic ? config.minimumHookFeePips! : config.hookFeePips);
|
|
57
|
+
}
|
|
58
|
+
const tick = getTickAtSqrtRatio(price), liquidityNet = new Map<number, bigint>();
|
|
59
|
+
let liquidity = 0n;
|
|
60
|
+
for (const position of positions) {
|
|
61
|
+
liquidityNet.set(position.tickLower, (liquidityNet.get(position.tickLower) ?? 0n) + position.liquidity);
|
|
62
|
+
liquidityNet.set(position.tickUpper, (liquidityNet.get(position.tickUpper) ?? 0n) - position.liquidity);
|
|
63
|
+
if (position.tickLower <= tick && tick < position.tickUpper) liquidity += position.liquidity;
|
|
64
|
+
}
|
|
65
|
+
return { price, tick, liquidity, spacing, lpFee, hookFee, liquidityNet, ticks: [...liquidityNet.keys()].sort((a, b) => a - b) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function lowerBound(ticks: readonly number[], value: number): number {
|
|
69
|
+
let left = 0, right = ticks.length;
|
|
70
|
+
while (left < right) {
|
|
71
|
+
const middle = (left + right) >>> 1;
|
|
72
|
+
if (ticks[middle]! < value) left = middle + 1; else right = middle;
|
|
73
|
+
}
|
|
74
|
+
return left;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function nextTick(state: OpeningState, zeroForOne: boolean): number {
|
|
78
|
+
const compressed = Math.floor(state.tick / state.spacing) + (zeroForOne ? 0 : 1);
|
|
79
|
+
const first = Math.floor(compressed / 256) * 256, last = first + 255;
|
|
80
|
+
const index = lowerBound(state.ticks, (compressed + (zeroForOne ? 1 : 0)) * state.spacing) - (zeroForOne ? 1 : 0);
|
|
81
|
+
const initialized = state.ticks[index];
|
|
82
|
+
const selected = initialized !== undefined && initialized >= first * state.spacing && initialized <= last * state.spacing
|
|
83
|
+
? initialized : (zeroForOne ? first : last) * state.spacing;
|
|
84
|
+
return Math.max(-887272, Math.min(887272, selected));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function amount0(a: bigint, b: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
88
|
+
if (a > b) [a, b] = [b, a];
|
|
89
|
+
const numerator = liquidity * Q96 * (b - a), denominator = a * b;
|
|
90
|
+
return roundUp ? ceil(numerator, denominator) : numerator / denominator;
|
|
91
|
+
}
|
|
92
|
+
function amount1(a: bigint, b: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
93
|
+
const numerator = liquidity * (a > b ? a - b : b - a);
|
|
94
|
+
return roundUp ? ceil(numerator, Q96) : numerator / Q96;
|
|
95
|
+
}
|
|
96
|
+
function inputPrice(price: bigint, liquidity: bigint, input: bigint, zeroForOne: boolean): bigint {
|
|
97
|
+
if (input === 0n) return price;
|
|
98
|
+
if (!zeroForOne) return price + input * Q96 / liquidity;
|
|
99
|
+
const numerator = liquidity * Q96, product = input * price, denominator = numerator + product;
|
|
100
|
+
return product <= MAX_UINT256 && denominator <= MAX_UINT256
|
|
101
|
+
? ceil(numerator * price, denominator)
|
|
102
|
+
: ceil(numerator, numerator / price + input);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function swap(state: OpeningState, amount: bigint, limit: bigint, zeroForOne: boolean): { tokenOut: bigint; quoteSpent: bigint } {
|
|
106
|
+
let remaining = amount, tokenOut = 0n;
|
|
107
|
+
while (remaining > 0n && state.price !== limit) {
|
|
108
|
+
const tick = nextTick(state, zeroForOne), boundary = getSqrtRatioAtTick(tick), before = state.price;
|
|
109
|
+
const target = zeroForOne ? boundary < limit ? limit : boundary : boundary > limit ? limit : boundary;
|
|
110
|
+
const available = remaining * (PIPS - state.lpFee) / PIPS;
|
|
111
|
+
const required = zeroForOne ? amount0(target, before, state.liquidity, true) : amount1(before, target, state.liquidity, true);
|
|
112
|
+
let next: bigint, spent: bigint;
|
|
113
|
+
if (available >= required) {
|
|
114
|
+
next = target;
|
|
115
|
+
spent = required + (state.lpFee === PIPS ? 0n : ceil(required * state.lpFee, PIPS - state.lpFee));
|
|
116
|
+
} else {
|
|
117
|
+
next = inputPrice(before, state.liquidity, available, zeroForOne);
|
|
118
|
+
spent = remaining;
|
|
119
|
+
}
|
|
120
|
+
tokenOut += zeroForOne ? amount1(next, before, state.liquidity, false) : amount0(before, next, state.liquidity, false);
|
|
121
|
+
remaining -= spent;
|
|
122
|
+
state.price = next;
|
|
123
|
+
if (next === boundary) {
|
|
124
|
+
const net = state.liquidityNet.get(tick);
|
|
125
|
+
if (net !== undefined) state.liquidity += zeroForOne ? -net : net;
|
|
126
|
+
state.tick = zeroForOne ? tick - 1 : tick;
|
|
127
|
+
} else if (next !== before) state.tick = getTickAtSqrtRatio(next);
|
|
128
|
+
}
|
|
129
|
+
return { tokenOut, quoteSpent: amount - remaining };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Exact integer opening-curve outputs for ordered, unopened release templates.
|
|
133
|
+
* This is not a live-pool quote or execution proof. Funding, token callbacks,
|
|
134
|
+
* current registry admission and wallet execution remain separate authorities. */
|
|
135
|
+
export function computeLifecycleOpeningBuys(options: { plan: LaunchPlanV1; predictedToken: Address }): LifecycleOpeningBuyQuote[] {
|
|
136
|
+
const { plan, predictedToken } = options, states = new Map<number, OpeningState>();
|
|
137
|
+
const quotes: LifecycleOpeningBuyQuote[] = [];
|
|
138
|
+
for (const [buyIndex, buy] of plan.buys.entries()) {
|
|
139
|
+
if (!Number.isSafeInteger(buy.marketIndex) || buy.marketIndex < 0 || typeof buy.quoteAmountIn !== "bigint" || buy.quoteAmountIn <= 0n || buy.quoteAmountIn > MAX_UINT256 >> 1n) throw new LifecyclePlanningError("INVALID_BUY", "Opening buy requires a valid market and positive int256 input");
|
|
140
|
+
let state = states.get(buy.marketIndex);
|
|
141
|
+
if (state === undefined) { state = openingState(plan, buy.marketIndex); states.set(buy.marketIndex, state); }
|
|
142
|
+
const market = plan.markets[buy.marketIndex]!, zeroForOne = BigInt(market.quoteAsset) < BigInt(predictedToken);
|
|
143
|
+
if (typeof buy.sqrtPriceLimitX96 !== "bigint") throw new LifecyclePlanningError("INVALID_BUY_PRICE_LIMIT", "Opening buy price limit must be an exact bigint");
|
|
144
|
+
// Only the V4 lifecycle adapter resolves this sentinel. Abyss calls its
|
|
145
|
+
// pool directly, so zero remains invalid there. Never rewrite calldata.
|
|
146
|
+
const limit = market.configVersion !== 1 && buy.sqrtPriceLimitX96 === 0n
|
|
147
|
+
? zeroForOne ? MIN_PRICE + 1n : MAX_PRICE - 1n : buy.sqrtPriceLimitX96;
|
|
148
|
+
if (market.quoteAsset.toLowerCase() === predictedToken.toLowerCase() || limit <= MIN_PRICE || limit >= MAX_PRICE ||
|
|
149
|
+
(zeroForOne ? limit >= state.price : limit <= state.price)) throw new LifecyclePlanningError("INVALID_BUY_PRICE_LIMIT", "Opening buy price limit must be strictly beyond the current curve price in the input direction");
|
|
150
|
+
const hookCharge = buy.quoteAmountIn * state.hookFee / PIPS;
|
|
151
|
+
const adjusted = buy.quoteAmountIn - hookCharge;
|
|
152
|
+
const result = swap(state, adjusted, limit, zeroForOne);
|
|
153
|
+
if (hookCharge > 0n && result.quoteSpent !== adjusted) throw new LifecyclePlanningError("INCOMPLETE_BUY_FILL", "A specified-input hook fee requires the complete adjusted input to fill");
|
|
154
|
+
quotes.push({ buyIndex, marketIndex: buy.marketIndex, quoteAmountIn: buy.quoteAmountIn, quoteSpent: result.quoteSpent + hookCharge, tokenOut: result.tokenOut, sqrtPriceAfterX96: state.price });
|
|
155
|
+
}
|
|
156
|
+
return quotes;
|
|
157
|
+
}
|