@exponent-labs/market-three-math 0.1.8 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/build/addLiquidity.d.ts +65 -4
  2. package/build/addLiquidity.js +762 -36
  3. package/build/addLiquidity.js.map +1 -1
  4. package/build/bisect.d.ts +11 -0
  5. package/build/bisect.js +22 -12
  6. package/build/bisect.js.map +1 -1
  7. package/build/index.d.ts +5 -4
  8. package/build/index.js +14 -7
  9. package/build/index.js.map +1 -1
  10. package/build/liquidityHistogram.d.ts +6 -1
  11. package/build/liquidityHistogram.js +57 -12
  12. package/build/liquidityHistogram.js.map +1 -1
  13. package/build/quote.d.ts +1 -1
  14. package/build/quote.js +70 -84
  15. package/build/quote.js.map +1 -1
  16. package/build/swap.js +36 -18
  17. package/build/swap.js.map +1 -1
  18. package/build/swapV2.d.ts +6 -1
  19. package/build/swapV2.js +394 -52
  20. package/build/swapV2.js.map +1 -1
  21. package/build/types.d.ts +51 -0
  22. package/build/utils.d.ts +8 -2
  23. package/build/utils.js +37 -19
  24. package/build/utils.js.map +1 -1
  25. package/build/utilsV2.d.ts +9 -0
  26. package/build/utilsV2.js +131 -9
  27. package/build/utilsV2.js.map +1 -1
  28. package/build/withdrawLiquidity.js +12 -7
  29. package/build/withdrawLiquidity.js.map +1 -1
  30. package/build/ytTrades.d.ts +7 -0
  31. package/build/ytTrades.js +166 -146
  32. package/build/ytTrades.js.map +1 -1
  33. package/build/ytTradesLegacy.js +3 -4
  34. package/build/ytTradesLegacy.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/addLiquidity.ts +1012 -38
  37. package/src/bisect.ts +22 -11
  38. package/src/index.ts +11 -5
  39. package/src/liquidityHistogram.ts +54 -9
  40. package/src/quote.ts +73 -95
  41. package/src/swap.ts +35 -19
  42. package/src/swapV2.ts +999 -0
  43. package/src/types.ts +55 -0
  44. package/src/utils.ts +24 -3
  45. package/src/utilsV2.ts +337 -0
  46. package/src/withdrawLiquidity.ts +12 -6
  47. package/src/ytTrades.ts +191 -172
  48. package/src/ytTradesLegacy.ts +419 -0
  49. package/build/swap-v2.d.ts +0 -20
  50. package/build/swap-v2.js +0 -261
  51. package/build/swap-v2.js.map +0 -1
  52. package/build/swapLegacy.d.ts +0 -16
  53. package/build/swapLegacy.js +0 -229
  54. package/build/swapLegacy.js.map +0 -1
  55. package/src/swapLegacy.ts +0 -272
