@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
@@ -2,19 +2,17 @@
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"
5
+ import type { LpPositionCLMM, Ticks } from "@exponent-labs/exponent-fetcher"
6
6
 
7
+ import { computeExistingPositionBudgetEffect } from "./existingPositionEqualization"
7
8
  import { simulateSwap, simulateSwapExactOut } from "./swapV2"
8
9
  import {
9
10
  AddLiquidityArgs,
10
11
  AddLiquidityOutcome,
11
- CrossingScaleParams,
12
- CrossingTickState,
13
12
  LiquidityNeeds,
14
13
  MarketThreeState,
15
14
  SwapDirection,
16
15
  } from "./types"
17
- import { findTickByKey, getSuccessorTickKey } from "./utils"
18
16
  import { EffSnap, TicksWrapper, normalizedTimeRemaining } from "./utilsV2"
19
17
 
20
18
  const TICK_KEY_BASE_POINTS = 1_000_000
@@ -24,49 +22,16 @@ const GAP_TOKEN_NEEDS = 20
24
22
  const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100
25
23
  const SWAP_EXACT_OUT_SY_HEADROOM = 100
26
24
  const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000
27
- const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000
28
25
  const FLASH_SY_POSITION_HEADROOM = 1_000
29
26
  const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000
30
27
  const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0
31
28
  const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000
32
29
  const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0
30
+ const ZERO_BIGINT = 0n
31
+ const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER)
33
32
 
