Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add uniswap swap quote provider #73

Merged
merged 32 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
048ddfb
feat: add empty uniswap adapter
janndriessen Jun 27, 2024
20aa784
build: add uniswap v3 sdk
janndriessen Jun 27, 2024
a2eb92f
test: getting quote amount out
janndriessen Jun 27, 2024
9c5130e
feat: add getting pools
janndriessen Jun 27, 2024
f23cebe
feat: return null if pool does not exist
janndriessen Jun 27, 2024
ab49768
lint
janndriessen Jun 27, 2024
541d8f0
feat: add first iteration of returning a quote
janndriessen Jun 28, 2024
13e12de
build: fix lock after rebase
janndriessen Jun 28, 2024
a67bb37
feat: add get path
janndriessen Jul 1, 2024
8dfcc4d
feat: use get path in uniswap adapter
janndriessen Jul 1, 2024
40cf20f
build: add tokenlists
janndriessen Jul 1, 2024
3747a1a
refactor: use tokenlists for token data
janndriessen Jul 1, 2024
5cf0d40
refactor: update swap data
janndriessen Jul 1, 2024
0665957
feat: add quote exact input/output via encoded path
janndriessen Jul 3, 2024
3b59c57
feat: add empty curve swap quote provider
janndriessen Jul 5, 2024
0917456
feat: add getting curve swap quotes
janndriessen Jul 8, 2024
a465e51
test: fix curve adapter tests
janndriessen Jul 10, 2024
be5963d
chore: remove logs
janndriessen Jul 11, 2024
df43731
feat: add empty index swap quote adapter
janndriessen Jul 11, 2024
84b6290
feat: add getting quotes for steth/eth
janndriessen Jul 11, 2024
2a6bb6f
feat: use curve swap quote provider for eth/steth only
janndriessen Jul 12, 2024
b1e8430
docs: remove todo for curve adapter
janndriessen Jul 17, 2024
f6cafd9
feat: add uniswap smart order router
janndriessen Jul 18, 2024
ae6b417
test: add tests for arbitrum swap quotes
janndriessen Jul 19, 2024
d2e9bbe
lint
janndriessen Jul 19, 2024
21644f2
docs
janndriessen Jul 19, 2024
75263b5
feat: use quickswap for uni v2
janndriessen Jul 22, 2024
aa04e59
test: fix arbitrum test
janndriessen Jul 22, 2024
151651b
feat: uniswap adapter should return null for same tokens
janndriessen Jul 22, 2024
87106db
test: add tests
janndriessen Jul 22, 2024
88514d0
chore: remove obsolete todo
janndriessen Jul 22, 2024
ec148c6
Merge branch 'main' into feat/add-uniswap-swap-quote-provider
janndriessen Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
})
33 changes: 33 additions & 0 deletions src/quote/swap/adapters/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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> {
const { inputToken, outputToken } = request
// 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)
}
const uniswapSwapQuoteProvider = new UniswapSwapQuoteProvider(this.rpcUrl)
return await uniswapSwapQuoteProvider.getSwapQuote(request)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a simple check to take Curve for ETH/stETH. Otherwise, use Uniswap V3 (or V2).

}
}
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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adapter we need for a few products that specifically use quotes of a Curve pool for ETH/stETH.

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be correct with what I saw in tests but please confirm @ckoopmann

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, should be correct.

I'm not 100% sure now at one point the condition fees.length = path.length - 1 is checked. It should only be checked for uniswap swaps, but if you do end up getting "path / fee length mismatch" errors, then just set fees: [0].
Just in case, shouldn't be necessary though.

pool: '0xdc24316b9ae028f1497c275eb9192a3ea0f67022',
}
}
167 changes: 167 additions & 0 deletions src/quote/swap/adapters/uniswap/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { EthAddress } from 'constants/addresses'
import { ChainId } from 'constants/chains'
import { USDC, WETH } from 'constants/tokens'
import { AlchemyProviderUrl, AlchemyProviderUrlArbitrum } from 'tests/utils'

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

