@exponent-labs/market-three-math 0.9.16 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.18
4
+
5
+ - Version bump only for package @exponent-labs/market-three-math
6
+
3
7
  ## 0.1.0
4
8
 
5
9
  - Initial release
@@ -2,46 +2,19 @@
2
2
  * CLMM Add Liquidity simulation
3
3
  * Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
4
4
  */
5
- import { Ticks } from "@exponent-labs/exponent-fetcher";
6
- import { AddLiquidityArgs, AddLiquidityOutcome, CrossingScaleParams, CrossingTickState, LiquidityNeeds, MarketThreeState } from "./types";
5
+ import type { LpPositionCLMM } from "@exponent-labs/exponent-fetcher";
6
+ import { AddLiquidityArgs, AddLiquidityOutcome, LiquidityNeeds, MarketThreeState } from "./types";
7
7
  import { EffSnap } from "./utilsV2";
8
- /**
9
- * Get crossing tick state and price boundaries from Ticks.
10
- * Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
11
- */
12
- export declare function getCrossingTickStateFromTicks(ticks: Ticks): {
13
- crossingTickState: CrossingTickState;
14
- crossingTickPriceLeft: number;
15
- crossingTickPriceRight: number;
8
+ type AddLiquiditySimulationOptions = {
9
+ existingPosition?: LpPositionCLMM;
16
10
  };
17
11
  /**
18
12
  * Compute liquidity target and token needs based on position range and budgets
19
13
  * Ported from compute_liquidity_target_and_token_needs in math.rs
20
14
  */
21
15
  export declare function computeLiquidityTargetAndTokenNeeds(snap: EffSnap, spotPriceCurrent: number, priceEffLower: number, priceEffUpper: number, lowerPrice: number, upperPrice: number, lowerTickIdx: number, upperTickIdx: number, currentIndex: number, maxSy: number, maxPt: number, epsilonClamp: number): LiquidityNeeds;
22
- /**
23
- * Scale crossing tick token inputs from original max values to prevent double trimming.
24
- *
25
- * When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
26
- * 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
27
- * 2. Then by AMM formula in simulateAddLiquidityProportional
28
- *
29
- * This function calculates the segment's proportion of the total distribution and scales
30
- * the original max values accordingly, ensuring maximum token utilization.
31
- *
32
- * @param scaleParams - Original max values and CLMM distribution extents
33
- * @param duPtPart - PT segment length (spot price delta for this segment)
34
- * @param syPerL1 - SY per unit liquidity for this segment
35
- * @param clmmPtDelta - PT amount calculated from CLMM formula
36
- * @param clmmSyDelta - SY amount calculated from CLMM formula
37
- * @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
38
- */
39
- export declare function scaleCrossingTickInputs(scaleParams: CrossingScaleParams, duPtPart: number, syPerL1: number, clmmPtDelta: number, clmmSyDelta: number): [number, number];
40
- /**
41
- * Compute token needs with crossing tick adjustment
42
- * This matches the Rust compute_token_needs_with_crossing function
43
- */
44
- export declare function computeTokenNeedsWithCrossing(snap: EffSnap, spotPriceCurrent: number, priceEffLower: number, priceEffUpper: number, lowerPrice: number, upperPrice: number, maxSy: number, maxPt: number, epsilonClamp: number, crossingTickState: CrossingTickState, crossingTickPriceLeft: number, crossingTickPriceRight: number): [number, number];
16
+ /** Compute curve-level token needs without raw crossing-tick cap scaling. */
17
+ export declare function computeTokenNeedsWithCrossing(snap: EffSnap, spotPriceCurrent: number, priceEffLower: number, priceEffUpper: number, lowerPrice: number, upperPrice: number, maxSy: number, maxPt: number, epsilonClamp: number): [number, number];
45
18
  /**
46
19
  * Simulate adding liquidity to the CLMM market
47
20
  * This is a pure function that does not mutate the market state
@@ -69,12 +42,6 @@ export declare function calcDepositSyAndPtFromBaseAmount(params: {
69
42
  expirationTs: number;
70
43
  currentSpotPrice: number;
71
44
  syExchangeRate: number;
72
- /** Optional crossing tick state for accurate ratio prediction.
73
- * When provided, the prediction accounts for the existing PT/SY proportions
74
- * in the active tick, matching the on-chain behaviour more closely. */
75
- crossingTickState?: CrossingTickState;
76
- crossingTickPriceLeft?: number;
77
- crossingTickPriceRight?: number;
78
45
  epsilonClamp?: number;
79
46
  }): {
80
47
  syNeeded: number;
@@ -94,9 +61,10 @@ export declare function calcDepositSyAndPtFromBaseAmount(params: {
94
61
  * @param lowerTick - Lower tick (APY in basis points)
95
62
  * @param upperTick - Upper tick (APY in basis points)
96
63
  * @param syExchangeRate - SY exchange rate
64
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
97
65
  * @returns Simulation result with LP out, YT out, amounts spent, etc.
98
66
  */
99
- export declare function simulateWrapperProvideLiquidity(marketState: MarketThreeState, amountBase: number, lowerTick: number, upperTick: number, syExchangeRate: number): {
67
+ export declare function simulateWrapperProvideLiquidity(marketState: MarketThreeState, amountBase: number, lowerTick: number, upperTick: number, syExchangeRate: number, options?: AddLiquiditySimulationOptions): {
100
68
  lpOut: number;
101
69
  ytOut: number;
102
70
  syToStrip: number;
@@ -118,9 +86,10 @@ export declare function simulateWrapperProvideLiquidity(marketState: MarketThree
118
86
  * @param lowerTick - Lower tick key (APY in basis points)
119
87
  * @param upperTick - Upper tick key (APY in basis points)
120
88
  * @param syExchangeRate - SY exchange rate
89
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
121
90
  * @returns Simulation result with LP out, PT to buy, SY constraint, etc.
122
91
  */
123
- export declare function simulateSwapAndSupply(marketState: MarketThreeState, amountBase: number, lowerTick: number, upperTick: number, syExchangeRate: number): {
92
+ export declare function simulateSwapAndSupply(marketState: MarketThreeState, amountBase: number, lowerTick: number, upperTick: number, syExchangeRate: number, options?: AddLiquiditySimulationOptions): {
124
93
  lpOut: number;
125
94
  ptToBuy: number;
126
95
  syConstraint: number;
@@ -130,3 +99,4 @@ export declare function simulateSwapAndSupply(marketState: MarketThreeState, amo
130
99
  syDeposited: number;
131
100
  ptDeposited: number;
132
101
  } | null;
102
+ export {};
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getCrossingTickStateFromTicks = getCrossingTickStateFromTicks;
4
3
  exports.computeLiquidityTargetAndTokenNeeds = computeLiquidityTargetAndTokenNeeds;
5
- exports.scaleCrossingTickInputs = scaleCrossingTickInputs;
6
4
  exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
7
5
  exports.simulateAddLiquidity = simulateAddLiquidity;
8
6
  exports.calculateLpOut = calculateLpOut;
@@ -10,9 +8,9 @@ exports.estimateBalancedDeposit = estimateBalancedDeposit;
10
8
  exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
11
9
  exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
12
10
  exports.simulateSwapAndSupply = simulateSwapAndSupply;
11
+ const existingPositionEqualization_1 = require("./existingPositionEqualization");
13
12
  const swapV2_1 = require("./swapV2");
14
13
  const types_1 = require("./types");
15
- const utils_1 = require("./utils");
16
14
  const utilsV2_1 = require("./utilsV2");
17
15
  const TICK_KEY_BASE_POINTS = 1_000_000;
18
16
  // Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
@@ -21,44 +19,13 @@ const GAP_TOKEN_NEEDS = 20;
21
19
  const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100;
22
20
  const SWAP_EXACT_OUT_SY_HEADROOM = 100;
23
21
  const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000;
24
- const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000;
25
22
  const FLASH_SY_POSITION_HEADROOM = 1_000;
26
23
  const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000;
27
24
  const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0;
28
25
  const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000;
29
26
  const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0;
30
- const EMPTY_CROSSING_TICK_STATE = {
31
- principalPt: 0,
32
- principalSy: 0,
33
- principalShareSupply: 0,
34
- };
35
- /**
36
- * Get crossing tick state and price boundaries from Ticks.
37
- * Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
38
- */
39
- function getCrossingTickStateFromTicks(ticks) {
40
- const currentTickNode = ticks.ticksTree[ticks.currentTick - 1];
41
- if (!currentTickNode || ticks.currentTick <= 0) {
42
- return {
43
- crossingTickState: EMPTY_CROSSING_TICK_STATE,
44
- crossingTickPriceLeft: 0,
45
- crossingTickPriceRight: 0,
46
- };
47
- }
48
- const crossingTickPriceLeft = currentTickNode.impliedRate;
49
- const successorKey = (0, utils_1.getSuccessorTickKey)(ticks, currentTickNode.apyBasePoints);
50
- const successorTick = successorKey != null ? (0, utils_1.findTickByKey)(ticks, successorKey)?.tick : null;
51
- const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY;
52
- return {
53
- crossingTickState: {
54
- principalPt: Number(currentTickNode.principalPt),
55
- principalSy: Number(currentTickNode.principalSy),
56
- principalShareSupply: Number(currentTickNode.principalShareSupply),
57
- },
58
- crossingTickPriceLeft,
59
- crossingTickPriceRight,
60
- };
61
- }
27
+ const ZERO_BIGINT = 0n;
28
+ const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
62
29
  /**
63
30
  * Compute liquidity target and token needs based on position range and budgets
64
31
  * Ported from compute_liquidity_target_and_token_needs in math.rs
@@ -78,11 +45,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
78
45
  ptNeeded: 0,
79
46
  priceSplitForNeed: lowerPrice,
80
47
  priceSplitTickIdx: lowerTickIdx,
81
- // No crossing possible when below range
82
- originalMaxSy: maxSy,
83
- originalMaxPt: maxPt,
84
- duLeftTotal: 0,
85
- deltaCRightTotal: deltaCTotal,
86
48
  };
87
49
  }
88
50
  else if (spotPriceCurrent >= upperPrice) {
@@ -97,11 +59,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
97
59
  ptNeeded: Math.floor(ptNeed),
98
60
  priceSplitForNeed: upperPrice,
99
61
  priceSplitTickIdx: upperTickIdx,
100
- // No crossing possible when above range
101
- originalMaxSy: maxSy,
102
- originalMaxPt: maxPt,
103
- duLeftTotal: duTotal,
104
- deltaCRightTotal: 0,
105
62
  };
106
63
  }
107
64
  else {
@@ -120,75 +77,29 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
120
77
  ptNeeded: Math.floor(ptNeed),
121
78
  priceSplitForNeed: spotPriceCurrent,
122
79
  priceSplitTickIdx: currentIndex,
123
- // Store for crossing tick scaling
124
- originalMaxSy: maxSy,
125
- originalMaxPt: maxPt,
126
- duLeftTotal: duLeft,
127
- deltaCRightTotal: deltaCRight,
128
80
  };
129
81
  }
130
82
  }
131
- /**
132
- * Scale crossing tick token inputs from original max values to prevent double trimming.
133
- *
134
- * When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
135
- * 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
136
- * 2. Then by AMM formula in simulateAddLiquidityProportional
137
- *
138
- * This function calculates the segment's proportion of the total distribution and scales
139
- * the original max values accordingly, ensuring maximum token utilization.
140
- *
141
- * @param scaleParams - Original max values and CLMM distribution extents
142
- * @param duPtPart - PT segment length (spot price delta for this segment)
143
- * @param syPerL1 - SY per unit liquidity for this segment
144
- * @param clmmPtDelta - PT amount calculated from CLMM formula
145
- * @param clmmSyDelta - SY amount calculated from CLMM formula
146
- * @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
147
- */
148
- function scaleCrossingTickInputs(scaleParams, duPtPart, syPerL1, clmmPtDelta, clmmSyDelta) {
149
- const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams;
150
- if (duLeftTotal > 0 && deltaCRightTotal > 0) {
151
- // Calculate segment's proportion of total distribution
152
- const ptSegmentRatio = duPtPart / duLeftTotal;
153
- const sySegmentRatio = syPerL1 / deltaCRightTotal;
154
- // Scale original max values by segment proportion
155
- const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio);
156
- const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio);
157
- // Use max of CLMM-calculated value and scaled original value
158
- // This ensures we don't lose tokens due to double trimming
159
- return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)];
160
- }
161
- // Fallback to original logic if no crossing scale params
162
- return [clmmPtDelta, clmmSyDelta];
83
+ function toU64BigInt(value) {
84
+ return BigInt(Math.max(0, Math.floor(value)));
163
85
  }
164
- /**
165
- * Simulate the proportional add_liquidity logic
166
- * Returns [usedPt, usedSy] based on existing tick proportions
167
- */
168
- function simulateAddLiquidityProportional(intentPt, intentSy, marketTotalPt, marketTotalSy, marketTotalLp) {
169
- if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
170
- // Empty tick - use all intended amounts
171
- return [intentPt, intentSy];
172
- }
173
- const lpFromPt = (marketTotalLp * intentPt) / marketTotalPt;
174
- const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy;
175
- if (lpFromPt < lpFromSy) {
176
- // PT is the limiting factor
177
- const lpTokensOut = lpFromPt;
178
- const usedPt = intentPt;
179
- const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp);
180
- return [usedPt, usedSy];
86
+ function lamportsNumberToBigInt(value, label) {
87
+ const lamports = Math.max(0, Math.floor(value));
88
+ if (!Number.isSafeInteger(lamports)) {
89
+ throw new Error(`${label} exceeds JS safe integer range: ${value}`);
181
90
  }
182
- else {
183
- // SY is the limiting factor
184
- const lpTokensOut = lpFromSy;
185
- const usedSy = intentSy;
186
- const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp);
187
- return [usedPt, usedSy];
91
+ return BigInt(lamports);
92
+ }
93
+ function safeLamportsBigIntToNumber(value, label) {
94
+ if (value < ZERO_BIGINT || value > MAX_SAFE_INTEGER_BIGINT) {
95
+ throw new Error(`${label} exceeds JS safe integer range: ${value.toString()}`);
188
96
  }
97
+ return Number(value);
189
98
  }
190
- function toU64BigInt(value) {
191
- return BigInt(Math.max(0, Math.floor(value)));
99
+ function finalizeEqualizedSpend(params) {
100
+ const grossSpend = lamportsNumberToBigInt(params.simulatedSpend, params.label) + params.fixedSpent;
101
+ const finalSpend = grossSpend > params.fixedReleased ? grossSpend - params.fixedReleased : ZERO_BIGINT;
102
+ return safeLamportsBigIntToNumber(finalSpend, params.label);
192
103
  }
193
104
  function ceilDivBigInt(numerator, denominator) {
194
105
  if (denominator <= 0n) {
@@ -223,6 +134,78 @@ function getVirtualTickState(key, ticksWrapper, virtualStates) {
223
134
  virtualStates.set(key, state);
224
135
  return state;
225
136
  }
137
+ function ceilNormalizedPrincipal(value, cap) {
138
+ if (!Number.isFinite(value) || value < 0) {
139
+ throw new Error("Invalid active tick normalization value");
140
+ }
141
+ const capNumber = Number(cap);
142
+ if (value >= capNumber) {
143
+ return cap;
144
+ }
145
+ return BigInt(Math.min(Math.ceil(value), capNumber));
146
+ }
147
+ function computeNormalizedActiveTickPrincipals(params) {
148
+ const { principalPt, principalSy, ptPerL, syPerL } = params;
149
+ if (!Number.isFinite(ptPerL) || !Number.isFinite(syPerL) || ptPerL < 0 || syPerL < 0) {
150
+ throw new Error("Invalid active tick normalization ratio");
151
+ }
152
+ if (ptPerL === 0 && syPerL === 0) {
153
+ return { normalizedPt: 0n, normalizedSy: 0n };
154
+ }
155
+ if (ptPerL === 0) {
156
+ return { normalizedPt: 0n, normalizedSy: principalSy };
157
+ }
158
+ if (syPerL === 0) {
159
+ return { normalizedPt: principalPt, normalizedSy: 0n };
160
+ }
161
+ const lFromPt = Number(principalPt) / ptPerL;
162
+ const lFromSy = Number(principalSy) / syPerL;
163
+ if (!Number.isFinite(lFromPt) || !Number.isFinite(lFromSy)) {
164
+ throw new Error("Invalid active tick normalization liquidity");
165
+ }
166
+ if (lFromPt <= lFromSy) {
167
+ return {
168
+ normalizedPt: principalPt,
169
+ normalizedSy: ceilNormalizedPrincipal(lFromPt * syPerL, principalSy),
170
+ };
171
+ }
172
+ return {
173
+ normalizedPt: ceilNormalizedPrincipal(lFromSy * ptPerL, principalPt),
174
+ normalizedSy: principalSy,
175
+ };
176
+ }
177
+ function normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey) {
178
+ if (ticksWrapper.currentPrefixSum === 0n) {
179
+ return;
180
+ }
181
+ const currentTickKey = ticksWrapper.currentTickKey;
182
+ if (lowerTickKey > currentTickKey || currentTickKey >= upperTickKey) {
183
+ return;
184
+ }
185
+ const rightTickKey = ticksWrapper.successorKey(currentTickKey);
186
+ if (rightTickKey == null) {
187
+ return;
188
+ }
189
+ const spotPrice = ticksWrapper.currentSpotPrice;
190
+ const leftPrice = ticksWrapper.getSpotPrice(currentTickKey);
191
+ const rightPrice = ticksWrapper.getSpotPrice(rightTickKey);
192
+ if (spotPrice < leftPrice || spotPrice > rightPrice) {
193
+ return;
194
+ }
195
+ const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentTickKey);
196
+ if (principalPt === 0n && principalSy === 0n) {
197
+ return;
198
+ }
199
+ const ptPerL = Math.max(spotPrice - leftPrice, 0);
200
+ const syPerL = spotPrice >= rightPrice ? 0 : snap.getEffectivePrice(spotPrice) - snap.getEffectivePrice(rightPrice);
201
+ const { normalizedPt, normalizedSy } = computeNormalizedActiveTickPrincipals({
202
+ principalPt,
203
+ principalSy,
204
+ ptPerL,
205
+ syPerL,
206
+ });
207
+ ticksWrapper.setPrincipals(currentTickKey, normalizedPt, normalizedSy);
208
+ }
226
209
  function simulateMintSharesForTickPrestateCalcUsed(params) {
227
210
  const { key, dptIn, dsyIn, ticksWrapper, virtualStates } = params;
228
211
  const state = getVirtualTickState(key, ticksWrapper, virtualStates);
@@ -310,10 +293,11 @@ function simulateMintSharesForTickPrestateCalcUsed(params) {
310
293
  return { minted, usedPt, usedSy };
311
294
  }
312
295
  function simulateAccruePrincipalForDeposit(params) {
313
- const { ticks, snap, lowerPrice, upperPrice, priceSplitForNeed, splitTickKey, lowerTickKey, upperTickKey, liquidityTarget, originalMaxSy, originalMaxPt, } = params;
296
+ const { ticks, snap, lowerPrice, upperPrice, priceSplitForNeed, splitTickKey, lowerTickKey, upperTickKey, liquidityTarget, } = params;
314
297
  const ticksWrapper = new utilsV2_1.TicksWrapper(ticks);
315
298
  ticksWrapper.upsertBoundaryTick(lowerTickKey, snap);
316
299
  ticksWrapper.upsertBoundaryTick(upperTickKey, snap);
300
+ normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey);
317
301
  const virtualStates = new Map();
318
302
  let totalSySpend = 0n;
319
303
  let totalPtSpend = 0n;
@@ -363,9 +347,7 @@ function simulateAccruePrincipalForDeposit(params) {
363
347
  if (isCrossing) {
364
348
  pendingCrossPt = {
365
349
  leftKey,
366
- rightKey,
367
350
  ptDelta: principalPtDelta,
368
- duPart: segmentLength,
369
351
  };
370
352
  return;
371
353
  }
@@ -400,10 +382,7 @@ function simulateAccruePrincipalForDeposit(params) {
400
382
  const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed;
401
383
  if (isCrossing) {
402
384
  pendingCrossSy = {
403
- leftKey,
404
- rightKey,
405
385
  syDelta: principalSyDelta,
406
- syPerL,
407
386
  };
408
387
  return;
409
388
  }
@@ -421,14 +400,10 @@ function simulateAccruePrincipalForDeposit(params) {
421
400
  totalSySpend += minted.usedSy;
422
401
  });
423
402
  if (pendingCrossPt && pendingCrossSy) {
424
- const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS));
425
- const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS));
426
- const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft;
427
- const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft;
428
403
  const minted = simulateMintSharesForTickPrestateCalcUsed({
429
404
  key: pendingCrossPt.leftKey,
430
- dptIn: scaledPtIn,
431
- dsyIn: scaledSyIn,
405
+ dptIn: pendingCrossPt.ptDelta,
406
+ dsyIn: pendingCrossSy.syDelta,
432
407
  ticksWrapper,
433
408
  virtualStates,
434
409
  });
@@ -445,11 +420,8 @@ function simulateAccruePrincipalForDeposit(params) {
445
420
  }
446
421
  return { sySpent, ptSpent };
447
422
  }
448
- /**
449
- * Compute token needs with crossing tick adjustment
450
- * This matches the Rust compute_token_needs_with_crossing function
451
- */
452
- function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, priceEffUpper, lowerPrice, upperPrice, maxSy, maxPt, epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight) {
423
+ /** Compute curve-level token needs without raw crossing-tick cap scaling. */
424
+ function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, priceEffUpper, lowerPrice, upperPrice, maxSy, maxPt, epsilonClamp) {
453
425
  // Below range: SY only
454
426
  if (spotPriceCurrent <= lowerPrice) {
455
427
  const deltaCTotal = priceEffLower - priceEffUpper;
@@ -471,45 +443,8 @@ function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, pr
471
443
  const liquidityFromPt = maxPt / duLeft;
472
444
  const liquidityFromSy = maxSy / deltaCRight;
473
445
  const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy);
474
- let ptNeed = liquidityTarget * duLeft;
475
- let syNeed = liquidityTarget * deltaCRight;
476
- // Apply crossing tick adjustment if the tick has existing liquidity
477
- // Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
478
- const isCrossingTickActive = crossingTickState.principalShareSupply > 0 &&
479
- crossingTickPriceLeft < spotPriceCurrent &&
480
- crossingTickPriceRight > spotPriceCurrent;
481
- if (isCrossingTickActive) {
482
- // Calculate PT and SY portions that would go into the crossing tick
483
- // PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
484
- const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice);
485
- const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment);
486
- // SY portion: from spotPriceCurrent to crossingTickPriceRight
487
- const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft);
488
- const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice);
489
- const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd);
490
- const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0));
491
- if (crossingPtIntended > 0 && crossingSyIntended > 0) {
492
- // Tokens already allocated to non-crossing segments before crossing processing.
493
- const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0);
494
- const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0);
495
- // Apply scaling from original max values to prevent double trimming
496
- const [scaledPtIn, scaledSyIn] = duLeft > 0 && deltaCRight > 0
497
- ? (() => {
498
- const totalPtSpend = Math.ceil(ptOutsideCrossing);
499
- const totalSySpend = Math.ceil(syOutsideCrossing);
500
- const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0);
501
- const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0);
502
- // Use max of CLMM-calculated value and scaled original value
503
- return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)];
504
- })()
505
- : [crossingPtIntended, crossingSyIntended];
506
- // Simulate add_liquidity proportional logic with scaled inputs
507
- const [usedPt, usedSy] = simulateAddLiquidityProportional(scaledPtIn, scaledSyIn, crossingTickState.principalPt, crossingTickState.principalSy, crossingTickState.principalShareSupply);
508
- // Adjust needs based on what would actually be used
509
- ptNeed = ptOutsideCrossing + usedPt;
510
- syNeed = syOutsideCrossing + usedSy;
511
- }
512
- }
446
+ const ptNeed = liquidityTarget * duLeft;
447
+ const syNeed = liquidityTarget * deltaCRight;
513
448
  return [Math.ceil(syNeed), Math.ceil(ptNeed)];