34
- const EMPTY_CROSSING_TICK_STATE: CrossingTickState = {
35
- principalPt: 0,
36
- principalSy: 0,
37
- principalShareSupply: 0,
38
- }
39
-
40
- /**
41
- * Get crossing tick state and price boundaries from Ticks.
42
- * Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
43
- */
44
- export function getCrossingTickStateFromTicks(ticks: Ticks): {
45
- crossingTickState: CrossingTickState
46
- crossingTickPriceLeft: number
47
- crossingTickPriceRight: number
48
- } {
49
- const currentTickNode = ticks.ticksTree[ticks.currentTick - 1]
50
- if (!currentTickNode || ticks.currentTick <= 0) {
51
- return {
52
- crossingTickState: EMPTY_CROSSING_TICK_STATE,
53
- crossingTickPriceLeft: 0,
54
- crossingTickPriceRight: 0,
55
- }
56
- }
57
- const crossingTickPriceLeft = currentTickNode.impliedRate
58
- const successorKey = getSuccessorTickKey(ticks, currentTickNode.apyBasePoints)
59
- const successorTick = successorKey != null ? findTickByKey(ticks, successorKey)?.tick : null
60
- const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY
61
- return {
62
- crossingTickState: {
63
- principalPt: Number(currentTickNode.principalPt),
64
- principalSy: Number(currentTickNode.principalSy),
65
- principalShareSupply: Number(currentTickNode.principalShareSupply),
66
- },
67
- crossingTickPriceLeft,
68
- crossingTickPriceRight,
69
- }
33
+ type AddLiquiditySimulationOptions = {
34
+ existingPosition?: LpPositionCLMM
70
35
  }
71
36
 
72
37
  /**
@@ -103,11 +68,6 @@ export function computeLiquidityTargetAndTokenNeeds(
103
68
  ptNeeded: 0,
104
69
  priceSplitForNeed: lowerPrice,
105
70
  priceSplitTickIdx: lowerTickIdx,
106
- // No crossing possible when below range
107
- originalMaxSy: maxSy,
108
- originalMaxPt: maxPt,
109
- duLeftTotal: 0,
110
- deltaCRightTotal: deltaCTotal,
111
71
  }
112
72
  } else if (spotPriceCurrent >= upperPrice) {
113
73
  // Above range: PT only (NOT SY!)
@@ -122,11 +82,6 @@ export function computeLiquidityTargetAndTokenNeeds(
122
82
  ptNeeded: Math.floor(ptNeed),
123
83
  priceSplitForNeed: upperPrice,
124
84
  priceSplitTickIdx: upperTickIdx,
125
- // No crossing possible when above range
126
- originalMaxSy: maxSy,
127
- originalMaxPt: maxPt,
128
- duLeftTotal: duTotal,
129
- deltaCRightTotal: 0,
130
85
  }
131
86
  } else {
132
87
  // Inside range: both sides - crossing tick possible
@@ -147,95 +102,39 @@ export function computeLiquidityTargetAndTokenNeeds(
147
102
  ptNeeded: Math.floor(ptNeed),
148
103
  priceSplitForNeed: spotPriceCurrent,
149
104
  priceSplitTickIdx: currentIndex,
150
- // Store for crossing tick scaling
151
- originalMaxSy: maxSy,
152
- originalMaxPt: maxPt,
153
- duLeftTotal: duLeft,
154
- deltaCRightTotal: deltaCRight,
155
105
  }
156
106
  }
157
107
  }
158
108
 
159
- /**
160
- * Scale crossing tick token inputs from original max values to prevent double trimming.
161
- *
162
- * When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
163
- * 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
164
- * 2. Then by AMM formula in simulateAddLiquidityProportional
165
- *
166
- * This function calculates the segment's proportion of the total distribution and scales
167
- * the original max values accordingly, ensuring maximum token utilization.
168
- *
169
- * @param scaleParams - Original max values and CLMM distribution extents
170
- * @param duPtPart - PT segment length (spot price delta for this segment)
171
- * @param syPerL1 - SY per unit liquidity for this segment
172
- * @param clmmPtDelta - PT amount calculated from CLMM formula
173
- * @param clmmSyDelta - SY amount calculated from CLMM formula
174
- * @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
175
- */
176
- export function scaleCrossingTickInputs(
177
- scaleParams: CrossingScaleParams,
178
- duPtPart: number,
179
- syPerL1: number,
180
- clmmPtDelta: number,
181
- clmmSyDelta: number,
182
- ): [number, number] {
183
- const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams
184
-
185
- if (duLeftTotal > 0 && deltaCRightTotal > 0) {
186
- // Calculate segment's proportion of total distribution
187
- const ptSegmentRatio = duPtPart / duLeftTotal
188
- const sySegmentRatio = syPerL1 / deltaCRightTotal
189
-
190
- // Scale original max values by segment proportion
191
- const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio)
192
- const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio)
109
+ function toU64BigInt(value: number): bigint {
110
+ return BigInt(Math.max(0, Math.floor(value)))
111
+ }
193
112
 
194
- // Use max of CLMM-calculated value and scaled original value
195
- // This ensures we don't lose tokens due to double trimming
196
- return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)]
113
+ function lamportsNumberToBigInt(value: number, label: string): bigint {
114
+ const lamports = Math.max(0, Math.floor(value))
115
+ if (!Number.isSafeInteger(lamports)) {
116
+ throw new Error(`${label} exceeds JS safe integer range: ${value}`)
197
117
  }
198
-
199
- // Fallback to original logic if no crossing scale params
200
- return [clmmPtDelta, clmmSyDelta]
118
+ return BigInt(lamports)
201
119
  }
202
120
 
203
- /**
204
- * Simulate the proportional add_liquidity logic
205
- * Returns [usedPt, usedSy] based on existing tick proportions
206
- */
207
- function simulateAddLiquidityProportional(
208
- intentPt: number,
209
- intentSy: number,
210
- marketTotalPt: number,
211
- marketTotalSy: number,
212
- marketTotalLp: number,
213
- ): [number, number] {
214
- if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
215
- // Empty tick - use all intended amounts
216
- return [intentPt, intentSy]
121
+ function safeLamportsBigIntToNumber(value: bigint, label: string): number {
122
+ if (value < ZERO_BIGINT || value > MAX_SAFE_INTEGER_BIGINT) {
123
+ throw new Error(`${label} exceeds JS safe integer range: ${value.toString()}`)
217
124
  }
218
125
 
219
- const lpFromPt = (marketTotalLp * intentPt) / marketTotalPt
220
- const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy
221
-
222
- if (lpFromPt < lpFromSy) {
223
- // PT is the limiting factor
224
- const lpTokensOut = lpFromPt
225
- const usedPt = intentPt
226
- const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp)
227
- return [usedPt, usedSy]
228
- } else {
229
- // SY is the limiting factor
230
- const lpTokensOut = lpFromSy
231
- const usedSy = intentSy
232
- const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp)
233
- return [usedPt, usedSy]
234
- }
126
+ return Number(value)
235
127
  }
