@black-market/sdk 0.14.0 → 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.
Files changed (73) hide show
  1. package/README.md +4 -3
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/gas-profile.d.ts +35 -0
  5. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  6. package/dist/lifecycle/gas-profile.js +183 -0
  7. package/dist/lifecycle/gas-profile.js.map +1 -0
  8. package/dist/lifecycle/index.d.ts +3 -1
  9. package/dist/lifecycle/index.d.ts.map +1 -1
  10. package/dist/lifecycle/index.js +3 -1
  11. package/dist/lifecycle/index.js.map +1 -1
  12. package/dist/lifecycle/lens.d.ts +1 -1
  13. package/dist/lifecycle/lens.d.ts.map +1 -1
  14. package/dist/lifecycle/lens.js +1 -1
  15. package/dist/lifecycle/lens.js.map +1 -1
  16. package/dist/lifecycle/opening-buys.d.ts +13 -0
  17. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  18. package/dist/lifecycle/opening-buys.js +171 -0
  19. package/dist/lifecycle/opening-buys.js.map +1 -0
  20. package/dist/lifecycle/planner.d.ts +2 -2
  21. package/dist/lifecycle/planner.d.ts.map +1 -1
  22. package/dist/lifecycle/planner.js +178 -163
  23. package/dist/lifecycle/planner.js.map +1 -1
  24. package/dist/lifecycle/presets.d.ts +6 -1
  25. package/dist/lifecycle/presets.d.ts.map +1 -1
  26. package/dist/lifecycle/presets.js +7 -0
  27. package/dist/lifecycle/presets.js.map +1 -1
  28. package/dist/lifecycle/progress.d.ts +1 -1
  29. package/dist/lifecycle/progress.d.ts.map +1 -1
  30. package/dist/lifecycle/progress.js +21 -20
  31. package/dist/lifecycle/progress.js.map +1 -1
  32. package/dist/lifecycle/rpc.d.ts +24 -19
  33. package/dist/lifecycle/rpc.d.ts.map +1 -1
  34. package/dist/lifecycle/rpc.js +58 -50
  35. package/dist/lifecycle/rpc.js.map +1 -1
  36. package/dist/lifecycle/simulation.d.ts +3 -4
  37. package/dist/lifecycle/simulation.d.ts.map +1 -1
  38. package/dist/lifecycle/simulation.js +98 -247
  39. package/dist/lifecycle/simulation.js.map +1 -1
  40. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  41. package/dist/lifecycle/snapshot.js +1 -2
  42. package/dist/lifecycle/snapshot.js.map +1 -1
  43. package/dist/lifecycle/transactions.d.ts +19 -3
  44. package/dist/lifecycle/transactions.d.ts.map +1 -1
  45. package/dist/lifecycle/transactions.js +25 -20
  46. package/dist/lifecycle/transactions.js.map +1 -1
  47. package/dist/lifecycle/types.d.ts +53 -45
  48. package/dist/lifecycle/types.d.ts.map +1 -1
  49. package/dist/lifecycle/types.js.map +1 -1
  50. package/dist/lifecycle/ufg.d.ts.map +1 -1
  51. package/dist/lifecycle/ufg.js +3 -7
  52. package/dist/lifecycle/ufg.js.map +1 -1
  53. package/docs/launch-lifecycle.md +71 -82
  54. package/docs/lifecycle-gas-profiles.md +90 -0
  55. package/docs/reference.md +53 -51
  56. package/examples/README.md +13 -10
  57. package/examples/launch-example.ts +35 -25
  58. package/examples/launch-lifecycle-smoke.ts +5 -3
  59. package/package.json +1 -1
  60. package/src/lifecycle/authors.ts +2 -2
  61. package/src/lifecycle/gas-profile.ts +175 -0
  62. package/src/lifecycle/index.ts +3 -1
  63. package/src/lifecycle/lens.ts +1 -1
  64. package/src/lifecycle/opening-buys.ts +157 -0
  65. package/src/lifecycle/planner.ts +164 -151
  66. package/src/lifecycle/presets.ts +12 -1
  67. package/src/lifecycle/progress.ts +19 -20
  68. package/src/lifecycle/rpc.ts +56 -44
  69. package/src/lifecycle/simulation.ts +85 -231
  70. package/src/lifecycle/snapshot.ts +1 -2
  71. package/src/lifecycle/transactions.ts +33 -21
  72. package/src/lifecycle/types.ts +52 -42
  73. 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
