Skip to content

Commit

Permalink
Merge pull request #73 from IndexCoop/feat/add-uniswap-swap-quote-pro…
Browse files Browse the repository at this point in the history
…vider

feat: add uniswap swap quote provider
  • Loading branch information
janndriessen authored Jul 25, 2024
2 parents cb84390 + ec148c6 commit 923c7c6
Show file tree
Hide file tree
Showing 20 changed files with 3,563 additions and 905 deletions.
3,562 changes: 2,667 additions & 895 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@
"@ethersproject/contracts": "^5.6.2",
"@ethersproject/providers": "^5.6.8",
"@ethersproject/units": "^5.6.1",
"@indexcoop/tokenlists": "1.48.0",
"@lifi/sdk": "3.0.0-beta.1",
"@uniswap/sdk-core": "^5.3.1",
"@uniswap/smart-order-router": "^3.35.12",
"@uniswap/v3-sdk": "^3.13.1",
"axios": "^0.27.2",
"viem": "^2.10.2"
}
Expand Down
57 changes: 57 additions & 0 deletions src/quote/swap/adapters/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { ETH, stETH, WETH } from 'constants/tokens'
import { AlchemyProviderUrl } from 'tests/utils'

import { IndexSwapQuoteProvider } from './adapter'
import { Exchange } from 'utils'

const rpcUrl = AlchemyProviderUrl

// ETH/stETH
const curvePool = '0xdc24316b9ae028f1497c275eb9192a3ea0f67022'
const eth = ETH.address!
const steth = stETH.address!
const weth = WETH.address!
const ONE = '1000000000000000000'

describe('IndexSwapQuoteProvider', () => {
test('returns a swap quote for buying stETH with ETH', async () => {
const request = {
chainId: 1,
inputToken: eth,
outputToken: steth,
outputAmount: ONE,
}
const provider = new IndexSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.Curve)
expect(quote.swapData?.path.length).toBe(2)
expect(quote.swapData?.fees.length).toBe(0)
expect(quote.swapData?.path).toEqual([weth, steth])
expect(quote.swapData?.pool).toBe(curvePool)
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.inputAmount) > BigInt(0)).toBe(true)
})

test('returns a swap quote for selling stETH for ETH', async () => {
const request = {
chainId: 1,
inputToken: steth,
outputToken: eth,
inputAmount: ONE,
}
const provider = new IndexSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.Curve)
expect(quote.swapData?.path.length).toBe(2)
expect(quote.swapData?.fees.length).toBe(0)
expect(quote.swapData?.path).toEqual([weth, steth])
expect(quote.swapData?.pool).toBe(curvePool)
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.inputAmount) > BigInt(0)).toBe(true)
})
})
50 changes: 50 additions & 0 deletions src/quote/swap/adapters/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ETH, stETH } from 'constants/tokens'
import { CurveSwapQuoteProvider } from 'quote/swap/adapters/curve'
import { UniswapSwapQuoteProvider } from 'quote/swap/adapters/uniswap'
import {
SwapQuote,
SwapQuoteProvider,
SwapQuoteRequest,
} from 'quote/swap/interfaces'
import { isSameAddress } from 'utils'

export class IndexSwapQuoteProvider implements SwapQuoteProvider {
constructor(readonly rpcUrl: string) {}

public async getSwapQuote(
request: SwapQuoteRequest
): Promise<SwapQuote | null> {
let inputToken = request.inputToken
let outputToken = request.outputToken
if (inputToken === 'ETH') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
inputToken = ETH.address!
}
if (outputToken === 'ETH') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
outputToken = ETH.address!
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eth = ETH.address!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const stEth = stETH.address!
const isEth =
isSameAddress(inputToken, eth) || isSameAddress(outputToken, eth)
const isStEth =
isSameAddress(inputToken, stEth) || isSameAddress(outputToken, stEth)
if (isStEth && isEth) {
const curveSwapQuoteProvider = new CurveSwapQuoteProvider(this.rpcUrl)
return await curveSwapQuoteProvider.getSwapQuote({
...request,
inputToken,
outputToken,
})
}
const uniswapSwapQuoteProvider = new UniswapSwapQuoteProvider(this.rpcUrl)
return await uniswapSwapQuoteProvider.getSwapQuote({
...request,
inputToken,
outputToken,
})
}
}
57 changes: 57 additions & 0 deletions src/quote/swap/adapters/curve/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { ETH, stETH, WETH } from 'constants/tokens'
import { AlchemyProviderUrl } from 'tests/utils'