514
449
  }
515
450
  /**
@@ -546,8 +481,6 @@ function simulateAddLiquidity(marketState, args) {
546
481
  lowerTickKey: args.lowerTick,
547
482
  upperTickKey: args.upperTick,
548
483
  liquidityTarget: liquidityNeeds.liquidityTarget,
549
- originalMaxSy: liquidityNeeds.originalMaxSy,
550
- originalMaxPt: liquidityNeeds.originalMaxPt,
551
484
  });
552
485
  // Enforce budgets
553
486
  const sySpent = syNeededWithCrossing;
@@ -564,6 +497,37 @@ function simulateAddLiquidity(marketState, args) {
564
497
  ptSpent,
565
498
  };
566
499
  }
500
+ function simulateAddLiquidityForPosition(marketState, args, existingPosition) {
501
+ if (!existingPosition) {
502
+ return simulateAddLiquidity(marketState, args);
503
+ }
504
+ const budgetEffect = (0, existingPositionEqualization_1.computeExistingPositionBudgetEffect)({
505
+ ticks: marketState.ticks,
506
+ position: existingPosition,
507
+ userMaxSy: lamportsNumberToBigInt(args.maxSy, "user max SY"),
508
+ userMaxPt: lamportsNumberToBigInt(args.maxPt, "user max PT"),
509
+ });
510
+ const addLiquidityResult = simulateAddLiquidity(marketState, {
511
+ ...args,
512
+ maxSy: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxSy, "effective max SY"),
513
+ maxPt: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxPt, "effective max PT"),
514
+ });
515
+ return {
516
+ deltaL: addLiquidityResult.deltaL,
517
+ sySpent: finalizeEqualizedSpend({
518
+ simulatedSpend: addLiquidityResult.sySpent,
519
+ fixedSpent: budgetEffect.fixedSySpent,
520
+ fixedReleased: budgetEffect.fixedSyReleased,
521
+ label: "equalized SY spend",
522
+ }),
523
+ ptSpent: finalizeEqualizedSpend({
524
+ simulatedSpend: addLiquidityResult.ptSpent,
525
+ fixedSpent: budgetEffect.fixedPtSpent,
526
+ fixedReleased: budgetEffect.fixedPtReleased,
527
+ label: "equalized PT spend",
528
+ }),
529
+ };
530
+ }
567
531
  /**
568
532
  * Calculate the LP tokens that will be received for a given deposit
569
533
  * This is useful for UI display and slippage calculations
@@ -611,7 +575,7 @@ function estimateBalancedDeposit(marketState, targetLiquidity, lowerTickApy, upp
611
575
  /** Calculate SY and PT needed to deposit into liquidity pool from base token amount */