236
128
 
237
- function toU64BigInt(value: number): bigint {
238
- return BigInt(Math.max(0, Math.floor(value)))
129
+ function finalizeEqualizedSpend(params: {
130
+ simulatedSpend: number
131
+ fixedSpent: bigint
132
+ fixedReleased: bigint
133
+ label: string
134
+ }): number {
135
+ const grossSpend = lamportsNumberToBigInt(params.simulatedSpend, params.label) + params.fixedSpent
136
+ const finalSpend = grossSpend > params.fixedReleased ? grossSpend - params.fixedReleased : ZERO_BIGINT
137
+ return safeLamportsBigIntToNumber(finalSpend, params.label)
239
138
  }
240
139
 
241
140
  function ceilDivBigInt(numerator: bigint, denominator: bigint): bigint {
@@ -285,6 +184,104 @@ function getVirtualTickState(
285
184
  return state
286
185
  }
287
186
 
187
+ function ceilNormalizedPrincipal(value: number, cap: bigint): bigint {
188
+ if (!Number.isFinite(value) || value < 0) {
189
+ throw new Error("Invalid active tick normalization value")
190
+ }
191
+
192
+ const capNumber = Number(cap)
193
+ if (value >= capNumber) {
194
+ return cap
195
+ }
196
+
197
+ return BigInt(Math.min(Math.ceil(value), capNumber))
198
+ }
199
+
200
+ function computeNormalizedActiveTickPrincipals(params: {
201
+ principalPt: bigint
202
+ principalSy: bigint
203
+ ptPerL: number
204
+ syPerL: number
205
+ }): { normalizedPt: bigint; normalizedSy: bigint } {
206
+ const { principalPt, principalSy, ptPerL, syPerL } = params
207
+ if (!Number.isFinite(ptPerL) || !Number.isFinite(syPerL) || ptPerL < 0 || syPerL < 0) {
208
+ throw new Error("Invalid active tick normalization ratio")
209
+ }
210
+
211
+ if (ptPerL === 0 && syPerL === 0) {
212
+ return { normalizedPt: 0n, normalizedSy: 0n }
213
+ }
214
+ if (ptPerL === 0) {
215
+ return { normalizedPt: 0n, normalizedSy: principalSy }
216
+ }
217
+ if (syPerL === 0) {
218
+ return { normalizedPt: principalPt, normalizedSy: 0n }
219
+ }
220
+
221
+ const lFromPt = Number(principalPt) / ptPerL
222
+ const lFromSy = Number(principalSy) / syPerL
223
+ if (!Number.isFinite(lFromPt) || !Number.isFinite(lFromSy)) {
224
+ throw new Error("Invalid active tick normalization liquidity")
225
+ }
226
+
227
+ if (lFromPt <= lFromSy) {
228
+ return {
229
+ normalizedPt: principalPt,
230
+ normalizedSy: ceilNormalizedPrincipal(lFromPt * syPerL, principalSy),
231
+ }
232
+ }
233
+
234
+ return {
235
+ normalizedPt: ceilNormalizedPrincipal(lFromSy * ptPerL, principalPt),
236
+ normalizedSy: principalSy,
237
+ }
238
+ }
239
+
240
+ function normalizeActiveTickPrincipalsBeforeDeposit(
241
+ ticksWrapper: TicksWrapper,
242
+ snap: EffSnap,
243
+ lowerTickKey: number,
244
+ upperTickKey: number,
245
+ ): void {
246
+ if (ticksWrapper.currentPrefixSum === 0n) {
247
+ return
248
+ }
249
+
250
+ const currentTickKey = ticksWrapper.currentTickKey
251
+ if (lowerTickKey > currentTickKey || currentTickKey >= upperTickKey) {
252
+ return
253
+ }
254
+
255
+ const rightTickKey = ticksWrapper.successorKey(currentTickKey)
256
+ if (rightTickKey == null) {
257
+ return
258
+ }
259
+
260
+ const spotPrice = ticksWrapper.currentSpotPrice
261
+ const leftPrice = ticksWrapper.getSpotPrice(currentTickKey)
262
+ const rightPrice = ticksWrapper.getSpotPrice(rightTickKey)
263
+ if (spotPrice < leftPrice || spotPrice > rightPrice) {
264
+ return
265
+ }
266
+
267
+ const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentTickKey)
268
+ if (principalPt === 0n && principalSy === 0n) {
269
+ return
270
+ }
271
+
272
+ const ptPerL = Math.max(spotPrice - leftPrice, 0)
273
+ const syPerL =
274
+ spotPrice >= rightPrice ? 0 : snap.getEffectivePrice(spotPrice) - snap.getEffectivePrice(rightPrice)
275
+ const { normalizedPt, normalizedSy } = computeNormalizedActiveTickPrincipals({
276
+ principalPt,
277
+ principalSy,
278
+ ptPerL,
279
+ syPerL,
280
+ })
281
+
282
+ ticksWrapper.setPrincipals(currentTickKey, normalizedPt, normalizedSy)
283
+ }
284
+
288
285
  function simulateMintSharesForTickPrestateCalcUsed(params: {
289
286
  key: number
290
287
  dptIn: bigint
@@ -390,8 +387,6 @@ function simulateAccruePrincipalForDeposit(params: {
390
387
  lowerTickKey: number
391
388
  upperTickKey: number
392
389
  liquidityTarget: number
393
- originalMaxSy: number
394
- originalMaxPt: number
395
390
  }): { sySpent: number; ptSpent: number } {
396
391
  const {
397
392
  ticks,
@@ -403,19 +398,18 @@ function simulateAccruePrincipalForDeposit(params: {
403
398
  lowerTickKey,
404
399
  upperTickKey,
405
400
  liquidityTarget,
406
- originalMaxSy,
407
- originalMaxPt,
408
401
  } = params
409
402
 
410
403
  const ticksWrapper = new TicksWrapper(ticks)
411
404
  ticksWrapper.upsertBoundaryTick(lowerTickKey, snap)
412
405
  ticksWrapper.upsertBoundaryTick(upperTickKey, snap)
406
+ normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey)
413
407
  const virtualStates = new Map<number, VirtualTickState>()
414
408
 
415
409
  let totalSySpend = 0n
416
410
  let totalPtSpend = 0n
417
- let pendingCrossPt: { leftKey: number; rightKey: number; ptDelta: bigint; duPart: number } | null = null
418
- let pendingCrossSy: { leftKey: number; rightKey: number; syDelta: bigint; syPerL: number } | null = null
411
+ let pendingCrossPt: { leftKey: number; ptDelta: bigint } | null = null
412
+ let pendingCrossSy: { syDelta: bigint } | null = null
419
413
 
420
414
  const resolveTraversalStartKey = (candidateKey: number): number => {
421
415
  if (ticksWrapper.getTickByKey(candidateKey)) {
@@ -473,9 +467,7 @@ function simulateAccruePrincipalForDeposit(params: {
473
467
  if (isCrossing) {
474
468
  pendingCrossPt = {
475
469
  leftKey,
476
- rightKey,
477
470
  ptDelta: principalPtDelta,
478
- duPart: segmentLength,
479
471
  }
480
472
  return
481
473
  }
@@ -516,10 +508,7 @@ function simulateAccruePrincipalForDeposit(params: {
516
508
  const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed
517
509
  if (isCrossing) {
518
510
  pendingCrossSy = {
519
- leftKey,
520
- rightKey,
521
511
  syDelta: principalSyDelta,
522
- syPerL,
523
512
  }
524
513
  return
525
514
  }
@@ -540,15 +529,10 @@ function simulateAccruePrincipalForDeposit(params: {
540
529
  })
541
530
 
542
531
  if (pendingCrossPt && pendingCrossSy) {
543
- const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS))
544
- const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS))
545
- const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft
546
- const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft
547
-
548
532
  const minted = simulateMintSharesForTickPrestateCalcUsed({
549
533
  key: pendingCrossPt.leftKey,
550
- dptIn: scaledPtIn,
551
- dsyIn: scaledSyIn,
534
+ dptIn: pendingCrossPt.ptDelta,
535
+ dsyIn: pendingCrossSy.syDelta,
552
536
  ticksWrapper,
553
537
  virtualStates,
554
538
  })
@@ -569,10 +553,7 @@ function simulateAccruePrincipalForDeposit(params: {
569
553
  return { sySpent, ptSpent }
570
554
  }
571
555
 
572
- /**
573
- * Compute token needs with crossing tick adjustment
574
- * This matches the Rust compute_token_needs_with_crossing function
575
- */
556
+ /** Compute curve-level token needs without raw crossing-tick cap scaling. */
576
557
  export function computeTokenNeedsWithCrossing(
577
558
  snap: EffSnap,
578
559
  spotPriceCurrent: number,
@@ -583,9 +564,6 @@ export function computeTokenNeedsWithCrossing(
583
564
  maxSy: number,
584
565
  maxPt: number,
585
566
  epsilonClamp: number,
586
- crossingTickState: CrossingTickState,
587
- crossingTickPriceLeft: number,
588
- crossingTickPriceRight: number,
589
567
  ): [number, number] {
590
568
  // Below range: SY only
591
569
  if (spotPriceCurrent <= lowerPrice) {
@@ -612,62 +590,8 @@ export function computeTokenNeedsWithCrossing(
612
590
  const liquidityFromSy = maxSy / deltaCRight
613
591
 
614
592
  const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy)
615
- let ptNeed = liquidityTarget * duLeft
616
- let syNeed = liquidityTarget * deltaCRight
617
-
618
- // Apply crossing tick adjustment if the tick has existing liquidity
619
- // Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
620
- const isCrossingTickActive =
621
- crossingTickState.principalShareSupply > 0 &&
622
- crossingTickPriceLeft < spotPriceCurrent &&
623
- crossingTickPriceRight > spotPriceCurrent
624
-
625
- if (isCrossingTickActive) {
626
- // Calculate PT and SY portions that would go into the crossing tick
627
- // PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
628
- const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice)
629
- const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment)
630
-
631
- // SY portion: from spotPriceCurrent to crossingTickPriceRight
632
- const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft)
633
- const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice)
634
- const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd)
635
- const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0))
636
-
637
- if (crossingPtIntended > 0 && crossingSyIntended > 0) {
638
- // Tokens already allocated to non-crossing segments before crossing processing.
639
- const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0)
640
- const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0)
641
-
642
- // Apply scaling from original max values to prevent double trimming
643
- const [scaledPtIn, scaledSyIn] =
644
- duLeft > 0 && deltaCRight > 0
645
- ? (() => {
646
- const totalPtSpend = Math.ceil(ptOutsideCrossing)
647
- const totalSySpend = Math.ceil(syOutsideCrossing)
648
- const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0)
649
- const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0)
650
-
651
- // Use max of CLMM-calculated value and scaled original value
652
- return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)]
653
- })()
654
- : [crossingPtIntended, crossingSyIntended]
655
-
656
- // Simulate add_liquidity proportional logic with scaled inputs
657
- const [usedPt, usedSy] = simulateAddLiquidityProportional(
658
- scaledPtIn,
659
- scaledSyIn,
660
- crossingTickState.principalPt,
661
- crossingTickState.principalSy,
662
- crossingTickState.principalShareSupply,
663
- )
664
-
665
- // Adjust needs based on what would actually be used
666
- ptNeed = ptOutsideCrossing + usedPt
667
-
668
- syNeed = syOutsideCrossing + usedSy
669
- }
670
- }
593
+ const ptNeed = liquidityTarget * duLeft
594
+ const syNeed = liquidityTarget * deltaCRight
671
595
 
