@black-market/sdk 0.14.1 → 0.15.1

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