@exponent-labs/market-three-math 0.9.15 → 0.9.17
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 +11 -41
- package/build/addLiquidity.js +169 -201
- package/build/addLiquidity.js.map +1 -1
- package/build/bisect.js +1 -2
- package/build/bisect.js.map +1 -1
- package/build/existingPositionEqualization.d.ts +3 -3
- package/build/existingPositionEqualization.js +45 -66
- package/build/existingPositionEqualization.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +1 -3
- package/build/index.js.map +1 -1
- package/build/liquidityHistogram.js +2 -3
- package/build/liquidityHistogram.js.map +1 -1
- package/build/quote.js +2 -2
- package/build/quote.js.map +1 -1
- package/build/swap.js +2 -2
- package/build/swap.js.map +1 -1
- package/build/swapV2.js +56 -20
- package/build/swapV2.js.map +1 -1
- package/build/types.d.ts +4 -21
- package/build/utils.js +17 -17
- package/build/utils.js.map +1 -1
- package/build/utilsV2.js +10 -7
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.d.ts +1 -1
- package/build/withdrawLiquidity.js +120 -72
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.js +3 -4
- 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 +203 -246
- package/src/existingPositionEqualization.test.ts +33 -0
- package/src/existingPositionEqualization.ts +52 -83
- package/src/index.ts +0 -4
- package/src/swap.ts +1 -0
- package/src/swapV2.ts +96 -18
- package/src/types.ts +4 -23
- package/src/utilsV2.ts +9 -4
- package/src/withdrawLiquidity.test.ts +189 -0
- package/src/withdrawLiquidity.ts +148 -89
package/build/addLiquidity.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.computeLiquidityTargetAndTokenNeeds = computeLiquidityTargetAndTokenNeeds;
|
|
4
|
+
exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
|
|
5
|
+
exports.simulateAddLiquidity = simulateAddLiquidity;
|
|
6
|
+
exports.calculateLpOut = calculateLpOut;
|
|
7
|
+
exports.estimateBalancedDeposit = estimateBalancedDeposit;
|
|
8
|
+
exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
|
|
9
|
+
exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
|
|
10
|
+
exports.simulateSwapAndSupply = simulateSwapAndSupply;
|
|
11
|
+
const existingPositionEqualization_1 = require("./existingPositionEqualization");
|
|
4
12
|
const swapV2_1 = require("./swapV2");
|
|
5
13
|
const types_1 = require("./types");
|
|
6
|
-
const utils_1 = require("./utils");
|
|
7
14
|
const utilsV2_1 = require("./utilsV2");
|
|
8
15
|
const TICK_KEY_BASE_POINTS = 1_000_000;
|
|
9
16
|
// Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
|
|
@@ -12,45 +19,13 @@ const GAP_TOKEN_NEEDS = 20;
|
|
|
12
19
|
const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100;
|
|
13
20
|
const SWAP_EXACT_OUT_SY_HEADROOM = 100;
|
|
14
21
|
const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000;
|
|
15
|
-
const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000;
|
|
16
22
|
const FLASH_SY_POSITION_HEADROOM = 1_000;
|
|
17
23
|
const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000;
|
|
18
24
|
const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0;
|
|
19
25
|
const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000;
|
|
20
26
|
const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
principalSy: 0,
|
|
24
|
-
principalShareSupply: 0,
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Get crossing tick state and price boundaries from Ticks.
|
|
28
|
-
* Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
|
|
29
|
-
*/
|
|
30
|
-
function getCrossingTickStateFromTicks(ticks) {
|
|
31
|
-
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1];
|
|
32
|
-
if (!currentTickNode || ticks.currentTick <= 0) {
|
|
33
|
-
return {
|
|
34
|
-
crossingTickState: EMPTY_CROSSING_TICK_STATE,
|
|
35
|
-
crossingTickPriceLeft: 0,
|
|
36
|
-
crossingTickPriceRight: 0,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const crossingTickPriceLeft = currentTickNode.impliedRate;
|
|
40
|
-
const successorKey = (0, utils_1.getSuccessorTickKey)(ticks, currentTickNode.apyBasePoints);
|
|
41
|
-
const successorTick = successorKey != null ? (0, utils_1.findTickByKey)(ticks, successorKey)?.tick : null;
|
|
42
|
-
const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY;
|
|
43
|
-
return {
|
|
44
|
-
crossingTickState: {
|
|
45
|
-
principalPt: Number(currentTickNode.principalPt),
|
|
46
|
-
principalSy: Number(currentTickNode.principalSy),
|
|
47
|
-
principalShareSupply: Number(currentTickNode.principalShareSupply),
|
|
48
|
-
},
|
|
49
|
-
crossingTickPriceLeft,
|
|
50
|
-
crossingTickPriceRight,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
exports.getCrossingTickStateFromTicks = getCrossingTickStateFromTicks;
|
|
27
|
+
const ZERO_BIGINT = 0n;
|
|
28
|
+
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
54
29
|
/**
|
|
55
30
|
* Compute liquidity target and token needs based on position range and budgets
|
|
56
31
|
* Ported from compute_liquidity_target_and_token_needs in math.rs
|
|
@@ -70,11 +45,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
70
45
|
ptNeeded: 0,
|
|
71
46
|
priceSplitForNeed: lowerPrice,
|
|
72
47
|
priceSplitTickIdx: lowerTickIdx,
|
|
73
|
-
// No crossing possible when below range
|
|
74
|
-
originalMaxSy: maxSy,
|
|
75
|
-
originalMaxPt: maxPt,
|
|
76
|
-
duLeftTotal: 0,
|
|
77
|
-
deltaCRightTotal: deltaCTotal,
|
|
78
48
|
};
|
|
79
49
|
}
|
|
80
50
|
else if (spotPriceCurrent >= upperPrice) {
|
|
@@ -89,11 +59,6 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
89
59
|
ptNeeded: Math.floor(ptNeed),
|
|
90
60
|
priceSplitForNeed: upperPrice,
|
|
91
61
|
priceSplitTickIdx: upperTickIdx,
|
|
92
|
-
// No crossing possible when above range
|
|
93
|
-
originalMaxSy: maxSy,
|
|
94
|
-
originalMaxPt: maxPt,
|
|
95
|
-
duLeftTotal: duTotal,
|
|
96
|
-
deltaCRightTotal: 0,
|
|
97
62
|
};
|
|
98
63
|
}
|
|
99
64
|
else {
|
|
@@ -112,77 +77,29 @@ function computeLiquidityTargetAndTokenNeeds(snap, spotPriceCurrent, priceEffLow
|
|
|
112
77
|
ptNeeded: Math.floor(ptNeed),
|
|
113
78
|
priceSplitForNeed: spotPriceCurrent,
|
|
114
79
|
priceSplitTickIdx: currentIndex,
|
|
115
|
-
// Store for crossing tick scaling
|
|
116
|
-
originalMaxSy: maxSy,
|
|
117
|
-
originalMaxPt: maxPt,
|
|
118
|
-
duLeftTotal: duLeft,
|
|
119
|
-
deltaCRightTotal: deltaCRight,
|
|
120
80
|
};
|
|
121
81
|
}
|
|
122
82
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* Scale crossing tick token inputs from original max values to prevent double trimming.
|
|
126
|
-
*
|
|
127
|
-
* When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
|
|
128
|
-
* 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
|
|
129
|
-
* 2. Then by AMM formula in simulateAddLiquidityProportional
|
|
130
|
-
*
|
|
131
|
-
* This function calculates the segment's proportion of the total distribution and scales
|
|
132
|
-
* the original max values accordingly, ensuring maximum token utilization.
|
|
133
|
-
*
|
|
134
|
-
* @param scaleParams - Original max values and CLMM distribution extents
|
|
135
|
-
* @param duPtPart - PT segment length (spot price delta for this segment)
|
|
136
|
-
* @param syPerL1 - SY per unit liquidity for this segment
|
|
137
|
-
* @param clmmPtDelta - PT amount calculated from CLMM formula
|
|
138
|
-
* @param clmmSyDelta - SY amount calculated from CLMM formula
|
|
139
|
-
* @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
|
|
140
|
-
*/
|
|
141
|
-
function scaleCrossingTickInputs(scaleParams, duPtPart, syPerL1, clmmPtDelta, clmmSyDelta) {
|
|
142
|
-
const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams;
|
|
143
|
-
if (duLeftTotal > 0 && deltaCRightTotal > 0) {
|
|
144
|
-
// Calculate segment's proportion of total distribution
|
|
145
|
-
const ptSegmentRatio = duPtPart / duLeftTotal;
|
|
146
|
-
const sySegmentRatio = syPerL1 / deltaCRightTotal;
|
|
147
|
-
// Scale original max values by segment proportion
|
|
148
|
-
const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio);
|
|
149
|
-
const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio);
|
|
150
|
-
// Use max of CLMM-calculated value and scaled original value
|
|
151
|
-
// This ensures we don't lose tokens due to double trimming
|
|
152
|
-
return [Math.max(clmmPtDelta, scaledPt), Math.max(clmmSyDelta, scaledSy)];
|
|
153
|
-
}
|
|
154
|
-
// Fallback to original logic if no crossing scale params
|
|
155
|
-
return [clmmPtDelta, clmmSyDelta];
|
|
83
|
+
function toU64BigInt(value) {
|
|
84
|
+
return BigInt(Math.max(0, Math.floor(value)));
|
|
156
85
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*/
|
|
162
|
-
function simulateAddLiquidityProportional(intentPt, intentSy, marketTotalPt, marketTotalSy, marketTotalLp) {
|
|
163
|
-
if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
|
|
164
|
-
// Empty tick - use all intended amounts
|
|
165
|
-
return [intentPt, intentSy];
|
|
86
|
+
function lamportsNumberToBigInt(value, label) {
|
|
87
|
+
const lamports = Math.max(0, Math.floor(value));
|
|
88
|
+
if (!Number.isSafeInteger(lamports)) {
|
|
89
|
+
throw new Error(`${label} exceeds JS safe integer range: ${value}`);
|
|
166
90
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const usedPt = intentPt;
|
|
173
|
-
const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp);
|
|
174
|
-
return [usedPt, usedSy];
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
// SY is the limiting factor
|
|
178
|
-
const lpTokensOut = lpFromSy;
|
|
179
|
-
const usedSy = intentSy;
|
|
180
|
-
const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp);
|
|
181
|
-
return [usedPt, usedSy];
|
|
91
|
+
return BigInt(lamports);
|
|
92
|
+
}
|
|
93
|
+
function safeLamportsBigIntToNumber(value, label) {
|
|
94
|
+
if (value < ZERO_BIGINT || value > MAX_SAFE_INTEGER_BIGINT) {
|
|
95
|
+
throw new Error(`${label} exceeds JS safe integer range: ${value.toString()}`);
|
|
182
96
|
}
|
|
97
|
+
return Number(value);
|
|
183
98
|
}
|
|
184
|
-
function
|
|
185
|
-
|
|
99
|
+
function finalizeEqualizedSpend(params) {
|
|
100
|
+
const grossSpend = lamportsNumberToBigInt(params.simulatedSpend, params.label) + params.fixedSpent;
|
|
101
|
+
const finalSpend = grossSpend > params.fixedReleased ? grossSpend - params.fixedReleased : ZERO_BIGINT;
|
|
102
|
+
return safeLamportsBigIntToNumber(finalSpend, params.label);
|
|
186
103
|
}
|
|
187
104
|
function ceilDivBigInt(numerator, denominator) {
|
|
188
105
|
if (denominator <= 0n) {
|
|
@@ -217,6 +134,78 @@ function getVirtualTickState(key, ticksWrapper, virtualStates) {
|
|
|
217
134
|
virtualStates.set(key, state);
|
|
218
135
|
return state;
|
|
219
136
|
}
|
|
137
|
+
function ceilNormalizedPrincipal(value, cap) {
|
|
138
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
139
|
+
throw new Error("Invalid active tick normalization value");
|
|
140
|
+
}
|
|
141
|
+
const capNumber = Number(cap);
|
|
142
|
+
if (value >= capNumber) {
|
|
143
|
+
return cap;
|
|
144
|
+
}
|
|
145
|
+
return BigInt(Math.min(Math.ceil(value), capNumber));
|
|
146
|
+
}
|
|
147
|
+
function computeNormalizedActiveTickPrincipals(params) {
|
|
148
|
+
const { principalPt, principalSy, ptPerL, syPerL } = params;
|
|
149
|
+
if (!Number.isFinite(ptPerL) || !Number.isFinite(syPerL) || ptPerL < 0 || syPerL < 0) {
|
|
150
|
+
throw new Error("Invalid active tick normalization ratio");
|
|
151
|
+
}
|
|
152
|
+
if (ptPerL === 0 && syPerL === 0) {
|
|
153
|
+
return { normalizedPt: 0n, normalizedSy: 0n };
|
|
154
|
+
}
|
|
155
|
+
if (ptPerL === 0) {
|
|
156
|
+
return { normalizedPt: 0n, normalizedSy: principalSy };
|
|
157
|
+
}
|
|
158
|
+
if (syPerL === 0) {
|
|
159
|
+
return { normalizedPt: principalPt, normalizedSy: 0n };
|
|
160
|
+
}
|
|
161
|
+
const lFromPt = Number(principalPt) / ptPerL;
|
|
162
|
+
const lFromSy = Number(principalSy) / syPerL;
|
|
163
|
+
if (!Number.isFinite(lFromPt) || !Number.isFinite(lFromSy)) {
|
|
164
|
+
throw new Error("Invalid active tick normalization liquidity");
|
|
165
|
+
}
|
|
166
|
+
if (lFromPt <= lFromSy) {
|
|
167
|
+
return {
|
|
168
|
+
normalizedPt: principalPt,
|
|
169
|
+
normalizedSy: ceilNormalizedPrincipal(lFromPt * syPerL, principalSy),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
normalizedPt: ceilNormalizedPrincipal(lFromSy * ptPerL, principalPt),
|
|
174
|
+
normalizedSy: principalSy,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey) {
|
|
178
|
+
if (ticksWrapper.currentPrefixSum === 0n) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const currentTickKey = ticksWrapper.currentTickKey;
|
|
182
|
+
if (lowerTickKey > currentTickKey || currentTickKey >= upperTickKey) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const rightTickKey = ticksWrapper.successorKey(currentTickKey);
|
|
186
|
+
if (rightTickKey == null) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const spotPrice = ticksWrapper.currentSpotPrice;
|
|
190
|
+
const leftPrice = ticksWrapper.getSpotPrice(currentTickKey);
|
|
191
|
+
const rightPrice = ticksWrapper.getSpotPrice(rightTickKey);
|
|
192
|
+
if (spotPrice < leftPrice || spotPrice > rightPrice) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentTickKey);
|
|
196
|
+
if (principalPt === 0n && principalSy === 0n) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const ptPerL = Math.max(spotPrice - leftPrice, 0);
|
|
200
|
+
const syPerL = spotPrice >= rightPrice ? 0 : snap.getEffectivePrice(spotPrice) - snap.getEffectivePrice(rightPrice);
|
|
201
|
+
const { normalizedPt, normalizedSy } = computeNormalizedActiveTickPrincipals({
|
|
202
|
+
principalPt,
|
|
203
|
+
principalSy,
|
|
204
|
+
ptPerL,
|
|
205
|
+
syPerL,
|
|
206
|
+
});
|
|
207
|
+
ticksWrapper.setPrincipals(currentTickKey, normalizedPt, normalizedSy);
|
|
208
|
+
}
|
|
220
209
|
function simulateMintSharesForTickPrestateCalcUsed(params) {
|
|
221
210
|
const { key, dptIn, dsyIn, ticksWrapper, virtualStates } = params;
|
|
222
211
|
const state = getVirtualTickState(key, ticksWrapper, virtualStates);
|
|
@@ -304,10 +293,11 @@ function simulateMintSharesForTickPrestateCalcUsed(params) {
|
|
|
304
293
|
return { minted, usedPt, usedSy };
|
|
305
294
|
}
|
|
306
295
|
function simulateAccruePrincipalForDeposit(params) {
|
|
307
|
-
const { ticks, snap, lowerPrice, upperPrice, priceSplitForNeed, splitTickKey, lowerTickKey, upperTickKey, liquidityTarget,
|
|
296
|
+
const { ticks, snap, lowerPrice, upperPrice, priceSplitForNeed, splitTickKey, lowerTickKey, upperTickKey, liquidityTarget, } = params;
|
|
308
297
|
const ticksWrapper = new utilsV2_1.TicksWrapper(ticks);
|
|
309
298
|
ticksWrapper.upsertBoundaryTick(lowerTickKey, snap);
|
|
310
299
|
ticksWrapper.upsertBoundaryTick(upperTickKey, snap);
|
|
300
|
+
normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey);
|
|
311
301
|
const virtualStates = new Map();
|
|
312
302
|
let totalSySpend = 0n;
|
|
313
303
|
let totalPtSpend = 0n;
|
|
@@ -357,9 +347,7 @@ function simulateAccruePrincipalForDeposit(params) {
|
|
|
357
347
|
if (isCrossing) {
|
|
358
348
|
pendingCrossPt = {
|
|
359
349
|
leftKey,
|
|
360
|
-
rightKey,
|
|
361
350
|
ptDelta: principalPtDelta,
|
|
362
|
-
duPart: segmentLength,
|
|
363
351
|
};
|
|
364
352
|
return;
|
|
365
353
|
}
|
|
@@ -394,10 +382,7 @@ function simulateAccruePrincipalForDeposit(params) {
|
|
|
394
382
|
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed;
|
|
395
383
|
if (isCrossing) {
|
|
396
384
|
pendingCrossSy = {
|
|
397
|
-
leftKey,
|
|
398
|
-
rightKey,
|
|
399
385
|
syDelta: principalSyDelta,
|
|
400
|
-
syPerL,
|
|
401
386
|
};
|
|
402
387
|
return;
|
|
403
388
|
}
|
|
@@ -415,14 +400,10 @@ function simulateAccruePrincipalForDeposit(params) {
|
|
|
415
400
|
totalSySpend += minted.usedSy;
|
|
416
401
|
});
|
|
417
402
|
if (pendingCrossPt && pendingCrossSy) {
|
|
418
|
-
const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS));
|
|
419
|
-
const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS));
|
|
420
|
-
const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft;
|
|
421
|
-
const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft;
|
|
422
403
|
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
423
404
|
key: pendingCrossPt.leftKey,
|
|
424
|
-
dptIn:
|
|
425
|
-
dsyIn:
|
|
405
|
+
dptIn: pendingCrossPt.ptDelta,
|
|
406
|
+
dsyIn: pendingCrossSy.syDelta,
|
|
426
407
|
ticksWrapper,
|
|
427
408
|
virtualStates,
|
|
428
409
|
});
|
|
@@ -439,11 +420,8 @@ function simulateAccruePrincipalForDeposit(params) {
|
|
|
439
420
|
}
|
|
440
421
|
return { sySpent, ptSpent };
|
|
441
422
|
}
|
|
442
|
-
/**
|
|
443
|
-
|
|
444
|
-
* This matches the Rust compute_token_needs_with_crossing function
|
|
445
|
-
*/
|
|
446
|
-
function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, priceEffUpper, lowerPrice, upperPrice, maxSy, maxPt, epsilonClamp, crossingTickState, crossingTickPriceLeft, crossingTickPriceRight) {
|
|
423
|
+
/** Compute curve-level token needs without raw crossing-tick cap scaling. */
|
|
424
|
+
function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, priceEffUpper, lowerPrice, upperPrice, maxSy, maxPt, epsilonClamp) {
|
|
447
425
|
// Below range: SY only
|
|
448
426
|
if (spotPriceCurrent <= lowerPrice) {
|
|
449
427
|
const deltaCTotal = priceEffLower - priceEffUpper;
|
|
@@ -465,48 +443,10 @@ function computeTokenNeedsWithCrossing(snap, spotPriceCurrent, priceEffLower, pr
|
|
|
465
443
|
const liquidityFromPt = maxPt / duLeft;
|
|
466
444
|
const liquidityFromSy = maxSy / deltaCRight;
|
|
467
445
|
const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// Apply crossing tick adjustment if the tick has existing liquidity
|
|
471
|
-
// Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
|
|
472
|
-
const isCrossingTickActive = crossingTickState.principalShareSupply > 0 &&
|
|
473
|
-
crossingTickPriceLeft < spotPriceCurrent &&
|
|
474
|
-
crossingTickPriceRight > spotPriceCurrent;
|
|
475
|
-
if (isCrossingTickActive) {
|
|
476
|
-
// Calculate PT and SY portions that would go into the crossing tick
|
|
477
|
-
// PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
|
|
478
|
-
const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice);
|
|
479
|
-
const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment);
|
|
480
|
-
// SY portion: from spotPriceCurrent to crossingTickPriceRight
|
|
481
|
-
const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft);
|
|
482
|
-
const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice);
|
|
483
|
-
const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd);
|
|
484
|
-
const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0));
|
|
485
|
-
if (crossingPtIntended > 0 && crossingSyIntended > 0) {
|
|
486
|
-
// Tokens already allocated to non-crossing segments before crossing processing.
|
|
487
|
-
const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0);
|
|
488
|
-
const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0);
|
|
489
|
-
// Apply scaling from original max values to prevent double trimming
|
|
490
|
-
const [scaledPtIn, scaledSyIn] = duLeft > 0 && deltaCRight > 0
|
|
491
|
-
? (() => {
|
|
492
|
-
const totalPtSpend = Math.ceil(ptOutsideCrossing);
|
|
493
|
-
const totalSySpend = Math.ceil(syOutsideCrossing);
|
|
494
|
-
const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0);
|
|
495
|
-
const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0);
|
|
496
|
-
// Use max of CLMM-calculated value and scaled original value
|
|
497
|
-
return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)];
|
|
498
|
-
})()
|
|
499
|
-
: [crossingPtIntended, crossingSyIntended];
|
|
500
|
-
// Simulate add_liquidity proportional logic with scaled inputs
|
|
501
|
-
const [usedPt, usedSy] = simulateAddLiquidityProportional(scaledPtIn, scaledSyIn, crossingTickState.principalPt, crossingTickState.principalSy, crossingTickState.principalShareSupply);
|
|
502
|
-
// Adjust needs based on what would actually be used
|
|
503
|
-
ptNeed = ptOutsideCrossing + usedPt;
|
|
504
|
-
syNeed = syOutsideCrossing + usedSy;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
446
|
+
const ptNeed = liquidityTarget * duLeft;
|
|
447
|
+
const syNeed = liquidityTarget * deltaCRight;
|
|
507
448
|
return [Math.ceil(syNeed), Math.ceil(ptNeed)];
|
|
508
449
|
}
|
|
509
|
-
exports.computeTokenNeedsWithCrossing = computeTokenNeedsWithCrossing;
|
|
510
450
|
/**
|
|
511
451
|
* Simulate adding liquidity to the CLMM market
|
|
512
452
|
* This is a pure function that does not mutate the market state
|
|
@@ -541,8 +481,6 @@ function simulateAddLiquidity(marketState, args) {
|
|
|
541
481
|
lowerTickKey: args.lowerTick,
|
|
542
482
|
upperTickKey: args.upperTick,
|
|
543
483
|
liquidityTarget: liquidityNeeds.liquidityTarget,
|
|
544
|
-
originalMaxSy: liquidityNeeds.originalMaxSy,
|
|
545
|
-
originalMaxPt: liquidityNeeds.originalMaxPt,
|
|
546
484
|
});
|
|
547
485
|
// Enforce budgets
|
|
548
486
|
const sySpent = syNeededWithCrossing;
|
|
@@ -559,7 +497,37 @@ function simulateAddLiquidity(marketState, args) {
|
|
|
559
497
|
ptSpent,
|
|
560
498
|
};
|
|
561
499
|
}
|
|
562
|
-
|
|
500
|
+
function simulateAddLiquidityForPosition(marketState, args, existingPosition) {
|
|
501
|
+
if (!existingPosition) {
|
|
502
|
+
return simulateAddLiquidity(marketState, args);
|
|
503
|
+
}
|
|
504
|
+
const budgetEffect = (0, existingPositionEqualization_1.computeExistingPositionBudgetEffect)({
|
|
505
|
+
ticks: marketState.ticks,
|
|
506
|
+
position: existingPosition,
|
|
507
|
+
userMaxSy: lamportsNumberToBigInt(args.maxSy, "user max SY"),
|
|
508
|
+
userMaxPt: lamportsNumberToBigInt(args.maxPt, "user max PT"),
|
|
509
|
+
});
|
|
510
|
+
const addLiquidityResult = simulateAddLiquidity(marketState, {
|
|
511
|
+
...args,
|
|
512
|
+
maxSy: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxSy, "effective max SY"),
|
|
513
|
+
maxPt: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxPt, "effective max PT"),
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
deltaL: addLiquidityResult.deltaL,
|
|
517
|
+
sySpent: finalizeEqualizedSpend({
|
|
518
|
+
simulatedSpend: addLiquidityResult.sySpent,
|
|
519
|
+
fixedSpent: budgetEffect.fixedSySpent,
|
|
520
|
+
fixedReleased: budgetEffect.fixedSyReleased,
|
|
521
|
+
label: "equalized SY spend",
|
|
522
|
+
}),
|
|
523
|
+
ptSpent: finalizeEqualizedSpend({
|
|
524
|
+
simulatedSpend: addLiquidityResult.ptSpent,
|
|
525
|
+
fixedSpent: budgetEffect.fixedPtSpent,
|
|
526
|
+
fixedReleased: budgetEffect.fixedPtReleased,
|
|
527
|
+
label: "equalized PT spend",
|
|
528
|
+
}),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
563
531
|
/**
|
|
564
532
|
* Calculate the LP tokens that will be received for a given deposit
|
|
565
533
|
* This is useful for UI display and slippage calculations
|
|
@@ -572,7 +540,6 @@ function calculateLpOut(liquidityAdded, totalLiquidityBefore) {
|
|
|
572
540
|
// LP tokens are proportional to liquidity added
|
|
573
541
|
return liquidityAdded;
|
|
574
542
|
}
|
|
575
|
-
exports.calculateLpOut = calculateLpOut;
|
|
576
543
|
/**
|
|
577
544
|
* Estimate the balanced amounts of PT and SY needed for a liquidity deposit
|
|
578
545
|
* Given a target liquidity amount, calculate how much PT and SY are needed
|
|
@@ -605,11 +572,10 @@ function estimateBalancedDeposit(marketState, targetLiquidity, lowerTickApy, upp
|
|
|
605
572
|
return { ptNeeded, syNeeded };
|
|
606
573
|
}
|
|
607
574
|
}
|
|
608
|
-
exports.estimateBalancedDeposit = estimateBalancedDeposit;
|
|
609
575
|
/** Calculate SY and PT needed to deposit into liquidity pool from base token amount */
|
|
610
576
|
/** Off-chain analogue of on-chain wrapper_provide_liquidity function */
|
|
611
577
|
function calcDepositSyAndPtFromBaseAmount(params) {
|
|
612
|
-
const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount,
|
|
578
|
+
const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount, epsilonClamp = 1e-18, } = params;
|
|
613
579
|
if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
|
|
614
580
|
return {
|
|
615
581
|
syNeeded: 0,
|
|
@@ -621,13 +587,12 @@ function calcDepositSyAndPtFromBaseAmount(params) {
|
|
|
621
587
|
const priceEffLower = effSnap.getEffectivePrice(lowerPrice);
|
|
622
588
|
const priceEffUpper = effSnap.getEffectivePrice(upperPrice);
|
|
623
589
|
// We mirror the on-chain logic in `wrapper_provide_liquidity`:
|
|
624
|
-
// 1. Use
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
// PT/SY proportions in the active tick, significantly improving accuracy.
|
|
590
|
+
// 1. Use curve-level token needs to infer the SY/PT ratio for this range.
|
|
591
|
+
// Raw active-tick proportions are ignored because the deposit CPI
|
|
592
|
+
// normalizes the active tick before minting new shares.
|
|
628
593
|
// 2. Use that ratio plus the current SY exchange rate to decide how much of the
|
|
629
594
|
// minted SY should be stripped into PT (calc_strip_amount).
|
|
630
|
-
const [syMock, ptMock] = computeTokenNeedsWithCrossing(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 1e9, 1e9, epsilonClamp
|
|
595
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 1e9, 1e9, epsilonClamp);
|
|
631
596
|
// Total SY the user would get by minting SY from base off-chain
|
|
632
597
|
// (we approximate on-chain `mint_sy_return_data.sy_out_amount`).
|
|
633
598
|
const syAmount = Math.floor(baseTokenAmount * syExchangeRate);
|
|
@@ -647,7 +612,6 @@ function calcDepositSyAndPtFromBaseAmount(params) {
|
|
|
647
612
|
ptNeeded: ptFromStrip,
|
|
648
613
|
};
|
|
649
614
|
}
|
|
650
|
-
exports.calcDepositSyAndPtFromBaseAmount = calcDepositSyAndPtFromBaseAmount;
|
|
651
615
|
/**
|
|
652
616
|
* Calculate the amount of SY to strip into PT based on market liquidity ratio
|
|
653
617
|
* This matches the Rust implementation: calc_strip_amount in wrapper_provide_liquidity.rs
|
|
@@ -678,9 +642,10 @@ function calcStripAmount(totalAmountSy, curSyRate, marketPtLiq, marketSyLiq) {
|
|
|
678
642
|
* @param lowerTick - Lower tick (APY in basis points)
|
|
679
643
|
* @param upperTick - Upper tick (APY in basis points)
|
|
680
644
|
* @param syExchangeRate - SY exchange rate
|
|
645
|
+
* @param options - Optional existing LP position to include fixed crossing-split equalization spend
|
|
681
646
|
* @returns Simulation result with LP out, YT out, amounts spent, etc.
|
|
682
647
|
*/
|
|
683
|
-
function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
|
|
648
|
+
function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upperTick, syExchangeRate, options = {}) {
|
|
684
649
|
try {
|
|
685
650
|
const { financials, configurationOptions, ticks } = marketState;
|
|
686
651
|
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000);
|
|
@@ -697,24 +662,22 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
|
|
|
697
662
|
// Precompute effective prices
|
|
698
663
|
const priceEffLower = snap.getEffectivePrice(lowerPrice);
|
|
699
664
|
const priceEffUpper = snap.getEffectivePrice(upperPrice);
|
|
700
|
-
// Step 3:
|
|
701
|
-
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks);
|
|
702
|
-
// Step 4: Calculate mock token needs using compute_token_needs_with_crossing
|
|
665
|
+
// Step 3: Calculate curve-level token needs.
|
|
703
666
|
// max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
|
|
704
667
|
const maxPt = syAmount * syExchangeRate;
|
|
705
|
-
const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp
|
|
706
|
-
// Step
|
|
668
|
+
const [syMock, ptMock] = computeTokenNeedsWithCrossing(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, syAmount, maxPt, configurationOptions.epsilonClamp);
|
|
669
|
+
// Step 4: Calculate how much SY to strip (calc_strip_amount on-chain)
|
|
707
670
|
// Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
|
|
708
671
|
const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock);
|
|
709
672
|
const syToStrip = Math.min(syToStripRaw, syAmount);
|
|
710
|
-
// Step
|
|
673
|
+
// Step 5: Calculate PT and YT from stripping
|
|
711
674
|
// When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
|
|
712
675
|
const ptFromStrip = syToStrip * syExchangeRate;
|
|
713
676
|
const ytOut = ptFromStrip; // YT amount equals PT amount from strip
|
|
714
|
-
// Step
|
|
677
|
+
// Step 6: Calculate remaining SY after strip
|
|
715
678
|
// Use Math.max to ensure non-negative (safety net for floating point edge cases)
|
|
716
679
|
const syRemainder = Math.max(0, syAmount - syToStrip);
|
|
717
|
-
// Step
|
|
680
|
+
// Step 7: Simulate deposit liquidity with remaining SY and PT
|
|
718
681
|
const depositResult = simulateAddLiquidity(marketState, {
|
|
719
682
|
lowerTick,
|
|
720
683
|
upperTick,
|
|
@@ -740,7 +703,6 @@ function simulateWrapperProvideLiquidity(marketState, amountBase, lowerTick, upp
|
|
|
740
703
|
return null;
|
|
741
704
|
}
|
|
742
705
|
}
|
|
743
|
-
exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
|
|
744
706
|
/**
|
|
745
707
|
* Simulate swap & supply operation (ixProvideLiquidityBase)
|
|
746
708
|
* 1. Mints SY from base asset
|
|
@@ -754,15 +716,16 @@ exports.simulateWrapperProvideLiquidity = simulateWrapperProvideLiquidity;
|
|
|
754
716
|
* @param lowerTick - Lower tick key (APY in basis points)
|
|
755
717
|
* @param upperTick - Upper tick key (APY in basis points)
|
|
756
718
|
* @param syExchangeRate - SY exchange rate
|
|
719
|
+
* @param options - Optional existing LP position to include fixed crossing-split equalization spend
|
|
757
720
|
* @returns Simulation result with LP out, PT to buy, SY constraint, etc.
|
|
758
721
|
*/
|
|
759
|
-
function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, syExchangeRate) {
|
|
722
|
+
function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, syExchangeRate, options = {}) {
|
|
760
723
|
try {
|
|
761
724
|
// Wrapper provide-liquidity-base debits base as:
|
|
762
725
|
// base_needed = ceil(total_sy_spent * sy_exchange_rate)
|
|
763
726
|
// So the strict SY budget for a user-provided base input is floor(base / rate).
|
|
764
727
|
const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate);
|
|
765
|
-
if (syBudget <= 0
|
|
728
|
+
if (syBudget <= 0) {
|
|
766
729
|
return {
|
|
767
730
|
lpOut: 0,
|
|
768
731
|
ptToBuy: 0,
|
|
@@ -831,7 +794,7 @@ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, sy
|
|
|
831
794
|
if (cached !== undefined) {
|
|
832
795
|
return cached;
|
|
833
796
|
}
|
|
834
|
-
const candidate = simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, key, syExchangeRate, exactOutEstimateSlack);
|
|
797
|
+
const candidate = simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, key, syExchangeRate, exactOutEstimateSlack, options.existingPosition);
|
|
835
798
|
candidateCache.set(key, candidate);
|
|
836
799
|
return candidate;
|
|
837
800
|
};
|
|
@@ -951,8 +914,7 @@ function simulateSwapAndSupply(marketState, amountBase, lowerTick, upperTick, sy
|
|
|
951
914
|
return null;
|
|
952
915
|
}
|
|
953
916
|
}
|
|
954
|
-
|
|
955
|
-
function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack) {
|
|
917
|
+
function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack, existingPosition) {
|
|
956
918
|
let swapResult;
|
|
957
919
|
try {
|
|
958
920
|
swapResult = simulateBuyPtExactOutWrapper(marketState, syBudget, ptConstraint, syExchangeRate, exactOutEstimateSlack);
|
|
@@ -962,13 +924,19 @@ function simulateSwapAndSupplyForPtConstraint(marketState, lowerTick, upperTick,
|
|
|
962
924
|
return null;
|
|
963
925
|
}
|
|
964
926
|
const depositState = swapResult.postMarketState ?? marketState;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
927
|
+
let depositResult;
|
|
928
|
+
try {
|
|
929
|
+
depositResult = simulateAddLiquidityForPosition(depositState, {
|
|
930
|
+
lowerTick,
|
|
931
|
+
upperTick,
|
|
932
|
+
maxSy: syBudget,
|
|
933
|
+
maxPt: swapResult.ptOut,
|
|
934
|
+
syExchangeRate,
|
|
935
|
+
}, existingPosition);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
972
940
|
// Swap & supply must actually add liquidity.
|
|
973
941
|
// Discard candidates that end up as swap-only (deltaL == 0).
|
|
974
942
|
if (depositResult.deltaL <= 0) {
|