Skip to content

Commit 78991dd

Browse files
committed
add unichain support
1 parent 05a70cc commit 78991dd

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

.env.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ RPC_URL_31337=""
2626
RPC_URL_146=""
2727
# bera
2828
RPC_URL_80094=""
29+
# unichain
30+
RPC_URL_130=""
2931

3032
# Provider API keys
3133
LIFI_API_KEY=""
@@ -37,4 +39,5 @@ OKX_PASSPHRASE=""
3739
OKX_SECRET_KEY=""
3840
ODOS_API_KEY=""
3941
ODOS_REFERRAL_CODE=""
40-
OOGABOOGA_API_KEY=""
42+
OOGABOOGA_API_KEY=""
43+
OX_API_KEY=""

src/common/utils/contractBook.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const contractBook: any = {
1010
[1923]: "0x05Eb1A647265D974a1B0A57206048312604Ac6C3",
1111
[146]: "0xbAf5B12c92711a3657DD4adA6b3C7801e83Bb56a",
1212
[80094]: "0x4A35e6A872cf35623cd3fD07ebECEDFc0170D705",
13+
[130]: "0x319E8ecd3BaB57fE684ca1aCfaB60c5603087B3A",
1314
},
1415
},
1516
swapVerifier: {
@@ -21,6 +22,7 @@ const contractBook: any = {
2122
[1923]: "0x392C1570b3Bf29B113944b759cAa9a9282DA12Fe",
2223
[146]: "0x003ef4048b45a5A79D4499aaBd52108B3Bc9209f",
2324
[80094]: "0x6fFf8Ac4AB123B62FF5e92aBb9fF702DCBD6C939",
25+
[130]: "0x7eaf8C22480129E5D7426e3A33880D7bE19B50a7",
2426
},
2527
},
2628
}

src/swapService/config/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const defaultRoutingConfig: ChainRoutingConfig = [
2323
"li-fi",
2424
"open-ocean",
2525
"uniswap",
26+
"0x",
2627
],
2728
},
2829
},

src/swapService/strategies/balmySDK/customSourceList.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CustomOneInchQuoteSource } from "./oneInchQuoteSource"
77
import { CustomOogaboogaQuoteSource } from "./oogaboogaQuoteSource"
88
import { CustomOpenOceanQuoteSource } from "./openOceanQuoteSource"
99
import { CustomPendleQuoteSource } from "./pendleQuoteSource"
10+
import { CustomUniswapQuoteSource } from "./uniswapQuoteSource"
1011