672
596
  return [Math.ceil(syNeed), Math.ceil(ptNeed)]
673
597
  }
@@ -728,8 +652,6 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
728
652
  lowerTickKey: args.lowerTick,
729
653
  upperTickKey: args.upperTick,
730
654
  liquidityTarget: liquidityNeeds.liquidityTarget,
731
- originalMaxSy: liquidityNeeds.originalMaxSy,
732
- originalMaxPt: liquidityNeeds.originalMaxPt,
733
655
  })
734
656
 
735
657
  // Enforce budgets
@@ -750,6 +672,45 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
750
672
  }
751
673
  }
752
674
 
675
+ function simulateAddLiquidityForPosition(
676
+ marketState: MarketThreeState,
677
+ args: AddLiquidityArgs,
678
+ existingPosition?: LpPositionCLMM,
679
+ ): AddLiquidityOutcome {
680
+ if (!existingPosition) {
681
+ return simulateAddLiquidity(marketState, args)
682
+ }
683
+
684
+ const budgetEffect = computeExistingPositionBudgetEffect({
685
+ ticks: marketState.ticks,
686
+ position: existingPosition,
687
+ userMaxSy: lamportsNumberToBigInt(args.maxSy, "user max SY"),
688
+ userMaxPt: lamportsNumberToBigInt(args.maxPt, "user max PT"),
689
+ })
690
+
691
+ const addLiquidityResult = simulateAddLiquidity(marketState, {
692
+ ...args,
693
+ maxSy: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxSy, "effective max SY"),
694
+ maxPt: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxPt, "effective max PT"),
695
+ })
696
+
697
+ return {
698
+ deltaL: addLiquidityResult.deltaL,
699
+ sySpent: finalizeEqualizedSpend({
700
+ simulatedSpend: addLiquidityResult.sySpent,
701
+ fixedSpent: budgetEffect.fixedSySpent,
702
+ fixedReleased: budgetEffect.fixedSyReleased,
703
+ label: "equalized SY spend",
704
+ }),
705
+ ptSpent: finalizeEqualizedSpend({
706
+ simulatedSpend: addLiquidityResult.ptSpent,
707
+ fixedSpent: budgetEffect.fixedPtSpent,
708
+ fixedReleased: budgetEffect.fixedPtReleased,
709
+ label: "equalized PT spend",
710
+ }),
711
+ }
712
+ }
713
+
753
714
  /**
754
715
  * Calculate the LP tokens that will be received for a given deposit
755
716
  * This is useful for UI display and slippage calculations
@@ -813,12 +774,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
813
774
  expirationTs: number
814
775
  currentSpotPrice: number
815
776
  syExchangeRate: number
816
- /** Optional crossing tick state for accurate ratio prediction.
817
- * When provided, the prediction accounts for the existing PT/SY proportions
818
- * in the active tick, matching the on-chain behaviour more closely. */
819
- crossingTickState?: CrossingTickState
820
- crossingTickPriceLeft?: number
821
- crossingTickPriceRight?: number
822
777
  epsilonClamp?: number
