@exponent-labs/market-three-math 0.9.15 → 0.9.17

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 (42) hide show
  1. package/build/addLiquidity.d.ts +11 -41
  2. package/build/addLiquidity.js +169 -201
  3. package/build/addLiquidity.js.map +1 -1
  4. package/build/bisect.js +1 -2
  5. package/build/bisect.js.map +1 -1
  6. package/build/existingPositionEqualization.d.ts +3 -3
  7. package/build/existingPositionEqualization.js +45 -66
  8. package/build/existingPositionEqualization.js.map +1 -1
  9. package/build/index.d.ts +2 -2
  10. package/build/index.js +1 -3
  11. package/build/index.js.map +1 -1
  12. package/build/liquidityHistogram.js +2 -3
  13. package/build/liquidityHistogram.js.map +1 -1
  14. package/build/quote.js +2 -2
  15. package/build/quote.js.map +1 -1
  16. package/build/swap.js +2 -2
  17. package/build/swap.js.map +1 -1
  18. package/build/swapV2.js +56 -20
  19. package/build/swapV2.js.map +1 -1
  20. package/build/types.d.ts +4 -21
  21. package/build/utils.js +17 -17
  22. package/build/utils.js.map +1 -1
  23. package/build/utilsV2.js +10 -7
  24. package/build/utilsV2.js.map +1 -1
  25. package/build/withdrawLiquidity.d.ts +1 -1
  26. package/build/withdrawLiquidity.js +120 -72
  27. package/build/withdrawLiquidity.js.map +1 -1
  28. package/build/ytTrades.js +3 -4
  29. package/build/ytTrades.js.map +1 -1
  30. package/build/ytTradesLegacy.js +3 -4
  31. package/build/ytTradesLegacy.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/addLiquidity.ts +203 -246
  34. package/src/existingPositionEqualization.test.ts +33 -0
  35. package/src/existingPositionEqualization.ts +52 -83
  36. package/src/index.ts +0 -4
  37. package/src/swap.ts +1 -0
  38. package/src/swapV2.ts +96 -18
  39. package/src/types.ts +4 -23
  40. package/src/utilsV2.ts +9 -4
  41. package/src/withdrawLiquidity.test.ts +189 -0
  42. package/src/withdrawLiquidity.ts +148 -89
@@ -1,9 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.simulateSwapAndSupply = exports.simulateWrapperProvideLiquidity = exports.calcDepositSyAndPtFromBaseAmount = exports.estimateBalancedDeposit = exports.calculateLpOut = exports.simulateAddLiquidity = exports.computeTokenNeedsWithCrossing = exports.scaleCrossingTickInputs = exports.computeLiquidityTargetAndTokenNeeds = exports.getCrossingTickStateFromTicks = void 0;
3
+ exports.computeLiquidityTargetAndTokenNeeds = computeLiquidityTargetAndTokenNeeds;
4
+ exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
5
+ exports.simulateAddLiquidity = simulateAddLiquidity;
6
+ exports.calculateLpOut = calculateLpOut;
7
+ exports.estimateBalancedDeposit = estimateBalancedDeposit;
8
+ exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
9
+ exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
10
+ exports.simulateSwapAndSupply = simulateSwapAndSupply;
11
+ const existingPositionEqualization_1 = require("./existingPositionEqualization");
4
12
  const swapV2_1 = require("./swapV2");
5
13
  const types_1 = require("./types");
6
- const utils_1 = require("./utils");
7
14
  const utilsV2_1 = require("./utilsV2");
8
15
  const TICK_KEY_BASE_POINTS = 1_000_000;
9
16
  // Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
@@ -12,45 +19,13 @@ const GAP_TOKEN_NEEDS = 20;
12
19
  const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100;
13
20
  const SWAP_EXACT_OUT_SY_HEADROOM = 100;
14
21
  const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000;
15
- const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000;
16
22
  const FLASH_SY_POSITION_HEADROOM = 1_000;
17
23
  const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000;
18
24
  const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0;
19
25
  const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000;
20
26
  const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0;
