@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.
Files changed (50) hide show
  1. package/build/addLiquidity.d.ts +65 -4
  2. package/build/addLiquidity.js +757 -30
  3. package/build/addLiquidity.js.map +1 -1
  4. package/build/bisect.d.ts +11 -0
  5. package/build/bisect.js +21 -10
  6. package/build/bisect.js.map +1 -1
  7. package/build/index.d.ts +5 -4
  8. package/build/index.js +14 -7
  9. package/build/index.js.map +1 -1
  10. package/build/liquidityHistogram.d.ts +6 -1
  11. package/build/liquidityHistogram.js +55 -9
  12. package/build/liquidityHistogram.js.map +1 -1
  13. package/build/quote.d.ts +1 -1
  14. package/build/quote.js +68 -82
  15. package/build/quote.js.map +1 -1
  16. package/build/swap.js +35 -16
  17. package/build/swap.js.map +1 -1
  18. package/build/swapV2.d.ts +6 -1
  19. package/build/swapV2.js +394 -51
  20. package/build/swapV2.js.map +1 -1
  21. package/build/types.d.ts +51 -0
  22. package/build/utils.d.ts +8 -2
  23. package/build/utils.js +23 -5
  24. package/build/utils.js.map +1 -1
  25. package/build/utilsV2.d.ts +9 -0
  26. package/build/utilsV2.js +127 -5
  27. package/build/utilsV2.js.map +1 -1
  28. package/build/withdrawLiquidity.js +11 -5
  29. package/build/withdrawLiquidity.js.map +1 -1
  30. package/build/ytTrades.d.ts +7 -0
  31. package/build/ytTrades.js +163 -142
  32. package/build/ytTrades.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/addLiquidity.ts +1012 -38
  35. package/src/bisect.ts +22 -11
  36. package/src/index.ts +11 -5
  37. package/src/liquidityHistogram.ts +54 -9
  38. package/src/quote.ts +73 -95
  39. package/src/swap.ts +35 -19
  40. package/src/swapV2.ts +999 -0
  41. package/src/types.ts +55 -0
  42. package/src/utils.ts +24 -3
  43. package/src/utilsV2.ts +337 -0
  44. package/src/withdrawLiquidity.ts +12 -6
  45. package/src/ytTrades.ts +191 -172
  46. package/src/ytTradesLegacy.ts +419 -0
  47. package/build/swap-v2.d.ts +0 -20
  48. package/build/swap-v2.js +0 -261
  49. package/build/swap-v2.js.map +0 -1
  50. package/src/swapLegacy.ts +0 -272
@@ -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 utils_1.EffSnap((0, utils_1.normalizedTimeRemaining)(secondsRemaining), args.syExchangeRate);
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, args.maxSy, args.maxPt, configurationOptions.epsilonClamp);
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 = liquidityNeeds.syNeeded;
82
- const ptSpent = liquidityNeeds.ptNeeded;
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 utils_1.EffSnap((0, utils_1.normalizedTimeRemaining)(secondsRemaining), marketState.currentSyExchangeRate);
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 utils_1.EffSnap((0, utils_1.normalizedTimeRemaining)(secondsRemaining), syExchangeRate);
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 a large mock amount for both SY and PT to infer the *ratio* of SY/PT
158
- // the market "wants" for this price range (compute_token_needs on-chain).
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 to keep the user-close to pool proportions.
161
- const { syNeeded: syMock, ptNeeded: ptMock } = computeLiquidityTargetAndTokenNeeds(effSnap, currentSpotPrice, priceEffLower, priceEffUpper, lowerPrice, upperPrice, 0, 0, 0, 1e9, 1e9, 1e-18);
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 toStrip = (totalAmountSy * marketPtLiq) / (curSyRate * marketSyLiq + marketPtLiq);
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 utils_1.EffSnap((0, utils_1.normalizedTimeRemaining)(secondsRemaining), syExchangeRate);
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: Calculate "mock" token needs using MOCK_AMOUNT (1e9)
229
- // This gives us the ratio of SY:PT needed for this tick range
230
- const MOCK_AMOUNT = 1e9;
231
- const mockNeeds = computeLiquidityTargetAndTokenNeeds(snap, currentSpot, priceEffLower, priceEffUpper, lowerPrice, upperPrice, lowerTick, upperTick, ticks.currentTick, MOCK_AMOUNT, MOCK_AMOUNT, configurationOptions.epsilonClamp);
232
- // Step 4: Calculate how much SY to strip
233
- // Uses the mock amounts to determine the ratio
234
- const syToStrip = calcStripAmount(syAmount, syExchangeRate, mockNeeds.ptNeeded, mockNeeds.syNeeded);
235
- console.log("[simulateWrapperProvideLiquidity] Strip calculation:", {
236
- syToStrip,
237
- formula: `(${syAmount} * ${mockNeeds.ptNeeded}) / (${syExchangeRate} * ${mockNeeds.syNeeded} + ${mockNeeds.ptNeeded})`,
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 6: Calculate remaining SY after strip
244
- const syRemainder = syAmount - syToStrip;
245
- // Step 7: Simulate deposit liquidity with remaining SY and PT
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