Commit 280d3be3 authored by Moody Salem's avatar Moody Salem Committed by GitHub

make the fetching code more tree shakeable (#27)

* make the fetching code more tree shakeable

* more documentation everywhere
parent f10af5d0
...@@ -13,8 +13,17 @@ export class Currency { ...@@ -13,8 +13,17 @@ export class Currency {
public readonly symbol?: string public readonly symbol?: string
public readonly name?: string public readonly name?: string
/**
* The only instance of the base class `Currency`.
*/
public static readonly ETHER: Currency = new Currency(18, 'ETH', 'Ether') public static readonly ETHER: Currency = new Currency(18, 'ETH', 'Ether')
/**
* Constructs an instance of the base class `Currency`. The only instance of the base class `Currency` is `Currency.ETHER`.
* @param decimals decimals of the currency
* @param symbol symbol of the currency
* @param name of the currency
*/
protected constructor(decimals: number, symbol?: string, name?: string) { protected constructor(decimals: number, symbol?: string, name?: string) {
validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8) validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8)
......
...@@ -31,25 +31,29 @@ export class CurrencyAmount extends Fraction { ...@@ -31,25 +31,29 @@ export class CurrencyAmount extends Fraction {
this.currency = currency this.currency = currency
} }
get raw(): JSBI { public get raw(): JSBI {
return this.numerator return this.numerator
} }
add(other: CurrencyAmount): CurrencyAmount { public add(other: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(this.currency, other.currency), 'TOKEN') invariant(currencyEquals(this.currency, other.currency), 'TOKEN')
return new CurrencyAmount(this.currency, JSBI.add(this.raw, other.raw)) return new CurrencyAmount(this.currency, JSBI.add(this.raw, other.raw))
} }
subtract(other: CurrencyAmount): CurrencyAmount { public subtract(other: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(this.currency, other.currency), 'TOKEN') invariant(currencyEquals(this.currency, other.currency), 'TOKEN')
return new CurrencyAmount(this.currency, JSBI.subtract(this.raw, other.raw)) return new CurrencyAmount(this.currency, JSBI.subtract(this.raw, other.raw))
} }
toSignificant(significantDigits: number = 6, format?: object, rounding: Rounding = Rounding.ROUND_DOWN): string { public toSignificant(
significantDigits: number = 6,
format?: object,
rounding: Rounding = Rounding.ROUND_DOWN
): string {
return super.toSignificant(significantDigits, format, rounding) return super.toSignificant(significantDigits, format, rounding)
} }
toFixed( public toFixed(
decimalPlaces: number = this.currency.decimals, decimalPlaces: number = this.currency.decimals,
format?: object, format?: object,
rounding: Rounding = Rounding.ROUND_DOWN rounding: Rounding = Rounding.ROUND_DOWN
...@@ -58,7 +62,7 @@ export class CurrencyAmount extends Fraction { ...@@ -58,7 +62,7 @@ export class CurrencyAmount extends Fraction {
return super.toFixed(decimalPlaces, format, rounding) return super.toFixed(decimalPlaces, format, rounding)
} }
toExact(format: object = { groupSeparator: '' }): string { public toExact(format: object = { groupSeparator: '' }): string {
Big.DP = this.currency.decimals Big.DP = this.currency.decimals
return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format) return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format)
} }
......
...@@ -27,26 +27,26 @@ export class Fraction { ...@@ -27,26 +27,26 @@ export class Fraction {
public readonly numerator: JSBI public readonly numerator: JSBI
public readonly denominator: JSBI public readonly denominator: JSBI
constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) { public constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) {
this.numerator = parseBigintIsh(numerator) this.numerator = parseBigintIsh(numerator)
this.denominator = parseBigintIsh(denominator) this.denominator = parseBigintIsh(denominator)
} }
// performs floor division // performs floor division
get quotient(): JSBI { public get quotient(): JSBI {
return JSBI.divide(this.numerator, this.denominator) return JSBI.divide(this.numerator, this.denominator)
} }
// remainder after floor division // remainder after floor division
get remainder(): Fraction { public get remainder(): Fraction {
return new Fraction(JSBI.remainder(this.numerator, this.denominator), this.denominator) return new Fraction(JSBI.remainder(this.numerator, this.denominator), this.denominator)
} }
invert(): Fraction { public invert(): Fraction {
return new Fraction(this.denominator, this.numerator) return new Fraction(this.denominator, this.numerator)
} }
add(other: Fraction | BigintIsh): Fraction { public add(other: Fraction | BigintIsh): Fraction {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
if (JSBI.equal(this.denominator, otherParsed.denominator)) { if (JSBI.equal(this.denominator, otherParsed.denominator)) {
return new Fraction(JSBI.add(this.numerator, otherParsed.numerator), this.denominator) return new Fraction(JSBI.add(this.numerator, otherParsed.numerator), this.denominator)
...@@ -60,7 +60,7 @@ export class Fraction { ...@@ -60,7 +60,7 @@ export class Fraction {
) )
} }
subtract(other: Fraction | BigintIsh): Fraction { public subtract(other: Fraction | BigintIsh): Fraction {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
if (JSBI.equal(this.denominator, otherParsed.denominator)) { if (JSBI.equal(this.denominator, otherParsed.denominator)) {
return new Fraction(JSBI.subtract(this.numerator, otherParsed.numerator), this.denominator) return new Fraction(JSBI.subtract(this.numerator, otherParsed.numerator), this.denominator)
...@@ -74,7 +74,7 @@ export class Fraction { ...@@ -74,7 +74,7 @@ export class Fraction {
) )
} }
lessThan(other: Fraction | BigintIsh): boolean { public lessThan(other: Fraction | BigintIsh): boolean {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return JSBI.lessThan( return JSBI.lessThan(
JSBI.multiply(this.numerator, otherParsed.denominator), JSBI.multiply(this.numerator, otherParsed.denominator),
...@@ -82,7 +82,7 @@ export class Fraction { ...@@ -82,7 +82,7 @@ export class Fraction {
) )
} }
equalTo(other: Fraction | BigintIsh): boolean { public equalTo(other: Fraction | BigintIsh): boolean {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return JSBI.equal( return JSBI.equal(
JSBI.multiply(this.numerator, otherParsed.denominator), JSBI.multiply(this.numerator, otherParsed.denominator),
...@@ -90,7 +90,7 @@ export class Fraction { ...@@ -90,7 +90,7 @@ export class Fraction {
) )
} }
greaterThan(other: Fraction | BigintIsh): boolean { public greaterThan(other: Fraction | BigintIsh): boolean {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return JSBI.greaterThan( return JSBI.greaterThan(
JSBI.multiply(this.numerator, otherParsed.denominator), JSBI.multiply(this.numerator, otherParsed.denominator),
...@@ -98,7 +98,7 @@ export class Fraction { ...@@ -98,7 +98,7 @@ export class Fraction {
) )
} }
multiply(other: Fraction | BigintIsh): Fraction { public multiply(other: Fraction | BigintIsh): Fraction {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return new Fraction( return new Fraction(
JSBI.multiply(this.numerator, otherParsed.numerator), JSBI.multiply(this.numerator, otherParsed.numerator),
...@@ -106,7 +106,7 @@ export class Fraction { ...@@ -106,7 +106,7 @@ export class Fraction {
) )
} }
divide(other: Fraction | BigintIsh): Fraction { public divide(other: Fraction | BigintIsh): Fraction {
const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other))
return new Fraction( return new Fraction(
JSBI.multiply(this.numerator, otherParsed.denominator), JSBI.multiply(this.numerator, otherParsed.denominator),
...@@ -114,7 +114,7 @@ export class Fraction { ...@@ -114,7 +114,7 @@ export class Fraction {
) )
} }
toSignificant( public toSignificant(
significantDigits: number, significantDigits: number,
format: object = { groupSeparator: '' }, format: object = { groupSeparator: '' },
rounding: Rounding = Rounding.ROUND_HALF_UP rounding: Rounding = Rounding.ROUND_HALF_UP
...@@ -129,7 +129,7 @@ export class Fraction { ...@@ -129,7 +129,7 @@ export class Fraction {
return quotient.toFormat(quotient.decimalPlaces(), format) return quotient.toFormat(quotient.decimalPlaces(), format)
} }
toFixed( public toFixed(
decimalPlaces: number, decimalPlaces: number,
format: object = { groupSeparator: '' }, format: object = { groupSeparator: '' },
rounding: Rounding = Rounding.ROUND_HALF_UP rounding: Rounding = Rounding.ROUND_HALF_UP
......
...@@ -4,11 +4,11 @@ import { Fraction } from './fraction' ...@@ -4,11 +4,11 @@ import { Fraction } from './fraction'
const _100_PERCENT = new Fraction(_100) const _100_PERCENT = new Fraction(_100)
export class Percent extends Fraction { export class Percent extends Fraction {
toSignificant(significantDigits: number = 5, format?: object, rounding?: Rounding): string { public toSignificant(significantDigits: number = 5, format?: object, rounding?: Rounding): string {
return this.multiply(_100_PERCENT).toSignificant(significantDigits, format, rounding) return this.multiply(_100_PERCENT).toSignificant(significantDigits, format, rounding)
} }
toFixed(decimalPlaces: number = 2, format?: object, rounding?: Rounding): string { public toFixed(decimalPlaces: number = 2, format?: object, rounding?: Rounding): string {
return this.multiply(_100_PERCENT).toFixed(decimalPlaces, format, rounding) return this.multiply(_100_PERCENT).toFixed(decimalPlaces, format, rounding)
} }
} }
...@@ -15,7 +15,7 @@ export class Price extends Fraction { ...@@ -15,7 +15,7 @@ export class Price extends Fraction {
public readonly quoteCurrency: Currency // 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 { public static fromRoute(route: Route): Price {
const prices: Price[] = [] const prices: Price[] = []
for (const [i, pair] of route.pairs.entries()) { for (const [i, pair] of route.pairs.entries()) {
prices.push( prices.push(
...@@ -28,7 +28,7 @@ export class Price extends Fraction { ...@@ -28,7 +28,7 @@ export class Price extends Fraction {
} }
// 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(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh) { public constructor(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh) {
super(numerator, denominator) super(numerator, denominator)
this.baseCurrency = baseCurrency this.baseCurrency = baseCurrency
...@@ -39,26 +39,26 @@ export class Price extends Fraction { ...@@ -39,26 +39,26 @@ export class Price extends Fraction {
) )
} }
get raw(): Fraction { public get raw(): Fraction {
return new Fraction(this.numerator, this.denominator) return new Fraction(this.numerator, this.denominator)
} }
get adjusted(): Fraction { public get adjusted(): Fraction {
return super.multiply(this.scalar) return super.multiply(this.scalar)
} }
invert(): Price { public invert(): Price {
return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator) return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator)
} }
multiply(other: Price): Price { public multiply(other: Price): Price {
invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN') invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN')
const fraction = super.multiply(other) const fraction = super.multiply(other)
return new Price(this.baseCurrency, other.quoteCurrency, 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(currencyAmount: CurrencyAmount): CurrencyAmount { public quote(currencyAmount: CurrencyAmount): CurrencyAmount {
invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN') invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN')
if (this.quoteCurrency instanceof Token) { if (this.quoteCurrency instanceof Token) {
return new TokenAmount(this.quoteCurrency, super.multiply(currencyAmount.raw).quotient) return new TokenAmount(this.quoteCurrency, super.multiply(currencyAmount.raw).quotient)
...@@ -66,11 +66,11 @@ export class Price extends Fraction { ...@@ -66,11 +66,11 @@ export class Price extends Fraction {
return CurrencyAmount.ether(super.multiply(currencyAmount.raw).quotient) return CurrencyAmount.ether(super.multiply(currencyAmount.raw).quotient)
} }
toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string { public toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string {
return this.adjusted.toSignificant(significantDigits, format, rounding) return this.adjusted.toSignificant(significantDigits, format, rounding)
} }
toFixed(decimalPlaces: number = 4, format?: object, rounding?: Rounding): string { public toFixed(decimalPlaces: number = 4, format?: object, rounding?: Rounding): string {
return this.adjusted.toFixed(decimalPlaces, format, rounding) return this.adjusted.toFixed(decimalPlaces, format, rounding)
} }
} }
...@@ -9,17 +9,17 @@ export class TokenAmount extends CurrencyAmount { ...@@ -9,17 +9,17 @@ 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) { public constructor(token: Token, amount: BigintIsh) {
super(token, amount) super(token, amount)
this.token = token this.token = token
} }
add(other: TokenAmount): TokenAmount { public 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))
} }
subtract(other: TokenAmount): TokenAmount { public subtract(other: TokenAmount): TokenAmount {
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))
} }
......
import { TokenAmount } from './fractions/tokenAmount' 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 { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { pack, keccak256 } from '@ethersproject/solidity' import { pack, keccak256 } from '@ethersproject/solidity'
import { getCreate2Address } from '@ethersproject/address' import { getCreate2Address } from '@ethersproject/address'
...@@ -19,25 +16,24 @@ import { ...@@ -19,25 +16,24 @@ import {
_1000, _1000,
ChainId ChainId
} from '../constants' } from '../constants'
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'
let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {} let PAIR_ADDRESS_CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {}
export class Pair { export class Pair {
public readonly liquidityToken: Token public readonly liquidityToken: Token
private readonly tokenAmounts: [TokenAmount, TokenAmount] private readonly tokenAmounts: [TokenAmount, TokenAmount]
static getAddress(tokenA: Token, tokenB: Token): string { public static getAddress(tokenA: Token, tokenB: Token): string {
const tokens = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks const tokens = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks
if (CACHE?.[tokens[0].address]?.[tokens[1].address] === undefined) { if (PAIR_ADDRESS_CACHE?.[tokens[0].address]?.[tokens[1].address] === undefined) {
CACHE = { PAIR_ADDRESS_CACHE = {
...CACHE, ...PAIR_ADDRESS_CACHE,
[tokens[0].address]: { [tokens[0].address]: {
...CACHE?.[tokens[0].address], ...PAIR_ADDRESS_CACHE?.[tokens[0].address],
[tokens[1].address]: getCreate2Address( [tokens[1].address]: getCreate2Address(
FACTORY_ADDRESS, FACTORY_ADDRESS,
keccak256(['bytes'], [pack(['address', 'address'], [tokens[0].address, tokens[1].address])]), keccak256(['bytes'], [pack(['address', 'address'], [tokens[0].address, tokens[1].address])]),
...@@ -47,22 +43,10 @@ export class Pair { ...@@ -47,22 +43,10 @@ export class Pair {
} }
} }
return CACHE[tokens[0].address][tokens[1].address] return PAIR_ADDRESS_CACHE[tokens[0].address][tokens[1].address]
} }
static async fetchData( public constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount) {
tokenA: Token,
tokenB: Token,
provider = getDefaultProvider(getNetwork(tokenA.chainId))
): Promise<Pair> {
invariant(tokenA.chainId === tokenB.chainId, 'CHAIN_ID')
const address = Pair.getAddress(tokenA, tokenB)
const [reserves0, reserves1] = await new Contract(address, IUniswapV2Pair.abi, provider).getReserves()
const balances = tokenA.sortsBefore(tokenB) ? [reserves0, reserves1] : [reserves1, reserves0]
return new Pair(new TokenAmount(tokenA, balances[0]), new TokenAmount(tokenB, balances[1]))
}
constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount) {
const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks
? [tokenAmountA, tokenAmountB] ? [tokenAmountA, tokenAmountB]
: [tokenAmountB, tokenAmountA] : [tokenAmountB, tokenAmountA]
...@@ -88,28 +72,28 @@ export class Pair { ...@@ -88,28 +72,28 @@ export class Pair {
return this.token0.chainId return this.token0.chainId
} }
get token0(): Token { public get token0(): Token {
return this.tokenAmounts[0].token return this.tokenAmounts[0].token
} }
get token1(): Token { public get token1(): Token {
return this.tokenAmounts[1].token return this.tokenAmounts[1].token
} }
get reserve0(): TokenAmount { public get reserve0(): TokenAmount {
return this.tokenAmounts[0] return this.tokenAmounts[0]
} }
get reserve1(): TokenAmount { public get reserve1(): TokenAmount {
return this.tokenAmounts[1] return this.tokenAmounts[1]
} }
reserveOf(token: Token): TokenAmount { public reserveOf(token: Token): TokenAmount {
invariant(this.involvesToken(token), '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] { public getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(this.involvesToken(inputAmount.token), '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()
...@@ -129,7 +113,7 @@ export class Pair { ...@@ -129,7 +113,7 @@ export class Pair {
return [outputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] return [outputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
} }
getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] { public getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] {
invariant(this.involvesToken(outputAmount.token), 'TOKEN') invariant(this.involvesToken(outputAmount.token), 'TOKEN')
if ( if (
JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve0.raw, ZERO) ||
...@@ -150,7 +134,11 @@ export class Pair { ...@@ -150,7 +134,11 @@ export class Pair {
return [inputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] return [inputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
} }
getLiquidityMinted(totalSupply: TokenAmount, tokenAmountA: TokenAmount, tokenAmountB: TokenAmount): TokenAmount { public getLiquidityMinted(
totalSupply: TokenAmount,
tokenAmountA: TokenAmount,
tokenAmountB: TokenAmount
): TokenAmount {
invariant(totalSupply.token.equals(this.liquidityToken), 'LIQUIDITY') invariant(totalSupply.token.equals(this.liquidityToken), 'LIQUIDITY')
const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks
? [tokenAmountA, tokenAmountB] ? [tokenAmountA, tokenAmountB]
...@@ -171,7 +159,7 @@ export class Pair { ...@@ -171,7 +159,7 @@ export class Pair {
return new TokenAmount(this.liquidityToken, liquidity) return new TokenAmount(this.liquidityToken, liquidity)
} }
getLiquidityValue( public getLiquidityValue(
token: Token, token: Token,
totalSupply: TokenAmount, totalSupply: TokenAmount,
liquidity: TokenAmount, liquidity: TokenAmount,
......
...@@ -13,7 +13,7 @@ export class Route { ...@@ -13,7 +13,7 @@ export class Route {
public readonly output: Currency public readonly output: Currency
public readonly midPrice: Price public readonly midPrice: Price
constructor(pairs: Pair[], input: Currency, output?: Currency) { public constructor(pairs: Pair[], input: Currency, output?: Currency) {
invariant(pairs.length > 0, 'PAIRS') invariant(pairs.length > 0, 'PAIRS')
invariant( invariant(
pairs.every(pair => pair.chainId === pairs[0].chainId), pairs.every(pair => pair.chainId === pairs[0].chainId),
...@@ -46,7 +46,7 @@ export class Route { ...@@ -46,7 +46,7 @@ export class Route {
this.output = output ?? path[path.length - 1] this.output = output ?? path[path.length - 1]
} }
get chainId(): ChainId { public get chainId(): ChainId {
return this.pairs[0].chainId return this.pairs[0].chainId
} }
} }
import { Currency } from './currency' import { Currency } from './currency'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { ChainId } from '../constants' import { ChainId } from '../constants'
import ERC20 from '../abis/ERC20.json'
import { validateAndParseAddress } from '../utils' import { validateAndParseAddress } from '../utils'
let CACHE: { [chainId: number]: { [address: string]: number } } = {
[ChainId.MAINNET]: {
'0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A': 9 // DGD
}
}
/** /**
* Represents an ERC20 token with a unique address and some metadata. * Represents an ERC20 token with a unique address and some metadata.
*/ */
...@@ -21,36 +10,13 @@ export class Token extends Currency { ...@@ -21,36 +10,13 @@ export class Token extends Currency {
public readonly chainId: ChainId public readonly chainId: ChainId
public readonly address: string public readonly address: string
static async fetchData( public constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) {
chainId: ChainId,
address: string,
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 = {
...CACHE,
[chainId]: {
...CACHE?.[chainId],
[address]: decimals
}
}
return decimals
})
return new Token(chainId, address, parsedDecimals, symbol, name)
}
constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) {
super(decimals, symbol, name) super(decimals, symbol, name)
this.chainId = chainId this.chainId = chainId
this.address = validateAndParseAddress(address) this.address = validateAndParseAddress(address)
} }
equals(other: Token): boolean { public equals(other: Token): boolean {
// short circuit on reference equality // short circuit on reference equality
if (this === other) { if (this === other) {
return true return true
...@@ -65,7 +31,7 @@ export class Token extends Currency { ...@@ -65,7 +31,7 @@ export class Token extends Currency {
return equivalent return equivalent
} }
sortsBefore(other: Token): boolean { public sortsBefore(other: Token): boolean {
invariant(this.chainId === other.chainId, 'CHAIN_IDS') invariant(this.chainId === other.chainId, 'CHAIN_IDS')
invariant(this.address !== other.address, 'ADDRESSES') invariant(this.address !== other.address, 'ADDRESSES')
return this.address.toLowerCase() < other.address.toLowerCase() return this.address.toLowerCase() < other.address.toLowerCase()
......
...@@ -177,7 +177,10 @@ export class Trade { ...@@ -177,7 +177,10 @@ export class Trade {
this.priceImpact = computePriceImpact(route.midPrice, this.inputAmount, this.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
* @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
*/
public minimumAmountOut(slippageTolerance: Percent): CurrencyAmount { 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) {
...@@ -193,7 +196,10 @@ export class Trade { ...@@ -193,7 +196,10 @@ export class Trade {
} }
} }
// 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
* @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
*/
public maximumAmountIn(slippageTolerance: Percent): CurrencyAmount { 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) {
...@@ -206,10 +212,20 @@ export class Trade { ...@@ -206,10 +212,20 @@ export class Trade {
} }
} }
// given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token /**
// amount to an output token, making at most `maxHops` hops * Given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token
// note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting * amount to an output token, making at most `maxHops` hops.
// the amount in among multiple routes. * Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting
* the amount in among multiple routes.
* @param pairs the pairs to consider in finding the best trade
* @param currencyAmountIn exact amount of input currency to spend
* @param currencyOut the desired currency out
* @param maxNumResults maximum number of results to return
* @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
* @param currentPairs used in recursion; the current list of pairs
* @param originalAmountIn used in recursion; the original value of the currencyAmountIn parameter
* @param bestTrades used in recursion; the current list of best trades
*/
public static bestTradeExactIn( public static bestTradeExactIn(
pairs: Pair[], pairs: Pair[],
currencyAmountIn: CurrencyAmount, currencyAmountIn: CurrencyAmount,
...@@ -283,11 +299,21 @@ export class Trade { ...@@ -283,11 +299,21 @@ export class Trade {
return bestTrades return bestTrades
} }
// similar to the above method but instead targets a fixed output amount /**
// given a list of pairs, and a fixed amount out, returns the top `maxNumResults` trades that go from an input token * similar to the above method but instead targets a fixed output amount
// to an output token amount, making at most `maxHops` hops * given a list of pairs, and a fixed amount out, returns the top `maxNumResults` trades that go from an input token
// note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting * to an output token amount, making at most `maxHops` hops
// the amount in among multiple routes. * note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting
* the amount in among multiple routes.
* @param pairs the pairs to consider in finding the best trade
* @param currencyIn the currency to spend
* @param currencyAmountOut the exact amount of currency out
* @param maxNumResults maximum number of results to return
* @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
* @param currentPairs used in recursion; the current list of pairs
* @param originalAmountOut used in recursion; the original value of the currencyAmountOut parameter
* @param bestTrades used in recursion; the current list of best trades
*/
public static bestTradeExactOut( public static bestTradeExactOut(
pairs: Pair[], pairs: Pair[],
currencyIn: Currency, currencyIn: Currency,
......
// see https://stackoverflow.com/a/41102306 // see https://stackoverflow.com/a/41102306
const CAN_SET_PROTOTYPE = 'setPrototypeOf' in Object const CAN_SET_PROTOTYPE = 'setPrototypeOf' in Object
/**
* Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be
* obtained by sending any amount of input.
*/
export class InsufficientReservesError extends Error { export class InsufficientReservesError extends Error {
public readonly isInsufficientReservesError: true = true public readonly isInsufficientReservesError: true = true
...@@ -11,6 +15,10 @@ export class InsufficientReservesError extends Error { ...@@ -11,6 +15,10 @@ export class InsufficientReservesError extends Error {
} }
} }
/**
* Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less
* than the price of a single unit of output after fees.
*/
export class InsufficientInputAmountError extends Error { export class InsufficientInputAmountError extends Error {
public readonly isInsufficientInputAmountError: true = true public readonly isInsufficientInputAmountError: true = true
......
import { Contract } from '@ethersproject/contracts'
import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { TokenAmount } from './entities/fractions/tokenAmount'
import { Pair } from './entities/pair'
import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import invariant from 'tiny-invariant'
import ERC20 from './abis/ERC20.json'
import { ChainId } from './constants'
import { Token } from './entities/token'
let TOKEN_DECIMALS_CACHE: { [chainId: number]: { [address: string]: number } } = {
[ChainId.MAINNET]: {
'0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A': 9 // DGD
}
}
/**
* Contains methods for constructing instances of pairs and tokens from on-chain data.
*/
export abstract class Fetcher {
/**
* Cannot be constructed.
*/
private constructor() {}
/**
* Fetch information for a given token on the given chain, using the given ethers provider.
* @param chainId chain of the token
* @param address address of the token on the chain
* @param provider provider used to fetch the token
* @param symbol optional symbol of the token
* @param name optional name of the token
*/
public static async fetchTokenData(
chainId: ChainId,
address: string,
provider = getDefaultProvider(getNetwork(chainId)),
symbol?: string,
name?: string
): Promise<Token> {
const parsedDecimals =
typeof TOKEN_DECIMALS_CACHE?.[chainId]?.[address] === 'number'
? TOKEN_DECIMALS_CACHE[chainId][address]
: await new Contract(address, ERC20, provider).decimals().then((decimals: number): number => {
TOKEN_DECIMALS_CACHE = {
...TOKEN_DECIMALS_CACHE,
[chainId]: {
...TOKEN_DECIMALS_CACHE?.[chainId],
[address]: decimals
}
}
return decimals
})
return new Token(chainId, address, parsedDecimals, symbol, name)
}
/**
* Fetches information about a pair and constructs a pair from the given two tokens.
* @param tokenA first token
* @param tokenB second token
* @param provider the provider to use to fetch the data
*/
public static async fetchPairData(
tokenA: Token,
tokenB: Token,
provider = getDefaultProvider(getNetwork(tokenA.chainId))
): Promise<Pair> {
invariant(tokenA.chainId === tokenB.chainId, 'CHAIN_ID')
const address = Pair.getAddress(tokenA, tokenB)
const [reserves0, reserves1] = await new Contract(address, IUniswapV2Pair.abi, provider).getReserves()
const balances = tokenA.sortsBefore(tokenB) ? [reserves0, reserves1] : [reserves1, reserves0]
return new Pair(new TokenAmount(tokenA, balances[0]), new TokenAmount(tokenB, balances[1]))
}
}
...@@ -14,3 +14,4 @@ export { ...@@ -14,3 +14,4 @@ export {
export * from './errors' export * from './errors'
export * from './entities' export * from './entities'
export * from './router' export * from './router'
export * from './fetcher'
...@@ -3,26 +3,46 @@ import invariant from 'tiny-invariant' ...@@ -3,26 +3,46 @@ import invariant from 'tiny-invariant'
import { validateAndParseAddress } from './utils' import { validateAndParseAddress } from './utils'
import { CurrencyAmount, ETHER, Percent, Trade } from './entities' import { CurrencyAmount, ETHER, Percent, Trade } from './entities'
/**
* Options for producing the arguments to send call to the router.
*/
export interface TradeOptions { export interface TradeOptions {
// how much the execution price is allowed to move unfavorably from the trade execution price /**
* How much the execution price is allowed to move unfavorably from the trade execution price.
*/
allowedSlippage: Percent 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 * How long the swap is valid until it expires, in seconds.
// are generated. * This will be used to produce a `deadline` parameter which is computed from when the swap call parameters
* are generated.
*/
ttl: number ttl: number
// the account that should receive the output of the swap /**
* The account that should receive the output of the swap.
*/
recipient: string recipient: string
// whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods /**
* Whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods
*/
feeOnTransfer?: boolean feeOnTransfer?: boolean
} }
/**
* The parameters to use in the call to the Uniswap V2 Router to execute a trade.
*/
export interface SwapParameters { export interface SwapParameters {
// the method to call on the Uniswap V2 Router /**
* The method to call on the Uniswap V2 Router.
*/
methodName: string methodName: string
// the arguments to pass to the method, all hex encoded /**
* The arguments to pass to the method, all hex encoded.
*/
args: (string | string[])[] args: (string | string[])[]
// the amount of wei to send in hex /**
* The amount of wei to send in hex.
*/
value: string value: string
} }
......
import { ChainId, WETH, Token, Pair } from '../src' import { ChainId, WETH, Token, Fetcher } from '../src'
// TODO: replace the provider in these tests // TODO: replace the provider in these tests
describe.skip('data', () => { describe.skip('data', () => {
it('Token', async () => { it('Token', async () => {
const token = await Token.fetchData(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F') // DAI const token = await Fetcher.fetchTokenData(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F') // DAI
expect(token.decimals).toEqual(18) expect(token.decimals).toEqual(18)
}) })
it('Token:CACHE', async () => { it('Token:CACHE', async () => {
const token = await Token.fetchData(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A') // DGD const token = await Fetcher.fetchTokenData(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A') // DGD
expect(token.decimals).toEqual(9) expect(token.decimals).toEqual(9)
}) })
it('Pair', async () => { it('Pair', async () => {
const token = new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18) // DAI const token = new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18) // DAI
const pair = await Pair.fetchData(WETH[ChainId.RINKEBY], token) const pair = await Fetcher.fetchPairData(WETH[ChainId.RINKEBY], token)
expect(pair.liquidityToken.address).toEqual('0x8507903035cFc0Bc7c8c62334dea16E2DA1EcecD') expect(pair.liquidityToken.address).toEqual('0x8B22F85d0c844Cf793690F6D9DFE9F11Ddb35449')
}) })
}) })
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