Commit 0e801850 authored by Noah Zinsmeister's avatar Noah Zinsmeister

big refactor, more abstraction

add data fetching
parent 0badfed0
[
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
[
{
"constant": true,
"inputs": [
{
"name": "tokenA",
"type": "address"
},
{
"name": "tokenB",
"type": "address"
}
],
"name": "getExchange",
"outputs": [
{
"name": "exchange",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
...@@ -2,15 +2,19 @@ import JSBI from 'jsbi' ...@@ -2,15 +2,19 @@ import JSBI from 'jsbi'
// exports for external consumption // exports for external consumption
export enum ChainId { export enum ChainId {
RINKEBY = 4 MAINNET = 1,
ROPSTEN = 3,
RINKEBY = 4,
GÖRLI = 5,
KOVAN = 42
} }
export const WETH = { export const FACTORY_ADDRESS = {
[ChainId.RINKEBY]: { [ChainId.MAINNET]: '',
chainId: ChainId.RINKEBY, [ChainId.ROPSTEN]: '',
address: '0xc778417E063141139Fce010982780140Aa0cD5Ab', [ChainId.RINKEBY]: '0xbe52bB8cCa36fcD6061C307f14bAB48F09A760f9',
decimals: 18 [ChainId.GÖRLI]: '',
} [ChainId.KOVAN]: ''
} }
export enum TradeType { export enum TradeType {
...@@ -27,6 +31,6 @@ export const _997 = JSBI.BigInt(997) ...@@ -27,6 +31,6 @@ export const _997 = JSBI.BigInt(997)
export const _1000 = JSBI.BigInt(1000) export const _1000 = JSBI.BigInt(1000)
export enum SolidityType { export enum SolidityType {
uint8, uint8 = 'uint8',
uint256 uint256 = 'uint256'
} }
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { SolidityType } from '../constants' import { FACTORY_ADDRESS, ZERO, ONE, _997, _1000 } from '../constants'
import { BigintIsh } from '../types' import UniswapV2Factory from '../abis/UniswapV2Factory.json'
import { parseBigintIsh } from '../utils/parseInputs' import ERC20 from '../abis/ERC20.json'
import { validateSolidityTypeInstance } from '../utils/validateInputs' import { validateAndParseAddress } from '../utils'
import { Token } from './token' import { Token } from './token'
import { TokenAmount } from './fractions/tokenAmount'
export class Exchange { export class Exchange {
public readonly pair: [Token, Token] private readonly tokenAmounts: [TokenAmount, TokenAmount]
public readonly balances: [JSBI, JSBI] public readonly address?: string
static validate(pair: [Token, Token], balances: [JSBI, JSBI]) { static async fetchData(
// validate components of an Exchange tokenA: Token,
balances.forEach(balance => validateSolidityTypeInstance(balance, SolidityType.uint256)) tokenB: Token,
provider = getDefaultProvider(getNetwork(tokenA.chainId)),
address?: string
): Promise<Exchange> {
const parsedAddress =
typeof address === 'string'
? address
: await new Contract(FACTORY_ADDRESS[tokenA.chainId], UniswapV2Factory, provider).getExchange(
tokenA.address,
tokenB.address
)
const balances = await Promise.all([
new Contract(tokenA.address, ERC20, provider).balanceOf(parsedAddress),
new Contract(tokenB.address, ERC20, provider).balanceOf(parsedAddress)
])
return new Exchange(new TokenAmount(tokenA, balances[0]), new TokenAmount(tokenB, balances[1]), parsedAddress)
}
constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount, address?: string) {
invariant(tokenAmountA.token.chainId === tokenAmountB.token.chainId, 'CHAIN_IDS')
const tokenAmounts: [TokenAmount, TokenAmount] =
tokenAmountA.token.address < tokenAmountB.token.address
? [tokenAmountA, tokenAmountB]
: [tokenAmountB, tokenAmountA]
invariant(tokenAmounts[0].token.address < tokenAmounts[1].token.address, 'ADDRESSES')
this.tokenAmounts = tokenAmounts
if (typeof address === 'string') this.address = validateAndParseAddress(address)
}
public get reserve0(): TokenAmount {
return this.tokenAmounts[0]
}
public get reserve1(): TokenAmount {
return this.tokenAmounts[1]
}
public get token0(): Token {
return this.tokenAmounts[0].token
}
public get token1(): Token {
return this.tokenAmounts[1].token
}
public getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Exchange] {
invariant(inputAmount.token.equals(this.token0) || inputAmount.token.equals(this.token1), 'TOKEN')
invariant(JSBI.greaterThan(inputAmount.raw, ZERO), 'ZERO')
invariant(JSBI.greaterThan(this.reserve0.raw, ZERO), 'ZERO')
invariant(JSBI.greaterThan(this.reserve1.raw, ZERO), 'ZERO')
// validate conditions that must be true of an Exchange const inputReserve = inputAmount.token.equals(this.reserve0.token) ? this.reserve0 : this.reserve1
const chainIds = pair.map(token => token.chainId) const outputReserve = inputAmount.token.equals(this.reserve0.token) ? this.reserve1 : this.reserve0
invariant(chainIds[0] === chainIds[1], `${chainIds} are not equal.`) const inputAmountWithFee = JSBI.multiply(inputAmount.raw, _997)
const addresses = pair.map(token => token.address) const numerator = JSBI.multiply(inputAmountWithFee, outputReserve.raw)
invariant(addresses[0] < addresses[1], `${addresses} are not ordered.`) const denominator = JSBI.add(JSBI.multiply(inputReserve.raw, _1000), inputAmountWithFee)
const output = new TokenAmount(
inputAmount.token.equals(this.token0) ? this.token1 : this.token0,
JSBI.divide(numerator, denominator)
)
return [output, new Exchange(inputReserve.add(inputAmount), outputReserve.subtract(output), this.address)]
} }
constructor(pair: [Token, Token], balances: [BigintIsh, BigintIsh]) { public getInputAmount(outputAmount: TokenAmount): [TokenAmount, Exchange] {
const balancesParsed = balances.map(balance => parseBigintIsh(balance)) invariant(outputAmount.token.equals(this.token0) || outputAmount.token.equals(this.token1), 'TOKEN')
const inOrder = pair[0].address < pair[1].address invariant(JSBI.greaterThan(outputAmount.raw, ZERO), 'ZERO')
const orderedPair = (inOrder ? pair : pair.slice().reverse()) as [Token, Token] invariant(JSBI.greaterThan(this.reserve0.raw, ZERO), 'ZERO')
const orderedBalances = (inOrder ? balancesParsed : balancesParsed.slice().reverse()) as [JSBI, JSBI] invariant(JSBI.greaterThan(this.reserve1.raw, ZERO), 'ZERO')
Exchange.validate(orderedPair, orderedBalances)
this.pair = orderedPair const inputReserve = outputAmount.token.equals(this.reserve0.token) ? this.reserve1 : this.reserve0
this.balances = orderedBalances const outputReserve = outputAmount.token.equals(this.reserve0.token) ? this.reserve0 : this.reserve1
const numerator = JSBI.multiply(JSBI.multiply(inputReserve.raw, outputAmount.raw), _1000)
const denominator = JSBI.multiply(JSBI.subtract(outputReserve.raw, outputAmount.raw), _997)
const input = new TokenAmount(
outputAmount.token.equals(this.token0) ? this.token1 : this.token0,
JSBI.add(JSBI.divide(numerator, denominator), ONE)
)
return [input, new Exchange(inputReserve.add(input), outputReserve.subtract(outputAmount), this.address)]
} }
} }
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { ZERO, ONE, TEN, _100 } from '../constants'
import { BigintIsh } from '../types'
import { parseBigintIsh } from '../utils/parseInputs'
import { formatSignificant, formatFixed } from '../utils/formatOutputs'
import { Route } from './route'
export class Fraction {
public readonly numerator: JSBI
public readonly denominator: JSBI
constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) {
this.numerator = parseBigintIsh(numerator)
this.denominator = parseBigintIsh(denominator)
}
// warning: this can truncate!
get quotient() {
return JSBI.divide(this.numerator, this.denominator)
}
public invert(): Fraction {
return new Fraction(this.denominator, this.numerator)
}
public multiply(other: Fraction): Fraction {
return new Fraction(
JSBI.multiply(this.numerator, other.numerator),
JSBI.multiply(this.denominator, other.denominator)
)
}
public formatSignificant(significantDigits: number, ...rest: any[]): string {
return formatSignificant(this.numerator, this.denominator, significantDigits, ...rest)
}
public formatFixed(decimalPlaces: number, ...rest: any[]): string {
return formatFixed(this.numerator, this.denominator, decimalPlaces, ...rest)
}
}
export class Price {
public readonly price: Fraction // normalized
public readonly scalar: Fraction // used to convert back to raw balances
static fromRoute(route: Route): Price {
const prices: Fraction[] = route.exchanges.map((exchange, i) => {
const input = route.path[i]
const baseIndex = input.address === exchange.pair[0].address ? 0 : 1
const quoteIndex = input.address === exchange.pair[0].address ? 1 : 0
return new Fraction(
JSBI.multiply(
exchange.balances[quoteIndex],
JSBI.exponentiate(TEN, JSBI.BigInt(exchange.pair[baseIndex].decimals))
),
JSBI.multiply(
exchange.balances[baseIndex],
JSBI.exponentiate(TEN, JSBI.BigInt(exchange.pair[quoteIndex].decimals))
)
)
})
const price = prices.reduce((accumulator, currentValue) => accumulator.multiply(currentValue), new Fraction(ONE))
const scalar = new Fraction(
JSBI.exponentiate(TEN, JSBI.BigInt(route.output.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(route.input.decimals))
)
return new Price(price, scalar)
}
constructor(price: Fraction, scalar: Fraction) {
this.price = price
this.scalar = scalar
}
public invert(): Price {
return new Price(this.price.invert(), this.scalar.invert())
}
public quote(amount: BigintIsh): JSBI {
const amountParsed = parseBigintIsh(amount)
invariant(JSBI.greaterThan(amountParsed, ZERO), `${amountParsed} isn't positive.`)
return this.price.multiply(this.scalar).multiply(new Fraction(amount)).quotient
}
public formatSignificant(significantDigits = 6, ...rest: any[]): string {
return this.price.formatSignificant(significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 6, ...rest: any[]): string {
return this.price.formatFixed(decimalPlaces, ...rest)
}
}
export class Percent {
public readonly percent: Fraction
constructor(percent: Fraction) {
this.percent = percent
}
public formatSignificant(significantDigits = 5, ...rest: any[]): string {
return this.percent.multiply(new Fraction(_100)).formatSignificant(significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 2, ...rest: any[]): string {
return this.percent.multiply(new Fraction(_100)).formatFixed(decimalPlaces, ...rest)
}
}
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import _Decimal from 'decimal.js-light'
import _Big, { RoundingMode } from 'big.js'
import toFormat from 'toformat'
import { BigintIsh, Rounding } from '../../types'
import { ONE } from '../../constants'
import { parseBigintIsh } from '../../utils'
const Decimal = toFormat(_Decimal)
const Big = toFormat(_Big)
const toSignificantRounding = {
[Rounding.ROUND_DOWN]: Decimal.ROUND_DOWN,
[Rounding.ROUND_HALF_UP]: Decimal.ROUND_HALF_UP,
[Rounding.ROUND_UP]: Decimal.ROUND_UP
}
const toFixedRounding = {
[Rounding.ROUND_DOWN]: RoundingMode.RoundDown,
[Rounding.ROUND_HALF_UP]: RoundingMode.RoundHalfUp,
[Rounding.ROUND_UP]: RoundingMode.RoundUp
}
export class Fraction {
public readonly numerator: JSBI
public readonly denominator: JSBI
public constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) {
this.numerator = parseBigintIsh(numerator)
this.denominator = parseBigintIsh(denominator)
}
// performs floor division
public get quotient(): JSBI {
return JSBI.divide(this.numerator, this.denominator)
}
public invert(): Fraction {
return new Fraction(this.denominator, this.numerator)
}
public multiply(other: Fraction | BigintIsh): Fraction {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return new Fraction(
JSBI.multiply(this.numerator, otherParsed.numerator),
JSBI.multiply(this.denominator, otherParsed.denominator)
)
}
public toSignificant(
significantDigits: number,
format: object = { groupSeparator: '' },
rounding: Rounding = Rounding.ROUND_HALF_UP,
maximumDecimalPlaces: number = Number.MAX_SAFE_INTEGER // should only be used to properly bound token amounts
): string {
invariant(Number.isInteger(significantDigits), `${significantDigits} is not a positive integer.`)
invariant(significantDigits > 0, `${significantDigits} is not positive.`)
invariant(Number.isInteger(maximumDecimalPlaces), `${maximumDecimalPlaces} is not an integer.`)
invariant(maximumDecimalPlaces >= 0, `maximumDecimalPlaces ${maximumDecimalPlaces} is negative.`)
Decimal.set({ precision: significantDigits + 1, rounding: toSignificantRounding[rounding] })
const quotient = new Decimal(this.numerator.toString())
.div(this.denominator.toString())
.toSignificantDigits(significantDigits)
const decimalPlaces =
quotient.precision(true) >= significantDigits
? quotient.decimalPlaces()
: significantDigits - (quotient.precision(true) - quotient.decimalPlaces())
return quotient.toFormat(Math.min(decimalPlaces, maximumDecimalPlaces), format)
}
public toFixed(
decimalPlaces: number,
format: object = { groupSeparator: '' },
rounding: Rounding = Rounding.ROUND_HALF_UP
): string {
invariant(Number.isInteger(decimalPlaces), `${decimalPlaces} is not an integer.`)
invariant(decimalPlaces >= 0, `${decimalPlaces} is negative.`)
Big.DP = decimalPlaces
Big.RM = toFixedRounding[rounding]
return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(decimalPlaces, format)
}
}
export * from './fraction'
export * from './percent'
export * from './tokenAmount'
export * from './price'
import { Rounding } from '../../types'
import { _100 } from '../../constants'
import { Fraction } from './fraction'
const _100Percent = new Fraction(_100)
export class Percent extends Fraction {
public toSignificant(significantDigits: number = 5, format?: object, rounding?: Rounding): string {
return this.multiply(_100Percent).toSignificant(significantDigits, format, rounding)
}
public toFixed(decimalPlaces: number = 2, format?: object, rounding?: Rounding): string {
return this.multiply(_100Percent).toSignificant(decimalPlaces, format, rounding)
}
}
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { BigintIsh, Rounding } from '../../types'
import { TEN } from '../../constants'
import { Token } from '../token'
import { Route } from '../route'
import { Fraction } from './fraction'
import { TokenAmount } from './tokenAmount'
export class Price extends Fraction {
public readonly baseToken: Token // input i.e. denominator
public readonly quoteToken: Token // output i.e. numerator
public readonly scalar: Fraction // used to adjust the raw fraction w/r/t the decimals of the {base,quote}Tokens
static fromRoute(route: Route): Price {
const prices: Price[] = []
for (const [i, exchange] of route.exchanges.entries()) {
prices.push(
route.path[i].equals(exchange.token0)
? new Price(exchange.reserve0.token, exchange.reserve1.token, exchange.reserve0.raw, exchange.reserve1.raw)
: new Price(exchange.reserve1.token, exchange.reserve0.token, exchange.reserve1.raw, exchange.reserve0.raw)
)
}
return prices.slice(1).reduce((accumulator, currentValue) => accumulator.multiply(currentValue), prices[0])
}
// denominator and numerator _must be_ scaled in units of the {base,quote}Tokens
constructor(baseToken: Token, quoteToken: Token, denominator: BigintIsh, numerator: BigintIsh) {
super(numerator, denominator)
this.baseToken = baseToken
this.quoteToken = quoteToken
this.scalar = new Fraction(
JSBI.exponentiate(TEN, JSBI.BigInt(baseToken.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteToken.decimals))
)
}
public get raw(): Fraction {
return new Fraction(this.numerator, this.denominator)
}
public get adjusted(): Fraction {
return super.multiply(this.scalar)
}
public invert(): Price {
return new Price(this.quoteToken, this.baseToken, this.numerator, this.denominator)
}
public multiply(other: Price): Price {
invariant(this.quoteToken.equals(other.baseToken), 'BASE')
const fraction = super.multiply(other)
return new Price(this.baseToken, other.quoteToken, fraction.denominator, fraction.numerator)
}
// performs floor division on overflow
public quote(tokenAmount: TokenAmount): TokenAmount {
invariant(tokenAmount.token.equals(this.baseToken), 'TOKEN')
return new TokenAmount(this.quoteToken, super.multiply(tokenAmount.raw).quotient)
}
public toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string {
return this.adjusted.toSignificant(significantDigits, format, rounding)
}
public toFixed(decimalPlaces: number = 6, format?: object, rounding?: Rounding): string {
return this.adjusted.toFixed(decimalPlaces, format, rounding)
}
}
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { BigintIsh, Rounding } from '../../types'
import { TEN, SolidityType } from '../../constants'
import { parseBigintIsh, validateSolidityTypeInstance } from '../../utils'
import { Token } from '../token'
import { Fraction } from './fraction'
export class TokenAmount extends Fraction {
public readonly token: Token
// amount _must be_ scaled in units of the token
constructor(token: Token, amount: BigintIsh) {
const parsedAmount = parseBigintIsh(amount)
validateSolidityTypeInstance(parsedAmount, SolidityType.uint256)
super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(token.decimals)))
this.token = token
}
public get raw(): JSBI {
return this.numerator
}
public get adjusted(): Fraction {
return this
}
public add(other: TokenAmount): TokenAmount {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.add(this.raw, other.raw))
}
public subtract(other: TokenAmount): TokenAmount {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.subtract(this.raw, other.raw))
}
public toSignificant(significantDigits: number, format?: object, rounding: Rounding = Rounding.ROUND_DOWN): string {
return super.toSignificant(significantDigits, format, rounding, this.token.decimals)
}
public 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)
}
}
...@@ -2,5 +2,4 @@ export * from './token' ...@@ -2,5 +2,4 @@ export * from './token'
export * from './exchange' export * from './exchange'
export * from './route' export * from './route'
export * from './trade' export * from './trade'
export * from './fractions' export * from './fractions'
...@@ -2,34 +2,27 @@ import invariant from 'tiny-invariant' ...@@ -2,34 +2,27 @@ import invariant from 'tiny-invariant'
import { Token } from './token' import { Token } from './token'
import { Exchange } from './exchange' import { Exchange } from './exchange'
import { Price } from './fractions' import { Price } from './fractions/price'
export class Route { export class Route {
public readonly exchanges: Exchange[] public readonly exchanges: Exchange[]
public readonly path: Token[] public readonly path: Token[]
public readonly midPrice: Price public readonly midPrice: Price
static validate(exchanges: Exchange[], input: Token): Token[] { constructor(exchanges: Exchange[], input: Token) {
// validate components of a Route invariant(exchanges.length > 0, 'EXCHANGES')
invariant(exchanges.length > 0, `${exchanges} does not consist of at least 1 exchange.`) invariant(
exchanges.map(exchange => exchange.token0.chainId === exchanges[0].token0.chainId).every(x => x),
// validate conditions that must be true of a Route 'CHAIN_IDS'
const chainIds = exchanges.map(exchange => exchange.pair[0].chainId) // a sufficent check since exchanges are valid )
chainIds.forEach((chainId, _, array) => invariant(chainId === array[0], `${chainIds} are not all equal.`))
const path = [input] const path = [input]
exchanges.forEach((exchange, i) => { for (const [i, exchange] of exchanges.entries()) {
const currentInput = path[i] const currentInput = path[i]
const addresses = exchange.pair.map(token => token.address) invariant(currentInput.equals(exchange.token0) || currentInput.equals(exchange.token1), 'PATH')
invariant(addresses.includes(currentInput.address), `${addresses} does not contain ${input.address}.`) const output = currentInput.equals(exchange.token0) ? exchange.token1 : exchange.token0
const output = currentInput.address === addresses[0] ? exchange.pair[1] : exchange.pair[0]
path.push(output) path.push(output)
}) }
invariant(path.length === new Set(path).size, `${path} contains duplicate addresses.`) invariant(path.length === new Set(path).size, 'PATH')
return path
}
constructor(exchanges: Exchange[], input: Token) {
const path = Route.validate(exchanges, input)
this.exchanges = exchanges this.exchanges = exchanges
this.path = path this.path = path
......
import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { SolidityType } from '../constants' import { ChainId, SolidityType } from '../constants'
import { validateChainId, validateAddress, validateSolidityTypeInstance } from '../utils/validateInputs' import ERC20 from '../abis/ERC20.json'
import { validateAndParseAddress, validateSolidityTypeInstance } from '../utils'
const CACHE: { [chainId: number]: { [address: string]: number } } = {
1: {
'0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A': 9 // DGD
}
}
export class Token { export class Token {
public readonly chainId: number public readonly chainId: ChainId
public readonly address: string public readonly address: string
public readonly decimals: number public readonly decimals: number
public readonly symbol?: string
public readonly name?: string
static validate(chainId: number, address: string, decimals: number) { static async fetchData(
validateChainId(chainId) chainId: ChainId,
validateAddress(address) address: string,
validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8) provider = getDefaultProvider(getNetwork(chainId)),
symbol?: string,
name?: string
): Promise<Token> {
const parsedDecimals =
typeof CACHE?.[chainId]?.[address] === 'number'
? CACHE[chainId][address]
: await new Contract(address, ERC20, provider).decimals().then((decimals: number): number => {
CACHE[chainId][address] = decimals
return decimals
})
return new Token(chainId, address, parsedDecimals, symbol, name)
} }
constructor(chainId: number, address: string, decimals: number) { constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) {
Token.validate(chainId, address, decimals) validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8)
this.chainId = chainId this.chainId = chainId
this.address = address this.address = validateAndParseAddress(address)
this.decimals = decimals this.decimals = decimals
if (typeof symbol === 'string') this.symbol = symbol
if (typeof name === 'string') this.name = name
} }
public equals(other: Token): boolean {
const equal = this.chainId === other.chainId && this.address === other.address
if (equal) invariant(this.decimals === other.decimals, 'DECIMALS')
return equal
}
}
export const WETH = {
[ChainId.MAINNET]: new Token(
ChainId.MAINNET,
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
18,
'WETH',
'Wrapped Ether'
),
[ChainId.ROPSTEN]: new Token(
ChainId.ROPSTEN,
'0xc778417E063141139Fce010982780140Aa0cD5Ab',
18,
'WETH',
'Wrapped Ether'
),
[ChainId.RINKEBY]: new Token(
ChainId.RINKEBY,
'0xc778417E063141139Fce010982780140Aa0cD5Ab',
18,
'WETH',
'Wrapped Ether'
),
[ChainId.GÖRLI]: new Token(ChainId.GÖRLI, '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', 18, 'WETH', 'Wrapped Ether'),
[ChainId.KOVAN]: new Token(ChainId.KOVAN, '0xd0A1E359811322d97991E03f863a0C30C2cF029C', 18, 'WETH', 'Wrapped Ether')
} }
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { ZERO, ONE, _997, _1000, SolidityType, TradeType } from '../constants' import { TradeType } from '../constants'
import { BigintIsh } from '../types'
import { parseBigintIsh } from '../utils/parseInputs'
import { validateSolidityTypeInstance } from '../utils/validateInputs'
import { Exchange } from './exchange' import { Exchange } from './exchange'
import { Route } from './route' import { Route } from './route'
import { Fraction, Price, Percent } from './fractions' import { Fraction, TokenAmount } from './fractions'
import { Price } from './fractions/price'
import { Percent } from './fractions/percent'
function getOutputAmount(inputAmount: JSBI, inputReserve: JSBI, outputReserve: JSBI): JSBI { function getSlippage(inputAmount: TokenAmount, midPrice: Price, outputAmount: TokenAmount): Percent {
invariant(JSBI.greaterThan(inputAmount, ZERO), `${inputAmount} is not positive.`) const exactQuote = midPrice.raw.multiply(inputAmount.raw)
invariant(JSBI.greaterThan(inputReserve, ZERO), `${inputReserve} is not positive.`) // calculate (outputAmount - exactQuote) / exactQuote
invariant(JSBI.greaterThan(outputReserve, ZERO), `${outputReserve} is not positive.`) const exactDifference = new Fraction(
const inputAmountWithFee = JSBI.multiply(inputAmount, _997) JSBI.subtract(JSBI.multiply(outputAmount.raw, exactQuote.denominator), exactQuote.numerator),
const numerator = JSBI.multiply(inputAmountWithFee, outputReserve)
const denominator = JSBI.add(JSBI.multiply(inputReserve, _1000), inputAmountWithFee)
return JSBI.divide(numerator, denominator)
}
function getInputAmount(outputAmount: JSBI, inputReserve: JSBI, outputReserve: JSBI): JSBI {
invariant(JSBI.greaterThan(outputAmount, ZERO), `${outputAmount} is not positive.`)
invariant(JSBI.greaterThan(inputReserve, ZERO), `${inputReserve} is not positive.`)
invariant(JSBI.greaterThan(outputReserve, ZERO), `${outputReserve} is not positive.`)
const numerator = JSBI.multiply(JSBI.multiply(inputReserve, outputAmount), _1000)
const denominator = JSBI.multiply(JSBI.subtract(outputReserve, outputAmount), _997)
return JSBI.add(JSBI.divide(numerator, denominator), ONE)
}
function getSlippage(inputAmount: JSBI, midPrice: Price, outputAmount: JSBI): Percent {
const exactQuote = midPrice.price.multiply(midPrice.scalar).multiply(new Fraction(inputAmount))
const normalizedNumerator = new Fraction(
JSBI.subtract(JSBI.multiply(outputAmount, exactQuote.denominator), exactQuote.numerator),
exactQuote.denominator exactQuote.denominator
) )
const invertedDenominator = exactQuote.invert() const slippage = exactDifference.multiply(exactQuote.invert())
return new Percent(normalizedNumerator.multiply(invertedDenominator)) return new Percent(slippage.numerator, slippage.denominator)
} }
function getPercentChange(referenceRate: Price, newRate: Price): Percent { function getPercentChange(referenceRate: Price, newRate: Price): Percent {
const normalizedNumerator = new Fraction( // calculate (newRate - referenceRate) / referenceRate
const difference = new Fraction(
JSBI.subtract( JSBI.subtract(
JSBI.multiply(newRate.price.numerator, referenceRate.price.denominator), JSBI.multiply(newRate.adjusted.numerator, referenceRate.adjusted.denominator),
JSBI.multiply(referenceRate.price.numerator, newRate.price.denominator) JSBI.multiply(referenceRate.adjusted.numerator, newRate.adjusted.denominator)
), ),
JSBI.multiply(referenceRate.price.denominator, newRate.price.denominator) JSBI.multiply(referenceRate.adjusted.denominator, newRate.adjusted.denominator)
) )
const invertedDenominator = referenceRate.price.invert() const percentChange = difference.multiply(referenceRate.adjusted.invert())
return new Percent(normalizedNumerator.multiply(invertedDenominator)) return new Percent(percentChange.numerator, percentChange.denominator)
} }
export class Trade { export class Trade {
public readonly route: Route public readonly route: Route
public readonly inputAmount: JSBI
public readonly outputAmount: JSBI
public readonly tradeType: TradeType public readonly tradeType: TradeType
public readonly inputAmount: TokenAmount
public readonly outputAmount: TokenAmount
public readonly executionPrice: Price public readonly executionPrice: Price
public readonly nextMidPrice: Price public readonly nextMidPrice: Price
public readonly slippage: Percent public readonly slippage: Percent
public readonly midPricePercentChange: Percent public readonly midPricePercentChange: Percent
static validate(amount: JSBI) { constructor(route: Route, amount: TokenAmount, tradeType: TradeType) {
validateSolidityTypeInstance(amount, SolidityType.uint256) invariant(amount.token.equals(tradeType === TradeType.EXACT_INPUT ? route.input : route.output), 'TOKEN')
} const amounts: TokenAmount[] = new Array(route.path.length)
constructor(route: Route, amount: BigintIsh, tradeType: TradeType) {
const amountParsed = parseBigintIsh(amount)
Trade.validate(amountParsed)
const amounts: JSBI[] = new Array(route.exchanges.length + 1)
const nextExchanges: Exchange[] = new Array(route.exchanges.length) const nextExchanges: Exchange[] = new Array(route.exchanges.length)
if (tradeType === TradeType.EXACT_INPUT) { if (tradeType === TradeType.EXACT_INPUT) {
amounts[0] = amountParsed amounts[0] = amount
route.exchanges.forEach((exchange, i) => { for (let i = 0; i < route.path.length - 1; i++) {
const input = route.path[i] const exchange = route.exchanges[i]
const inputIndex = input.address === exchange.pair[0].address ? 0 : 1 const [outputAmount, nextExchange] = exchange.getOutputAmount(amounts[i])
const outputIndex = input.address === exchange.pair[0].address ? 1 : 0
const inputAmount = amounts[i]
const outputAmount = getOutputAmount(inputAmount, exchange.balances[inputIndex], exchange.balances[outputIndex])
amounts[i + 1] = outputAmount amounts[i + 1] = outputAmount
const nextExchange = new Exchange(
[exchange.pair[inputIndex], exchange.pair[outputIndex]],
[
JSBI.add(exchange.balances[inputIndex], inputAmount),
JSBI.subtract(exchange.balances[outputIndex], outputAmount)
]
)
nextExchanges[i] = nextExchange nextExchanges[i] = nextExchange
}) }
} else if (tradeType === TradeType.EXACT_OUTPUT) { } else {
amounts[amounts.length - 1] = amountParsed amounts[amounts.length - 1] = amount
route.exchanges for (let i = route.path.length - 1; i > 0; i--) {
.slice() const exchange = route.exchanges[i - 1]
.reverse() const [inputAmount, nextExchange] = exchange.getInputAmount(amounts[i])
.forEach((exchange, i) => { amounts[i - 1] = inputAmount
const inverseIndex = route.exchanges.length - 1 - i nextExchanges[i - 1] = nextExchange
const input = route.path[inverseIndex] }
const inputIndex = input.address === exchange.pair[0].address ? 0 : 1
const outputIndex = input.address === exchange.pair[0].address ? 1 : 0
const outputAmount = amounts[inverseIndex + 1]
const inputAmount = getInputAmount(
outputAmount,
exchange.balances[inputIndex],
exchange.balances[outputIndex]
)
amounts[inverseIndex] = inputAmount
const nextExchange = new Exchange(
[exchange.pair[inputIndex], exchange.pair[outputIndex]],
[
JSBI.add(exchange.balances[inputIndex], inputAmount),
JSBI.subtract(exchange.balances[outputIndex], outputAmount)
]
)
nextExchanges[inverseIndex] = nextExchange
})
} }
this.route = route this.route = route
this.tradeType = tradeType
const inputAmount = amounts[0] const inputAmount = amounts[0]
const outputAmount = amounts[amounts.length - 1] const outputAmount = amounts[amounts.length - 1]
this.inputAmount = inputAmount this.inputAmount = inputAmount
this.outputAmount = outputAmount this.outputAmount = outputAmount
this.tradeType = tradeType this.executionPrice = new Price(route.input, route.output, inputAmount.raw, outputAmount.raw)
this.executionPrice = new Price(
new Fraction(outputAmount, inputAmount).multiply(route.midPrice.scalar.invert()),
route.midPrice.scalar
)
const nextMidPrice = Price.fromRoute(new Route(nextExchanges, route.input)) const nextMidPrice = Price.fromRoute(new Route(nextExchanges, route.input))
this.nextMidPrice = nextMidPrice this.nextMidPrice = nextMidPrice
this.slippage = getSlippage(inputAmount, route.midPrice, outputAmount) this.slippage = getSlippage(inputAmount, route.midPrice, outputAmount)
......
export * from './types' export * from './types'
export { ChainId, WETH, TradeType } from './constants' export * from './constants'
export * from './entities' export * from './entities'
export * from './utils'
import JSBI from 'jsbi' import JSBI from 'jsbi'
export type BigintIsh = bigint | JSBI | string export type BigintIsh = JSBI | bigint | string
export enum Rounding {
ROUND_DOWN,
ROUND_HALF_UP,
ROUND_UP
}
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import JSBI from 'jsbi'
import { getAddress } from '@ethersproject/address'
import { BigintIsh } from './types'
import { ZERO, SolidityType } from './constants'
const SOLIDITY_TYPE_MAXIMA = {
[SolidityType.uint8]: JSBI.BigInt('0xff'),
[SolidityType.uint256]: JSBI.BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
}
// only works for uint{8,256}
export function validateSolidityTypeInstance(value: JSBI, solidityType: SolidityType): void {
invariant(JSBI.greaterThanOrEqual(value, ZERO), `${value} is not a ${solidityType}.`)
invariant(JSBI.lessThanOrEqual(value, SOLIDITY_TYPE_MAXIMA[solidityType]), `${value} is not a ${solidityType}.`)
}
// warns if addresses are not checksummed
export function validateAndParseAddress(address: string): string {
try {
const checksummedAddress = getAddress(address)
warning(address === checksummedAddress, `${address} is not checksummed.`)
return checksummedAddress
} catch (error) {
invariant(false, `${address} is not a valid address.`)
}
}
export function parseBigintIsh(bigintIsh: BigintIsh): JSBI {
return bigintIsh instanceof JSBI
? bigintIsh
: typeof bigintIsh === 'bigint'
? JSBI.BigInt(bigintIsh.toString())
: JSBI.BigInt(bigintIsh)
}
import invariant from 'tiny-invariant'
import _Decimal from 'decimal.js-light'
import _Big, { RoundingMode } from 'big.js'
import toFormat from 'toformat'
import { BigintIsh } from '../types'
import { ONE } from '../constants'
import { parseBigintIsh } from './parseInputs'
const Decimal = toFormat(_Decimal)
const Big = toFormat(_Big)
export function formatSignificant(
numerator: BigintIsh,
denominator: BigintIsh = ONE,
significantDigits: number,
format: object = { groupSeparator: '' },
roundingMode: number = Decimal.ROUND_HALF_UP
): string {
invariant(Number.isInteger(significantDigits), `${significantDigits} is not an integer.`)
invariant(significantDigits > 0, `${significantDigits} isn't positive.`)
const numeratorParsed = parseBigintIsh(numerator)
const denominatorParsed = parseBigintIsh(denominator)
Decimal.set({ precision: significantDigits + 1, rounding: roundingMode })
const quotient = new Decimal(numeratorParsed.toString())
.div(denominatorParsed.toString())
.toSignificantDigits(significantDigits)
return quotient.toFormat(
quotient.precision(true) >= significantDigits
? quotient.decimalPlaces()
: significantDigits - (quotient.precision(true) - quotient.decimalPlaces()),
format
)
}
export function formatFixed(
numerator: BigintIsh,
denominator: BigintIsh = ONE,
decimalPlaces: number,
format: object = { groupSeparator: '' },
roundingMode: RoundingMode = RoundingMode.RoundHalfUp
): string {
invariant(Number.isInteger(decimalPlaces), `${decimalPlaces} is not an integer.`)
invariant(decimalPlaces >= 0, `${decimalPlaces} is negative.`)
const numeratorParsed = parseBigintIsh(numerator)
const denominatorParsed = parseBigintIsh(denominator)
Big.DP = decimalPlaces
Big.RM = roundingMode
return new Big(numeratorParsed.toString()).div(denominatorParsed.toString()).toFormat(decimalPlaces, format)
}
export * from './formatOutputs'
import JSBI from 'jsbi'
import { BigintIsh } from '../types'
export function parseBigintIsh(bigintIsh: BigintIsh): JSBI {
return typeof bigintIsh === 'bigint'
? JSBI.BigInt(bigintIsh.toString())
: bigintIsh instanceof JSBI
? bigintIsh
: JSBI.BigInt(bigintIsh)
}
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { getAddress } from '@ethersproject/address'
import { ZERO, ChainId, SolidityType } from '../constants'
export function validateChainId(chainId: number) {
invariant(!!ChainId[chainId], `${chainId} is not a supported chainId.`)
}
export function validateAddress(address: string) {
try {
if (address !== getAddress(address)) {
throw Error('Address is not properly checksummed.')
}
} catch (error) {
invariant(false, `${address} is an invalid address. ${error}`)
}
}
const SolidityTypeMaxima = {
[SolidityType.uint8]: JSBI.BigInt(2 ** 8 - 1),
[SolidityType.uint256]: JSBI.BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
}
export function validateSolidityTypeInstance(value: JSBI, solidityType: SolidityType) {
invariant(JSBI.greaterThanOrEqual(value, ZERO), `${value} is negative.`)
invariant(JSBI.lessThanOrEqual(value, SolidityTypeMaxima[solidityType]), `${value} is too large.`)
}
import { ChainId, WETH as _WETH, TradeType, Token, Exchange, Route, Trade } from '../src'
const ADDRESSES = [
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000001',
'0x0000000000000000000000000000000000000002'
]
const CHAIN_ID = ChainId.RINKEBY
const WETH = _WETH[ChainId.RINKEBY]
function getTokens(n: number, decimals: number | number[]) {
return ADDRESSES.slice(0, n).map(
(address, i) => new Token(CHAIN_ID, address, typeof decimals === 'number' ? decimals : decimals[i])
)
}
function decimalize(amount: number, decimal: number): bigint {
return BigInt(amount) * BigInt(10) ** BigInt(decimal)
}
describe('entities', () => {
;[
[0, 0, 0],
[0, 9, 18],
[18, 18, 18]
].forEach(decimals => {
describe(`decimals: ${decimals}`, () => {
let tokens: Token[]
it('Token', () => {
tokens = getTokens(3, decimals)
tokens.forEach((token, i) => {
expect(token.chainId).toEqual(CHAIN_ID)
expect(token.address).toEqual(ADDRESSES[i])
expect(token.decimals).toEqual(decimals[i])
})
})
let exchanges: Exchange[]
it('Exchange', () => {
const pairs: [Token, Token][] = [
[tokens[0], tokens[1]],
[tokens[1], tokens[2]],
[tokens[2], WETH]
]
const balances: [bigint, bigint][] = [
[decimalize(1, pairs[0][0].decimals), decimalize(1, pairs[0][1].decimals)],
[decimalize(1, pairs[1][0].decimals), decimalize(1, pairs[1][1].decimals)],
[decimalize(1, pairs[2][0].decimals), decimalize(1234, pairs[2][1].decimals)]
]
exchanges = [
new Exchange(pairs[0], balances[0]),
new Exchange(pairs[1], balances[1]),
new Exchange(pairs[2], balances[2])
]
})
let route: Route
it('Route', () => {
route = new Route(exchanges, tokens[0])
expect(route.path).toEqual(tokens.concat([WETH]))
expect(route.input).toEqual(tokens[0])
expect(route.output).toEqual(WETH)
})
it('Price via Route.marketRate', () => {
expect(route.midPrice.quote(decimalize(1, route.input.decimals)).toString()).toEqual(
decimalize(1234, route.output.decimals).toString()
)
expect(
route.midPrice
.invert()
.quote(decimalize(1234, route.output.decimals))
.toString()
).toEqual(decimalize(1, route.input.decimals).toString())
expect(route.midPrice.formatSignificant(1)).toEqual('1000')
expect(route.midPrice.formatSignificant(2)).toEqual('1200')
expect(route.midPrice.formatSignificant(3)).toEqual('1230')
expect(route.midPrice.formatSignificant(4)).toEqual('1234')
expect(route.midPrice.formatSignificant(5)).toEqual('1234.0')
expect(route.midPrice.formatSignificant(5, { groupSeparator: ',' })).toEqual('1,234.0')
expect(route.midPrice.invert().formatSignificant(1)).toEqual('0.0008')
expect(route.midPrice.invert().formatSignificant(2)).toEqual('0.00081')
expect(route.midPrice.invert().formatSignificant(3)).toEqual('0.000810')
expect(route.midPrice.invert().formatSignificant(4)).toEqual('0.0008104')
expect(route.midPrice.invert().formatSignificant(4, undefined, 1)).toEqual('0.0008103')
expect(route.midPrice.invert().formatSignificant(5)).toEqual('0.00081037')
expect(route.midPrice.formatFixed(0)).toEqual('1234')
expect(route.midPrice.formatFixed(1)).toEqual('1234.0')
expect(route.midPrice.formatFixed(2)).toEqual('1234.00')
expect(route.midPrice.formatFixed(2, { groupSeparator: ',' })).toEqual('1,234.00')
expect(route.midPrice.invert().formatFixed(0)).toEqual('0')
expect(route.midPrice.invert().formatFixed(1)).toEqual('0.0')
expect(route.midPrice.invert().formatFixed(2)).toEqual('0.00')
expect(route.midPrice.invert().formatFixed(3)).toEqual('0.001')
expect(route.midPrice.invert().formatFixed(4)).toEqual('0.0008')
expect(route.midPrice.invert().formatFixed(5)).toEqual('0.00081')
expect(route.midPrice.invert().formatFixed(6)).toEqual('0.000810')
expect(route.midPrice.invert().formatFixed(7)).toEqual('0.0008104')
expect(route.midPrice.invert().formatFixed(7, undefined, 0)).toEqual('0.0008103')
expect(route.midPrice.invert().formatFixed(8)).toEqual('0.00081037')
})
describe('Trade', () => {
it('TradeType.EXACT_INPUT', () => {
const exchanges = [
new Exchange([tokens[1], WETH], [decimalize(5, tokens[1].decimals), decimalize(10, WETH.decimals)])
]
const route = new Route(exchanges, tokens[1])
const inputAmount = decimalize(1, tokens[1].decimals)
const trade = new Trade(route, inputAmount, TradeType.EXACT_INPUT)
expect(trade.inputAmount.toString()).toEqual(inputAmount.toString())
expect(trade.outputAmount.toString()).toEqual('1662497915624478906')
expect(trade.executionPrice.formatSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.invert().formatSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(inputAmount).toString()).toEqual(trade.outputAmount.toString())
expect(
trade.executionPrice
.invert()
.quote(trade.outputAmount)
.toString()
).toEqual(inputAmount.toString())
expect(trade.nextMidPrice.formatSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.invert().formatSignificant(18)).toEqual('0.719640000000000000')
expect(trade.slippage.formatSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.midPricePercentChange.formatSignificant(18)).toEqual('-30.5208159635373242')
})
it('TradeType.EXACT_OUTPUT', () => {
const exchanges = [
new Exchange([tokens[1], WETH], [decimalize(5, tokens[1].decimals), decimalize(10, WETH.decimals)])
]
const route = new Route(exchanges, tokens[1])
const outputAmount = BigInt('1662497915624478906')
const trade = new Trade(route, outputAmount, TradeType.EXACT_OUTPUT)
expect(trade.inputAmount.toString()).toEqual(decimalize(1, tokens[1].decimals).toString())
expect(trade.outputAmount.toString()).toEqual(outputAmount.toString())
// TODO think about inverse execution price?
expect(trade.executionPrice.formatSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.invert().formatSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(trade.inputAmount).toString()).toEqual(outputAmount.toString())
expect(
trade.executionPrice
.invert()
.quote(outputAmount)
.toString()
).toEqual(trade.inputAmount.toString())
expect(trade.nextMidPrice.formatSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.invert().formatSignificant(18)).toEqual('0.719640000000000000')
expect(trade.slippage.formatSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.midPricePercentChange.formatSignificant(18)).toEqual('-30.5208159635373242')
})
it('minimum TradeType.EXACT_INPUT', () => {
if ([9, 18].includes(tokens[1].decimals)) {
const exchanges = [
new Exchange(
[tokens[1], WETH],
[
decimalize(1, tokens[1].decimals),
decimalize(10, WETH.decimals) +
(tokens[1].decimals === 9 ? BigInt('30090280812437312') : BigInt('30090270812437322'))
]
)
]
const route = new Route(exchanges, tokens[1])
const trade = new Trade(route, '1', TradeType.EXACT_INPUT)
expect(trade.slippage.formatSignificant(18)).toEqual(
tokens[1].decimals === 9 ? '-0.300000099400899902' : '-0.300000000000000100'
)
}
})
})
})
})
})
import { ChainId, WETH, Token, Exchange } from '../src'
describe('data', () => {
it('Token', async () => {
const token = await Token.fetchData(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F') // DAI
expect(token.decimals).toEqual(18)
})
it('Token:CACHE', async () => {
const token = await Token.fetchData(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A') // DGD
expect(token.decimals).toEqual(9)
})
it('Exchange', async () => {
const token = new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18) // DAI
const exchange = await Exchange.fetchData(WETH[ChainId.RINKEBY], token)
expect(exchange.address).toEqual('0xC0568fA2FC63123B7352c506076DFa5623D62Db5')
})
})
import { ChainId, WETH as _WETH, TradeType, Rounding, Token, TokenAmount, Exchange, Route, Trade } from '../src'
const ADDRESSES = [
'0x0000000000000000000000000000000000000001',
'0x0000000000000000000000000000000000000002',
'0x0000000000000000000000000000000000000003'
]
const CHAIN_ID = ChainId.RINKEBY
const WETH = _WETH[ChainId.RINKEBY]
const DECIMAL_PERMUTATIONS: [number, number, number][] = [
[0, 0, 0],
[0, 9, 18],
[18, 18, 18]
]
function decimalize(amount: number, decimals: number): bigint {
return BigInt(amount) * BigInt(10) ** BigInt(decimals)
}
describe('entities', () => {
DECIMAL_PERMUTATIONS.forEach(decimals => {
describe(`decimals permutation: ${decimals}`, () => {
let tokens: Token[]
it('Token', () => {
tokens = ADDRESSES.map((address, i) => new Token(CHAIN_ID, address, decimals[i]))
tokens.forEach((token, i) => {
expect(token.chainId).toEqual(CHAIN_ID)
expect(token.address).toEqual(ADDRESSES[i])
expect(token.decimals).toEqual(decimals[i])
})
})
let exchanges: Exchange[]
it('Exchange', () => {
exchanges = [
new Exchange(
new TokenAmount(tokens[0], decimalize(1, tokens[0].decimals)),
new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
),
new Exchange(
new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)),
new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals))
),
new Exchange(
new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals)),
new TokenAmount(WETH, decimalize(1234, WETH.decimals))
)
]
})
let route: Route
it('Route', () => {
route = new Route(exchanges, tokens[0])
expect(route.exchanges).toEqual(exchanges)
expect(route.path).toEqual(tokens.concat([WETH]))
expect(route.input).toEqual(tokens[0])
expect(route.output).toEqual(WETH)
})
it('Price:Route.midPrice', () => {
expect(route.midPrice.quote(new TokenAmount(route.input, decimalize(1, route.input.decimals)))).toEqual(
new TokenAmount(route.output, decimalize(1234, route.output.decimals))
)
expect(
route.midPrice.invert().quote(new TokenAmount(route.output, decimalize(1234, route.output.decimals)))
).toEqual(new TokenAmount(route.input, decimalize(1, route.input.decimals)))
expect(route.midPrice.toSignificant(1)).toEqual('1000')
expect(route.midPrice.toSignificant(2)).toEqual('1200')
expect(route.midPrice.toSignificant(3)).toEqual('1230')
expect(route.midPrice.toSignificant(4)).toEqual('1234')
expect(route.midPrice.toSignificant(5)).toEqual('1234.0')
expect(route.midPrice.toSignificant(5, { groupSeparator: ',' })).toEqual('1,234.0')
expect(route.midPrice.invert().toSignificant(1)).toEqual('0.0008')
expect(route.midPrice.invert().toSignificant(2)).toEqual('0.00081')
expect(route.midPrice.invert().toSignificant(3)).toEqual('0.000810')
expect(route.midPrice.invert().toSignificant(4)).toEqual('0.0008104')
expect(route.midPrice.invert().toSignificant(4, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103')
expect(route.midPrice.invert().toSignificant(5)).toEqual('0.00081037')
expect(route.midPrice.toFixed(0)).toEqual('1234')
expect(route.midPrice.toFixed(1)).toEqual('1234.0')
expect(route.midPrice.toFixed(2)).toEqual('1234.00')
expect(route.midPrice.toFixed(2, { groupSeparator: ',' })).toEqual('1,234.00')
expect(route.midPrice.invert().toFixed(0)).toEqual('0')
expect(route.midPrice.invert().toFixed(1)).toEqual('0.0')
expect(route.midPrice.invert().toFixed(2)).toEqual('0.00')
expect(route.midPrice.invert().toFixed(3)).toEqual('0.001')
expect(route.midPrice.invert().toFixed(4)).toEqual('0.0008')
expect(route.midPrice.invert().toFixed(5)).toEqual('0.00081')
expect(route.midPrice.invert().toFixed(6)).toEqual('0.000810')
expect(route.midPrice.invert().toFixed(7)).toEqual('0.0008104')
expect(route.midPrice.invert().toFixed(7, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103')
expect(route.midPrice.invert().toFixed(8)).toEqual('0.00081037')
})
describe('Trade', () => {
let route: Route
it('TradeType.EXACT_INPUT', () => {
route = new Route(
[
new Exchange(
new TokenAmount(tokens[1], decimalize(5, tokens[1].decimals)),
new TokenAmount(WETH, decimalize(10, WETH.decimals))
)
],
tokens[1]
)
const inputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
const expectedOutputAmount = new TokenAmount(WETH, '1662497915624478906')
const trade = new Trade(route, inputAmount, TradeType.EXACT_INPUT)
expect(trade.route).toEqual(route)
expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT)
expect(trade.inputAmount).toEqual(inputAmount)
expect(trade.outputAmount).toEqual(expectedOutputAmount)
expect(trade.executionPrice.toSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(inputAmount)).toEqual(expectedOutputAmount)
expect(trade.executionPrice.invert().quote(expectedOutputAmount)).toEqual(inputAmount)
expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.719640000000000000')
expect(trade.slippage.toSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.midPricePercentChange.toSignificant(18)).toEqual('-30.5208159635373242')
})
it('TradeType.EXACT_OUTPUT', () => {
const outputAmount = new TokenAmount(WETH, '1662497915624478906')
const expectedInputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
const trade = new Trade(route, outputAmount, TradeType.EXACT_OUTPUT)
expect(trade.route).toEqual(route)
expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT)
expect(trade.outputAmount).toEqual(outputAmount)
expect(trade.inputAmount).toEqual(expectedInputAmount)
expect(trade.executionPrice.toSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(expectedInputAmount)).toEqual(outputAmount)
expect(trade.executionPrice.invert().quote(outputAmount)).toEqual(expectedInputAmount)
expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.719640000000000000')
expect(trade.slippage.toSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.midPricePercentChange.toSignificant(18)).toEqual('-30.5208159635373242')
})
it('minimum TradeType.EXACT_INPUT', () => {
if ([9, 18].includes(tokens[1].decimals)) {
const route = new Route(
[
new Exchange(
new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)),
new TokenAmount(
WETH,
decimalize(10, WETH.decimals) +
(tokens[1].decimals === 9 ? BigInt('30090280812437312') : BigInt('30090270812437322'))
)
)
],
tokens[1]
)
const outputAmount = new TokenAmount(tokens[1], '1')
const trade = new Trade(route, outputAmount, TradeType.EXACT_INPUT)
expect(trade.slippage.toSignificant(18)).toEqual(
tokens[1].decimals === 9 ? '-0.300000099400899902' : '-0.300000000000000100'
)
}
})
})
})
})
})
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
"paths": { "paths": {
"*": ["src/*", "node_modules/*"] "*": ["src/*", "node_modules/*"]
}, },
"esModuleInterop": true "esModuleInterop": true,
"resolveJsonModule": true
} }
} }
This diff is collapsed.
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