21
- const EMPTY_CROSSING_TICK_STATE = {
22
- principalPt: 0,
23
- principalSy: 0,
24
- principalShareSupply: 0,
25
- };
26
- /**
27
- * Get crossing tick state and price boundaries from Ticks.
28
- * Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
29
- */
30
- function getCrossingTickStateFromTicks(ticks) {
31
- const currentTickNode = ticks.ticksTree[ticks.currentTick - 1];
32
- if (!currentTickNode || ticks.currentTick <= 0) {
33
- return {
34
- crossingTickState: EMPTY_CROSSING_TICK_STATE,
35
- crossingTickPriceLeft: 0,
36
- crossingTickPriceRight: 0,
37
- };
38
- }
39
- const crossingTickPriceLeft = currentTickNode.impliedRate;
40
- const successorKey = (0, utils_1.getSuccessorTickKey)(ticks, currentTickNode.apyBasePoints);
41
- const successorTick = successorKey != null ? (0, utils_1.findTickByKey)(ticks, successorKey)?.tick : null;
42
- const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY;
43
- return {
44
- crossingTickState: {
45
- principalPt: Number(currentTickNode.principalPt),
46
- principalSy: Number(currentTickNode.principalSy),
47
- principalShareSupply: Number(currentTickNode.principalShareSupply),
48
- },
49
- crossingTickPriceLeft,
50
- crossingTickPriceRight,
51
- };
52
- }
53
- exports.getCrossingTickStateFromTicks = getCrossingTickStateFromTicks;
27
+ const ZERO_BIGINT = 0n;
28
+ const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
54
29
  /**
55
30
  * Compute liquidity target and token needs based on position range and budgets
56
31
  * Ported from compute_liquidity_target_and_token_needs in math.rs
@@ -70,11 +45,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
70
45
  ptNeeded: 0,
71
46
  priceSplitForNeed: lowerPrice,
72
47
  priceSplitTickIdx: lowerTickIdx,
73
- // No crossing possible when below range
74
- originalMaxSy: maxSy,
75
- originalMaxPt: maxPt,
76
- duLeftTotal: 0,
77
- deltaCRightTotal: deltaCTotal,
78
48
  };
79
49
  }
80
50
  else if (spotPriceCurrent >= upperPrice) {
@@ -89,11 +59,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
89
59
  ptNeeded: Math.floor(ptNeed),
90
60
  priceSplitForNeed: upperPrice,
91
61
  priceSplitTickIdx: upperTickIdx,
92
- // No crossing possible when above range
93
- originalMaxSy: maxSy,
94
- originalMaxPt: maxPt,
95
- duLeftTotal: duTotal,
96
- deltaCRightTotal: 0,
97
62
  };
98
63
  }
99
64
  else {
@@ -112,77 +77,29 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
112
77
  ptNeeded: Math.floor(ptNeed),
113
78
  priceSplitForNeed: spotPriceCurrent,
114
79
  priceSplitTickIdx: currentIndex,
115
- // Store for crossing tick scaling
116
- originalMaxSy: maxSy,
117
- originalMaxPt: maxPt,
118
- duLeftTotal: duLeft,
119
- deltaCRightTotal: deltaCRight,
120
80
  };
121
81
  }
122
82
  }
123
- exports.computeLiquidityTargetAndTokenNeeds = computeLiquidityTargetAndTokenNeeds;
124
- /**
125
- * Scale crossing tick token inputs from original max values to prevent double trimming.
126
- *
127
- * When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
128
- * 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
129
- * 2. Then by AMM formula in simulateAddLiquidityProportional
130
- *
131
- * This function calculates the segment's proportion of the total distribution and scales
132
- * the original max values accordingly, ensuring maximum token utilization.
133
- *
134
- * @param scaleParams - Original max values and CLMM distribution extents
135
- * @param duPtPart - PT segment length (spot price delta for this segment)
136
- * @param syPerL1 - SY per unit liquidity for this segment
137
- * @param clmmPtDelta - PT amount calculated from CLMM formula
138
- * @param clmmSyDelta - SY amount calculated from CLMM formula
139
- * @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
140
- */
141
- function scaleCrossingTickInputs(scaleParams, duPtPart, syPerL1, clmmPtDelta, clmmSyDelta) {
142
- const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams;
143
- if (duLeftTotal > 0 && deltaCRightTotal > 0) {
144
- // Calculate segment's proportion of total distribution
145
- const ptSegmentRatio = duPtPart / duLeftTotal;
146
- const sySegmentRatio = syPerL1 / deltaCRightTotal;
147
- // Scale original max values by segment proportion
148
- const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio);
149
- const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio);
150
- // Use max of CLMM-calculated value and scaled original value
151
- // This ensures we don't lose tokens due to double trimming
152
- return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)];
153
- }
154
- // Fallback to original logic if no crossing scale params
155
- return [clmmPtDelta, clmmSyDelta];
83
+ function toU64BigInt(value) {
84
+ return BigInt(Math.max(0, Math.floor(value)));
156
85
  }
