Skip to content

Commit

Permalink
add a currency entity for ether (Uniswap#26)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
moodysalem committed Jul 6, 2020
1 parent d8cc586 commit 14aeb6e
Show file tree
Hide file tree
Showing 18 changed files with 890 additions and 140 deletions.
28 changes: 28 additions & 0 deletions src/entities/currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 }
65 changes: 65 additions & 0 deletions src/entities/fractions/currencyAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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)
}
}
1 change: 1 addition & 0 deletions src/entities/fractions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './fraction'
export * from './percent'
export * from './tokenAmount'
export * from './currencyAmount'
export * from './price'
40 changes: 23 additions & 17 deletions src/entities/fractions/price.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
import { Token } from '../token'
import { TokenAmount } from './tokenAmount'
import { currencyEquals } from '../token'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'

import { BigintIsh, Rounding, TEN } from '../../constants'
import { Token } from '../token'
import { Currency } from '../currency'
import { Route } from '../route'
import { Fraction } from './fraction'
import { TokenAmount } from './tokenAmount'
import { CurrencyAmount } from './currencyAmount'

export class Price extends Fraction {
public readonly baseToken: Token // input i.e. denominator
public readonly quoteToken: Token // output i.e. numerator
public readonly baseCurrency: Currency // input i.e. denominator
public readonly quoteCurrency: Currency // output i.e. numerator
public readonly scalar: Fraction // used to adjust the raw fraction w/r/t the decimals of the {base,quote}Token

static fromRoute(route: Route): Price {
const prices: Price[] = []
for (const [i, pair] of route.pairs.entries()) {
prices.push(
route.path[i].equals(pair.token0)
? new Price(pair.reserve0.token, pair.reserve1.token, pair.reserve0.raw, pair.reserve1.raw)
: new Price(pair.reserve1.token, pair.reserve0.token, pair.reserve1.raw, pair.reserve0.raw)
? new Price(pair.reserve0.currency, pair.reserve1.currency, pair.reserve0.raw, pair.reserve1.raw)
: new Price(pair.reserve1.currency, pair.reserve0.currency, pair.reserve1.raw, pair.reserve0.raw)
)
}
return prices.slice(1).reduce((accumulator, currentValue) => accumulator.multiply(currentValue), prices[0])
}

// denominator and numerator _must_ be raw, i.e. in the native representation
constructor(baseToken: Token, quoteToken: Token, denominator: BigintIsh, numerator: BigintIsh) {
constructor(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh) {
super(numerator, denominator)

this.baseToken = baseToken
this.quoteToken = quoteToken
this.baseCurrency = baseCurrency
this.quoteCurrency = quoteCurrency
this.scalar = new Fraction(
JSBI.exponentiate(TEN, JSBI.BigInt(baseToken.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteToken.decimals))
JSBI.exponentiate(TEN, JSBI.BigInt(baseCurrency.decimals)),
JSBI.exponentiate(TEN, JSBI.BigInt(quoteCurrency.decimals))
)
}

Expand All @@ -45,19 +48,22 @@ export class Price extends Fraction {
}

invert(): Price {
return new Price(this.quoteToken, this.baseToken, this.numerator, this.denominator)
return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator)
}

multiply(other: Price): Price {
invariant(this.quoteToken.equals(other.baseToken), 'BASE')
invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN')
const fraction = super.multiply(other)
return new Price(this.baseToken, other.quoteToken, fraction.denominator, fraction.numerator)
return new Price(this.baseCurrency, other.quoteCurrency, fraction.denominator, fraction.numerator)
}

// performs floor division on overflow
quote(tokenAmount: TokenAmount): TokenAmount {
invariant(tokenAmount.token.equals(this.baseToken), 'TOKEN')
return new TokenAmount(this.quoteToken, super.multiply(tokenAmount.raw).quotient)
quote(currencyAmount: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN')
if (this.quoteCurrency instanceof Token) {
return new TokenAmount(this.quoteCurrency, super.multiply(currencyAmount.raw).quotient)
}
return CurrencyAmount.ether(super.multiply(currencyAmount.raw).quotient)
}

toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string {
Expand Down
40 changes: 5 additions & 35 deletions src/entities/fractions/tokenAmount.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
import { CurrencyAmount } from './currencyAmount'
import { Token } from '../token'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import _Big from 'big.js'
import toFormat from 'toformat'

import { BigintIsh, Rounding, TEN, SolidityType } from '../../constants'
import { parseBigintIsh, validateSolidityTypeInstance } from '../../utils'
import { Token } from '../token'
import { Fraction } from './fraction'

const Big = toFormat(_Big)
import { BigintIsh } from '../../constants'

export class TokenAmount extends Fraction {
export class TokenAmount extends CurrencyAmount {
public readonly token: Token

// amount _must_ be raw, i.e. in the native representation
constructor(token: Token, amount: BigintIsh) {
const parsedAmount = parseBigintIsh(amount)
validateSolidityTypeInstance(parsedAmount, SolidityType.uint256)

super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(token.decimals)))
super(token, amount)
this.token = token
}

get raw(): JSBI {
return this.numerator
}

add(other: TokenAmount): TokenAmount {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.add(this.raw, other.raw))
Expand All @@ -35,22 +23,4 @@ export class TokenAmount extends Fraction {
invariant(this.token.equals(other.token), 'TOKEN')
return new TokenAmount(this.token, JSBI.subtract(this.raw, other.raw))
}

toSignificant(significantDigits: number = 6, format?: object, rounding: Rounding = Rounding.ROUND_DOWN): string {
return super.toSignificant(significantDigits, format, rounding)
}

toFixed(
decimalPlaces: number = this.token.decimals,
format?: object,
rounding: Rounding = Rounding.ROUND_DOWN
): string {
invariant(decimalPlaces <= this.token.decimals, 'DECIMALS')
return super.toFixed(decimalPlaces, format, rounding)
}

toExact(format: object = { groupSeparator: '' }): string {
Big.DP = this.token.decimals
return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format)
}
}
1 change: 1 addition & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './token'
export * from './pair'
export * from './route'
export * from './trade'
export * from './currency'

export * from './fractions'
27 changes: 20 additions & 7 deletions src/entities/pair.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TokenAmount } from './fractions/tokenAmount'
import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { getNetwork } from '@ethersproject/networks'
Expand All @@ -15,13 +16,13 @@ import {
ONE,
FIVE,
_997,
_1000
_1000,
ChainId
} from '../constants'
import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { sqrt, parseBigintIsh } from '../utils'
import { InsufficientReservesError, InsufficientInputAmountError } from '../errors'
import { Token } from './token'
import { TokenAmount } from './fractions/tokenAmount'

