Commit 14aeb6ed authored by Moody Salem's avatar Moody Salem Committed by GitHub

add a currency entity for ether (#26)

* add a currency entity

* revert test change, remove unnecessary `as any`

* remove unnecessary truthy assertions

* support currency amounts for input/output of trades

* support currency amounts for input/output of trades

* allow routes to encode if they end in ETH/WETH

* get the trade/route to support ether input/output

* router test

* add some todos

* working best trade exact in

* working best trade exact out

* remove only

* tests for exact in router methods

* complete the router tests

* add value as an output swap parameter
parent d8cc5865
import JSBI from 'jsbi'
import { SolidityType } from '../constants'
import { validateSolidityTypeInstance } from '../utils'
/**
* A currency is any fungible financial instrument on Ethereum, including Ether and all ERC20 tokens.
*
* The only instance of the base class `Currency` is Ether.
*/
export class Currency {
public readonly decimals: number
public readonly symbol?: string
public readonly name?: string
public static readonly ETHER: Currency = new Currency(18, 'ETH', 'Ether')
protected constructor(decimals: number, symbol?: string, name?: string) {
validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8)
this.decimals = decimals
this.symbol = symbol
this.name = name
}
}
const ETHER = Currency.ETHER
export { ETHER }
import { currencyEquals } from '../token'
import { Currency, ETHER } from '../currency'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import _Big from 'big.js'
import toFormat from 'toformat'
import { BigintIsh, Rounding, TEN, SolidityType } from '../../constants'
import { parseBigintIsh, validateSolidityTypeInstance } from '../../utils'
import { Fraction } from './fraction'
const Big = toFormat(_Big)
export class CurrencyAmount extends Fraction {
public readonly currency: Currency
/**
* Helper that calls the constructor with the ETHER currency
* @param amount ether amount in wei
*/
public static ether(amount: BigintIsh): CurrencyAmount {
return new CurrencyAmount(ETHER, amount)
}
// amount _must_ be raw, i.e. in the native representation
protected constructor(currency: Currency, amount: BigintIsh) {
const parsedAmount = parseBigintIsh(amount)
validateSolidityTypeInstance(parsedAmount, SolidityType.uint256)
super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(currency.decimals)))
this.currency = currency
}
get raw(): JSBI {
return this.numerator
}
add(other: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(this.currency, other.currency), 'TOKEN')
return new CurrencyAmount(this.currency, JSBI.add(this.raw, other.raw))
}
subtract(other: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(this.currency, other.currency), 'TOKEN')
return new CurrencyAmount(this.currency, JSBI.subtract(this.raw, other.raw))
}
toSignificant(significantDigits: number = 6, format?: object, rounding: Rounding = Rounding.ROUND_DOWN): string {
return super.toSignificant(significantDigits, format, rounding)
}
toFixed(
decimalPlaces: number = this.currency.decimals,
format?: object,
rounding: Rounding = Rounding.ROUND_DOWN
): string {
invariant(decimalPlaces <= this.currency.decimals, 'DECIMALS')
return super.toFixed(decimalPlaces, format, rounding)
}
toExact(format: object = { groupSeparator: '' }): string {
Big.DP = this.currency.decimals
return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format)
}
}
export * from './fraction'
export * from './percent'
export * from './tokenAmount'
export * from './currencyAmount'
export * from './price'
import { Token } from '../token'
import { TokenAmount } from './tokenAmount'
import { currencyEquals } from '../token'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { BigintIsh, Rounding, TEN } from '../../constants'
import { Token } from '../token'
import { Currency } from '../currency'
import { Route } from '../route'
import { Fraction } from './fraction'
import { TokenAmount } from './tokenAmount'
import { CurrencyAmount } from './currencyAmount'
export class Price extends Fraction {
public readonly baseToken: Token // input i.e. denominator
public readonly quoteToken: Token // output i.e. numerator
public readonly baseCurrency: Currency // input i.e. denominator
public readonly quoteCurrency: Currency // output i.e. numerator
public readonly scalar: Fraction // used to adjust the raw fraction w/r/t the decimals of the {base,quote}Token
static fromRoute(route: Route): Price {
......@@ -17,22 +20,22 @@ export class Price extends Fraction {
for (const [i, pair] of route.pairs.entries()) {
prices.push(
route.path[i].equals(pair.token0)
? new Price(pair.reserve0.token, pair.reserve1.token, pair.reserve0.raw, pair.reserve1.raw)
: new Price(pair.reserve1.token, pair.reserve0.token, pair.reserve1.raw, pair.reserve0.raw)
? new Price(pair.reserve0.currency, pair.reserve1.currency, pair.reserve0.raw, pair.reserve1.raw)
: new Price(pair.reserve1.currency, pair.reserve0.currency, pair.reserve1.raw, pair.reserve0.raw)
)
}
return prices.slice(1).reduce((accumulator, currentValue) => accumulator.multiply(currentValue), prices[0])
}
// denominator and numerator _must_ be raw, i.e. in the native representation
constructor(baseToken: Token, quoteToken: Token, denominator: BigintIsh, numerator: BigintIsh) {
constructor(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh) {
super(numerator, denominator)
this.baseToken = baseToken
this.quoteToken = quoteToken
this.baseCurrency = baseCurrency
this.quoteCurrency = quoteCurrency
this.scalar = new Fraction(
JSBI.exponentiate(TEN, JSBI.BigInt(baseToken.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteToken.decimals))
JSBI.exponentiate(TEN, JSBI.BigInt(baseCurrency.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteCurrency.decimals))
)
}
......@@ -45,19 +48,22 @@ export class Price extends Fraction {
}
invert(): Price {
return new Price(this.quoteToken, this.baseToken, this.numerator, this.denominator)
return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator)
}
multiply(other: Price): Price {
invariant(this.quoteToken.equals(other.baseToken), 'BASE')
invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN')
const fraction = super.multiply(other)
return new Price(this.baseToken, other.quoteToken, fraction.denominator, fraction.numerator)
return new Price(this.baseCurrency, other.quoteCurrency, fraction.denominator, fraction.numerator)
}
// performs floor division on overflow
quote(tokenAmount: TokenAmount): TokenAmount {
invariant(tokenAmount.token.equals(this.baseToken), 'TOKEN')
return new TokenAmount(this.quoteToken, super.multiply(tokenAmount.raw).quotient)
quote(currencyAmount: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN')
if (this.quoteCurrency instanceof Token) {
return new TokenAmount(this.quoteCurrency, super.multiply(currencyAmount.raw).quotient)
}
return CurrencyAmount.ether(super.multiply(currencyAmount.raw).quotient)
}
toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string {
......
import { CurrencyAmount } from './currencyAmount'
import { Token } from '../token'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import _Big from 'big.js'
import toFormat from 'toformat'
import { BigintIsh, Rounding, TEN, SolidityType } from '../../constants'
import { parseBigintIsh, validateSolidityTypeInstance } from '../../utils'
import { Token } from '../token'
import { Fraction } from './fraction'
const Big = toFormat(_Big)
import { BigintIsh } from '../../constants'
export class TokenAmount extends Fraction {
export class TokenAmount extends CurrencyAmount {
public readonly token: Token
// amount _must_ be raw, i.e. in the native representation
constructor(token: Token, amount: BigintIsh) {
const parsedAmount = parseBigintIsh(amount)
validateSolidityTypeInstance(parsedAmount, SolidityType.uint256)
super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(token.decimals)))
super(token, amount)
this.token = token
}
get raw(): JSBI {
return this.numerator
}
add(other: TokenAmount): TokenAmount {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.add(this.raw, other.raw))
......@@ -35,22 +23,4 @@ export class TokenAmount extends Fraction {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.subtract(this.raw, other.raw))
}
toSignificant(significantDigits: number = 6, format?: object, rounding: Rounding = Rounding.ROUND_DOWN): string {
return super.toSignificant(significantDigits, format, rounding)
}
toFixed(
decimalPlaces: number = this.token.decimals,
format?: object,
rounding: Rounding = Rounding.ROUND_DOWN
): string {
invariant(decimalPlaces <= this.token.decimals, 'DECIMALS')
return super.toFixed(decimalPlaces, format, rounding)
}
toExact(format: object = { groupSeparator: '' }): string {
Big.DP = this.token.decimals
return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format)
}
}
......@@ -2,5 +2,6 @@ export * from './token'
export * from './pair'
export * from './route'
export * from './trade'
export * from './currency'
export * from './fractions'
import { TokenAmount } from './fractions/tokenAmount'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks'
......@@ -15,13 +16,13 @@ import {
ONE,
FIVE,
_997,
_1000
_1000,
ChainId
} from '../constants'
import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { sqrt, parseBigintIsh } from '../utils'
import { InsufficientReservesError, InsufficientInputAmountError } from '../errors'
import { Token } from './token'
import { TokenAmount } from './fractions/tokenAmount'
let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {}
......@@ -75,6 +76,18 @@ export class Pair {
this.tokenAmounts = tokenAmounts as [TokenAmount, TokenAmount]
}
/**
* Returns true if the token is either token0 or token1
* @param token to check
*/
public involvesToken(token: Token): boolean {
return token.equals(this.token0) || token.equals(this.token1)
}
public get chainId(): ChainId {
return this.token0.chainId
}
get token0(): Token {
return this.tokenAmounts[0].token
}
......@@ -92,12 +105,12 @@ export class Pair {
}
reserveOf(token: Token): TokenAmount {
invariant(token.equals(this.token0) || token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(token), 'TOKEN')
return token.equals(this.token0) ? this.reserve0 : this.reserve1
}
getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(inputAmount.token.equals(this.token0) || inputAmount.token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(inputAmount.token), 'TOKEN')
if (JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve1.raw, ZERO)) {
throw new InsufficientReservesError()
}
......@@ -117,7 +130,7 @@ export class Pair {
}
getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(outputAmount.token.equals(this.token0) || outputAmount.token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(outputAmount.token), 'TOKEN')
if (
JSBI.equal(this.reserve0.raw, ZERO) ||
JSBI.equal(this.reserve1.raw, ZERO) ||
......@@ -165,7 +178,7 @@ export class Pair {
feeOn: boolean = false,
kLast?: BigintIsh
): TokenAmount {
invariant(token.equals(this.token0) || token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(token), 'TOKEN')
invariant(totalSupply.token.equals(this.liquidityToken), 'TOTAL_SUPPLY')
invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY')
invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY')
......@@ -175,7 +188,7 @@ export class Pair {
totalSupplyAdjusted = totalSupply
} else {
invariant(!!kLast, 'K_LAST')
const kLastParsed = parseBigintIsh(kLast as any)
const kLastParsed = parseBigintIsh(kLast)
if (!JSBI.equal(kLastParsed, ZERO)) {
const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw))
const rootKLast = sqrt(kLastParsed)
......
import { ChainId } from '../constants'
import invariant from 'tiny-invariant'
import { Token } from './token'
import { Currency, ETHER } from './currency'
import { Token, WETH } from './token'
import { Pair } from './pair'
import { Price } from './fractions/price'
export class Route {
public readonly pairs: Pair[]
public readonly path: Token[]
public readonly input: Currency
public readonly output: Currency
public readonly midPrice: Price
constructor(pairs: Pair[], input: Token) {
constructor(pairs: Pair[], input: Currency, output?: Currency) {
invariant(pairs.length > 0, 'PAIRS')
invariant(
pairs.map(pair => pair.token0.chainId === pairs[0].token0.chainId).every(x => x),
pairs.every(pair => pair.chainId === pairs[0].chainId),
'CHAIN_IDS'
)
const path = [input]
invariant(
(input instanceof Token && pairs[0].involvesToken(input)) ||
(input === ETHER && pairs[0].involvesToken(WETH[pairs[0].chainId])),
'INPUT'
)
invariant(
typeof output === 'undefined' ||
(output instanceof Token && pairs[pairs.length - 1].involvesToken(output)) ||
(output === ETHER && pairs[pairs.length - 1].involvesToken(WETH[pairs[0].chainId])),
'OUTPUT'
)
const path: Token[] = [input instanceof Token ? input : WETH[pairs[0].chainId]]
for (const [i, pair] of pairs.entries()) {
const currentInput = path[i]
invariant(currentInput.equals(pair.token0) || currentInput.equals(pair.token1), 'PATH')
const output = currentInput.equals(pair.token0) ? pair.token1 : pair.token0
path.push(output)
}
invariant(path.length === new Set(path).size, 'PATH')
this.pairs = pairs
this.path = path
this.midPrice = Price.fromRoute(this)
this.input = input
this.output = output ?? path[path.length - 1]
}
get input(): Token {
return this.path[0]
}
get output(): Token {
return this.path[this.path.length - 1]
get chainId(): ChainId {
return this.pairs[0].chainId
}
}
import { Currency } from './currency'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { ChainId, SolidityType } from '../constants'
import { ChainId } from '../constants'
import ERC20 from '../abis/ERC20.json'
import { validateAndParseAddress, validateSolidityTypeInstance } from '../utils'
import { validateAndParseAddress } from '../utils'
let CACHE: { [chainId: number]: { [address: string]: number } } = {
[ChainId.MAINNET]: {
......@@ -14,12 +14,12 @@ let CACHE: { [chainId: number]: { [address: string]: number } } = {
}
}
export class Token {
/**
* Represents an ERC20 token with a unique address and some metadata.
*/
export class Token extends Currency {
public readonly chainId: ChainId
public readonly address: string
public readonly decimals: number
public readonly symbol?: string
public readonly name?: string
static async fetchData(
chainId: ChainId,
......@@ -45,23 +45,24 @@ export class Token {
}
constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) {
validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8)
super(decimals, symbol, name)
this.chainId = chainId
this.address = validateAndParseAddress(address)
this.decimals = decimals
if (typeof symbol === 'string') this.symbol = symbol
if (typeof name === 'string') this.name = name
}
equals(other: Token): boolean {
const equal = this.chainId === other.chainId && this.address === other.address
if (equal) {
// short circuit on reference equality
if (this === other) {
return true
}
const equivalent = this.chainId === other.chainId && this.address === other.address
if (equivalent) {
// reference the same token, must have the same decimals/symbol/name
invariant(this.decimals === other.decimals, 'DECIMALS')
if (this.symbol && other.symbol) invariant(this.symbol === other.symbol, 'SYMBOL')
if (this.name && other.name) invariant(this.name === other.name, 'NAME')
}
return equal
return equivalent
}
sortsBefore(other: Token): boolean {
......@@ -71,6 +72,21 @@ export class Token {
}
}
/**
* Compares two currencies for equality
*/
export function currencyEquals(currencyA: Currency, currencyB: Currency): boolean {
if (currencyA instanceof Token && currencyB instanceof Token) {
return currencyA.equals(currencyB)
} else if (currencyA instanceof Token) {
return false
} else if (currencyB instanceof Token) {
return false
} else {
return currencyA === currencyB
}
}
export const WETH = {
[ChainId.MAINNET]: new Token(
ChainId.MAINNET,
......
import { Token } from 'entities/token'
import invariant from 'tiny-invariant'
import { ONE, TradeType, ZERO } from '../constants'
import { ChainId, ONE, TradeType, ZERO } from '../constants'
import { sortedInsert } from '../utils'
import { Fraction, TokenAmount } from './fractions'
import { Currency, ETHER } from './currency'
import { CurrencyAmount } from './fractions/currencyAmount'
import { Fraction } from './fractions/fraction'
import { Percent } from './fractions/percent'
import { Price } from './fractions/price'
import { TokenAmount } from './fractions/tokenAmount'
import { Pair } from './pair'
import { Route } from './route'
import { currencyEquals, Token, WETH } from './token'
// returns the percent difference between the mid price and the execution price
// we call this price impact in the UI
function computePriceImpact(midPrice: Price, inputAmount: TokenAmount, outputAmount: TokenAmount): Percent {
function computePriceImpact(midPrice: Price, inputAmount: CurrencyAmount, outputAmount: CurrencyAmount): Percent {
const exactQuote = midPrice.raw.multiply(inputAmount.raw)
// calculate slippage := (exactQuote - outputAmount) / exactQuote
const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote)
......@@ -20,16 +23,16 @@ function computePriceImpact(midPrice: Price, inputAmount: TokenAmount, outputAmo
// minimal interface so the input output comparator may be shared across types
interface InputOutput {
readonly inputAmount: TokenAmount
readonly outputAmount: TokenAmount
readonly inputAmount: CurrencyAmount
readonly outputAmount: CurrencyAmount
}
// comparator function that allows sorting trades by their output amounts, in decreasing order, and then input amounts
// in increasing order. i.e. the best trades have the most outputs for the least inputs and are sorted first
export function inputOutputComparator(a: InputOutput, b: InputOutput): number {
// must have same input and output token for comparison
invariant(a.inputAmount.token.equals(b.inputAmount.token), 'INPUT_TOKEN')
invariant(a.outputAmount.token.equals(b.outputAmount.token), 'OUTPUT_TOKEN')
invariant(currencyEquals(a.inputAmount.currency, b.inputAmount.currency), 'INPUT_CURRENCY')
invariant(currencyEquals(a.outputAmount.currency, b.outputAmount.currency), 'OUTPUT_CURRENCY')
if (a.outputAmount.equalTo(b.outputAmount)) {
if (a.inputAmount.equalTo(b.inputAmount)) {
return 0
......@@ -75,11 +78,28 @@ export interface BestTradeOptions {
maxHops?: number
}
/**
* Given a currency amount and a chain ID, returns the equivalent representation as the token amount.
* In other words, if the currency is ETHER, returns the WETH token amount for the given chain. Otherwise, returns
* the input currency amount.
*/
function wrappedAmount(currencyAmount: CurrencyAmount, chainId: ChainId): TokenAmount {
if (currencyAmount instanceof TokenAmount) return currencyAmount
if (currencyAmount.currency === ETHER) return new TokenAmount(WETH[chainId], currencyAmount.raw)
invariant(false, 'CURRENCY')
}
function wrappedCurrency(currency: Currency, chainId: ChainId): Token {
if (currency instanceof Token) return currency
if (currency === ETHER) return WETH[chainId]
invariant(false, 'CURRENCY')
}
export class Trade {
public readonly route: Route
public readonly tradeType: TradeType
public readonly inputAmount: TokenAmount
public readonly outputAmount: TokenAmount
public readonly inputAmount: CurrencyAmount
public readonly outputAmount: CurrencyAmount
// the price expressed in terms of output/input
public readonly executionPrice: Price
// the mid price after the trade executes assuming zero slippage
......@@ -92,12 +112,30 @@ export class Trade {
return this.priceImpact
}
public constructor(route: Route, amount: TokenAmount, tradeType: TradeType) {
invariant(amount.token.equals(tradeType === TradeType.EXACT_INPUT ? route.input : route.output), 'TOKEN')
/**
* Constructs an exact in trade with the given amount in and route
* @param route route of the exact in trade
* @param amountIn the amount being passed in
*/
public static exactIn(route: Route, amountIn: CurrencyAmount): Trade {
return new Trade(route, amountIn, TradeType.EXACT_INPUT)
}
/**
* Constructs an exact out trade with the given amount out and route
* @param route route of the exact out trade
* @param amountOut the amount returned by the trade
*/
public static exactOut(route: Route, amountOut: CurrencyAmount): Trade {
return new Trade(route, amountOut, TradeType.EXACT_OUTPUT)
}
public constructor(route: Route, amount: CurrencyAmount, tradeType: TradeType) {
const amounts: TokenAmount[] = new Array(route.path.length)
const nextPairs: Pair[] = new Array(route.pairs.length)
if (tradeType === TradeType.EXACT_INPUT) {
amounts[0] = amount
invariant(currencyEquals(amount.currency, route.input), 'INPUT')
amounts[0] = wrappedAmount(amount, route.chainId)
for (let i = 0; i < route.path.length - 1; i++) {
const pair = route.pairs[i]
const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i])
......@@ -105,7 +143,8 @@ export class Trade {
nextPairs[i] = nextPair
}
} else {
amounts[amounts.length - 1] = amount
invariant(currencyEquals(amount.currency, route.output), 'OUTPUT')
amounts[amounts.length - 1] = wrappedAmount(amount, route.chainId)
for (let i = route.path.length - 1; i > 0; i--) {
const pair = route.pairs[i - 1]
const [inputAmount, nextPair] = pair.getInputAmount(amounts[i])
......@@ -116,41 +155,54 @@ export class Trade {
this.route = route
this.tradeType = tradeType
const inputAmount = amounts[0]
const outputAmount = amounts[amounts.length - 1]
this.inputAmount = inputAmount
this.outputAmount = outputAmount
this.executionPrice = new Price(route.input, route.output, inputAmount.raw, outputAmount.raw)
this.inputAmount =
tradeType === TradeType.EXACT_INPUT
? amount
: route.input === ETHER
? CurrencyAmount.ether(amounts[0].raw)
: amounts[0]
this.outputAmount =
tradeType === TradeType.EXACT_OUTPUT
? amount
: route.output === ETHER
? CurrencyAmount.ether(amounts[amounts.length - 1].raw)
: amounts[amounts.length - 1]
this.executionPrice = new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.inputAmount.raw,
this.outputAmount.raw
)
this.nextMidPrice = Price.fromRoute(new Route(nextPairs, route.input))
this.priceImpact = computePriceImpact(route.midPrice, inputAmount, outputAmount)
this.priceImpact = computePriceImpact(route.midPrice, this.inputAmount, this.outputAmount)
}
// get the minimum amount that must be received from this trade for the given slippage tolerance
public minimumAmountOut(slippageTolerance: Percent): TokenAmount {
public minimumAmountOut(slippageTolerance: Percent): CurrencyAmount {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_OUTPUT) {
return this.outputAmount
} else {
return new TokenAmount(
this.outputAmount.token,
new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(this.outputAmount.raw).quotient
)
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(this.outputAmount.raw).quotient
return this.outputAmount instanceof TokenAmount
? new TokenAmount(this.outputAmount.token, slippageAdjustedAmountOut)
: CurrencyAmount.ether(slippageAdjustedAmountOut)
}
}
// get the maximum amount in that can be spent via this trade for the given slippage tolerance
public maximumAmountIn(slippageTolerance: Percent): TokenAmount {
public maximumAmountIn(slippageTolerance: Percent): CurrencyAmount {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_INPUT) {
return this.inputAmount
} else {
return new TokenAmount(
this.inputAmount.token,
new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient
)
const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient
return this.inputAmount instanceof TokenAmount
? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn)
: CurrencyAmount.ether(slippageAdjustedAmountIn)
}
}
......@@ -160,18 +212,27 @@ export class Trade {
// the amount in among multiple routes.
public static bestTradeExactIn(
pairs: Pair[],
amountIn: TokenAmount,
tokenOut: Token,
currencyAmountIn: CurrencyAmount,
currencyOut: Currency,
{ maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {},
// used in recursion.
currentPairs: Pair[] = [],
originalAmountIn: TokenAmount = amountIn,
originalAmountIn: CurrencyAmount = currencyAmountIn,
bestTrades: Trade[] = []
): Trade[] {
invariant(pairs.length > 0, 'PAIRS')
invariant(maxHops > 0, 'MAX_HOPS')
invariant(originalAmountIn === amountIn || currentPairs.length > 0, 'INVALID_RECURSION')
invariant(originalAmountIn === currencyAmountIn || currentPairs.length > 0, 'INVALID_RECURSION')
const chainId: ChainId | undefined =
currencyAmountIn instanceof TokenAmount
? currencyAmountIn.token.chainId
: currencyOut instanceof Token
? currencyOut.chainId
: undefined
invariant(chainId !== undefined, 'CHAIN_ID')
const amountIn = wrappedAmount(currencyAmountIn, chainId)
const tokenOut = wrappedCurrency(currencyOut, chainId)
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i]
// pair irrelevant
......@@ -189,11 +250,11 @@ export class Trade {
throw error
}
// we have arrived at the output token, so this is the final trade of one of the paths
if (amountOut!.token.equals(tokenOut)) {
if (amountOut.token.equals(tokenOut)) {
sortedInsert(
bestTrades,
new Trade(
new Route([...currentPairs, pair], originalAmountIn.token),
new Route([...currentPairs, pair], originalAmountIn.currency, currencyOut),
originalAmountIn,
TradeType.EXACT_INPUT
),
......@@ -206,8 +267,8 @@ export class Trade {
// otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops
Trade.bestTradeExactIn(
pairsExcludingThisPair,
amountOut!,
tokenOut,
amountOut,
currencyOut,
{
maxNumResults,
maxHops: maxHops - 1
......@@ -229,18 +290,27 @@ export class Trade {
// the amount in among multiple routes.
public static bestTradeExactOut(
pairs: Pair[],
tokenIn: Token,
amountOut: TokenAmount,
currencyIn: Currency,
currencyAmountOut: CurrencyAmount,
{ maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {},
// used in recursion.
currentPairs: Pair[] = [],
originalAmountOut: TokenAmount = amountOut,
originalAmountOut: CurrencyAmount = currencyAmountOut,
bestTrades: Trade[] = []
): Trade[] {
invariant(pairs.length > 0, 'PAIRS')
invariant(maxHops > 0, 'MAX_HOPS')
invariant(originalAmountOut === amountOut || currentPairs.length > 0, 'INVALID_RECURSION')
invariant(originalAmountOut === currencyAmountOut || currentPairs.length > 0, 'INVALID_RECURSION')
const chainId: ChainId | undefined =
currencyAmountOut instanceof TokenAmount
? currencyAmountOut.token.chainId
: currencyIn instanceof Token
? currencyIn.chainId
: undefined
invariant(chainId !== undefined, 'CHAIN_ID')
const amountOut = wrappedAmount(currencyAmountOut, chainId)
const tokenIn = wrappedCurrency(currencyIn, chainId)
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i]
// pair irrelevant
......@@ -258,10 +328,14 @@ export class Trade {
throw error
}
// we have arrived at the input token, so this is the first trade of one of the paths
if (amountIn!.token.equals(tokenIn)) {
if (amountIn.token.equals(tokenIn)) {
sortedInsert(
bestTrades,
new Trade(new Route([pair, ...currentPairs], tokenIn), originalAmountOut, TradeType.EXACT_OUTPUT),
new Trade(
new Route([pair, ...currentPairs], currencyIn, originalAmountOut.currency),
originalAmountOut,
TradeType.EXACT_OUTPUT
),
maxNumResults,
tradeComparator
)
......@@ -271,8 +345,8 @@ export class Trade {
// otherwise, consider all the other paths that arrive at this token as long as we have not exceeded maxHops
Trade.bestTradeExactOut(
pairsExcludingThisPair,
tokenIn,
amountIn!,
currencyIn,
amountIn,
{
maxNumResults,
maxHops: maxHops - 1
......
......@@ -13,3 +13,4 @@ export {
export * from './errors'
export * from './entities'
export * from './router'
import { TradeType } from './constants'
import invariant from 'tiny-invariant'
import { validateAndParseAddress } from './utils'
import { CurrencyAmount, ETHER, Percent, Trade } from './entities'
export interface TradeOptions {
// how much the execution price is allowed to move unfavorably from the trade execution price
allowedSlippage: Percent
// how long the swap is valid until it expires, in seconds
// this will be used to produce a `deadline` parameter which is computed from when the swap call parameters
// are generated.
ttl: number
// the account that should receive the output of the swap
recipient: string
// whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods
feeOnTransfer?: boolean
}
export interface SwapParameters {
// the method to call on the Uniswap V2 Router
methodName: string
// the arguments to pass to the method, all hex encoded
args: (string | string[])[]
// the amount of wei to send in hex
value: string
}
function toHex(currencyAmount: CurrencyAmount) {
return `0x${currencyAmount.raw.toString(16)}`
}
const ZERO_HEX = '0x0'
/**
* Represents the Uniswap V2 Router, and has static methods for helping execute trades.
*/
export abstract class Router {
/**
* Cannot be constructed.
*/
private constructor() {}
/**
* Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade.
* @param trade to produce call parameters for
* @param options options for the call parameters
*/
public static swapCallParameters(trade: Trade, options: TradeOptions): SwapParameters {
const etherIn = trade.inputAmount.currency === ETHER
const etherOut = trade.outputAmount.currency === ETHER
// the router does not support both ether in and out
invariant(!(etherIn && etherOut), 'ETHER_IN_OUT')
invariant(options.ttl > 0, 'TTL')
const to: string = validateAndParseAddress(options.recipient)
const amountIn: string = toHex(trade.maximumAmountIn(options.allowedSlippage))
const amountOut: string = toHex(trade.minimumAmountOut(options.allowedSlippage))
const path: string[] = trade.route.path.map(token => token.address)
const deadline = `0x${(Math.floor(new Date().getTime() / 1000) + options.ttl).toString(16)}`
const useFeeOnTransfer = Boolean(options.feeOnTransfer)
let methodName: string
let args: (string | string[])[]
let value: string
switch (trade.tradeType) {
case TradeType.EXACT_INPUT:
if (etherIn) {
methodName = useFeeOnTransfer ? 'swapExactETHForTokensSupportingFeeOnTransferTokens' : 'swapExactETHForTokens'
// (uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountOut, path, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = useFeeOnTransfer ? 'swapExactTokensForETHSupportingFeeOnTransferTokens' : 'swapExactTokensForETH'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, path, to, deadline]
value = ZERO_HEX
} else {
methodName = useFeeOnTransfer
? 'swapExactTokensForTokensSupportingFeeOnTransferTokens'
: 'swapExactTokensForTokens'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, path, to, deadline]
value = ZERO_HEX
}
break
case TradeType.EXACT_OUTPUT:
invariant(!useFeeOnTransfer, 'EXACT_OUT_FOT')
if (etherIn) {
methodName = 'swapETHForExactTokens'
// (uint amountOut, address[] calldata path, address to, uint deadline)
args = [amountOut, path, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = 'swapTokensForExactETH'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, path, to, deadline]
value = ZERO_HEX
} else {
methodName = 'swapTokensForExactTokens'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, path, to, deadline]
value = ZERO_HEX
}
break
}
return {
methodName,
args,
value
}
}
}
......@@ -9,7 +9,7 @@ const COMPUTED_INIT_CODE_HASH = keccak256(['bytes'], [`0x${bytecode}`])
describe('constants', () => {
describe('INIT_CODE_HASH', () => {
it.only('matches computed bytecode hash', () => {
it('matches computed bytecode hash', () => {
expect(COMPUTED_INIT_CODE_HASH).toEqual(INIT_CODE_HASH)
})
})
......
import invariant from 'tiny-invariant'
import { ChainId, WETH as _WETH, TradeType, Rounding, Token, TokenAmount, Pair, Route, Trade } from '../src'
const ADDRESSES = [
......@@ -58,6 +59,8 @@ describe('entities', () => {
})
it('Price:Route.midPrice', () => {
invariant(route.input instanceof Token)
invariant(route.output instanceof Token)
expect(route.midPrice.quote(new TokenAmount(route.input, decimalize(1, route.input.decimals)))).toEqual(
new TokenAmount(route.output, decimalize(1234, route.output.decimals))
)
......
import { Token, Pair } from '../src/entities'
import { Token, Pair, TokenAmount, WETH } from '../src/entities'
import { ChainId } from '../src/constants'
describe('Pair', () => {
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 18, 'USDC', 'USD Coin')
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'DAI Stablecoin')
describe('constructor', () => {
it('cannot be used for tokens on different chains', () => {
expect(() => new Pair(new TokenAmount(USDC, '100'), new TokenAmount(WETH[ChainId.RINKEBY], '100'))).toThrow(
'CHAIN_IDS'
)
})
})
describe('#getAddress', () => {
it('returns the correct address', () => {
const usdc = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 18, 'USDC', 'USD Coin')
const dai = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'DAI Stablecoin')
expect(Pair.getAddress(usdc, dai)).toEqual('0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5')
expect(Pair.getAddress(USDC, DAI)).toEqual('0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5')
})
})
describe('#token0', () => {
it('always is the token that sorts before', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).token0).toEqual(DAI)
expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).token0).toEqual(DAI)
})
})
describe('#token1', () => {
it('always is the token that sorts after', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).token1).toEqual(USDC)
expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).token1).toEqual(USDC)
})
})
describe('#reserve0', () => {
it('always comes from the token that sorts before', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserve0).toEqual(
new TokenAmount(DAI, '101')
)
expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserve0).toEqual(
new TokenAmount(DAI, '101')
)
})
})
describe('#reserve1', () => {
it('always comes from the token that sorts after', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserve1).toEqual(
new TokenAmount(USDC, '100')
)
expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserve1).toEqual(
new TokenAmount(USDC, '100')
)
})
})
describe('#reserveOf', () => {
it('returns reserves of the given token', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserveOf(USDC)).toEqual(
new TokenAmount(USDC, '100')
)
expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserveOf(USDC)).toEqual(
new TokenAmount(USDC, '100')
)
})
it('throws if not in the pair', () => {
expect(() =>
new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserveOf(WETH[ChainId.MAINNET])
).toThrow('TOKEN')
})
})
describe('#chainId', () => {
it('returns the token0 chainId', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).chainId).toEqual(ChainId.MAINNET)
expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).chainId).toEqual(ChainId.MAINNET)
})
})
describe('#involvesToken', () => {
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(USDC)).toEqual(true)
expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(DAI)).toEqual(true)
expect(
new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(WETH[ChainId.MAINNET])
).toEqual(false)
})
})
import { Token, WETH, ChainId, Pair, TokenAmount, Route, ETHER } from '../src'
describe('Route', () => {
const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0')
const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18, 't1')
const weth = WETH[ChainId.MAINNET]
const pair_0_1 = new Pair(new TokenAmount(token0, '100'), new TokenAmount(token1, '200'))
const pair_0_weth = new Pair(new TokenAmount(token0, '100'), new TokenAmount(weth, '100'))
const pair_1_weth = new Pair(new TokenAmount(token1, '175'), new TokenAmount(weth, '100'))
it('constructs a path from the tokens', () => {
const route = new Route([pair_0_1], token0)
expect(route.pairs).toEqual([pair_0_1])
expect(route.path).toEqual([token0, token1])
expect(route.input).toEqual(token0)
expect(route.output).toEqual(token1)
expect(route.chainId).toEqual(ChainId.MAINNET)
})
it('can have a token as both input and output', () => {
const route = new Route([pair_0_weth, pair_0_1, pair_1_weth], weth)
expect(route.pairs).toEqual([pair_0_weth, pair_0_1, pair_1_weth])
expect(route.input).toEqual(weth)
expect(route.output).toEqual(weth)
})
it('supports ether input', () => {
const route = new Route([pair_0_weth], ETHER)
expect(route.pairs).toEqual([pair_0_weth])
expect(route.input).toEqual(ETHER)
expect(route.output).toEqual(token0)
})
it('supports ether output', () => {
const route = new Route([pair_0_weth], token0, ETHER)
expect(route.pairs).toEqual([pair_0_weth])
expect(route.input).toEqual(token0)
expect(route.output).toEqual(ETHER)
})
})
import invariant from 'tiny-invariant'
import { ChainId, CurrencyAmount, ETHER, Pair, Percent, Route, Router, Token, TokenAmount, Trade, WETH } from '../src'
import JSBI from 'jsbi'
function checkDeadline(deadline: string[] | string): void {
expect(typeof deadline).toBe('string')
invariant(typeof deadline === 'string')
// less than 5 seconds on the deadline
expect(new Date().getTime() / 1000 - parseInt(deadline)).toBeLessThanOrEqual(5)
}
describe('Router', () => {
const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0')
const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18, 't1')
const pair_0_1 = new Pair(new TokenAmount(token0, JSBI.BigInt(1000)), new TokenAmount(token1, JSBI.BigInt(1000)))
const pair_weth_0 = new Pair(new TokenAmount(WETH[ChainId.MAINNET], '1000'), new TokenAmount(token0, '1000'))
describe('#swapCallParameters', () => {
describe('exact in', () => {
it('ether to token1', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_weth_0, pair_0_1], ETHER, token1), CurrencyAmount.ether(JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapExactETHForTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x51',
[WETH[ChainId.MAINNET].address, token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x64')
checkDeadline(result.args[result.args.length - 1])
})
it('token1 to ether', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_0_1, pair_weth_0], token1, ETHER), new TokenAmount(token1, JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapExactTokensForETH')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x51',
[token1.address, token0.address, WETH[ChainId.MAINNET].address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
it('token0 to token1', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_0_1], token0, token1), new TokenAmount(token0, JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapExactTokensForTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x59',
[token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
})
describe('exact out', () => {
it('ether to token1', () => {
const result = Router.swapCallParameters(
Trade.exactOut(new Route([pair_weth_0, pair_0_1], ETHER, token1), new TokenAmount(token1, JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapETHForExactTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
[WETH[ChainId.MAINNET].address, token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x80')
checkDeadline(result.args[result.args.length - 1])
})
it('token1 to ether', () => {
const result = Router.swapCallParameters(
Trade.exactOut(new Route([pair_0_1, pair_weth_0], token1, ETHER), CurrencyAmount.ether(JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapTokensForExactETH')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x80',
[token1.address, token0.address, WETH[ChainId.MAINNET].address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
it('token0 to token1', () => {
const result = Router.swapCallParameters(
Trade.exactOut(new Route([pair_0_1], token0, token1), new TokenAmount(token1, JSBI.BigInt(100))),
{ ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') }
)
expect(result.methodName).toEqual('swapTokensForExactTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x71',
[token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
})
describe('supporting fee on transfer', () => {
describe('exact in', () => {
it('ether to token1', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_weth_0, pair_0_1], ETHER, token1), CurrencyAmount.ether(JSBI.BigInt(100))),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
expect(result.methodName).toEqual('swapExactETHForTokensSupportingFeeOnTransferTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x51',
[WETH[ChainId.MAINNET].address, token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x64')
checkDeadline(result.args[result.args.length - 1])
})
it('token1 to ether', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_0_1, pair_weth_0], token1, ETHER), new TokenAmount(token1, JSBI.BigInt(100))),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
expect(result.methodName).toEqual('swapExactTokensForETHSupportingFeeOnTransferTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x51',
[token1.address, token0.address, WETH[ChainId.MAINNET].address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
it('token0 to token1', () => {
const result = Router.swapCallParameters(
Trade.exactIn(new Route([pair_0_1], token0, token1), new TokenAmount(token0, JSBI.BigInt(100))),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
expect(result.methodName).toEqual('swapExactTokensForTokensSupportingFeeOnTransferTokens')
expect(result.args.slice(0, -1)).toEqual([
'0x64',
'0x59',
[token0.address, token1.address],
'0x0000000000000000000000000000000000000004'
])
expect(result.value).toEqual('0x0')
checkDeadline(result.args[result.args.length - 1])
})
})
describe('exact out', () => {
it('ether to token1', () => {
expect(() =>
Router.swapCallParameters(
Trade.exactOut(
new Route([pair_weth_0, pair_0_1], ETHER, token1),
new TokenAmount(token1, JSBI.BigInt(100))
),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
).toThrow('EXACT_OUT_FOT')
})
it('token1 to ether', () => {
expect(() =>
Router.swapCallParameters(
Trade.exactOut(new Route([pair_0_1, pair_weth_0], token1, ETHER), CurrencyAmount.ether(JSBI.BigInt(100))),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
).toThrow('EXACT_OUT_FOT')
})
it('token0 to token1', () => {
expect(() =>
Router.swapCallParameters(
Trade.exactOut(new Route([pair_0_1], token0, token1), new TokenAmount(token1, JSBI.BigInt(100))),
{
ttl: 50,
recipient: '0x0000000000000000000000000000000000000004',
allowedSlippage: new Percent('1', '100'),
feeOnTransfer: true
}
)
).toThrow('EXACT_OUT_FOT')
})
})
})
})
})
import JSBI from 'jsbi'
import { ChainId, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType } from '../src'
import {
ChainId,
ETHER,
CurrencyAmount,
Pair,
Percent,
Route,
Token,
TokenAmount,
Trade,
TradeType,
WETH
} from '../src'
describe('Trade', () => {
const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0')
......@@ -13,8 +25,51 @@ describe('Trade', () => {
const pair_1_2 = new Pair(new TokenAmount(token1, JSBI.BigInt(1200)), new TokenAmount(token2, JSBI.BigInt(1000)))
const pair_1_3 = new Pair(new TokenAmount(token1, JSBI.BigInt(1200)), new TokenAmount(token3, JSBI.BigInt(1300)))
const pair_weth_0 = new Pair(
new TokenAmount(WETH[ChainId.MAINNET], JSBI.BigInt(1000)),
new TokenAmount(token0, JSBI.BigInt(1000))
)
const empty_pair_0_1 = new Pair(new TokenAmount(token0, JSBI.BigInt(0)), new TokenAmount(token1, JSBI.BigInt(0)))
it('can be constructed with ETHER as input', () => {
const trade = new Trade(
new Route([pair_weth_0], ETHER),
CurrencyAmount.ether(JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(ETHER)
expect(trade.outputAmount.currency).toEqual(token0)
})
it('can be constructed with ETHER as input for exact output', () => {
const trade = new Trade(
new Route([pair_weth_0], ETHER, token0),
new TokenAmount(token0, JSBI.BigInt(100)),
TradeType.EXACT_OUTPUT
)
expect(trade.inputAmount.currency).toEqual(ETHER)
expect(trade.outputAmount.currency).toEqual(token0)
})
it('can be constructed with ETHER as output', () => {
const trade = new Trade(
new Route([pair_weth_0], token0, ETHER),
CurrencyAmount.ether(JSBI.BigInt(100)),
TradeType.EXACT_OUTPUT
)
expect(trade.inputAmount.currency).toEqual(token0)
expect(trade.outputAmount.currency).toEqual(ETHER)
})
it('can be constructed with ETHER as output for exact input', () => {
const trade = new Trade(
new Route([pair_weth_0], token0, ETHER),
new TokenAmount(token0, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token0)
expect(trade.outputAmount.currency).toEqual(ETHER)
})
describe('#bestTradeExactIn', () => {
it('throws with empty pairs', () => {
expect(() => Trade.bestTradeExactIn([], new TokenAmount(token0, JSBI.BigInt(100)), token2)).toThrow('PAIRS')
......@@ -91,6 +146,35 @@ describe('Trade', () => {
)
expect(result).toHaveLength(0)
})
it('works for ETHER currency input', () => {
const result = Trade.bestTradeExactIn(
[pair_weth_0, pair_0_1, pair_0_3, pair_1_3],
CurrencyAmount.ether(JSBI.BigInt(100)),
token3
)
expect(result).toHaveLength(2)
expect(result[0].inputAmount.currency).toEqual(ETHER)
expect(result[0].route.path).toEqual([WETH[ChainId.MAINNET], token0, token1, token3])
expect(result[0].outputAmount.currency).toEqual(token3)
expect(result[1].inputAmount.currency).toEqual(ETHER)
expect(result[1].route.path).toEqual([WETH[ChainId.MAINNET], token0, token3])
expect(result[1].outputAmount.currency).toEqual(token3)
})
it('works for ETHER currency output', () => {
const result = Trade.bestTradeExactIn(
[pair_weth_0, pair_0_1, pair_0_3, pair_1_3],
new TokenAmount(token3, JSBI.BigInt(100)),
ETHER
)
expect(result).toHaveLength(2)
expect(result[0].inputAmount.currency).toEqual(token3)
expect(result[0].route.path).toEqual([token3, token0, WETH[ChainId.MAINNET]])
expect(result[0].outputAmount.currency).toEqual(ETHER)
expect(result[1].inputAmount.currency).toEqual(token3)
expect(result[1].route.path).toEqual([token3, token1, token0, WETH[ChainId.MAINNET]])
expect(result[1].outputAmount.currency).toEqual(ETHER)
})
})
describe('#maximumAmountIn', () => {
......@@ -287,5 +371,34 @@ describe('Trade', () => {
)
expect(result).toHaveLength(0)
})
it('works for ETHER currency input', () => {
const result = Trade.bestTradeExactOut(
[pair_weth_0, pair_0_1, pair_0_3, pair_1_3],
ETHER,
new TokenAmount(token3, JSBI.BigInt(100))
)
expect(result).toHaveLength(2)
expect(result[0].inputAmount.currency).toEqual(ETHER)
expect(result[0].route.path).toEqual([WETH[ChainId.MAINNET], token0, token1, token3])
expect(result[0].outputAmount.currency).toEqual(token3)
expect(result[1].inputAmount.currency).toEqual(ETHER)
expect(result[1].route.path).toEqual([WETH[ChainId.MAINNET], token0, token3])
expect(result[1].outputAmount.currency).toEqual(token3)
})
it('works for ETHER currency output', () => {
const result = Trade.bestTradeExactOut(
[pair_weth_0, pair_0_1, pair_0_3, pair_1_3],
token3,
CurrencyAmount.ether(JSBI.BigInt(100))
)
expect(result).toHaveLength(2)
expect(result[0].inputAmount.currency).toEqual(token3)
expect(result[0].route.path).toEqual([token3, token0, WETH[ChainId.MAINNET]])
expect(result[0].outputAmount.currency).toEqual(ETHER)
expect(result[1].inputAmount.currency).toEqual(token3)
expect(result[1].route.path).toEqual([token3, token1, token0, WETH[ChainId.MAINNET]])
expect(result[1].outputAmount.currency).toEqual(ETHER)
})
})
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment