@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/addLiquidity.ts
CHANGED
|
@@ -2,10 +2,63 @@
|
|
|
2
2
|
* CLMM Add Liquidity simulation
|
|
3
3
|
* Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { simulateSwap, simulateSwapExactOut } from "./swapV2"
|
|
6
|
+
import {
|
|
7
|
+
AddLiquidityArgs,
|
|
8
|
+
AddLiquidityOutcome,
|
|
9
|
+
CrossingScaleParams,
|
|
10
|
+
CrossingTickState,
|
|
11
|
+
LiquidityNeeds,
|
|
12
|
+
MarketThreeState,
|
|
13
|
+
SwapDirection,
|
|
14
|
+
} from "./types"
|
|
15
|
+
import { EffSnap, normalizedTimeRemaining, TicksWrapper } from "./utilsV2"
|
|
16
|
+
import { findTickByKey, getSuccessorTickKey } from "./utils"
|
|
17
|
+
import { Ticks } from "@exponent-labs/exponent-fetcher"
|
|
7
18
|
|
|
8
19
|
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
20
|
+
// Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
|
|
21
|
+
// quotes match the on-chain classic deposit path as closely as possible.
|
|
22
|
+
const GAP_TOKEN_NEEDS = 5
|
|
23
|
+
const SWAP_EXACT_OUT_SY_HEADROOM = 10
|
|
24
|
+
|
|
25
|
+
const EMPTY_CROSSING_TICK_STATE: CrossingTickState = {
|
|
26
|
+
principalPt: 0,
|
|
27
|
+
principalSy: 0,
|
|
28
|
+
principalShareSupply: 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get crossing tick state and price boundaries from Ticks.
|
|
33
|
+
* Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
|
|
34
|
+
*/
|
|
35
|
+
export function getCrossingTickStateFromTicks(ticks: Ticks): {
|
|
36
|
+
crossingTickState: CrossingTickState
|
|
37
|
+
crossingTickPriceLeft: number
|
|
38
|
+
crossingTickPriceRight: number
|
|
39
|
+
} {
|
|
40
|
+
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1]
|
|
41
|
+
if (!currentTickNode || ticks.currentTick <= 0) {
|
|
42
|
+
return {
|
|
43
|
+
crossingTickState: EMPTY_CROSSING_TICK_STATE,
|
|
44
|
+
crossingTickPriceLeft: 0,
|
|
45
|
+
crossingTickPriceRight: 0,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const crossingTickPriceLeft = currentTickNode.impliedRate
|
|
49
|
+
const successorKey = getSuccessorTickKey(ticks, currentTickNode.apyBasePoints)
|
|
50
|
+
const successorTick = successorKey != null ? findTickByKey(ticks, successorKey)?.tick : null
|
|
51
|
+
const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY
|
|
52
|
+
return {
|
|
53
|
+
crossingTickState: {
|
|
54
|
+
principalPt: Number(currentTickNode.principalPt),
|
|
55
|
+
principalSy: Number(currentTickNode.principalSy),
|
|
56
|
+
principalShareSupply: Number(currentTickNode.principalShareSupply),
|
|
57
|
+
},
|
|
58
|
+
crossingTickPriceLeft,
|
|
59
|
+
crossingTickPriceRight,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
9
62
|
|
|
10
63
|
/**
|
|
11
64
|
* Compute liquidity target and token needs based on position range and budgets
|
|
@@ -41,6 +94,11 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
41
94
|
ptNeeded: 0,
|
|
42
95
|
priceSplitForNeed: lowerPrice,
|
|
43
96
|
priceSplitTickIdx: lowerTickIdx,
|
|
97
|
+
// No crossing possible when below range
|
|
98
|
+
originalMaxSy: maxSy,
|
|
99
|
+
originalMaxPt: maxPt,
|
|
100
|
+
duLeftTotal: 0,
|
|
101
|
+
deltaCRightTotal: deltaCTotal,
|
|
44
102
|
}
|
|
45
103
|
} else if (spotPriceCurrent >= upperPrice) {
|
|
46
104
|
// Above range: PT only (NOT SY!)
|
|
@@ -55,9 +113,14 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
55
113
|
ptNeeded: Math.floor(ptNeed),
|
|
56
114
|
priceSplitForNeed: upperPrice,
|
|
57
115
|
priceSplitTickIdx: upperTickIdx,
|
|
116
|
+
// No crossing possible when above range
|
|
117
|
+
originalMaxSy: maxSy,
|
|
118
|
+
originalMaxPt: maxPt,
|
|
119
|
+
duLeftTotal: duTotal,
|
|
120
|
+
deltaCRightTotal: 0,
|
|
58
121
|
}
|
|
59
122
|
} else {
|
|
60
|
-
// Inside range: both sides
|
|
123
|
+
// Inside range: both sides - crossing tick possible
|
|
61
124
|
const duLeft = Math.max(spotPriceCurrent - lowerPrice, epsilonClamp)
|
|
62
125
|
const liquidityFromPt = maxPt / duLeft
|
|
63
126
|
|
|
@@ -75,8 +138,529 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
75
138
|
ptNeeded: Math.floor(ptNeed),
|
|
76
139
|
priceSplitForNeed: spotPriceCurrent,
|
|
77
140
|
priceSplitTickIdx: currentIndex,
|
|
141
|
+
// Store for crossing tick scaling
|
|
142
|
+
originalMaxSy: maxSy,
|
|
143
|
+
originalMaxPt: maxPt,
|
|
144
|
+
duLeftTotal: duLeft,
|
|
145
|
+
deltaCRightTotal: deltaCRight,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Scale crossing tick token inputs from original max values to prevent double trimming.
|
|
152
|
+
*
|
|
153
|
+
* When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
|
|
154
|
+
* 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
|
|
155
|
+
* 2. Then by AMM formula in simulateAddLiquidityProportional
|
|
156
|
+
*
|
|
157
|
+
* This function calculates the segment's proportion of the total distribution and scales
|
|
158
|
+
* the original max values accordingly, ensuring maximum token utilization.
|
|
159
|
+
*
|
|
160
|
+
* @param scaleParams - Original max values and CLMM distribution extents
|
|
161
|
+
* @param duPtPart - PT segment length (spot price delta for this segment)
|
|
162
|
+
* @param syPerL1 - SY per unit liquidity for this segment
|
|
163
|
+
* @param clmmPtDelta - PT amount calculated from CLMM formula
|
|
164
|
+
* @param clmmSyDelta - SY amount calculated from CLMM formula
|
|
165
|
+
* @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
|
|
166
|
+
*/
|
|
167
|
+
export function scaleCrossingTickInputs(
|
|
168
|
+
scaleParams: CrossingScaleParams,
|
|
169
|
+
duPtPart: number,
|
|
170
|
+
syPerL1: number,
|
|
171
|
+
clmmPtDelta: number,
|
|
172
|
+
clmmSyDelta: number,
|
|
173
|
+
): [number, number] {
|
|
174
|
+
const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams
|
|
175
|
+
|
|
176
|
+
if (duLeftTotal > 0 && deltaCRightTotal > 0) {
|
|
177
|
+
// Calculate segment's proportion of total distribution
|
|
178
|
+
const ptSegmentRatio = duPtPart / duLeftTotal
|
|
179
|
+
const sySegmentRatio = syPerL1 / deltaCRightTotal
|
|
180
|
+
|
|
181
|
+
// Scale original max values by segment proportion
|
|
182
|
+
const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio)
|
|
183
|
+
const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio)
|
|
184
|
+
|
|
185
|
+
// Use max of CLMM-calculated value and scaled original value
|
|
186
|
+
// This ensures we don't lose tokens due to double trimming
|
|
187
|
+
return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fallback to original logic if no crossing scale params
|
|
191
|
+
return [clmmPtDelta, clmmSyDelta]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Simulate the proportional add_liquidity logic
|
|
196
|
+
* Returns [usedPt, usedSy] based on existing tick proportions
|
|
197
|
+
*/
|
|
198
|
+
function simulateAddLiquidityProportional(
|
|
199
|
+
intentPt: number,
|
|
200
|
+
intentSy: number,
|
|
201
|
+
marketTotalPt: number,
|
|
202
|
+
marketTotalSy: number,
|
|
203
|
+
marketTotalLp: number,
|
|
204
|
+
): [number, number] {
|
|
205
|
+
if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
|
|
206
|
+
// Empty tick - use all intended amounts
|
|
207
|
+
return [intentPt, intentSy]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const lpFromPt = (marketTotalLp * intentPt) / marketTotalPt
|
|
211
|
+
const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy
|
|
212
|
+
|
|
213
|
+
if (lpFromPt < lpFromSy) {
|
|
214
|
+
// PT is the limiting factor
|
|
215
|
+
const lpTokensOut = lpFromPt
|
|
216
|
+
const usedPt = intentPt
|
|
217
|
+
const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp)
|
|
218
|
+
return [usedPt, usedSy]
|
|
219
|
+
} else {
|
|
220
|
+
// SY is the limiting factor
|
|
221
|
+
const lpTokensOut = lpFromSy
|
|
222
|
+
const usedSy = intentSy
|
|
223
|
+
const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp)
|
|
224
|
+
return [usedPt, usedSy]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toU64BigInt(value: number): bigint {
|
|
229
|
+
return BigInt(Math.max(0, Math.floor(value)))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ceilDivBigInt(numerator: bigint, denominator: bigint): bigint {
|
|
233
|
+
if (denominator <= 0n) {
|
|
234
|
+
return 0n
|
|
235
|
+
}
|
|
236
|
+
return numerator <= 0n ? 0n : (numerator + denominator - 1n) / denominator
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function floorSqrtBigInt(value: bigint): bigint {
|
|
240
|
+
if (value <= 1n) {
|
|
241
|
+
return value
|
|
242
|
+
}
|
|
243
|
+
let x0 = value
|
|
244
|
+
let x1 = (x0 + 1n) >> 1n
|
|
245
|
+
while (x1 < x0) {
|
|
246
|
+
x0 = x1
|
|
247
|
+
x1 = (x1 + value / x1) >> 1n
|
|
248
|
+
}
|
|
249
|
+
return x0
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type VirtualTickState = {
|
|
253
|
+
principalPt: bigint
|
|
254
|
+
principalSy: bigint
|
|
255
|
+
principalShareSupply: bigint
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getVirtualTickState(
|
|
259
|
+
key: number,
|
|
260
|
+
ticksWrapper: TicksWrapper,
|
|
261
|
+
virtualStates: Map<number, VirtualTickState>,
|
|
262
|
+
): VirtualTickState {
|
|
263
|
+
const cached = virtualStates.get(key)
|
|
264
|
+
if (cached) {
|
|
265
|
+
return cached
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const { principalPt, principalSy } = ticksWrapper.getPrincipals(key)
|
|
269
|
+
const tick = ticksWrapper.getTickByKey(key)
|
|
270
|
+
const state: VirtualTickState = {
|
|
271
|
+
principalPt,
|
|
272
|
+
principalSy,
|
|
273
|
+
principalShareSupply: tick ? BigInt(tick.principalShareSupply) : 0n,
|
|
274
|
+
}
|
|
275
|
+
virtualStates.set(key, state)
|
|
276
|
+
return state
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function simulateMintSharesForTickPrestateCalcUsed(params: {
|
|
280
|
+
key: number
|
|
281
|
+
dptIn: bigint
|
|
282
|
+
dsyIn: bigint
|
|
283
|
+
ticksWrapper: TicksWrapper
|
|
284
|
+
virtualStates: Map<number, VirtualTickState>
|
|
285
|
+
}): { minted: bigint; usedPt: bigint; usedSy: bigint } | null {
|
|
286
|
+
const { key, dptIn, dsyIn, ticksWrapper, virtualStates } = params
|
|
287
|
+
const state = getVirtualTickState(key, ticksWrapper, virtualStates)
|
|
288
|
+
|
|
289
|
+
const ptBefore = state.principalPt
|
|
290
|
+
const syBefore = state.principalSy
|
|
291
|
+
const supply = state.principalShareSupply
|
|
292
|
+
|
|
293
|
+
let minted = 0n
|
|
294
|
+
let usedPt = 0n
|
|
295
|
+
let usedSy = 0n
|
|
296
|
+
|
|
297
|
+
const hasPt = dptIn > 0n
|
|
298
|
+
const hasSy = dsyIn > 0n
|
|
299
|
+
|
|
300
|
+
if (hasPt && hasSy) {
|
|
301
|
+
if (supply === 0n) {
|
|
302
|
+
minted = floorSqrtBigInt(dptIn * dsyIn)
|
|
303
|
+
usedPt = dptIn
|
|
304
|
+
usedSy = dsyIn
|
|
305
|
+
} else {
|
|
306
|
+
if (ptBefore === 0n || syBefore === 0n) {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const lpFromPt = (supply * dptIn) / ptBefore
|
|
311
|
+
const lpFromSy = (supply * dsyIn) / syBefore
|
|
312
|
+
const usePtSide = lpFromPt < lpFromSy
|
|
313
|
+
const lpTokensOut = usePtSide ? lpFromPt : lpFromSy
|
|
314
|
+
|
|
315
|
+
minted = lpTokensOut
|
|
316
|
+
if (minted === 0n) {
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (usePtSide) {
|
|
321
|
+
usedPt = dptIn
|
|
322
|
+
usedSy = ceilDivBigInt(syBefore * lpTokensOut, supply)
|
|
323
|
+
} else {
|
|
324
|
+
usedSy = dsyIn
|
|
325
|
+
usedPt = ceilDivBigInt(ptBefore * lpTokensOut, supply)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else if (hasPt && !hasSy) {
|
|
329
|
+
usedSy = 0n
|
|
330
|
+
if (supply === 0n && ptBefore === 0n) {
|
|
331
|
+
minted = dptIn
|
|
332
|
+
usedPt = dptIn
|
|
333
|
+
} else {
|
|
334
|
+
if (supply === 0n || ptBefore === 0n) {
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
minted = (supply * dptIn) / ptBefore
|
|
338
|
+
if (minted === 0n) {
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
usedPt = dptIn
|
|
342
|
+
}
|
|
343
|
+
} else if (!hasPt && hasSy) {
|
|
344
|
+
usedPt = 0n
|
|
345
|
+
if (supply === 0n && syBefore === 0n) {
|
|
346
|
+
minted = dsyIn
|
|
347
|
+
usedSy = dsyIn
|
|
348
|
+
} else {
|
|
349
|
+
if (supply === 0n || syBefore === 0n) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
minted = (supply * dsyIn) / syBefore
|
|
353
|
+
if (minted === 0n) {
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
usedSy = dsyIn
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
return { minted: 0n, usedPt: 0n, usedSy: 0n }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (minted > 0n) {
|
|
363
|
+
state.principalShareSupply += minted
|
|
364
|
+
}
|
|
365
|
+
if (usedPt > 0n || usedSy > 0n) {
|
|
366
|
+
state.principalPt += usedPt
|
|
367
|
+
state.principalSy += usedSy
|
|
368
|
+
ticksWrapper.setPrincipals(key, state.principalPt, state.principalSy)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { minted, usedPt, usedSy }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function simulateAccruePrincipalForDeposit(params: {
|
|
375
|
+
ticks: Ticks
|
|
376
|
+
snap: EffSnap
|
|
377
|
+
lowerPrice: number
|
|
378
|
+
upperPrice: number
|
|
379
|
+
priceSplitForNeed: number
|
|
380
|
+
splitTickKey: number
|
|
381
|
+
lowerTickKey: number
|
|
382
|
+
upperTickKey: number
|
|
383
|
+
liquidityTarget: number
|
|
384
|
+
originalMaxSy: number
|
|
385
|
+
originalMaxPt: number
|
|
386
|
+
}): { sySpent: number; ptSpent: number } {
|
|
387
|
+
const {
|
|
388
|
+
ticks,
|
|
389
|
+
snap,
|
|
390
|
+
lowerPrice,
|
|
391
|
+
upperPrice,
|
|
392
|
+
priceSplitForNeed,
|
|
393
|
+
splitTickKey,
|
|
394
|
+
lowerTickKey,
|
|
395
|
+
upperTickKey,
|
|
396
|
+
liquidityTarget,
|
|
397
|
+
originalMaxSy,
|
|
398
|
+
originalMaxPt,
|
|
399
|
+
} = params
|
|
400
|
+
|
|
401
|
+
const ticksWrapper = new TicksWrapper(ticks)
|
|
402
|
+
ticksWrapper.upsertBoundaryTick(lowerTickKey, snap)
|
|
403
|
+
ticksWrapper.upsertBoundaryTick(upperTickKey, snap)
|
|
404
|
+
const virtualStates = new Map<number, VirtualTickState>()
|
|
405
|
+
|
|
406
|
+
let totalSySpend = 0n
|
|
407
|
+
let totalPtSpend = 0n
|
|
408
|
+
let pendingCrossPt: { leftKey: number; rightKey: number; ptDelta: bigint; duPart: number } | null = null
|
|
409
|
+
let pendingCrossSy: { leftKey: number; rightKey: number; syDelta: bigint; syPerL: number } | null = null
|
|
410
|
+
|
|
411
|
+
const resolveTraversalStartKey = (candidateKey: number): number => {
|
|
412
|
+
if (ticksWrapper.getTickByKey(candidateKey)) {
|
|
413
|
+
return candidateKey
|
|
414
|
+
}
|
|
415
|
+
const predecessor = ticksWrapper.predecessorKey(candidateKey)
|
|
416
|
+
return predecessor ?? candidateKey
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const visitIntervals = (startKey: number, priceStart: number, priceEnd: number, visitor: (params: {
|
|
420
|
+
leftKey: number
|
|
421
|
+
rightKey: number
|
|
422
|
+
leftPrice: number
|
|
423
|
+
rightPrice: number
|
|
424
|
+
}) => void) => {
|
|
425
|
+
if (!(priceStart < priceEnd)) {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let currentKey = startKey
|
|
430
|
+
while (true) {
|
|
431
|
+
const rightKey = ticksWrapper.successorKey(currentKey)
|
|
432
|
+
if (rightKey == null) {
|
|
433
|
+
break
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const leftPrice = ticksWrapper.getSpotPrice(currentKey)
|
|
437
|
+
const rightPrice = ticksWrapper.getSpotPrice(rightKey)
|
|
438
|
+
visitor({ leftKey: currentKey, rightKey, leftPrice, rightPrice })
|
|
439
|
+
|
|
440
|
+
if (rightPrice >= priceEnd) {
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
currentKey = rightKey
|
|
78
444
|
}
|
|
79
445
|
}
|
|
446
|
+
|
|
447
|
+
const ptSliceStart = lowerPrice
|
|
448
|
+
const ptSliceEnd = Math.min(priceSplitForNeed, upperPrice)
|
|
449
|
+
const ptStartKey = resolveTraversalStartKey(lowerTickKey)
|
|
450
|
+
visitIntervals(ptStartKey, ptSliceStart, ptSliceEnd, ({ leftKey, rightKey, leftPrice, rightPrice }) => {
|
|
451
|
+
const segmentStart = Math.max(leftPrice, ptSliceStart)
|
|
452
|
+
const segmentEnd = Math.min(rightPrice, ptSliceEnd)
|
|
453
|
+
const segmentLength = segmentEnd - segmentStart
|
|
454
|
+
if (!(segmentLength > 0)) {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed
|
|
459
|
+
const principalPtDelta = toU64BigInt(Math.ceil(liquidityTarget * segmentLength))
|
|
460
|
+
if (principalPtDelta <= 0n) {
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (isCrossing) {
|
|
465
|
+
pendingCrossPt = {
|
|
466
|
+
leftKey,
|
|
467
|
+
rightKey,
|
|
468
|
+
ptDelta: principalPtDelta,
|
|
469
|
+
duPart: segmentLength,
|
|
470
|
+
}
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
475
|
+
key: leftKey,
|
|
476
|
+
dptIn: principalPtDelta,
|
|
477
|
+
dsyIn: 0n,
|
|
478
|
+
ticksWrapper,
|
|
479
|
+
virtualStates,
|
|
480
|
+
})
|
|
481
|
+
if (!minted) {
|
|
482
|
+
throw new Error("Deposit too small to mint shares in PT segment")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
totalPtSpend += minted.usedPt
|
|
486
|
+
totalSySpend += minted.usedSy
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const sySliceStart = Math.max(priceSplitForNeed, lowerPrice)
|
|
490
|
+
const sySliceEnd = upperPrice
|
|
491
|
+
const syStartCandidate = splitTickKey >= lowerTickKey ? splitTickKey : lowerTickKey
|
|
492
|
+
const syStartKey = resolveTraversalStartKey(syStartCandidate)
|
|
493
|
+
|
|
494
|
+
visitIntervals(syStartKey, sySliceStart, sySliceEnd, ({ leftKey, rightKey, leftPrice, rightPrice }) => {
|
|
495
|
+
const segmentStart = Math.max(leftPrice, sySliceStart)
|
|
496
|
+
const segmentEnd = Math.min(rightPrice, sySliceEnd)
|
|
497
|
+
if (!(segmentEnd > segmentStart)) {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const syPerL = snap.getEffectivePrice(segmentStart) - snap.getEffectivePrice(segmentEnd)
|
|
502
|
+
const principalSyDelta = toU64BigInt(Math.ceil(liquidityTarget * syPerL))
|
|
503
|
+
if (principalSyDelta <= 0n) {
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed
|
|
508
|
+
if (isCrossing) {
|
|
509
|
+
pendingCrossSy = {
|
|
510
|
+
leftKey,
|
|
511
|
+
rightKey,
|
|
512
|
+
syDelta: principalSyDelta,
|
|
513
|
+
syPerL,
|
|
514
|
+
}
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
519
|
+
key: leftKey,
|
|
520
|
+
dptIn: 0n,
|
|
521
|
+
dsyIn: principalSyDelta,
|
|
522
|
+
ticksWrapper,
|
|
523
|
+
virtualStates,
|
|
524
|
+
})
|
|
525
|
+
if (!minted) {
|
|
526
|
+
throw new Error("Deposit too small to mint shares in SY segment")
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
totalPtSpend += minted.usedPt
|
|
530
|
+
totalSySpend += minted.usedSy
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
if (pendingCrossPt && pendingCrossSy) {
|
|
534
|
+
const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS))
|
|
535
|
+
const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS))
|
|
536
|
+
const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft
|
|
537
|
+
const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft
|
|
538
|
+
|
|
539
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
540
|
+
key: pendingCrossPt.leftKey,
|
|
541
|
+
dptIn: scaledPtIn,
|
|
542
|
+
dsyIn: scaledSyIn,
|
|
543
|
+
ticksWrapper,
|
|
544
|
+
virtualStates,
|
|
545
|
+
})
|
|
546
|
+
if (!minted) {
|
|
547
|
+
throw new Error("Deposit too small to mint shares in crossing segment")
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
totalPtSpend += minted.usedPt
|
|
551
|
+
totalSySpend += minted.usedSy
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const sySpent = Number(totalSySpend)
|
|
555
|
+
const ptSpent = Number(totalPtSpend)
|
|
556
|
+
if (!Number.isSafeInteger(sySpent) || !Number.isSafeInteger(ptSpent)) {
|
|
557
|
+
throw new Error("Token spend exceeds JS safe integer range")
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { sySpent, ptSpent }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Compute token needs with crossing tick adjustment
|
|
565
|
+
* This matches the Rust compute_token_needs_with_crossing function
|
|
566
|
+
*/
|
|
567
|
+
export function computeTokenNeedsWithCrossing(
|
|
568
|
+
snap: EffSnap,
|
|
569
|
+
spotPriceCurrent: number,
|
|
570
|
+
priceEffLower: number,
|
|
571
|
+
priceEffUpper: number,
|
|
572
|
+
lowerPrice: number,
|
|
573
|
+
upperPrice: number,
|
|
574
|
+
maxSy: number,
|
|
575
|
+
maxPt: number,
|
|
576
|
+
epsilonClamp: number,
|
|
577
|
+
crossingTickState: CrossingTickState,
|
|
578
|
+
crossingTickPriceLeft: number,
|
|
579
|
+
crossingTickPriceRight: number,
|
|
580
|
+
): [number, number] {
|
|
581
|
+
// Below range: SY only
|
|
582
|
+
if (spotPriceCurrent <= lowerPrice) {
|
|
583
|
+
const deltaCTotal = priceEffLower - priceEffUpper
|
|
584
|
+
const liquidityFromSy = maxSy / deltaCTotal
|
|
585
|
+
const syNeed = liquidityFromSy * deltaCTotal
|
|
586
|
+
return [Math.ceil(syNeed), 0]
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Above range: PT only
|
|
590
|
+
if (spotPriceCurrent >= upperPrice) {
|
|
591
|
+
const duTotal = upperPrice - lowerPrice
|
|
592
|
+
const liquidityFromPt = maxPt / duTotal
|
|
593
|
+
const ptNeed = liquidityFromPt * duTotal
|
|
594
|
+
return [0, Math.ceil(ptNeed)]
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Inside range: both sides
|
|
598
|
+
const duLeft = Math.max(spotPriceCurrent - lowerPrice, epsilonClamp)
|
|
599
|
+
const priceEffCurrent = snap.getEffectivePrice(spotPriceCurrent)
|
|
600
|
+
const deltaCRight = Math.max(priceEffCurrent - priceEffUpper, epsilonClamp)
|
|
601
|
+
|
|
602
|
+
const liquidityFromPt = maxPt / duLeft
|
|
603
|
+
const liquidityFromSy = maxSy / deltaCRight
|
|
604
|
+
|
|
605
|
+
const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy)
|
|
606
|
+
let ptNeed = liquidityTarget * duLeft
|
|
607
|
+
let syNeed = liquidityTarget * deltaCRight
|
|
608
|
+
|
|
609
|
+
// Apply crossing tick adjustment if the tick has existing liquidity
|
|
610
|
+
// Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
|
|
611
|
+
const isCrossingTickActive =
|
|
612
|
+
crossingTickState.principalShareSupply > 0 &&
|
|
613
|
+
crossingTickPriceLeft < spotPriceCurrent &&
|
|
614
|
+
crossingTickPriceRight > spotPriceCurrent
|
|
615
|
+
|
|
616
|
+
if (isCrossingTickActive) {
|
|
617
|
+
// Calculate PT and SY portions that would go into the crossing tick
|
|
618
|
+
// PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
|
|
619
|
+
const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice)
|
|
620
|
+
const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment)
|
|
621
|
+
|
|
622
|
+
// SY portion: from spotPriceCurrent to crossingTickPriceRight
|
|
623
|
+
const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft)
|
|
624
|
+
const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice)
|
|
625
|
+
const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd)
|
|
626
|
+
const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0))
|
|
627
|
+
|
|
628
|
+
if (crossingPtIntended > 0 && crossingSyIntended > 0) {
|
|
629
|
+
// Tokens already allocated to non-crossing segments before crossing processing.
|
|
630
|
+
const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0)
|
|
631
|
+
const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0)
|
|
632
|
+
|
|
633
|
+
// Apply scaling from original max values to prevent double trimming
|
|
634
|
+
const [scaledPtIn, scaledSyIn] =
|
|
635
|
+
duLeft > 0 && deltaCRight > 0
|
|
636
|
+
? (() => {
|
|
637
|
+
const totalPtSpend = Math.ceil(ptOutsideCrossing)
|
|
638
|
+
const totalSySpend = Math.ceil(syOutsideCrossing)
|
|
639
|
+
const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0)
|
|
640
|
+
const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0)
|
|
641
|
+
|
|
642
|
+
// Use max of CLMM-calculated value and scaled original value
|
|
643
|
+
return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)]
|
|
644
|
+
})()
|
|
645
|
+
: [crossingPtIntended, crossingSyIntended]
|
|
646
|
+
|
|
647
|
+
// Simulate add_liquidity proportional logic with scaled inputs
|
|
648
|
+
const [usedPt, usedSy] = simulateAddLiquidityProportional(
|
|
649
|
+
scaledPtIn,
|
|
650
|
+
scaledSyIn,
|
|
651
|
+
crossingTickState.principalPt,
|
|
652
|
+
crossingTickState.principalSy,
|
|
653
|
+
crossingTickState.principalShareSupply,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
// Adjust needs based on what would actually be used
|
|
657
|
+
ptNeed = ptOutsideCrossing + usedPt
|
|
658
|
+
|
|
659
|
+
syNeed = syOutsideCrossing + usedSy
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return [Math.ceil(syNeed), Math.ceil(ptNeed)]
|
|
80
664
|
}
|
|
81
665
|
|
|
82
666
|
/**
|
|
@@ -101,6 +685,9 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
|
|
|
101
685
|
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
102
686
|
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
103
687
|
|
|
688
|
+
const maxSyForNeeds = Math.max(0, Math.floor(args.maxSy) - GAP_TOKEN_NEEDS)
|
|
689
|
+
const maxPtForNeeds = Math.max(0, Math.floor(args.maxPt) - GAP_TOKEN_NEEDS)
|
|
690
|
+
|
|
104
691
|
// Calculate liquidity needs
|
|
105
692
|
const liquidityNeeds = computeLiquidityTargetAndTokenNeeds(
|
|
106
693
|
snap,
|
|
@@ -112,14 +699,33 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
|
|
|
112
699
|
args.lowerTick,
|
|
113
700
|
args.upperTick,
|
|
114
701
|
ticks.currentTick,
|
|
115
|
-
|
|
116
|
-
|
|
702
|
+
maxSyForNeeds,
|
|
703
|
+
maxPtForNeeds,
|
|
117
704
|
configurationOptions.epsilonClamp,
|
|
118
705
|
)
|
|
119
706
|
|
|
707
|
+
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1]
|
|
708
|
+
const currentTickKey =
|
|
709
|
+
currentTickNode && currentTickNode.apyBasePoints > 0 ? currentTickNode.apyBasePoints : args.lowerTick
|
|
710
|
+
const splitTickKey =
|
|
711
|
+
currentSpot <= lowerPrice ? args.lowerTick : currentSpot >= upperPrice ? args.upperTick : currentTickKey
|
|
712
|
+
const { sySpent: syNeededWithCrossing, ptSpent: ptNeededWithCrossing } = simulateAccruePrincipalForDeposit({
|
|
713
|
+
ticks,
|
|
714
|
+
snap,
|
|
715
|
+
lowerPrice,
|
|
716
|
+
upperPrice,
|
|
717
|
+
priceSplitForNeed: liquidityNeeds.priceSplitForNeed,
|
|
718
|
+
splitTickKey,
|
|
719
|
+
lowerTickKey: args.lowerTick,
|
|
720
|
+
upperTickKey: args.upperTick,
|
|
721
|
+
liquidityTarget: liquidityNeeds.liquidityTarget,
|
|
722
|
+
originalMaxSy: liquidityNeeds.originalMaxSy,
|
|
723
|
+
originalMaxPt: liquidityNeeds.originalMaxPt,
|
|
724
|
+
})
|
|
725
|
+
|
|
120
726
|
// Enforce budgets
|
|
121
|
-
const sySpent =
|
|
122
|
-
const ptSpent =
|
|
727
|
+
const sySpent = syNeededWithCrossing
|
|
728
|
+
const ptSpent = ptNeededWithCrossing
|
|
123
729
|
|
|
124
730
|
if (sySpent > args.maxSy) {
|
|
125
731
|
throw new Error(`Insufficient SY budget: need ${sySpent}, have ${args.maxSy}`)
|
|
@@ -198,8 +804,26 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
198
804
|
expirationTs: number
|
|
199
805
|
currentSpotPrice: number
|
|
200
806
|
syExchangeRate: number
|
|
807
|
+
/** Optional crossing tick state for accurate ratio prediction.
|
|
808
|
+
* When provided, the prediction accounts for the existing PT/SY proportions
|
|
809
|
+
* in the active tick, matching the on-chain behaviour more closely. */
|
|
810
|
+
crossingTickState?: CrossingTickState
|
|
811
|
+
crossingTickPriceLeft?: number
|
|
812
|
+
crossingTickPriceRight?: number
|
|
813
|
+
epsilonClamp?: number
|
|
201
814
|
}) {
|
|
202
|
-
const {
|
|
815
|
+
const {
|
|
816
|
+
expirationTs,
|
|
817
|
+
currentSpotPrice,
|
|
818
|
+
syExchangeRate,
|
|
819
|
+
lowerPrice,
|
|
820
|
+
upperPrice,
|
|
821
|
+
baseTokenAmount,
|
|
822
|
+
crossingTickState = EMPTY_CROSSING_TICK_STATE,
|
|
823
|
+
crossingTickPriceLeft = 0,
|
|
824
|
+
crossingTickPriceRight = 0,
|
|
825
|
+
epsilonClamp = 1e-18,
|
|
826
|
+
} = params
|
|
203
827
|
|
|
204
828
|
if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
|
|
205
829
|
return {
|
|
@@ -215,23 +839,25 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
215
839
|
const priceEffUpper = effSnap.getEffectivePrice(upperPrice)
|
|
216
840
|
|
|
217
841
|
// We mirror the on-chain logic in `wrapper_provide_liquidity`:
|
|
218
|
-
// 1. Use
|
|
219
|
-
// the market "wants" for this price range
|
|
842
|
+
// 1. Use compute_token_needs_with_crossing to infer the ratio of SY/PT
|
|
843
|
+
// the market "wants" for this price range.
|
|
844
|
+
// When crossing tick state is provided, this accounts for the existing
|
|
845
|
+
// PT/SY proportions in the active tick, significantly improving accuracy.
|
|
220
846
|
// 2. Use that ratio plus the current SY exchange rate to decide how much of the
|
|
221
|
-
// minted SY should be stripped into PT
|
|
222
|
-
const
|
|
847
|
+
// minted SY should be stripped into PT (calc_strip_amount).
|
|
848
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(
|
|
223
849
|
effSnap,
|
|
224
850
|
currentSpotPrice,
|
|
225
851
|
priceEffLower,
|
|
226
852
|
priceEffUpper,
|
|
227
853
|
lowerPrice,
|
|
228
854
|
upperPrice,
|
|
229
|
-
0,
|
|
230
|
-
0,
|
|
231
|
-
0,
|
|
232
855
|
1e9,
|
|
233
856
|
1e9,
|
|
234
|
-
|
|
857
|
+
epsilonClamp,
|
|
858
|
+
crossingTickState,
|
|
859
|
+
crossingTickPriceLeft,
|
|
860
|
+
crossingTickPriceRight,
|
|
235
861
|
)
|
|
236
862
|
|
|
237
863
|
// Total SY the user would get by minting SY from base off-chain
|
|
@@ -267,7 +893,9 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
267
893
|
* Solution: B = (totalAmountSy * marketPtLiq) / (marketPtLiq + marketSyLiq * curSyRate)
|
|
268
894
|
*/
|
|
269
895
|
function calcStripAmount(totalAmountSy: number, curSyRate: number, marketPtLiq: number, marketSyLiq: number): number {
|
|
270
|
-
const
|
|
896
|
+
const denominator = curSyRate * marketSyLiq + marketPtLiq
|
|
897
|
+
if (denominator <= 0) return 0
|
|
898
|
+
const toStrip = (totalAmountSy * marketPtLiq) / denominator
|
|
271
899
|
return Math.ceil(toStrip)
|
|
272
900
|
}
|
|
273
901
|
|
|
@@ -324,42 +952,43 @@ export function simulateWrapperProvideLiquidity(
|
|
|
324
952
|
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
325
953
|
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
326
954
|
|
|
327
|
-
// Step 3:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
955
|
+
// Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
|
|
956
|
+
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } =
|
|
957
|
+
getCrossingTickStateFromTicks(ticks)
|
|
958
|
+
|
|
959
|
+
// Step 4: Calculate mock token needs using compute_token_needs_with_crossing
|
|
960
|
+
// max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
|
|
961
|
+
const maxPt = syAmount * syExchangeRate
|
|
962
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(
|
|
331
963
|
snap,
|
|
332
964
|
currentSpot,
|
|
333
965
|
priceEffLower,
|
|
334
966
|
priceEffUpper,
|
|
335
967
|
lowerPrice,
|
|
336
968
|
upperPrice,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
ticks.currentTick,
|
|
340
|
-
MOCK_AMOUNT,
|
|
341
|
-
MOCK_AMOUNT,
|
|
969
|
+
syAmount,
|
|
970
|
+
maxPt,
|
|
342
971
|
configurationOptions.epsilonClamp,
|
|
972
|
+
crossingTickState,
|
|
973
|
+
crossingTickPriceLeft,
|
|
974
|
+
crossingTickPriceRight,
|
|
343
975
|
)
|
|
344
976
|
|
|
345
|
-
// Step
|
|
346
|
-
//
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
console.log("[simulateWrapperProvideLiquidity] Strip calculation:", {
|
|
350
|
-
syToStrip,
|
|
351
|
-
formula: `(${syAmount} * ${mockNeeds.ptNeeded}) / (${syExchangeRate} * ${mockNeeds.syNeeded} + ${mockNeeds.ptNeeded})`,
|
|
352
|
-
})
|
|
977
|
+
// Step 5: Calculate how much SY to strip (calc_strip_amount on-chain)
|
|
978
|
+
// Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
|
|
979
|
+
const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock)
|
|
980
|
+
const syToStrip = Math.min(syToStripRaw, syAmount)
|
|
353
981
|
|
|
354
|
-
// Step
|
|
982
|
+
// Step 6: Calculate PT and YT from stripping
|
|
355
983
|
// When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
|
|
356
984
|
const ptFromStrip = syToStrip * syExchangeRate
|
|
357
985
|
const ytOut = ptFromStrip // YT amount equals PT amount from strip
|
|
358
986
|
|
|
359
|
-
// Step
|
|
360
|
-
|
|
987
|
+
// Step 7: Calculate remaining SY after strip
|
|
988
|
+
// Use Math.max to ensure non-negative (safety net for floating point edge cases)
|
|
989
|
+
const syRemainder = Math.max(0, syAmount - syToStrip)
|
|
361
990
|
|
|
362
|
-
// Step
|
|
991
|
+
// Step 8: Simulate deposit liquidity with remaining SY and PT
|
|
363
992
|
const depositResult = simulateAddLiquidity(marketState, {
|
|
364
993
|
lowerTick,
|
|
365
994
|
upperTick,
|
|
@@ -382,3 +1011,348 @@ export function simulateWrapperProvideLiquidity(
|
|
|
382
1011
|
return null
|
|
383
1012
|
}
|
|
384
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Simulate swap & supply operation (ixProvideLiquidityBase)
|
|
1017
|
+
* 1. Mints SY from base asset
|
|
1018
|
+
* 2. Swaps some SY for PT on the market (instead of stripping)
|
|
1019
|
+
* 3. Deposits remaining SY + bought PT into liquidity position
|
|
1020
|
+
*
|
|
1021
|
+
* This approach has lower slippage than minting because it uses market liquidity.
|
|
1022
|
+
*
|
|
1023
|
+
* @param marketState - Current market state
|
|
1024
|
+
* @param amountBase - Amount of base tokens (in lamports)
|
|
1025
|
+
* @param lowerTick - Lower tick key (APY in basis points)
|
|
1026
|
+
* @param upperTick - Upper tick key (APY in basis points)
|
|
1027
|
+
* @param syExchangeRate - SY exchange rate
|
|
1028
|
+
* @returns Simulation result with LP out, PT to buy, SY constraint, etc.
|
|
1029
|
+
*/
|
|
1030
|
+
export function simulateSwapAndSupply(
|
|
1031
|
+
marketState: MarketThreeState,
|
|
1032
|
+
amountBase: number,
|
|
1033
|
+
lowerTick: number,
|
|
1034
|
+
upperTick: number,
|
|
1035
|
+
syExchangeRate: number,
|
|
1036
|
+
): {
|
|
1037
|
+
lpOut: number
|
|
1038
|
+
ptToBuy: number
|
|
1039
|
+
syConstraint: number
|
|
1040
|
+
syForSwap: number
|
|
1041
|
+
syRemainder: number
|
|
1042
|
+
ptFromSwap: number
|
|
1043
|
+
syDeposited: number
|
|
1044
|
+
ptDeposited: number
|
|
1045
|
+
} | null {
|
|
1046
|
+
try {
|
|
1047
|
+
// Wrapper provide-liquidity-base debits base as:
|
|
1048
|
+
// base_needed = ceil(total_sy_spent * sy_exchange_rate)
|
|
1049
|
+
// So the strict SY budget for a user-provided base input is floor(base / rate).
|
|
1050
|
+
const syBudget = convertBaseToSyBudget(amountBase)
|
|
1051
|
+
|
|
1052
|
+
if (syBudget <= 0) {
|
|
1053
|
+
return {
|
|
1054
|
+
lpOut: 0,
|
|
1055
|
+
ptToBuy: 0,
|
|
1056
|
+
syConstraint: 0,
|
|
1057
|
+
syForSwap: 0,
|
|
1058
|
+
syRemainder: 0,
|
|
1059
|
+
ptFromSwap: 0,
|
|
1060
|
+
syDeposited: 0,
|
|
1061
|
+
ptDeposited: 0,
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const MOCK_AMOUNT = 1e9
|
|
1066
|
+
const mockNeeds = simulateAddLiquidity(marketState, {
|
|
1067
|
+
lowerTick,
|
|
1068
|
+
upperTick,
|
|
1069
|
+
maxSy: MOCK_AMOUNT,
|
|
1070
|
+
maxPt: MOCK_AMOUNT,
|
|
1071
|
+
syExchangeRate,
|
|
1072
|
+
})
|
|
1073
|
+
const searchBudget = Math.max(0, syBudget - SWAP_EXACT_OUT_SY_HEADROOM)
|
|
1074
|
+
|
|
1075
|
+
// Wrapper flow still executes a flash swap even when external_pt_to_buy is 0
|
|
1076
|
+
// (effective exact-out target becomes 2 due +2 safety margin).
|
|
1077
|
+
let externalPtToBuy = 0
|
|
1078
|
+
if (syBudget > 0 && mockNeeds.ptSpent > 0) {
|
|
1079
|
+
if (mockNeeds.sySpent > 0) {
|
|
1080
|
+
// Normal case: position range includes current spot price
|
|
1081
|
+
// Calculate optimal split between swap and deposit
|
|
1082
|
+
const denominator = syExchangeRate * mockNeeds.sySpent + mockNeeds.ptSpent
|
|
1083
|
+
const syToSwapGuess = denominator > 0 ? Math.ceil((syBudget * mockNeeds.ptSpent) / denominator) : 0
|
|
1084
|
+
|
|
1085
|
+
if (syToSwapGuess > 0) {
|
|
1086
|
+
const guessSwap = simulateSwap(marketState, {
|
|
1087
|
+
direction: SwapDirection.SyToPt,
|
|
1088
|
+
amountIn: syToSwapGuess,
|
|
1089
|
+
syExchangeRate,
|
|
1090
|
+
isCurrentFlashSwap: true,
|
|
1091
|
+
})
|
|
1092
|
+
externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut))
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
// Below-range case: position only needs PT (sySpent = 0)
|
|
1096
|
+
// Swap all SY to PT
|
|
1097
|
+
const guessSwap = simulateSwap(marketState, {
|
|
1098
|
+
direction: SwapDirection.SyToPt,
|
|
1099
|
+
amountIn: syBudget,
|
|
1100
|
+
syExchangeRate,
|
|
1101
|
+
isCurrentFlashSwap: true,
|
|
1102
|
+
})
|
|
1103
|
+
externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut))
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const candidateCache = new Map<number, SwapAndSupplyCandidate | null>()
|
|
1108
|
+
const evaluateCandidate = (ptConstraint: number) => {
|
|
1109
|
+
const key = Math.max(0, Math.floor(ptConstraint))
|
|
1110
|
+
const cached = candidateCache.get(key)
|
|
1111
|
+
if (cached !== undefined) {
|
|
1112
|
+
return cached
|
|
1113
|
+
}
|
|
1114
|
+
const candidate = simulateSwapAndSupplyForPtConstraint(
|
|
1115
|
+
marketState,
|
|
1116
|
+
lowerTick,
|
|
1117
|
+
upperTick,
|
|
1118
|
+
syBudget,
|
|
1119
|
+
key,
|
|
1120
|
+
syExchangeRate,
|
|
1121
|
+
)
|
|
1122
|
+
candidateCache.set(key, candidate)
|
|
1123
|
+
return candidate
|
|
1124
|
+
}
|
|
1125
|
+
const ptCap = 1_000_000_000
|
|
1126
|
+
let bestUnderBudget: SwapAndSupplyCandidate | null = null
|
|
1127
|
+
let leastOverspend: SwapAndSupplyCandidate | null = null
|
|
1128
|
+
const considerCandidate = (candidate: SwapAndSupplyCandidate | null) => {
|
|
1129
|
+
if (!candidate) {
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
if (candidate.totalSySpent <= searchBudget) {
|
|
1133
|
+
if (
|
|
1134
|
+
!bestUnderBudget ||
|
|
1135
|
+
candidate.totalSySpent > bestUnderBudget.totalSySpent ||
|
|
1136
|
+
(candidate.totalSySpent === bestUnderBudget.totalSySpent && candidate.lpOut > bestUnderBudget.lpOut)
|
|
1137
|
+
) {
|
|
1138
|
+
bestUnderBudget = candidate
|
|
1139
|
+
}
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (
|
|
1144
|
+
!leastOverspend ||
|
|
1145
|
+
candidate.totalSySpent < leastOverspend.totalSySpent ||
|
|
1146
|
+
(candidate.totalSySpent === leastOverspend.totalSySpent && candidate.lpOut > leastOverspend.lpOut)
|
|
1147
|
+
) {
|
|
1148
|
+
leastOverspend = candidate
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Always evaluate from zero to establish a valid lower bound.
|
|
1153
|
+
considerCandidate(evaluateCandidate(0))
|
|
1154
|
+
|
|
1155
|
+
let lowPt = 0
|
|
1156
|
+
let highPt = Math.max(1, externalPtToBuy)
|
|
1157
|
+
let highCandidate = evaluateCandidate(highPt)
|
|
1158
|
+
considerCandidate(highCandidate)
|
|
1159
|
+
if (highCandidate && highCandidate.totalSySpent <= searchBudget) {
|
|
1160
|
+
lowPt = highPt
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Expand upward until we hit over-budget/null region so binary search can maximize usage.
|
|
1164
|
+
for (let i = 0; i < 18 && highPt < ptCap; i++) {
|
|
1165
|
+
if (!highCandidate || highCandidate.totalSySpent > searchBudget) {
|
|
1166
|
+
break
|
|
1167
|
+
}
|
|
1168
|
+
const nextHigh = Math.min(ptCap, highPt * 2)
|
|
1169
|
+
if (nextHigh === highPt) {
|
|
1170
|
+
break
|
|
1171
|
+
}
|
|
1172
|
+
highPt = nextHigh
|
|
1173
|
+
highCandidate = evaluateCandidate(highPt)
|
|
1174
|
+
considerCandidate(highCandidate)
|
|
1175
|
+
if (highCandidate && highCandidate.totalSySpent <= searchBudget) {
|
|
1176
|
+
lowPt = highPt
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Binary search the feasibility boundary.
|
|
1181
|
+
let searchLow = Math.max(0, lowPt + 1)
|
|
1182
|
+
let searchHigh = highPt
|
|
1183
|
+
while (searchLow <= searchHigh) {
|
|
1184
|
+
const mid = Math.floor((searchLow + searchHigh) / 2)
|
|
1185
|
+
const candidate = evaluateCandidate(mid)
|
|
1186
|
+
considerCandidate(candidate)
|
|
1187
|
+
|
|
1188
|
+
if (candidate && candidate.totalSySpent <= searchBudget) {
|
|
1189
|
+
searchLow = mid + 1
|
|
1190
|
+
} else {
|
|
1191
|
+
searchHigh = mid - 1
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Local sweep around the boundary to handle small non-monotonic steps.
|
|
1196
|
+
const localRadius = 16
|
|
1197
|
+
const localStart = Math.max(0, searchHigh - localRadius)
|
|
1198
|
+
const localEnd = Math.min(ptCap, searchHigh + localRadius)
|
|
1199
|
+
for (let pt = localStart; pt <= localEnd; pt++) {
|
|
1200
|
+
considerCandidate(evaluateCandidate(pt))
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const selected = bestUnderBudget ?? leastOverspend
|
|
1204
|
+
if (!selected) {
|
|
1205
|
+
throw new Error("Unable to simulate swap & supply for initial PT constraint")
|
|
1206
|
+
}
|
|
1207
|
+
if (process.env.DEBUG_SWAP_SUPPLY === "1") {
|
|
1208
|
+
console.log("[simulateSwapAndSupply]", {
|
|
1209
|
+
syBudget,
|
|
1210
|
+
searchBudget,
|
|
1211
|
+
mockNeeds,
|
|
1212
|
+
externalPtToBuy,
|
|
1213
|
+
selected,
|
|
1214
|
+
})
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return {
|
|
1218
|
+
lpOut: selected.lpOut,
|
|
1219
|
+
ptToBuy: selected.ptConstraint,
|
|
1220
|
+
syConstraint: syBudget,
|
|
1221
|
+
syForSwap: selected.tradeSySpent,
|
|
1222
|
+
syRemainder: selected.syRemainderAfterSwap,
|
|
1223
|
+
ptFromSwap: selected.tradePtOut,
|
|
1224
|
+
syDeposited: selected.depositSySpent,
|
|
1225
|
+
ptDeposited: selected.depositPtSpent,
|
|
1226
|
+
}
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
console.error("[simulateSwapAndSupply] Error:", error)
|
|
1229
|
+
return null
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
type SwapAndSupplyCandidate = {
|
|
1234
|
+
ptConstraint: number
|
|
1235
|
+
tradeSySpent: number
|
|
1236
|
+
tradePtOut: number
|
|
1237
|
+
depositSySpent: number
|
|
1238
|
+
depositPtSpent: number
|
|
1239
|
+
lpOut: number
|
|
1240
|
+
totalSySpent: number
|
|
1241
|
+
syRemainderAfterSwap: number
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function simulateSwapAndSupplyForPtConstraint(
|
|
1245
|
+
marketState: MarketThreeState,
|
|
1246
|
+
lowerTick: number,
|
|
1247
|
+
upperTick: number,
|
|
1248
|
+
syBudget: number,
|
|
1249
|
+
ptConstraint: number,
|
|
1250
|
+
syExchangeRate: number,
|
|
1251
|
+
): SwapAndSupplyCandidate | null {
|
|
1252
|
+
let swapResult: {
|
|
1253
|
+
sySpent: number
|
|
1254
|
+
ptOut: number
|
|
1255
|
+
postMarketState?: MarketThreeState
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
try {
|
|
1259
|
+
swapResult = simulateBuyPtExactOutWrapper(
|
|
1260
|
+
marketState,
|
|
1261
|
+
syBudget,
|
|
1262
|
+
ptConstraint,
|
|
1263
|
+
syExchangeRate,
|
|
1264
|
+
)
|
|
1265
|
+
} catch {
|
|
1266
|
+
// Budget insufficient for this PT constraint
|
|
1267
|
+
return null
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const depositState = swapResult.postMarketState ?? marketState
|
|
1271
|
+
const syAvailableForDeposit = Math.max(0, syBudget - swapResult.sySpent)
|
|
1272
|
+
const depositResult = simulateAddLiquidity(depositState, {
|
|
1273
|
+
lowerTick,
|
|
1274
|
+
upperTick,
|
|
1275
|
+
maxSy: syBudget,
|
|
1276
|
+
maxPt: swapResult.ptOut,
|
|
1277
|
+
syExchangeRate,
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
// Swap & supply must actually add liquidity.
|
|
1281
|
+
// Discard candidates that end up as swap-only (deltaL == 0).
|
|
1282
|
+
if (depositResult.deltaL <= 0) {
|
|
1283
|
+
return null
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const ptRemainder = Math.max(0, swapResult.ptOut - depositResult.ptSpent)
|
|
1287
|
+
if (ptRemainder > 1_000) {
|
|
1288
|
+
return null
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const totalSySpent = swapResult.sySpent + depositResult.sySpent
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
ptConstraint,
|
|
1295
|
+
tradeSySpent: swapResult.sySpent,
|
|
1296
|
+
tradePtOut: swapResult.ptOut,
|
|
1297
|
+
depositSySpent: depositResult.sySpent,
|
|
1298
|
+
depositPtSpent: depositResult.ptSpent,
|
|
1299
|
+
lpOut: depositResult.deltaL,
|
|
1300
|
+
totalSySpent,
|
|
1301
|
+
syRemainderAfterSwap: Math.max(0, syBudget - swapResult.sySpent),
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function convertBaseToSyBudget(amountBase: number): number {
|
|
1306
|
+
const baseAmount = Math.max(0, Math.floor(amountBase))
|
|
1307
|
+
return baseAmount
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function simulateBuyPtExactOutWrapper(
|
|
1311
|
+
marketState: MarketThreeState,
|
|
1312
|
+
syBudget: number,
|
|
1313
|
+
ptConstraint: number,
|
|
1314
|
+
syExchangeRate: number,
|
|
1315
|
+
): {
|
|
1316
|
+
sySpent: number
|
|
1317
|
+
ptOut: number
|
|
1318
|
+
postMarketState?: MarketThreeState
|
|
1319
|
+
} {
|
|
1320
|
+
const maxSyBudget = Math.max(0, Math.floor(syBudget))
|
|
1321
|
+
const effectivePtOutTarget = Math.max(0, Math.floor(ptConstraint)) + 2
|
|
1322
|
+
|
|
1323
|
+
if (maxSyBudget === 0) {
|
|
1324
|
+
return {
|
|
1325
|
+
sySpent: 0,
|
|
1326
|
+
ptOut: 0,
|
|
1327
|
+
postMarketState: marketState,
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const tryExactOut = (targetPtOut: number) => {
|
|
1332
|
+
return simulateSwapExactOut(marketState, {
|
|
1333
|
+
direction: SwapDirection.SyToPt,
|
|
1334
|
+
amountOut: targetPtOut,
|
|
1335
|
+
syExchangeRate,
|
|
1336
|
+
isCurrentFlashSwap: true,
|
|
1337
|
+
amountInConstraint: maxSyBudget,
|
|
1338
|
+
})
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// WrapperProvideLiquidityBase always executes exact-out with `pt_constraint + 2`.
|
|
1342
|
+
// Do not fallback to lower exact-out targets here, otherwise client simulation can
|
|
1343
|
+
// accept candidates that deterministically fail on-chain with InsufficientBudgetSY.
|
|
1344
|
+
let bestQuote: ReturnType<typeof tryExactOut>
|
|
1345
|
+
try {
|
|
1346
|
+
bestQuote = tryExactOut(effectivePtOutTarget)
|
|
1347
|
+
} catch {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`Insufficient SY budget for wrapper exact-out simulation (budget=${maxSyBudget}, pt_constraint=${ptConstraint})`,
|
|
1350
|
+
)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return {
|
|
1354
|
+
sySpent: bestQuote.amountInConsumed,
|
|
1355
|
+
ptOut: bestQuote.amountOut,
|
|
1356
|
+
postMarketState: bestQuote.postMarketState,
|
|
1357
|
+
}
|
|
1358
|
+
}
|