let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {}

Expand Down Expand Up @@ -75,6 +76,18 @@ export class Pair {
this.tokenAmounts = tokenAmounts as [TokenAmount, TokenAmount]
}

/**
* Returns true if the token is either token0 or token1
* @param token to check
*/
public involvesToken(token: Token): boolean {
return token.equals(this.token0) || token.equals(this.token1)
}

public get chainId(): ChainId {
return this.token0.chainId
}

get token0(): Token {
return this.tokenAmounts[0].token
}
Expand All @@ -92,12 +105,12 @@ export class Pair {
}

reserveOf(token: Token): TokenAmount {
invariant(token.equals(this.token0) || token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(token), 'TOKEN')
return token.equals(this.token0) ? this.reserve0 : this.reserve1
}

getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(inputAmount.token.equals(this.token0) || inputAmount.token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(inputAmount.token), 'TOKEN')
if (JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve1.raw, ZERO)) {
throw new InsufficientReservesError()
}
Expand All @@ -117,7 +130,7 @@ export class Pair {
}

getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(outputAmount.token.equals(this.token0) || outputAmount.token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(outputAmount.token), 'TOKEN')
if (
JSBI.equal(this.reserve0.raw, ZERO) ||
JSBI.equal(this.reserve1.raw, ZERO) ||
Expand Down Expand Up @@ -165,7 +178,7 @@ export class Pair {
feeOn: boolean = false,
kLast?: BigintIsh
): TokenAmount {
invariant(token.equals(this.token0) || token.equals(this.token1), 'TOKEN')
invariant(this.involvesToken(token), 'TOKEN')
invariant(totalSupply.token.equals(this.liquidityToken), 'TOTAL_SUPPLY')
invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY')
invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY')
Expand All @@ -175,7 +188,7 @@ export class Pair {
totalSupplyAdjusted = totalSupply
} else {
invariant(!!kLast, 'K_LAST')
const kLastParsed = parseBigintIsh(kLast as any)
const kLastParsed = parseBigintIsh(kLast)
if (!JSBI.equal(kLastParsed, ZERO)) {
const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw))
const rootKLast = sqrt(kLastParsed)
Expand Down
35 changes: 24 additions & 11 deletions src/entities/route.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
import { ChainId } from '../constants'
import invariant from 'tiny-invariant'

import { Token } from './token'
import { Currency, ETHER } from './currency'
import { Token, WETH } from './token'
import { Pair } from './pair'
import { Price } from './fractions/price'

export class Route {
public readonly pairs: Pair[]
public readonly path: Token[]
public readonly input: Currency
public readonly output: Currency
public readonly midPrice: Price

constructor(pairs: Pair[], input: Token) {
constructor(pairs: Pair[], input: Currency, output?: Currency) {
invariant(pairs.length > 0, 'PAIRS')
invariant(
pairs.map(pair => pair.token0.chainId === pairs[0].token0.chainId).every(x => x),
pairs.every(pair => pair.chainId === pairs[0].chainId),
'CHAIN_IDS'
)
const path = [input]
invariant(
(input instanceof Token && pairs[0].involvesToken(input)) ||
(input === ETHER && pairs[0].involvesToken(WETH[pairs[0].chainId])),
'INPUT'
)
invariant(
typeof output === 'undefined' ||
(output instanceof Token && pairs[pairs.length - 1].involvesToken(output)) ||
(output === ETHER && pairs[pairs.length - 1].involvesToken(WETH[pairs[0].chainId])),
'OUTPUT'
)

const path: Token[] = [input instanceof Token ? input : WETH[pairs[0].chainId]]
for (const [i, pair] of pairs.entries()) {
const currentInput = path[i]
invariant(currentInput.equals(pair.token0) || currentInput.equals(pair.token1), 'PATH')
const output = currentInput.equals(pair.token0) ? pair.token1 : pair.token0
path.push(output)
}
invariant(path.length === new Set(path).size, 'PATH')

this.pairs = pairs
this.path = path
this.midPrice = Price.fromRoute(this)
this.input = input
this.output = output ?? path[path.length - 1]
}

get input(): Token {
return this.path[0]
}

get output(): Token {
return this.path[this.path.length - 1]
get chainId(): ChainId {
return this.pairs[0].chainId
}
}
Loading

0 comments on commit 14aeb6e

Please sign in to comment.