823
778
  }) {
824
779
  const {
@@ -828,9 +783,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
828
783
  lowerPrice,
829
784
  upperPrice,
830
785
  baseTokenAmount,
831
- crossingTickState = EMPTY_CROSSING_TICK_STATE,
832
- crossingTickPriceLeft = 0,
833
- crossingTickPriceRight = 0,
834
786
  epsilonClamp = 1e-18,
835
787
  } = params
836
788
 
@@ -848,10 +800,9 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
848
800
  const priceEffUpper = effSnap.getEffectivePrice(upperPrice)
849
801
 
850
802
  // We mirror the on-chain logic in `wrapper_provide_liquidity`:
851
- // 1. Use compute_token_needs_with_crossing to infer the ratio of SY/PT
852
- // the market "wants" for this price range.
853
- // When crossing tick state is provided, this accounts for the existing
854
- // PT/SY proportions in the active tick, significantly improving accuracy.
803
+ // 1. Use curve-level token needs to infer the SY/PT ratio for this range.
804
+ // Raw active-tick proportions are ignored because the deposit CPI
805
+ // normalizes the active tick before minting new shares.
855
806
  // 2. Use that ratio plus the current SY exchange rate to decide how much of the
856
807
  // minted SY should be stripped into PT (calc_strip_amount).
857
808
  const [syMock, ptMock] = computeTokenNeedsWithCrossing(
@@ -864,9 +815,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
864
815
  1e9,
865
816
  1e9,
866
817
  epsilonClamp,
867
- crossingTickState,
868
- crossingTickPriceLeft,
869
- crossingTickPriceRight,
870
818
  )
871
819
 
872
820
  // Total SY the user would get by minting SY from base off-chain
@@ -922,6 +870,7 @@ function calcStripAmount(totalAmountSy: number, curSyRate: number, marketPtLiq:
922
870
  * @param lowerTick - Lower tick (APY in basis points)
923
871
  * @param upperTick - Upper tick (APY in basis points)
924
872
  * @param syExchangeRate - SY exchange rate
873
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
925
874
  * @returns Simulation result with LP out, YT out, amounts spent, etc.
926
875
  */
927
876
  export function simulateWrapperProvideLiquidity(
@@ -930,6 +879,7 @@ export function simulateWrapperProvideLiquidity(
930
879
  lowerTick: number,
931
880
  upperTick: number,
932
881
  syExchangeRate: number,
882
+ options: AddLiquiditySimulationOptions = {},
933
883
  ): {
934
884
  lpOut: number
935
885
  ytOut: number
@@ -961,10 +911,7 @@ export function simulateWrapperProvideLiquidity(
961
911
  const priceEffLower = snap.getEffectivePrice(lowerPrice)
962
912
  const priceEffUpper = snap.getEffectivePrice(upperPrice)
963
913
 
964
- // Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
965
- const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks)
966
-
967
- // Step 4: Calculate mock token needs using compute_token_needs_with_crossing
914
+ // Step 3: Calculate curve-level token needs.
968
915
  // max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
969
916
  const maxPt = syAmount * syExchangeRate
970
917
  const [syMock, ptMock] = computeTokenNeedsWithCrossing(
@@ -977,26 +924,23 @@ export function simulateWrapperProvideLiquidity(
977
924
  syAmount,
978
925
  maxPt,
979
926
  configurationOptions.epsilonClamp,
980
- crossingTickState,
981
- crossingTickPriceLeft,
982
- crossingTickPriceRight,
983
927
  )
984
928
 
985
- // Step 5: Calculate how much SY to strip (calc_strip_amount on-chain)
929
+ // Step 4: Calculate how much SY to strip (calc_strip_amount on-chain)
986
930
  // Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
987
931
  const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock)
988
932
  const syToStrip = Math.min(syToStripRaw, syAmount)
989
933
 
990
- // Step 6: Calculate PT and YT from stripping
934
+ // Step 5: Calculate PT and YT from stripping
991
935
  // When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
992
936
  const ptFromStrip = syToStrip * syExchangeRate
993
937
  const ytOut = ptFromStrip // YT amount equals PT amount from strip
994
938
 
995
- // Step 7: Calculate remaining SY after strip
939
+ // Step 6: Calculate remaining SY after strip
996
940
  // Use Math.max to ensure non-negative (safety net for floating point edge cases)
997
941
  const syRemainder = Math.max(0, syAmount - syToStrip)
998
942
 
999
- // Step 8: Simulate deposit liquidity with remaining SY and PT
943
+ // Step 7: Simulate deposit liquidity with remaining SY and PT
1000
944
  const depositResult = simulateAddLiquidity(marketState, {
1001
945
  lowerTick,
1002
946
  upperTick,
@@ -1037,6 +981,7 @@ export function simulateWrapperProvideLiquidity(
1037
981
  * @param lowerTick - Lower tick key (APY in basis points)
1038
982
  * @param upperTick - Upper tick key (APY in basis points)
1039
983
  * @param syExchangeRate - SY exchange rate
984
+ * @param options - Optional existing LP position to include fixed crossing-split equalization spend
1040
985
  * @returns Simulation result with LP out, PT to buy, SY constraint, etc.
1041
986
  */
1042
987
  export function simulateSwapAndSupply(
@@ -1045,6 +990,7 @@ export function simulateSwapAndSupply(
1045
990
  lowerTick: number,
1046
991
  upperTick: number,
1047
992
  syExchangeRate: number,
993
+ options: AddLiquiditySimulationOptions = {},
1048
994
  ): {
1049
995
  lpOut: number
1050
996
  ptToBuy: number
@@ -1061,7 +1007,7 @@ export function simulateSwapAndSupply(
1061
1007
  // So the strict SY budget for a user-provided base input is floor(base / rate).
1062
1008
  const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate)
1063
1009
 
1064
- if (syBudget <= 0 || syBudget < MIN_SWAP_AND_SUPPLY_SY_BUDGET) {
1010
+ if (syBudget <= 0) {
1065
1011
  return {
1066
1012
  lpOut: 0,
1067
1013
  ptToBuy: 0,
@@ -1143,6 +1089,7 @@ export function simulateSwapAndSupply(
1143
1089
  key,
1144
1090
  syExchangeRate,
1145
1091
  exactOutEstimateSlack,
1092
+ options.existingPosition,
1146
1093
  )
1147
1094
  candidateCache.set(key, candidate)
1148
1095
  return candidate
@@ -1294,6 +1241,7 @@ function simulateSwapAndSupplyForPtConstraint(
1294
1241
  ptConstraint: number,
1295
1242
  syExchangeRate: number,
1296
1243
  exactOutEstimateSlack: number,
1244
+ existingPosition?: LpPositionCLMM,
1297
1245
  ): SwapAndSupplyCandidate | null {
1298
1246
  let swapResult: {
1299
1247
  sySpent: number
@@ -1315,13 +1263,22 @@ function simulateSwapAndSupplyForPtConstraint(
1315
1263
  }
1316
1264
 
1317
1265
  const depositState = swapResult.postMarketState ?? marketState
1318
- const depositResult = simulateAddLiquidity(depositState, {
1319
- lowerTick,
1320
- upperTick,
1321
- maxSy: syBudget,
1322
- maxPt: swapResult.ptOut,
1323
- syExchangeRate,
1324
- })
1266
+ let depositResult: AddLiquidityOutcome
1267
+ try {
1268
+ depositResult = simulateAddLiquidityForPosition(
1269
+ depositState,
1270
+ {
1271
+ lowerTick,
1272
+ upperTick,
1273
+ maxSy: syBudget,
1274
+ maxPt: swapResult.ptOut,
1275
+ syExchangeRate,
1276
+ },
1277
+ existingPosition,
1278
+ )
1279
+ } catch {
1280
+ return null
1281
+ }
1325
1282
 
1326
1283
  // Swap & supply must actually add liquidity.
1327
1284
  // Discard candidates that end up as swap-only (deltaL == 0).