612
576
  /** Off-chain analogue of on-chain wrapper_provide_liquidity function */
613
577
  function calcDepositSyAndPtFromBaseAmount(params) {
614
- const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount, crossingTickState = EMPTY_CROSSING_TICK_STATE, crossingTickPriceLeft = 0, crossingTickPriceRight = 0, epsilonClamp = 1e-18, } = params;
578
+ const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount, epsilonClamp = 1e-18, } = params;
615
579
  if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
616
580
  return {
617
581
  syNeeded: 0,
@@ -623,13 +587,12 @@ function calcDepositSyAndPtFromBaseAmount(params) {
623
587
  const priceEffLower = effSnap.getEffectivePrice(lowerPrice);
624
588
  const priceEffUpper = effSnap.getEffectivePrice(upperPrice);
625
589
  // We mirror the on-chain logic in `wrapper_provide_liquidity`:
626
- // 1. Use compute_token_needs_with_crossing to infer the ratio of SY/PT
627
- // the market "wants" for this price range.
628
- // When crossing tick state is provided, this accounts for the existing
629
- // PT/SY proportions in the active tick, significantly improving accuracy.
590
+ // 1. Use curve-level token needs to infer the SY/PT ratio for this range.
591
+ // Raw active-tick proportions are ignored because the deposit CPI
592
+ // normalizes the active tick before minting new shares.
630
593
  // 2. Use that ratio plus the current SY exchange rate to decide how much of the
631
594
  // minted SY should be stripped into PT (calc_strip_amount).
632
- const [syMock, ptMock] = computeTokenNeedsWithCrossing(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 1e9, 1e9, epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight);
595
+ const [syMock, ptMock] = computeTokenNeedsWithCrossing(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 1e9, 1e9, epsilonClamp);
633
596
  // Total SY the user would get by minting SY from base off-chain
634
597
  // (we approximate on-chain `mint_sy_return_data.sy_out_amount`).
635
598
  const syAmount = Math.floor(baseTokenAmount * syExchangeRate);
@@ -679,9 +642,10 @@ function calcStripAmount(totalAmountSy, curSyRate, marketPtLiq, marketSyLiq) {
679
642
  * @param lowerTick - Lower tick (APY in basis points)
680
643
  * @param upperTick - Upper tick (APY in basis points)
681
644
  * @param syExchangeRate - SY exchange rate
645
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
682
646
  * @returns Simulation result with LP out, YT out, amounts spent, etc.
683
647
  */
684
- function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
648
+ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate, options = {}) {
685
649
  try {
686
650
  const { financials, configurationOptions, ticks } = marketState;
687
651
  const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000);
@@ -698,24 +662,22 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
698
662
  // Precompute effective prices
699
663
  const priceEffLower = snap.getEffectivePrice(lowerPrice);
700
664
  const priceEffUpper = snap.getEffectivePrice(upperPrice);
701
- // Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
702
- const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks);
703
- // Step 4: Calculate mock token needs using compute_token_needs_with_crossing
665
+ // Step 3: Calculate curve-level token needs.
704
666
  // max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
705
667
  const maxPt = syAmount * syExchangeRate;
706
- const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight);
707
- // Step 5: Calculate how much SY to strip (calc_strip_amount on-chain)
668
+ const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp);
669
+ // Step 4: Calculate how much SY to strip (calc_strip_amount on-chain)
708
670
  // Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
