Skip to content

Commit 55d7a00

Browse files
committed
feat: add pendle quote source for balmy
1 parent 047c769 commit 55d7a00

File tree

6 files changed

+182
-3
lines changed

6 files changed

+182
-3
lines changed

src/swapService/config/mainnet.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ const mainnetRoutingConfig: ChainRoutingConfig = [
2929
},
3030
// SPECIAL CASE TOKENS
3131
{
32-
strategy: StrategyPendle.name(),
33-
match: {}, // strategy supports() will match pendle PT tokens on input/output
32+
strategy: StrategyBalmySDK.name(),
33+
config: {
34+
sourcesFilter: {
35+
includeSources: ["pendle", "li-fi", "open-ocean"],
36+
},
37+
},
38+
match: { isPendlePT: true },
3439
},
3540
{
3641
strategy: StrategyMTBILL.name(),
@@ -130,6 +135,7 @@ const mainnetRoutingConfig: ChainRoutingConfig = [
130135
"uniswap",
131136
],
132137
},
138+
tryExactOut: true,
133139
},
134140
match: {},
135141
},

src/swapService/runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function runPipeline(
6262
}
6363

6464
// TODO timeouts on balmy
65+
// TODO review and add sources
6566
// TODO tokenlist, interfaces
6667
// TODO price impact
6768
// TODO cache pipeline, tokenlists