package/src/bisect.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Binary search (bisection method) to find root of a monotonic function.
3
+ * Finds x where f(x) = 0 within the range [lo, hi].
4
+ *
5
+ * @param func - Monotonic function to find root of
6
+ * @param lo - Lower bound of search range
7
+ * @param hi - Upper bound of search range
8
+ * @param epsilon - Convergence tolerance (default 0.0001)
9
+ * @param maxIterations - Maximum iterations (default 2000)
10
+ * @returns The x value where f(x) ≈ 0, or null if root is not bracketed
11
+ */
1
12
  export function bisectSearch2(
2
13
  func: (x: number) => number,
3
14
  lo: number,
@@ -9,13 +20,12 @@ export function bisectSearch2(
9
20
  throw new Error("lo must be less than hi")
10
21
  }
11
22
 
12
- let iterations = 0
13
-
14
23
  // Evaluate boundaries once
15
- const fLo = func(lo)
16
- const fHi = func(hi)
24
+ let fLo = func(lo)
25
+ let fHi = func(hi)
17
26
 
18
27
  // Check if the target is bracketed between lo and hi
28
+ // Root exists only if f(lo) and f(hi) have opposite signs
19
29
  if (fLo * fHi > 0) {
20
30
  return null
21
31
  }
@@ -28,6 +38,8 @@ export function bisectSearch2(
28
38
  let bestMid = (lo + hi) / 2
29
39
  let bestError = Infinity
30
40
 
41
+ let iterations = 0
42
+
31
43
  // Binary search within the range
32
44
  while (iterations < maxIterations) {
33
45
  // Use midpoint for standard bisection
@@ -48,22 +60,21 @@ export function bisectSearch2(
48
60
  return mid
49
61
  }
50
62
 
51
- // Early termination if range is too small relative to epsilon
63
+ // Early termination if range is too small
52
64
  const rangeSize = hi - lo
53
65
  if (rangeSize < epsilon * 0.01) {
54
66
  return bestMid
55
67
  }
56
68
 
57
- // Adjust the search range based on the target
58
- // Use fLo instead of recalculating func(lo)
69
+ // Adjust the search range based on signs
70
+ // If f(lo) and f(mid) have opposite signs, root is in [lo, mid]
71
+ // Otherwise, root is in [mid, hi]
59
72
  if (fLo * fMid < 0) {
60
- // eslint-disable-next-line no-param-reassign
61
73
  hi = mid
62
- // Note: fHi would be fMid, but we don't need to track it
74
+ fHi = fMid
63
75
  } else {
64
- // eslint-disable-next-line no-param-reassign
65
76
  lo = mid
66
- // Note: fLo would be fMid, but we recalculate in the condition anyway
77
+ fLo = fMid
67
78
  }
68
79
  }
69
80
 
package/src/index.ts CHANGED
@@ -15,10 +15,7 @@ export * from "./types"
15
15
 
16
16
  // Export utility functions
17
17
  export {
18
- EffSnap,
19
18
  normalizedTimeRemaining,
20
- calculateFeeRate,
21
- getFeeFromAmount,
22
19
  getActiveLiquidity,
23
20
  getSuccessorTickKey,
24
21
  getPredecessorTickKey,
@@ -27,13 +24,16 @@ export {
27
24
  convertApyToApyBp,
28
25
  convertApyBpToApy,
29
26
  calcPtPriceInAsset,
30
- getSuccessorTickByIdx,
27
+ getSuccessorTickIdxByIdx,
31
28
  } from "./utils"
32
29
 
33
30
  // Export swap functions
34
- export { simulateSwap } from "./swap"
31
+ export { simulateSwap } from "./swapV2"
35
32
  export { getSwapQuote, QuoteDirection } from "./quote"
36
33
 
34
+ // Export V2 utilities
35
+ export { EffSnap, TicksWrapper, calculateFeeRate, getFeeFromAmount } from "./utilsV2"
36
+
37
37
  // Export YT trade functions
38
38
  export { simulateBuyYt, simulateSellYt, simulateBuyYtWithSyIn } from "./ytTrades"
39
39
  export type {
@@ -47,10 +47,14 @@ export type {
47
47
  export {
48
48
  simulateAddLiquidity,
49
49
  computeLiquidityTargetAndTokenNeeds,
50
+ computeTokenNeedsWithCrossing,
51
+ getCrossingTickStateFromTicks,
52
+ scaleCrossingTickInputs,
50
53
  calculateLpOut,
51
54
  estimateBalancedDeposit,
52
55
  calcDepositSyAndPtFromBaseAmount,
53
56
  simulateWrapperProvideLiquidity,
57
+ simulateSwapAndSupply,
54
58
  } from "./addLiquidity"
55
59
 
56
60
  export { getPtAndSyOnWithdrawLiquidity } from "./withdrawLiquidity"
@@ -69,6 +73,8 @@ export type {
69
73
  AddLiquidityArgs,
70
74
  AddLiquidityOutcome,
71
75
  LiquidityNeeds,
76
+ CrossingTickState,
77
+ CrossingScaleParams,
72
78
  MarketThreeState,
73
79
  } from "./types"
74
80
  export { SwapDirection } from "./types"
@@ -25,6 +25,10 @@ export interface LiquidityHistogramBin {
25
25
  * from remove_liquidity.rs, where liquidity is distributed proportionally based on
26
26
  * the spot price ranges.
27
27
  *
28
+ * When currentTickBp is provided, PT is only assigned to bins below current price
29
+ * and SY is only assigned to bins above current price. This ensures the visual
30
+ * representation correctly shows the boundary between PT and SY zones.
31
+ *
28
32
  * Example:
29
33
  * - Created ticks at: 1%, 5%, 6%
30
34
  * - tickSpace: 1% (100 basis points)
@@ -39,9 +43,10 @@ export interface LiquidityHistogramBin {
39
43
  *
40
44
  * @param ticks - The market's ticks state
41
45
  * @param tickSpace - The tick spacing in basis points (e.g., 100 for 1%)
46
+ * @param currentTickBp - Optional current tick in basis points for proper PT/SY zone splitting
42
47
  * @returns Array of histogram bins aligned to tickSpace
43
48
  */
44
- export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): LiquidityHistogramBin[] {
49
+ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number, currentTickBp?: number): LiquidityHistogramBin[] {
45
50
  const histogram: LiquidityHistogramBin[] = []
46
51
 
47
52
  // Edge case: no ticks created yet
@@ -49,8 +54,15 @@ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): Liquid
49
54
  return histogram
50
55
  }
51
56
 
52
- // Find min and max tick keys (apyBasePoints)
53
- const tickKeys = ticks.ticksTree.map((t) => t.apyBasePoints).sort((a, b) => a - b)
57
+ // Only consider ticks with actual liquidity when determining range
58
+ const ticksWithLiquidity = ticks.ticksTree.filter(t => t.principalPt > 0 || t.principalSy > 0)
59
+
60
+ if (ticksWithLiquidity.length === 0) {
61
+ return histogram
62
+ }
63
+
64
+ // Find min and max tick keys (apyBasePoints) from ticks with liquidity
65
+ const tickKeys = ticksWithLiquidity.map((t) => t.apyBasePoints).sort((a, b) => a - b)
54
66
  const minTickKey = tickKeys[0]
55
67
  const maxTickKey = tickKeys[tickKeys.length - 1]
56
68
 
@@ -102,8 +114,19 @@ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): Liquid
102
114
  const containingBinKey = Math.floor(intervalStartKey / tickSpace) * tickSpace
103
115
  const bin = binMap.get(containingBinKey)
104
116
  if (bin) {
105
- bin.principalPt += Number(intervalPrincipalPt)
106
- bin.principalSy += Number(intervalPrincipalSy)
117
+ // If currentTickBp is provided, only assign PT to bins below current price
118
+ // and SY to bins above current price
119
+ if (currentTickBp !== undefined) {
120
+ if (containingBinKey < currentTickBp) {
121
+ bin.principalPt += Number(intervalPrincipalPt)
122
+ }
123
+ if (containingBinKey >= currentTickBp) {
124
+ bin.principalSy += Number(intervalPrincipalSy)
125
+ }
126
+ } else {
127
+ bin.principalPt += Number(intervalPrincipalPt)
128
+ bin.principalSy += Number(intervalPrincipalSy)
129
+ }
107
130
  }
108
131
  } else {
109
132
  // Distribute proportionally across bins
@@ -119,8 +142,19 @@ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): Liquid
119
142
  // Proportional share of liquidity for this bin
120
143
  const share = binRange / fullRange
121
144
 
122
- bin.principalPt += Math.floor(Number(intervalPrincipalPt) * share)
123
- bin.principalSy += Math.floor(Number(intervalPrincipalSy) * share)
145
+ // If currentTickBp is provided, only assign PT to bins below current price
146
+ // and SY to bins above current price
147
+ if (currentTickBp !== undefined) {
148
+ if (binKey < currentTickBp) {
149
+ bin.principalPt += Math.floor(Number(intervalPrincipalPt) * share)
150
+ }
151
+ if (binKey >= currentTickBp) {
152
+ bin.principalSy += Math.floor(Number(intervalPrincipalSy) * share)
153
+ }
154
+ } else {
155
+ bin.principalPt += Math.floor(Number(intervalPrincipalPt) * share)
156
+ bin.principalSy += Math.floor(Number(intervalPrincipalSy) * share)
157
+ }
124
158
  }
125
159
  }
126
160
  } else {
@@ -128,8 +162,19 @@ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): Liquid
128
162
  const containingBinKey = Math.floor(currentTickKey / tickSpace) * tickSpace
