-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from 26 commits
048ddfb
20aa784
a2eb92f
9c5130e
f23cebe
ab49768
541d8f0
13e12de
a67bb37
8dfcc4d
40cf20f
3747a1a
5cf0d40
0665957
3b59c57
0917456
a465e51
be5963d
df43731
84b6290
2a6bb6f
b1e8430
f6cafd9
ae6b417
d2e9bbe
21644f2
75263b5
aa04e59
151651b
87106db
88514d0
ec148c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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) | ||
}) | ||
}) |
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) | ||
} | ||
} |
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) | ||
}) | ||
}) |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(), | ||
} | ||
} | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
pool: '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', | ||
} | ||
} |
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) | ||
}) | ||
}) |
There was a problem hiding this comment.
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).