@black-market/sdk 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/gas-profile.d.ts +35 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +183 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.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 +2 -2
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +178 -163
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +3 -0
- 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 +24 -19
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +58 -50
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +3 -4
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +98 -247
- package/dist/lifecycle/simulation.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 +19 -3
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +25 -20
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +53 -45
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +3 -7
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +67 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +43 -45
- package/examples/README.md +11 -8
- package/examples/launch-example.ts +35 -25
- package/examples/launch-lifecycle-smoke.ts +5 -3
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/gas-profile.ts +175 -0
- package/src/lifecycle/index.ts +3 -1
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +164 -151
- package/src/lifecycle/presets.ts +5 -0
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +56 -44
- package/src/lifecycle/simulation.ts +85 -231
- package/src/lifecycle/snapshot.ts +1 -2
- package/src/lifecycle/transactions.ts +33 -21
- package/src/lifecycle/types.ts +52 -42
- package/src/lifecycle/ufg.ts +3 -7
|
@@ -309,15 +309,23 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
|
|
|
309
309
|
}
|
|
310
310
|
const activationTransaction = planned.transactions.find((transaction) => ["atomic", "activate"].includes(transaction.kind));
|
|
311
311
|
const activationStep = planned.simulation.steps.find((step) => step.transactionId === activationTransaction?.id);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
312
|
+
let orderedOutputs, buyEvidence;
|
|
313
|
+
if (activationStep?.success && activationStep.returnData) {
|
|
314
|
+
const receipt = decodeFunctionResult({ abi: sdk.launchLifecycleAbi,
|
|
315
|
+
functionName: activationTransaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", data: activationStep.returnData });
|
|
316
|
+
assert.equal(receipt.planHash.toLowerCase(), planned.planHash.toLowerCase(), "Economic receipt exact plan");
|
|
317
|
+
assert.equal(receipt.tokenOut.length, planned.plan.buys.length, "Economic receipt ordered outputs");
|
|
318
|
+
assert.equal(receipt.quoteSpent.length, planned.plan.buys.length, "Economic receipt ordered spending");
|
|
319
|
+
orderedOutputs = receipt.tokenOut.map((tokenOut, index) => ({ tokenOut, quoteSpent: receipt.quoteSpent[index] }));
|
|
320
|
+
buyEvidence = "simulated-execution";
|
|
321
|
+
} else if (planned.simulation.backend === "profiled" && planned.simulation.gasProfile) {
|
|
322
|
+
orderedOutputs = planned.simulation.gasProfile.initialBuys;
|
|
323
|
+
buyEvidence = "local-exact-curve";
|
|
324
|
+
} else throw exampleError("MISSING_ECONOMIC_OUTPUTS", "Economic review requires ordered buy outputs from exact execution or the covered local curve");
|
|
325
|
+
assert.equal(orderedOutputs.length, planned.plan.buys.length, "Economic review ordered outputs");
|
|
326
|
+
const reviewedBuys = planned.plan.buys.map((buy, index) => {
|
|
327
|
+
const { tokenOut, quoteSpent } = orderedOutputs[index];
|
|
328
|
+
assert.ok(tokenOut >= buy.minTokenOut && tokenOut > 0n && quoteSpent <= buy.quoteAmountIn, "Exact protected opening buy");
|
|
321
329
|
const market = marketEconomics[buy.marketIndex];
|
|
322
330
|
const ratio = market.rawQuotePerRawToken;
|
|
323
331
|
return { buyIndex: index, marketIndex: buy.marketIndex, quoteAsset: market.quoteAsset, quoteSymbol: market.quoteSymbol, quoteSpent, tokenOut,
|
|
@@ -325,10 +333,10 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
|
|
|
325
333
|
feeAndPriceImpactPremiumBps: (quoteSpent * ratio.denominator - tokenOut * ratio.numerator) * 10000n /
|
|
326
334
|
(tokenOut * ratio.numerator),
|
|
327
335
|
// Effective execution price includes fees and path impact, not post-buy marginal spot.
|
|
328
|
-
priceBasis:
|
|
336
|
+
priceBasis: `${buyEvidence}-all-in-average-including-fees-and-price-impact` };
|
|
329
337
|
});
|
|
330
338
|
const economicReview = {
|
|
331
|
-
schema: "black-market.launch-economic-review.
|
|
339
|
+
schema: "black-market.launch-economic-review.v2", caseId: scenario.id, chainId: fixture.chainId,
|
|
332
340
|
targetMarketCapUsdX18: fixture.targetMarketCapUsdX18, quotes, metadata, image: image?.descriptor ?? null,
|
|
333
341
|
actualMintedSupply: token.supply, nftUnit: token.nftUnit, tokenDecimals: 18,
|
|
334
342
|
predictedToken, markets: marketEconomics,
|
|
@@ -337,7 +345,7 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
|
|
|
337
345
|
executorFeeBps: plan.executorFeeBps, feeAssets, v4Markets: scenario.markets.flatMap((spec, marketIndex) => spec.venue === "v4" ?
|
|
338
346
|
[{ marketIndex, feeMode: spec.feeMode ?? 0, protocolFeeDenominator: selected.v4.envelope.protocolFeeDenominator,
|
|
339
347
|
treasury: selected.v4.envelope.protocolTreasury, developerBeneficiary: selected.v4.envelope.beneficiary, developerFeeBps: 0 }] : []) },
|
|
340
|
-
initialBuy: { slippageBps: BUY_SLIPPAGE_BPS, funding: planned.plan.funding, orderedBuys: planned.plan.buys,
|
|
348
|
+
initialBuy: { slippageBps: BUY_SLIPPAGE_BPS, funding: planned.plan.funding, orderedBuys: planned.plan.buys, reviewedBuys, evidence: buyEvidence,
|
|
341
349
|
observedBlockNumber: planned.simulation.blockNumber, observedBlockHash: planned.simulation.blockHash },
|
|
342
350
|
planHash: planned.planHash, finalizedPlan: finalizedPlan,
|
|
343
351
|
};
|
|
@@ -346,7 +354,7 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
|
|
|
346
354
|
// Complete exact intent is owner-only and durable before attribution or chain signing.
|
|
347
355
|
artifacts.privateRecovery({
|
|
348
356
|
schema: "black-market.launch-example-recovery.v1", sdk: "node", caseId: artifacts.result.caseId,
|
|
349
|
-
plan: finalizedPlan, mode:
|
|
357
|
+
plan: finalizedPlan, mode: planned.mode, confirmations, executionLimits: fixture.executionLimits, economicReview: { ...economicReview, reviewHash },
|
|
350
358
|
});
|
|
351
359
|
artifacts.save("plan.json", finalizedPlan);
|
|
352
360
|
artifacts.save("hook-deployments.json", planned.hookDeployments);
|
|
@@ -490,11 +498,11 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
490
498
|
let activation;
|
|
491
499
|
for (let step = 0; step < 64; step += 1) {
|
|
492
500
|
artifacts.stage("build-next", { step });
|
|
493
|
-
const
|
|
494
|
-
const
|
|
501
|
+
const stored = planned.transactions[step];
|
|
502
|
+
const reviewedTransaction = stored?.gas === undefined || stored.gasPrice === undefined ? undefined : stored;
|
|
503
|
+
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, reviewedTransaction, signal });
|
|
495
504
|
if (!next) return { references, activation };
|
|
496
|
-
assert.
|
|
497
|
-
for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "nonce", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
|
|
505
|
+
if (reviewedTransaction) for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved exact envelope`);
|
|
498
506
|
if (retainedReferences.length) {
|
|
499
507
|
const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
|
|
500
508
|
assertRetainedExampleReceipts({ progress, references, artifacts });
|
|
@@ -509,11 +517,11 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
509
517
|
assert.ok(functionName, "Native-input conversion cases do not permit unrelated approval/cancellation writes");
|
|
510
518
|
const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
|
|
511
519
|
assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
|
|
512
|
-
assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.admission?.admitted === true, "Actual next transaction needs
|
|
513
|
-
assert.
|
|
514
|
-
|
|
520
|
+
assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.admission?.admitted === true, "Actual next transaction needs an admitted gas envelope");
|
|
521
|
+
assert.ok(next.admission.executionProof === "proved" && next.admission.protocolFit === "proved" ||
|
|
522
|
+
next.admission.executionProof === "not-run" && next.admission.protocolFit === "profiled", "Distinguish exact execution from conditional capacity admission");
|
|
515
523
|
const ceiling = next.admission.limits.transactionGasCeiling;
|
|
516
|
-
assert.ok(next.gas
|
|
524
|
+
assert.ok(next.gas === next.admission.limits.executionGasCeiling && (ceiling === undefined || next.gas <= ceiling), "Launch gas must equal the native ceiling and fit supplied restrictions");
|
|
517
525
|
const calldataCap = next.admission.limits.maxCalldataBytes;
|
|
518
526
|
assert.ok(calldataCap === undefined || (next.data.length - 2) / 2 <= calldataCap, "Exact calldata must fit any supplied cap");
|
|
519
527
|
const envelope = transactionEnvelope(next);
|
|
@@ -549,7 +557,7 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
549
557
|
addressEqual(includedBlock?.hash, receipt.blockHash, "Canonical included block");
|
|
550
558
|
const latest = BigInt(await client.request({ method: "eth_blockNumber" }));
|
|
551
559
|
assert.ok(latest >= BigInt(receipt.blockNumber) + BigInt(planned.confirmations) - 1n, "Required confirmations must be included canonically");
|
|
552
|
-
const reference = { transactionHash: row.transactionHash, observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: planned.confirmations };
|
|
560
|
+
const reference = { transactionHash: row.transactionHash, nonce: BigInt(row.transaction.nonce), observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: planned.confirmations };
|
|
553
561
|
row.status = "confirmed"; row.reference = reference; artifacts.saveReceipts();
|
|
554
562
|
references.push(reference);
|
|
555
563
|
if (["atomic", "activate"].includes(next.kind)) {
|
|
@@ -883,7 +891,8 @@ async function executeExample({ fixture, privateKey, scenario, sdk, artifacts, s
|
|
|
883
891
|
const { plan } = planned;
|
|
884
892
|
artifacts.save("planning.json", planned);
|
|
885
893
|
artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, prerequisites: planned.prerequisites, transactions: planned.transactions });
|
|
886
|
-
|
|
894
|
+
if (scenario.mode === "staged") assert.equal(planned.mode, "staged", "Explicit staged mode is retained");
|
|
895
|
+
addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
|
|
887
896
|
if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
|
|
888
897
|
const usd = (value) => `${value / UNIT}.${(value % UNIT).toString().padStart(18, "0")}`;
|
|
889
898
|
const fundingSummary = economicReview.quotes.every((quote) => quote.conversion.nativeWrap === true)
|
|
@@ -894,7 +903,7 @@ async function executeExample({ fixture, privateKey, scenario, sdk, artifacts, s
|
|
|
894
903
|
console.log(`Economic review ${reviewHash}\n${metadata.name} (${metadata.symbol}); target USD ${usd(economicReview.targetMarketCapUsdX18)}; minted supply ${usd(economicReview.actualMintedSupply)}\n` +
|
|
895
904
|
economicReview.markets.map((market) => `Market ${market.marketIndex} ${market.venue}/${market.quoteSymbol}: retained-supply opening valuation USD ${usd(market.retainedSupplyOpeningValuationUsdX18)} (minted-supply USD ${usd(market.openingValuationUsdX18)}), token USD ${usd(market.unitPriceUsdX18)}, token ${market.quoteSymbol} ${usd(market.unitPriceQuoteX18)}, ${market.positions.length} positions`).join("\n") +
|
|
896
905
|
`\nExpected rounding burn ${economicReview.expectedUnusedBurn} raw token units; ${planned.plan.buys.length} ordered buys, native input ${fixture.buyAmountWei} wei each (${fundingSummary}); 50 bps conversion and ordered-buy protection.\nLP fee 3000 pips; V4 hook max 10000/min 1000 pips (fixed maximum schedule); executor 275 bps.\nImage ${image ? `${image.descriptor.contentType}, ${image.descriptor.contentLength} bytes, ${image.descriptor.sha256}` : "none (metadata only)"}.\nReview allocations, fees and exact protected buys: ${artifacts.directory}/economic-review.json`);
|
|
897
|
-
console.log(economicReview.initialBuy.
|
|
906
|
+
console.log(economicReview.initialBuy.reviewedBuys.map((buy) => `Buy ${buy.buyIndex} market ${buy.marketIndex}/${buy.quoteSymbol}: quote spent ${buy.quoteSpent} raw units; ${economicReview.initialBuy.evidence} all-in USD/token ${usd(buy.allInUnitPriceUsdX18)}; fees + price-impact premium ${buy.feeAndPriceImpactPremiumBps} bps vs opening`).join("\n"));
|
|
898
907
|
for (const quote of economicReview.quotes) {
|
|
899
908
|
const seed = quote.usdSeed;
|
|
900
909
|
console.log(`${quote.symbol}/USD ${usd(seed.priceUsdX18)}; source ${seed.source}; chain ${seed.chainId}; quote ${seed.tokenAddress} (${seed.tokenDecimals} decimals); retrieved ${seed.retrievedAt}.\n` +
|
|
@@ -1054,7 +1063,8 @@ export async function resumeLaunchExample({ fixture, privateKey, sdk, artifacts,
|
|
|
1054
1063
|
assert.match(reference.transactionHash, /^0x[\da-fA-F]{64}$/, "Original receipt hash");
|
|
1055
1064
|
assert.match(reference.observedBlockHash, /^0x[\da-fA-F]{64}$/, "Original receipt block hash");
|
|
1056
1065
|
assert.ok(reference.observedBlockNumber !== undefined && reference.observedBlockNumber !== null, "Original receipt block number");
|
|
1057
|
-
|
|
1066
|
+
if (reference.nonce !== undefined) assert.ok(typeof reference.nonce === "bigint" || typeof reference.nonce === "string" && /^(0|[1-9]\d*)$/.test(reference.nonce), "Retained submitted nonce must be an exact unsigned integer");
|
|
1067
|
+
return { ...reference, ...(reference.nonce === undefined ? {} : { nonce: BigInt(reference.nonce) }), observedBlockNumber: BigInt(reference.observedBlockNumber), confirmations: reference.confirmations ?? confirmations };
|
|
1058
1068
|
});
|
|
1059
1069
|
const retainedHashes = new Set(references.map((reference) => reference.transactionHash.toLowerCase()));
|
|
1060
1070
|
if (references.some((reference) => reference.replacementHash && !same(reference.replacementHash, reference.transactionHash)) ||
|
|
@@ -46,7 +46,7 @@ async function send(transaction) {
|
|
|
46
46
|
const gasPrice = transaction.gasPrice ?? BigInt(await client.request({ method: "eth_gasPrice" }));
|
|
47
47
|
const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
|
|
48
48
|
const ceiling = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
|
|
49
|
-
const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice)
|
|
49
|
+
const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) };
|
|
50
50
|
const estimated = transaction.gas ?? ((BigInt(await client.request({ method: "eth_estimateGas", params: [{ ...wire, gas: toHex(ceiling) }] })) * BigInt(10000 + Number(policy.headroomBps)) + 9999n) / 10000n);
|
|
51
51
|
assert.ok(estimated <= ceiling, "Unsigned helper must still fit explicit local execution admission");
|
|
52
52
|
const transactionHash = await client.request({ method: "eth_sendTransaction", params: [{ ...wire, gas: toHex(estimated) }] });
|
|
@@ -103,9 +103,11 @@ for (const row of exported.plans) for (const mode of ["atomic", "staged"]) {
|
|
|
103
103
|
const receipts = [];
|
|
104
104
|
let activationReceipt;
|
|
105
105
|
for (let guard = 0; guard < 64; ++guard) {
|
|
106
|
-
const next = await buildNextTransaction({ client, planned, receipts, limits, fork
|
|
106
|
+
const next = await buildNextTransaction({ client, planned, receipts, limits, fork });
|
|
107
107
|
if (!next) break;
|
|
108
|
-
assert.equal(next.admission?.
|
|
108
|
+
assert.equal(next.admission?.admitted, true);
|
|
109
|
+
assert.ok(next.admission.executionProof === "proved" && next.admission.protocolFit === "proved" ||
|
|
110
|
+
next.admission.executionProof === "not-run" && next.admission.protocolFit === "profiled");
|
|
109
111
|
const submitted = await send(next);
|
|
110
112
|
receipts.push(submitted.reference);
|
|
111
113
|
if (["atomic", "activate"].includes(next.kind)) activationReceipt = submitted.receipt;
|
package/package.json
CHANGED
package/src/lifecycle/authors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decodeEventLog, encodeFunctionData, isAddress, zeroAddress, type Address, type Hex } from "viem";
|
|
2
2
|
import { launchLifecycleAbi, lifecycleFeeHubAbi, lifecycleFeeHubFactoryAbi, lifecycleRegistryAbi } from "./abi.js";
|
|
3
|
-
import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, readLifecycleContract, rpcQuantity } from "./rpc.js";
|
|
3
|
+
import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, rpcQuantity } from "./rpc.js";
|
|
4
4
|
import type { SourceTermsV3 } from "./schema.js";
|
|
5
5
|
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleRpcClient } from "./types.js";
|
|
6
6
|
|
|
@@ -50,7 +50,7 @@ function requirePage(offset: bigint, limit: bigint, maximum: bigint): void {
|
|
|
50
50
|
async function readAuthorContext(options: LifecycleAuthorOptions, block: LifecycleBlock): Promise<LifecycleAuthor> {
|
|
51
51
|
const { client, registry, authorId } = options;
|
|
52
52
|
requireAddress(registry, "Implementation registry"); requireAddress(authorId, "Stable author identity");
|
|
53
|
-
const chainId =
|
|
53
|
+
const chainId = await readLifecycleChainId(client);
|
|
54
54
|
const core = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block);
|
|
55
55
|
requireAddress(core, "Registry core");
|
|
56
56
|
if ((await readLifecycleContract<Address>(client, core, launchLifecycleAbi, "registry", [], block)).toLowerCase() !== registry.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Selected core does not bind this implementation registry");
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { toHex, zeroAddress, type Address } from "viem";
|
|
2
|
+
import { AUCTION_QUOTE_OPTIONS } from "../auction.js";
|
|
3
|
+
import { getTickAtSqrtRatio } from "../launch.js";
|
|
4
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig } from "./markets.js";
|
|
5
|
+
import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
|
|
6
|
+
import { getKnownLifecycleProfile, hmmV2ForwardingReserve } from "./presets.js";
|
|
7
|
+
import { lifecyclePinnedRpc, lifecycleSessionRead, nitroPosterGasBound, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity } from "./rpc.js";
|
|
8
|
+
import { LifecycleFundingKind, LifecycleRewardMode, LifecycleTokenKind, type LaunchPlanV1 } from "./schema.js";
|
|
9
|
+
import { buildLifecycleCommand } from "./transactions.js";
|
|
10
|
+
import { UFG_LAUNCH_PROXY_ADDRESS, ufgPlanPrincipal, type UfgLaunchConfig } from "./ufg-config.js";
|
|
11
|
+
import type { NextTransactionEnvelope } from "./simulation.js";
|
|
12
|
+
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLaunchGasBounds, type LifecycleOpeningBuyQuote, type LifecycleSimulation, type LifecycleTransaction, type SimulateLaunchPlanOptions } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const LIFECYCLE_GAS_PROFILE_VERSION = "robinhood-4663-arbos61-v1";
|
|
15
|
+
|
|
16
|
+
// Coverage, not token admission: unlisted currencies still use exact execution.
|
|
17
|
+
const profiledQuotes = new Set<string>();
|
|
18
|
+
for (const quote of AUCTION_QUOTE_OPTIONS) if (quote.address !== zeroAddress) profiledQuotes.add(quote.address.toLowerCase());
|
|
19
|
+
const ceil = (value: bigint, divisor: bigint) => (value + divisor - 1n) / divisor;
|
|
20
|
+
const approval = (transaction: LifecycleTransaction) => transaction.kind === "approve" || transaction.kind === "approve-reset";
|
|
21
|
+
type MarketGas = { abyss: boolean; hmm: boolean; ufgHook: boolean; positions: number; spacing: number; openingTick: number; ticks: number[] };
|
|
22
|
+
|
|
23
|
+
function covered(plan: LaunchPlanV1): boolean {
|
|
24
|
+
return plan.chainId === 4663n && hasProfiledOpeningMath(plan) &&
|
|
25
|
+
plan.markets.every((market) => profiledQuotes.has(market.quoteAsset.toLowerCase())) &&
|
|
26
|
+
plan.funding.every((funding) => funding.kind !== LifecycleFundingKind.ERC20 || profiledQuotes.has(funding.inputAsset.toLowerCase()));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function marketGas(plan: LaunchPlanV1): MarketGas[] {
|
|
30
|
+
return plan.markets.map((market) => {
|
|
31
|
+
const preset = getKnownLifecycleProfile({ chainId: plan.chainId, orchestrator: plan.orchestrator, profileId: market.profileId })!;
|
|
32
|
+
const abyss = market.configVersion === 1;
|
|
33
|
+
const config = abyss ? decodeAbyssLifecycleMarketConfig(market.config) : decodePoolBoundV4LifecycleMarketConfig(market.config, 6);
|
|
34
|
+
const price = "openingSqrtPriceX96" in config ? config.openingSqrtPriceX96 : config.sqrtPriceX96;
|
|
35
|
+
const spacing = "tickSpacing" in config ? config.tickSpacing : config.fee === 100 ? 1 : config.fee === 500 ? 10 : config.fee === 3000 ? 60 : 200;
|
|
36
|
+
const ticks = new Set<number>();
|
|
37
|
+
for (const position of config.positions) { ticks.add(position.tickLower); ticks.add(position.tickUpper); }
|
|
38
|
+
return { abyss, hmm: preset.key.includes("hmm"), ufgHook: preset.key.includes("ufg"), positions: config.positions.length,
|
|
39
|
+
spacing, openingTick: getTickAtSqrtRatio(price), ticks: [...ticks] };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function utf8Bytes(value: string): number {
|
|
44
|
+
let bytes = 0;
|
|
45
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
46
|
+
const code = value.charCodeAt(index);
|
|
47
|
+
if (code < 0x80) bytes += 1;
|
|
48
|
+
else if (code < 0x800) bytes += 2;
|
|
49
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length && value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) { bytes += 4; index += 1; }
|
|
50
|
+
else bytes += 3;
|
|
51
|
+
}
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tokenCreationGas(token: LaunchPlanV1["token"]): bigint {
|
|
56
|
+
return (token.kind === LifecycleTokenKind.ERC404 ? 2_500_000n : 0n) +
|
|
57
|
+
(token.rewardMode === LifecycleRewardMode.Staking ? 2_500_000n : token.rewardMode === LifecycleRewardMode.Dividends ? 500_000n : 0n) +
|
|
58
|
+
22_000n * BigInt(Math.ceil(utf8Bytes(token.name) / 32) + Math.ceil(utf8Bytes(token.symbol) / 32) + Math.ceil(utf8Bytes(token.metadataURI) / 32));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Capacity classification only. No value returned here ever sets a transaction gas limit. */
|
|
62
|
+
function capacityBasis(plan: LaunchPlanV1, transaction: LifecycleTransaction, markets: readonly MarketGas[], quotes: readonly LifecycleOpeningBuyQuote[], ufgRoute: boolean): bigint {
|
|
63
|
+
const atomic = transaction.kind === "atomic";
|
|
64
|
+
let gas = atomic ? 19_500_000n + tokenCreationGas(plan.token) + (ufgRoute ? 6_500_000n : 0n) : 1_000_000n;
|
|
65
|
+
for (const [index, market] of markets.entries()) {
|
|
66
|
+
if (!atomic) gas += market.abyss ? 2_500_000n + 1_600_000n * BigInt(market.positions) : 2_000_000n + 600_000n * BigInt(market.positions);
|
|
67
|
+
else gas += (index === 0 ? market.hmm ? 7_000_000n : market.ufgHook ? 1_000_000n : market.abyss ? 500_000n : 0n
|
|
68
|
+
: market.hmm ? 15_000_000n : market.ufgHook ? 9_000_000n : 8_000_000n) +
|
|
69
|
+
(market.abyss ? 1_500_000n : 600_000n) * BigInt(market.positions - 1);
|
|
70
|
+
if (!atomic && market.hmm) gas += 6_000_000n;
|
|
71
|
+
}
|
|
72
|
+
const ticks = markets.map((market) => market.openingTick);
|
|
73
|
+
for (const quote of quotes) {
|
|
74
|
+
const market = markets[quote.marketIndex]!, before = ticks[quote.marketIndex]!, after = getTickAtSqrtRatio(quote.sqrtPriceAfterX96);
|
|
75
|
+
const low = Math.min(before, after), high = Math.max(before, after);
|
|
76
|
+
const words = Math.abs(Math.floor(after / (market.spacing * 256)) - Math.floor(before / (market.spacing * 256))) + 2;
|
|
77
|
+
let crossings = 0;
|
|
78
|
+
for (const tick of market.ticks) if (tick >= low && tick <= high) crossings += 1;
|
|
79
|
+
gas += 300_000n + 8_000n * BigInt(words) + 80_000n * BigInt(crossings);
|
|
80
|
+
if (plan.token.kind === LifecycleTokenKind.ERC404) gas += 70_000n * ceil(quote.tokenOut, plan.token.nftUnit);
|
|
81
|
+
ticks[quote.marketIndex] = after;
|
|
82
|
+
}
|
|
83
|
+
return gas + 24n * BigInt((transaction.data.length - 2) / 2) + 100_000n * BigInt(plan.feeAssets.length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type CapacityOptions = { plan: LaunchPlanV1; predictedToken: Address; ufg?: { config: UfgLaunchConfig }; headroomBps?: number };
|
|
87
|
+
export type ProfiledLaunchCapacity = {
|
|
88
|
+
bounds: LifecycleLaunchGasBounds; initialBuys: readonly LifecycleOpeningBuyQuote[]; headroomBps: number;
|
|
89
|
+
atomic: LifecycleTransaction; activation: LifecycleTransaction;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Internal shared computation; callers reuse both compiled commands and ordered math. */
|
|
93
|
+
export function deriveLifecycleLaunchCapacity(options: CapacityOptions, openingBuys?: readonly LifecycleOpeningBuyQuote[]): ProfiledLaunchCapacity | undefined {
|
|
94
|
+
const { plan, ufg } = options;
|
|
95
|
+
if (!covered(plan) || plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
|
|
96
|
+
(plan.creator.toLowerCase() === UFG_LAUNCH_PROXY_ADDRESS.toLowerCase()) !== (ufg !== undefined)) return undefined;
|
|
97
|
+
const headroomBps = options.headroomBps ?? 1500;
|
|
98
|
+
if (!Number.isSafeInteger(headroomBps) || headroomBps < 0 || headroomBps > 10000) throw new LifecyclePlanningError("INVALID_LIMITS", "Headroom must be between zero and10000 basis points");
|
|
99
|
+
const initialBuys = openingBuys ?? computeLifecycleOpeningBuys(options);
|
|
100
|
+
const route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
|
|
101
|
+
const atomic = buildLifecycleCommand(plan, { kind: "atomic" }, route), activation = buildLifecycleCommand(plan, { kind: "activate" }, route);
|
|
102
|
+
const markets = marketGas(plan), reserve = hmmV2ForwardingReserve(plan);
|
|
103
|
+
const bound = (transaction: LifecycleTransaction) => {
|
|
104
|
+
const basis = capacityBasis(plan, transaction, markets, initialBuys, ufg !== undefined);
|
|
105
|
+
const buffered = ceil(basis * BigInt(10000 + headroomBps), 10000n);
|
|
106
|
+
return buffered > basis + reserve ? buffered : basis + reserve;
|
|
107
|
+
};
|
|
108
|
+
return { bounds: { atomicComputeGasUpperBound: bound(atomic), activationComputeGasUpperBound: bound(activation) }, initialBuys, headroomBps, atomic, activation };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Pure, versioned conservative compute bounds for mode/builder capacity, excluding poster gas.
|
|
112
|
+
* Undefined means unprofiled, not unavailable functionality. A large upper bound is uncertain. */
|
|
113
|
+
export function computeLifecycleLaunchGasBounds(options: CapacityOptions): LifecycleLaunchGasBounds | undefined {
|
|
114
|
+
return deriveLifecycleLaunchCapacity(options)?.bounds;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ProfiledLifecycleTransactions = { simulation: LifecycleSimulation; profileOnly: boolean };
|
|
118
|
+
|
|
119
|
+
/** Native ceilings determine every launch envelope. Profiles decide capacity only;
|
|
120
|
+
* conversions and uncertain activation shapes still require exact execution. */
|
|
121
|
+
export async function profileLifecycleTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], block: LifecycleBlock,
|
|
122
|
+
reviewed?: NextTransactionEnvelope, openingBuys?: readonly LifecycleOpeningBuyQuote[], capacity?: ProfiledLaunchCapacity): Promise<ProfiledLifecycleTransactions | undefined> {
|
|
123
|
+
const { client, planned } = options, plan = planned.plan;
|
|
124
|
+
const onlyApprovals = transactions.every(approval);
|
|
125
|
+
if (planned.chainId !== 4663n || !onlyApprovals && !covered(plan)) return undefined;
|
|
126
|
+
const context = { client, account: planned.account, orchestrator: plan.orchestrator, block, chainId: planned.chainId };
|
|
127
|
+
const observed = await resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits);
|
|
128
|
+
if (!onlyApprovals && observed.limits.arbOSVersion !== 61n) return undefined;
|
|
129
|
+
const [prices, balanceValue, accountCode] = await Promise.all([
|
|
130
|
+
readNitroPrices(client, block),
|
|
131
|
+
lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block),
|
|
132
|
+
lifecycleSessionRead(client, `${planned.chainId}:account-code:${planned.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block)),
|
|
133
|
+
]);
|
|
134
|
+
if (rpcHex(accountCode, "account code") !== "0x") return undefined;
|
|
135
|
+
options.signal?.throwIfAborted(); observed.assertCurrent(); registerLifecycleFinalCheck(client, observed.assertCurrent);
|
|
136
|
+
const { limits } = observed, baseFee = block.baseFeePerGas ?? 0n;
|
|
137
|
+
const requiredGasPrice = prices[5] > baseFee ? prices[5] : baseFee;
|
|
138
|
+
const gasPrice = reviewed?.gasPrice ?? (prices[5] > baseFee * 2n ? prices[5] : baseFee * 2n);
|
|
139
|
+
if (reviewed !== undefined && !onlyApprovals && reviewed.gas !== limits.executionGasCeiling) throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling; review the launch again");
|
|
140
|
+
const needsBuys = transactions.some((transaction) => transaction.kind === "atomic" || transaction.kind === "activate");
|
|
141
|
+
const currentCapacity = needsBuys ? capacity?.headroomBps === limits.headroomBps ? capacity : deriveLifecycleLaunchCapacity({ plan, predictedToken: planned.predictedToken, ufg: planned.ufg, headroomBps: limits.headroomBps }, openingBuys) : undefined;
|
|
142
|
+
if (needsBuys && currentCapacity === undefined) return undefined;
|
|
143
|
+
const quotes = currentCapacity?.initialBuys ?? [], bounds = currentCapacity?.bounds;
|
|
144
|
+
let profileOnly = !plan.funding.some((funding) => funding.kind === LifecycleFundingKind.Swap) ||
|
|
145
|
+
!transactions.some((transaction) => transaction.kind === "begin" || transaction.kind === "atomic" || transaction.kind === "activate");
|
|
146
|
+
for (const transaction of transactions) {
|
|
147
|
+
if (transaction.kind !== "atomic" && transaction.kind !== "activate") continue;
|
|
148
|
+
const compute = transaction.kind === "atomic" ? bounds!.atomicComputeGasUpperBound : bounds!.activationComputeGasUpperBound;
|
|
149
|
+
if (compute + nitroPosterGasBound(transaction, prices).posterGas > limits.executionGasCeiling) profileOnly = false;
|
|
150
|
+
}
|
|
151
|
+
const launch = transactions.find((transaction) => !approval(transaction));
|
|
152
|
+
const immediate = transactions[0], gasLimit = limits.executionGasCeiling;
|
|
153
|
+
const envelopeFailure = launch !== undefined && limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling;
|
|
154
|
+
const calldataFailure = transactions.find((transaction) => limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes);
|
|
155
|
+
const feeFailure = reviewed !== undefined && gasPrice < requiredGasPrice;
|
|
156
|
+
const affordabilityFailure = launch !== undefined && launch.value + gasLimit * gasPrice > rpcQuantity(balanceValue, "creator native balance");
|
|
157
|
+
const semanticFailure = quotes.find((quote) => quote.tokenOut < plan.buys[quote.buyIndex]!.minTokenOut);
|
|
158
|
+
const expired = block.timestamp > plan.deadline && transactions.some((transaction) => transaction.kind !== "cancel");
|
|
159
|
+
const refused = envelopeFailure || calldataFailure !== undefined || feeFailure || affordabilityFailure || semanticFailure !== undefined || expired;
|
|
160
|
+
const poster = immediate === undefined || approval(immediate) ? undefined : nitroPosterGasBound(immediate, prices).posterGas;
|
|
161
|
+
const simulation: LifecycleSimulation = {
|
|
162
|
+
backend: "profiled", confidence: "profiled", executionProof: "not-run", protocolFit: "profiled", admitted: profileOnly && !refused,
|
|
163
|
+
blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, steps: [], deferredTransactionIds: [],
|
|
164
|
+
...(bounds === undefined ? {} : { gasProfile: { version: LIFECYCLE_GAS_PROFILE_VERSION, bounds, initialBuys: quotes } }),
|
|
165
|
+
...(immediate === undefined || approval(immediate) ? {} : { estimate: { gasLimit, gasPrice, executionFee: gasLimit * gasPrice, dataFee: 0n, totalFee: gasLimit * gasPrice,
|
|
166
|
+
posterGas: poster, posterFee: poster! * gasPrice, dataFeeIncludedInGas: true, feeConfidence: "native-ceiling-and-data" as const } }),
|
|
167
|
+
...(envelopeFailure ? { failureCategory: "capacity" as const, capacityConstraint: "gas-envelope" as const, failedTransactionId: launch!.id, reason: "Native transaction ceiling exceeds the supplied complete gas policy" } : {}),
|
|
168
|
+
...(calldataFailure === undefined ? {} : { failureCategory: "capacity" as const, capacityConstraint: "calldata" as const, failedTransactionId: calldataFailure.id, reason: "Encoded command exceeds the caller's calldata byte policy" }),
|
|
169
|
+
...(affordabilityFailure ? { failureCategory: "affordability" as const, failedTransactionId: launch!.id, reason: "Creator native balance cannot reserve transaction value and the native gas ceiling" } : {}),
|
|
170
|
+
...(semanticFailure === undefined ? {} : { failureCategory: "semantic" as const, failedTransactionId: transactions.find((transaction) => transaction.kind === "atomic" || transaction.kind === "activate")?.id, reason: "Opening curve output is below the committed minimum" }),
|
|
171
|
+
...(expired ? { failureCategory: "semantic" as const, failedTransactionId: transactions.find((transaction) => transaction.kind !== "cancel")?.id, reason: "Launch deadline has expired at the pinned block" } : {}),
|
|
172
|
+
...(feeFailure ? { failedTransactionId: immediate?.id, reason: "Reviewed gas price is below the pinned required fee" } : {}),
|
|
173
|
+
};
|
|
174
|
+
return { simulation, profileOnly: profileOnly || refused };
|
|
175
|
+
}
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -11,9 +11,11 @@ export { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLifecycleTokenAtBlock, readLifecycle
|
|
|
11
11
|
export * from "./fork.js";
|
|
12
12
|
export * from "./authors.js";
|
|
13
13
|
export * from "./presets.js";
|
|
14
|
-
export
|
|
14
|
+
export { preparationGasEstimate, packPreparationBatches, buildLaunchTransactions } 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
18
|
export { planLaunch, simulateLaunchPlan, 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";
|
|
@@ -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;
|
|
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
|
+
}
|