import { CurveSwapQuoteProvider } from './'
import { Exchange } from 'utils'

const rpcUrl = AlchemyProviderUrl

// ETH/stETH
const curvePool = '0xdc24316b9ae028f1497c275eb9192a3ea0f67022'
const eth = ETH.address!
const steth = stETH.address!
const weth = WETH.address!
const ONE = '1000000000000000000'

describe('CurveSwapQuoteProvider', () => {
test('getting a swap quote for buying stETH', async () => {
const request = {
chainId: 1,
inputToken: eth,
outputToken: steth,
outputAmount: ONE,
}
const provider = new CurveSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.Curve)
expect(quote.swapData?.path.length).toBe(2)
expect(quote.swapData?.fees.length).toBe(0)
expect(quote.swapData?.path).toEqual([weth, steth])
expect(quote.swapData?.pool).toBe(curvePool)
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.inputAmount) > BigInt(0)).toBe(true)
})

test('getting a swap quote for selling stETH', async () => {
const request = {
chainId: 1,
inputToken: steth,
outputToken: eth,
inputAmount: ONE,
}
const provider = new CurveSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.Curve)
expect(quote.swapData?.path.length).toBe(2)
expect(quote.swapData?.fees.length).toBe(0)
expect(quote.swapData?.path).toEqual([weth, steth])
expect(quote.swapData?.pool).toBe(curvePool)
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.inputAmount) > BigInt(0)).toBe(true)
})
})
53 changes: 53 additions & 0 deletions src/quote/swap/adapters/curve/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts'

import {
SwapQuoteProvider,
SwapQuoteRequest,
SwapQuote,
} from 'quote/swap/interfaces'
import { getRpcProvider } from 'utils/rpc-provider'

import { getSwapData } from './swap-data'

export class CurveSwapQuoteProvider implements SwapQuoteProvider {
constructor(readonly rpcUrl: string) {}

getPoolContract() {
const rpcProvider = getRpcProvider(this.rpcUrl)
const pool = '0xdc24316b9ae028f1497c275eb9192a3ea0f67022'
const abi = [
'function get_dy(int128 i, int128 j, uint256 dx) public view returns (uint256)',
]
return new Contract(pool, abi, rpcProvider)
}

async getSwapQuote(request: SwapQuoteRequest): Promise<SwapQuote | null> {
const {
chainId,
inputAmount,
inputToken,
outputAmount,
outputToken,
slippage,
} = request
const pool = this.getPoolContract()
let quoteAmount = BigNumber.from(0)
if (outputAmount) {
quoteAmount = await pool.get_dy(0, 1, BigNumber.from(outputAmount))
} else {
quoteAmount = await pool.get_dy(1, 0, BigNumber.from(inputAmount))
}
return {
chainId,
inputToken,
outputToken,
inputAmount: inputAmount ?? quoteAmount.toString(),
outputAmount: outputAmount ?? quoteAmount.toString(),
// Will not be used anywhere, so no need to return constructed call data
callData: '0x',
slippage: slippage ?? 0,
swapData: getSwapData(),
}
}
}
14 changes: 14 additions & 0 deletions src/quote/swap/adapters/curve/swap-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { stETH, WETH } from 'constants/tokens'
import { Exchange, SwapData } from 'utils'

export function getSwapData(): SwapData {
// The curve adapter is mostly just used for ETH/stETH swapping, so we can
// hard-code the return here.
return {
exchange: Exchange.Curve,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
path: [WETH.address!, stETH.address!],
fees: [], // not needed for curve
pool: '0xdc24316b9ae028f1497c275eb9192a3ea0f67022',
}
}
1 change: 1 addition & 0 deletions src/quote/swap/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './adapter'
export * from './zeroex'
Loading

0 comments on commit 923c7c6

Please sign in to comment.