709
671
  const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock);
710
672
  const syToStrip = Math.min(syToStripRaw, syAmount);
711
- // Step 6: Calculate PT and YT from stripping
673
+ // Step 5: Calculate PT and YT from stripping
712
674
  // When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
713
675
  const ptFromStrip = syToStrip * syExchangeRate;
714
676
  const ytOut = ptFromStrip; // YT amount equals PT amount from strip
715
- // Step 7: Calculate remaining SY after strip
677
+ // Step 6: Calculate remaining SY after strip
716
678
  // Use Math.max to ensure non-negative (safety net for floating point edge cases)
717
679
  const syRemainder = Math.max(0, syAmount - syToStrip);
718
- // Step 8: Simulate deposit liquidity with remaining SY and PT
680
+ // Step 7: Simulate deposit liquidity with remaining SY and PT
719
681
  const depositResult = simulateAddLiquidity(marketState, {
720
682
  lowerTick,
721
683
  upperTick,
@@ -754,15 +716,16 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
754
716
  * @param lowerTick - Lower tick key (APY in basis points)
755
717
  * @param upperTick - Upper tick key (APY in basis points)
756
718
  * @param syExchangeRate - SY exchange rate
719
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
757
720
  * @returns Simulation result with LP out, PT to buy, SY constraint, etc.
758
721
  */
759
- function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
722
+ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, syExchangeRate, options = {}) {
760
723
  try {
761
724
  // Wrapper provide-liquidity-base debits base as:
762
725
  // base_needed = ceil(total_sy_spent * sy_exchange_rate)
763
726
  // So the strict SY budget for a user-provided base input is floor(base / rate).
764
727
  const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate);
765
- if (syBudget <= 0 || syBudget < MIN_SWAP_AND_SUPPLY_SY_BUDGET) {
728
+ if (syBudget <= 0) {
766
729
  return {
767
730
  lpOut: 0,
768
731
  ptToBuy: 0,
@@ -831,7 +794,7 @@ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, sy
831
794
  if (cached !== undefined) {
832
795
  return cached;
833
796
  }
834
- const candidate = simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, key, syExchangeRate, exactOutEstimateSlack);
797
+ const candidate = simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, key, syExchangeRate, exactOutEstimateSlack, options.existingPosition);
835
798
  candidateCache.set(key, candidate);
836
799
  return candidate;
837
800
  };
@@ -951,7 +914,7 @@ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, sy
951
914
  return null;
952
915
  }
953
916
  }
954
- function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack) {
917
+ function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack, existingPosition) {
955
918
  let swapResult;
956
919
  try {
957
920
  swapResult = simulateBuyPtExactOutWrapper(marketState, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack);
@@ -961,13 +924,19 @@ function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick,
961
924
  return null;
962
925
  }
963
926
  const depositState = swapResult.postMarketState ?? marketState;
964
- const depositResult = simulateAddLiquidity(depositState, {
965
- lowerTick,
966
- upperTick,
967
- maxSy: syBudget,
968
- maxPt: swapResult.ptOut,
969
- syExchangeRate,
970
- });
927
+ let depositResult;
928
+ try {
929
+ depositResult = simulateAddLiquidityForPosition(depositState, {
930
+ lowerTick,
931
+ upperTick,
932
+ maxSy: syBudget,
933
+ maxPt: swapResult.ptOut,
934
+ syExchangeRate,
935
+ }, existingPosition);
936
+ }
937
+ catch {
938
+ return null;
939
+ }
971
940
  // Swap & supply must actually add liquidity.
972
941
  // Discard candidates that end up as swap-only (deltaL == 0).
973
942
  if (depositResult.deltaL <= 0) {