Skip to content

Commit 602a69f

Browse files
committed
feat: handle PT redeem
1 parent 98a0f62 commit 602a69f

File tree

2 files changed

+188
-19
lines changed

2 files changed

+188
-19
lines changed

src/swapService/strategies/balmySDK/pendleQuoteSource.ts

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type { TokenListItem } from "@/common/utils/tokenList"
12
import { findToken } from "@/swapService/utils"
2-
import { Chains } from "@balmy/sdk"
3+
import { Chains, type IFetchService } from "@balmy/sdk"
34
import type {
45
BuildTxParams,
56
IQuoteSource,
@@ -14,7 +15,7 @@ import {
1415
failed,
1516
} from "@balmy/sdk/dist/services/quotes/quote-sources/utils"
1617
import qs from "qs"
17-
import { getAddress } from "viem"
18+
import { type Address, getAddress, isAddressEqual } from "viem"
1819

1920
// https://api-v2.pendle.finance/core/docs#/Chains/ChainsController_getSupportedChainIds
2021
export const PENDLE_METADATA: QuoteSourceMetadata<PendleSupport> = {
@@ -39,9 +40,27 @@ type CustomOrAPIKeyConfig =
3940
| { customUrl?: undefined; apiKey: string }
4041
type PendleConfig = CustomOrAPIKeyConfig
4142
type PendleData = { tx: SourceQuoteTransaction }
43+
44+
type ExpiredMarketsCache = {
45+
[chainId: number]: {
46+
lastUpdatedUTCDate: number
47+
markets: {
48+
name: string
49+
address: Address
50+
expiry: string
51+
pt: string
52+
yt: string
53+
sy: string
54+
underlyingAsset: string
55+
}[]
56+
}
57+
}
58+
4259
export class CustomPendleQuoteSource
4360
implements IQuoteSource<PendleSupport, PendleConfig, PendleData>
4461
{
62+
private expiredMarketsCache: ExpiredMarketsCache = {}
63+
4564
getMetadata() {
4665
return PENDLE_METADATA
4766
}
@@ -90,9 +109,10 @@ export class CustomPendleQuoteSource
90109
}: QuoteParams<PendleSupport, PendleConfig>) {
91110
const tokenIn = findToken(chainId, getAddress(sellToken))
92111
const tokenOut = findToken(chainId, getAddress(buyToken))
112+
if (!tokenIn || !tokenOut) throw new Error("Missing token in or out")
93113

94114
let url
95-
if (tokenIn?.meta?.isPendlePT && tokenOut?.meta?.isPendlePT) {
115+
if (tokenIn.meta?.isPendlePT && tokenOut.meta?.isPendlePT) {
96116
// rollover
97117
const queryParams = {
98118
receiver: recipient || takeFrom,
@@ -107,7 +127,33 @@ export class CustomPendleQuoteSource
107127
})
108128

109129
const pendleMarket = tokenIn.meta.pendleMarket
110-
url = `${getUrl()}/${chainId}/markets/${pendleMarket}/roll-over-pt?${queryString}`
130+
url = `${getUrl()}/sdk/${chainId}/markets/${pendleMarket}/roll-over-pt?${queryString}`
131+
} else if (
132+
tokenIn.meta?.isPendlePT &&
133+
!!(await this.getExpiredMarket(fetchService, chainId, tokenIn, timeout))
134+
) {
135+
// redeem expired PT
136+
const market = await this.getExpiredMarket(
137+
fetchService,
138+
chainId,
139+
tokenIn,
140+
timeout,
141+
)
142+
const queryParams = {
143+
receiver: recipient || takeFrom,
144+
slippage: slippagePercentage / 100, // 1 = 100%
145+
enableAggregator: true,
146+
yt: market?.yt.slice(2),
147+
amountIn: order.sellAmount.toString(),
148+
tokenOut: buyToken,
149+
}
150+
151+
const queryString = qs.stringify(queryParams, {
152+
skipNulls: true,
153+
arrayFormat: "comma",
154+
})
155+
156+
url = `${getUrl()}/sdk/${chainId}/redeem?${queryString}`
111157
} else {
112158
// swap
113159
const queryParams = {
@@ -127,7 +173,7 @@ export class CustomPendleQuoteSource
127173
const pendleMarket =
128174
tokenIn?.meta?.pendleMarket || tokenOut?.meta?.pendleMarket
129175

130-
url = `${getUrl()}/${chainId}/markets/${pendleMarket}/swap?${queryString}`
176+
url = `${getUrl()}/sdk/${chainId}/markets/${pendleMarket}/swap?${queryString}`
131177
}
132178

133179
const response = await fetchService.fetch(url, {
@@ -154,6 +200,42 @@ export class CustomPendleQuoteSource
154200
return { dstAmount, to, data }
155201
}
156202

203+
private async getExpiredMarket(
204+
fetchService: IFetchService,
205+
chainId: number,
206+
token: TokenListItem,
207+
timeout?: string,
208+
) {
209+
if (
210+
!this.expiredMarketsCache[chainId] ||
211+
this.expiredMarketsCache[chainId].lastUpdatedUTCDate !==
212+
new Date().getUTCDate()
213+
) {
214+
this.expiredMarketsCache[chainId] = {
215+
markets: [],
216+
lastUpdatedUTCDate: -1,
217+
}
218+
219+
const url = `${getUrl()}/${chainId}/markets/inactive`
220+
const response = await fetchService.fetch(url, {
221+
timeout: timeout as any,
222+
})
223+
224+
if (response.ok) {
225+
const { markets } = await response.json()
226+
227+
this.expiredMarketsCache[chainId] = {
228+
markets,
229+
lastUpdatedUTCDate: new Date().getUTCDate(),
230+
}
231+
}
232+
}
233+
234+
return this.expiredMarketsCache[chainId].markets.find((m) =>
235+
isAddressEqual(m.address, token.meta?.pendleMarket as Address),
236+
)
237+
}
238+
157239
isConfigAndContextValidForQuoting(
158240
config: Partial<PendleConfig> | undefined,
159241
): config is PendleConfig {
@@ -168,7 +250,7 @@ export class CustomPendleQuoteSource
168250
}
169251

170252
function getUrl() {
171-
return "https://api-v2.pendle.finance/core/v1/sdk"
253+
return "https://api-v2.pendle.finance/core/v1"
172254
}
173255

174256
function getHeaders(config: PendleConfig) {

src/swapService/strategies/strategyBalmySDK.ts

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type Hex,
1515
encodeAbiParameters,
1616
parseAbiParameters,
17+
parseUnits,
1718
} from "viem"
1819
import { SwapperMode } from "../interface"
1920
import type { StrategyResult, SwapParams, SwapQuote } from "../types"
@@ -24,6 +25,7 @@ import {
2425
buildApiResponseExactInputFromQuote,
2526
buildApiResponseSwap,
2627
buildApiResponseVerifyDebtMax,
28+
calculateEstimatedAmountFrom,
2729
encodeSwapMulticallItem,
2830
isExactInRepay,
2931
matchParams,
@@ -239,13 +241,6 @@ export class StrategyBalmySDK {
239241
}
240242
}
241243

242-
const reverseSwapParams = {
243-
...swapParams,
244-
tokenIn: swapParams.tokenOut,
245-
tokenOut: swapParams.tokenIn,
246-
swapperMode: SwapperMode.EXACT_IN,
247-
}
248-
249244
let sourcesFilter
250245
if (this.config.sourcesFilter?.includeSources) {
251246
sourcesFilter = {
@@ -263,12 +258,28 @@ export class StrategyBalmySDK {
263258
} else {
264259
sourcesFilter = { excludeSources: BINARY_SEARCH_EXCLUDE_SOURCES }
265260
}
261+
const swapParamsExactIn = {
262+
...swapParams,
263+
swapperMode: SwapperMode.EXACT_IN,
264+
receiver: swapParams.from,
265+
isRepay: false,
266+
}
267+
const { amountTo: unitAmountTo } = await fetchQuote(
268+
{
269+
...swapParamsExactIn,
270+
amount: parseUnits("1", swapParams.tokenIn.decimals),
271+
},
272+
sourcesFilter,
273+
)
266274

267-
const reverseQuote = await fetchQuote(reverseSwapParams, sourcesFilter)
268-
const estimatedAmountIn = reverseQuote.amountTo
269-
if (estimatedAmountIn === 0n) throw new Error("quote not found")
275+
const estimatedAmountIn = calculateEstimatedAmountFrom(
276+
unitAmountTo,
277+
swapParamsExactIn.amount,
278+
swapParamsExactIn.tokenIn.decimals,
279+
swapParamsExactIn.tokenOut.decimals,
280+
)
270281

271-
const bestSourceId = reverseQuote.quote.source.id
282+
if (estimatedAmountIn === 0n) throw new Error("quote not found")
272283

273284
const overSwapTarget = adjustForInterest(swapParams.amount)
274285

@@ -277,10 +288,19 @@ export class StrategyBalmySDK {
277288
currentAmountTo < overSwapTarget ||
278289
(currentAmountTo * 1000n) / overSwapTarget > 1005n
279290

291+
let bestSourceId: string
292+
280293
const quote = await binarySearchQuote(
281294
swapParams,
282-
(swapParams: SwapParams) =>
283-
fetchQuote(swapParams, { includeSources: [bestSourceId] }), // preselect single source to avoid oscilations
295+
async (swapParams: SwapParams) => {
296+
let bestSourceConfig
297+
if (bestSourceId) {
298+
bestSourceConfig = { includeSources: [bestSourceId] }
299+
}
300+
const q = await fetchQuote(swapParams, bestSourceConfig) // preselect single source to avoid oscilations
301+
if (!bestSourceId) bestSourceId = q.quote.source.id
302+
return q
303+
},
284304
overSwapTarget,
285305
estimatedAmountIn,
286306
shouldContinue,
@@ -293,6 +313,73 @@ export class StrategyBalmySDK {
293313
return this.#getSwapQuoteFromSDKQuoteWithTx(swapParams, quoteWithTx)
294314
}
295315

316+
// async #binarySearchOverswapQuote(swapParams: SwapParams) {
317+
// const fetchQuote = async (
318+
// sp: SwapParams,
319+
// sourcesFilter?: SourcesFilter,
320+
// ) => {
321+
// const quote = await this.#getBestSDKQuote(sp, sourcesFilter)
322+
// return {
323+
// quote,
324+
// amountTo: quote.buyAmount.amount,
325+
// }
326+
// }
327+
328+
// const reverseSwapParams = {
329+
// ...swapParams,
330+
// tokenIn: swapParams.tokenOut,
331+
// tokenOut: swapParams.tokenIn,
332+
// swapperMode: SwapperMode.EXACT_IN,
333+
// }
334+
335+
// let sourcesFilter
336+
// if (this.config.sourcesFilter?.includeSources) {
337+
// sourcesFilter = {
338+
// includeSources: this.config.sourcesFilter.includeSources.filter(
339+
// (s) => !BINARY_SEARCH_EXCLUDE_SOURCES.includes(s),
340+
// ),
341+
// }
342+
// } else if (this.config.sourcesFilter?.excludeSources) {
343+
// sourcesFilter = {
344+
// excludeSources: [
345+
// ...this.config.sourcesFilter.excludeSources,
346+
// ...BINARY_SEARCH_EXCLUDE_SOURCES,
347+
// ],
348+
// }
349+
// } else {
350+
// sourcesFilter = { excludeSources: BINARY_SEARCH_EXCLUDE_SOURCES }
351+
// }
352+
// console.log(11);
353+
// const reverseQuote = await fetchQuote(reverseSwapParams, sourcesFilter)
354+
// console.log(22);
355+
// const estimatedAmountIn = reverseQuote.amountTo
356+
// if (estimatedAmountIn === 0n) throw new Error("quote not found")
357+
358+
// const bestSourceId = reverseQuote.quote.source.id
359+
360+
// const overSwapTarget = adjustForInterest(swapParams.amount)
361+
362+
// const shouldContinue = (currentAmountTo: bigint): boolean =>
363+
// // search until quote is 100 - 100.5% target
364+
// currentAmountTo < overSwapTarget ||
365+
// (currentAmountTo * 1000n) / overSwapTarget > 1005n
366+
367+
// const quote = await binarySearchQuote(
368+
// swapParams,
369+
// (swapParams: SwapParams) =>
370+
// fetchQuote(swapParams, { includeSources: [bestSourceId] }), // preselect single source to avoid oscilations
371+
// overSwapTarget,
372+
// estimatedAmountIn,
373+
// shouldContinue,
374+
// )
375+
// const quoteWithTx = {
376+
// ...quote,
377+
// tx: await this.#getTxForQuote(quote),
378+
// }
379+
380+
// return this.#getSwapQuoteFromSDKQuoteWithTx(swapParams, quoteWithTx)
381+
// }
382+
296383
async #getBestSDKQuote(
297384
swapParams: SwapParams,
298385
sourcesFilter?: SourcesFilter,

0 commit comments

Comments
 (0)