- if (!activationStep?.success || !activationStep.returnData) throw exampleError("MISSING_ECONOMIC_OUTPUTS", "Economic review requires the admitted activation receipt and every ordered buy output");
313
- const simulatedReceipt = decodeFunctionResult({ abi: sdk.launchLifecycleAbi,
314
- functionName: activationTransaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", data: activationStep.returnData });
315
- assert.equal(simulatedReceipt.planHash.toLowerCase(), planned.planHash.toLowerCase(), "Economic receipt exact plan");
316
- assert.equal(simulatedReceipt.tokenOut.length, planned.plan.buys.length, "Economic receipt ordered outputs");
317
- const simulatedBuys = planned.plan.buys.map((buy, index) => {
318
- const tokenOut = simulatedReceipt.tokenOut[index];
319
- const quoteSpent = simulatedReceipt.quoteSpent[index];
320
- assert.ok(tokenOut >= buy.minTokenOut && tokenOut > 0n && quoteSpent <= buy.quoteAmountIn, "Exact protected simulated buy");
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: "admitted-simulation-all-in-average-including-fees-and-price-impact" };
336
+ priceBasis: `${buyEvidence}-all-in-average-including-fees-and-price-impact` };
329
337
  });
330
338
  const economicReview = {
331
- schema: "black-market.launch-economic-review.v1", caseId: scenario.id, chainId: fixture.chainId,
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, simulatedBuys,
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: scenario.mode, confirmations, executionLimits: fixture.executionLimits, economicReview: { ...economicReview, reviewHash },
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 reviewedTransaction = planned.transactions[step];
494
- const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client, reviewedTransaction, signal });
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.ok(reviewedTransaction, "No transaction outside the saved review may be submitted");
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 stateful gas/envelope admission");
513
- assert.equal(next.admission.executionProof, "proved"); assert.equal(next.admission.protocolFit, "proved");
514
- assert.equal(next.admission.transportPreflight, "passed", "Exact immediate-next submission RPC preflight is required before signing");
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 > 0n && (ceiling === undefined || next.gas <= ceiling), "Complete gas envelope must fit supplied restrictions; Nitro compute is separately proved");
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
- assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
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.simulatedBuys.map((buy) => `Buy ${buy.buyIndex} market ${buy.marketIndex}/${buy.quoteSymbol}: quote spent ${buy.quoteSpent} raw units; simulated all-in USD/token ${usd(buy.allInUnitPriceUsdX18)}; fees + price-impact premium ${buy.feeAndPriceImpactPremiumBps} bps vs opening`).join("\n"));
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
- return { ...reference, observedBlockNumber: BigInt(reference.observedBlockNumber), confirmations: reference.confirmations ?? confirmations };
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), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) };
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, submissionClient: client });
106
+ const next = await buildNextTransaction({ client, planned, receipts, limits, fork });
107
107
  if (!next) break;
108
- assert.equal(next.admission?.transportPreflight, "passed");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@black-market/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "TypeScript SDK for Abyss token launches, swaps, and lending.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 = rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "author chain ID");
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
+ }
@@ -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 * from "./transactions.js";
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";
@@ -2,7 +2,7 @@ import { type Address, type Hex } from "viem";
2
2
  import { launchPlanV1Components, launchProgressV1Components, preparedMarketV1Components, marketLiveStateV1Components, type LaunchProgressV1, type PreparedMarketV1, type MarketLiveStateV1 } from "./schema.js";
3
3
  import type { LifecycleBlock, LifecycleRpcClient } from "./types.js";
4
4
 
5
- /** ILaunchLens.sol, versioned planning snapshot. No preset lens is deployed by this release. */
5
+ /** ILaunchLens.sol, versioned planning snapshot. Its address is owned by the deployment preset. */
6
6
  export const launchLensAbi = [{ type: "function", name: "snapshot", stateMutability: "view",
7
7
  inputs: [{ name: "plan", type: "tuple", components: launchPlanV1Components }, { name: "owner", type: "address" },
8
8
  { name: "fundingEscrow", type: "address" }, { name: "tokens", type: "address[]" }, { name: "maxAttempts", type: "uint256" }],
@@ -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
+ }