@exponent-labs/market-three-math 0.1.8 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/addLiquidity.d.ts +65 -4
- package/build/addLiquidity.js +762 -36
- package/build/addLiquidity.js.map +1 -1
- package/build/bisect.d.ts +11 -0
- package/build/bisect.js +22 -12
- package/build/bisect.js.map +1 -1
- package/build/index.d.ts +5 -4
- package/build/index.js +14 -7
- package/build/index.js.map +1 -1
- package/build/liquidityHistogram.d.ts +6 -1
- package/build/liquidityHistogram.js +57 -12
- package/build/liquidityHistogram.js.map +1 -1
- package/build/quote.d.ts +1 -1
- package/build/quote.js +70 -84
- package/build/quote.js.map +1 -1
- package/build/swap.js +36 -18
- package/build/swap.js.map +1 -1
- package/build/swapV2.d.ts +6 -1
- package/build/swapV2.js +394 -52
- package/build/swapV2.js.map +1 -1
- package/build/types.d.ts +51 -0
- package/build/utils.d.ts +8 -2
- package/build/utils.js +37 -19
- package/build/utils.js.map +1 -1
- package/build/utilsV2.d.ts +9 -0
- package/build/utilsV2.js +131 -9
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.js +12 -7
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.d.ts +7 -0
- package/build/ytTrades.js +166 -146
- package/build/ytTrades.js.map +1 -1
- package/build/ytTradesLegacy.js +3 -4
- package/build/ytTradesLegacy.js.map +1 -1
- package/package.json +2 -2
- package/src/addLiquidity.ts +1012 -38
- package/src/bisect.ts +22 -11
- package/src/index.ts +11 -5
- package/src/liquidityHistogram.ts +54 -9
- package/src/quote.ts +73 -95
- package/src/swap.ts +35 -19
- package/src/swapV2.ts +999 -0
- package/src/types.ts +55 -0
- package/src/utils.ts +24 -3
- package/src/utilsV2.ts +337 -0
- package/src/withdrawLiquidity.ts +12 -6
- package/src/ytTrades.ts +191 -172
- package/src/ytTradesLegacy.ts +419 -0
- package/build/swap-v2.d.ts +0 -20
- package/build/swap-v2.js +0 -261
- package/build/swap-v2.js.map +0 -1
- package/build/swapLegacy.d.ts +0 -16
- package/build/swapLegacy.js +0 -229
- package/build/swapLegacy.js.map +0 -1
- package/src/swapLegacy.ts +0 -272
package/src/ytTrades.ts
CHANGED
|
@@ -6,20 +6,31 @@
|
|
|
6
6
|
* - Sell YT: Merge PT+YT → SY, then buy PT from pool to repay
|
|
7
7
|
*/
|
|
8
8
|
import { bisectSearch2 } from "./bisect"
|
|
9
|
-
import { simulateSwap } from "./
|
|
10
|
-
import { MarketThreeState, SwapDirection,
|
|
9
|
+
import { simulateSwap, simulateSwapExactOut } from "./swapV2"
|
|
10
|
+
import { MarketThreeState, SwapDirection, SwapOutcomeV2 } from "./types"
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Helper function to convert PY to SY
|
|
14
|
-
* @param syExchangeRate - The SY exchange rate
|
|
13
|
+
* Helper function to convert PY to SY (floor)
|
|
14
|
+
* @param syExchangeRate - The SY exchange rate (SY value in terms of underlying)
|
|
15
15
|
* @param pyAmount - The PY (PT or YT) amount
|
|
16
|
-
* @returns The equivalent SY amount
|
|
16
|
+
* @returns The equivalent SY amount (floored)
|
|
17
17
|
*/
|
|
18
18
|
function pyToSy(syExchangeRate: number, pyAmount: number): number {
|
|
19
19
|
if (syExchangeRate <= 0) return 0
|
|
20
20
|
return Math.floor(pyAmount / syExchangeRate)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Helper function to convert PY to SY (ceil) - for when you need AT LEAST this much SY
|
|
25
|
+
* @param syExchangeRate - The SY exchange rate
|
|
26
|
+
* @param pyAmount - The PY (PT or YT) amount
|
|
27
|
+
* @returns The equivalent SY amount (ceiled)
|
|
28
|
+
*/
|
|
29
|
+
function pyToSyCeil(syExchangeRate: number, pyAmount: number): number {
|
|
30
|
+
if (syExchangeRate <= 0) return 0
|
|
31
|
+
return Math.ceil(pyAmount / syExchangeRate)
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/**
|
|
24
35
|
* Helper function to convert SY to PY
|
|
25
36
|
* @param syExchangeRate - The SY exchange rate
|
|
@@ -75,14 +86,13 @@ export function simulateBuyYt(marketState: MarketThreeState, args: BuyYtSimulati
|
|
|
75
86
|
const { ytOut, syExchangeRate, priceSpotLimit } = args
|
|
76
87
|
|
|
77
88
|
// Calculate how much SY needs to be stripped to get the desired YT
|
|
78
|
-
//
|
|
79
|
-
const syToStrip =
|
|
89
|
+
// Use ceiling to ensure we strip enough SY
|
|
90
|
+
const syToStrip = pyToSyCeil(syExchangeRate, ytOut)
|
|
80
91
|
|
|
81
|
-
// Stripping gives
|
|
92
|
+
// Stripping gives equal amounts of PT and YT (1:1 ratio based on underlying value)
|
|
82
93
|
const ptFromStrip = ytOut
|
|
83
94
|
|
|
84
95
|
// Simulate selling the PT to get SY back
|
|
85
|
-
// Note: We use ytOut as the amount because PT out = YT out from the strip
|
|
86
96
|
const swapResult = simulateSwap(marketState, {
|
|
87
97
|
direction: SwapDirection.PtToSy,
|
|
88
98
|
amountIn: ytOut,
|
|
@@ -117,6 +127,31 @@ export interface BuyYtWithSyInSimulationArgs {
|
|
|
117
127
|
priceSpotLimit?: number
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Helper to calculate net SY cost for a given YT amount
|
|
132
|
+
*/
|
|
133
|
+
function calculateNetSyCost(
|
|
134
|
+
marketState: MarketThreeState,
|
|
135
|
+
ytAmount: number,
|
|
136
|
+
syExchangeRate: number,
|
|
137
|
+
priceSpotLimit?: number,
|
|
138
|
+
): { netSyCost: number; syToStrip: number; syFromPtSale: number } {
|
|
139
|
+
const syToStrip = pyToSyCeil(syExchangeRate, ytAmount)
|
|
140
|
+
|
|
141
|
+
const swapResult = simulateSwap(marketState, {
|
|
142
|
+
direction: SwapDirection.PtToSy,
|
|
143
|
+
amountIn: ytAmount,
|
|
144
|
+
priceSpotLimit,
|
|
145
|
+
syExchangeRate,
|
|
146
|
+
isCurrentFlashSwap: true,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const syFromPtSale = swapResult.amountOut
|
|
150
|
+
const netSyCost = syToStrip - syFromPtSale
|
|
151
|
+
|
|
152
|
+
return { netSyCost, syToStrip, syFromPtSale }
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
/**
|
|
121
156
|
* Simulates buying YT tokens given a SY input amount
|
|
122
157
|
*
|
|
@@ -139,130 +174,131 @@ export function simulateBuyYtWithSyIn(
|
|
|
139
174
|
): BuyYtSimulationResult {
|
|
140
175
|
const { syIn, syExchangeRate, priceSpotLimit } = args
|
|
141
176
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
if (syIn <= 0) {
|
|
178
|
+
throw new Error("syIn must be positive")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (syExchangeRate <= 0) {
|
|
182
|
+
throw new Error("syExchangeRate must be positive")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Lower bound: minimum meaningful YT amount
|
|
149
186
|
const minPossibleYt = 1
|
|
150
187
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
const marketPtLiquidity = Number(marketState.financials.ptBalance)
|
|
188
|
+
// Buy-YT uses a flash-swap PT->SY path, so the tradeable size is determined by active
|
|
189
|
+
// tick liquidity, not by the idle PT balance stored on the market account.
|
|
154
190
|
const marketSyLiquidity = Number(marketState.financials.syBalance)
|
|
155
191
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
192
|
+
// Calculate cost at minimum YT to verify function behavior
|
|
193
|
+
let minCost: number
|
|
194
|
+
try {
|
|
195
|
+
const minResult = calculateNetSyCost(marketState, minPossibleYt, syExchangeRate, priceSpotLimit)
|
|
196
|
+
minCost = minResult.netSyCost
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error("Market cannot support even minimum YT trade")
|
|
199
|
+
}
|
|
164
200
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
201
|
+
// If minimum cost already exceeds syIn, we can't do this trade
|
|
202
|
+
if (minCost > syIn) {
|
|
203
|
+
throw new Error(`Trade too small: minimum YT (${minPossibleYt}) costs ${minCost} SY, but only ${syIn} SY available`)
|
|
204
|
+
}
|
|
169
205
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
// Find upper bound using exponential search
|
|
207
|
+
// We need to find ytMax where netSyCost(ytMax) > syIn
|
|
208
|
+
let maxPossibleYt = Math.max(minPossibleYt + 1, syToPy(syExchangeRate, syIn)) // Start with naive estimate
|
|
209
|
+
let foundUpperBound = false
|
|
210
|
+
let lastValidYt = minPossibleYt
|
|
211
|
+
let lastValidCost = minCost
|
|
174
212
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
syExchangeRate,
|
|
180
|
-
isCurrentFlashSwap: true,
|
|
181
|
-
})
|
|
213
|
+
// Exponential search to find upper bound
|
|
214
|
+
const maxAttempts = 40
|
|
215
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
216
|
+
const testYt = Math.max(minPossibleYt + 1, Math.floor(maxPossibleYt))
|
|
182
217
|
|
|
183
|
-
|
|
184
|
-
const
|
|
218
|
+
try {
|
|
219
|
+
const result = calculateNetSyCost(marketState, testYt, syExchangeRate, priceSpotLimit)
|
|
185
220
|
|
|
186
|
-
|
|
187
|
-
|
|
221
|
+
if (result.netSyCost > syIn) {
|
|
222
|
+
// Found upper bound
|
|
223
|
+
maxPossibleYt = testYt
|
|
188
224
|
foundUpperBound = true
|
|
189
225
|
break
|
|
190
226
|
}
|
|
191
227
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const costRatio = syIn / Math.max(netSyCost, 1)
|
|
196
|
-
const growthFactor = attempt < 3 ? 2.0 : Math.min(1.5, 1 + costRatio * 0.3)
|
|
228
|
+
// This YT amount is still affordable, save it
|
|
229
|
+
lastValidYt = testYt
|
|
230
|
+
lastValidCost = result.netSyCost
|
|
197
231
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
// Increase estimate using adaptive growth
|
|
233
|
+
// The closer lastValidCost is to syIn, the smaller the growth factor
|
|
234
|
+
const remainingBudget = syIn - lastValidCost
|
|
235
|
+
const growthFactor = Math.max(1.5, Math.min(3, 1 + remainingBudget / Math.max(lastValidCost, 1)))
|
|
236
|
+
maxPossibleYt = Math.ceil(testYt * growthFactor)
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Swap failed - we've exceeded what the market can handle
|
|
239
|
+
if (lastValidYt > minPossibleYt) {
|
|
240
|
+
// Use binary search between lastValidYt and testYt to find exact upper bound
|
|
241
|
+
let searchLo = lastValidYt
|
|
242
|
+
let searchHi = testYt
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < 10; i++) {
|
|
245
|
+
const mid = Math.floor((searchLo + searchHi) / 2)
|
|
246
|
+
try {
|
|
247
|
+
const midResult = calculateNetSyCost(marketState, mid, syExchangeRate, priceSpotLimit)
|
|
248
|
+
if (midResult.netSyCost > syIn) {
|
|
249
|
+
searchHi = mid
|
|
250
|
+
foundUpperBound = true
|
|
251
|
+
} else {
|
|
252
|
+
searchLo = mid
|
|
253
|
+
lastValidYt = mid
|
|
254
|
+
lastValidCost = midResult.netSyCost
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
searchHi = mid
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (foundUpperBound) {
|
|
262
|
+
maxPossibleYt = searchHi
|
|
263
|
+
} else {
|
|
264
|
+
// Couldn't find upper bound - trade exceeds market capacity
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Trade exceeds market capacity: max affordable YT is ~${lastValidYt} (cost: ${lastValidCost}), ` +
|
|
267
|
+
`syIn = ${syIn}`,
|
|
268
|
+
)
|
|
269
|
+
}
|
|
204
270
|
break
|
|
205
271
|
}
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Interpolate to find better upper bound
|
|
211
|
-
const estimatedMax = maxPossibleYt * 0.7 * (syIn / lastValidCost)
|
|
212
|
-
maxPossibleYt = Math.max(estimatedMax, minPossibleYt * 1.2)
|
|
213
|
-
} else {
|
|
214
|
-
// No valid cost yet, use a conservative upper bound
|
|
215
|
-
maxPossibleYt = Math.max(maxPossibleYt * 0.8, minPossibleYt * 1.5)
|
|
272
|
+
|
|
273
|
+
// Re-throw if we haven't found any valid YT amount
|
|
274
|
+
if (error instanceof Error) {
|
|
275
|
+
throw error
|
|
216
276
|
}
|
|
217
|
-
|
|
218
|
-
break
|
|
277
|
+
throw new Error("Failed to find valid YT range")
|
|
219
278
|
}
|
|
220
279
|
}
|
|
221
280
|
|
|
222
281
|
if (!foundUpperBound) {
|
|
223
|
-
throw new Error(
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Could not find upper bound for YT amount after ${maxAttempts} attempts. ` +
|
|
284
|
+
`Last valid: ${lastValidYt} (cost: ${lastValidCost}), syIn: ${syIn}`,
|
|
285
|
+
)
|
|
224
286
|
}
|
|
225
287
|
|
|
226
|
-
//
|
|
288
|
+
// Sanity check bounds
|
|
227
289
|
if (maxPossibleYt <= minPossibleYt) {
|
|
228
|
-
|
|
290
|
+
throw new Error(`Invalid bounds: min=${minPossibleYt}, max=${maxPossibleYt}`)
|
|
229
291
|
}
|
|
230
292
|
|
|
231
293
|
// Use bisection search to find the ytOut that results in netSyCost = syIn
|
|
232
|
-
//
|
|
233
|
-
const adaptiveEpsilon = Math.max(
|
|
234
|
-
|
|
235
|
-
// Reduce max iterations since we have better bounds
|
|
236
|
-
const maxIterations = 10000
|
|
237
|
-
|
|
238
|
-
// Debug info for bounds validation
|
|
239
|
-
if (maxPossibleYt <= minPossibleYt) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
`Invalid bisection bounds for buy YT. ` +
|
|
242
|
-
`Min: ${minPossibleYt}, Max: ${maxPossibleYt}, ` +
|
|
243
|
-
`SyIn: ${syIn}, SyExchangeRate: ${syExchangeRate}, ` +
|
|
244
|
-
`MarketPtBalance: ${marketPtLiquidity}, MarketSyBalance: ${marketSyLiquidity}`,
|
|
245
|
-
)
|
|
246
|
-
}
|
|
294
|
+
// Epsilon should be small relative to the expected result magnitude
|
|
295
|
+
const adaptiveEpsilon = Math.max(1, syIn * 0.0001)
|
|
296
|
+
const maxIterations = 100 // Bisection converges fast with good bounds
|
|
247
297
|
|
|
248
298
|
const ytOut = bisectSearch2(
|
|
249
299
|
(ytGuess: number) => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const swapResult = simulateSwap(marketState, {
|
|
254
|
-
direction: SwapDirection.PtToSy,
|
|
255
|
-
amountIn: ytGuess,
|
|
256
|
-
priceSpotLimit,
|
|
257
|
-
syExchangeRate,
|
|
258
|
-
isCurrentFlashSwap: true,
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
const syFromPtSale = swapResult.amountOut
|
|
262
|
-
const netSyCost = syToStrip - syFromPtSale
|
|
263
|
-
|
|
264
|
-
// Return the difference between actual cost and target syIn
|
|
265
|
-
return netSyCost - syIn
|
|
300
|
+
const result = calculateNetSyCost(marketState, ytGuess, syExchangeRate, priceSpotLimit)
|
|
301
|
+
return result.netSyCost - syIn
|
|
266
302
|
},
|
|
267
303
|
minPossibleYt,
|
|
268
304
|
maxPossibleYt,
|
|
@@ -271,19 +307,37 @@ export function simulateBuyYtWithSyIn(
|
|
|
271
307
|
)
|
|
272
308
|
|
|
273
309
|
if (ytOut === null) {
|
|
310
|
+
// This shouldn't happen if bounds are correct
|
|
311
|
+
// Provide detailed debug info
|
|
312
|
+
const minResult = calculateNetSyCost(marketState, minPossibleYt, syExchangeRate, priceSpotLimit)
|
|
313
|
+
let maxResult: { netSyCost: number } | null = null
|
|
314
|
+
try {
|
|
315
|
+
maxResult = calculateNetSyCost(marketState, maxPossibleYt, syExchangeRate, priceSpotLimit)
|
|
316
|
+
} catch {
|
|
317
|
+
// Max might fail
|
|
318
|
+
}
|
|
319
|
+
|
|
274
320
|
throw new Error(
|
|
275
|
-
`Failed to converge
|
|
276
|
-
`
|
|
277
|
-
`
|
|
321
|
+
`Failed to converge: bounds [${minPossibleYt}, ${maxPossibleYt}], ` +
|
|
322
|
+
`f(min)=${minResult.netSyCost - syIn}, f(max)=${maxResult ? maxResult.netSyCost - syIn : "error"}, ` +
|
|
323
|
+
`syIn=${syIn}, epsilon=${adaptiveEpsilon}`,
|
|
278
324
|
)
|
|
279
325
|
}
|
|
280
326
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
ytOut,
|
|
327
|
+
// Return full result using the found ytOut
|
|
328
|
+
const result = simulateBuyYt(marketState, {
|
|
329
|
+
ytOut: Math.floor(ytOut), // Floor to ensure we don't over-promise
|
|
284
330
|
syExchangeRate,
|
|
285
331
|
priceSpotLimit,
|
|
286
332
|
})
|
|
333
|
+
|
|
334
|
+
if (marketSyLiquidity * syExchangeRate < result.ytOut * 2) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Insufficient SY liquidity in the market. Required: ${result.ytOut * 2}, Available: ${marketSyLiquidity}`,
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
export interface SellYtSimulationArgs {
|
|
@@ -293,6 +347,13 @@ export interface SellYtSimulationArgs {
|
|
|
293
347
|
syExchangeRate: number
|
|
294
348
|
/** Optional spot price limit (anti-sandwich) */
|
|
295
349
|
priceSpotLimit?: number
|
|
350
|
+
/**
|
|
351
|
+
* Safety margin in basis points to apply to the expected output.
|
|
352
|
+
* This can be used as an optional conservative buffer on top of the simulated output.
|
|
353
|
+
*
|
|
354
|
+
* Default: 0 (no buffer). Set to a positive value to reduce reported output.
|
|
355
|
+
*/
|
|
356
|
+
safetyMarginBps?: number
|
|
296
357
|
}
|
|
297
358
|
|
|
298
359
|
export interface SellYtSimulationResult {
|
|
@@ -328,84 +389,42 @@ export interface SellYtSimulationResult {
|
|
|
328
389
|
* @returns Simulation result with net SY received
|
|
329
390
|
*/
|
|
330
391
|
export function simulateSellYt(marketState: MarketThreeState, args: SellYtSimulationArgs): SellYtSimulationResult {
|
|
331
|
-
const { ytIn, syExchangeRate, priceSpotLimit } = args
|
|
392
|
+
const { ytIn, syExchangeRate, priceSpotLimit, safetyMarginBps = 0 } = args
|
|
332
393
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (marketState.financials.ptBalance < ytIn * 2) {
|
|
336
|
-
throw new Error(
|
|
337
|
-
`Insufficient PT liquidity in the market. Required: ${ytIn * 2}, Available: ${marketState.financials.ptBalance}`,
|
|
338
|
-
)
|
|
394
|
+
if (ytIn <= 0) {
|
|
395
|
+
throw new Error("ytIn must be positive")
|
|
339
396
|
}
|
|
340
397
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const ptNeeded = ytIn
|
|
346
|
-
|
|
347
|
-
const upperBoundSwap = simulateSwap(marketState, {
|
|
348
|
-
direction: SwapDirection.SyToPt,
|
|
349
|
-
amountIn: syFromMerge,
|
|
350
|
-
priceSpotLimit,
|
|
351
|
-
syExchangeRate,
|
|
352
|
-
isCurrentFlashSwap: true,
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
// Check that we can buy enough PT from CLMM
|
|
356
|
-
if (upperBoundSwap.amountOut < ptNeeded) {
|
|
357
|
-
throw new Error(
|
|
358
|
-
`Cannot buy enough PT with available SY. Need ${ytIn} PT but can only afford ${upperBoundSwap.amountOut} PT`,
|
|
359
|
-
)
|
|
398
|
+
// Check if there's sufficient PT liquidity
|
|
399
|
+
const marketPtBalance = Number(marketState.financials.ptBalance)
|
|
400
|
+
if (marketPtBalance < ytIn * 2) {
|
|
401
|
+
throw new Error(`Insufficient PT liquidity in the market. Required: ${ytIn * 2}, Available: ${marketPtBalance}`)
|
|
360
402
|
}
|
|
361
403
|
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
// based on the current price
|
|
365
|
-
const estimatedLowerBound = Math.min(syFromMerge * 0.5, syFromMerge * 0.9) // Start from 50% of max, but not too close to upper
|
|
366
|
-
|
|
367
|
-
// Adaptive epsilon based on PT needed
|
|
368
|
-
const adaptiveEpsilon = Math.max(0.01, ptNeeded * 0.0001)
|
|
369
|
-
|
|
370
|
-
// Safety check: ensure lower bound is less than upper bound
|
|
371
|
-
const safeLowerBound = Math.min(estimatedLowerBound, syFromMerge * 0.95)
|
|
372
|
-
const safeUpperBound = syFromMerge
|
|
404
|
+
// Merging PT + YT gives back the original SY (floor since you can't get fractional SY)
|
|
405
|
+
const syFromMerge = pyToSy(syExchangeRate, ytIn)
|
|
373
406
|
|
|
374
|
-
if (
|
|
375
|
-
throw new Error(`
|
|
407
|
+
if (syFromMerge <= 0) {
|
|
408
|
+
throw new Error(`Trade too small: merging ${ytIn} YT yields 0 SY`)
|
|
376
409
|
}
|
|
377
410
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const swapResult = simulateSwap(marketState, {
|
|
381
|
-
direction: SwapDirection.SyToPt,
|
|
382
|
-
amountIn: syGuess,
|
|
383
|
-
priceSpotLimit,
|
|
384
|
-
syExchangeRate,
|
|
385
|
-
isCurrentFlashSwap: true,
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
return swapResult.amountOut - ptNeeded
|
|
389
|
-
},
|
|
390
|
-
safeLowerBound,
|
|
391
|
-
safeUpperBound,
|
|
392
|
-
adaptiveEpsilon,
|
|
393
|
-
100, // Reduced iterations with better bounds
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
if (syToSpend === null) {
|
|
397
|
-
throw new Error("Failed to converge on correct SY amount using bisection search")
|
|
398
|
-
}
|
|
411
|
+
// On-chain uses pt_constraint + 2 to account for rounding, so we match that behavior
|
|
412
|
+
const ptNeeded = ytIn + 2
|
|
399
413
|
|
|
400
|
-
|
|
414
|
+
// Match on-chain SellYt exactly: TradePtExactOut with pt_target = yt_in + 2 and SY budget = syFromMerge
|
|
415
|
+
const swapResult = simulateSwapExactOut(marketState, {
|
|
401
416
|
direction: SwapDirection.SyToPt,
|
|
402
|
-
|
|
417
|
+
amountOut: ptNeeded,
|
|
403
418
|
priceSpotLimit,
|
|
404
419
|
syExchangeRate,
|
|
405
420
|
isCurrentFlashSwap: true,
|
|
421
|
+
amountInConstraint: syFromMerge,
|
|
406
422
|
})
|
|
407
423
|
|
|
408
|
-
|
|
424
|
+
// Optional conservative output buffer.
|
|
425
|
+
const rawNetSyReceived = syFromMerge - swapResult.amountInConsumed
|
|
426
|
+
const safetyDeduction = safetyMarginBps > 0 ? Math.ceil((rawNetSyReceived * safetyMarginBps) / 10000) : 0
|
|
427
|
+
const netSyReceived = rawNetSyReceived - safetyDeduction
|
|
409
428
|
|
|
410
429
|
return {
|
|
411
430
|
ytIn,
|