Skip to content
This repository has been archived by the owner on Apr 25, 2024. It is now read-only.

feat: add router trade adapter #168

Merged
merged 28 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@uniswap/universal-router-sdk",
"version": "1.8.2",
"version": "2.0.0",
"description": "sdk for integrating with the Universal Router contracts",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -53,7 +53,7 @@
"@uniswap/router-sdk": "^1.9.0",
"@uniswap/sdk-core": "^4.2.0",
"@uniswap/universal-router": "1.6.0",
"@uniswap/v2-sdk": "^4.2.0",
"@uniswap/v2-sdk": "^4.3.0",
"@uniswap/v3-sdk": "^3.11.0",
"bignumber.js": "^9.0.2",
"ethers": "^5.3.1"
Expand Down
27 changes: 17 additions & 10 deletions src/entities/protocols/uniswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export type FlatFeeOptions = {
// the existing router permit object doesn't include enough data for permit2
// so we extend swap options with the permit2 permit
// when safe mode is enabled, the SDK will add an extra ETH sweep for security
// when useRouterBalance is enabled the SDK will use the balance in the router for the swap
export type SwapOptions = Omit<RouterSwapOptions, 'inputTokenPermit'> & {
useRouterBalance?: boolean
inputTokenPermit?: Permit2Permit
flatFee?: FlatFeeOptions
safeMode?: boolean
Expand All @@ -48,22 +50,28 @@ interface Swap<TInput extends Currency, TOutput extends Currency> {
// also translates trade objects from previous (v2, v3) SDKs
export class UniswapTrade implements Command {
readonly tradeType: RouterTradeType = RouterTradeType.UniswapTrade
readonly payerIsUser: boolean

constructor(public trade: RouterTrade<Currency, Currency, TradeType>, public options: SwapOptions) {
if (!!options.fee && !!options.flatFee) throw new Error('Only one fee option permitted')

if (this.inputRequiresWrap) this.payerIsUser = false
else if (this.options.useRouterBalance) this.payerIsUser = false
else this.payerIsUser = true
}

encode(planner: RoutePlanner, _config: TradeConfig): void {
let payerIsUser = true
get inputRequiresWrap(): boolean {
return this.trade.inputAmount.currency.isNative
}

encode(planner: RoutePlanner, _config: TradeConfig): void {
// If the input currency is the native currency, we need to wrap it with the router as the recipient
if (this.trade.inputAmount.currency.isNative) {
if (this.inputRequiresWrap) {
// TODO: optimize if only one v2 pool we can directly send this to the pool
planner.addCommand(CommandType.WRAP_ETH, [
ROUTER_AS_RECIPIENT,
this.trade.maximumAmountIn(this.options.slippageTolerance).quotient.toString(),
])
// since WETH is now owned by the router, the router pays for inputs
payerIsUser = false
}
// The overall recipient at the end of the trade, SENDER_AS_RECIPIENT uses the msg.sender
this.options.recipient = this.options.recipient ?? SENDER_AS_RECIPIENT
Expand All @@ -75,19 +83,18 @@ export class UniswapTrade implements Command {
const performAggregatedSlippageCheck =
this.trade.tradeType === TradeType.EXACT_INPUT && this.trade.routes.length > 2
const outputIsNative = this.trade.outputAmount.currency.isNative
const inputIsNative = this.trade.inputAmount.currency.isNative
const routerMustCustody = performAggregatedSlippageCheck || outputIsNative || hasFeeOption(this.options)

for (const swap of this.trade.swaps) {
switch (swap.route.protocol) {
case Protocol.V2:
addV2Swap(planner, swap, this.trade.tradeType, this.options, payerIsUser, routerMustCustody)
addV2Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
break
case Protocol.V3:
addV3Swap(planner, swap, this.trade.tradeType, this.options, payerIsUser, routerMustCustody)
addV3Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
break
case Protocol.MIXED:
addMixedSwap(planner, swap, this.trade.tradeType, this.options, payerIsUser, routerMustCustody)
addMixedSwap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
break
default:
throw new Error('UNSUPPORTED_TRADE_PROTOCOL')
Expand Down Expand Up @@ -149,7 +156,7 @@ export class UniswapTrade implements Command {
}
}

if (inputIsNative && (this.trade.tradeType === TradeType.EXACT_OUTPUT || riskOfPartialFill(this.trade))) {
if (this.inputRequiresWrap && (this.trade.tradeType === TradeType.EXACT_OUTPUT || riskOfPartialFill(this.trade))) {
// for exactOutput swaps that take native currency as input
// we need to send back the change to the user
planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, 0])
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { SwapRouter } from './swapRouter'
export * from './entities'
export * from './utils/routerTradeAdapter'
export { RoutePlanner, CommandType } from './utils/routerCommands'
export {
UNIVERSAL_ROUTER_ADDRESS,
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'

export const CONTRACT_BALANCE = BigNumber.from(2).pow(255)
export const ETH_ADDRESS = '0x0000000000000000000000000000000000000000'
export const E_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
export const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1)
export const MAX_UINT160 = BigNumber.from(2).pow(160).sub(1)
Expand Down
206 changes: 206 additions & 0 deletions src/utils/routerTradeAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { MixedRouteSDK, Trade as RouterTrade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Ether, Token, TradeType } from '@uniswap/sdk-core'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { Pool, Route as V3Route, FeeAmount } from '@uniswap/v3-sdk'
import { BigNumber } from 'ethers'
import { ETH_ADDRESS, E_ETH_ADDRESS } from './constants'

export type TokenInRoute = {
address: string
chainId: number
symbol: string
decimals: string
name?: string
buyFeeBps?: string
sellFeeBps?: string
}

export enum PoolType {
V2Pool = 'v2-pool',
V3Pool = 'v3-pool',
}

export type V2Reserve = {
token: TokenInRoute
quotient: string
}

export type V2PoolInRoute = {
type: PoolType.V2Pool
address?: string
tokenIn: TokenInRoute
tokenOut: TokenInRoute
reserve0: V2Reserve
reserve1: V2Reserve
amountIn?: string
amountOut?: string
}

export type V3PoolInRoute = {
type: PoolType.V3Pool
address?: string
tokenIn: TokenInRoute
tokenOut: TokenInRoute
sqrtRatioX96: string
liquidity: string
tickCurrent: string
fee: string
amountIn?: string
amountOut?: string
}

export type PartialClassicQuote = {
// We need tokenIn/Out to support native currency
tokenIn: string
tokenOut: string
tradeType: TradeType
route: Array<(V3PoolInRoute | V2PoolInRoute)[]>
}

interface RouteResult {
routev3: V3Route<Currency, Currency> | null
routev2: V2Route<Currency, Currency> | null
mixedRoute: MixedRouteSDK<Currency, Currency> | null
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}

export const isNativeCurrency = (address: string) =>
address.toLowerCase() === ETH_ADDRESS.toLowerCase() || address.toLowerCase() === E_ETH_ADDRESS.toLowerCase()

// Helper class to convert routing-specific quote entities to RouterTrade entities
// the returned RouterTrade can then be used to build the UniswapTrade entity in this package
export class RouterTradeAdapter {
// Generate a RouterTrade using fields from a classic quote response
static fromClassicQuote(quote: PartialClassicQuote) {
const { route, tokenIn, tokenOut } = quote

if (!route) throw new Error('Expected route to be present')
if (!route.length) throw new Error('Expected there to be at least one route')
if (route.some((r) => !r.length)) throw new Error('Expected all routes to have at least one pool')
const firstRoute = route[0]

const tokenInData = firstRoute[0].tokenIn
const tokenOutData = firstRoute[firstRoute.length - 1].tokenOut

if (!tokenInData || !tokenOutData) throw new Error('Expected both tokenIn and tokenOut to be present')
if (tokenInData.chainId !== tokenOutData.chainId)
throw new Error('Expected tokenIn and tokenOut to be have same chainId')

const parsedCurrencyIn = RouterTradeAdapter.toCurrency(isNativeCurrency(tokenIn), tokenInData)
const parsedCurrencyOut = RouterTradeAdapter.toCurrency(isNativeCurrency(tokenOut), tokenOutData)

const typedRoutes: RouteResult[] = route.map((subRoute) => {
const rawAmountIn = subRoute[0].amountIn
const rawAmountOut = subRoute[subRoute.length - 1].amountOut

if (!rawAmountIn || !rawAmountOut) {
throw new Error('Expected both raw amountIn and raw amountOut to be present')
}

const inputAmount = CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn)
const outputAmount = CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut)

const isOnlyV2 = RouterTradeAdapter.isVersionedRoute<V2PoolInRoute>(PoolType.V2Pool, subRoute)
const isOnlyV3 = RouterTradeAdapter.isVersionedRoute<V3PoolInRoute>(PoolType.V3Pool, subRoute)

return {
routev3: isOnlyV3
? new V3Route(
(subRoute as V3PoolInRoute[]).map(RouterTradeAdapter.toPool),
parsedCurrencyIn,
parsedCurrencyOut
)
: null,
routev2: isOnlyV2
? new V2Route(
(subRoute as V2PoolInRoute[]).map(RouterTradeAdapter.toPair),
parsedCurrencyIn,
parsedCurrencyOut
)
: null,
mixedRoute:
!isOnlyV3 && !isOnlyV2
? new MixedRouteSDK(subRoute.map(RouterTradeAdapter.toPoolOrPair), parsedCurrencyIn, parsedCurrencyOut)
: null,
inputAmount,
outputAmount,
}
})

return new RouterTrade({
v2Routes: typedRoutes
.filter((route) => route.routev2)
.map((route) => ({
routev2: route.routev2 as V2Route<Currency, Currency>,
inputAmount: route.inputAmount,
outputAmount: route.outputAmount,
})),
v3Routes: typedRoutes
.filter((route) => route.routev3)
.map((route) => ({
routev3: route.routev3 as V3Route<Currency, Currency>,
inputAmount: route.inputAmount,
outputAmount: route.outputAmount,
})),
mixedRoutes: typedRoutes
.filter((route) => route.mixedRoute)
.map((route) => ({
mixedRoute: route.mixedRoute as MixedRouteSDK<Currency, Currency>,
inputAmount: route.inputAmount,
outputAmount: route.outputAmount,
})),
tradeType: quote.tradeType,
})
}

private static toCurrency(isNative: boolean, token: TokenInRoute): Currency {
if (isNative) {
return Ether.onChain(token.chainId)
}
return this.toToken(token)
}

private static toPoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
return pool.type === PoolType.V3Pool ? RouterTradeAdapter.toPool(pool) : RouterTradeAdapter.toPair(pool)
}

private static toToken(token: TokenInRoute): Token {
const { chainId, address, decimals, symbol, buyFeeBps, sellFeeBps } = token
return new Token(
chainId,
address,
parseInt(decimals.toString()),
symbol,
/* name */ undefined,
false,
buyFeeBps ? BigNumber.from(buyFeeBps) : undefined,
sellFeeBps ? BigNumber.from(sellFeeBps) : undefined
)
}

private static toPool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool {
return new Pool(
RouterTradeAdapter.toToken(tokenIn),
RouterTradeAdapter.toToken(tokenOut),
parseInt(fee) as FeeAmount,
sqrtRatioX96,
liquidity,
parseInt(tickCurrent)
)
}

private static toPair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => {
return new Pair(
CurrencyAmount.fromRawAmount(RouterTradeAdapter.toToken(reserve0.token), reserve0.quotient),
CurrencyAmount.fromRawAmount(RouterTradeAdapter.toToken(reserve1.token), reserve1.quotient)
)
}

private static isVersionedRoute<T extends V2PoolInRoute | V3PoolInRoute>(
type: PoolType,
route: (V3PoolInRoute | V2PoolInRoute)[]
): route is T[] {
return route.every((pool) => pool.type === type)
}
}
Loading
Loading