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

Commit

Permalink
feat: add router trade adapter (#168)
Browse files Browse the repository at this point in the history
* initial commit - copy from external api

* Add constants and wire up test

* fix tests

* Add v2 tests

* yarn prettier

* comments

* .only

* Allow for payer as User as swapoptions

* bump major

* prettier fix

* back down to jsbi 3.1.4

* v2-sdk to 4.3.0

* Fix version to major and fix imorts

* Support 0xeee...eee for ether for trading api compatability

* Add split route test

* Address comments

* Add exact out, multi hop, split route tests

* prettier

* Add helper func

* Remove local native currency

* Add error handling tests

* Remove weth mapping

* fix mixed routes test

* fix imports

* prettier

* Fix comments

* prettier fix
  • Loading branch information
zhongeric authored Mar 29, 2024
1 parent fdfb320 commit afbaf31
Show file tree
Hide file tree
Showing 7 changed files with 860 additions and 19 deletions.
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

0 comments on commit afbaf31

Please sign in to comment.