157
- exports.scaleCrossingTickInputs = scaleCrossingTickInputs;
158
- /**
159
- * Simulate the proportional add_liquidity logic
160
- * Returns [usedPt, usedSy] based on existing tick proportions
161
- */
162
- function simulateAddLiquidityProportional(intentPt, intentSy, marketTotalPt, marketTotalSy, marketTotalLp) {
163
- if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
164
- // Empty tick - use all intended amounts
165
- return [intentPt, intentSy];
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}`);
166
90
  }
167
- const lpFromPt = (marketTotalLp * intentPt) / marketTotalPt;
168
- const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy;
169
- if (lpFromPt < lpFromSy) {
170
- // PT is the limiting factor
171
- const lpTokensOut = lpFromPt;
172
- const usedPt = intentPt;
173
- const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp);
174
- return [usedPt, usedSy];
175
- }
176
- else {
177
- // SY is the limiting factor
178
- const lpTokensOut = lpFromSy;
179
- const usedSy = intentSy;
180
- const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp);
181
- 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()}`);
182
96
  }
97
+ return Number(value);
183
98
  }
184
- function toU64BigInt(value) {
185
- 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);
186
103
  }
187
104
  function ceilDivBigInt(numerator, denominator) {
188
105
  if (denominator <= 0n) {
@@ -217,6 +134,78 @@ function getVirtualTickState(key, ticksWrapper, virtualStates) {
217
134
  virtualStates.set(key, state);
218
135
  return state;
219
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
+ }
220
209
  function simulateMintSharesForTickPrestateCalcUsed(params) {
221
210
  const { key, dptIn, dsyIn, ticksWrapper, virtualStates } = params;
222
211
  const state = getVirtualTickState(key, ticksWrapper, virtualStates);
@@ -304,10 +293,11 @@ function simulateMintSharesForTickPrestateCalcUsed(params) {
304
293
  return { minted, usedPt, usedSy };
305
294
  }
306
295
  function simulateAccruePrincipalForDeposit(params) {
307
- 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;
308
297
  const ticksWrapper = new utilsV2_1.TicksWrapper(ticks);
309
298
  ticksWrapper.upsertBoundaryTick(lowerTickKey, snap);
310
299
  ticksWrapper.upsertBoundaryTick(upperTickKey, snap);
300
+ normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey);
311
301
  const virtualStates = new Map();
312
302
  let totalSySpend = 0n;
313
303
  let totalPtSpend = 0n;
@@ -357,9 +347,7 @@ function simulateAccruePrincipalForDeposit(params) {
357
347
  if (isCrossing) {
358
348
  pendingCrossPt = {
359
349
  leftKey,
360
- rightKey,
361
350
  ptDelta: principalPtDelta,
362
- duPart: segmentLength,
363
351
  };
364
352
  return;
365
353
  }
@@ -394,10 +382,7 @@ function simulateAccruePrincipalForDeposit(params) {
394
382
  const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed;
395
383
  if (isCrossing) {
396
384
  pendingCrossSy = {
397
- leftKey,
398
- rightKey,
399
385
  syDelta: principalSyDelta,
400
- syPerL,
401
386
  };
402
387
  return;
403
388
  }
@@ -415,14 +400,10 @@ function simulateAccruePrincipalForDeposit(params) {
415
400
  totalSySpend += minted.usedSy;
416
401
  });
417
402
  if (pendingCrossPt && pendingCrossSy) {
418
- const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS));
419
- const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS));
420
- const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft;
421
- const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft;
422
403
  const minted = simulateMintSharesForTickPrestateCalcUsed({
423
404
  key: pendingCrossPt.leftKey,
424
- dptIn: scaledPtIn,
425
- dsyIn: scaledSyIn,
405
+ dptIn: pendingCrossPt.ptDelta,
406
+ dsyIn: pendingCrossSy.syDelta,
426
407
  ticksWrapper,
427
408
  virtualStates,
428
409
  });
@@ -439,11 +420,8 @@ function simulateAccruePrincipalForDeposit(params) {
439
420
  }
440
421
  return { sySpent, ptSpent };
441
422
  }
