@exponent-labs/market-three-math 0.1.8

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 (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +197 -0
  3. package/build/addLiquidity.d.ts +67 -0
  4. package/build/addLiquidity.js +269 -0
  5. package/build/addLiquidity.js.map +1 -0
  6. package/build/bisect.d.ts +1 -0
  7. package/build/bisect.js +62 -0
  8. package/build/bisect.js.map +1 -0
  9. package/build/index.d.ts +24 -0
  10. package/build/index.js +76 -0
  11. package/build/index.js.map +1 -0
  12. package/build/liquidityHistogram.d.ts +50 -0
  13. package/build/liquidityHistogram.js +162 -0
  14. package/build/liquidityHistogram.js.map +1 -0
  15. package/build/quote.d.ts +18 -0
  16. package/build/quote.js +106 -0
  17. package/build/quote.js.map +1 -0
  18. package/build/swap-v2.d.ts +20 -0
  19. package/build/swap-v2.js +261 -0
  20. package/build/swap-v2.js.map +1 -0
  21. package/build/swap.d.ts +15 -0
  22. package/build/swap.js +249 -0
  23. package/build/swap.js.map +1 -0
  24. package/build/swapLegacy.d.ts +16 -0
  25. package/build/swapLegacy.js +229 -0
  26. package/build/swapLegacy.js.map +1 -0
  27. package/build/swapV2.d.ts +11 -0
  28. package/build/swapV2.js +406 -0
  29. package/build/swapV2.js.map +1 -0
  30. package/build/types.d.ts +73 -0
  31. package/build/types.js +9 -0
  32. package/build/types.js.map +1 -0
  33. package/build/utils.d.ts +119 -0
  34. package/build/utils.js +219 -0
  35. package/build/utils.js.map +1 -0
  36. package/build/utilsV2.d.ts +88 -0
  37. package/build/utilsV2.js +180 -0
  38. package/build/utilsV2.js.map +1 -0
  39. package/build/withdrawLiquidity.d.ts +8 -0
  40. package/build/withdrawLiquidity.js +174 -0
  41. package/build/withdrawLiquidity.js.map +1 -0
  42. package/build/ytTrades.d.ts +106 -0
  43. package/build/ytTrades.js +292 -0
  44. package/build/ytTrades.js.map +1 -0
  45. package/build/ytTradesLegacy.d.ts +106 -0
  46. package/build/ytTradesLegacy.js +292 -0
  47. package/build/ytTradesLegacy.js.map +1 -0
  48. package/examples/.env.example +1 -0
  49. package/examples/test-histogram-simple.ts +172 -0
  50. package/examples/test-histogram.ts +112 -0
  51. package/package.json +26 -0
  52. package/src/addLiquidity.ts +384 -0
  53. package/src/bisect.ts +72 -0
  54. package/src/index.ts +74 -0
  55. package/src/liquidityHistogram.ts +192 -0
  56. package/src/quote.ts +128 -0
  57. package/src/swap.ts +299 -0
  58. package/src/swapLegacy.ts +272 -0
  59. package/src/types.ts +80 -0
  60. package/src/utils.ts +235 -0
  61. package/src/withdrawLiquidity.ts +240 -0
  62. package/src/ytTrades.ts +419 -0
  63. package/tsconfig.json +17 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Liquidity histogram utilities for CLMM markets
3
+ * Builds a histogram of liquidity distribution across tick_space aligned bins
4
+ */
5
+
6
+ import { Ticks } from "@exponent-labs/exponent-fetcher"
7
+ import { convertApyBpToApy, getSuccessorTickKey } from "./utils"
8
+
9
+ export interface LiquidityHistogramBin {
10
+ /** APY for this bin (tick_space aligned) */
11
+ apy: number
12
+ /** Principal PT amount in this bin */
13
+ principalPt: number
14
+ /** Principal SY amount in this bin */
15
+ principalSy: number
16
+ /** Tick key (basis points) */
17
+ tickKey: number
18
+ }
19
+
20
+ /**
21
+ * Build a liquidity distribution histogram with tick_space aligned bins
22
+ *
23
+ * This function projects liquidity from created ticks onto virtual bins aligned
24
+ * to tickSpace. The projection logic is analogous to project_anchor_shares_to_current_ticks
25
+ * from remove_liquidity.rs, where liquidity is distributed proportionally based on
26
+ * the spot price ranges.
27
+ *
28
+ * Example:
29
+ * - Created ticks at: 1%, 5%, 6%
30
+ * - tickSpace: 1% (100 basis points)
31
+ * - Result bins: 1%, 2%, 3%, 4%, 5%, 6%
32
+ *
33
+ * For a tick interval [1%, 5%] with principalPt=1000:
34
+ * - Range: 5% - 1% = 4%
35
+ * - Bin [1%, 2%]: gets 1000 * (2%-1%)/(5%-1%) = 250
36
+ * - Bin [2%, 3%]: gets 1000 * (3%-2%)/(5%-1%) = 250
37
+ * - Bin [3%, 4%]: gets 1000 * (4%-3%)/(5%-1%) = 250
38
+ * - Bin [4%, 5%]: gets 1000 * (5%-4%)/(5%-1%) = 250
39
+ *
40
+ * @param ticks - The market's ticks state
41
+ * @param tickSpace - The tick spacing in basis points (e.g., 100 for 1%)
42
+ * @returns Array of histogram bins aligned to tickSpace
43
+ */
44
+ export function buildLiquidityHistogram(ticks: Ticks, tickSpace: number): LiquidityHistogramBin[] {
45
+ const histogram: LiquidityHistogramBin[] = []
46
+
47
+ // Edge case: no ticks created yet
48
+ if (!ticks.ticksTree.length || ticks.ticksTree.length === 0) {
49
+ return histogram
50
+ }
51
+
52
+ // Find min and max tick keys (apyBasePoints)
53
+ const tickKeys = ticks.ticksTree.map((t) => t.apyBasePoints).sort((a, b) => a - b)
54
+ const minTickKey = tickKeys[0]
55
+ const maxTickKey = tickKeys[tickKeys.length - 1]
56
+
57
+ // Align min and max to tickSpace boundaries
58
+ const minAlignedKey = Math.floor(minTickKey / tickSpace) * tickSpace
59
+ const maxAlignedKey = Math.ceil(maxTickKey / tickSpace) * tickSpace
60
+
61
+ // Create map of created ticks for fast lookup
62
+ const tickMap = new Map<number, (typeof ticks.ticksTree)[0]>()
63
+ for (const tick of ticks.ticksTree) {
64
+ tickMap.set(tick.apyBasePoints, tick)
65
+ }
66
+
67
+ // Create bins map (virtual ticks aligned to tickSpace)
68
+ const binMap = new Map<number, { principalPt: number; principalSy: number }>()
69
+
70
+ // Initialize all bins from min to max with tickSpace steps
71
+ for (let binKey = minAlignedKey; binKey <= maxAlignedKey; binKey += tickSpace) {
72
+ binMap.set(binKey, { principalPt: 0, principalSy: 0 })
73
+ }
74
+
75
+ // Iterate through all created tick intervals and project liquidity
76
+ let currentTickKey: number | null = minTickKey
77
+
78
+ while (currentTickKey !== null) {
79
+ const currentTick = tickMap.get(currentTickKey)
80
+ const nextTickKey = getSuccessorTickKey(ticks, currentTickKey)
81
+
82
+ if (currentTick) {
83
+ // Principal amounts in this tick
84
+ const intervalPrincipalPt = currentTick.principalPt
85
+ const intervalPrincipalSy = currentTick.principalSy
86
+
87
+ // Skip if no liquidity in this tick
88
+ if (intervalPrincipalPt > 0 || intervalPrincipalSy > 0) {
89
+ if (nextTickKey !== null) {
90
+ // We have an interval [currentTickKey, nextTickKey)
91
+ const intervalStartKey = currentTickKey
92
+ const intervalEndKey = nextTickKey
93
+ const fullRange = intervalEndKey - intervalStartKey
94
+
95
+ // Find all bins that intersect with this interval
96
+ const firstBinKey = Math.ceil(intervalStartKey / tickSpace) * tickSpace
97
+ const lastBinKey = Math.floor((intervalEndKey - 1) / tickSpace) * tickSpace
98
+
99
+ // Special case: interval is smaller than tickSpace
100
+ if (firstBinKey > lastBinKey) {
101
+ // Put all liquidity in the bin that contains the interval start
102
+ const containingBinKey = Math.floor(intervalStartKey / tickSpace) * tickSpace
103
+ const bin = binMap.get(containingBinKey)
104
+ if (bin) {
105
+ bin.principalPt += Number(intervalPrincipalPt)
106
+ bin.principalSy += Number(intervalPrincipalSy)
107
+ }
108
+ } else {
109
+ // Distribute proportionally across bins
110
+ for (let binKey = firstBinKey; binKey <= lastBinKey; binKey += tickSpace) {
111
+ const bin = binMap.get(binKey)
112
+ if (!bin) continue
113
+
114
+ // Calculate the range of this bin that overlaps with the interval
115
+ const binStart = Math.max(binKey, intervalStartKey)
116
+ const binEnd = Math.min(binKey + tickSpace, intervalEndKey)
117
+ const binRange = binEnd - binStart
118
+
119
+ // Proportional share of liquidity for this bin
120
+ const share = binRange / fullRange
121
+
122
+ bin.principalPt += Math.floor(Number(intervalPrincipalPt) * share)
123
+ bin.principalSy += Math.floor(Number(intervalPrincipalSy) * share)
124
+ }
125
+ }
126
+ } else {
127
+ // Last tick - put all liquidity in its bin
128
+ const containingBinKey = Math.floor(currentTickKey / tickSpace) * tickSpace
129
+ const bin = binMap.get(containingBinKey)
130
+ if (bin) {
131
+ bin.principalPt += Number(intervalPrincipalPt)
132
+ bin.principalSy += Number(intervalPrincipalSy)
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ currentTickKey = nextTickKey
139
+ }
140
+
141
+ // Convert map to sorted array
142
+ const sortedBinKeys = Array.from(binMap.keys()).sort((a, b) => a - b)
143
+
144
+ for (const binKey of sortedBinKeys) {
145
+ const bin = binMap.get(binKey)!
146
+
147
+ // Only include bins with non-zero liquidity
148
+ if (bin.principalPt > 0 || bin.principalSy > 0) {
149
+ histogram.push({
150
+ apy: convertApyBpToApy(binKey),
151
+ principalPt: bin.principalPt,
152
+ principalSy: bin.principalSy,
153
+ tickKey: binKey,
154
+ })
155
+ }
156
+ }
157
+
158
+ return histogram
159
+ }
160
+
161
+ /**
162
+ * Build a simplified histogram without projection
163
+ *
164
+ * This version simply returns the principal amounts at each created tick,
165
+ * without projecting onto tickSpace-aligned bins.
166
+ *
167
+ * @param ticks - The market's ticks state
168
+ * @returns Array of histogram bins for created ticks only
169
+ */
170
+ export function buildLiquidityHistogramSimple(ticks: Ticks): LiquidityHistogramBin[] {
171
+ const histogram: LiquidityHistogramBin[] = []
172
+
173
+ if (!ticks.ticksTree || ticks.ticksTree.length === 0) {
174
+ return histogram
175
+ }
176
+
177
+ // Sort by apyBasePoints and iterate
178
+ const sortedTicks = [...ticks.ticksTree].sort((a, b) => a.apyBasePoints - b.apyBasePoints)
179
+
180
+ for (const tick of sortedTicks) {
181
+ if (tick.principalPt > 0 || tick.principalSy > 0) {
182
+ histogram.push({
183
+ apy: convertApyBpToApy(tick.apyBasePoints),
184
+ principalPt: Number(tick.principalPt),
185
+ principalSy: Number(tick.principalSy),
186
+ tickKey: tick.apyBasePoints,
187
+ })
188
+ }
189
+ }
190
+
191
+ return histogram
192
+ }
package/src/quote.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
2
+
3
+ import { simulateSwap } from "./swap"
4
+ import { MarketThreeState, SwapDirection, SwapOutcome } from "./types"
5
+ import { simulateBuyYtWithSyIn, simulateSellYt } from "./ytTrades"
6
+
7
+ export enum QuoteDirection {
8
+ PtToSy = "PtToSy",
9
+ SyToPt = "SyToPt",
10
+ YtToSy = "YtToSy",
11
+ SyToYt = "SyToYt",
12
+ }
13
+
14
+ export type Quote = {
15
+ amountIn: number
16
+ amountOut: number
17
+ lpFee: number
18
+ protocolFee: number
19
+ }
20
+
21
+ /**
22
+ * Calculate the expected output for a given input amount
23
+ * This is a convenience wrapper around simulateSwap
24
+ */
25
+ 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,
63
+ },
64
+ )
65
+ return {
66
+ amountIn: amountInConsumed,
67
+ amountOut: amountOut,
68
+ lpFee: lpFeeChargedOutToken,
69
+ protocolFee: protocolFeeChargedOutToken,
70
+ }
71
+ }
72
+
73
+ if (direction === QuoteDirection.YtToSy) {
74
+ const { ytIn, netSyReceived, lpFee, protocolFee } = simulateSellYt(marketStateMutated, {
75
+ ytIn: amountIn,
76
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
77
+ })
78
+
79
+ return {
80
+ amountIn: ytIn,
81
+ amountOut: netSyReceived,
82
+ lpFee: lpFee,
83
+ protocolFee: protocolFee,
84
+ }
85
+ }
86
+
87
+ if (direction === QuoteDirection.SyToYt) {
88
+ const { ytOut, netSyCost, lpFee, protocolFee } = simulateBuyYtWithSyIn(marketStateMutated, {
89
+ syIn: amountIn,
90
+ syExchangeRate: marketStateMutated.currentSyExchangeRate,
91
+ })
92
+
93
+ return {
94
+ amountIn: netSyCost,
95
+ amountOut: ytOut,
96
+ lpFee: lpFee,
97
+ protocolFee,
98
+ }
99
+ }
100
+
101
+ throw new Error(`Unknown quote direction: ${direction}`)
102
+ }
103
+
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
115
+
116
+ for (const tick of ticksData.ticksTree) {
117
+ const tickRate = Number(tick.impliedRate)
118
+ if (tickRate <= ticksData.currentSpotPrice) {
119
+ currentTickApyBps = tick.apyBasePoints
120
+ }
121
+ }
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
+ }
package/src/swap.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * CLMM Swap simulation - V2
3
+ * Updated to match the latest Rust on-chain implementation
4
+ * Key changes:
5
+ * - Uses currentPrefixSum for active liquidity
6
+ * - Implements kappa scaling for principal-limited swaps
7
+ * - Correct tick key conversions (1e6 basis)
8
+ */
9
+ import { MarketThreeState, SwapArgs, SwapDirection, SwapOutcome } from "./types"
10
+ import {
11
+ EffSnap,
12
+ bigIntMax,
13
+ bigIntMin,
14
+ calculateFeeRate,
15
+ findTickByIndex,
16
+ findTickByKey,
17
+ getFeeFromAmount,
18
+ getImpliedRate,
19
+ getPredecessorTickKey,
20
+ getSuccessorTickKey,
21
+ normalizedTimeRemaining,
22
+ } from "./utils"
23
+
24
+ const BASE_POINTS = 10000
25
+
26
+ /**
27
+ * Simulate a swap on the CLMM market
28
+ * This is a pure function that does not mutate the market state
29
+ * Returns the swap outcome including amounts and final state
30
+ */
31
+ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): SwapOutcome {
32
+ const { financials, configurationOptions, ticks } = marketState
33
+ const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
34
+
35
+ // Create effective price snapshot
36
+ const snapshot: EffSnap = new EffSnap(normalizedTimeRemaining(secondsRemaining), args.syExchangeRate)
37
+
38
+ // Current state
39
+ let currentPriceSpot: number = ticks.currentSpotPrice
40
+ let currentLeftBoundaryIndex: number = ticks.currentTick
41
+
42
+ // Use currentPrefixSum if available, otherwise fall back to calculating it
43
+ let activeLiquidityU64: bigint = ticks.currentPrefixSum ?? 0n
44
+ let activeLiquidityF64: number = Number(activeLiquidityU64)
45
+
46
+ // Fees
47
+ const lpFeeRate: number = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
48
+ const protocolFeeBps = configurationOptions.treasuryFeeBps
49
+
50
+ // Check price limits
51
+ if (args.priceSpotLimit !== undefined) {
52
+ if (args.direction === SwapDirection.PtToSy) {
53
+ if (args.priceSpotLimit < currentPriceSpot) {
54
+ throw new Error("Price limit violated: limit must be >= current price for PtToSy")
55
+ }
56
+ } else {
57
+ if (args.priceSpotLimit > currentPriceSpot) {
58
+ throw new Error("Price limit violated: limit must be <= current price for SyToPt")
59
+ }
60
+ }
61
+ }
62
+
63
+ // Accumulators
64
+ let amountOutNet: number = 0
65
+ let feeLpOut: number = 0
66
+ let feeProtocolOut: number = 0
67
+ let amountInLeft: number = args.amountIn
68
+
69
+ // Main loop across contiguous intervals
70
+ let iterations: number = 0
71
+ const MAX_ITERATIONS: number = 1000 // Safety limit
72
+ const debug = false // Set to true for debugging
73
+
74
+ if (debug) console.log(`\nSwap Debug: direction=${args.direction}, amountIn=${args.amountIn}`)
75
+ if (debug)
76
+ console.log(
77
+ `Initial: currentTick=${currentLeftBoundaryIndex}, spotPrice=${currentPriceSpot}, activeLiq=${activeLiquidityU64}`,
78
+ )
79
+
80
+ while (amountInLeft > 0 && iterations < MAX_ITERATIONS) {
81
+ iterations++
82
+ if (debug) console.log(`\n--- Iteration ${iterations}, amountInLeft=${amountInLeft} ---`)
83
+
84
+ // Get right boundary of current interval
85
+ const rightBoundaryIndexOpt = findTickByKey(ticks, getSuccessorTickKey(ticks, currentLeftBoundaryIndex))?.index
86
+ if (debug) console.log(`rightBoundary=${rightBoundaryIndexOpt}`)
87
+
88
+ if (rightBoundaryIndexOpt === null) {
89
+ if (args.direction === SwapDirection.SyToPt) {
90
+ // Cross to create a new interval
91
+ const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
92
+ if (predecessor === null) break
93
+
94
+ // Update active liquidity by subtracting liquidity_net at boundary
95
+ const boundaryTick = findTickByKey(ticks, predecessor)
96
+
97
+ // When crossing downward (SyToPt), update state
98
+ currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex) // Boundary we're crossing
99
+ currentLeftBoundaryIndex = boundaryTick.index // New left boundary
100
+
101
+ if (boundaryTick) {
102
+ activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
103
+ activeLiquidityF64 = Number(activeLiquidityU64)
104
+ }
105
+ continue
106
+ } else {
107
+ // No more liquidity available
108
+ break
109
+ }
110
+ }
111
+
112
+ const rightBoundaryIndex: number = rightBoundaryIndexOpt ?? 0
113
+
114
+ // Get anchor prices for interval boundaries
115
+ const anchorULeft: number = getImpliedRate(findTickByIndex(ticks, currentLeftBoundaryIndex)?.apyBasePoints ?? 0)
116
+ const anchorURight: number = getImpliedRate(findTickByIndex(ticks, rightBoundaryIndex)?.apyBasePoints ?? 0)
117
+
118
+ // Effective price at current spot
119
+ const cEffOld: number = snapshot.getEffectivePrice(currentPriceSpot)
120
+
121
+ // Get principal ledgers for the interval
122
+ const currentTickData = findTickByIndex(ticks, currentLeftBoundaryIndex)
123
+
124
+ const principalPt: bigint = currentTickData?.principalPt ?? 0n
125
+ const principalSy: bigint = currentTickData?.principalSy ?? 0n
126
+
127
+ const eps: number = configurationOptions.epsilonClamp
128
+
129
+ // Calculate kappa (scaling factor based on available principal)
130
+ // Y_max = (L/τ) * (C(u_old) - C(u_right))
131
+ const cEffAtBoundary: number = snapshot.getEffectivePrice(anchorURight)
132
+ const yMaxToBoundaryF: number = activeLiquidityF64 * (cEffOld - cEffAtBoundary)
133
+ const kappaSy: number = yMaxToBoundaryF > 0 ? Number(principalSy) / yMaxToBoundaryF : 0
134
+
135
+ const duToLeft: number = currentPriceSpot - anchorULeft
136
+ const ptMaxToLeftF: number = activeLiquidityF64 * duToLeft
137
+ const kappaPt: number = ptMaxToLeftF > 0 ? Number(principalPt) / ptMaxToLeftF : 0
138
+
139
+ const kappa = Math.min(kappaPt, kappaSy)
140
+
141
+ // Boundary ΔC if we go to u_right
142
+ const lTradeF64: number = activeLiquidityF64 * kappa
143
+
144
+ if (args.direction === SwapDirection.PtToSy) {
145
+ // PT -> SY swap (buying SY with PT)
146
+ const duByInput: number = lTradeF64 > 0 ? amountInLeft / lTradeF64 : 0
147
+ const duToBoundary: number = anchorURight - currentPriceSpot
148
+ const duActual: number = Math.min(duByInput, duToBoundary)
149
+
150
+ if (duToBoundary <= eps) {
151
+ // Cross boundary
152
+ const boundaryTick = findTickByIndex(ticks, rightBoundaryIndex)
153
+ if (boundaryTick) {
154
+ activeLiquidityU64 += boundaryTick.liquidityNet
155
+ activeLiquidityF64 = Number(activeLiquidityU64)
156
+ }
157
+ currentLeftBoundaryIndex = rightBoundaryIndex
158
+ currentPriceSpot = anchorURight
159
+ continue
160
+ }
161
+
162
+ // Token flows for this segment
163
+ const ptInSegment: number = Math.floor(lTradeF64 * duActual)
164
+ const anchorUNew: number = currentPriceSpot + duActual
165
+ const cEffNew: number = snapshot.getEffectivePrice(anchorUNew)
166
+ const syOutGross: number = Math.floor(lTradeF64 * (cEffOld - cEffNew))
167
+ const syOutGrossClamped: number = Math.min(syOutGross, Number(principalSy))
168
+
169
+ if (syOutGrossClamped > 0) {
170
+ const totalFeeOut: number = getFeeFromAmount(syOutGrossClamped, lpFeeRate)
171
+ const protocolFeeOut: number = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
172
+ const lpFeeOut: number = totalFeeOut - protocolFeeOut
173
+ const syOutNet: number = syOutGrossClamped - totalFeeOut
174
+
175
+ amountOutNet += syOutNet
176
+ feeLpOut += lpFeeOut
177
+ feeProtocolOut += protocolFeeOut
178
+ }
179
+
180
+ amountInLeft -= ptInSegment
181
+ currentPriceSpot = anchorUNew
182
+
183
+ // If we hit boundary, cross
184
+ if (Math.abs(anchorURight - currentPriceSpot) <= eps && amountInLeft > 0) {
185
+ const boundaryTick = findTickByIndex(ticks, rightBoundaryIndex)
186
+ if (boundaryTick) {
187
+ activeLiquidityU64 += boundaryTick.liquidityNet
188
+ activeLiquidityF64 = Number(activeLiquidityU64)
189
+ }
190
+ currentLeftBoundaryIndex = rightBoundaryIndex
191
+ currentPriceSpot = anchorURight
192
+ }
193
+ } else {
194
+ // SY -> PT swap (buying PT with SY)
195
+ const cEffLeft: number = snapshot.getEffectivePrice(anchorULeft)
196
+ const deltaCByInput: number = lTradeF64 > 0 ? amountInLeft / lTradeF64 : 0
197
+ const deltaCToLeftBoundary: number = Math.max(0, cEffLeft - cEffOld)
198
+ const deltaCActual: number = Math.min(deltaCByInput, deltaCToLeftBoundary)
199
+
200
+ if (debug) {
201
+ console.log(`SyToPt deltas: byInput=${deltaCByInput}, toBoundary=${deltaCToLeftBoundary}`)
202
+ console.log(` deltaCActual=${deltaCActual}, eps=${eps}, kappa=${kappa}, lTrade=${lTradeF64}`)
203
+ }
204
+
205
+ if (deltaCToLeftBoundary <= eps) {
206
+ // Cross boundary to the left
207
+ const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
208
+ if (predecessor === null) break
209
+
210
+ // Update active liquidity by subtracting liquidity_net at boundary
211
+ const boundaryTick = findTickByIndex(ticks, currentLeftBoundaryIndex)
212
+ if (boundaryTick) {
213
+ activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.liquidityNet)
214
+ activeLiquidityF64 = Number(activeLiquidityU64)
215
+ }
216
+
217
+ currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
218
+ currentLeftBoundaryIndex = predecessor
219
+ continue
220
+ }
221
+
222
+ // New effective price and spot price after consuming ΔC
223
+ const cEffNew: number = cEffOld + deltaCActual
224
+ const spotPriceNew: number = snapshot.spotPriceFromEffectivePrice(cEffNew)
225
+
226
+ // Token flows
227
+ const syInSegmentF: number = lTradeF64 * (cEffNew - cEffOld)
228
+ const duAbs: number = currentPriceSpot - spotPriceNew
229
+ const ptOutGrossF: number = lTradeF64 * duAbs
230
+
231
+ // Clamp gross PT by available principal
232
+ const ptOutGrossU64: bigint = bigIntMin(BigInt(Math.floor(ptOutGrossF)), principalPt)
233
+ const syInSegmentU64: bigint = BigInt(Math.floor(syInSegmentF))
234
+
235
+ if (debug) {
236
+ console.log(`SyToPt: deltaCActual=${deltaCActual}, cEffNew=${cEffNew}, spotPriceNew=${spotPriceNew}`)
237
+ console.log(` duAbs=${duAbs}, ptOutGrossF=${ptOutGrossF}, ptOutGrossU64=${ptOutGrossU64}`)
238
+ console.log(` syInSegmentU64=${syInSegmentU64}, principalPt=${principalPt}`)
239
+ }
240
+
241
+ if (ptOutGrossU64 === 0n) {
242
+ // Nothing to pay out; try to cross
243
+ const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
244
+ if (predecessor === null) break
245
+
246
+ // Update active liquidity
247
+ const boundaryTick = findTickByIndex(ticks, currentLeftBoundaryIndex)
248
+ if (boundaryTick) {
249
+ activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.liquidityNet)
250
+ activeLiquidityF64 = Number(activeLiquidityU64)
251
+ }
252
+
253
+ currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
254
+ currentLeftBoundaryIndex = predecessor
255
+ continue
256
+ }
257
+
258
+ // Fees in token_out (PT)
259
+ const totalFeeOut: number = getFeeFromAmount(Number(ptOutGrossU64), lpFeeRate)
260
+ const protocolFeeOut: number = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
261
+ const lpFeeOut: number = totalFeeOut - protocolFeeOut
262
+ const ptOutNet: number = Number(ptOutGrossU64) - totalFeeOut
263
+
264
+ // Accumulate to user
265
+ amountOutNet += ptOutNet
266
+ feeLpOut += lpFeeOut
267
+ feeProtocolOut += protocolFeeOut
268
+
269
+ // Consume input and advance state
270
+ amountInLeft -= Number(syInSegmentU64)
271
+ currentPriceSpot = spotPriceNew
272
+
273
+ // If we hit boundary, cross
274
+ if (Math.abs(currentPriceSpot - anchorULeft) <= eps && amountInLeft > 0) {
275
+ const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
276
+ if (predecessor === null) break
277
+
278
+ // Update active liquidity
279
+ const boundaryTick = findTickByIndex(ticks, currentLeftBoundaryIndex)
280
+ if (boundaryTick) {
281
+ activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.liquidityNet)
282
+ activeLiquidityF64 = Number(activeLiquidityU64)
283
+ }
284
+
285
+ currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
286
+ currentLeftBoundaryIndex = predecessor
287
+ }
288
+ }
289
+ }
290
+
291
+ return {
292
+ amountInConsumed: args.amountIn - amountInLeft,
293
+ amountOut: amountOutNet,
294
+ lpFeeChargedOutToken: feeLpOut,
295
+ protocolFeeChargedOutToken: feeProtocolOut,
296
+ finalSpotPrice: currentPriceSpot,
297
+ finalTickIndex: currentLeftBoundaryIndex,
298
+ }
299
+ }