@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.
- package/build/addLiquidity.d.ts +65 -4
- package/build/addLiquidity.js +762 -36
- package/build/addLiquidity.js.map +1 -1
- package/build/bisect.d.ts +11 -0
- package/build/bisect.js +22 -12
- package/build/bisect.js.map +1 -1
- package/build/index.d.ts +5 -4
- package/build/index.js +14 -7
- package/build/index.js.map +1 -1
- package/build/liquidityHistogram.d.ts +6 -1
- package/build/liquidityHistogram.js +57 -12
- package/build/liquidityHistogram.js.map +1 -1
- package/build/quote.d.ts +1 -1
- package/build/quote.js +70 -84
- package/build/quote.js.map +1 -1
- package/build/swap.js +36 -18
- package/build/swap.js.map +1 -1
- package/build/swapV2.d.ts +6 -1
- package/build/swapV2.js +394 -52
- package/build/swapV2.js.map +1 -1
- package/build/types.d.ts +51 -0
- package/build/utils.d.ts +8 -2
- package/build/utils.js +37 -19
- package/build/utils.js.map +1 -1
- package/build/utilsV2.d.ts +9 -0
- package/build/utilsV2.js +131 -9
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.js +12 -7
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.d.ts +7 -0
- package/build/ytTrades.js +166 -146
- package/build/ytTrades.js.map +1 -1
- package/build/ytTradesLegacy.js +3 -4
- package/build/ytTradesLegacy.js.map +1 -1
- package/package.json +2 -2
- package/src/addLiquidity.ts +1012 -38
- package/src/bisect.ts +22 -11
- package/src/index.ts +11 -5
- package/src/liquidityHistogram.ts +54 -9
- package/src/quote.ts +73 -95
- package/src/swap.ts +35 -19
- package/src/swapV2.ts +999 -0
- package/src/types.ts +55 -0
- package/src/utils.ts +24 -3
- package/src/utilsV2.ts +337 -0
- package/src/withdrawLiquidity.ts +12 -6
- package/src/ytTrades.ts +191 -172
- package/src/ytTradesLegacy.ts +419 -0
- package/build/swap-v2.d.ts +0 -20
- package/build/swap-v2.js +0 -261
- package/build/swap-v2.js.map +0 -1
- package/build/swapLegacy.d.ts +0 -16
- package/build/swapLegacy.js +0 -229
- package/build/swapLegacy.js.map +0 -1
- 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
|
+
}
|