src/swapService/strategies/balmySDK/customSourceList.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IFetchService, IProviderService } from "@balmy/sdk"
22
import { LocalSourceList } from "@balmy/sdk/dist/services/quotes/source-lists/local-source-list"
33
import { CustomLiFiQuoteSource } from "./lifiQuoteSource"
44
import { CustomOneInchQuoteSource } from "./oneInchQuoteSource"
5+
import { CustomPendleQuoteSource } from "./pendleQuoteSource"
56
type ConstructorParameters = {
67
providerService: IProviderService
78
fetchService: IFetchService
@@ -10,6 +11,7 @@ type ConstructorParameters = {
1011
const customSources = {
1112
"1inch": new CustomOneInchQuoteSource(),
1213
"li-fi": new CustomLiFiQuoteSource(),
14+
pendle: new CustomPendleQuoteSource(),
1315
}
1416
export class CustomSourceList extends LocalSourceList {
1517
constructor({ providerService, fetchService }: ConstructorParameters) {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { findToken } from "@/swapService/utils"
2+
import { Chains } from "@balmy/sdk"
3+
import { isSameAddress } from "@balmy/sdk"
4+
import type {
5+
BuildTxParams,
6+
IQuoteSource,
7+
QuoteParams,
8+
QuoteSourceMetadata,
9+
SourceQuoteResponse,
10+
SourceQuoteTransaction,
11+
} from "@balmy/sdk/dist/services/quotes/quote-sources/types"
12+
import {
13+
addQuoteSlippage,
14+
calculateAllowanceTarget,
15+
failed,
16+
} from "@balmy/sdk/dist/services/quotes/quote-sources/utils"
17+
import qs from "qs"
18+
import { Address, getAddress } from "viem"
19+
20+
// https://api-v2.pendle.finance/core/docs#/Chains/ChainsController_getSupportedChainIds
21+
export const PENDLE_METADATA: QuoteSourceMetadata<PendleSupport> = {
22+
name: "Pendle",
23+
supports: {
24+
chains: [
25+
Chains.ETHEREUM.chainId,
26+
Chains.OPTIMISM.chainId,
27+
Chains.BNB_CHAIN.chainId,
28+
Chains.MANTLE.chainId,
29+
Chains.BASE.chainId,
30+
Chains.ARBITRUM.chainId,
31+
],
32+
swapAndTransfer: true,
33+
buyOrders: false,
34+
},
35+
logoURI: "",
36+
}
37+
type PendleSupport = { buyOrders: false; swapAndTransfer: true }
38+
type CustomOrAPIKeyConfig =
39+
| { customUrl: string; apiKey?: undefined }
40+
| { customUrl?: undefined; apiKey: string }
41+
type PendleConfig = CustomOrAPIKeyConfig
42+
type PendleData = { tx: SourceQuoteTransaction }
43+
export class CustomPendleQuoteSource
44+
implements IQuoteSource<PendleSupport, PendleConfig, PendleData>
45+
{
46+
getMetadata() {
47+
return PENDLE_METADATA
48+
}
49+
50+
async quote(
51+
params: QuoteParams<PendleSupport, PendleConfig>,
52+
): Promise<SourceQuoteResponse<PendleData>> {
53+
const { dstAmount, to, data } = await this.getQuote(params)
54+
55+
const quote = {
56+
sellAmount: params.request.order.sellAmount,
57+
buyAmount: BigInt(dstAmount),
58+
allowanceTarget: calculateAllowanceTarget(params.request.sellToken, to),
59+
customData: {
60+
tx: {
61+
to,
62+
calldata: data,
63+
},
64+
},
65+
}
66+
67+
return addQuoteSlippage(
68+
quote,
69+
params.request.order.type,
70+
params.request.config.slippagePercentage,
71+
)
72+
}
73+
74+
async buildTx({
75+
request,
76+
}: BuildTxParams<PendleConfig, PendleData>): Promise<SourceQuoteTransaction> {
77+
return request.customData.tx
78+
}
79+
80+
private async getQuote({
81+
components: { fetchService },
82+
request: {
83+
chainId,
84+
sellToken,
85+
buyToken,
86+
order,
87+
config: { slippagePercentage, timeout },
88+
accounts: { takeFrom, recipient },
89+
},
90+
config,
91+
}: QuoteParams<PendleSupport, PendleConfig>) {
92+
const queryParams = {
93+
receiver: recipient || takeFrom,
94+
slippage: slippagePercentage / 100, // 1 = 100%
95+
enableAggregator: true,
96+
tokenIn: sellToken,
97+
tokenOut: buyToken,
98+
amountIn: order.sellAmount.toString(),
99+
}
100+
101+
const queryString = qs.stringify(queryParams, {
102+
skipNulls: true,
103+
arrayFormat: "comma",
104+
})
105+
const tokenIn = findToken(chainId, getAddress(sellToken))
106+
const tokenOut = findToken(chainId, getAddress(buyToken))
107+
108+
const pendleMarket =
109+
tokenIn.meta?.pendleMarket || tokenOut.meta?.pendleMarket
110+
111+
const url = `${getUrl()}/${chainId}/markets/${pendleMarket}/swap?${queryString}`
112+
const response = await fetchService.fetch(url, {
113+
timeout,
114+
headers: getHeaders(config),
115+
})
116+
117+
if (!response.ok) {
118+
failed(
119+
PENDLE_METADATA,
120+
chainId,
121+
sellToken,
122+
buyToken,
123+
(await response.text()) || `Failed with status ${response.status}`,
124+
)
125+
}
126+
const {
127+
data: { amountOut: dstAmount },
128+
tx: { to, data },
129+
} = await response.json()
130+
131+
return { dstAmount, to, data }
132+
}
133+
134+
isConfigAndContextValidForQuoting(
135+
config: Partial<PendleConfig> | undefined,
136+
): config is PendleConfig {
137+
return true
138+
}
139+
140+
isConfigAndContextValidForTxBuilding(
141+
config: Partial<PendleConfig> | undefined,
142+
): config is PendleConfig {
143+
return true
144+
}
145+
}
146+
147+
function getUrl() {
148+
return "https://api-v2.pendle.finance/core/v1/sdk"
149+
}
150+
151+
function getHeaders(config: PendleConfig) {
152+
const headers: Record<string, string> = {
153+
accept: "application/json",
154+
}
155+
if (config.apiKey) {
156+
headers["Authorization"] = `Bearer ${config.apiKey}`
157+
}
158+
return headers
159+
}

src/swapService/strategies/strategyBalmySDK.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,13 @@ export class StrategyBalmySDK {
9595
"li-fi": {
9696
apiKey: String(process.env.LIFI_API_KEY),
9797
},
98+
pendle: {
99+
apiKey: String(process.env.PENDLE_API_KEY),
100+
},
98101
},
99102
},
100103
},
101-
}
104+
} as BuildParams
102105
this.sdk = buildSDK(buildParams)
103106
this.match = match
104107
this.config = config
@@ -292,6 +295,7 @@ export class StrategyBalmySDK {
292295
sourcesFilter?: SourcesFilter,
293296
) {
294297
// TODO type
298+
// console.log('this.sdk.quoteService: ', this.sdk.quoteService);
295299
const bestQuote = await this.sdk.quoteService.getBestQuote({
296300
request: this.#getSDKQuoteFromSwapParams(swapParams, sourcesFilter),
297301
config: {

src/swapService/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export function matchParams(
6565
if (match.isRepay) {
6666
if (swapParams.isRepay !== match.isRepay) return false
6767
}
68+
if (match.isPendlePT) {
69+
if (
70+
!swapParams.tokenIn.meta?.isPendlePT &&
71+
!swapParams.tokenOut.meta?.isPendlePT
72+
)
73+
return false
74+
}
6875

6976
return true
7077
}

0 commit comments

Comments
 (0)