442
- /**
443
- * Compute token needs with crossing tick adjustment
444
- * This matches the Rust compute_token_needs_with_crossing function
445
- */
446
- 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) {
447
425
  // Below range: SY only
448
426
  if (spotPriceCurrent <= lowerPrice) {
449
427
  const deltaCTotal = priceEffLower - priceEffUpper;
@@ -465,48 +443,10 @@ function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, pr
465
443
  const liquidityFromPt = maxPt / duLeft;
466
444
  const liquidityFromSy = maxSy / deltaCRight;
467
445
  const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy);
468
- let ptNeed = liquidityTarget * duLeft;
469
- let syNeed = liquidityTarget * deltaCRight;
470
- // Apply crossing tick adjustment if the tick has existing liquidity
471
- // Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
472
- const isCrossingTickActive = crossingTickState.principalShareSupply > 0 &&
473
- crossingTickPriceLeft < spotPriceCurrent &&
474
- crossingTickPriceRight > spotPriceCurrent;
475
- if (isCrossingTickActive) {
476
- // Calculate PT and SY portions that would go into the crossing tick
477
- // PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
478
- const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice);
479
- const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment);
480
- // SY portion: from spotPriceCurrent to crossingTickPriceRight
481
- const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft);
482
- const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice);
483
- const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd);
484
- const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0));
485
- if (crossingPtIntended > 0 && crossingSyIntended > 0) {
486
- // Tokens already allocated to non-crossing segments before crossing processing.
487
- const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0);
488
- const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0);
489
- // Apply scaling from original max values to prevent double trimming
490
- const [scaledPtIn, scaledSyIn] = duLeft > 0 && deltaCRight > 0
491
- ? (() => {
492
- const totalPtSpend = Math.ceil(ptOutsideCrossing);
493
- const totalSySpend = Math.ceil(syOutsideCrossing);
494
- const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0);
495
- const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0);
496
- // Use max of CLMM-calculated value and scaled original value
497
- return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)];
498
- })()
499
- : [crossingPtIntended, crossingSyIntended];
500
- // Simulate add_liquidity proportional logic with scaled inputs
501
- const [usedPt, usedSy] = simulateAddLiquidityProportional(scaledPtIn, scaledSyIn, crossingTickState.principalPt, crossingTickState.principalSy, crossingTickState.principalShareSupply);
502
- // Adjust needs based on what would actually be used
503
- ptNeed = ptOutsideCrossing + usedPt;
504
- syNeed = syOutsideCrossing + usedSy;
505
- }
506
- }
446
+ const ptNeed = liquidityTarget * duLeft;
447
+ const syNeed = liquidityTarget * deltaCRight;
507
448
  return [Math.ceil(syNeed), Math.ceil(ptNeed)];
508
449
  }
509
- exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
510
450
  /**
511
451
  * Simulate adding liquidity to the CLMM market
512
452
  * This is a pure function that does not mutate the market state
@@ -541,8 +481,6 @@ function simulateAddLiquidity(marketState, args) {
541
481
  lowerTickKey: args.lowerTick,
542
482
  upperTickKey: args.upperTick,
543
483
  liquidityTarget: liquidityNeeds.liquidityTarget,
544
- originalMaxSy: liquidityNeeds.originalMaxSy,
545
- originalMaxPt: liquidityNeeds.originalMaxPt,
546
484
  });
547
485
  // Enforce budgets
548
486
  const sySpent = syNeededWithCrossing;
@@ -559,7 +497,37 @@ function simulateAddLiquidity(marketState, args) {
559
497
  ptSpent,
560
498
  };
561
499
  }
562
- exports.simulateAddLiquidity = simulateAddLiquidity;
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
+ }
563
531
  /**
564
532
  * Calculate the LP tokens that will be received for a given deposit
565
533
  * This is useful for UI display and slippage calculations
@@ -572,7 +540,6 @@ function calculateLpOut(liquidityAdded, totalLiquidityBefore) {
572
540
  // LP tokens are proportional to liquidity added
573
541
  return liquidityAdded;
574
542
  }
575
- exports.calculateLpOut = calculateLpOut;
576
543
  /**
577
544
  * Estimate the balanced amounts of PT and SY needed for a liquidity deposit
578
545
  * Given a target liquidity amount, calculate how much PT and SY are needed
@@ -605,11 +572,10 @@ function estimateBalancedDeposit(marketState, targetLiquidity, lowerTickApy, upp
605
572
  return { ptNeeded, syNeeded };
606
573
  }
607
574
  }
608
- exports.estimateBalancedDeposit = estimateBalancedDeposit;
609
575
  /** Calculate SY and PT needed to deposit into liquidity pool from base token amount */
610
576
  /** Off-chain analogue of on-chain wrapper_provide_liquidity function */