const rpcUrl = AlchemyProviderUrl
const rpcUrlArbitrum = AlchemyProviderUrlArbitrum

const weth = WETH.address!
const usdc = USDC.address!
const ONE = '1000000000000000000'

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

test('getting a swap quote for a specified input amount', async () => {
const request = {
chainId: 1,
inputToken: usdc,
outputToken: weth,
inputAmount: '100000000',
}
const provider = new UniswapSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.UniV3)
expect(quote.swapData?.path.length).toBe(2)
expect(quote.swapData?.path).toEqual([
request.inputToken,
request.outputToken,
])
expect(quote.swapData?.fees.length).toBe(1)
expect(quote.inputAmount).toEqual(request.inputAmount)
expect(quote.inputAmount).not.toEqual(quote.outputAmount)
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.outputAmount) > BigInt(0)).toBe(true)
})

test('getting a swap quote for a specified input amount (stETH)', async () => {
const request = {
chainId: 1,
inputToken: usdc,
outputToken: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84',
inputAmount: '100000000',
}
const provider = new UniswapSwapQuoteProvider(rpcUrl)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData?.exchange).toBe(Exchange.Sushiswap)
expect(quote.swapData?.path.length).toBe(3)
expect(quote.swapData?.path).toEqual([
request.inputToken,
weth,
request.outputToken,
])
expect(quote.swapData?.fees).toEqual([3000, 3000])
// expect(quote.callData).not.toBe('0x')
expect(BigInt(quote.outputAmount) > BigInt(0)).toBe(true)
})

test('getting a swap quote for a specified input amount - Arbitrum', async () => {
const request = {
chainId: ChainId.Arbitrum,
inputToken: EthAddress,
outputToken: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', // wBTC
inputAmount: ONE,
}
const provider = new UniswapSwapQuoteProvider(rpcUrlArbitrum)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData).toEqual({
exchange: 3,
path: [
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
'0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
],
fees: [500],
pool: '0x0000000000000000000000000000000000000000',
})
// expect(quote.callData).not.toBe('0x')
expect(quote.inputAmount).toEqual(request.inputAmount)
expect(quote.inputAmount).not.toEqual(quote.outputAmount)
expect(BigInt(quote.outputAmount) > BigInt(0)).toBe(true)
})

test('getting a swap quote for a specified input amount - Arbitrum', async () => {
const request = {
chainId: ChainId.Arbitrum,
inputToken: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', // wBTC
outputToken: EthAddress,
inputAmount: ONE,
}
const provider = new UniswapSwapQuoteProvider(rpcUrlArbitrum)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData).toEqual({
exchange: 3,
path: [
'0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
'0x912CE59144191C1204E64559FE8253a0e49E6548',
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
],
fees: [3000, 500],
pool: '0x0000000000000000000000000000000000000000',
})
// expect(quote.callData).not.toBe('0x')
expect(quote.inputAmount).toEqual(request.inputAmount)
expect(quote.inputAmount).not.toEqual(quote.outputAmount)
expect(BigInt(quote.outputAmount) > BigInt(0)).toBe(true)
})

test('getting a swap quote for a specified input amount - Arbitrum', async () => {
const request = {
chainId: ChainId.Arbitrum,
inputToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH
outputToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC
outputAmount: '100000000',
}
const provider = new UniswapSwapQuoteProvider(rpcUrlArbitrum)
const quote = await provider.getSwapQuote(request)
if (!quote) fail()
expect(quote).not.toBeNull()
expect(quote.swapData).toEqual({
exchange: 3,
path: [
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
'0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
],
fees: [500],
pool: '0x0000000000000000000000000000000000000000',
})
// expect(quote.callData).not.toBe('0x')
expect(quote.outputAmount).toEqual(request.outputAmount)
expect(quote.inputAmount).not.toEqual(quote.outputAmount)
expect(BigInt(quote.inputAmount) > BigInt(0)).toBe(true)
})
})
Loading