Skip to content

Commit c70b2e4

Browse files
committed
add custom odos source
1 parent 3f8787e commit c70b2e4

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-0
lines changed

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 { CustomNeptuneQuoteSource } from "./neptuneQuoteSource"
5+
import { CustomOdosQuoteSource } from "./odosQuoteSource"
56
import { CustomOneInchQuoteSource } from "./oneInchQuoteSource"
67
import { CustomOpenOceanQuoteSource } from "./openOceanQuoteSource"
78
import { CustomPendleQuoteSource } from "./pendleQuoteSource"
@@ -17,6 +18,7 @@ const customSources = {
1718
pendle: new CustomPendleQuoteSource(),
1819
"open-ocean": new CustomOpenOceanQuoteSource(),
1920
neptune: new CustomNeptuneQuoteSource(),
21+
odos: new CustomOdosQuoteSource(),
2022
}
2123
export class CustomSourceList extends LocalSourceList {
2224
constructor({ providerService, fetchService }: ConstructorParameters) {
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import {
2+
type Address,
3+
Addresses,
4+
Chains,
5+
isSameAddress,
6+
timeoutPromise,
7+
} from "@balmy/sdk"
8+
import { AlwaysValidConfigAndContextSource } from "@balmy/sdk/dist/services/quotes/quote-sources/base/always-valid-source"
9+
import type {
10+
BuildTxParams,
11+
QuoteParams,
12+
QuoteSourceMetadata,
13+
SourceQuoteResponse,
14+
SourceQuoteTransaction,
15+
} from "@balmy/sdk/dist/services/quotes/quote-sources/types"
16+
import {
17+
addQuoteSlippage,
18+
calculateAllowanceTarget,
19+
checksum,
20+
failed,
21+
} from "@balmy/sdk/dist/services/quotes/quote-sources/utils"
22+
23+
// Supported Networks: https://docs.odos.xyz/#future-oriented-and-scalable
24+
const ODOS_METADATA: QuoteSourceMetadata<OdosSupport> = {
25+
name: "Odos",
26+
supports: {
27+
chains: [
28+
Chains.ETHEREUM.chainId,
29+
Chains.POLYGON.chainId,
30+
Chains.ARBITRUM.chainId,
31+
Chains.OPTIMISM.chainId,
32+
Chains.AVALANCHE.chainId,
33+
Chains.BNB_CHAIN.chainId,
34+
Chains.FANTOM.chainId,
35+
Chains.BASE_GOERLI.chainId,
36+
Chains.BASE.chainId,
37+
Chains.MODE.chainId,
38+
Chains.LINEA.chainId,
39+
Chains.MANTLE.chainId,
40+
Chains.SCROLL.chainId,
41+
],
42+
swapAndTransfer: true,
43+
buyOrders: false,
44+
},
45+
logoURI: "ipfs://Qma71evDJfVUSBU53qkf8eDDysUgojsZNSnFRWa4qWragz",
46+
}
47+
48+
type SourcesConfig =
49+
| { sourceAllowlist?: string[]; sourceDenylist?: undefined }
50+
| { sourceAllowlist?: undefined; sourceDenylist?: string[] }
51+
type OdosSupport = { buyOrders: false; swapAndTransfer: true }
52+
type OdosConfig = {
53+
supportRFQs?: boolean
54+
referralCode?: number
55+
} & SourcesConfig
56+
type OdosData = { tx: SourceQuoteTransaction }
57+
export class CustomOdosQuoteSource extends AlwaysValidConfigAndContextSource<
58+
OdosSupport,
59+
OdosConfig,
60+
OdosData
61+
> {
62+
getMetadata() {
63+
return ODOS_METADATA
64+
}
65+
66+
async quote(
67+
params: QuoteParams<OdosSupport, OdosConfig>,
68+
): Promise<SourceQuoteResponse<OdosData>> {
69+
// Note: Odos supports simple and advanced quotes. Simple quotes may offer worse prices, but it resolves faster. Since the advanced quote
70+
// might timeout, we will make two quotes (one simple and one advanced) and we'll return the simple one if the other one timeouts
71+
const simpleQuote = getQuote({ ...params, simple: true })
72+
const advancedQuote = timeoutPromise(
73+
getQuote({ ...params, simple: false }),
74+
params.request.config.timeout,
75+
{ reduceBy: "100ms" },
76+
)
77+
const [simple, advanced] = await Promise.allSettled([
78+
simpleQuote,
79+
advancedQuote,
80+
])
81+
82+
if (advanced.status === "fulfilled") {
83+
return advanced.value
84+
}
85+
if (simple.status === "fulfilled") {
86+
return simple.value
87+
}
88+
89+
return Promise.reject(simple.reason)
90+
}
91+
92+
async buildTx({
93+
request,
94+
}: BuildTxParams<OdosConfig, OdosData>): Promise<SourceQuoteTransaction> {
95+
return request.customData.tx
96+
}
97+
}
98+
99+
async function getQuote({
100+
simple,
101+
components: { fetchService },
102+
request: {
103+
chainId,
104+
sellToken,
105+
buyToken,
106+
order,
107+
accounts: { takeFrom, recipient },
108+
config: { slippagePercentage, timeout },
109+
},
110+
config,
111+
}: QuoteParams<OdosSupport, OdosConfig> & { simple: boolean }): Promise<
112+
SourceQuoteResponse<OdosData>
113+
> {
114+
const checksummedSell = checksumAndMapIfNecessary(sellToken)
115+
const checksummedBuy = checksumAndMapIfNecessary(buyToken)
116+
const userAddr = checksum(takeFrom)
117+
const quoteBody = {
118+
chainId,
119+
inputTokens: [
120+
{ tokenAddress: checksummedSell, amount: order.sellAmount.toString() },
121+
],
122+
outputTokens: [{ tokenAddress: checksummedBuy, proportion: 1 }],
123+
userAddr,
124+
slippageLimitPercent: slippagePercentage,
125+
sourceWhitelist: config?.sourceAllowlist,
126+
sourceBlacklist: config?.sourceDenylist,
127+
simulate: !config.disableValidation,
128+
pathViz: false,
129+
disableRFQs: !config?.supportRFQs, // Disable by default
130+
simple,
131+
...(config?.referralCode ? { referralCode: config?.referralCode } : {}),
132+
}
133+
134+
const [quoteResponse, routerResponse] = await Promise.all([
135+
fetchService.fetch("https://api.odos.xyz/sor/quote/v2", {
136+
body: JSON.stringify(quoteBody),
137+
method: "POST",
138+
headers: { "Content-Type": "application/json" },
139+
timeout,
140+
}),
141+
fetchService.fetch(`https://api.odos.xyz/info/router/v2/${chainId}`, {
142+
headers: { "Content-Type": "application/json" },
143+
timeout,
144+
}),
145+
])
146+
147+
if (!quoteResponse.ok) {
148+
failed(
149+
ODOS_METADATA,
150+
chainId,
151+
sellToken,
152+
buyToken,
153+
await quoteResponse.text(),
154+
)
155+
}
156+
if (!routerResponse.ok) {
157+
failed(
158+
ODOS_METADATA,
159+
chainId,
160+
sellToken,
161+
buyToken,
162+
await routerResponse.text(),
163+
)
164+
}
165+
const {
166+
pathId,
167+
gasEstimate,
168+
outAmounts: [outputTokenAmount],
169+
}: QuoteResponse = await quoteResponse.json()
170+
171+
const { address } = await routerResponse.json()
172+
173+
const assembleResponse = await fetchService.fetch(
174+
"https://api.odos.xyz/sor/assemble",
175+
{
176+
body: JSON.stringify({ userAddr, pathId, receiver: recipient }),
177+
method: "POST",
178+
headers: { "Content-Type": "application/json" },
179+
timeout,
180+
},
181+
)
182+
if (!assembleResponse.ok) {
183+
failed(
184+
ODOS_METADATA,
185+
chainId,
186+
sellToken,
187+
buyToken,
188+
await assembleResponse.text(),
189+
)
190+
}
191+
const {
192+
transaction: { data, to, value },
193+
}: AssemblyResponse = await assembleResponse.json()
194+
195+
const quote = {
196+
sellAmount: order.sellAmount,
197+
buyAmount: BigInt(outputTokenAmount),
198+
estimatedGas: BigInt(gasEstimate),
199+
allowanceTarget: calculateAllowanceTarget(sellToken, address),
200+
customData: {
201+
tx: {
202+
to,
203+
calldata: data,
204+
value: BigInt(value),
205+
},
206+
pathId,
207+
userAddr,
208+
recipient: recipient ? checksum(recipient) : userAddr,
209+
},
210+
}
211+
212+
return addQuoteSlippage(quote, "sell", slippagePercentage)
213+
}
214+
215+
function checksumAndMapIfNecessary(address: Address) {
216+
return isSameAddress(address, Addresses.NATIVE_TOKEN)
217+
? Addresses.ZERO_ADDRESS
218+
: checksum(address)
219+
}
220+
221+
type QuoteResponse = {
222+
gasEstimate: number
223+
pathId: string
224+
outAmounts: string[]
225+
}
226+
227+
type AssemblyResponse = {
228+
transaction: {
229+
to: Address
230+
data: string
231+
value: number
232+
}
233+
}

0 commit comments

Comments
 (0)