611
577
  function calcDepositSyAndPtFromBaseAmount(params) {
612
- 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;
613
579
  if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
614
580
  return {
615
581
  syNeeded: 0,
@@ -621,13 +587,12 @@ function calcDepositSyAndPtFromBaseAmount(params) {
621
587
  const priceEffLower = effSnap.getEffectivePrice(lowerPrice);
622
588
  const priceEffUpper = effSnap.getEffectivePrice(upperPrice);
623
589
  // We mirror the on-chain logic in `wrapper_provide_liquidity`:
624
- // 1. Use compute_token_needs_with_crossing to infer the ratio of SY/PT
625
- // the market "wants" for this price range.
626
- // When crossing tick state is provided, this accounts for the existing
627
- // 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.
628
593
  // 2. Use that ratio plus the current SY exchange rate to decide how much of the
629
594
  // minted SY should be stripped into PT (calc_strip_amount).
630
- 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);
631
596
  // Total SY the user would get by minting SY from base off-chain
632
597
  // (we approximate on-chain `mint_sy_return_data.sy_out_amount`).
633
598
  const syAmount = Math.floor(baseTokenAmount * syExchangeRate);
@@ -647,7 +612,6 @@ function calcDepositSyAndPtFromBaseAmount(params) {
647
612
  ptNeeded: ptFromStrip,
648
613
  };
649
614
  }
650
- exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
651
615
  /**
652
616
  * Calculate the amount of SY to strip into PT based on market liquidity ratio
653
617
  * This matches the Rust implementation: calc_strip_amount in wrapper_provide_liquidity.rs
@@ -678,9 +642,10 @@ function calcStripAmount(totalAmountSy, curSyRate, marketPtLiq, marketSyLiq) {
678
642
  * @param lowerTick - Lower tick (APY in basis points)
679
643
  * @param upperTick - Upper tick (APY in basis points)
680
644
  * @param syExchangeRate - SY exchange rate
645
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
681
646
  * @returns Simulation result with LP out, YT out, amounts spent, etc.
682
647
  */
683
- function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
648
+ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate, options = {}) {
684
649
  try {
685
650
  const { financials, configurationOptions, ticks } = marketState;
686
651
  const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000);
@@ -697,24 +662,22 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
697
662
  // Precompute effective prices
698
663
  const priceEffLower = snap.getEffectivePrice(lowerPrice);
699
664
  const priceEffUpper = snap.getEffectivePrice(upperPrice);
700
- // Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
701
- const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks);
702
- // Step 4: Calculate mock token needs using compute_token_needs_with_crossing
665
+ // Step 3: Calculate curve-level token needs.
703
666
  // max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
704
667
  const maxPt = syAmount * syExchangeRate;
705
- const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight);
706
- // 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)
707
670
  // Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
708
671
  const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock);
709
672
  const syToStrip = Math.min(syToStripRaw, syAmount);
710
- // Step 6: Calculate PT and YT from stripping
673
+ // Step 5: Calculate PT and YT from stripping
711
674
  // When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
712
675
  const ptFromStrip = syToStrip * syExchangeRate;
713
676
  const ytOut = ptFromStrip; // YT amount equals PT amount from strip
714
- // Step 7: Calculate remaining SY after strip
677
+ // Step 6: Calculate remaining SY after strip
715
678
  // Use Math.max to ensure non-negative (safety net for floating point edge cases)
716
679
  const syRemainder = Math.max(0, syAmount - syToStrip);
717
- // Step 8: Simulate deposit liquidity with remaining SY and PT
680
+ // Step 7: Simulate deposit liquidity with remaining SY and PT
718
681
  const depositResult = simulateAddLiquidity(marketState, {
719
682
  lowerTick,
720
683
  upperTick,
@@ -740,7 +703,6 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
740
703
  return null;
741
704
  }
742
705
  }
743
- exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
744
706
  /**
745
707
  * Simulate swap & supply operation (ixProvideLiquidityBase)
746
708
  * 1. Mints SY from base asset
@@ -754,15 +716,16 @@ exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
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,8 +914,7 @@ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, sy
951
914
  return null;
952
915
  }
953
916
  }
954
- exports.simulateSwapAndSupply = simulateSwapAndSupply;
955
- function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack) {
917
+ function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack, existingPosition) {
956
918
  let swapResult;
957
919
  try {
958
920
  swapResult = simulateBuyPtExactOutWrapper(marketState, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack);
@@ -962,13 +924,19 @@ function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick,
962
924
  return null;
963
925
  }
964
926
  const depositState = swapResult.postMarketState ?? marketState;
965
- const depositResult = simulateAddLiquidity(depositState, {
966
- lowerTick,
967
- upperTick,
968
- maxSy: syBudget,
969
- maxPt: swapResult.ptOut,
970
- syExchangeRate,
971
- });
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
+ }
972
940
  // Swap & supply must actually add liquidity.
973
941
  // Discard candidates that end up as swap-only (deltaL == 0).
974
942
  if (depositResult.deltaL <= 0) {