1112
type ConstructorParameters = {
1213
providerService: IProviderService
@@ -21,6 +22,7 @@ const customSources = {
2122
neptune: new CustomNeptuneQuoteSource(),
2223
odos: new CustomOdosQuoteSource(),
2324
oogabooga: new CustomOogaboogaQuoteSource(),
25+
uniswap: new CustomUniswapQuoteSource(),
2426
}
2527
export class CustomSourceList extends LocalSourceList {
2628
constructor({ providerService, fetchService }: ConstructorParameters) {
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { Chains, getChainByKey } from "@balmy/sdk"
2+
import type { ChainId, TokenAddress } from "@balmy/sdk"
3+
import { Addresses } from "@balmy/sdk"
4+
import { isSameAddress, subtractPercentage, timeToSeconds } from "@balmy/sdk"
5+
import { AlwaysValidConfigAndContextSource } from "@balmy/sdk/dist/services/quotes/quote-sources/base/always-valid-source"
6+
import type {
7+
BuildTxParams,
8+
QuoteParams,
9+
QuoteSourceMetadata,
10+
SourceQuoteResponse,
11+
SourceQuoteTransaction,
12+
} from "@balmy/sdk/dist/services/quotes/quote-sources/types"
13+
import {
14+
addQuoteSlippage,
15+
calculateAllowanceTarget,
16+
failed,
17+
} from "@balmy/sdk/dist/services/quotes/quote-sources/utils"
18+
import qs from "qs"
19+
import { encodeFunctionData, parseAbi } from "viem"
20+
21+
const ROUTER_ADDRESS: Record<ChainId, string> = {
22+
[Chains.ETHEREUM.chainId]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
23+
[Chains.OPTIMISM.chainId]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
24+
[Chains.POLYGON.chainId]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
25+
[Chains.ARBITRUM.chainId]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
26+
[Chains.CELO.chainId]: "0x5615CDAb10dc425a742d643d949a7F474C01abc4",
27+
[Chains.ETHEREUM_GOERLI.chainId]:
28+
"0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
29+
[Chains.POLYGON_MUMBAI.chainId]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
30+
[Chains.BNB_CHAIN.chainId]: "0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2",
31+
[Chains.BASE.chainId]: "0x2626664c2603336E57B271c5C0b26F421741e481",
32+
[Chains.AVALANCHE.chainId]: "0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE",
33+
[130]: "0x73855d06de49d0fe4a9c42636ba96c62da12ff9c",
34+
}
35+
36+
const UNISWAP_METADATA: QuoteSourceMetadata<UniswapSupport> = {
37+
name: "Uniswap",
38+
supports: {
39+
chains: Object.keys(ROUTER_ADDRESS).map(Number),
40+
swapAndTransfer: true,
41+
buyOrders: true,
42+
},
43+
logoURI: "ipfs://QmNa3YBYAYS5qSCLuXataV5XCbtxP9ZB4rHUfomRxrpRhJ",
44+
}
45+
type UniswapSupport = { buyOrders: true; swapAndTransfer: true }
46+
type UniswapConfig = object
47+
type UniswapData = { tx: SourceQuoteTransaction }
48+
export class CustomUniswapQuoteSource extends AlwaysValidConfigAndContextSource<
49+
UniswapSupport,
50+
UniswapConfig,
51+
UniswapData
52+
> {
53+
getMetadata() {
54+
return UNISWAP_METADATA
55+
}
56+
57+
async quote({
58+
components: { fetchService },
59+
request: {
60+
chainId,
61+
sellToken,
62+
buyToken,
63+
order,
64+
config: { slippagePercentage, timeout, txValidFor },
65+
accounts: { takeFrom, recipient },
66+
},
67+
}: QuoteParams<UniswapSupport>): Promise<SourceQuoteResponse<UniswapData>> {
68+
const amount = order.type === "sell" ? order.sellAmount : order.buyAmount
69+
const isSellTokenNativeToken = isSameAddress(
70+
sellToken,
71+
Addresses.NATIVE_TOKEN,
72+
)
73+
const isBuyTokenNativeToken = isSameAddress(
74+
buyToken,
75+
Addresses.NATIVE_TOKEN,
76+
)
77+
if (isSellTokenNativeToken && order.type === "buy") {
78+
// We do this because it's very hard and expensive to wrap native to wToken, spend only
79+
// some of it and then return the extra native token to the caller
80+
throw new Error("Uniswap does not support buy orders with native token")
81+
}
82+
const router = ROUTER_ADDRESS[chainId]
83+
recipient = recipient ?? takeFrom
84+
85+
const queryParams = {
86+
protocols: "v2,v3,mixed",
87+
tokenInAddress: mapToWTokenIfNecessary(chainId, sellToken),
88+
tokenInChainId: chainId,
89+
tokenOutAddress: mapToWTokenIfNecessary(chainId, buyToken),
90+
tokenOutChainId: chainId,
91+
amount: amount.toString(),
92+
type: order.type === "sell" ? "exactIn" : "exactOut",
93+
recipient: isBuyTokenNativeToken ? router : recipient,
94+
deadline: timeToSeconds(txValidFor ?? "3h"),
95+
slippageTolerance: slippagePercentage,
96+
}
97+
98+
const queryString = qs.stringify(queryParams, {
99+
skipNulls: true,
100+
arrayFormat: "comma",
101+
})
102+
103+
// These are needed so that the API allows us to make the call
104+
const headers = {
105+
origin: "https://app.uniswap.org",
106+
referer: "https://app.uniswap.org/",
107+
}
108+
const url = `https://api.uniswap.org/v1/quote?${queryString}`
109+
const response = await fetchService.fetch(url, { headers, timeout })
110+
if (!response.ok) {
111+
failed(
112+
UNISWAP_METADATA,
113+
chainId,
114+
sellToken,
115+
buyToken,
116+
await response.text(),
117+
)
118+
}
119+
let {
120+
quote: quoteAmount,
121+
methodParameters: { calldata },
122+
} = await response.json()
123+
124+
const sellAmount =
125+
order.type === "sell" ? order.sellAmount : BigInt(quoteAmount)
126+
const buyAmount =
127+
order.type === "sell" ? BigInt(quoteAmount) : order.buyAmount
128+
const value = isSellTokenNativeToken ? sellAmount : undefined
129+
130+
if (isBuyTokenNativeToken) {
131+
// Use multicall to unwrap wToken
132+
const minBuyAmount = calculateMinBuyAmount(
133+
order.type,
134+
buyAmount,
135+
slippagePercentage,
136+
)
137+
const unwrapData = encodeFunctionData({
138+
abi: ROUTER_ABI,
139+
functionName: "unwrapWETH9",
140+
args: [minBuyAmount, recipient],
141+
})
142+
const multicallData = encodeFunctionData({
143+
abi: ROUTER_ABI,
144+
functionName: "multicall",
145+
args: [[calldata, unwrapData]],
146+
})
147+
148+
// Update calldata and gas estimate
149+
calldata = multicallData!
150+
}
151+
152+
const quote = {
153+
sellAmount,
154+
buyAmount,
155+
allowanceTarget: calculateAllowanceTarget(sellToken, router),
156+
customData: {
157+
tx: {
158+
to: router,
159+
calldata,
160+
value,
161+
},
162+
},
163+
}
164+
165+
return addQuoteSlippage(quote, order.type, slippagePercentage)
166+
}
167+
168+
async buildTx({
169+
request,
170+
}: BuildTxParams<
171+
UniswapConfig,
172+
UniswapData
173+
>): Promise<SourceQuoteTransaction> {
174+
return request.customData.tx
175+
}
176+
}
177+
178+
function calculateMinBuyAmount(
179+
type: "sell" | "buy",
180+
buyAmount: bigint,
181+
slippagePercentage: number,
182+
) {
183+
return type === "sell"
184+
? BigInt(subtractPercentage(buyAmount, slippagePercentage, "up"))
185+
: buyAmount
186+
}
187+
188+
function mapToWTokenIfNecessary(chainId: ChainId, address: TokenAddress) {
189+
const chain = getChainByKey(chainId)
190+
return chain && isSameAddress(address, Addresses.NATIVE_TOKEN)
191+
? chain.wToken
192+
: address
193+
}
194+
195+
const ROUTER_HUMAN_READABLE_ABI = [
196+
"function unwrapWETH9(uint256 amountMinimum, address recipient) payable",
197+
"function multicall(bytes[] data) payable returns (bytes[] memory results)",
198+
]
199+
200+
const ROUTER_ABI = parseAbi(ROUTER_HUMAN_READABLE_ABI)

src/swapService/strategies/strategyBalmySDK.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ export class StrategyBalmySDK {
129129
oogabooga: {
130130
apiKey: String(process.env.OOGABOOGA_API_KEY),
131131
},
132+
"0x": {
133+
apiKey: String(process.env.OX_API_KEY),
134+
},
132135
},
133136
},
134137
},

src/tokenLists/tokenList_130.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"addressInfo": "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
4+
"chainId": 130,
5+
"name": "USDC",
6+
"symbol": "USDC",
7+
"decimals": 6,
8+
"logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
9+
"meta": {}
10+
},
11+
{
12+
"addressInfo": "0x4200000000000000000000000000000000000006",
13+
"chainId": 130,
14+
"name": "WETH",
15+
"symbol": "WETH",
16+
"decimals": 18,
17+
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
18+
"meta": {}
19+
}
20+
]

0 commit comments

Comments
 (0)