@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
@@ -0,0 +1,419 @@
1
+ /**
2
+ * YT (Yield Token) trade simulations
3
+ *
4
+ * YT trades are multi-step operations that involve PT trades:
5
+ * - Buy YT: Strip SY → PT+YT, then sell PT back to pool
6
+ * - Sell YT: Merge PT+YT → SY, then buy PT from pool to repay
7
+ */
8
+ import { bisectSearch2 } from "./bisect"
9
+ import { simulateSwap } from "./swapV2"
10
+ import { MarketThreeState, SwapDirection } from "./types"
11
+
12
+ /**
13
+ * Helper function to convert PY to SY
14
+ * @param syExchangeRate - The SY exchange rate
15
+ * @param pyAmount - The PY (PT or YT) amount
16
+ * @returns The equivalent SY amount
17
+ */
18
+ function pyToSy(syExchangeRate: number, pyAmount: number): number {
19
+ if (syExchangeRate <= 0) return 0
20
+ return Math.floor(pyAmount / syExchangeRate)
21
+ }
22
+
23
+ /**
24
+ * Helper function to convert SY to PY
25
+ * @param syExchangeRate - The SY exchange rate
26
+ * @param syAmount - The SY amount
27
+ * @returns The equivalent PY amount
28
+ */
29
+ function syToPy(syExchangeRate: number, syAmount: number): number {
30
+ return Math.floor(syAmount * syExchangeRate)
31
+ }
32
+
33
+ export interface BuyYtSimulationArgs {
34
+ /** Amount of YT desired to buy */
35
+ ytOut: number
36
+ /** SY exchange rate */
37
+ syExchangeRate: number
38
+ /** Optional spot price limit (anti-sandwich) */
39
+ priceSpotLimit?: number
40
+ }
41
+
42
+ export interface BuyYtSimulationResult {
43
+ /** Amount of YT received */
44
+ ytOut: number
45
+ /** Net SY cost to the trader */
46
+ netSyCost: number
47
+ /** Amount of SY that needs to be stripped */
48
+ syToStrip: number
49
+ /** Amount of PT received from stripping */
50
+ ptFromStrip: number
51
+ /** Amount of SY received from selling PT */
52
+ syFromPtSale: number
53
+ /** LP fee charged */
54
+ lpFee: number
55
+ /** Protocol fee charged */
56
+ protocolFee: number
57
+ /** Final spot price after the trade */
58
+ finalSpotPrice: number
59
+ }
60
+
61
+ /**
62
+ * Simulates buying YT tokens
63
+ *
64
+ * Process:
65
+ * 1. Calculate how much SY to strip to get desired YT
66
+ * 2. Strip SY → PT + YT (PT amount ≈ YT amount)
67
+ * 3. Sell PT to the pool (PtToSy direction)
68
+ * 4. Net cost = SY stripped - SY received from PT sale
69
+ *
70
+ * @param marketState - Current market state
71
+ * @param args - Simulation arguments
72
+ * @returns Simulation result with net SY cost
73
+ */
74
+ export function simulateBuyYt(marketState: MarketThreeState, args: BuyYtSimulationArgs): BuyYtSimulationResult {
75
+ const { ytOut, syExchangeRate, priceSpotLimit } = args
76
+
77
+ // Calculate how much SY needs to be stripped to get the desired YT
78
+ // Add 1 to counter-act the flooring function when converting from PY to SY
79
+ const syToStrip = pyToSy(syExchangeRate, ytOut) + 1
80
+
81
+ // Stripping gives approximately equal amounts of PT and YT
82
+ const ptFromStrip = ytOut
83
+
84
+ // Simulate selling the PT to get SY back
85
+ // Note: We use ytOut as the amount because PT out = YT out from the strip
86
+ const swapResult = simulateSwap(marketState, {
87
+ direction: SwapDirection.PtToSy,
88
+ amountIn: ytOut,
89
+ priceSpotLimit,
90
+ syExchangeRate,
91
+ isCurrentFlashSwap: true,
92
+ })
93
+
94
+ const syFromPtSale = swapResult.amountOut
95
+
96
+ // Net cost is the difference between what was stripped and what was received
97
+ const netSyCost = syToStrip - syFromPtSale
98
+
99
+ return {
100
+ ytOut,
101
+ netSyCost,
102
+ syToStrip,
103
+ ptFromStrip,
104
+ syFromPtSale,
105
+ lpFee: swapResult.lpFeeChargedOutToken,
106
+ protocolFee: swapResult.protocolFeeChargedOutToken,
107
+ finalSpotPrice: swapResult.finalSpotPrice,
108
+ }
109
+ }
110
+
111
+ export interface BuyYtWithSyInSimulationArgs {
112
+ /** Amount of SY to spend */
113
+ syIn: number
114
+ /** SY exchange rate */
115
+ syExchangeRate: number
116
+ /** Optional spot price limit (anti-sandwich) */
117
+ priceSpotLimit?: number
118
+ }
119
+
120
+ /**
121
+ * Simulates buying YT tokens given a SY input amount
122
+ *
123
+ * Process:
124
+ * 1. Strip syIn → PT + YT
125
+ * 2. Sell PT → get SY back (with price impact)
126
+ * 3. Strip SY again → more PT + YT
127
+ * 4. Repeat until convergence
128
+ * Total YT = YT₁ + YT₂ + YT₃ + ...
129
+ *
130
+ * Uses bisection search to find ytOut such that netSyCost = syIn
131
+ *
132
+ * @param marketState - Current market state
133
+ * @param args - Simulation arguments
134
+ * @returns Simulation result with calculated YT output
135
+ */
136
+ export function simulateBuyYtWithSyIn(
137
+ marketState: MarketThreeState,
138
+ args: BuyYtWithSyInSimulationArgs,
139
+ ): BuyYtSimulationResult {
140
+ const { syIn, syExchangeRate, priceSpotLimit } = args
141
+
142
+ // Lower bound: Start very low since PT recycling allows buying much more YT than naive calculation
143
+ // The actual minimum depends on PT price (how much SY we get back from selling PT)
144
+ // If PT price is high (close to 1), we get most SY back, so we can buy much more YT
145
+ // Example: With 1000 SY and PT price 0.9:
146
+ // - To get 5000 YT, strip 5000 SY → 5000 PT + 5000 YT
147
+ // - Sell 5000 PT at 0.9 → 4500 SY back
148
+ // - Net cost: 5000 - 4500 = 500 SY (way less than 1000!)
149
+ const minPossibleYt = 1
150
+
151
+ // Better initial upper bound estimate based on market liquidity
152
+ // Maximum possible YT is constrained by available PT liquidity in the market
153
+ const marketPtLiquidity = Number(marketState.financials.ptBalance)
154
+ const marketSyLiquidity = Number(marketState.financials.syBalance)
155
+
156
+ // Conservative estimate: we can't buy more YT than there's PT in the market
157
+ // High multiplier needed because PT recycling amplifies buying power dramatically
158
+ // Formula: ytOut ≈ syIn / (1 - ptPrice)
159
+ // Examples: PT@0.99 → 100x, PT@0.95 → 20x, PT@0.90 → 10x, PT@0.80 → 5x
160
+ const liquidityBasedMax = Math.min(
161
+ marketPtLiquidity * 0.9, // 90% of PT liquidity to be safe
162
+ syToPy(syExchangeRate, syIn) * 100, // 100x to handle PT prices up to 0.99
163
+ )
164
+
165
+ // Start with a reasonable initial guess (10x covers most realistic scenarios)
166
+ let maxPossibleYt = Math.min(syToPy(syExchangeRate, syIn) * 10, liquidityBasedMax)
167
+ let foundUpperBound = false
168
+ let lastValidCost = 0
169
+
170
+ // Use exponential search with better growth rate
171
+ for (let attempt = 0; attempt < 12; attempt++) {
172
+ try {
173
+ const syToStrip = pyToSy(syExchangeRate, maxPossibleYt) + 1
174
+
175
+ const swapResult = simulateSwap(marketState, {
176
+ direction: SwapDirection.PtToSy,
177
+ amountIn: maxPossibleYt,
178
+ priceSpotLimit,
179
+ syExchangeRate,
180
+ isCurrentFlashSwap: true,
181
+ })
182
+
183
+ const syFromPtSale = swapResult.amountOut
184
+ const netSyCost = syToStrip - syFromPtSale
185
+
186
+ // If this costs more than syIn, we found our upper bound
187
+ if (netSyCost > syIn) {
188
+ foundUpperBound = true
189
+ break
190
+ }
191
+
192
+ lastValidCost = netSyCost
193
+
194
+ // Use adaptive growth rate based on how far we are from target
195
+ const costRatio = syIn / Math.max(netSyCost, 1)
196
+ const growthFactor = attempt < 3 ? 2.0 : Math.min(1.5, 1 + costRatio * 0.3)
197
+
198
+ maxPossibleYt *= growthFactor
199
+
200
+ // Don't exceed liquidity constraints
201
+ if (maxPossibleYt > liquidityBasedMax) {
202
+ maxPossibleYt = liquidityBasedMax
203
+ foundUpperBound = true
204
+ break
205
+ }
206
+ } catch (error) {
207
+ // If simulation fails, we've exceeded market capacity
208
+ // Use last valid value with small buffer, ensuring it's > minPossibleYt
209
+ if (lastValidCost > 0) {
210
+ // Interpolate to find better upper bound
211
+ const estimatedMax = maxPossibleYt * 0.7 * (syIn / lastValidCost)
212
+ maxPossibleYt = Math.max(estimatedMax, minPossibleYt * 1.2)
213
+ } else {
214
+ // No valid cost yet, use a conservative upper bound
215
+ maxPossibleYt = Math.max(maxPossibleYt * 0.8, minPossibleYt * 1.5)
216
+ }
217
+ foundUpperBound = true
218
+ break
219
+ }
220
+ }
221
+
222
+ if (!foundUpperBound) {
223
+ throw new Error(`Could not find upper bound for YT amount. Market may have unusual price dynamics.`)
224
+ }
225
+
226
+ // Final safety check: ensure maxPossibleYt > minPossibleYt
227
+ if (maxPossibleYt <= minPossibleYt) {
228
+ maxPossibleYt = minPossibleYt * 2
229
+ }
230
+
231
+ // Use bisection search to find the ytOut that results in netSyCost = syIn
232
+ // Adaptive epsilon based on input size for better precision scaling
233
+ const adaptiveEpsilon = Math.max(0.01, syIn * 0.0001)
234
+
235
+ // Reduce max iterations since we have better bounds
236
+ const maxIterations = 10000
237
+
238
+ // Debug info for bounds validation
239
+ if (maxPossibleYt <= minPossibleYt) {
240
+ throw new Error(
241
+ `Invalid bisection bounds for buy YT. ` +
242
+ `Min: ${minPossibleYt}, Max: ${maxPossibleYt}, ` +
243
+ `SyIn: ${syIn}, SyExchangeRate: ${syExchangeRate}, ` +
244
+ `MarketPtBalance: ${marketPtLiquidity}, MarketSyBalance: ${marketSyLiquidity}`,
245
+ )
246
+ }
247
+
248
+ const ytOut = bisectSearch2(
249
+ (ytGuess: number) => {
250
+ // Calculate the cost for this ytGuess
251
+ const syToStrip = pyToSy(syExchangeRate, ytGuess) + 1
252
+
253
+ const swapResult = simulateSwap(marketState, {
254
+ direction: SwapDirection.PtToSy,
255
+ amountIn: ytGuess,
256
+ priceSpotLimit,
257
+ syExchangeRate,
258
+ isCurrentFlashSwap: true,
259
+ })
260
+
261
+ const syFromPtSale = swapResult.amountOut
262
+ const netSyCost = syToStrip - syFromPtSale
263
+
264
+ // Return the difference between actual cost and target syIn
265
+ return netSyCost - syIn
266
+ },
267
+ minPossibleYt,
268
+ maxPossibleYt,
269
+ adaptiveEpsilon,
270
+ maxIterations,
271
+ )
272
+
273
+ if (ytOut === null) {
274
+ throw new Error(
275
+ `Failed to converge on correct YT amount. ` +
276
+ `Search range: [${minPossibleYt}, ${maxPossibleYt}], ` +
277
+ `Epsilon: ${adaptiveEpsilon}`,
278
+ )
279
+ }
280
+
281
+ // Now calculate the full result with the found ytOut
282
+ return simulateBuyYt(marketState, {
283
+ ytOut,
284
+ syExchangeRate,
285
+ priceSpotLimit,
286
+ })
287
+ }
288
+
289
+ export interface SellYtSimulationArgs {
290
+ /** Amount of YT to sell */
291
+ ytIn: number
292
+ /** SY exchange rate */
293
+ syExchangeRate: number
294
+ /** Optional spot price limit (anti-sandwich) */
295
+ priceSpotLimit?: number
296
+ }
297
+
298
+ export interface SellYtSimulationResult {
299
+ /** Amount of YT sold */
300
+ ytIn: number
301
+ /** Net SY received by the trader */
302
+ netSyReceived: number
303
+ /** Amount of SY received from merging PT + YT */
304
+ syFromMerge: number
305
+ /** Amount of SY spent buying PT back */
306
+ sySpentOnPt: number
307
+ /** LP fee charged */
308
+ lpFee: number
309
+ /** Protocol fee charged */
310
+ protocolFee: number
311
+ /** Final spot price after the trade */
312
+ finalSpotPrice: number
313
+ }
314
+
315
+ /**
316
+ * Simulates selling YT tokens
317
+ *
318
+ * Process:
319
+ * 1. Merge PT + YT → SY (receive SY from the merge)
320
+ * 2. Buy PT from the pool to repay the borrowed PT (SyToPt direction)
321
+ * 3. Net received = SY from merge - SY spent on PT
322
+ *
323
+ * Note: The market must have at least 2x the YT amount in PT liquidity
324
+ * because the trader borrows PT, which is then bought back.
325
+ *
326
+ * @param marketState - Current market state
327
+ * @param args - Simulation arguments
328
+ * @returns Simulation result with net SY received
329
+ */
330
+ export function simulateSellYt(marketState: MarketThreeState, args: SellYtSimulationArgs): SellYtSimulationResult {
331
+ const { ytIn, syExchangeRate, priceSpotLimit } = args
332
+
333
+ // Check if there's sufficient PT liquidity
334
+ // The market needs at least 2x the YT amount in PT
335
+ if (marketState.financials.ptBalance < ytIn * 2) {
336
+ throw new Error(
337
+ `Insufficient PT liquidity in the market. Required: ${ytIn * 2}, Available: ${marketState.financials.ptBalance}`,
338
+ )
339
+ }
340
+
341
+ // Merging PT + YT gives back the original SY
342
+ // The amount of PT needed equals ytIn (1:1 ratio)
343
+ const syFromMerge = pyToSy(syExchangeRate, ytIn)
344
+
345
+ const ptNeeded = ytIn
346
+
347
+ const upperBoundSwap = simulateSwap(marketState, {
348
+ direction: SwapDirection.SyToPt,
349
+ amountIn: syFromMerge,
350
+ priceSpotLimit,
351
+ syExchangeRate,
352
+ isCurrentFlashSwap: true,
353
+ })
354
+
355
+ // Check that we can buy enough PT from CLMM
356
+ if (upperBoundSwap.amountOut < ptNeeded) {
357
+ throw new Error(
358
+ `Cannot buy enough PT with available SY. Need ${ytIn} PT but can only afford ${upperBoundSwap.amountOut} PT`,
359
+ )
360
+ }
361
+
362
+ // Better initial bounds for bisection
363
+ // We know the upper bound from the check above, and we can estimate a better lower bound
364
+ // based on the current price
365
+ const estimatedLowerBound = Math.min(syFromMerge * 0.5, syFromMerge * 0.9) // Start from 50% of max, but not too close to upper
366
+
367
+ // Adaptive epsilon based on PT needed
368
+ const adaptiveEpsilon = Math.max(0.01, ptNeeded * 0.0001)
369
+
370
+ // Safety check: ensure lower bound is less than upper bound
371
+ const safeLowerBound = Math.min(estimatedLowerBound, syFromMerge * 0.95)
372
+ const safeUpperBound = syFromMerge
373
+
374
+ if (safeLowerBound >= safeUpperBound) {
375
+ throw new Error(`Invalid bisection bounds for sell YT. Lower: ${safeLowerBound}, Upper: ${safeUpperBound}`)
376
+ }
377
+
378
+ const syToSpend = bisectSearch2(
379
+ (syGuess: number) => {
380
+ const swapResult = simulateSwap(marketState, {
381
+ direction: SwapDirection.SyToPt,
382
+ amountIn: syGuess,
383
+ priceSpotLimit,
384
+ syExchangeRate,
385
+ isCurrentFlashSwap: true,
386
+ })
387
+
388
+ return swapResult.amountOut - ptNeeded
389
+ },
390
+ safeLowerBound,
391
+ safeUpperBound,
392
+ adaptiveEpsilon,
393
+ 100, // Reduced iterations with better bounds
394
+ )
395
+
396
+ if (syToSpend === null) {
397
+ throw new Error("Failed to converge on correct SY amount using bisection search")
398
+ }
399
+
400
+ const swapResult = simulateSwap(marketState, {
401
+ direction: SwapDirection.SyToPt,
402
+ amountIn: syToSpend,
403
+ priceSpotLimit,
404
+ syExchangeRate,
405
+ isCurrentFlashSwap: true,
406
+ })
407
+
408
+ const netSyReceived = syFromMerge - swapResult.amountInConsumed
409
+
410
+ return {
411
+ ytIn,
412
+ netSyReceived,
413
+ syFromMerge,
414
+ sySpentOnPt: swapResult.amountInConsumed,
415
+ lpFee: swapResult.lpFeeChargedOutToken,
416
+ protocolFee: swapResult.protocolFeeChargedOutToken,
417
+ finalSpotPrice: swapResult.finalSpotPrice,
418
+ }
419
+ }
@@ -1,20 +0,0 @@
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
- /**
11
- * Simulate a swap on the CLMM market
12
- * This is a pure function that does not mutate the market state
13
- * Returns the swap outcome including amounts and final state
14
- */
15
- export declare function simulateSwap(marketState: MarketThreeState, args: SwapArgs): SwapOutcome;
16
- /**
17
- * Calculate the expected output for a given input amount
18
- * This is a convenience wrapper around simulateSwap
19
- */
20
- export declare function getSwapQuote(marketState: MarketThreeState, amountIn: number, direction: SwapDirection): SwapOutcome;
package/build/swap-v2.js DELETED
@@ -1,261 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSwapQuote = exports.simulateSwap = void 0;
4
- /**
5
- * CLMM Swap simulation - V2
6
- * Updated to match the latest Rust on-chain implementation
7
- * Key changes:
8
- * - Uses currentPrefixSum for active liquidity
9
- * - Implements kappa scaling for principal-limited swaps
10
- * - Correct tick key conversions (1e6 basis)
11
- */
12
- const types_1 = require("./types");
13
- const utils_1 = require("./utils");
14
- const BASE_POINTS = 10000;
15
- /**
16
- * Simulate a swap on the CLMM market
17
- * This is a pure function that does not mutate the market state
18
- * Returns the swap outcome including amounts and final state
19
- */
20
- function simulateSwap(marketState, args) {
21
- const { financials, configurationOptions, ticks } = marketState;
22
- const secondsRemaining = Math.max(0, financials.expirationTs - Date.now() / 1000);
23
- // Create effective price snapshot
24
- const snapshot = new utils_1.EffSnap((0, utils_1.normalizedTimeRemaining)(secondsRemaining), args.syExchangeRate);
25
- // Current state
26
- let currentPriceSpot = ticks.currentSpotPrice;
27
- let currentLeftBoundaryIndex = ticks.currentTick;
28
- // Use currentPrefixSum if available, otherwise fall back to calculating it
29
- let activeLiquidityU64 = ticks.currentPrefixSum ?? 0;
30
- let activeLiquidityF64 = activeLiquidityU64;
31
- // Fees
32
- const lpFeeRate = (0, utils_1.calculateFeeRate)(configurationOptions.lnFeeRateRoot, secondsRemaining);
33
- const protocolFeeBps = configurationOptions.treasuryFeeBps;
34
- // Check price limits
35
- if (args.priceSpotLimit !== undefined) {
36
- if (args.direction === types_1.SwapDirection.PtToSy) {
37
- if (args.priceSpotLimit < currentPriceSpot) {
38
- throw new Error("Price limit violated: limit must be >= current price for PtToSy");
39
- }
40
- }
41
- else {
42
- if (args.priceSpotLimit > currentPriceSpot) {
43
- throw new Error("Price limit violated: limit must be <= current price for SyToPt");
44
- }
45
- }
46
- }
47
- // Accumulators
48
- let amountOutNet = 0;
49
- let feeLpOut = 0;
50
- let feeProtocolOut = 0;
51
- let amountInLeft = args.amountIn;
52
- // Main loop across contiguous intervals
53
- let iterations = 0;
54
- const maxIterations = 1000; // Safety limit
55
- const debug = false; // Set to true for debugging
56
- if (debug)
57
- console.log(`\nSwap Debug: direction=${args.direction}, amountIn=${args.amountIn}`);
58
- if (debug)
59
- console.log(`Initial: currentTick=${currentLeftBoundaryIndex}, spotPrice=${currentPriceSpot}, activeLiq=${activeLiquidityU64}`);
60
- while (amountInLeft > 0 && iterations < maxIterations) {
61
- iterations++;
62
- if (debug)
63
- console.log(`\n--- Iteration ${iterations}, amountInLeft=${amountInLeft} ---`);
64
- // Get right boundary of current interval
65
- const rightBoundaryIndexOpt = (0, utils_1.getSuccessorTick)(ticks, currentLeftBoundaryIndex);
66
- if (debug)
67
- console.log(`rightBoundary=${rightBoundaryIndexOpt}`);
68
- if (rightBoundaryIndexOpt === null) {
69
- if (args.direction === types_1.SwapDirection.SyToPt) {
70
- // Cross to create a new interval
71
- const predecessor = (0, utils_1.getPredecessorTick)(ticks, currentLeftBoundaryIndex);
72
- if (predecessor === null)
73
- break;
74
- // When crossing downward (SyToPt), update state
75
- currentPriceSpot = (0, utils_1.getLnImpliedRate)(currentLeftBoundaryIndex); // Boundary we're crossing
76
- currentLeftBoundaryIndex = predecessor; // New left boundary
77
- // Update active liquidity by subtracting liquidity_net at boundary
78
- const boundaryTick = (0, utils_1.findTick)(ticks, predecessor);
79
- if (boundaryTick) {
80
- activeLiquidityU64 = Math.max(0, activeLiquidityU64 - boundaryTick.tick.liquidityNet);
81
- activeLiquidityF64 = activeLiquidityU64;
82
- }
83
- continue;
84
- }
85
- else {
86
- // No more liquidity available
87
- break;
88
- }
89
- }
90
- const rightBoundaryIndex = rightBoundaryIndexOpt;
91
- // Get anchor prices for interval boundaries
92
- const anchorULeft = (0, utils_1.getLnImpliedRate)(currentLeftBoundaryIndex);
93
- const anchorURight = (0, utils_1.getLnImpliedRate)(rightBoundaryIndex);
94
- // Effective price at current spot
95
- const cEffOld = snapshot.getEffectivePrice(currentPriceSpot);
96
- // Get principal ledgers for the interval
97
- const currentTickData = (0, utils_1.findTick)(ticks, currentLeftBoundaryIndex);
98
- const principalPt = currentTickData?.tick.principalPt ?? 0;
99
- const principalSy = currentTickData?.tick.principalSy ?? 0;
100
- const eps = configurationOptions.epsilonClamp;
101
- // Calculate kappa (scaling factor based on available principal)
102
- // Y_max = (L/τ) * (C(u_old) - C(u_right))
103
- const cEffAtBoundary = snapshot.getEffectivePrice(anchorURight);
104
- const yMaxToBoundaryF = (activeLiquidityF64 / snapshot.timeFactor) * (cEffOld - cEffAtBoundary);
105
- const kappaSy = yMaxToBoundaryF > 0 ? principalSy / yMaxToBoundaryF : 0;
106
- const duToLeft = currentPriceSpot - anchorULeft;
107
- const ptMaxToLeftF = activeLiquidityF64 * duToLeft;
108
- const kappaPt = ptMaxToLeftF > 0 ? principalPt / ptMaxToLeftF : 0;
109
- const kappa = Math.min(kappaPt, kappaSy, 1.0);
110
- const lTradeF64 = activeLiquidityF64 * kappa;
111
- if (args.direction === types_1.SwapDirection.PtToSy) {
112
- // PT -> SY swap (buying SY with PT)
113
- const duByInput = lTradeF64 > 0 ? amountInLeft / lTradeF64 : 0;
114
- const duToBoundary = anchorURight - currentPriceSpot;
115
- const duActual = Math.min(duByInput, duToBoundary);
116
- if (duToBoundary <= eps) {
117
- // Cross boundary
118
- const boundaryTick = (0, utils_1.findTick)(ticks, rightBoundaryIndex);
119
- if (boundaryTick) {
120
- activeLiquidityU64 += boundaryTick.tick.liquidityNet;
121
- activeLiquidityF64 = activeLiquidityU64;
122
- }
123
- currentLeftBoundaryIndex = rightBoundaryIndex;
124
- currentPriceSpot = anchorURight;
125
- continue;
126
- }
127
- // Token flows for this segment
128
- const ptInSegment = Math.floor(lTradeF64 * duActual);
129
- const anchorUNew = currentPriceSpot + duActual;
130
- const cEffNew = snapshot.getEffectivePrice(anchorUNew);
131
- const syOutGross = Math.floor((lTradeF64 / snapshot.timeFactor) * (cEffOld - cEffNew));
132
- const syOutGrossClamped = Math.min(syOutGross, principalSy);
133
- if (syOutGrossClamped > 0) {
134
- const totalFeeOut = (0, utils_1.getFeeFromAmount)(syOutGrossClamped, lpFeeRate);
135
- const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS);
136
- const lpFeeOut = totalFeeOut - protocolFeeOut;
137
- const syOutNet = syOutGrossClamped - totalFeeOut;
138
- amountOutNet += syOutNet;
139
- feeLpOut += lpFeeOut;
140
- feeProtocolOut += protocolFeeOut;
141
- }
142
- amountInLeft -= ptInSegment;
143
- currentPriceSpot = anchorUNew;
144
- // If we hit boundary, cross
145
- if (Math.abs(anchorURight - currentPriceSpot) <= eps && amountInLeft > 0) {
146
- const boundaryTick = (0, utils_1.findTick)(ticks, rightBoundaryIndex);
147
- if (boundaryTick) {
148
- activeLiquidityU64 += boundaryTick.tick.liquidityNet;
149
- activeLiquidityF64 = activeLiquidityU64;
150
- }
151
- currentLeftBoundaryIndex = rightBoundaryIndex;
152
- currentPriceSpot = anchorURight;
153
- }
154
- }
155
- else {
156
- // SY -> PT swap (buying PT with SY)
157
- const cEffLeft = snapshot.getEffectivePrice(anchorULeft);
158
- const deltaCByInput = lTradeF64 > 0 ? (snapshot.timeFactor / lTradeF64) * amountInLeft : 0;
159
- const deltaCToLeftBoundary = Math.max(0, cEffLeft - cEffOld);
160
- const deltaCActual = Math.min(deltaCByInput, deltaCToLeftBoundary);
161
- if (debug) {
162
- console.log(`SyToPt deltas: byInput=${deltaCByInput}, toBoundary=${deltaCToLeftBoundary}`);
163
- console.log(` deltaCActual=${deltaCActual}, eps=${eps}, kappa=${kappa}, lTrade=${lTradeF64}`);
164
- }
165
- if (deltaCToLeftBoundary <= eps) {
166
- // Cross boundary to the left
167
- const predecessor = (0, utils_1.getPredecessorTick)(ticks, currentLeftBoundaryIndex);
168
- if (predecessor === null)
169
- break;
170
- // Update active liquidity by subtracting liquidity_net at boundary
171
- const boundaryTick = (0, utils_1.findTick)(ticks, currentLeftBoundaryIndex);
172
- if (boundaryTick) {
173
- activeLiquidityU64 = Math.max(0, activeLiquidityU64 - boundaryTick.tick.liquidityNet);
174
- activeLiquidityF64 = activeLiquidityU64;
175
- }
176
- currentPriceSpot = (0, utils_1.getLnImpliedRate)(currentLeftBoundaryIndex);
177
- currentLeftBoundaryIndex = predecessor;
178
- continue;
179
- }
180
- // New effective price and spot price after consuming ΔC
181
- const cEffNew = cEffOld + deltaCActual;
182
- const spotPriceNew = snapshot.spotPriceFromEffectivePrice(cEffNew);
183
- // Token flows
184
- const syInSegmentF = (lTradeF64 / snapshot.timeFactor) * (cEffNew - cEffOld);
185
- const duAbs = currentPriceSpot - spotPriceNew;
186
- const ptOutGrossF = lTradeF64 * duAbs;
187
- // Clamp gross PT by available principal
188
- const ptOutGrossU64 = Math.min(Math.floor(ptOutGrossF), principalPt);
189
- const syInSegmentU64 = Math.floor(syInSegmentF);
190
- if (debug) {
191
- console.log(`SyToPt: deltaCActual=${deltaCActual}, cEffNew=${cEffNew}, spotPriceNew=${spotPriceNew}`);
192
- console.log(` duAbs=${duAbs}, ptOutGrossF=${ptOutGrossF}, ptOutGrossU64=${ptOutGrossU64}`);
193
- console.log(` syInSegmentU64=${syInSegmentU64}, principalPt=${principalPt}`);
194
- }
195
- if (ptOutGrossU64 === 0) {
196
- // Nothing to pay out; try to cross
197
- const predecessor = (0, utils_1.getPredecessorTick)(ticks, currentLeftBoundaryIndex);
198
- if (predecessor === null)
199
- break;
200
- // Update active liquidity
201
- const boundaryTick = (0, utils_1.findTick)(ticks, currentLeftBoundaryIndex);
202
- if (boundaryTick) {
203
- activeLiquidityU64 = Math.max(0, activeLiquidityU64 - boundaryTick.tick.liquidityNet);
204
- activeLiquidityF64 = activeLiquidityU64;
205
- }
206
- currentPriceSpot = (0, utils_1.getLnImpliedRate)(currentLeftBoundaryIndex);
207
- currentLeftBoundaryIndex = predecessor;
208
- continue;
209
- }
210
- // Fees in token_out (PT)
211
- const totalFeeOut = (0, utils_1.getFeeFromAmount)(ptOutGrossU64, lpFeeRate);
212
- const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS);
213
- const lpFeeOut = totalFeeOut - protocolFeeOut;
214
- const ptOutNet = ptOutGrossU64 - totalFeeOut;
215
- // Accumulate to user
216
- amountOutNet += ptOutNet;
217
- feeLpOut += lpFeeOut;
218
- feeProtocolOut += protocolFeeOut;
219
- // Consume input and advance state
220
- amountInLeft -= syInSegmentU64;
221
- currentPriceSpot = spotPriceNew;
222
- // If we hit boundary, cross
223
- if (Math.abs(currentPriceSpot - anchorULeft) <= eps && amountInLeft > 0) {
224
- const predecessor = (0, utils_1.getPredecessorTick)(ticks, currentLeftBoundaryIndex);
225
- if (predecessor === null)
226
- break;
227
- // Update active liquidity
228
- const boundaryTick = (0, utils_1.findTick)(ticks, currentLeftBoundaryIndex);
229
- if (boundaryTick) {
230
- activeLiquidityU64 = Math.max(0, activeLiquidityU64 - boundaryTick.tick.liquidityNet);
231
- activeLiquidityF64 = activeLiquidityU64;
232
- }
233
- currentPriceSpot = (0, utils_1.getLnImpliedRate)(currentLeftBoundaryIndex);
234
- currentLeftBoundaryIndex = predecessor;
235
- }
236
- }
237
- }
238
- return {
239
- amountInConsumed: args.amountIn - amountInLeft,
240
- amountOut: amountOutNet,
241
- lpFeeChargedOutToken: feeLpOut,
242
- protocolFeeChargedOutToken: feeProtocolOut,
243
- finalSpotPrice: currentPriceSpot,
244
- finalTickIndex: currentLeftBoundaryIndex,
245
- };
246
- }
247
- exports.simulateSwap = simulateSwap;
248
- /**
249
- * Calculate the expected output for a given input amount
250
- * This is a convenience wrapper around simulateSwap
251
- */
252
- function getSwapQuote(marketState, amountIn, direction) {
253
- return simulateSwap(marketState, {
254
- direction,
255
- amountIn,
256
- syExchangeRate: marketState.currentSyExchangeRate,
257
- isCurrentFlashSwap: false,
258
- });
259
- }
260
- exports.getSwapQuote = getSwapQuote;
261
- //# sourceMappingURL=swap-v2.js.map