129
163
  const bin = binMap.get(containingBinKey)
130
164
  if (bin) {
131
- bin.principalPt += Number(intervalPrincipalPt)
132
- bin.principalSy += Number(intervalPrincipalSy)
165
+ // If currentTickBp is provided, only assign PT to bins below current price
166
+ // and SY to bins above current price
167
+ if (currentTickBp !== undefined) {
168
+ if (containingBinKey < currentTickBp) {
169
+ bin.principalPt += Number(intervalPrincipalPt)
170
+ }
171
+ if (containingBinKey >= currentTickBp) {
172
+ bin.principalSy += Number(intervalPrincipalSy)
173
+ }
174
+ } else {
175
+ bin.principalPt += Number(intervalPrincipalPt)
176
+ bin.principalSy += Number(intervalPrincipalSy)
177
+ }
133
178
  }
134
179
  }
135
180
  }
package/src/quote.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
2
-
3
- import { simulateSwap } from "./swap"
4
- import { MarketThreeState, SwapDirection, SwapOutcome } from "./types"
1
+ import { simulateSwap } from "./swapV2"
2
+ import { MarketThreeState, SwapDirection } from "./types"
5
3
  import { simulateBuyYtWithSyIn, simulateSellYt } from "./ytTrades"
6
4
 
7
5
  export enum QuoteDirection {
@@ -20,109 +18,89 @@ export type Quote = {
20
18
 
21
19
  /**
22
20
  * Calculate the expected output for a given input amount
23
- * This is a convenience wrapper around simulateSwap
21
+ * This is a convenience wrapper around simulateSwapV2
24
22
  */
25
23
  export function getSwapQuote(marketState: MarketThreeState, amountIn: number, direction: QuoteDirection): Quote {
26
- //? Calculate correct currentTick index
27
- //TODO Remove this logic when contract is updated!!!
28
- const currentTickIndex = calculateCurrentTickIndex(marketState.ticks)
29
-
30
- const marketStateMutated: MarketThreeState = {
31
- ...marketState,
32
- ticks: {
33
- ...marketState.ticks,
34
- currentTick: currentTickIndex,
35
- },
36
- }
37
-
38
- if (direction === QuoteDirection.PtToSy) {
39
- const { amountInConsumed, amountOut, lpFeeChargedOutToken, protocolFeeChargedOutToken } = simulateSwap(
40
- marketStateMutated,
41
- {
42
- direction: SwapDirection.PtToSy,
43
- amountIn,
44
- syExchangeRate: marketStateMutated.currentSyExchangeRate,
45
- isCurrentFlashSwap: false,
46
- },
47
- )
48
- return {
49
- amountIn: amountInConsumed,
50
- amountOut: amountOut,
51
- lpFee: lpFeeChargedOutToken,
52
- protocolFee: protocolFeeChargedOutToken,
53
- }
54
- }
55
- if (direction === QuoteDirection.SyToPt) {
56
- const { amountInConsumed, amountOut, lpFeeChargedOutToken, protocolFeeChargedOutToken } = simulateSwap(
57
- marketStateMutated,
58
- {
59
- direction: SwapDirection.SyToPt,
60
- amountIn,
61
- syExchangeRate: marketStateMutated.currentSyExchangeRate,
62
- isCurrentFlashSwap: false,
24
+ try {
25
+ const marketStateMutated: MarketThreeState = {
26
+ ...marketState,
27
+ ticks: {
28
+ ...marketState.ticks,
29
+ currentTick: marketState.ticks.currentTick,
63
30
  },
64
- )
65
- return {
66
- amountIn: amountInConsumed,
67
- amountOut: amountOut,
68
- lpFee: lpFeeChargedOutToken,
69
- protocolFee: protocolFeeChargedOutToken,
70
31
  }
71
- }
72
-
73
- if (direction === QuoteDirection.YtToSy) {
74
- const { ytIn, netSyReceived, lpFee, protocolFee } = simulateSellYt(marketStateMutated, {
75
- ytIn: amountIn,
76
- syExchangeRate: marketStateMutated.currentSyExchangeRate,
77
- })
78
32
 
79
- return {
80
- amountIn: ytIn,
81
- amountOut: netSyReceived,
82
- lpFee: lpFee,
83
- protocolFee: protocolFee,
33
+ if (direction === QuoteDirection.PtToSy) {
34
+ const { amountInConsumed, amountOut, lpFeeChargedOutToken, protocolFeeChargedOutToken } = simulateSwap(
35
+ marketStateMutated,
36
+ {
37
+ direction: SwapDirection.PtToSy,
38
+ amountIn,
39
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
40
+ isCurrentFlashSwap: false,
41
+ },
42
+ )
43
+ return {
44
+ amountIn: amountInConsumed,
45
+ amountOut: amountOut,
46
+ lpFee: lpFeeChargedOutToken,
47
+ protocolFee: protocolFeeChargedOutToken,
48
+ }
49
+ }
50
+ if (direction === QuoteDirection.SyToPt) {
51
+ const { amountInConsumed, amountOut, lpFeeChargedOutToken, protocolFeeChargedOutToken } = simulateSwap(
52
+ marketStateMutated,
53
+ {
54
+ direction: SwapDirection.SyToPt,
55
+ amountIn,
56
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
57
+ isCurrentFlashSwap: false,
58
+ },
59
+ )
60
+ return {
61
+ amountIn: amountInConsumed,
62
+ amountOut: amountOut,
63
+ lpFee: lpFeeChargedOutToken,
64
+ protocolFee: protocolFeeChargedOutToken,
65
+ }
84
66
  }
85
- }
86
67
 
87
- if (direction === QuoteDirection.SyToYt) {
88
- const { ytOut, netSyCost, lpFee, protocolFee } = simulateBuyYtWithSyIn(marketStateMutated, {
89
- syIn: amountIn,
90
- syExchangeRate: marketStateMutated.currentSyExchangeRate,
91
- })
68
+ if (direction === QuoteDirection.YtToSy) {
69
+ const { ytIn, netSyReceived, lpFee, protocolFee } = simulateSellYt(marketStateMutated, {
70
+ ytIn: amountIn,
71
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
72
+ })
92
73
 
93
- return {
94
- amountIn: netSyCost,
95
- amountOut: ytOut,
96
- lpFee: lpFee,
97
- protocolFee,
74
+ return {
75
+ amountIn: ytIn,
76
+ amountOut: netSyReceived,
77
+ lpFee: lpFee,
78
+ protocolFee: protocolFee,
79
+ }
98
80
  }
99
- }
100
81
 
101
- throw new Error(`Unknown quote direction: ${direction}`)
102
- }
82
+ if (direction === QuoteDirection.SyToYt) {
83
+ const { ytOut, netSyCost, lpFee, protocolFee } = simulateBuyYtWithSyIn(marketStateMutated, {
84
+ syIn: amountIn,
85
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
86
+ })
103
87
 
104
- /**
105
- * Calculate the correct currentTick index for the market state
106
- *
107
- * IMPORTANT: currentTick must be a 1-based ARRAY INDEX, not apyBasePoints!
108
- * The simulateSwap function uses findTickByIndex which does: ticksTree.at(index - 1)
109
- *
110
- * @returns The 1-based array index of the tick where impliedRate <= currentSpotPrice
111
- */
112
- function calculateCurrentTickIndex(ticksData: Ticks): number {
113
- // Find the tick with highest impliedRate <= currentSpotPrice
114
- let currentTickApyBps = ticksData.ticksTree[0]?.apyBasePoints || 0
88
+ return {
89
+ amountIn: netSyCost,
90
+ amountOut: ytOut,
91
+ lpFee: lpFee,
92
+ protocolFee,
93
+ }
94
+ }
115
95
 
116
- for (const tick of ticksData.ticksTree) {
117
- const tickRate = Number(tick.impliedRate)
118
- if (tickRate <= ticksData.currentSpotPrice) {
119
- currentTickApyBps = tick.apyBasePoints
96
+ throw new Error(`Unknown quote direction: ${direction}`)
97
+ } catch (error) {
98
+ console.error("Unable to get swap quote", error)
99
+ return {
100
+ amountIn,
101
+ amountOut: 0,
102
+ lpFee: 0,
103
+ protocolFee: 0,
120
104
  }
121
105
  }
122
-
123
- // Find the array index (0-based) of this tick
124
- const arrayIndex = ticksData.ticksTree.findIndex((t: Tick) => t.apyBasePoints === currentTickApyBps)
125
-
126
- // Convert to 1-based index for simulateSwap
127
- return arrayIndex !== -1 ? arrayIndex + 1 : 1
128
106
  }
package/src/swap.ts CHANGED
@@ -13,11 +13,10 @@ import {
13
13
  bigIntMin,
14
14
  calculateFeeRate,
15
15
  findTickByIndex,
16
- findTickByKey,
17
16
  getFeeFromAmount,
18
17
  getImpliedRate,
19
- getPredecessorTickKey,
20
- getSuccessorTickKey,
18
+ getPredecessorTickIdxByIdx,
19
+ getSuccessorTickIdxByIdx,
21
20
  normalizedTimeRemaining,
22
21
  } from "./utils"
23
22
 
@@ -81,25 +80,25 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
81
80
  iterations++
82
81
  if (debug) console.log(`\n--- Iteration ${iterations}, amountInLeft=${amountInLeft} ---`)
83
82
 
84
- // Get right boundary of current interval
85
- const rightBoundaryIndexOpt = findTickByKey(ticks, getSuccessorTickKey(ticks, currentLeftBoundaryIndex))?.index
83
+ // Get right boundary of current interval (successor tick index)
84
+ const rightBoundaryIndexOpt = getSuccessorTickIdxByIdx(ticks, currentLeftBoundaryIndex)
86
85
  if (debug) console.log(`rightBoundary=${rightBoundaryIndexOpt}`)
87
86
 
88
87
  if (rightBoundaryIndexOpt === null) {
89
88
  if (args.direction === SwapDirection.SyToPt) {
90
89
  // Cross to create a new interval
91
- const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
92
- if (predecessor === null) break
90
+ const predecessorIdx = getPredecessorTickIdxByIdx(ticks, currentLeftBoundaryIndex)
91
+ if (predecessorIdx === null) break
93
92
 
94
93
  // Update active liquidity by subtracting liquidity_net at boundary
95
- const boundaryTick = findTickByKey(ticks, predecessor)
94
+ const boundaryTick = findTickByIndex(ticks, currentLeftBoundaryIndex)
96
95
 
97
96
  // When crossing downward (SyToPt), update state
98
- currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex) // Boundary we're crossing
99
- currentLeftBoundaryIndex = boundaryTick.index // New left boundary
97
+ currentPriceSpot = getImpliedRate(boundaryTick?.apyBasePoints ?? 0) // Boundary we're crossing
98
+ currentLeftBoundaryIndex = predecessorIdx // New left boundary
100
99
 
101
100
  if (boundaryTick) {
102
- activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
101
+ activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.liquidityNet)
103
102
  activeLiquidityF64 = Number(activeLiquidityU64)
104
103
  }
105
104
  continue
@@ -167,7 +166,16 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
167
166
  const syOutGrossClamped: number = Math.min(syOutGross, Number(principalSy))
168
167
 
169
168
  if (syOutGrossClamped > 0) {
170
- const totalFeeOut: number = getFeeFromAmount(syOutGrossClamped, lpFeeRate)
169
+ // For flash swaps, fee is based on YT value = (pt - sy × sy_exchange_rate) / sy_exchange_rate (in SY terms)
170
+ let totalFeeOut: number
171
+ if (args.isCurrentFlashSwap) {
172
+ const syOutBase = syOutGrossClamped * args.syExchangeRate
173
+ const ytValueBase = ptInSegment - syOutBase
174
+ const ytValueSy = ytValueBase / args.syExchangeRate
175
+ totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValueSy)), lpFeeRate)
176
+ } else {
177
+ totalFeeOut = getFeeFromAmount(syOutGrossClamped, lpFeeRate)
178
+ }
171
179
  const protocolFeeOut: number = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
172
180
  const lpFeeOut: number = totalFeeOut - protocolFeeOut
173
181
  const syOutNet: number = syOutGrossClamped - totalFeeOut
@@ -204,7 +212,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
204
212
 
205
213
  if (deltaCToLeftBoundary <= eps) {
206
214
  // Cross boundary to the left
207
- const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
215
+ const predecessor = getPredecessorTickIdxByIdx(ticks, currentLeftBoundaryIndex)
208
216
  if (predecessor === null) break
209
217
 
210
218
  // Update active liquidity by subtracting liquidity_net at boundary
@@ -214,7 +222,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
214
222
  activeLiquidityF64 = Number(activeLiquidityU64)
215
223
  }
216
224
 
217
- currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
225
+ currentPriceSpot = getImpliedRate(boundaryTick?.apyBasePoints ?? 0)
218
226
  currentLeftBoundaryIndex = predecessor
219
227
  continue
220
228
  }
@@ -240,7 +248,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
240
248
 
241
249
  if (ptOutGrossU64 === 0n) {
242
250
  // Nothing to pay out; try to cross
243
- const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
251
+ const predecessor = getPredecessorTickIdxByIdx(ticks, currentLeftBoundaryIndex)
244
252
  if (predecessor === null) break
245
253
 
246
254
  // Update active liquidity
@@ -250,13 +258,21 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
250
258
  activeLiquidityF64 = Number(activeLiquidityU64)
251
259
  }
252
260
 
253
- currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
261
+ currentPriceSpot = getImpliedRate(boundaryTick?.apyBasePoints ?? 0)
254
262
  currentLeftBoundaryIndex = predecessor
255
263
  continue
256
264
  }
257
265
 
258
266
  // Fees in token_out (PT)
259
- const totalFeeOut: number = getFeeFromAmount(Number(ptOutGrossU64), lpFeeRate)
267
+ // For flash swaps, fee is based on YT value = pt - sy × sy_exchange_rate
268
+ let totalFeeOut: number
269
+ if (args.isCurrentFlashSwap) {
270
+ const syInBase = Number(syInSegmentU64) * args.syExchangeRate
271
+ const ytValue = Number(ptOutGrossU64) - syInBase
272
+ totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
273
+ } else {
274
+ totalFeeOut = getFeeFromAmount(Number(ptOutGrossU64), lpFeeRate)
275
+ }
260
276
  const protocolFeeOut: number = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
261
277
  const lpFeeOut: number = totalFeeOut - protocolFeeOut
262
278
  const ptOutNet: number = Number(ptOutGrossU64) - totalFeeOut
@@ -272,7 +288,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
272
288
 
273
289
  // If we hit boundary, cross
274
290
  if (Math.abs(currentPriceSpot - anchorULeft) <= eps && amountInLeft > 0) {
275
- const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
291
+ const predecessor = getPredecessorTickIdxByIdx(ticks, currentLeftBoundaryIndex)
276
292
  if (predecessor === null) break
277
293
 
278
294
  // Update active liquidity
@@ -282,7 +298,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
282
298
  activeLiquidityF64 = Number(activeLiquidityU64)
283
299
  }
284
300
 
285
- currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
301
+ currentPriceSpot = getImpliedRate(boundaryTick?.apyBasePoints ?? 0)
286
302
  currentLeftBoundaryIndex = predecessor
287
303
  }
288
304
  }