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 './fraction'
export * from './percent' export * from './percent'
export * from './tokenAmount' export * from './tokenAmount'
export * from './currencyAmount'
export * from './price' export * from './price'
import { Token } from '../token'
import { TokenAmount } from './tokenAmount'
import { currencyEquals } from '../token'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { BigintIsh, Rounding, TEN } from '../../constants' import { BigintIsh, Rounding, TEN } from '../../constants'
import { Token } from '../token' import { Currency } from '../currency'
import { Route } from '../route' import { Route } from '../route'
import { Fraction } from './fraction' import { Fraction } from './fraction'
import { TokenAmount } from './tokenAmount' import { CurrencyAmount } from './currencyAmount'
export class Price extends Fraction { export class Price extends Fraction {
public readonly baseToken: Token // input i.e. denominator public readonly baseCurrency: Currency // input i.e. denominator
public readonly quoteToken: Token // output i.e. numerator 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 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 { static fromRoute(route: Route): Price {
...@@ -17,22 +20,22 @@ export class Price extends Fraction { ...@@ -17,22 +20,22 @@ export class Price extends Fraction {
for (const [i, pair] of route.pairs.entries()) { for (const [i, pair] of route.pairs.entries()) {
prices.push( prices.push(
route.path[i].equals(pair.token0) route.path[i].equals(pair.token0)
? new Price(pair.reserve0.token, pair.reserve1.token, pair.reserve0.raw, pair.reserve1.raw) ? new Price(pair.reserve0.currency, pair.reserve1.currency, pair.reserve0.raw, pair.reserve1.raw)
: new Price(pair.reserve1.token, pair.reserve0.token, pair.reserve1.raw, pair.reserve0.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]) return prices.slice(1).reduce((accumulator, currentValue) => accumulator.multiply(currentValue), prices[0])
} }
// denominator and numerator _must_ be raw, i.e. in the native representation // 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) super(numerator, denominator)
this.baseToken = baseToken this.baseCurrency = baseCurrency
this.quoteToken = quoteToken this.quoteCurrency = quoteCurrency
this.scalar = new Fraction( this.scalar = new Fraction(
JSBI.exponentiate(TEN, JSBI.BigInt(baseToken.decimals)), JSBI.exponentiate(TEN, JSBI.BigInt(baseCurrency.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteToken.decimals)) JSBI.exponentiate(TEN, JSBI.BigInt(quoteCurrency.decimals))
) )
} }
...@@ -45,19 +48,22 @@ export class Price extends Fraction { ...@@ -45,19 +48,22 @@ export class Price extends Fraction {
} }
invert(): Price { 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 { multiply(other: Price): Price {
invariant(this.quoteToken.equals(other.baseToken), 'BASE') invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN')
const fraction = super.multiply(other) 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 // performs floor division on overflow
quote(tokenAmount: TokenAmount): TokenAmount { quote(currencyAmount: CurrencyAmount): CurrencyAmount {
invariant(tokenAmount.token.equals(this.baseToken), 'TOKEN') invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN')
return new TokenAmount(this.quoteToken, super.multiply(tokenAmount.raw).quotient) 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 { toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string {
......
import { CurrencyAmount } from './currencyAmount'
import { Token } from '../token'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' 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 public readonly token: Token
// amount _must_ be raw, i.e. in the native representation // amount _must_ be raw, i.e. in the native representation
constructor(token: Token, amount: BigintIsh) { constructor(token: Token, amount: BigintIsh) {
const parsedAmount = parseBigintIsh(amount) super(token, amount)
validateSolidityTypeInstance(parsedAmount, SolidityType.uint256)
super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(token.decimals)))
this.token = token this.token = token
} }
get raw(): JSBI {
return this.numerator
}
add(other: TokenAmount): TokenAmount { add(other: TokenAmount): TokenAmount {
invariant(this.token.equals(other.token), 'TOKEN') invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.add(this.raw, other.raw)) return new TokenAmount(this.token, JSBI.add(this.raw, other.raw))
...@@ -35,22 +23,4 @@ export class TokenAmount extends Fraction { ...@@ -35,22 +23,4 @@ export class TokenAmount extends Fraction {
invariant(this.token.equals(other.token), 'TOKEN') invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.subtract(this.raw, other.raw)) 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' ...@@ -2,5 +2,6 @@ export * from './token'
export * from './pair' export * from './pair'
export * from './route' export * from './route'
export * from './trade' export * from './trade'
export * from './currency'
export * from './fractions' export * from './fractions'
import { TokenAmount } from './fractions/tokenAmount'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks' import { getNetwork } from '@ethersproject/networks'
...@@ -15,13 +16,13 @@ import { ...@@ -15,13 +16,13 @@ import {
ONE, ONE,
FIVE, FIVE,
_997, _997,
_1000 _1000,
ChainId
} from '../constants' } from '../constants'
import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json' import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { sqrt, parseBigintIsh } from '../utils' import { sqrt, parseBigintIsh } from '../utils'
import { InsufficientReservesError, InsufficientInputAmountError } from '../errors' import { InsufficientReservesError, InsufficientInputAmountError } from '../errors'
import { Token } from './token' import { Token } from './token'
import { TokenAmount } from './fractions/tokenAmount'
let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {} let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {}
...@@ -75,6 +76,18 @@ export class Pair { ...@@ -75,6 +76,18 @@ export class Pair {
this.tokenAmounts = tokenAmounts as [TokenAmount, TokenAmount] 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 { get token0(): Token {
return this.tokenAmounts[0].token return this.tokenAmounts[0].token
} }
...@@ -92,12 +105,12 @@ export class Pair { ...@@ -92,12 +105,12 @@ export class Pair {
} }
reserveOf(token: Token): TokenAmount { 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 return token.equals(this.token0) ? this.reserve0 : this.reserve1
} }
getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] { 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)) { if (JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve1.raw, ZERO)) {
throw new InsufficientReservesError() throw new InsufficientReservesError()
} }
...@@ -117,7 +130,7 @@ export class Pair { ...@@ -117,7 +130,7 @@ export class Pair {
} }
getInputAmount(outputAmount: TokenAmount): [TokenAmount, 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 ( if (
JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve0.raw, ZERO) ||
JSBI.equal(this.reserve1.raw, ZERO) || JSBI.equal(this.reserve1.raw, ZERO) ||
...@@ -165,7 +178,7 @@ export class Pair { ...@@ -165,7 +178,7 @@ export class Pair {
feeOn: boolean = false, feeOn: boolean = false,
kLast?: BigintIsh kLast?: BigintIsh
): TokenAmount { ): 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(totalSupply.token.equals(this.liquidityToken), 'TOTAL_SUPPLY')
invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY') invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY')
invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY') invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY')
...@@ -175,7 +188,7 @@ export class Pair { ...@@ -175,7 +188,7 @@ export class Pair {
totalSupplyAdjusted = totalSupply totalSupplyAdjusted = totalSupply
} else { } else {
invariant(!!kLast, 'K_LAST') invariant(!!kLast, 'K_LAST')
const kLastParsed = parseBigintIsh(kLast as any) const kLastParsed = parseBigintIsh(kLast)
if (!JSBI.equal(kLastParsed, ZERO)) { if (!JSBI.equal(kLastParsed, ZERO)) {
const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw)) const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw))
const rootKLast = sqrt(kLastParsed) const rootKLast = sqrt(kLastParsed)
......
import { ChainId } from '../constants'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import { Token } from './token' import { Currency, ETHER } from './currency'
import { Token, WETH } from './token'
import { Pair } from './pair' import { Pair } from './pair'
import { Price } from './fractions/price' import { Price } from './fractions/price'
export class Route { export class Route {
public readonly pairs: Pair[] public readonly pairs: Pair[]
public readonly path: Token[] public readonly path: Token[]
public readonly input: Currency
public readonly output: Currency
public readonly midPrice: Price public readonly midPrice: Price
constructor(pairs: Pair[], input: Token) { constructor(pairs: Pair[], input: Currency, output?: Currency) {
invariant(pairs.length > 0, 'PAIRS') invariant(pairs.length > 0, 'PAIRS')
invariant( invariant(
pairs.map(pair => pair.token0.chainId === pairs[0].token0.chainId).every(x => x), pairs.every(pair => pair.chainId === pairs[0].chainId),
'CHAIN_IDS' '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()) { for (const [i, pair] of pairs.entries()) {
const currentInput = path[i] const currentInput = path[i]
invariant(currentInput.equals(pair.token0) || currentInput.equals(pair.token1), 'PATH') invariant(currentInput.equals(pair.token0) || currentInput.equals(pair.token1), 'PATH')
const output = currentInput.equals(pair.token0) ? pair.token1 : pair.token0 const output = currentInput.equals(pair.token0) ? pair.token1 : pair.token0
path.push(output) path.push(output)
} }
invariant(path.length === new Set(path).size, 'PATH')
this.pairs = pairs this.pairs = pairs
this.path = path this.path = path
this.midPrice = Price.fromRoute(this) this.midPrice = Price.fromRoute(this)
this.input = input
this.output = output ?? path[path.length - 1]
} }
get input(): Token { get chainId(): ChainId {
return this.path[0] return this.pairs[0].chainId
}
get output(): Token {
return this.path[this.path.length - 1]
} }
} }
import { Currency } from './currency'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks' import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers' import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { ChainId, SolidityType } from '../constants' import { ChainId } from '../constants'
import ERC20 from '../abis/ERC20.json' import ERC20 from '../abis/ERC20.json'
import { validateAndParseAddress, validateSolidityTypeInstance } from '../utils' import { validateAndParseAddress } from '../utils'
let CACHE: { [chainId: number]: { [address: string]: number } } = { let CACHE: { [chainId: number]: { [address: string]: number } } = {
[ChainId.MAINNET]: { [ChainId.MAINNET]: {
...@@ -14,12 +14,12 @@ let CACHE: { [chainId: number]: { [address: string]: number } } = { ...@@ -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 chainId: ChainId
public readonly address: string public readonly address: string
public readonly decimals: number
public readonly symbol?: string
public readonly name?: string
static async fetchData( static async fetchData(
chainId: ChainId, chainId: ChainId,
...@@ -45,23 +45,24 @@ export class Token { ...@@ -45,23 +45,24 @@ export class Token {
} }
constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) { 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.chainId = chainId
this.address = validateAndParseAddress(address) 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 { equals(other: Token): boolean {
const equal = this.chainId === other.chainId && this.address === other.address // short circuit on reference equality
if (equal) { 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') invariant(this.decimals === other.decimals, 'DECIMALS')
if (this.symbol && other.symbol) invariant(this.symbol === other.symbol, 'SYMBOL') if (this.symbol && other.symbol) invariant(this.symbol === other.symbol, 'SYMBOL')
if (this.name && other.name) invariant(this.name === other.name, 'NAME') if (this.name && other.name) invariant(this.name === other.name, 'NAME')
} }
return equal return equivalent
} }
sortsBefore(other: Token): boolean { sortsBefore(other: Token): boolean {
...@@ -71,6 +72,21 @@ export class Token { ...@@ -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 = { export const WETH = {
[ChainId.MAINNET]: new Token( [ChainId.MAINNET]: new Token(
ChainId.MAINNET, ChainId.MAINNET,
......
import { Token } from 'entities/token'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import { ONE, TradeType, ZERO } from '../constants' import { ChainId, ONE, TradeType, ZERO } from '../constants'
import { sortedInsert } from '../utils' 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 { Percent } from './fractions/percent'
import { Price } from './fractions/price' import { Price } from './fractions/price'
import { TokenAmount } from './fractions/tokenAmount'
import { Pair } from './pair' import { Pair } from './pair'
import { Route } from './route' import { Route } from './route'
import { currencyEquals, Token, WETH } from './token'
// returns the percent difference between the mid price and the execution price // returns the percent difference between the mid price and the execution price
// we call this price impact in the UI // 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) const exactQuote = midPrice.raw.multiply(inputAmount.raw)
// calculate slippage := (exactQuote - outputAmount) / exactQuote // calculate slippage := (exactQuote - outputAmount) / exactQuote
const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote) const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote)
...@@ -20,16 +23,16 @@ function computePriceImpact(midPrice: Price, inputAmount: TokenAmount, outputAmo ...@@ -20,16 +23,16 @@ function computePriceImpact(midPrice: Price, inputAmount: TokenAmount, outputAmo
// minimal interface so the input output comparator may be shared across types // minimal interface so the input output comparator may be shared across types
interface InputOutput { interface InputOutput {
readonly inputAmount: TokenAmount readonly inputAmount: CurrencyAmount
readonly outputAmount: TokenAmount readonly outputAmount: CurrencyAmount
} }
// comparator function that allows sorting trades by their output amounts, in decreasing order, and then input amounts // 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 // 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 { export function inputOutputComparator(a: InputOutput, b: InputOutput): number {
// must have same input and output token for comparison // must have same input and output token for comparison
invariant(a.inputAmount.token.equals(b.inputAmount.token), 'INPUT_TOKEN') invariant(currencyEquals(a.inputAmount.currency, b.inputAmount.currency), 'INPUT_CURRENCY')
invariant(a.outputAmount.token.equals(b.outputAmount.token), 'OUTPUT_TOKEN') invariant(currencyEquals(a.outputAmount.currency, b.outputAmount.currency), 'OUTPUT_CURRENCY')
if (a.outputAmount.equalTo(b.outputAmount)) { if (a.outputAmount.equalTo(b.outputAmount)) {
if (a.inputAmount.equalTo(b.inputAmount)) { if (a.inputAmount.equalTo(b.inputAmount)) {
return 0 return 0
...@@ -75,11 +78,28 @@ export interface BestTradeOptions { ...@@ -75,11 +78,28 @@ export interface BestTradeOptions {
maxHops?: number 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 { export class Trade {
public readonly route: Route public readonly route: Route
public readonly tradeType: TradeType public readonly tradeType: TradeType
public readonly inputAmount: TokenAmount public readonly inputAmount: CurrencyAmount
public readonly outputAmount: TokenAmount public readonly outputAmount: CurrencyAmount
// the price expressed in terms of output/input // the price expressed in terms of output/input
public readonly executionPrice: Price public readonly executionPrice: Price
// the mid price after the trade executes assuming zero slippage // the mid price after the trade executes assuming zero slippage
...@@ -92,12 +112,30 @@ export class Trade { ...@@ -92,12 +112,30 @@ export class Trade {
return this.priceImpact 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 amounts: TokenAmount[] = new Array(route.path.length)
const nextPairs: Pair[] = new Array(route.pairs.length) const nextPairs: Pair[] = new Array(route.pairs.length)
if (tradeType === TradeType.EXACT_INPUT) { 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++) { for (let i = 0; i < route.path.length - 1; i++) {
const pair = route.pairs[i] const pair = route.pairs[i]
const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i]) const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i])
...@@ -105,7 +143,8 @@ export class Trade { ...@@ -105,7 +143,8 @@ export class Trade {
nextPairs[i] = nextPair nextPairs[i] = nextPair
} }
} else { } 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--) { for (let i = route.path.length - 1; i > 0; i--) {
const pair = route.pairs[i - 1] const pair = route.pairs[i - 1]
const [inputAmount, nextPair] = pair.getInputAmount(amounts[i]) const [inputAmount, nextPair] = pair.getInputAmount(amounts[i])
...@@ -116,41 +155,54 @@ export class Trade { ...@@ -116,41 +155,54 @@ export class Trade {
this.route = route this.route = route
this.tradeType = tradeType this.tradeType = tradeType
const inputAmount = amounts[0] this.inputAmount =
const outputAmount = amounts[amounts.length - 1] tradeType === TradeType.EXACT_INPUT
this.inputAmount = inputAmount ? amount
this.outputAmount = outputAmount : route.input === ETHER
this.executionPrice = new Price(route.input, route.output, inputAmount.raw, outputAmount.raw) ? 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.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 // 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') invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_OUTPUT) { if (this.tradeType === TradeType.EXACT_OUTPUT) {
return this.outputAmount return this.outputAmount
} else { } else {
return new TokenAmount( const slippageAdjustedAmountOut = new Fraction(ONE)
this.outputAmount.token,
new Fraction(ONE)
.add(slippageTolerance) .add(slippageTolerance)
.invert() .invert()
.multiply(this.outputAmount.raw).quotient .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 // 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') invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_INPUT) { if (this.tradeType === TradeType.EXACT_INPUT) {
return this.inputAmount return this.inputAmount
} else { } else {
return new TokenAmount( const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient
this.inputAmount.token, return this.inputAmount instanceof TokenAmount
new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient ? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn)
) : CurrencyAmount.ether(slippageAdjustedAmountIn)
} }
} }
...@@ -160,18 +212,27 @@ export class Trade { ...@@ -160,18 +212,27 @@ export class Trade {
// the amount in among multiple routes. // the amount in among multiple routes.
public static bestTradeExactIn( public static bestTradeExactIn(
pairs: Pair[], pairs: Pair[],
amountIn: TokenAmount, currencyAmountIn: CurrencyAmount,
tokenOut: Token, currencyOut: Currency,
{ maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {},
// used in recursion. // used in recursion.
currentPairs: Pair[] = [], currentPairs: Pair[] = [],
originalAmountIn: TokenAmount = amountIn, originalAmountIn: CurrencyAmount = currencyAmountIn,
bestTrades: Trade[] = [] bestTrades: Trade[] = []
): Trade[] { ): Trade[] {
invariant(pairs.length > 0, 'PAIRS') invariant(pairs.length > 0, 'PAIRS')
invariant(maxHops > 0, 'MAX_HOPS') 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++) { for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i] const pair = pairs[i]
// pair irrelevant // pair irrelevant
...@@ -189,11 +250,11 @@ export class Trade { ...@@ -189,11 +250,11 @@ export class Trade {
throw error throw error
} }
// we have arrived at the output token, so this is the final trade of one of the paths // 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( sortedInsert(
bestTrades, bestTrades,
new Trade( new Trade(
new Route([...currentPairs, pair], originalAmountIn.token), new Route([...currentPairs, pair], originalAmountIn.currency, currencyOut),
originalAmountIn, originalAmountIn,
TradeType.EXACT_INPUT TradeType.EXACT_INPUT
), ),
...@@ -206,8 +267,8 @@ export class Trade { ...@@ -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 // otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops
Trade.bestTradeExactIn( Trade.bestTradeExactIn(
pairsExcludingThisPair, pairsExcludingThisPair,
amountOut!, amountOut,
tokenOut, currencyOut,
{ {
maxNumResults, maxNumResults,
maxHops: maxHops - 1 maxHops: maxHops - 1
...@@ -229,18 +290,27 @@ export class Trade { ...@@ -229,18 +290,27 @@ export class Trade {
// the amount in among multiple routes. // the amount in among multiple routes.
public static bestTradeExactOut( public static bestTradeExactOut(
pairs: Pair[], pairs: Pair[],
tokenIn: Token, currencyIn: Currency,
amountOut: TokenAmount, currencyAmountOut: CurrencyAmount,
{ maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {},
// used in recursion. // used in recursion.
currentPairs: Pair[] = [], currentPairs: Pair[] = [],
originalAmountOut: TokenAmount = amountOut, originalAmountOut: CurrencyAmount = currencyAmountOut,
bestTrades: Trade[] = [] bestTrades: Trade[] = []
): Trade[] { ): Trade[] {
invariant(pairs.length > 0, 'PAIRS') invariant(pairs.length > 0, 'PAIRS')
invariant(maxHops > 0, 'MAX_HOPS') 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++) { for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i] const pair = pairs[i]
// pair irrelevant // pair irrelevant
...@@ -258,10 +328,14 @@ export class Trade { ...@@ -258,10 +328,14 @@ export class Trade {
throw error throw error
} }
// we have arrived at the input token, so this is the first trade of one of the paths // 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( sortedInsert(
bestTrades, 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, maxNumResults,
tradeComparator tradeComparator
) )
...@@ -271,8 +345,8 @@ export class Trade { ...@@ -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 // otherwise, consider all the other paths that arrive at this token as long as we have not exceeded maxHops
Trade.bestTradeExactOut( Trade.bestTradeExactOut(
pairsExcludingThisPair, pairsExcludingThisPair,
tokenIn, currencyIn,
amountIn!, amountIn,
{ {
maxNumResults, maxNumResults,
maxHops: maxHops - 1 maxHops: maxHops - 1
......
...@@ -13,3 +13,4 @@ export { ...@@ -13,3 +13,4 @@ export {
export * from './errors' export * from './errors'
export * from './entities' 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}`]) ...@@ -9,7 +9,7 @@ const COMPUTED_INIT_CODE_HASH = keccak256(['bytes'], [`0x${bytecode}`])
describe('constants', () => { describe('constants', () => {
describe('INIT_CODE_HASH', () => { describe('INIT_CODE_HASH', () => {
it.only('matches computed bytecode hash', () => { it('matches computed bytecode hash', () => {
expect(COMPUTED_INIT_CODE_HASH).toEqual(INIT_CODE_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' import { ChainId, WETH as _WETH, TradeType, Rounding, Token, TokenAmount, Pair, Route, Trade } from '../src'
const ADDRESSES = [ const ADDRESSES = [
...@@ -58,6 +59,8 @@ describe('entities', () => { ...@@ -58,6 +59,8 @@ describe('entities', () => {
}) })
it('Price:Route.midPrice', () => { 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( expect(route.midPrice.quote(new TokenAmount(route.input, decimalize(1, route.input.decimals)))).toEqual(
new TokenAmount(route.output, decimalize(1234, route.output.decimals)) 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' import { ChainId } from '../src/constants'
describe('Pair', () => { 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', () => { describe('#getAddress', () => {
it('returns the correct address', () => { it('returns the correct address', () => {
const usdc = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 18, 'USDC', 'USD Coin') expect(Pair.getAddress(USDC, DAI)).toEqual('0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5')
const dai = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'DAI Stablecoin') })
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 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', () => { describe('Trade', () => {
const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0') const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0')
...@@ -13,8 +25,51 @@ describe('Trade', () => { ...@@ -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_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_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))) 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', () => { describe('#bestTradeExactIn', () => {
it('throws with empty pairs', () => { it('throws with empty pairs', () => {
expect(() => Trade.bestTradeExactIn([], new TokenAmount(token0, JSBI.BigInt(100)), token2)).toThrow('PAIRS') expect(() => Trade.bestTradeExactIn([], new TokenAmount(token0, JSBI.BigInt(100)), token2)).toThrow('PAIRS')
...@@ -91,6 +146,35 @@ describe('Trade', () => { ...@@ -91,6 +146,35 @@ describe('Trade', () => {
) )
expect(result).toHaveLength(0) 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', () => { describe('#maximumAmountIn', () => {
...@@ -287,5 +371,34 @@ describe('Trade', () => { ...@@ -287,5 +371,34 @@ describe('Trade', () => {
) )
expect(result).toHaveLength(0) 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