Skip to content

Commit c2a2bd3

Browse files
committed
add custom okx source
1 parent e495cfd commit c2a2bd3

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

src/swapService/config/mainnet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const mainnetRoutingConfig: ChainRoutingConfig = [
6969
"li-fi",
7070
"open-ocean",
7171
"magpie",
72+
"okx",
7273
],
7374
},
7475
},

src/swapService/strategies/balmySDK/customSourceList.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CustomMagpieQuoteSource } from "./sources/magpieQuoteSource"
1515
import { CustomNeptuneQuoteSource } from "./sources/neptuneQuoteSource"
1616
import { CustomOdosQuoteSource } from "./sources/odosQuoteSource"
1717
import { CustomOkuQuoteSource } from "./sources/okuQuoteSource"
18+
import { CustomOKXDexQuoteSource } from "./sources/okxDexQuoteSource"
1819
import { CustomOneInchQuoteSource } from "./sources/oneInchQuoteSource"
1920
import { CustomOogaboogaQuoteSource } from "./sources/oogaboogaQuoteSource"
2021
import { CustomOpenOceanQuoteSource } from "./sources/openOceanQuoteSource"
@@ -38,6 +39,7 @@ const customSources = {
3839
magpie: new CustomMagpieQuoteSource(),
3940
kyberswap: new CustomKyberswapQuoteSource(),
4041
enso: new CustomEnsoQuoteSource(),
42+
"okx-dex": new CustomOKXDexQuoteSource(),
4143
oku_bob_icecreamswap: new CustomOkuQuoteSource(
4244
"icecreamswap",
4345
"IceCreamSwap",
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import crypto from "node:crypto"
2+
import {
3+
type Address,
4+
Addresses,
5+
type ChainId,
6+
Chains,
7+
type IFetchService,
8+
type TimeString,
9+
Uint,
10+
isSameAddress,
11+
} from "@balmy/sdk"
12+
import type {
13+
BuildTxParams,
14+
IQuoteSource,
15+
QuoteParams,
16+
QuoteSourceMetadata,
17+
SourceQuoteResponse,
18+
SourceQuoteTransaction,
19+
} from "@balmy/sdk/dist/services/quotes/quote-sources/types"
20+
import { failed } from "@balmy/sdk/dist/services/quotes/quote-sources/utils"
21+
import qs from "qs"
22+
23+
// https://www.okx.com/web3/build/docs/waas/okx-waas-supported-networks
24+
const SUPPORTED_CHAINS = [
25+
Chains.ETHEREUM,
26+
Chains.OPTIMISM,
27+
Chains.POLYGON,
28+
Chains.BNB_CHAIN,
29+
Chains.OKC,
30+
Chains.AVALANCHE,
31+
Chains.FANTOM,
32+
Chains.ARBITRUM,
33+
Chains.LINEA,
34+
Chains.BASE,
35+
Chains.SCROLL,
36+
Chains.BLAST,
37+
Chains.POLYGON_ZKEVM,
38+
Chains.FANTOM,
39+
Chains.MANTLE,
40+
Chains.METIS_ANDROMEDA,
41+
Chains.ZK_SYNC_ERA,
42+
Chains.SONIC,
43+
{
44+
chainId: 130,
45+
name: "Unichain",
46+
},
47+
]
48+
49+
const OKX_DEX_METADATA: QuoteSourceMetadata<OKXDexSupport> = {
50+
name: "OKX Dex",
51+
supports: {
52+
chains: SUPPORTED_CHAINS.map(({ chainId }) => chainId),
53+
swapAndTransfer: true,
54+
buyOrders: false,
55+
},
56+
logoURI: "ipfs://QmarS9mPPLegvNaazZ8Kqg1gLvkbsvQE2tkdF6uZCvBrFn",
57+
}
58+
type OKXDexConfig = { apiKey: string; secretKey: string; passphrase: string }
59+
type OKXDexSupport = { buyOrders: false; swapAndTransfer: true }
60+
type OKXDexData = { tx: SourceQuoteTransaction }
61+
export class CustomOKXDexQuoteSource
62+
implements IQuoteSource<OKXDexSupport, OKXDexConfig>
63+
{
64+
getMetadata() {
65+
return OKX_DEX_METADATA
66+
}
67+
68+
async quote({
69+
components,
70+
request,
71+
config,
72+
}: QuoteParams<OKXDexSupport, OKXDexConfig>): Promise<
73+
SourceQuoteResponse<OKXDexData>
74+
> {
75+
const [approvalTargetResponse, quoteResponse] = await Promise.all([
76+
calculateApprovalTarget({ components, request, config }),
77+
calculateQuote({ components, request, config }),
78+
])
79+
const {
80+
data: [
81+
{
82+
routerResult: { toTokenAmount },
83+
tx: { minReceiveAmount, to, value, data, gas },
84+
},
85+
],
86+
} = quoteResponse
87+
const {
88+
data: [{ dexContractAddress: approvalTarget }],
89+
} = approvalTargetResponse
90+
91+
return {
92+
sellAmount: request.order.sellAmount,
93+
maxSellAmount: request.order.sellAmount,
94+
buyAmount: BigInt(toTokenAmount),
95+
minBuyAmount: BigInt(minReceiveAmount),
96+
estimatedGas: BigInt(gas),
97+
allowanceTarget: approvalTarget,
98+
type: "sell",
99+
customData: {
100+
tx: {
101+
calldata: data,
102+
to,
103+
value: BigInt(value ?? 0),
104+
},
105+
},
106+
}
107+
}
108+
109+
async buildTx({
110+
request,
111+
}: BuildTxParams<OKXDexConfig, OKXDexData>): Promise<SourceQuoteTransaction> {
112+
return request.customData.tx
113+
}
114+
115+
isConfigAndContextValidForQuoting(
116+
config: Partial<OKXDexConfig> | undefined,
117+
): config is OKXDexConfig {
118+
return !!config?.apiKey && !!config?.passphrase && !!config?.secretKey
119+
}
120+
121+
isConfigAndContextValidForTxBuilding(
122+
config: Partial<OKXDexConfig> | undefined,
123+
): config is OKXDexConfig {
124+
return true
125+
}
126+
}
127+
128+
async function calculateApprovalTarget({
129+
components: { fetchService },
130+
request: {
131+
chainId,
132+
sellToken,
133+
buyToken,
134+
config: { timeout },
135+
},
136+
config,
137+
}: QuoteParams<OKXDexSupport, OKXDexConfig>) {
138+
if (isSameAddress(sellToken, Addresses.NATIVE_TOKEN)) {
139+
return { data: [{ dexContractAddress: Addresses.ZERO_ADDRESS }] }
140+
}
141+
const queryParams = {
142+
chainId,
143+
tokenContractAddress: sellToken,
144+
approveAmount: Uint.MAX_256,
145+
}
146+
const queryString = qs.stringify(queryParams, {
147+
skipNulls: true,
148+
arrayFormat: "comma",
149+
})
150+
const path = `/api/v5/dex/aggregator/approve-transaction?${queryString}`
151+
return fetch({
152+
sellToken,
153+
buyToken,
154+
chainId,
155+
path,
156+
timeout,
157+
config,
158+
fetchService,
159+
})
160+
}
161+
162+
async function calculateQuote({
163+
components: { fetchService },
164+
request: {
165+
chainId,
166+
sellToken,
167+
buyToken,
168+
order,
169+
config: { slippagePercentage, timeout },
170+
accounts: { takeFrom, recipient },
171+
},
172+
config,
173+
}: QuoteParams<OKXDexSupport, OKXDexConfig>) {
174+
const queryParams = {
175+
chainIndex: chainId,
176+
amount: order.sellAmount.toString(),
177+
fromTokenAddress: sellToken,
178+
toTokenAddress: buyToken,
179+
slippage: slippagePercentage / 100,
180+
userWalletAddress: takeFrom,
181+
swapReceiverAddress: recipient,
182+
}
183+
const queryString = qs.stringify(queryParams, {
184+
skipNulls: true,
185+
arrayFormat: "comma",
186+
})
187+
const path = `/api/v5/dex/aggregator/swap?${queryString}`
188+
return fetch({
189+
sellToken,
190+
buyToken,
191+
chainId,
192+
path,
193+
timeout,
194+
config,
195+
fetchService,
196+
})
197+
}
198+
199+
async function fetch({
200+
sellToken,
201+
buyToken,
202+
chainId,
203+
path,
204+
fetchService,
205+
config,
206+
timeout,
207+
}: {
208+
sellToken: Address
209+
buyToken: Address
210+
chainId: ChainId
211+
path: string
212+
timeout?: TimeString
213+
config: OKXDexConfig
214+
fetchService: IFetchService
215+
}) {
216+
const timestamp = new Date().toISOString()
217+
const toHash = `${timestamp}GET${path}`
218+
const sign = crypto
219+
.createHmac("sha256", config.secretKey)
220+
.update(toHash)
221+
.digest("base64")
222+
223+
const headers: HeadersInit = {
224+
["OK-ACCESS-KEY"]: config.apiKey,
225+
["OK-ACCESS-PASSPHRASE"]: config.passphrase,
226+
["OK-ACCESS-TIMESTAMP"]: timestamp,
227+
["OK-ACCESS-SIGN"]: sign,
228+
}
229+
230+
const url = `https://web3.okx.com${path}`
231+
const response = await fetchService.fetch(url, { timeout, headers })
232+
console.log("response: ", response.status)
233+
if (!response.ok) {
234+
failed(
235+
OKX_DEX_METADATA,
236+
chainId,
237+
sellToken,
238+
buyToken,
239+
await response.text(),
240+
)
241+
}
242+
return response.json()
243+
}

0 commit comments

Comments
 (0)