@exponent-labs/market-three-math 0.1.8 → 0.9.0
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 +757 -30
- package/build/addLiquidity.js.map +1 -1
- package/build/bisect.d.ts +11 -0
- package/build/bisect.js +21 -10
- 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 +55 -9
- package/build/liquidityHistogram.js.map +1 -1
- package/build/quote.d.ts +1 -1
- package/build/quote.js +68 -82
- package/build/quote.js.map +1 -1
- package/build/swap.js +35 -16
- package/build/swap.js.map +1 -1
- package/build/swapV2.d.ts +6 -1
- package/build/swapV2.js +394 -51
- 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 +23 -5
- package/build/utils.js.map +1 -1
- package/build/utilsV2.d.ts +9 -0
- package/build/utilsV2.js +127 -5
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.js +11 -5
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.d.ts +7 -0
- package/build/ytTrades.js +163 -142
- package/build/ytTrades.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/src/swapLegacy.ts +0 -272
package/build/addLiquidity.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.simulateWrapperProvideLiquidity = exports.calcDepositSyAndPtFromBaseAmount = exports.estimateBalancedDeposit = exports.calculateLpOut = exports.simulateAddLiquidity = exports.computeLiquidityTargetAndTokenNeeds = void 0;
|
|
3
|
+
exports.simulateSwapAndSupply = exports.simulateWrapperProvideLiquidity = exports.calcDepositSyAndPtFromBaseAmount = exports.estimateBalancedDeposit = exports.calculateLpOut = exports.simulateAddLiquidity = exports.computeTokenNeedsWithCrossing = exports.scaleCrossingTickInputs = exports.computeLiquidityTargetAndTokenNeeds = exports.getCrossingTickStateFromTicks = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* CLMM Add Liquidity simulation
|
|
6
|
+
* Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
|
|
7
|
+
*/
|
|
8
|
+
const swapV2_1 = require("./swapV2");
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const utilsV2_1 = require("./utilsV2");
|
|
4
11
|
const utils_1 = require("./utils");
|
|
5
12
|
const TICK_KEY_BASE_POINTS = 1_000_000;
|
|
13
|
+
// Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
|
|
14
|
+
// quotes match the on-chain classic deposit path as closely as possible.
|
|
15
|
+
const GAP_TOKEN_NEEDS = 5;
|
|
16
|
+
const SWAP_EXACT_OUT_SY_HEADROOM = 10;
|
|
17
|
+
const EMPTY_CROSSING_TICK_STATE = {
|
|
18
|
+
principalPt: 0,
|
|
19
|
+
principalSy: 0,
|
|
20
|
+
principalShareSupply: 0,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Get crossing tick state and price boundaries from Ticks.
|
|
24
|
+
* Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
|
|
25
|
+
*/
|
|
26
|
+
function getCrossingTickStateFromTicks(ticks) {
|
|
27
|
+
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1];
|
|
28
|
+
if (!currentTickNode || ticks.currentTick <= 0) {
|
|
29
|
+
return {
|
|
30
|
+
crossingTickState: EMPTY_CROSSING_TICK_STATE,
|
|
31
|
+
crossingTickPriceLeft: 0,
|
|
32
|
+
crossingTickPriceRight: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const crossingTickPriceLeft = currentTickNode.impliedRate;
|
|
36
|
+
const successorKey = (0, utils_1.getSuccessorTickKey)(ticks, currentTickNode.apyBasePoints);
|
|
37
|
+
const successorTick = successorKey != null ? (0, utils_1.findTickByKey)(ticks, successorKey)?.tick : null;
|
|
38
|
+
const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY;
|
|
39
|
+
return {
|
|
40
|
+
crossingTickState: {
|
|
41
|
+
principalPt: Number(currentTickNode.principalPt),
|
|
42
|
+
principalSy: Number(currentTickNode.principalSy),
|
|
43
|
+
principalShareSupply: Number(currentTickNode.principalShareSupply),
|
|
44
|
+
},
|
|
45
|
+
crossingTickPriceLeft,
|
|
46
|
+
crossingTickPriceRight,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
exports.getCrossingTickStateFromTicks = getCrossingTickStateFromTicks;
|
|
6
50
|
/**
|
|
7
51
|
* Compute liquidity target and token needs based on position range and budgets
|
|
8
52
|
* Ported from compute_liquidity_target_and_token_needs in math.rs
|
|
@@ -22,6 +66,11 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
22
66
|
ptNeeded: 0,
|
|
23
67
|
priceSplitForNeed: lowerPrice,
|
|
24
68
|
priceSplitTickIdx: lowerTickIdx,
|
|
69
|
+
// No crossing possible when below range
|
|
70
|
+
originalMaxSy: maxSy,
|
|
71
|
+
originalMaxPt: maxPt,
|
|
72
|
+
duLeftTotal: 0,
|
|
73
|
+
deltaCRightTotal: deltaCTotal,
|
|
25
74
|
};
|
|
26
75
|
}
|
|
27
76
|
else if (spotPriceCurrent >= upperPrice) {
|
|
@@ -36,10 +85,15 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
36
85
|
ptNeeded: Math.floor(ptNeed),
|
|
37
86
|
priceSplitForNeed: upperPrice,
|
|
38
87
|
priceSplitTickIdx: upperTickIdx,
|
|
88
|
+
// No crossing possible when above range
|
|
89
|
+
originalMaxSy: maxSy,
|
|
90
|
+
originalMaxPt: maxPt,
|
|
91
|
+
duLeftTotal: duTotal,
|
|
92
|
+
deltaCRightTotal: 0,
|
|
39
93
|
};
|
|
40
94
|
}
|
|
41
95
|
else {
|
|
42
|
-
// Inside range: both sides
|
|
96
|
+
// Inside range: both sides - crossing tick possible
|
|
43
97
|
const duLeft = Math.max(spotPriceCurrent - lowerPrice, epsilonClamp);
|
|
44
98
|
const liquidityFromPt = maxPt / duLeft;
|
|
45
99
|
const priceEffCurrent = snap.getEffectivePrice(spotPriceCurrent);
|
|
@@ -54,10 +108,401 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
54
108
|
ptNeeded: Math.floor(ptNeed),
|
|
55
109
|
priceSplitForNeed: spotPriceCurrent,
|
|
56
110
|
priceSplitTickIdx: currentIndex,
|
|
111
|
+
// Store for crossing tick scaling
|
|
112
|
+
originalMaxSy: maxSy,
|
|
113
|
+
originalMaxPt: maxPt,
|
|
114
|
+
duLeftTotal: duLeft,
|
|
115
|
+
deltaCRightTotal: deltaCRight,
|
|
57
116
|
};
|
|
58
117
|
}
|
|
59
118
|
}
|
|
60
119
|
exports.computeLiquidityTargetAndTokenNeeds = computeLiquidityTargetAndTokenNeeds;
|
|
120
|
+
/**
|
|
121
|
+
* Scale crossing tick token inputs from original max values to prevent double trimming.
|
|
122
|
+
*
|
|
123
|
+
* When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
|
|
124
|
+
* 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
|
|
125
|
+
* 2. Then by AMM formula in simulateAddLiquidityProportional
|
|
126
|
+
*
|
|
127
|
+
* This function calculates the segment's proportion of the total distribution and scales
|
|
128
|
+
* the original max values accordingly, ensuring maximum token utilization.
|
|
129
|
+
*
|
|
130
|
+
* @param scaleParams - Original max values and CLMM distribution extents
|
|
131
|
+
* @param duPtPart - PT segment length (spot price delta for this segment)
|
|
132
|
+
* @param syPerL1 - SY per unit liquidity for this segment
|
|
133
|
+
* @param clmmPtDelta - PT amount calculated from CLMM formula
|
|
134
|
+
* @param clmmSyDelta - SY amount calculated from CLMM formula
|
|
135
|
+
* @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
|
|
136
|
+
*/
|
|
137
|
+
function scaleCrossingTickInputs(scaleParams, duPtPart, syPerL1, clmmPtDelta, clmmSyDelta) {
|
|
138
|
+
const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams;
|
|
139
|
+
if (duLeftTotal > 0 && deltaCRightTotal > 0) {
|
|
140
|
+
// Calculate segment's proportion of total distribution
|
|
141
|
+
const ptSegmentRatio = duPtPart / duLeftTotal;
|
|
142
|
+
const sySegmentRatio = syPerL1 / deltaCRightTotal;
|
|
143
|
+
// Scale original max values by segment proportion
|
|
144
|
+
const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio);
|
|
145
|
+
const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio);
|
|
146
|
+
// Use max of CLMM-calculated value and scaled original value
|
|
147
|
+
// This ensures we don't lose tokens due to double trimming
|
|
148
|
+
return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)];
|
|
149
|
+
}
|
|
150
|
+
// Fallback to original logic if no crossing scale params
|
|
151
|
+
return [clmmPtDelta, clmmSyDelta];
|
|
152
|
+
}
|
|
153
|
+
exports.scaleCrossingTickInputs = scaleCrossingTickInputs;
|
|
154
|
+
/**
|
|
155
|
+
* Simulate the proportional add_liquidity logic
|
|
156
|
+
* Returns [usedPt, usedSy] based on existing tick proportions
|
|
157
|
+
*/
|
|
158
|
+
function simulateAddLiquidityProportional(intentPt, intentSy, marketTotalPt, marketTotalSy, marketTotalLp) {
|
|
159
|
+
if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
|
|
160
|
+
// Empty tick - use all intended amounts
|
|
161
|
+
return [intentPt, intentSy];
|
|
162
|
+
}
|
|
163
|
+
const lpFromPt = (marketTotalLp * intentPt) / marketTotalPt;
|
|
164
|
+
const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy;
|
|
165
|
+
if (lpFromPt < lpFromSy) {
|
|
166
|
+
// PT is the limiting factor
|
|
167
|
+
const lpTokensOut = lpFromPt;
|
|
168
|
+
const usedPt = intentPt;
|
|
169
|
+
const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp);
|
|
170
|
+
return [usedPt, usedSy];
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// SY is the limiting factor
|
|
174
|
+
const lpTokensOut = lpFromSy;
|
|
175
|
+
const usedSy = intentSy;
|
|
176
|
+
const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp);
|
|
177
|
+
return [usedPt, usedSy];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function toU64BigInt(value) {
|
|
181
|
+
return BigInt(Math.max(0, Math.floor(value)));
|
|
182
|
+
}
|
|
183
|
+
function ceilDivBigInt(numerator, denominator) {
|
|
184
|
+
if (denominator <= 0n) {
|
|
185
|
+
return 0n;
|
|
186
|
+
}
|
|
187
|
+
return numerator <= 0n ? 0n : (numerator + denominator - 1n) / denominator;
|
|
188
|
+
}
|
|
189
|
+
function floorSqrtBigInt(value) {
|
|
190
|
+
if (value <= 1n) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
let x0 = value;
|
|
194
|
+
let x1 = (x0 + 1n) >> 1n;
|
|
195
|
+
while (x1 < x0) {
|
|
196
|
+
x0 = x1;
|
|
197
|
+
x1 = (x1 + value / x1) >> 1n;
|
|
198
|
+
}
|
|
199
|
+
return x0;
|
|
200
|
+
}
|
|
201
|
+
function getVirtualTickState(key, ticksWrapper, virtualStates) {
|
|
202
|
+
const cached = virtualStates.get(key);
|
|
203
|
+
if (cached) {
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
const { principalPt, principalSy } = ticksWrapper.getPrincipals(key);
|
|
207
|
+
const tick = ticksWrapper.getTickByKey(key);
|
|
208
|
+
const state = {
|
|
209
|
+
principalPt,
|
|
210
|
+
principalSy,
|
|
211
|
+
principalShareSupply: tick ? BigInt(tick.principalShareSupply) : 0n,
|
|
212
|
+
};
|
|
213
|
+
virtualStates.set(key, state);
|
|
214
|
+
return state;
|
|
215
|
+
}
|
|
216
|
+
function simulateMintSharesForTickPrestateCalcUsed(params) {
|
|
217
|
+
const { key, dptIn, dsyIn, ticksWrapper, virtualStates } = params;
|
|
218
|
+
const state = getVirtualTickState(key, ticksWrapper, virtualStates);
|
|
219
|
+
const ptBefore = state.principalPt;
|
|
220
|
+
const syBefore = state.principalSy;
|
|
221
|
+
const supply = state.principalShareSupply;
|
|
222
|
+
let minted = 0n;
|
|
223
|
+
let usedPt = 0n;
|
|
224
|
+
let usedSy = 0n;
|
|
225
|
+
const hasPt = dptIn > 0n;
|
|
226
|
+
const hasSy = dsyIn > 0n;
|
|
227
|
+
if (hasPt && hasSy) {
|
|
228
|
+
if (supply === 0n) {
|
|
229
|
+
minted = floorSqrtBigInt(dptIn * dsyIn);
|
|
230
|
+
usedPt = dptIn;
|
|
231
|
+
usedSy = dsyIn;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
if (ptBefore === 0n || syBefore === 0n) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const lpFromPt = (supply * dptIn) / ptBefore;
|
|
238
|
+
const lpFromSy = (supply * dsyIn) / syBefore;
|
|
239
|
+
const usePtSide = lpFromPt < lpFromSy;
|
|
240
|
+
const lpTokensOut = usePtSide ? lpFromPt : lpFromSy;
|
|
241
|
+
minted = lpTokensOut;
|
|
242
|
+
if (minted === 0n) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (usePtSide) {
|
|
246
|
+
usedPt = dptIn;
|
|
247
|
+
usedSy = ceilDivBigInt(syBefore * lpTokensOut, supply);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
usedSy = dsyIn;
|
|
251
|
+
usedPt = ceilDivBigInt(ptBefore * lpTokensOut, supply);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (hasPt && !hasSy) {
|
|
256
|
+
usedSy = 0n;
|
|
257
|
+
if (supply === 0n && ptBefore === 0n) {
|
|
258
|
+
minted = dptIn;
|
|
259
|
+
usedPt = dptIn;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
if (supply === 0n || ptBefore === 0n) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
minted = (supply * dptIn) / ptBefore;
|
|
266
|
+
if (minted === 0n) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
usedPt = dptIn;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (!hasPt && hasSy) {
|
|
273
|
+
usedPt = 0n;
|
|
274
|
+
if (supply === 0n && syBefore === 0n) {
|
|
275
|
+
minted = dsyIn;
|
|
276
|
+
usedSy = dsyIn;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
if (supply === 0n || syBefore === 0n) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
minted = (supply * dsyIn) / syBefore;
|
|
283
|
+
if (minted === 0n) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
usedSy = dsyIn;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
return { minted: 0n, usedPt: 0n, usedSy: 0n };
|
|
291
|
+
}
|
|
292
|
+
if (minted > 0n) {
|
|
293
|
+
state.principalShareSupply += minted;
|
|
294
|
+
}
|
|
295
|
+
if (usedPt > 0n || usedSy > 0n) {
|
|
296
|
+
state.principalPt += usedPt;
|
|
297
|
+
state.principalSy += usedSy;
|
|
298
|
+
ticksWrapper.setPrincipals(key, state.principalPt, state.principalSy);
|
|
299
|
+
}
|
|
300
|
+
return { minted, usedPt, usedSy };
|
|
301
|
+
}
|
|
302
|
+
function simulateAccruePrincipalForDeposit(params) {
|
|
303
|
+
const { ticks, snap, lowerPrice, upperPrice, priceSplitForNeed, splitTickKey, lowerTickKey, upperTickKey, liquidityTarget, originalMaxSy, originalMaxPt, } = params;
|
|
304
|
+
const ticksWrapper = new utilsV2_1.TicksWrapper(ticks);
|
|
305
|
+
ticksWrapper.upsertBoundaryTick(lowerTickKey, snap);
|
|
306
|
+
ticksWrapper.upsertBoundaryTick(upperTickKey, snap);
|
|
307
|
+
const virtualStates = new Map();
|
|
308
|
+
let totalSySpend = 0n;
|
|
309
|
+
let totalPtSpend = 0n;
|
|
310
|
+
let pendingCrossPt = null;
|
|
311
|
+
let pendingCrossSy = null;
|
|
312
|
+
const resolveTraversalStartKey = (candidateKey) => {
|
|
313
|
+
if (ticksWrapper.getTickByKey(candidateKey)) {
|
|
314
|
+
return candidateKey;
|
|
315
|
+
}
|
|
316
|
+
const predecessor = ticksWrapper.predecessorKey(candidateKey);
|
|
317
|
+
return predecessor ?? candidateKey;
|
|
318
|
+
};
|
|
319
|
+
const visitIntervals = (startKey, priceStart, priceEnd, visitor) => {
|
|
320
|
+
if (!(priceStart < priceEnd)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
let currentKey = startKey;
|
|
324
|
+
while (true) {
|
|
325
|
+
const rightKey = ticksWrapper.successorKey(currentKey);
|
|
326
|
+
if (rightKey == null) {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
const leftPrice = ticksWrapper.getSpotPrice(currentKey);
|
|
330
|
+
const rightPrice = ticksWrapper.getSpotPrice(rightKey);
|
|
331
|
+
visitor({ leftKey: currentKey, rightKey, leftPrice, rightPrice });
|
|
332
|
+
if (rightPrice >= priceEnd) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
currentKey = rightKey;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const ptSliceStart = lowerPrice;
|
|
339
|
+
const ptSliceEnd = Math.min(priceSplitForNeed, upperPrice);
|
|
340
|
+
const ptStartKey = resolveTraversalStartKey(lowerTickKey);
|
|
341
|
+
visitIntervals(ptStartKey, ptSliceStart, ptSliceEnd, ({ leftKey, rightKey, leftPrice, rightPrice }) => {
|
|
342
|
+
const segmentStart = Math.max(leftPrice, ptSliceStart);
|
|
343
|
+
const segmentEnd = Math.min(rightPrice, ptSliceEnd);
|
|
344
|
+
const segmentLength = segmentEnd - segmentStart;
|
|
345
|
+
if (!(segmentLength > 0)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed;
|
|
349
|
+
const principalPtDelta = toU64BigInt(Math.ceil(liquidityTarget * segmentLength));
|
|
350
|
+
if (principalPtDelta <= 0n) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (isCrossing) {
|
|
354
|
+
pendingCrossPt = {
|
|
355
|
+
leftKey,
|
|
356
|
+
rightKey,
|
|
357
|
+
ptDelta: principalPtDelta,
|
|
358
|
+
duPart: segmentLength,
|
|
359
|
+
};
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
363
|
+
key: leftKey,
|
|
364
|
+
dptIn: principalPtDelta,
|
|
365
|
+
dsyIn: 0n,
|
|
366
|
+
ticksWrapper,
|
|
367
|
+
virtualStates,
|
|
368
|
+
});
|
|
369
|
+
if (!minted) {
|
|
370
|
+
throw new Error("Deposit too small to mint shares in PT segment");
|
|
371
|
+
}
|
|
372
|
+
totalPtSpend += minted.usedPt;
|
|
373
|
+
totalSySpend += minted.usedSy;
|
|
374
|
+
});
|
|
375
|
+
const sySliceStart = Math.max(priceSplitForNeed, lowerPrice);
|
|
376
|
+
const sySliceEnd = upperPrice;
|
|
377
|
+
const syStartCandidate = splitTickKey >= lowerTickKey ? splitTickKey : lowerTickKey;
|
|
378
|
+
const syStartKey = resolveTraversalStartKey(syStartCandidate);
|
|
379
|
+
visitIntervals(syStartKey, sySliceStart, sySliceEnd, ({ leftKey, rightKey, leftPrice, rightPrice }) => {
|
|
380
|
+
const segmentStart = Math.max(leftPrice, sySliceStart);
|
|
381
|
+
const segmentEnd = Math.min(rightPrice, sySliceEnd);
|
|
382
|
+
if (!(segmentEnd > segmentStart)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const syPerL = snap.getEffectivePrice(segmentStart) - snap.getEffectivePrice(segmentEnd);
|
|
386
|
+
const principalSyDelta = toU64BigInt(Math.ceil(liquidityTarget * syPerL));
|
|
387
|
+
if (principalSyDelta <= 0n) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed;
|
|
391
|
+
if (isCrossing) {
|
|
392
|
+
pendingCrossSy = {
|
|
393
|
+
leftKey,
|
|
394
|
+
rightKey,
|
|
395
|
+
syDelta: principalSyDelta,
|
|
396
|
+
syPerL,
|
|
397
|
+
};
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
401
|
+
key: leftKey,
|
|
402
|
+
dptIn: 0n,
|
|
403
|
+
dsyIn: principalSyDelta,
|
|
404
|
+
ticksWrapper,
|
|
405
|
+
virtualStates,
|
|
406
|
+
});
|
|
407
|
+
if (!minted) {
|
|
408
|
+
throw new Error("Deposit too small to mint shares in SY segment");
|
|
409
|
+
}
|
|
410
|
+
totalPtSpend += minted.usedPt;
|
|
411
|
+
totalSySpend += minted.usedSy;
|
|
412
|
+
});
|
|
413
|
+
if (pendingCrossPt && pendingCrossSy) {
|
|
414
|
+
const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS));
|
|
415
|
+
const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS));
|
|
416
|
+
const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft;
|
|
417
|
+
const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft;
|
|
418
|
+
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
419
|
+
key: pendingCrossPt.leftKey,
|
|
420
|
+
dptIn: scaledPtIn,
|
|
421
|
+
dsyIn: scaledSyIn,
|
|
422
|
+
ticksWrapper,
|
|
423
|
+
virtualStates,
|
|
424
|
+
});
|
|
425
|
+
if (!minted) {
|
|
426
|
+
throw new Error("Deposit too small to mint shares in crossing segment");
|
|
427
|
+
}
|
|
428
|
+
totalPtSpend += minted.usedPt;
|
|
429
|
+
totalSySpend += minted.usedSy;
|
|
430
|
+
}
|
|
431
|
+
const sySpent = Number(totalSySpend);
|
|
432
|
+
const ptSpent = Number(totalPtSpend);
|
|
433
|
+
if (!Number.isSafeInteger(sySpent) || !Number.isSafeInteger(ptSpent)) {
|
|
434
|
+
throw new Error("Token spend exceeds JS safe integer range");
|
|
435
|
+
}
|
|
436
|
+
return { sySpent, ptSpent };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Compute token needs with crossing tick adjustment
|
|
440
|
+
* This matches the Rust compute_token_needs_with_crossing function
|
|
441
|
+
*/
|
|
442
|
+
function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, priceEffUpper, lowerPrice, upperPrice, maxSy, maxPt, epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight) {
|
|
443
|
+
// Below range: SY only
|
|
444
|
+
if (spotPriceCurrent <= lowerPrice) {
|
|
445
|
+
const deltaCTotal = priceEffLower - priceEffUpper;
|
|
446
|
+
const liquidityFromSy = maxSy / deltaCTotal;
|
|
447
|
+
const syNeed = liquidityFromSy * deltaCTotal;
|
|
448
|
+
return [Math.ceil(syNeed), 0];
|
|
449
|
+
}
|
|
450
|
+
// Above range: PT only
|
|
451
|
+
if (spotPriceCurrent >= upperPrice) {
|
|
452
|
+
const duTotal = upperPrice - lowerPrice;
|
|
453
|
+
const liquidityFromPt = maxPt / duTotal;
|
|
454
|
+
const ptNeed = liquidityFromPt * duTotal;
|
|
455
|
+
return [0, Math.ceil(ptNeed)];
|
|
456
|
+
}
|
|
457
|
+
// Inside range: both sides
|
|
458
|
+
const duLeft = Math.max(spotPriceCurrent - lowerPrice, epsilonClamp);
|
|
459
|
+
const priceEffCurrent = snap.getEffectivePrice(spotPriceCurrent);
|
|
460
|
+
const deltaCRight = Math.max(priceEffCurrent - priceEffUpper, epsilonClamp);
|
|
461
|
+
const liquidityFromPt = maxPt / duLeft;
|
|
462
|
+
const liquidityFromSy = maxSy / deltaCRight;
|
|
463
|
+
const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy);
|
|
464
|
+
let ptNeed = liquidityTarget * duLeft;
|
|
465
|
+
let syNeed = liquidityTarget * deltaCRight;
|
|
466
|
+
// Apply crossing tick adjustment if the tick has existing liquidity
|
|
467
|
+
// Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
|
|
468
|
+
const isCrossingTickActive = crossingTickState.principalShareSupply > 0 &&
|
|
469
|
+
crossingTickPriceLeft < spotPriceCurrent &&
|
|
470
|
+
crossingTickPriceRight > spotPriceCurrent;
|
|
471
|
+
if (isCrossingTickActive) {
|
|
472
|
+
// Calculate PT and SY portions that would go into the crossing tick
|
|
473
|
+
// PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
|
|
474
|
+
const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice);
|
|
475
|
+
const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment);
|
|
476
|
+
// SY portion: from spotPriceCurrent to crossingTickPriceRight
|
|
477
|
+
const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft);
|
|
478
|
+
const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice);
|
|
479
|
+
const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd);
|
|
480
|
+
const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0));
|
|
481
|
+
if (crossingPtIntended > 0 && crossingSyIntended > 0) {
|
|
482
|
+
// Tokens already allocated to non-crossing segments before crossing processing.
|
|
483
|
+
const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0);
|
|
484
|
+
const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0);
|
|
485
|
+
// Apply scaling from original max values to prevent double trimming
|
|
486
|
+
const [scaledPtIn, scaledSyIn] = duLeft > 0 && deltaCRight > 0
|
|
487
|
+
? (() => {
|
|
488
|
+
const totalPtSpend = Math.ceil(ptOutsideCrossing);
|
|
489
|
+
const totalSySpend = Math.ceil(syOutsideCrossing);
|
|
490
|
+
const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0);
|
|
491
|
+
const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0);
|
|
492
|
+
// Use max of CLMM-calculated value and scaled original value
|
|
493
|
+
return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)];
|
|
494
|
+
})()
|
|
495
|
+
: [crossingPtIntended, crossingSyIntended];
|
|
496
|
+
// Simulate add_liquidity proportional logic with scaled inputs
|
|
497
|
+
const [usedPt, usedSy] = simulateAddLiquidityProportional(scaledPtIn, scaledSyIn, crossingTickState.principalPt, crossingTickState.principalSy, crossingTickState.principalShareSupply);
|
|
498
|
+
// Adjust needs based on what would actually be used
|
|
499
|
+
ptNeed = ptOutsideCrossing + usedPt;
|
|
500
|
+
syNeed = syOutsideCrossing + usedSy;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return [Math.ceil(syNeed), Math.ceil(ptNeed)];
|
|
504
|
+
}
|
|
505
|
+
exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
|
|
61
506
|
/**
|
|
62
507
|
* Simulate adding liquidity to the CLMM market
|
|
63
508
|
* This is a pure function that does not mutate the market state
|
|
@@ -66,7 +511,7 @@ function simulateAddLiquidity(marketState, args) {
|
|
|
66
511
|
const { financials, configurationOptions, ticks } = marketState;
|
|
67
512
|
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000);
|
|
68
513
|
// Create effective price snapshot
|
|
69
|
-
const snap = new
|
|
514
|
+
const snap = new utilsV2_1.EffSnap((0, utilsV2_1.normalizedTimeRemaining)(secondsRemaining), args.syExchangeRate);
|
|
70
515
|
// Current spot price
|
|
71
516
|
const currentSpot = ticks.currentSpotPrice;
|
|
72
517
|
// Resolve tick prices - convert tick keys (parts per million) to spot prices
|
|
@@ -75,11 +520,29 @@ function simulateAddLiquidity(marketState, args) {
|
|
|
75
520
|
// Precompute effective prices
|
|
76
521
|
const priceEffLower = snap.getEffectivePrice(lowerPrice);
|
|
77
522
|
const priceEffUpper = snap.getEffectivePrice(upperPrice);
|
|
523
|
+
const maxSyForNeeds = Math.max(0, Math.floor(args.maxSy) - GAP_TOKEN_NEEDS);
|
|
524
|
+
const maxPtForNeeds = Math.max(0, Math.floor(args.maxPt) - GAP_TOKEN_NEEDS);
|
|
78
525
|
// Calculate liquidity needs
|
|
79
|
-
const liquidityNeeds = computeLiquidityTargetAndTokenNeeds(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, args.lowerTick, args.upperTick, ticks.currentTick,
|
|
526
|
+
const liquidityNeeds = computeLiquidityTargetAndTokenNeeds(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, args.lowerTick, args.upperTick, ticks.currentTick, maxSyForNeeds, maxPtForNeeds, configurationOptions.epsilonClamp);
|
|
527
|
+
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1];
|
|
528
|
+
const currentTickKey = currentTickNode && currentTickNode.apyBasePoints > 0 ? currentTickNode.apyBasePoints : args.lowerTick;
|
|
529
|
+
const splitTickKey = currentSpot <= lowerPrice ? args.lowerTick : currentSpot >= upperPrice ? args.upperTick : currentTickKey;
|
|
530
|
+
const { sySpent: syNeededWithCrossing, ptSpent: ptNeededWithCrossing } = simulateAccruePrincipalForDeposit({
|
|
531
|
+
ticks,
|
|
532
|
+
snap,
|
|
533
|
+
lowerPrice,
|
|
534
|
+
upperPrice,
|
|
535
|
+
priceSplitForNeed: liquidityNeeds.priceSplitForNeed,
|
|
536
|
+
splitTickKey,
|
|
537
|
+
lowerTickKey: args.lowerTick,
|
|
538
|
+
upperTickKey: args.upperTick,
|
|
539
|
+
liquidityTarget: liquidityNeeds.liquidityTarget,
|
|
540
|
+
originalMaxSy: liquidityNeeds.originalMaxSy,
|
|
541
|
+
originalMaxPt: liquidityNeeds.originalMaxPt,
|
|
542
|
+
});
|
|
80
543
|
// Enforce budgets
|
|
81
|
-
const sySpent =
|
|
82
|
-
const ptSpent =
|
|
544
|
+
const sySpent = syNeededWithCrossing;
|
|
545
|
+
const ptSpent = ptNeededWithCrossing;
|
|
83
546
|
if (sySpent > args.maxSy) {
|
|
84
547
|
throw new Error(`Insufficient SY budget: need ${sySpent}, have ${args.maxSy}`);
|
|
85
548
|
}
|
|
@@ -113,7 +576,7 @@ exports.calculateLpOut = calculateLpOut;
|
|
|
113
576
|
function estimateBalancedDeposit(marketState, targetLiquidity, lowerTickApy, upperTickApy) {
|
|
114
577
|
const { financials, ticks } = marketState;
|
|
115
578
|
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000);
|
|
116
|
-
const snap = new
|
|
579
|
+
const snap = new utilsV2_1.EffSnap((0, utilsV2_1.normalizedTimeRemaining)(secondsRemaining), marketState.currentSyExchangeRate);
|
|
117
580
|
const currentSpot = ticks.currentSpotPrice;
|
|
118
581
|
// Convert APY (in %) to spot price: 1 + apy/100
|
|
119
582
|
const lowerPrice = 1.0 + lowerTickApy / 100;
|
|
@@ -142,7 +605,7 @@ exports.estimateBalancedDeposit = estimateBalancedDeposit;
|
|
|
142
605
|
/** Calculate SY and PT needed to deposit into liquidity pool from base token amount */
|
|
143
606
|
/** Off-chain analogue of on-chain wrapper_provide_liquidity function */
|
|
144
607
|
function calcDepositSyAndPtFromBaseAmount(params) {
|
|
145
|
-
const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount } = params;
|
|
608
|
+
const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount, crossingTickState = EMPTY_CROSSING_TICK_STATE, crossingTickPriceLeft = 0, crossingTickPriceRight = 0, epsilonClamp = 1e-18, } = params;
|
|
146
609
|
if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
|
|
147
610
|
return {
|
|
148
611
|
syNeeded: 0,
|
|
@@ -150,15 +613,17 @@ function calcDepositSyAndPtFromBaseAmount(params) {
|
|
|
150
613
|
};
|
|
151
614
|
}
|
|
152
615
|
const secondsRemaining = Math.max(0, expirationTs);
|
|
153
|
-
const effSnap = new
|
|
616
|
+
const effSnap = new utilsV2_1.EffSnap((0, utilsV2_1.normalizedTimeRemaining)(secondsRemaining), syExchangeRate);
|
|
154
617
|
const priceEffLower = effSnap.getEffectivePrice(lowerPrice);
|
|
155
618
|
const priceEffUpper = effSnap.getEffectivePrice(upperPrice);
|
|
156
619
|
// We mirror the on-chain logic in `wrapper_provide_liquidity`:
|
|
157
|
-
// 1. Use
|
|
158
|
-
// the market "wants" for this price range
|
|
620
|
+
// 1. Use compute_token_needs_with_crossing to infer the ratio of SY/PT
|
|
621
|
+
// the market "wants" for this price range.
|
|
622
|
+
// When crossing tick state is provided, this accounts for the existing
|
|
623
|
+
// PT/SY proportions in the active tick, significantly improving accuracy.
|
|
159
624
|
// 2. Use that ratio plus the current SY exchange rate to decide how much of the
|
|
160
|
-
// minted SY should be stripped into PT
|
|
161
|
-
const
|
|
625
|
+
// minted SY should be stripped into PT (calc_strip_amount).
|
|
626
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 1e9, 1e9, epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight);
|
|
162
627
|
// Total SY the user would get by minting SY from base off-chain
|
|
163
628
|
// (we approximate on-chain `mint_sy_return_data.sy_out_amount`).
|
|
164
629
|
const syAmount = Math.floor(baseTokenAmount * syExchangeRate);
|
|
@@ -189,7 +654,10 @@ exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
|
|
|
189
654
|
* Solution: B = (totalAmountSy * marketPtLiq) / (marketPtLiq + marketSyLiq * curSyRate)
|
|
190
655
|
*/
|
|
191
656
|
function calcStripAmount(totalAmountSy, curSyRate, marketPtLiq, marketSyLiq) {
|
|
192
|
-
const
|
|
657
|
+
const denominator = curSyRate * marketSyLiq + marketPtLiq;
|
|
658
|
+
if (denominator <= 0)
|
|
659
|
+
return 0;
|
|
660
|
+
const toStrip = (totalAmountSy * marketPtLiq) / denominator;
|
|
193
661
|
return Math.ceil(toStrip);
|
|
194
662
|
}
|
|
195
663
|
/**
|
|
@@ -215,7 +683,7 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
|
|
|
215
683
|
// Step 1: Convert base to SY (simulating mint_sy)
|
|
216
684
|
const syAmount = amountBase / syExchangeRate;
|
|
217
685
|
// Step 2: Create effective price snapshot
|
|
218
|
-
const snap = new
|
|
686
|
+
const snap = new utilsV2_1.EffSnap((0, utilsV2_1.normalizedTimeRemaining)(secondsRemaining), syExchangeRate);
|
|
219
687
|
// Current spot price
|
|
220
688
|
const currentSpot = ticks.currentSpotPrice;
|
|
221
689
|
// Convert tick keys to prices using compute_spot_price formula
|
|
@@ -225,24 +693,24 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
|
|
|
225
693
|
// Precompute effective prices
|
|
226
694
|
const priceEffLower = snap.getEffectivePrice(lowerPrice);
|
|
227
695
|
const priceEffUpper = snap.getEffectivePrice(upperPrice);
|
|
228
|
-
// Step 3:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Step 5: Calculate PT and YT from stripping
|
|
696
|
+
// Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
|
|
697
|
+
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks);
|
|
698
|
+
// Step 4: Calculate mock token needs using compute_token_needs_with_crossing
|
|
699
|
+
// max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
|
|
700
|
+
const maxPt = syAmount * syExchangeRate;
|
|
701
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight);
|
|
702
|
+
// Step 5: Calculate how much SY to strip (calc_strip_amount on-chain)
|
|
703
|
+
// Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
|
|
704
|
+
const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock);
|
|
705
|
+
const syToStrip = Math.min(syToStripRaw, syAmount);
|
|
706
|
+
// Step 6: Calculate PT and YT from stripping
|
|
240
707
|
// When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
|
|
241
708
|
const ptFromStrip = syToStrip * syExchangeRate;
|
|
242
709
|
const ytOut = ptFromStrip; // YT amount equals PT amount from strip
|
|
243
|
-
// Step
|
|
244
|
-
|
|
245
|
-
|
|
710
|
+
// Step 7: Calculate remaining SY after strip
|
|
711
|
+
// Use Math.max to ensure non-negative (safety net for floating point edge cases)
|
|
712
|
+
const syRemainder = Math.max(0, syAmount - syToStrip);
|
|
713
|
+
// Step 8: Simulate deposit liquidity with remaining SY and PT
|
|
246
714
|
const depositResult = simulateAddLiquidity(marketState, {
|
|
247
715
|
lowerTick,
|
|
248
716
|
upperTick,
|
|
@@ -266,4 +734,263 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
|
|
|
266
734
|
}
|
|
267
735
|
}
|
|
268
736
|
exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
|
|
737
|
+
/**
|
|
738
|
+
* Simulate swap & supply operation (ixProvideLiquidityBase)
|
|
739
|
+
* 1. Mints SY from base asset
|
|
740
|
+
* 2. Swaps some SY for PT on the market (instead of stripping)
|
|
741
|
+
* 3. Deposits remaining SY + bought PT into liquidity position
|
|
742
|
+
*
|
|
743
|
+
* This approach has lower slippage than minting because it uses market liquidity.
|
|
744
|
+
*
|
|
745
|
+
* @param marketState - Current market state
|
|
746
|
+
* @param amountBase - Amount of base tokens (in lamports)
|
|
747
|
+
* @param lowerTick - Lower tick key (APY in basis points)
|
|
748
|
+
* @param upperTick - Upper tick key (APY in basis points)
|
|
749
|
+
* @param syExchangeRate - SY exchange rate
|
|
750
|
+
* @returns Simulation result with LP out, PT to buy, SY constraint, etc.
|
|
751
|
+
*/
|
|
752
|
+
function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
|
|
753
|
+
try {
|
|
754
|
+
// Wrapper provide-liquidity-base debits base as:
|
|
755
|
+
// base_needed = ceil(total_sy_spent * sy_exchange_rate)
|
|
756
|
+
// So the strict SY budget for a user-provided base input is floor(base / rate).
|
|
757
|
+
const syBudget = convertBaseToSyBudget(amountBase);
|
|
758
|
+
if (syBudget <= 0) {
|
|
759
|
+
return {
|
|
760
|
+
lpOut: 0,
|
|
761
|
+
ptToBuy: 0,
|
|
762
|
+
syConstraint: 0,
|
|
763
|
+
syForSwap: 0,
|
|
764
|
+
syRemainder: 0,
|
|
765
|
+
ptFromSwap: 0,
|
|
766
|
+
syDeposited: 0,
|
|
767
|
+
ptDeposited: 0,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const MOCK_AMOUNT = 1e9;
|
|
771
|
+
const mockNeeds = simulateAddLiquidity(marketState, {
|
|
772
|
+
lowerTick,
|
|
773
|
+
upperTick,
|
|
774
|
+
maxSy: MOCK_AMOUNT,
|
|
775
|
+
maxPt: MOCK_AMOUNT,
|
|
776
|
+
syExchangeRate,
|
|
777
|
+
});
|
|
778
|
+
const searchBudget = Math.max(0, syBudget - SWAP_EXACT_OUT_SY_HEADROOM);
|
|
779
|
+
// Wrapper flow still executes a flash swap even when external_pt_to_buy is 0
|
|
780
|
+
// (effective exact-out target becomes 2 due +2 safety margin).
|
|
781
|
+
let externalPtToBuy = 0;
|
|
782
|
+
if (syBudget > 0 && mockNeeds.ptSpent > 0) {
|
|
783
|
+
if (mockNeeds.sySpent > 0) {
|
|
784
|
+
// Normal case: position range includes current spot price
|
|
785
|
+
// Calculate optimal split between swap and deposit
|
|
786
|
+
const denominator = syExchangeRate * mockNeeds.sySpent + mockNeeds.ptSpent;
|
|
787
|
+
const syToSwapGuess = denominator > 0 ? Math.ceil((syBudget * mockNeeds.ptSpent) / denominator) : 0;
|
|
788
|
+
if (syToSwapGuess > 0) {
|
|
789
|
+
const guessSwap = (0, swapV2_1.simulateSwap)(marketState, {
|
|
790
|
+
direction: types_1.SwapDirection.SyToPt,
|
|
791
|
+
amountIn: syToSwapGuess,
|
|
792
|
+
syExchangeRate,
|
|
793
|
+
isCurrentFlashSwap: true,
|
|
794
|
+
});
|
|
795
|
+
externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
// Below-range case: position only needs PT (sySpent = 0)
|
|
800
|
+
// Swap all SY to PT
|
|
801
|
+
const guessSwap = (0, swapV2_1.simulateSwap)(marketState, {
|
|
802
|
+
direction: types_1.SwapDirection.SyToPt,
|
|
803
|
+
amountIn: syBudget,
|
|
804
|
+
syExchangeRate,
|
|
805
|
+
isCurrentFlashSwap: true,
|
|
806
|
+
});
|
|
807
|
+
externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const candidateCache = new Map();
|
|
811
|
+
const evaluateCandidate = (ptConstraint) => {
|
|
812
|
+
const key = Math.max(0, Math.floor(ptConstraint));
|
|
813
|
+
const cached = candidateCache.get(key);
|
|
814
|
+
if (cached !== undefined) {
|
|
815
|
+
return cached;
|
|
816
|
+
}
|
|
817
|
+
const candidate = simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, key, syExchangeRate);
|
|
818
|
+
candidateCache.set(key, candidate);
|
|
819
|
+
return candidate;
|
|
820
|
+
};
|
|
821
|
+
const ptCap = 1_000_000_000;
|
|
822
|
+
let bestUnderBudget = null;
|
|
823
|
+
let leastOverspend = null;
|
|
824
|
+
const considerCandidate = (candidate) => {
|
|
825
|
+
if (!candidate) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (candidate.totalSySpent <= searchBudget) {
|
|
829
|
+
if (!bestUnderBudget ||
|
|
830
|
+
candidate.totalSySpent > bestUnderBudget.totalSySpent ||
|
|
831
|
+
(candidate.totalSySpent === bestUnderBudget.totalSySpent && candidate.lpOut > bestUnderBudget.lpOut)) {
|
|
832
|
+
bestUnderBudget = candidate;
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!leastOverspend ||
|
|
837
|
+
candidate.totalSySpent < leastOverspend.totalSySpent ||
|
|
838
|
+
(candidate.totalSySpent === leastOverspend.totalSySpent && candidate.lpOut > leastOverspend.lpOut)) {
|
|
839
|
+
leastOverspend = candidate;
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
// Always evaluate from zero to establish a valid lower bound.
|
|
843
|
+
considerCandidate(evaluateCandidate(0));
|
|
844
|
+
let lowPt = 0;
|
|
845
|
+
let highPt = Math.max(1, externalPtToBuy);
|
|
846
|
+
let highCandidate = evaluateCandidate(highPt);
|
|
847
|
+
considerCandidate(highCandidate);
|
|
848
|
+
if (highCandidate && highCandidate.totalSySpent <= searchBudget) {
|
|
849
|
+
lowPt = highPt;
|
|
850
|
+
}
|
|
851
|
+
// Expand upward until we hit over-budget/null region so binary search can maximize usage.
|
|
852
|
+
for (let i = 0; i < 18 && highPt < ptCap; i++) {
|
|
853
|
+
if (!highCandidate || highCandidate.totalSySpent > searchBudget) {
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
const nextHigh = Math.min(ptCap, highPt * 2);
|
|
857
|
+
if (nextHigh === highPt) {
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
highPt = nextHigh;
|
|
861
|
+
highCandidate = evaluateCandidate(highPt);
|
|
862
|
+
considerCandidate(highCandidate);
|
|
863
|
+
if (highCandidate && highCandidate.totalSySpent <= searchBudget) {
|
|
864
|
+
lowPt = highPt;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Binary search the feasibility boundary.
|
|
868
|
+
let searchLow = Math.max(0, lowPt + 1);
|
|
869
|
+
let searchHigh = highPt;
|
|
870
|
+
while (searchLow <= searchHigh) {
|
|
871
|
+
const mid = Math.floor((searchLow + searchHigh) / 2);
|
|
872
|
+
const candidate = evaluateCandidate(mid);
|
|
873
|
+
considerCandidate(candidate);
|
|
874
|
+
if (candidate && candidate.totalSySpent <= searchBudget) {
|
|
875
|
+
searchLow = mid + 1;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
searchHigh = mid - 1;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
// Local sweep around the boundary to handle small non-monotonic steps.
|
|
882
|
+
const localRadius = 16;
|
|
883
|
+
const localStart = Math.max(0, searchHigh - localRadius);
|
|
884
|
+
const localEnd = Math.min(ptCap, searchHigh + localRadius);
|
|
885
|
+
for (let pt = localStart; pt <= localEnd; pt++) {
|
|
886
|
+
considerCandidate(evaluateCandidate(pt));
|
|
887
|
+
}
|
|
888
|
+
const selected = bestUnderBudget ?? leastOverspend;
|
|
889
|
+
if (!selected) {
|
|
890
|
+
throw new Error("Unable to simulate swap & supply for initial PT constraint");
|
|
891
|
+
}
|
|
892
|
+
if (process.env.DEBUG_SWAP_SUPPLY === "1") {
|
|
893
|
+
console.log("[simulateSwapAndSupply]", {
|
|
894
|
+
syBudget,
|
|
895
|
+
searchBudget,
|
|
896
|
+
mockNeeds,
|
|
897
|
+
externalPtToBuy,
|
|
898
|
+
selected,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
lpOut: selected.lpOut,
|
|
903
|
+
ptToBuy: selected.ptConstraint,
|
|
904
|
+
syConstraint: syBudget,
|
|
905
|
+
syForSwap: selected.tradeSySpent,
|
|
906
|
+
syRemainder: selected.syRemainderAfterSwap,
|
|
907
|
+
ptFromSwap: selected.tradePtOut,
|
|
908
|
+
syDeposited: selected.depositSySpent,
|
|
909
|
+
ptDeposited: selected.depositPtSpent,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
console.error("[simulateSwapAndSupply] Error:", error);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
exports.simulateSwapAndSupply = simulateSwapAndSupply;
|
|
918
|
+
function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate) {
|
|
919
|
+
let swapResult;
|
|
920
|
+
try {
|
|
921
|
+
swapResult = simulateBuyPtExactOutWrapper(marketState, syBudget, ptConstraint, syExchangeRate);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
// Budget insufficient for this PT constraint
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
const depositState = swapResult.postMarketState ?? marketState;
|
|
928
|
+
const syAvailableForDeposit = Math.max(0, syBudget - swapResult.sySpent);
|
|
929
|
+
const depositResult = simulateAddLiquidity(depositState, {
|
|
930
|
+
lowerTick,
|
|
931
|
+
upperTick,
|
|
932
|
+
maxSy: syBudget,
|
|
933
|
+
maxPt: swapResult.ptOut,
|
|
934
|
+
syExchangeRate,
|
|
935
|
+
});
|
|
936
|
+
// Swap & supply must actually add liquidity.
|
|
937
|
+
// Discard candidates that end up as swap-only (deltaL == 0).
|
|
938
|
+
if (depositResult.deltaL <= 0) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const ptRemainder = Math.max(0, swapResult.ptOut - depositResult.ptSpent);
|
|
942
|
+
if (ptRemainder > 1_000) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
const totalSySpent = swapResult.sySpent + depositResult.sySpent;
|
|
946
|
+
return {
|
|
947
|
+
ptConstraint,
|
|
948
|
+
tradeSySpent: swapResult.sySpent,
|
|
949
|
+
tradePtOut: swapResult.ptOut,
|
|
950
|
+
depositSySpent: depositResult.sySpent,
|
|
951
|
+
depositPtSpent: depositResult.ptSpent,
|
|
952
|
+
lpOut: depositResult.deltaL,
|
|
953
|
+
totalSySpent,
|
|
954
|
+
syRemainderAfterSwap: Math.max(0, syBudget - swapResult.sySpent),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function convertBaseToSyBudget(amountBase) {
|
|
958
|
+
const baseAmount = Math.max(0, Math.floor(amountBase));
|
|
959
|
+
return baseAmount;
|
|
960
|
+
}
|
|
961
|
+
function simulateBuyPtExactOutWrapper(marketState, syBudget, ptConstraint, syExchangeRate) {
|
|
962
|
+
const maxSyBudget = Math.max(0, Math.floor(syBudget));
|
|
963
|
+
const effectivePtOutTarget = Math.max(0, Math.floor(ptConstraint)) + 2;
|
|
964
|
+
if (maxSyBudget === 0) {
|
|
965
|
+
return {
|
|
966
|
+
sySpent: 0,
|
|
967
|
+
ptOut: 0,
|
|
968
|
+
postMarketState: marketState,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const tryExactOut = (targetPtOut) => {
|
|
972
|
+
return (0, swapV2_1.simulateSwapExactOut)(marketState, {
|
|
973
|
+
direction: types_1.SwapDirection.SyToPt,
|
|
974
|
+
amountOut: targetPtOut,
|
|
975
|
+
syExchangeRate,
|
|
976
|
+
isCurrentFlashSwap: true,
|
|
977
|
+
amountInConstraint: maxSyBudget,
|
|
978
|
+
});
|
|
979
|
+
};
|
|
980
|
+
// WrapperProvideLiquidityBase always executes exact-out with `pt_constraint + 2`.
|
|
981
|
+
// Do not fallback to lower exact-out targets here, otherwise client simulation can
|
|
982
|
+
// accept candidates that deterministically fail on-chain with InsufficientBudgetSY.
|
|
983
|
+
let bestQuote;
|
|
984
|
+
try {
|
|
985
|
+
bestQuote = tryExactOut(effectivePtOutTarget);
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
throw new Error(`Insufficient SY budget for wrapper exact-out simulation (budget=${maxSyBudget}, pt_constraint=${ptConstraint})`);
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
sySpent: bestQuote.amountInConsumed,
|
|
992
|
+
ptOut: bestQuote.amountOut,
|
|
993
|
+
postMarketState: bestQuote.postMarketState,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
269
996
|
//# sourceMappingURL=addLiquidity.js.map
|