@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/swapV2.ts ADDED
@@ -0,0 +1,999 @@
1
+ /**
2
+ * CLMM Swap simulation V2
3
+ * Closely mirrors the Rust on-chain implementation from swap.rs
4
+ * Uses TicksWrapper to simulate RB-tree behavior
5
+ */
6
+ import {
7
+ MarketThreeState,
8
+ SwapArgs,
9
+ SwapDirection,
10
+ SwapExactOutArgs,
11
+ SwapOutcomeV2,
12
+ } from "./types"
13
+ import {
14
+ EffSnap,
15
+ TicksWrapper,
16
+ calculateFeeRate,
17
+ getFeeFromAmount,
18
+ normalizedTimeRemaining,
19
+ } from "./utilsV2"
20
+
21
+ const BASE_POINTS = 10000
22
+
23
+ /**
24
+ * Simulate a swap on the CLMM market (V2 - mirrors Rust closely)
25
+ * This is a pure function that does not mutate the market state
26
+ */
27
+ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): SwapOutcomeV2 {
28
+ const DEBUG = false // Set to true for debugging
29
+
30
+ const { financials, configurationOptions, ticks } = marketState
31
+ const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
32
+
33
+ // 0) Snapshot of effective price parameters
34
+ const snapshot = new EffSnap(normalizedTimeRemaining(secondsRemaining), args.syExchangeRate)
35
+
36
+ // Wrap ticks for RB-tree-like operations
37
+ const ticksWrapper = new TicksWrapper(ticks)
38
+
39
+ // Current state in both spot and anchor coordinates
40
+ let currentPriceSpot = ticksWrapper.currentSpotPrice
41
+ let currentLeftBoundaryKey = ticksWrapper.currentTickKey
42
+
43
+ // Active liquidity at current interval (prefix-sum at left boundary)
44
+ let activeLiquidityU64 = ticksWrapper.currentPrefixSum
45
+ let activeLiquidityF64 = Number(activeLiquidityU64)
46
+
47
+ // Fees
48
+ const lpFeeRate = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
49
+ const protocolFeeBps = configurationOptions.treasuryFeeBps
50
+ const eps = configurationOptions.epsilonClamp
51
+
52
+ // Limits (optional spot)
53
+ if (args.priceSpotLimit !== undefined) {
54
+ if (args.direction === SwapDirection.PtToSy) {
55
+ if (args.priceSpotLimit < currentPriceSpot) {
56
+ throw new Error("Price limit violated: limit must be >= current price for PtToSy")
57
+ }
58
+ } else {
59
+ if (args.priceSpotLimit > currentPriceSpot) {
60
+ throw new Error("Price limit violated: limit must be <= current price for SyToPt")
61
+ }
62
+ }
63
+ }
64
+
65
+ // Accumulators
66
+ let amountOutNetU64 = 0
67
+ let feeLpOutU64 = 0
68
+ let feeProtocolOutU64 = 0
69
+ let amountInLeft = args.amountIn
70
+
71
+ if (DEBUG) {
72
+ console.log(`\nSwapV2 Debug: direction=${args.direction}, amountIn=${args.amountIn}`)
73
+ console.log(
74
+ `Initial: currentTickKey=${currentLeftBoundaryKey}, spotPrice=${currentPriceSpot}, activeLiq=${activeLiquidityU64}`,
75
+ )
76
+ }
77
+
78
+ // Main loop across contiguous intervals while we have input left
79
+ // Rust: while amount_in_left > 1
80
+ let iterations = 0
81
+ const MAX_ITERATIONS = 30
82
+
83
+ while (amountInLeft > 1 && iterations < MAX_ITERATIONS) {
84
+ iterations++
85
+
86
+ if (DEBUG) {
87
+ console.log(`\n--- Iteration ${iterations}, amountInLeft=${amountInLeft} ---`)
88
+ }
89
+
90
+ // Right boundary of current interval (must exist to have a half-open [left, right))
91
+ const rightBoundaryKeyOpt = ticksWrapper.successorKey(currentLeftBoundaryKey)
92
+
93
+ if (DEBUG) {
94
+ console.log(`rightBoundaryKey=${rightBoundaryKeyOpt}`)
95
+ }
96
+
97
+ // Handle case when no right boundary exists
98
+ if (rightBoundaryKeyOpt === null) {
99
+ if (args.direction === SwapDirection.SyToPt) {
100
+ // Cross boundary to the left (no right boundary means we need to go left)
101
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
102
+ currentLeftBoundaryKey,
103
+ currentPriceSpot,
104
+ activeLiquidityU64,
105
+ activeLiquidityF64,
106
+ })
107
+ if (!crossed) break
108
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
109
+ currentPriceSpot = crossed.currentPriceSpot
110
+ activeLiquidityU64 = crossed.activeLiquidityU64
111
+ activeLiquidityF64 = crossed.activeLiquidityF64
112
+ continue
113
+ } else {
114
+ break
115
+ }
116
+ }
117
+
118
+ const rightBoundaryKey = rightBoundaryKeyOpt
119
+
120
+ // Interval anchors (spot prices at boundaries)
121
+ const anchorULeft = ticksWrapper.getSpotPrice(currentLeftBoundaryKey)
122
+ const anchorURight = ticksWrapper.getSpotPrice(rightBoundaryKey)
123
+
124
+ // Effective price at current spot
125
+ const cEffOld = snapshot.getEffectivePrice(currentPriceSpot)
126
+
127
+ // Load principal ledgers for the interval (stored on the left boundary)
128
+ const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentLeftBoundaryKey)
129
+
130
+ // Y_max = L * (C(u_old) - C(u_right))
131
+ const cEffAtBoundary = snapshot.getEffectivePrice(anchorURight)
132
+ const yMaxToBoundaryF = activeLiquidityF64 * (cEffOld - cEffAtBoundary)
133
+
134
+ // PT_max_left = L * (u_old - u_left)
135
+ const duToLeft = currentPriceSpot - anchorULeft
136
+ const ptMaxToLeftF = activeLiquidityF64 * duToLeft
137
+
138
+ // Validate y_max and pt_max values before using them
139
+ if (
140
+ !Number.isFinite(yMaxToBoundaryF) ||
141
+ !Number.isFinite(ptMaxToLeftF) ||
142
+ yMaxToBoundaryF < 0 ||
143
+ ptMaxToLeftF < 0
144
+ ) {
145
+ const crossed = crossOneBoundary(
146
+ ticksWrapper,
147
+ args.direction,
148
+ args.direction === SwapDirection.PtToSy ? rightBoundaryKey : currentLeftBoundaryKey,
149
+ {
150
+ currentLeftBoundaryKey,
151
+ currentPriceSpot,
152
+ activeLiquidityU64,
153
+ activeLiquidityF64,
154
+ },
155
+ )
156
+ if (!crossed) break
157
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
158
+ currentPriceSpot = crossed.currentPriceSpot
159
+ activeLiquidityU64 = crossed.activeLiquidityU64
160
+ activeLiquidityF64 = crossed.activeLiquidityF64
161
+ continue
162
+ }
163
+
164
+ const lTradeF64 = computeKappaAndLTrade(
165
+ args.direction,
166
+ Number(principalPt),
167
+ Number(principalSy),
168
+ ptMaxToLeftF,
169
+ yMaxToBoundaryF,
170
+ activeLiquidityF64,
171
+ )
172
+
173
+ if (DEBUG) {
174
+ console.log(` anchorULeft=${anchorULeft}, anchorURight=${anchorURight}`)
175
+ console.log(` principalPt=${principalPt}, principalSy=${principalSy}`)
176
+ console.log(` lTradeF64=${lTradeF64}`)
177
+ }
178
+
179
+ if (lTradeF64 === null || !Number.isFinite(lTradeF64) || lTradeF64 <= eps) {
180
+ const crossed = crossOneBoundary(
181
+ ticksWrapper,
182
+ args.direction,
183
+ args.direction === SwapDirection.PtToSy ? rightBoundaryKey : currentLeftBoundaryKey,
184
+ {
185
+ currentLeftBoundaryKey,
186
+ currentPriceSpot,
187
+ activeLiquidityU64,
188
+ activeLiquidityF64,
189
+ },
190
+ )
191
+ if (!crossed) break
192
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
193
+ currentPriceSpot = crossed.currentPriceSpot
194
+ activeLiquidityU64 = crossed.activeLiquidityU64
195
+ activeLiquidityF64 = crossed.activeLiquidityF64
196
+ continue
197
+ }
198
+
199
+ if (args.direction === SwapDirection.PtToSy) {
200
+ // PT -> SY swap
201
+
202
+ // Final du in this interval
203
+ const duByInput = amountInLeft / lTradeF64
204
+ const duToBoundary = anchorURight - currentPriceSpot
205
+ const duActual = Math.min(duByInput, duToBoundary)
206
+
207
+ if (DEBUG) {
208
+ console.log(` PtToSy: duByInput=${duByInput}, duToBoundary=${duToBoundary}, duActual=${duActual}`)
209
+ console.log(` lTradeF64=${lTradeF64}`)
210
+ }
211
+
212
+ // Check if we need to cross boundary first (at boundary)
213
+ if (duToBoundary <= eps) {
214
+ // Cross boundary to unlock the next interval
215
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.PtToSy, rightBoundaryKey, {
216
+ currentLeftBoundaryKey,
217
+ currentPriceSpot,
218
+ activeLiquidityU64,
219
+ activeLiquidityF64,
220
+ })
221
+ if (!crossed) break
222
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
223
+ currentPriceSpot = crossed.currentPriceSpot
224
+ activeLiquidityU64 = crossed.activeLiquidityU64
225
+ activeLiquidityF64 = crossed.activeLiquidityF64
226
+ continue
227
+ }
228
+
229
+ // Token flows for this segment (ceil for input to protect pool)
230
+ const ptInSegment = Math.ceil(lTradeF64 * duActual)
231
+ const anchorUNew = currentPriceSpot + duActual
232
+ const cEffNew = snapshot.getEffectivePrice(anchorUNew)
233
+ const syOutGrossF = lTradeF64 * (cEffOld - cEffNew)
234
+
235
+ // Clamp by SY principal
236
+ let syOutGrossU64 = Math.floor(syOutGrossF)
237
+ syOutGrossU64 = Math.min(syOutGrossU64, Number(principalSy))
238
+
239
+ if (DEBUG) {
240
+ console.log(` ptInSegment=${ptInSegment}, syOutGrossU64=${syOutGrossU64}`)
241
+ }
242
+
243
+ // Fees in token_out (SY)
244
+ // For flash swaps, fee is based on YT value = (pt - sy × sy_exchange_rate) / sy_exchange_rate (in SY terms)
245
+ if (syOutGrossU64 > 0) {
246
+ let totalFeeOut: number
247
+ if (args.isCurrentFlashSwap) {
248
+ const syOutBase = syOutGrossU64 * args.syExchangeRate
249
+ const ytValueBase = ptInSegment - syOutBase
250
+ const ytValueSy = ytValueBase / args.syExchangeRate
251
+ totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValueSy)), lpFeeRate)
252
+ } else {
253
+ totalFeeOut = getFeeFromAmount(syOutGrossU64, lpFeeRate)
254
+ }
255
+ const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
256
+ const lpFeeOut = totalFeeOut - protocolFeeOut
257
+ const syOutNet = syOutGrossU64 - totalFeeOut
258
+
259
+ // Accumulate to user
260
+ amountOutNetU64 += syOutNet
261
+ feeLpOutU64 += lpFeeOut
262
+ feeProtocolOutU64 += protocolFeeOut
263
+
264
+ // Mirror on-chain principal mutation for this interval:
265
+ // +PT input (gross), -SY output (gross)
266
+ ticksWrapper.setPrincipals(
267
+ currentLeftBoundaryKey,
268
+ principalPt + BigInt(ptInSegment),
269
+ principalSy - BigInt(syOutGrossU64),
270
+ )
271
+
272
+ // Consume input and advance state
273
+ amountInLeft -= ptInSegment
274
+ currentPriceSpot = anchorUNew
275
+ }
276
+
277
+ // If we hit boundary, cross
278
+ if (anchorURight - currentPriceSpot <= eps && amountInLeft > 1) {
279
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.PtToSy, rightBoundaryKey, {
280
+ currentLeftBoundaryKey,
281
+ currentPriceSpot,
282
+ activeLiquidityU64,
283
+ activeLiquidityF64,
284
+ })
285
+ if (!crossed) break
286
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
287
+ currentPriceSpot = crossed.currentPriceSpot
288
+ activeLiquidityU64 = crossed.activeLiquidityU64
289
+ activeLiquidityF64 = crossed.activeLiquidityF64
290
+ }
291
+ } else {
292
+ // SY -> PT swap
293
+ const cEffLeft = snapshot.getEffectivePrice(anchorULeft)
294
+
295
+ // Final ΔC in this interval
296
+ const deltaCByInput = amountInLeft / lTradeF64
297
+ const deltaCToLeftBoundary = cEffLeft - cEffOld
298
+ const deltaCActual = Math.min(deltaCByInput, deltaCToLeftBoundary)
299
+
300
+ if (DEBUG) {
301
+ console.log(
302
+ ` SyToPt: deltaCByInput=${deltaCByInput}, deltaCToLeftBoundary=${deltaCToLeftBoundary}, deltaCActual=${deltaCActual}`,
303
+ )
304
+ console.log(` lTradeF64=${lTradeF64}, eps=${eps}`)
305
+ }
306
+
307
+ // Check if we need to cross boundary first (at boundary)
308
+ if (deltaCToLeftBoundary <= eps) {
309
+ // Cross boundary to the left
310
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
311
+ currentLeftBoundaryKey,
312
+ currentPriceSpot,
313
+ activeLiquidityU64,
314
+ activeLiquidityF64,
315
+ })
316
+ if (!crossed) break
317
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
318
+ currentPriceSpot = crossed.currentPriceSpot
319
+ activeLiquidityU64 = crossed.activeLiquidityU64
320
+ activeLiquidityF64 = crossed.activeLiquidityF64
321
+ continue
322
+ }
323
+
324
+ // New effective price and spot after consuming ΔC
325
+ const cEffNew = cEffOld + deltaCActual
326
+ const spotPriceNew = snapshot.spotPriceFromEffectivePrice(cEffNew)
327
+
328
+ // Token flows
329
+ const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
330
+ const duAbs = currentPriceSpot - spotPriceNew
331
+ const ptOutGrossF = lTradeF64 * duAbs
332
+
333
+ // Clamp gross PT by available principal
334
+ let ptOutGrossU64 = Math.floor(ptOutGrossF)
335
+ ptOutGrossU64 = Math.min(ptOutGrossU64, Number(principalPt))
336
+ // ceil for input to protect pool
337
+ const syInSegmentU64 = Math.ceil(syInSegmentF)
338
+
339
+ if (DEBUG) {
340
+ console.log(` spotPriceNew=${spotPriceNew}, duAbs=${duAbs}`)
341
+ console.log(` ptOutGrossU64=${ptOutGrossU64}, syInSegmentU64=${syInSegmentU64}`)
342
+ }
343
+
344
+ // Match on-chain behavior: if no PT can be taken from this interval, cross and continue.
345
+ if (ptOutGrossU64 === 0) {
346
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
347
+ currentLeftBoundaryKey,
348
+ currentPriceSpot,
349
+ activeLiquidityU64,
350
+ activeLiquidityF64,
351
+ })
352
+ if (!crossed) break
353
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
354
+ currentPriceSpot = crossed.currentPriceSpot
355
+ activeLiquidityU64 = crossed.activeLiquidityU64
356
+ activeLiquidityF64 = crossed.activeLiquidityF64
357
+ continue
358
+ }
359
+
360
+ if (ptOutGrossU64 > 0) {
361
+ // Fees in token_out (PT)
362
+ // For flash swaps, fee is based on YT value = pt - sy × sy_exchange_rate
363
+ let totalFeeOut: number
364
+ if (args.isCurrentFlashSwap) {
365
+ const syInBase = syInSegmentU64 * args.syExchangeRate
366
+ const ytValue = ptOutGrossU64 - syInBase
367
+ totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
368
+ } else {
369
+ totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
370
+ }
371
+ const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
372
+ const lpFeeOut = totalFeeOut - protocolFeeOut
373
+ const ptOutNet = ptOutGrossU64 - totalFeeOut
374
+
375
+ // Accumulate to user
376
+ amountOutNetU64 += ptOutNet
377
+ feeLpOutU64 += lpFeeOut
378
+ feeProtocolOutU64 += protocolFeeOut
379
+
380
+ // Mirror on-chain principal mutation for this interval:
381
+ // +SY input (gross), -PT output (gross)
382
+ ticksWrapper.setPrincipals(
383
+ currentLeftBoundaryKey,
384
+ principalPt - BigInt(ptOutGrossU64),
385
+ principalSy + BigInt(syInSegmentU64),
386
+ )
387
+ }
388
+
389
+ // Consume input and advance state
390
+ amountInLeft -= syInSegmentU64
391
+ currentPriceSpot = spotPriceNew
392
+
393
+ // If we hit boundary, cross
394
+ // Use effective price difference for consistency with pre-swap epsilon check
395
+ if (Math.abs(cEffNew - cEffLeft) <= eps && amountInLeft > 1) {
396
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
397
+ currentLeftBoundaryKey,
398
+ currentPriceSpot,
399
+ activeLiquidityU64,
400
+ activeLiquidityF64,
401
+ })
402
+ if (!crossed) break
403
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
404
+ currentPriceSpot = crossed.currentPriceSpot
405
+ activeLiquidityU64 = crossed.activeLiquidityU64
406
+ activeLiquidityF64 = crossed.activeLiquidityF64
407
+ }
408
+ }
409
+ }
410
+
411
+ const postMarketState: MarketThreeState = {
412
+ ...marketState,
413
+ ticks: ticksWrapper.toTicks(currentLeftBoundaryKey, currentPriceSpot, activeLiquidityU64),
414
+ }
415
+
416
+ return {
417
+ amountInConsumed: args.amountIn - amountInLeft,
418
+ amountOut: amountOutNetU64,
419
+ lpFeeChargedOutToken: feeLpOutU64,
420
+ protocolFeeChargedOutToken: feeProtocolOutU64,
421
+ finalSpotPrice: currentPriceSpot,
422
+ finalTickKey: currentLeftBoundaryKey, // This is now the key, not array index
423
+ postMarketState,
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Simulate a swap exact-out on the CLMM market.
429
+ * Mirrors on-chain swap_exact_out (currently only SyToPt is supported).
430
+ */
431
+ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapExactOutArgs): SwapOutcomeV2 {
432
+ if (args.direction !== SwapDirection.SyToPt) {
433
+ throw new Error("Exact-out simulation only supports SyToPt")
434
+ }
435
+
436
+ const { financials, configurationOptions, ticks } = marketState
437
+ const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
438
+ const snapshot = new EffSnap(normalizedTimeRemaining(secondsRemaining), args.syExchangeRate)
439
+
440
+ const ticksWrapper = new TicksWrapper(ticks)
441
+ let currentPriceSpot = ticksWrapper.currentSpotPrice
442
+ let currentLeftBoundaryKey = ticksWrapper.currentTickKey
443
+ let activeLiquidityU64 = ticksWrapper.currentPrefixSum
444
+ let activeLiquidityF64 = Number(activeLiquidityU64)
445
+
446
+ const lpFeeRate = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
447
+ const protocolFeeBps = configurationOptions.treasuryFeeBps
448
+ const eps = configurationOptions.epsilonClamp
449
+
450
+ if (args.priceSpotLimit !== undefined && args.priceSpotLimit > currentPriceSpot) {
451
+ throw new Error("Price limit violated: limit must be <= current price for SyToPt exact-out")
452
+ }
453
+
454
+ const amountOutTarget = Math.max(0, Math.floor(args.amountOut))
455
+ const amountInConstraint =
456
+ args.amountInConstraint !== undefined ? Math.max(0, Math.floor(args.amountInConstraint)) : undefined
457
+
458
+ if (amountOutTarget === 0) {
459
+ return {
460
+ amountInConsumed: 0,
461
+ amountOut: 0,
462
+ lpFeeChargedOutToken: 0,
463
+ protocolFeeChargedOutToken: 0,
464
+ finalSpotPrice: currentPriceSpot,
465
+ finalTickKey: currentLeftBoundaryKey,
466
+ postMarketState: marketState,
467
+ }
468
+ }
469
+
470
+ let amountOutLeft = amountOutTarget
471
+ let amountOutAccum = 0
472
+ let amountInConsumed = 0
473
+ let feeLpOutU64 = 0
474
+ let feeProtocolOutU64 = 0
475
+
476
+ let iterations = 0
477
+ const MAX_ITERATIONS = 1000
478
+
479
+ while (amountOutLeft > 2 && iterations < MAX_ITERATIONS) {
480
+ iterations++
481
+
482
+ const rightBoundaryKeyOpt = ticksWrapper.successorKey(currentLeftBoundaryKey)
483
+ const rightBoundaryKey = rightBoundaryKeyOpt
484
+
485
+ if (rightBoundaryKey === null) {
486
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
487
+ currentLeftBoundaryKey,
488
+ currentPriceSpot,
489
+ activeLiquidityU64,
490
+ activeLiquidityF64,
491
+ })
492
+ if (!crossed) break
493
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
494
+ currentPriceSpot = crossed.currentPriceSpot
495
+ activeLiquidityU64 = crossed.activeLiquidityU64
496
+ activeLiquidityF64 = crossed.activeLiquidityF64
497
+ continue
498
+ }
499
+
500
+ const anchorULeft = ticksWrapper.getSpotPrice(currentLeftBoundaryKey)
501
+ const anchorURight = ticksWrapper.getSpotPrice(rightBoundaryKey)
502
+ const cEffOld = snapshot.getEffectivePrice(currentPriceSpot)
503
+ const cEffLeft = snapshot.getEffectivePrice(anchorULeft)
504
+ const deltaCToLeftBoundary = cEffLeft - cEffOld
505
+
506
+ if (!Number.isFinite(deltaCToLeftBoundary) || deltaCToLeftBoundary <= eps) {
507
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
508
+ currentLeftBoundaryKey,
509
+ currentPriceSpot,
510
+ activeLiquidityU64,
511
+ activeLiquidityF64,
512
+ })
513
+ if (!crossed) break
514
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
515
+ currentPriceSpot = crossed.currentPriceSpot
516
+ activeLiquidityU64 = crossed.activeLiquidityU64
517
+ activeLiquidityF64 = crossed.activeLiquidityF64
518
+ continue
519
+ }
520
+
521
+ const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentLeftBoundaryKey)
522
+
523
+ const duToLeft = currentPriceSpot - anchorULeft
524
+ if (!Number.isFinite(duToLeft) || deltaCToLeftBoundary <= eps) {
525
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
526
+ currentLeftBoundaryKey,
527
+ currentPriceSpot,
528
+ activeLiquidityU64,
529
+ activeLiquidityF64,
530
+ })
531
+ if (!crossed) break
532
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
533
+ currentPriceSpot = crossed.currentPriceSpot
534
+ activeLiquidityU64 = crossed.activeLiquidityU64
535
+ activeLiquidityF64 = crossed.activeLiquidityF64
536
+ continue
537
+ }
538
+
539
+ const ptMaxToLeftF = activeLiquidityF64 * duToLeft
540
+ const cEffRight = snapshot.getEffectivePrice(anchorURight)
541
+ const deltaCToRightBoundary = cEffOld - cEffRight
542
+ const yMaxToBoundaryF = activeLiquidityF64 * deltaCToRightBoundary
543
+
544
+ if (
545
+ !Number.isFinite(yMaxToBoundaryF) ||
546
+ !Number.isFinite(ptMaxToLeftF) ||
547
+ yMaxToBoundaryF < 0 ||
548
+ ptMaxToLeftF < 0
549
+ ) {
550
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
551
+ currentLeftBoundaryKey,
552
+ currentPriceSpot,
553
+ activeLiquidityU64,
554
+ activeLiquidityF64,
555
+ })
556
+ if (!crossed) break
557
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
558
+ currentPriceSpot = crossed.currentPriceSpot
559
+ activeLiquidityU64 = crossed.activeLiquidityU64
560
+ activeLiquidityF64 = crossed.activeLiquidityF64
561
+ continue
562
+ }
563
+
564
+ const lTradeF64 = computeKappaAndLTrade(
565
+ SwapDirection.SyToPt,
566
+ Number(principalPt),
567
+ Number(principalSy),
568
+ ptMaxToLeftF,
569
+ yMaxToBoundaryF,
570
+ activeLiquidityF64,
571
+ )
572
+
573
+ if (lTradeF64 === null) {
574
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
575
+ currentLeftBoundaryKey,
576
+ currentPriceSpot,
577
+ activeLiquidityU64,
578
+ activeLiquidityF64,
579
+ })
580
+ if (!crossed) break
581
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
582
+ currentPriceSpot = crossed.currentPriceSpot
583
+ activeLiquidityU64 = crossed.activeLiquidityU64
584
+ activeLiquidityF64 = crossed.activeLiquidityF64
585
+ continue
586
+ }
587
+
588
+ let ptOutGrossBoundaryU64 = Math.floor(lTradeF64 * duToLeft)
589
+ ptOutGrossBoundaryU64 = Math.min(ptOutGrossBoundaryU64, Number(principalPt))
590
+
591
+ if (ptOutGrossBoundaryU64 === 0) {
592
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
593
+ currentLeftBoundaryKey,
594
+ currentPriceSpot,
595
+ activeLiquidityU64,
596
+ activeLiquidityF64,
597
+ })
598
+ if (!crossed) break
599
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
600
+ currentPriceSpot = crossed.currentPriceSpot
601
+ activeLiquidityU64 = crossed.activeLiquidityU64
602
+ activeLiquidityF64 = crossed.activeLiquidityF64
603
+ continue
604
+ }
605
+
606
+ const totalFeeOutBoundary = getFeeFromAmount(ptOutGrossBoundaryU64, lpFeeRate)
607
+ const ptOutNetBoundary = Math.max(0, ptOutGrossBoundaryU64 - totalFeeOutBoundary)
608
+
609
+ if (ptOutNetBoundary === 0) {
610
+ const boundarySegment = calculateSwapSegment(
611
+ ptOutGrossBoundaryU64,
612
+ lTradeF64,
613
+ currentPriceSpot,
614
+ duToLeft,
615
+ anchorULeft,
616
+ snapshot,
617
+ cEffOld,
618
+ lpFeeRate,
619
+ args.isCurrentFlashSwap,
620
+ )
621
+
622
+ let syInBoundary = boundarySegment.syInSegmentU64
623
+ if (ptOutGrossBoundaryU64 > 0 && syInBoundary === 0) {
624
+ syInBoundary = 1
625
+ }
626
+
627
+ if (
628
+ amountInConstraint !== undefined &&
629
+ amountInConsumed + syInBoundary > amountInConstraint
630
+ ) {
631
+ throw new Error("Insufficient SY budget for exact-out trade")
632
+ }
633
+
634
+ amountInConsumed += syInBoundary
635
+
636
+ if (boundarySegment.totalFeeOut > 0) {
637
+ const protocolFeeOut = Math.floor((boundarySegment.totalFeeOut * protocolFeeBps) / BASE_POINTS)
638
+ const lpFeeOut = boundarySegment.totalFeeOut - protocolFeeOut
639
+ feeLpOutU64 += lpFeeOut
640
+ feeProtocolOutU64 += protocolFeeOut
641
+ }
642
+
643
+ if (syInBoundary > 0 || boundarySegment.ptOutGrossActual > 0) {
644
+ ticksWrapper.setPrincipals(
645
+ currentLeftBoundaryKey,
646
+ principalPt - BigInt(boundarySegment.ptOutGrossActual),
647
+ principalSy + BigInt(syInBoundary),
648
+ )
649
+ }
650
+
651
+ currentPriceSpot = boundarySegment.spotPriceNew
652
+
653
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
654
+ currentLeftBoundaryKey,
655
+ currentPriceSpot,
656
+ activeLiquidityU64,
657
+ activeLiquidityF64,
658
+ })
659
+ if (!crossed) break
660
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
661
+ currentPriceSpot = crossed.currentPriceSpot
662
+ activeLiquidityU64 = crossed.activeLiquidityU64
663
+ activeLiquidityF64 = crossed.activeLiquidityF64
664
+ continue
665
+ }
666
+
667
+ const targetNet = Math.min(amountOutLeft, ptOutNetBoundary)
668
+ const grossCandidate = findGrossForTargetNet(targetNet, 0, ptOutGrossBoundaryU64, (gross) =>
669
+ calculateNetPtOut(
670
+ gross,
671
+ lTradeF64,
672
+ currentPriceSpot,
673
+ duToLeft,
674
+ anchorULeft,
675
+ lpFeeRate,
676
+ args.isCurrentFlashSwap,
677
+ snapshot,
678
+ cEffOld,
679
+ ),
680
+ )
681
+
682
+ const segment = calculateSwapSegment(
683
+ grossCandidate,
684
+ lTradeF64,
685
+ currentPriceSpot,
686
+ duToLeft,
687
+ anchorULeft,
688
+ snapshot,
689
+ cEffOld,
690
+ lpFeeRate,
691
+ args.isCurrentFlashSwap,
692
+ )
693
+
694
+ let syInSegmentU64 = segment.syInSegmentU64
695
+ if (grossCandidate > 0 && syInSegmentU64 === 0) {
696
+ syInSegmentU64 = 1
697
+ }
698
+
699
+ if (
700
+ amountInConstraint !== undefined &&
701
+ amountInConsumed + syInSegmentU64 > amountInConstraint
702
+ ) {
703
+ throw new Error("Insufficient SY budget for exact-out trade")
704
+ }
705
+
706
+ const netActual = segment.ptOutGrossActual - segment.totalFeeOut
707
+ if (netActual < 0) {
708
+ throw new Error("Exact-out net underflow")
709
+ }
710
+ if (netActual > amountOutLeft) {
711
+ throw new Error("Exact-out net exceeded remaining target")
712
+ }
713
+ amountOutLeft -= netActual
714
+ amountOutAccum += netActual
715
+ amountInConsumed += syInSegmentU64
716
+
717
+ const protocolFeeOut = Math.floor((segment.totalFeeOut * protocolFeeBps) / BASE_POINTS)
718
+ const lpFeeOut = segment.totalFeeOut - protocolFeeOut
719
+ feeLpOutU64 += lpFeeOut
720
+ feeProtocolOutU64 += protocolFeeOut
721
+
722
+ ticksWrapper.setPrincipals(
723
+ currentLeftBoundaryKey,
724
+ principalPt - BigInt(segment.ptOutGrossActual),
725
+ principalSy + BigInt(syInSegmentU64),
726
+ )
727
+
728
+ currentPriceSpot = segment.spotPriceNew
729
+
730
+ const cEffNew = snapshot.getEffectivePrice(currentPriceSpot)
731
+ if (
732
+ amountOutLeft > 2 &&
733
+ (Math.abs(cEffNew - cEffLeft) <= eps || segment.ptOutGrossActual === ptOutGrossBoundaryU64)
734
+ ) {
735
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
736
+ currentLeftBoundaryKey,
737
+ currentPriceSpot,
738
+ activeLiquidityU64,
739
+ activeLiquidityF64,
740
+ })
741
+ if (!crossed) break
742
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
743
+ currentPriceSpot = crossed.currentPriceSpot
744
+ activeLiquidityU64 = crossed.activeLiquidityU64
745
+ activeLiquidityF64 = crossed.activeLiquidityF64
746
+ }
747
+ }
748
+
749
+ if (amountOutLeft > 2) {
750
+ throw new Error("Insufficient PT output for exact-out trade")
751
+ }
752
+
753
+ const postMarketState: MarketThreeState = {
754
+ ...marketState,
755
+ ticks: ticksWrapper.toTicks(currentLeftBoundaryKey, currentPriceSpot, activeLiquidityU64),
756
+ }
757
+
758
+ return {
759
+ amountInConsumed,
760
+ amountOut: amountOutAccum,
761
+ lpFeeChargedOutToken: feeLpOutU64,
762
+ protocolFeeChargedOutToken: feeProtocolOutU64,
763
+ finalSpotPrice: currentPriceSpot,
764
+ finalTickKey: currentLeftBoundaryKey,
765
+ postMarketState,
766
+ }
767
+ }
768
+
769
+ function computeKappaAndLTrade(
770
+ direction: SwapDirection,
771
+ principalPtInInterval: number,
772
+ principalSyInInterval: number,
773
+ ptMaxToLeftF: number,
774
+ yMaxToBoundaryF: number,
775
+ activeLiquidityF64: number,
776
+ ): number | null {
777
+ const isPtOnlyRange = yMaxToBoundaryF <= 0 || principalSyInInterval === 0
778
+ const isSyOnlyRange = ptMaxToLeftF <= 0 || principalPtInInterval === 0
779
+
780
+ let kappaSy: number
781
+ let kappaPt: number
782
+
783
+ if (direction === SwapDirection.SyToPt) {
784
+ kappaPt = isSyOnlyRange
785
+ ? 0
786
+ : ptMaxToLeftF > 0
787
+ ? principalPtInInterval / ptMaxToLeftF
788
+ : Infinity
789
+ kappaSy = isPtOnlyRange
790
+ ? 1
791
+ : yMaxToBoundaryF > 0
792
+ ? principalSyInInterval / yMaxToBoundaryF
793
+ : Infinity
794
+ } else {
795
+ kappaSy = isPtOnlyRange
796
+ ? 0
797
+ : yMaxToBoundaryF > 0
798
+ ? principalSyInInterval / yMaxToBoundaryF
799
+ : Infinity
800
+ kappaPt = isSyOnlyRange
801
+ ? 1
802
+ : ptMaxToLeftF > 0
803
+ ? principalPtInInterval / ptMaxToLeftF
804
+ : Infinity
805
+ }
806
+
807
+ const kappa = Math.min(kappaPt, kappaSy, 1)
808
+ if (!Number.isFinite(kappa) || kappa <= 0) {
809
+ return null
810
+ }
811
+
812
+ const lTradeF64 = activeLiquidityF64 * kappa
813
+ if (!Number.isFinite(lTradeF64) || lTradeF64 <= 0) {
814
+ return null
815
+ }
816
+
817
+ return lTradeF64
818
+ }
819
+
820
+ function findGrossForTargetNet(
821
+ targetNet: number,
822
+ minGross: number,
823
+ maxGross: number,
824
+ calculateNet: (gross: number) => number,
825
+ ): number {
826
+ if (minGross >= maxGross) {
827
+ return minGross
828
+ }
829
+
830
+ let left = minGross
831
+ let right = maxGross
832
+ let bestGross = minGross
833
+
834
+ while (left <= right) {
835
+ const mid = left + Math.floor((right - left) / 2)
836
+ const net = calculateNet(mid)
837
+
838
+ if (net === targetNet) {
839
+ return mid
840
+ }
841
+
842
+ if (net < targetNet) {
843
+ bestGross = mid
844
+ left = mid + 1
845
+ } else {
846
+ if (mid === 0) {
847
+ break
848
+ }
849
+ right = mid - 1
850
+ }
851
+ }
852
+
853
+ return bestGross
854
+ }
855
+
856
+ function calculateNetPtOut(
857
+ grossCandidate: number,
858
+ lTradeF64: number,
859
+ currentPriceSpot: number,
860
+ duToLeft: number,
861
+ anchorULeft: number,
862
+ lpFeeRate: number,
863
+ isCurrentFlashSwap: boolean,
864
+ snapshot: EffSnap,
865
+ cEffOld: number,
866
+ ): number {
867
+ const duCandidate = grossCandidate / lTradeF64
868
+ const duClamped = Math.min(duCandidate, duToLeft)
869
+ let spotPriceNew = currentPriceSpot - duClamped
870
+ if (spotPriceNew < anchorULeft) {
871
+ spotPriceNew = anchorULeft
872
+ }
873
+
874
+ const ptOutGrossActual = Math.max(
875
+ 0,
876
+ Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
877
+ )
878
+
879
+ const totalFeeOut = isCurrentFlashSwap
880
+ ? (() => {
881
+ const cEffNew = snapshot.getEffectivePrice(spotPriceNew)
882
+ const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
883
+ const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
884
+ const ytValue = ptOutGrossActual - syInBase
885
+ return getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
886
+ })()
887
+ : getFeeFromAmount(ptOutGrossActual, lpFeeRate)
888
+
889
+ return Math.max(0, ptOutGrossActual - totalFeeOut)
890
+ }
891
+
892
+ function calculateSwapSegment(
893
+ grossCandidate: number,
894
+ lTradeF64: number,
895
+ currentPriceSpot: number,
896
+ duToLeft: number,
897
+ anchorULeft: number,
898
+ snapshot: EffSnap,
899
+ cEffOld: number,
900
+ lpFeeRate: number,
901
+ isCurrentFlashSwap: boolean,
902
+ ): {
903
+ spotPriceNew: number
904
+ syInSegmentU64: number
905
+ ptOutGrossActual: number
906
+ totalFeeOut: number
907
+ } {
908
+ const duCandidate = grossCandidate / lTradeF64
909
+ const duClamped = Math.min(duCandidate, duToLeft)
910
+ let spotPriceNew = currentPriceSpot - duClamped
911
+ if (spotPriceNew < anchorULeft) {
912
+ spotPriceNew = anchorULeft
913
+ }
914
+
915
+ const cEffNew = snapshot.getEffectivePrice(spotPriceNew)
916
+ const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
917
+ const syInSegmentU64 = Math.max(0, Math.ceil(syInSegmentF))
918
+ const ptOutGrossActual = Math.max(
919
+ 0,
920
+ Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
921
+ )
922
+
923
+ const normalFee = getFeeFromAmount(ptOutGrossActual, lpFeeRate)
924
+ const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
925
+ const ytValue = ptOutGrossActual - syInBase
926
+ const flashFee = getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
927
+ const totalFeeOut = isCurrentFlashSwap ? flashFee : normalFee
928
+
929
+ return {
930
+ spotPriceNew,
931
+ syInSegmentU64,
932
+ ptOutGrossActual,
933
+ totalFeeOut,
934
+ }
935
+ }
936
+
937
+ /**
938
+ * State during swap iteration
939
+ */
940
+ interface SwapState {
941
+ currentLeftBoundaryKey: number
942
+ currentPriceSpot: number
943
+ activeLiquidityU64: bigint
944
+ activeLiquidityF64: number
945
+ }
946
+
947
+ /**
948
+ * Cross one boundary and update core state
949
+ * Mirrors Rust's cross_one_boundary_and_update_core
950
+ */
951
+ function crossOneBoundary(
952
+ ticksWrapper: TicksWrapper,
953
+ direction: SwapDirection,
954
+ boundaryKeyToCross: number,
955
+ state: SwapState,
956
+ ): SwapState | null {
957
+ // Get liquidity_net at the boundary we're crossing
958
+ const deltaNetI64 = ticksWrapper.getLiquidityNet(boundaryKeyToCross)
959
+ const lOldI64 = state.activeLiquidityU64
960
+
961
+ // Update active liquidity
962
+ let lNewI64: bigint
963
+ if (direction === SwapDirection.PtToSy) {
964
+ // Crossing upward boundary: add liquidity_net
965
+ lNewI64 = lOldI64 + deltaNetI64
966
+ } else {
967
+ // Crossing downward boundary: subtract liquidity_net
968
+ lNewI64 = lOldI64 - deltaNetI64
969
+ }
970
+
971
+ // Ensure non-negative
972
+ if (lNewI64 < 0n) {
973
+ lNewI64 = 0n
974
+ }
975
+
976
+ // Move spot to the boundary we cross
977
+ const boundarySpot = ticksWrapper.getSpotPrice(boundaryKeyToCross)
978
+
979
+ // Advance current left boundary key
980
+ let newLeftBoundaryKey: number | null
981
+ if (direction === SwapDirection.PtToSy) {
982
+ // Moving right: new left boundary is the crossed boundary
983
+ newLeftBoundaryKey = boundaryKeyToCross
984
+ } else {
985
+ // Moving left: new left boundary is predecessor of current
986
+ newLeftBoundaryKey = ticksWrapper.predecessorKey(state.currentLeftBoundaryKey)
987
+ }
988
+
989
+ if (newLeftBoundaryKey === null) {
990
+ return null // End of range
991
+ }
992
+
993
+ return {
994
+ currentLeftBoundaryKey: newLeftBoundaryKey,
995
+ currentPriceSpot: boundarySpot,
996
+ activeLiquidityU64: lNewI64,
997
+ activeLiquidityF64: Number(lNewI64),
998
+ }
999
+ }