├── .babelrc ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .yarnrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── abis │ └── ERC20.json ├── constants.ts ├── declarations.d.ts ├── entities │ ├── currency.ts │ ├── fractions │ │ ├── currencyAmount.ts │ │ ├── fraction.ts │ │ ├── index.ts │ │ ├── percent.ts │ │ ├── price.ts │ │ └── tokenAmount.ts │ ├── index.ts │ ├── pair.ts │ ├── route.ts │ ├── token.ts │ └── trade.ts ├── errors.ts ├── fetcher.ts ├── index.ts ├── router.ts └── utils.ts ├── test ├── constants.test.ts ├── data.test.ts ├── entities.test.ts ├── fraction.test.ts ├── miscellaneous.test.ts ├── pair.test.ts ├── route.test.ts ├── router.test.ts ├── token.test.ts └── trade.test.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [] // "transform-jsbi-to-bigint" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | CI: true 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - v2 9 | push: 10 | branches: 11 | - v2 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | node: ['10.x', '12.x'] 18 | os: [ubuntu-latest, macOS-latest] 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node }} 27 | 28 | - run: npm install -g yarn 29 | 30 | - id: yarn-cache 31 | run: echo "::set-output name=dir::$(yarn cache dir)" 32 | - uses: actions/cache@v1 33 | with: 34 | path: ${{ steps.yarn-cache.outputs.dir }} 35 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ matrix.os }}-yarn- 38 | 39 | - run: yarn 40 | 41 | - run: yarn lint 42 | - run: yarn build 43 | - run: yarn test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Noah Zinsmeister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickSwap SDK 2 | 3 | This repository has been forked from [UniswapV2](https://github.com/Uniswap/uniswap-sdk) 4 | 5 | ## Running tests 6 | 7 | To run the tests, follow these steps. You must have at least node v10 and [yarn](https://yarnpkg.com/) installed. 8 | 9 | First clone the repository: 10 | 11 | ```sh 12 | git clone https://github.com/QuickSwap/QuickSwap-sdk.git 13 | ``` 14 | 15 | Move into the quickswap-sdk working directory 16 | 17 | ```sh 18 | cd QuickSwap-sdk/ 19 | ``` 20 | 21 | Install dependencies 22 | 23 | ```sh 24 | yarn install 25 | ``` 26 | 27 | Run tests 28 | 29 | ```sh 30 | yarn test 31 | ``` 32 | 33 | You should see output like the following: 34 | 35 | ```sh 36 | yarn run v1.22.4 37 | $ tsdx test 38 | PASS test/constants.test.ts 39 | PASS test/pair.test.ts 40 | PASS test/fraction.test.ts 41 | PASS test/miscellaneous.test.ts 42 | PASS test/entities.test.ts 43 | PASS test/trade.test.ts 44 | 45 | Test Suites: 1 skipped, 6 passed, 6 of 7 total 46 | Tests: 3 skipped, 82 passed, 85 total 47 | Snapshots: 0 total 48 | Time: 5.091s 49 | Ran all test suites. 50 | ✨ Done in 6.61s. 51 | ``` 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickswap-sdk", 3 | "license": "MIT", 4 | "version": "3.0.38", 5 | "description": "🛠 An SDK for building applications on top of Quickswap.", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": "https://github.com/sameepsi/quickswap-sdk.git", 12 | "keywords": [ 13 | "quickswap", 14 | "matic", 15 | "ethereum" 16 | ], 17 | "module": "dist/sdk.esm.js", 18 | "scripts": { 19 | "lint": "tsdx lint src test", 20 | "build": "tsdx build", 21 | "start": "tsdx watch", 22 | "test": "tsdx test", 23 | "prepublishOnly": "tsdx build" 24 | }, 25 | "dependencies": { 26 | "@uniswap/v2-core": "^1.0.0", 27 | "big.js": "^5.2.2", 28 | "decimal.js-light": "^2.5.0", 29 | "jsbi": "^3.1.1", 30 | "tiny-invariant": "^1.1.0", 31 | "tiny-warning": "^1.0.3", 32 | "toformat": "^2.0.0" 33 | }, 34 | "peerDependencies": { 35 | "@ethersproject/address": "^5.0.0-beta", 36 | "@ethersproject/contracts": "^5.0.0-beta", 37 | "@ethersproject/networks": "^5.0.0-beta", 38 | "@ethersproject/providers": "^5.0.0-beta", 39 | "@ethersproject/solidity": "^5.0.0-beta" 40 | }, 41 | "devDependencies": { 42 | "@ethersproject/address": "^5.0.2", 43 | "@ethersproject/contracts": "^5.0.2", 44 | "@ethersproject/networks": "^5.0.2", 45 | "@ethersproject/providers": "^5.0.5", 46 | "@ethersproject/solidity": "^5.0.2", 47 | "@types/big.js": "^4.0.5", 48 | "@types/jest": "^24.0.25", 49 | "babel-plugin-transform-jsbi-to-bigint": "^1.3.1", 50 | "tsdx": "^0.12.3" 51 | }, 52 | "engines": { 53 | "node": ">=10" 54 | }, 55 | "prettier": { 56 | "printWidth": 120, 57 | "semi": false, 58 | "singleQuote": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "decimals", 6 | "outputs": [{ "name": "", "type": "uint8" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": true, 13 | "inputs": [{ "name": "", "type": "address" }], 14 | "name": "balanceOf", 15 | "outputs": [{ "name": "", "type": "uint256" }], 16 | "payable": false, 17 | "stateMutability": "view", 18 | "type": "function" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | 3 | // exports for external consumption 4 | export type BigintIsh = JSBI | bigint | string 5 | 6 | export enum ChainId { 7 | 8 | MUMBAI = 80001, 9 | MATIC = 137, 10 | DOEGCHAIN_TESTNET = 568, 11 | DOGECHAIN = 2000, 12 | ZKTESTNET = 1442, 13 | ZKEVM = 1101, 14 | KAVA = 2222, 15 | MANTA = 169, 16 | ZKATANA = 1261120, 17 | BTTC = 199, 18 | X1 = 195, 19 | TIMX = 13473, 20 | IMX = 13371, 21 | ASTARZKEVM = 3776, 22 | LAYERX = 196, 23 | ETHEREUM = 1, 24 | MINATO = 1946, 25 | SONEIUM = 1868, 26 | SOMNIA = 50312, 27 | BASE = 8453, 28 | } 29 | 30 | export enum TradeType { 31 | EXACT_INPUT, 32 | EXACT_OUTPUT 33 | } 34 | 35 | export enum Rounding { 36 | ROUND_DOWN, 37 | ROUND_HALF_UP, 38 | ROUND_UP 39 | } 40 | 41 | export const FACTORY_ADDRESS = { 42 | 43 | [ChainId.MATIC]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32", 44 | [ChainId.DOGECHAIN]: "0xC3550497E591Ac6ed7a7E03ffC711CfB7412E57F", 45 | [ChainId.MUMBAI]: "0xC3550497E591Ac6ed7a7E03ffC711CfB7412E57F",//CHANGE THIS 46 | [ChainId.ZKEVM]: "0xC3550497E591Ac6ed7a7E03ffC711CfB7412E57F",//CHANGE THIS 47 | [ChainId.ZKTESTNET]: "0xC3550497E591Ac6ed7a7E03ffC711CfB7412E57F",//CHANGE THIS 48 | [ChainId.DOEGCHAIN_TESTNET]: "0xC3550497E591Ac6ed7a7E03ffC711CfB7412E57F",//CHANGE THIS 49 | [ChainId.KAVA]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 50 | [ChainId.MANTA]: "0x8515eC615BcD9dE302fE25419494DeE639f614be", 51 | [ChainId.ZKATANA]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 52 | [ChainId.BTTC]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 53 | [ChainId.X1]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 54 | [ChainId.TIMX]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 55 | [ChainId.IMX]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 56 | [ChainId.ASTARZKEVM]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 57 | [ChainId.LAYERX]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 58 | [ChainId.ETHEREUM]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 59 | [ChainId.MINATO]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 60 | [ChainId.SONEIUM]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",//DUMMY 61 | [ChainId.SOMNIA]: "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32"//DUMMY 62 | 63 | } 64 | 65 | export const INIT_CODE_HASH = '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' 66 | 67 | export const MINIMUM_LIQUIDITY = JSBI.BigInt(1000) 68 | 69 | // exports for internal consumption 70 | export const ZERO = JSBI.BigInt(0) 71 | export const ONE = JSBI.BigInt(1) 72 | export const TWO = JSBI.BigInt(2) 73 | export const THREE = JSBI.BigInt(3) 74 | export const FIVE = JSBI.BigInt(5) 75 | export const TEN = JSBI.BigInt(10) 76 | export const _100 = JSBI.BigInt(100) 77 | export const _997 = JSBI.BigInt(997) 78 | export const _1000 = JSBI.BigInt(1000) 79 | 80 | export enum SolidityType { 81 | uint8 = 'uint8', 82 | uint256 = 'uint256' 83 | } 84 | 85 | export const SOLIDITY_TYPE_MAXIMA = { 86 | [SolidityType.uint8]: JSBI.BigInt('0xff'), 87 | [SolidityType.uint256]: JSBI.BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 88 | } 89 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'toformat' 2 | -------------------------------------------------------------------------------- /src/entities/currency.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { ChainId } from '../constants' 3 | import { SolidityType } from '../constants' 4 | import { validateSolidityTypeInstance } from '../utils' 5 | 6 | /** 7 | * A currency is any fungible financial instrument on Ethereum, including Ether and all ERC20 tokens. 8 | * 9 | * The only instance of the base class `Currency` is Ether. 10 | */ 11 | export class Currency { 12 | public readonly decimals: number 13 | public readonly symbol?: string 14 | public readonly name?: string 15 | 16 | /** 17 | * The only instance of the base class `Currency`. 18 | */ 19 | 20 | public static readonly ETHER = { 21 | [ChainId.MUMBAI]: new Currency(18, 'MATIC', 'Matic'), 22 | [ChainId.MATIC]: new Currency(18, 'POL', 'Polygon Ecosystem Token'), 23 | [ChainId.DOEGCHAIN_TESTNET]: new Currency(18, 'WDOGE', 'Wrapped Doge'), 24 | [ChainId.DOGECHAIN]: new Currency(18, 'WDOGE', 'Wrapped Doge'), 25 | [ChainId.ZKTESTNET]: new Currency(18, 'ETH', 'Ether'), 26 | [ChainId.ZKEVM]: new Currency(18, 'ETH', 'Ether'), 27 | [ChainId.KAVA]: new Currency(18, 'KAVA', 'KAVA'), 28 | [ChainId.MANTA]: new Currency(18, 'ETH', 'Ether'), 29 | [ChainId.ZKATANA]: new Currency(18, 'ETH', 'Ether'), 30 | [ChainId.BTTC]: new Currency(18, 'BTT', 'Bit Torrent'), 31 | [ChainId.X1]: new Currency(18, 'OKB', 'OKB'), 32 | [ChainId.TIMX]: new Currency(18, 'IMX', 'IMX'), 33 | [ChainId.IMX]: new Currency(18, 'IMX', 'IMX'), 34 | [ChainId.ASTARZKEVM]: new Currency(18, 'ETH', 'Ether'), 35 | [ChainId.LAYERX]: new Currency(18, 'OKB', 'OKB'), 36 | [ChainId.ETHEREUM]: new Currency(18, 'ETH', 'Ether'), 37 | [ChainId.MINATO]: new Currency(18, 'ETH', 'Ether'), 38 | [ChainId.SONEIUM]: new Currency(18, 'ETH', 'Ether'), 39 | [ChainId.SOMNIA]: new Currency(18, 'STT', 'Somnia Testnet Tokens'), 40 | [ChainId.BASE]: new Currency(18, 'ETH', 'Ether'), 41 | } 42 | 43 | /** 44 | * Constructs an instance of the base class `Currency`. The only instance of the base class `Currency` is `Currency.ETHER`. 45 | * @param decimals decimals of the currency 46 | * @param symbol symbol of the currency 47 | * @param name of the currency 48 | */ 49 | protected constructor(decimals: number, symbol?: string, name?: string) { 50 | validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8) 51 | 52 | this.decimals = decimals 53 | this.symbol = symbol 54 | this.name = name 55 | } 56 | } 57 | 58 | const ETHER = Currency.ETHER 59 | export { ETHER } 60 | -------------------------------------------------------------------------------- /src/entities/fractions/currencyAmount.ts: -------------------------------------------------------------------------------- 1 | import { currencyEquals } from '../token' 2 | import { Currency, ETHER } from '../currency' 3 | import invariant from 'tiny-invariant' 4 | import JSBI from 'jsbi' 5 | import _Big from 'big.js' 6 | import toFormat from 'toformat' 7 | 8 | import { BigintIsh, Rounding, TEN, SolidityType, ChainId } from '../../constants' 9 | import { parseBigintIsh, validateSolidityTypeInstance } from '../../utils' 10 | import { Fraction } from './fraction' 11 | 12 | const Big = toFormat(_Big) 13 | 14 | export class CurrencyAmount extends Fraction { 15 | public readonly currency: Currency 16 | 17 | /** 18 | * Helper that calls the constructor with the ETHER currency 19 | * @param amount ether amount in wei 20 | */ 21 | public static ether(amount: BigintIsh, chainId: ChainId): CurrencyAmount { 22 | return new CurrencyAmount(ETHER[chainId], amount) 23 | } 24 | 25 | // amount _must_ be raw, i.e. in the native representation 26 | protected constructor(currency: Currency, amount: BigintIsh) { 27 | const parsedAmount = parseBigintIsh(amount) 28 | validateSolidityTypeInstance(parsedAmount, SolidityType.uint256) 29 | 30 | super(parsedAmount, JSBI.exponentiate(TEN, JSBI.BigInt(currency.decimals))) 31 | this.currency = currency 32 | } 33 | 34 | public get raw(): JSBI { 35 | return this.numerator 36 | } 37 | 38 | public add(other: CurrencyAmount): CurrencyAmount { 39 | invariant(currencyEquals(this.currency, other.currency), 'TOKEN') 40 | return new CurrencyAmount(this.currency, JSBI.add(this.raw, other.raw)) 41 | } 42 | 43 | public subtract(other: CurrencyAmount): CurrencyAmount { 44 | invariant(currencyEquals(this.currency, other.currency), 'TOKEN') 45 | return new CurrencyAmount(this.currency, JSBI.subtract(this.raw, other.raw)) 46 | } 47 | 48 | public toSignificant( 49 | significantDigits: number = 6, 50 | format?: object, 51 | rounding: Rounding = Rounding.ROUND_DOWN 52 | ): string { 53 | return super.toSignificant(significantDigits, format, rounding) 54 | } 55 | 56 | public toFixed( 57 | decimalPlaces: number = this.currency.decimals, 58 | format?: object, 59 | rounding: Rounding = Rounding.ROUND_DOWN 60 | ): string { 61 | invariant(decimalPlaces <= this.currency.decimals, 'DECIMALS') 62 | return super.toFixed(decimalPlaces, format, rounding) 63 | } 64 | 65 | public toExact(format: object = { groupSeparator: '' }): string { 66 | Big.DP = this.currency.decimals 67 | return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(format) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/entities/fractions/fraction.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import JSBI from 'jsbi' 3 | import _Decimal from 'decimal.js-light' 4 | import _Big, { RoundingMode } from 'big.js' 5 | import toFormat from 'toformat' 6 | 7 | import { BigintIsh, Rounding } from '../../constants' 8 | import { ONE } from '../../constants' 9 | import { parseBigintIsh } from '../../utils' 10 | 11 | const Decimal = toFormat(_Decimal) 12 | const Big = toFormat(_Big) 13 | 14 | const toSignificantRounding = { 15 | [Rounding.ROUND_DOWN]: Decimal.ROUND_DOWN, 16 | [Rounding.ROUND_HALF_UP]: Decimal.ROUND_HALF_UP, 17 | [Rounding.ROUND_UP]: Decimal.ROUND_UP 18 | } 19 | 20 | const toFixedRounding = { 21 | [Rounding.ROUND_DOWN]: RoundingMode.RoundDown, 22 | [Rounding.ROUND_HALF_UP]: RoundingMode.RoundHalfUp, 23 | [Rounding.ROUND_UP]: RoundingMode.RoundUp 24 | } 25 | 26 | export class Fraction { 27 | public readonly numerator: JSBI 28 | public readonly denominator: JSBI 29 | 30 | public constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) { 31 | this.numerator = parseBigintIsh(numerator) 32 | this.denominator = parseBigintIsh(denominator) 33 | } 34 | 35 | // performs floor division 36 | public get quotient(): JSBI { 37 | return JSBI.divide(this.numerator, this.denominator) 38 | } 39 | 40 | // remainder after floor division 41 | public get remainder(): Fraction { 42 | return new Fraction(JSBI.remainder(this.numerator, this.denominator), this.denominator) 43 | } 44 | 45 | public invert(): Fraction { 46 | return new Fraction(this.denominator, this.numerator) 47 | } 48 | 49 | public add(other: Fraction | BigintIsh): Fraction { 50 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 51 | if (JSBI.equal(this.denominator, otherParsed.denominator)) { 52 | return new Fraction(JSBI.add(this.numerator, otherParsed.numerator), this.denominator) 53 | } 54 | return new Fraction( 55 | JSBI.add( 56 | JSBI.multiply(this.numerator, otherParsed.denominator), 57 | JSBI.multiply(otherParsed.numerator, this.denominator) 58 | ), 59 | JSBI.multiply(this.denominator, otherParsed.denominator) 60 | ) 61 | } 62 | 63 | public subtract(other: Fraction | BigintIsh): Fraction { 64 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 65 | if (JSBI.equal(this.denominator, otherParsed.denominator)) { 66 | return new Fraction(JSBI.subtract(this.numerator, otherParsed.numerator), this.denominator) 67 | } 68 | return new Fraction( 69 | JSBI.subtract( 70 | JSBI.multiply(this.numerator, otherParsed.denominator), 71 | JSBI.multiply(otherParsed.numerator, this.denominator) 72 | ), 73 | JSBI.multiply(this.denominator, otherParsed.denominator) 74 | ) 75 | } 76 | 77 | public lessThan(other: Fraction | BigintIsh): boolean { 78 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 79 | return JSBI.lessThan( 80 | JSBI.multiply(this.numerator, otherParsed.denominator), 81 | JSBI.multiply(otherParsed.numerator, this.denominator) 82 | ) 83 | } 84 | 85 | public equalTo(other: Fraction | BigintIsh): boolean { 86 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 87 | return JSBI.equal( 88 | JSBI.multiply(this.numerator, otherParsed.denominator), 89 | JSBI.multiply(otherParsed.numerator, this.denominator) 90 | ) 91 | } 92 | 93 | public greaterThan(other: Fraction | BigintIsh): boolean { 94 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 95 | return JSBI.greaterThan( 96 | JSBI.multiply(this.numerator, otherParsed.denominator), 97 | JSBI.multiply(otherParsed.numerator, this.denominator) 98 | ) 99 | } 100 | 101 | public multiply(other: Fraction | BigintIsh): Fraction { 102 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 103 | return new Fraction( 104 | JSBI.multiply(this.numerator, otherParsed.numerator), 105 | JSBI.multiply(this.denominator, otherParsed.denominator) 106 | ) 107 | } 108 | 109 | public divide(other: Fraction | BigintIsh): Fraction { 110 | const otherParsed = other instanceof Fraction ? other : new Fraction(parseBigintIsh(other)) 111 | return new Fraction( 112 | JSBI.multiply(this.numerator, otherParsed.denominator), 113 | JSBI.multiply(this.denominator, otherParsed.numerator) 114 | ) 115 | } 116 | 117 | public toSignificant( 118 | significantDigits: number, 119 | format: object = { groupSeparator: '' }, 120 | rounding: Rounding = Rounding.ROUND_HALF_UP 121 | ): string { 122 | invariant(Number.isInteger(significantDigits), `${significantDigits} is not an integer.`) 123 | invariant(significantDigits > 0, `${significantDigits} is not positive.`) 124 | 125 | Decimal.set({ precision: significantDigits + 1, rounding: toSignificantRounding[rounding] }) 126 | const quotient = new Decimal(this.numerator.toString()) 127 | .div(this.denominator.toString()) 128 | .toSignificantDigits(significantDigits) 129 | return quotient.toFormat(quotient.decimalPlaces(), format) 130 | } 131 | 132 | public toFixed( 133 | decimalPlaces: number, 134 | format: object = { groupSeparator: '' }, 135 | rounding: Rounding = Rounding.ROUND_HALF_UP 136 | ): string { 137 | invariant(Number.isInteger(decimalPlaces), `${decimalPlaces} is not an integer.`) 138 | invariant(decimalPlaces >= 0, `${decimalPlaces} is negative.`) 139 | 140 | Big.DP = decimalPlaces 141 | Big.RM = toFixedRounding[rounding] 142 | return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(decimalPlaces, format) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/entities/fractions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fraction' 2 | export * from './percent' 3 | export * from './tokenAmount' 4 | export * from './currencyAmount' 5 | export * from './price' 6 | -------------------------------------------------------------------------------- /src/entities/fractions/percent.ts: -------------------------------------------------------------------------------- 1 | import { Rounding, _100 } from '../../constants' 2 | import { Fraction } from './fraction' 3 | 4 | const _100_PERCENT = new Fraction(_100) 5 | 6 | export class Percent extends Fraction { 7 | public toSignificant(significantDigits: number = 5, format?: object, rounding?: Rounding): string { 8 | return this.multiply(_100_PERCENT).toSignificant(significantDigits, format, rounding) 9 | } 10 | 11 | public toFixed(decimalPlaces: number = 2, format?: object, rounding?: Rounding): string { 12 | return this.multiply(_100_PERCENT).toFixed(decimalPlaces, format, rounding) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/entities/fractions/price.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../token' 2 | import { TokenAmount } from './tokenAmount' 3 | import { currencyEquals } from '../token' 4 | import invariant from 'tiny-invariant' 5 | import JSBI from 'jsbi' 6 | 7 | import { BigintIsh, ChainId, Rounding, TEN } from '../../constants' 8 | import { Currency } from '../currency' 9 | import { Route } from '../route' 10 | import { Fraction } from './fraction' 11 | import { CurrencyAmount } from './currencyAmount' 12 | 13 | export class Price extends Fraction { 14 | public readonly baseCurrency: Currency // input i.e. denominator 15 | public readonly quoteCurrency: Currency // output i.e. numerator 16 | public readonly scalar: Fraction // used to adjust the raw fraction w/r/t the decimals of the {base,quote}Token 17 | 18 | public static fromRoute(route: Route): Price { 19 | const prices: Price[] = [] 20 | for (const [i, pair] of route.pairs.entries()) { 21 | prices.push( 22 | route.path[i].equals(pair.token0) 23 | ? new Price(pair.reserve0.currency, pair.reserve1.currency, pair.reserve0.raw, pair.reserve1.raw) 24 | : new Price(pair.reserve1.currency, pair.reserve0.currency, pair.reserve1.raw, pair.reserve0.raw) 25 | ) 26 | } 27 | return prices.slice(1).reduce((accumulator, currentValue) => accumulator.multiply(currentValue), prices[0]) 28 | } 29 | 30 | // denominator and numerator _must_ be raw, i.e. in the native representation 31 | public constructor(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh) { 32 | super(numerator, denominator) 33 | 34 | this.baseCurrency = baseCurrency 35 | this.quoteCurrency = quoteCurrency 36 | this.scalar = new Fraction( 37 | JSBI.exponentiate(TEN, JSBI.BigInt(baseCurrency.decimals)), 38 | JSBI.exponentiate(TEN, JSBI.BigInt(quoteCurrency.decimals)) 39 | ) 40 | } 41 | 42 | public get raw(): Fraction { 43 | return new Fraction(this.numerator, this.denominator) 44 | } 45 | 46 | public get adjusted(): Fraction { 47 | return super.multiply(this.scalar) 48 | } 49 | 50 | public invert(): Price { 51 | return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator) 52 | } 53 | 54 | public multiply(other: Price): Price { 55 | invariant(currencyEquals(this.quoteCurrency, other.baseCurrency), 'TOKEN') 56 | const fraction = super.multiply(other) 57 | return new Price(this.baseCurrency, other.quoteCurrency, fraction.denominator, fraction.numerator) 58 | } 59 | 60 | // performs floor division on overflow 61 | public quote(currencyAmount: CurrencyAmount): CurrencyAmount { 62 | invariant(currencyEquals(currencyAmount.currency, this.baseCurrency), 'TOKEN') 63 | if (this.quoteCurrency instanceof Token) { 64 | return new TokenAmount(this.quoteCurrency, super.multiply(currencyAmount.raw).quotient) 65 | } 66 | return CurrencyAmount.ether(super.multiply(currencyAmount.raw).quotient, ChainId.MATIC/**Need to change this later 67 | */) 68 | } 69 | 70 | public toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string { 71 | return this.adjusted.toSignificant(significantDigits, format, rounding) 72 | } 73 | 74 | public toFixed(decimalPlaces: number = 4, format?: object, rounding?: Rounding): string { 75 | return this.adjusted.toFixed(decimalPlaces, format, rounding) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/entities/fractions/tokenAmount.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount } from './currencyAmount' 2 | import { Token } from '../token' 3 | import invariant from 'tiny-invariant' 4 | import JSBI from 'jsbi' 5 | 6 | import { BigintIsh } from '../../constants' 7 | 8 | export class TokenAmount extends CurrencyAmount { 9 | public readonly token: Token 10 | 11 | // amount _must_ be raw, i.e. in the native representation 12 | public constructor(token: Token, amount: BigintIsh) { 13 | super(token, amount) 14 | this.token = token 15 | } 16 | 17 | public add(other: TokenAmount): TokenAmount { 18 | invariant(this.token.equals(other.token), 'TOKEN') 19 | return new TokenAmount(this.token, JSBI.add(this.raw, other.raw)) 20 | } 21 | 22 | public subtract(other: TokenAmount): TokenAmount { 23 | invariant(this.token.equals(other.token), 'TOKEN') 24 | return new TokenAmount(this.token, JSBI.subtract(this.raw, other.raw)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './token' 2 | export * from './pair' 3 | export * from './route' 4 | export * from './trade' 5 | export * from './currency' 6 | 7 | export * from './fractions' 8 | -------------------------------------------------------------------------------- /src/entities/pair.ts: -------------------------------------------------------------------------------- 1 | import { Price } from './fractions/price' 2 | import { TokenAmount } from './fractions/tokenAmount' 3 | import invariant from 'tiny-invariant' 4 | import JSBI from 'jsbi' 5 | import { pack, keccak256 } from '@ethersproject/solidity' 6 | import { getCreate2Address } from '@ethersproject/address' 7 | 8 | import { 9 | BigintIsh, 10 | FACTORY_ADDRESS, 11 | INIT_CODE_HASH, 12 | MINIMUM_LIQUIDITY, 13 | ZERO, 14 | ONE, 15 | FIVE, 16 | _997, 17 | _1000, 18 | ChainId 19 | } from '../constants' 20 | import { sqrt, parseBigintIsh } from '../utils' 21 | import { InsufficientReservesError, InsufficientInputAmountError } from '../errors' 22 | import { Token } from './token' 23 | 24 | let PAIR_ADDRESS_CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {} 25 | 26 | export class Pair { 27 | public readonly liquidityToken: Token 28 | private readonly tokenAmounts: [TokenAmount, TokenAmount] 29 | 30 | public static getAddress(tokenA: Token, tokenB: Token): string { 31 | const tokens = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks 32 | 33 | if (PAIR_ADDRESS_CACHE?.[tokens[0].address]?.[tokens[1].address] === undefined) { 34 | PAIR_ADDRESS_CACHE = { 35 | ...PAIR_ADDRESS_CACHE, 36 | [tokens[0].address]: { 37 | ...PAIR_ADDRESS_CACHE?.[tokens[0].address], 38 | [tokens[1].address]: getCreate2Address( 39 | FACTORY_ADDRESS[tokenA.chainId], 40 | keccak256(['bytes'], [pack(['address', 'address'], [tokens[0].address, tokens[1].address])]), 41 | INIT_CODE_HASH 42 | ) 43 | } 44 | } 45 | } 46 | 47 | return PAIR_ADDRESS_CACHE[tokens[0].address][tokens[1].address] 48 | } 49 | 50 | public constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount) { 51 | const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks 52 | ? [tokenAmountA, tokenAmountB] 53 | : [tokenAmountB, tokenAmountA] 54 | this.liquidityToken = new Token( 55 | tokenAmounts[0].token.chainId, 56 | Pair.getAddress(tokenAmounts[0].token, tokenAmounts[1].token), 57 | 18, 58 | 'UNI-V2', 59 | 'Uniswap V2' 60 | ) 61 | this.tokenAmounts = tokenAmounts as [TokenAmount, TokenAmount] 62 | } 63 | 64 | /** 65 | * Returns true if the token is either token0 or token1 66 | * @param token to check 67 | */ 68 | public involvesToken(token: Token): boolean { 69 | return token.equals(this.token0) || token.equals(this.token1) 70 | } 71 | 72 | /** 73 | * Returns the current mid price of the pair in terms of token0, i.e. the ratio of reserve1 to reserve0 74 | */ 75 | public get token0Price(): Price { 76 | return new Price(this.token0, this.token1, this.tokenAmounts[0].raw, this.tokenAmounts[1].raw) 77 | } 78 | 79 | /** 80 | * Returns the current mid price of the pair in terms of token1, i.e. the ratio of reserve0 to reserve1 81 | */ 82 | public get token1Price(): Price { 83 | return new Price(this.token1, this.token0, this.tokenAmounts[1].raw, this.tokenAmounts[0].raw) 84 | } 85 | 86 | /** 87 | * Return the price of the given token in terms of the other token in the pair. 88 | * @param token token to return price of 89 | */ 90 | public priceOf(token: Token): Price { 91 | invariant(this.involvesToken(token), 'TOKEN') 92 | return token.equals(this.token0) ? this.token0Price : this.token1Price 93 | } 94 | 95 | /** 96 | * Returns the chain ID of the tokens in the pair. 97 | */ 98 | public get chainId(): ChainId { 99 | return this.token0.chainId 100 | } 101 | 102 | public get token0(): Token { 103 | return this.tokenAmounts[0].token 104 | } 105 | 106 | public get token1(): Token { 107 | return this.tokenAmounts[1].token 108 | } 109 | 110 | public get reserve0(): TokenAmount { 111 | return this.tokenAmounts[0] 112 | } 113 | 114 | public get reserve1(): TokenAmount { 115 | return this.tokenAmounts[1] 116 | } 117 | 118 | public reserveOf(token: Token): TokenAmount { 119 | invariant(this.involvesToken(token), 'TOKEN') 120 | return token.equals(this.token0) ? this.reserve0 : this.reserve1 121 | } 122 | 123 | public getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] { 124 | invariant(this.involvesToken(inputAmount.token), 'TOKEN') 125 | if (JSBI.equal(this.reserve0.raw, ZERO) || JSBI.equal(this.reserve1.raw, ZERO)) { 126 | throw new InsufficientReservesError() 127 | } 128 | const inputReserve = this.reserveOf(inputAmount.token) 129 | const outputReserve = this.reserveOf(inputAmount.token.equals(this.token0) ? this.token1 : this.token0) 130 | const inputAmountWithFee = JSBI.multiply(inputAmount.raw, _997) 131 | const numerator = JSBI.multiply(inputAmountWithFee, outputReserve.raw) 132 | const denominator = JSBI.add(JSBI.multiply(inputReserve.raw, _1000), inputAmountWithFee) 133 | const outputAmount = new TokenAmount( 134 | inputAmount.token.equals(this.token0) ? this.token1 : this.token0, 135 | JSBI.divide(numerator, denominator) 136 | ) 137 | if (JSBI.equal(outputAmount.raw, ZERO)) { 138 | throw new InsufficientInputAmountError() 139 | } 140 | return [outputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] 141 | } 142 | 143 | public getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] { 144 | invariant(this.involvesToken(outputAmount.token), 'TOKEN') 145 | if ( 146 | JSBI.equal(this.reserve0.raw, ZERO) || 147 | JSBI.equal(this.reserve1.raw, ZERO) || 148 | JSBI.greaterThanOrEqual(outputAmount.raw, this.reserveOf(outputAmount.token).raw) 149 | ) { 150 | throw new InsufficientReservesError() 151 | } 152 | 153 | const outputReserve = this.reserveOf(outputAmount.token) 154 | const inputReserve = this.reserveOf(outputAmount.token.equals(this.token0) ? this.token1 : this.token0) 155 | const numerator = JSBI.multiply(JSBI.multiply(inputReserve.raw, outputAmount.raw), _1000) 156 | const denominator = JSBI.multiply(JSBI.subtract(outputReserve.raw, outputAmount.raw), _997) 157 | const inputAmount = new TokenAmount( 158 | outputAmount.token.equals(this.token0) ? this.token1 : this.token0, 159 | JSBI.add(JSBI.divide(numerator, denominator), ONE) 160 | ) 161 | return [inputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] 162 | } 163 | 164 | public getLiquidityMinted( 165 | totalSupply: TokenAmount, 166 | tokenAmountA: TokenAmount, 167 | tokenAmountB: TokenAmount 168 | ): TokenAmount { 169 | invariant(totalSupply.token.equals(this.liquidityToken), 'LIQUIDITY') 170 | const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks 171 | ? [tokenAmountA, tokenAmountB] 172 | : [tokenAmountB, tokenAmountA] 173 | invariant(tokenAmounts[0].token.equals(this.token0) && tokenAmounts[1].token.equals(this.token1), 'TOKEN') 174 | 175 | let liquidity: JSBI 176 | if (JSBI.equal(totalSupply.raw, ZERO)) { 177 | liquidity = JSBI.subtract(sqrt(JSBI.multiply(tokenAmounts[0].raw, tokenAmounts[1].raw)), MINIMUM_LIQUIDITY) 178 | } else { 179 | const amount0 = JSBI.divide(JSBI.multiply(tokenAmounts[0].raw, totalSupply.raw), this.reserve0.raw) 180 | const amount1 = JSBI.divide(JSBI.multiply(tokenAmounts[1].raw, totalSupply.raw), this.reserve1.raw) 181 | liquidity = JSBI.lessThanOrEqual(amount0, amount1) ? amount0 : amount1 182 | } 183 | if (!JSBI.greaterThan(liquidity, ZERO)) { 184 | throw new InsufficientInputAmountError() 185 | } 186 | return new TokenAmount(this.liquidityToken, liquidity) 187 | } 188 | 189 | public getLiquidityValue( 190 | token: Token, 191 | totalSupply: TokenAmount, 192 | liquidity: TokenAmount, 193 | feeOn: boolean = false, 194 | kLast?: BigintIsh 195 | ): TokenAmount { 196 | invariant(this.involvesToken(token), 'TOKEN') 197 | invariant(totalSupply.token.equals(this.liquidityToken), 'TOTAL_SUPPLY') 198 | invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY') 199 | invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY') 200 | 201 | let totalSupplyAdjusted: TokenAmount 202 | if (!feeOn) { 203 | totalSupplyAdjusted = totalSupply 204 | } else { 205 | invariant(!!kLast, 'K_LAST') 206 | const kLastParsed = parseBigintIsh(kLast) 207 | if (!JSBI.equal(kLastParsed, ZERO)) { 208 | const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw)) 209 | const rootKLast = sqrt(kLastParsed) 210 | if (JSBI.greaterThan(rootK, rootKLast)) { 211 | const numerator = JSBI.multiply(totalSupply.raw, JSBI.subtract(rootK, rootKLast)) 212 | const denominator = JSBI.add(JSBI.multiply(rootK, FIVE), rootKLast) 213 | const feeLiquidity = JSBI.divide(numerator, denominator) 214 | totalSupplyAdjusted = totalSupply.add(new TokenAmount(this.liquidityToken, feeLiquidity)) 215 | } else { 216 | totalSupplyAdjusted = totalSupply 217 | } 218 | } else { 219 | totalSupplyAdjusted = totalSupply 220 | } 221 | } 222 | 223 | return new TokenAmount( 224 | token, 225 | JSBI.divide(JSBI.multiply(liquidity.raw, this.reserveOf(token).raw), totalSupplyAdjusted.raw) 226 | ) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/entities/route.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../constants' 2 | import invariant from 'tiny-invariant' 3 | 4 | import { Currency, ETHER } from './currency' 5 | import { Token, WETH } from './token' 6 | import { Pair } from './pair' 7 | import { Price } from './fractions/price' 8 | 9 | export class Route { 10 | public readonly pairs: Pair[] 11 | public readonly path: Token[] 12 | public readonly input: Currency 13 | public readonly output: Currency 14 | public readonly midPrice: Price 15 | 16 | public constructor(pairs: Pair[], input: Currency, output?: Currency) { 17 | invariant(pairs.length > 0, 'PAIRS') 18 | invariant( 19 | pairs.every(pair => pair.chainId === pairs[0].chainId), 20 | 'CHAIN_IDS' 21 | ) 22 | invariant( 23 | (input instanceof Token && pairs[0].involvesToken(input)) || 24 | (input === ETHER[pairs[0].chainId] && pairs[0].involvesToken(WETH[pairs[0].chainId])), 25 | 'INPUT' 26 | ) 27 | invariant( 28 | typeof output === 'undefined' || 29 | (output instanceof Token && pairs[pairs.length - 1].involvesToken(output)) || 30 | (output === ETHER[pairs[0].chainId] && pairs[pairs.length - 1].involvesToken(WETH[pairs[0].chainId])), 31 | 'OUTPUT' 32 | ) 33 | 34 | const path: Token[] = [input instanceof Token ? input : WETH[pairs[0].chainId]] 35 | for (const [i, pair] of pairs.entries()) { 36 | const currentInput = path[i] 37 | invariant(currentInput.equals(pair.token0) || currentInput.equals(pair.token1), 'PATH') 38 | const output = currentInput.equals(pair.token0) ? pair.token1 : pair.token0 39 | path.push(output) 40 | } 41 | 42 | this.pairs = pairs 43 | this.path = path 44 | this.midPrice = Price.fromRoute(this) 45 | this.input = input 46 | this.output = output ?? path[path.length - 1] 47 | } 48 | 49 | public get chainId(): ChainId { 50 | return this.pairs[0].chainId 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/entities/token.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { ChainId } from '../constants' 3 | import { validateAndParseAddress } from '../utils' 4 | import { Currency } from './currency' 5 | 6 | /** 7 | * Represents an ERC20 token with a unique address and some metadata. 8 | */ 9 | export class Token extends Currency { 10 | public readonly chainId: ChainId 11 | public readonly address: string 12 | 13 | public constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) { 14 | super(decimals, symbol, name) 15 | this.chainId = chainId 16 | this.address = validateAndParseAddress(address) 17 | } 18 | 19 | /** 20 | * Returns true if the two tokens are equivalent, i.e. have the same chainId and address. 21 | * @param other other token to compare 22 | */ 23 | public equals(other: Token): boolean { 24 | // short circuit on reference equality 25 | if (this === other) { 26 | return true 27 | } 28 | return this.chainId === other.chainId && this.address === other.address 29 | } 30 | 31 | /** 32 | * Returns true if the address of this token sorts before the address of the other token 33 | * @param other other token to compare 34 | * @throws if the tokens have the same address 35 | * @throws if the tokens are on different chains 36 | */ 37 | public sortsBefore(other: Token): boolean { 38 | invariant(this.chainId === other.chainId, 'CHAIN_IDS') 39 | invariant(this.address !== other.address, 'ADDRESSES') 40 | return this.address.toLowerCase() < other.address.toLowerCase() 41 | } 42 | } 43 | 44 | /** 45 | * Compares two currencies for equality 46 | */ 47 | export function currencyEquals(currencyA: Currency, currencyB: Currency): boolean { 48 | if (currencyA instanceof Token && currencyB instanceof Token) { 49 | return currencyA.equals(currencyB) 50 | } else if (currencyA instanceof Token) { 51 | return false 52 | } else if (currencyB instanceof Token) { 53 | return false 54 | } else { 55 | return currencyA === currencyB 56 | } 57 | } 58 | 59 | export const WETH = { 60 | 61 | [ChainId.MUMBAI]: new Token(ChainId.MUMBAI, '0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889', 18, 'WMATIC', 'Wrapped Matic'), 62 | [ChainId.MATIC]: new Token(ChainId.MATIC, '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 18, 'WPOL', 'Wrapped Polygon Ecosystem Token'), 63 | [ChainId.DOEGCHAIN_TESTNET]: new Token(ChainId.DOEGCHAIN_TESTNET, '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 18, 'WDOGE', 'Wrapped Doge'), 64 | [ChainId.DOGECHAIN]: new Token(ChainId.DOGECHAIN, '0xB7ddC6414bf4F5515b52D8BdD69973Ae205ff101', 18, 'WDOGE', 'Wrapped Doge'), 65 | [ChainId.ZKTESTNET]: new Token(ChainId.ZKTESTNET, '0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32', 18, 'WETH', 'Wrapped Ether'), 66 | [ChainId.ZKEVM]: new Token(ChainId.ZKEVM, '0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9', 18, 'WETH', 'Wrapped Ether'), 67 | [ChainId.KAVA]: new Token(ChainId.KAVA, '0xc86c7C0eFbd6A49B35E8714C5f59D99De09A225b', 18, 'WKAVA', 'Wrapped Kava'), 68 | [ChainId.MANTA]: new Token(ChainId.MANTA, '0x0Dc808adcE2099A9F62AA87D9670745AbA741746', 18, 'WETH', 'Wrapped Ether'), 69 | [ChainId.ZKATANA]: new Token(ChainId.ZKATANA, '0xd2480162Aa7F02Ead7BF4C127465446150D58452', 18, 'WETH', 'Wrapped Ether'), 70 | [ChainId.BTTC]: new Token(ChainId.BTTC, '0x23181f21dea5936e24163ffaba4ea3b316b57f3c', 18, 'WBTT', 'Wrapped BTT'), 71 | [ChainId.X1]: new Token(ChainId.X1, '0x87A851C652E5d772ba61ec320753c6349bb3C1E3', 18, 'OKB', 'Wrapped OKB'), 72 | [ChainId.TIMX]: new Token(ChainId.TIMX, '0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439', 18, 'WIMX', 'Wrapped IMX'), 73 | [ChainId.IMX]: new Token(ChainId.IMX, '0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d', 18, 'WIMX', 'Wrapped IMX'), 74 | [ChainId.ASTARZKEVM]: new Token(ChainId.ASTARZKEVM, '0xE9CC37904875B459Fa5D0FE37680d36F1ED55e38', 18, 'WETH', 'Wrapped Ether'), 75 | [ChainId.LAYERX]: new Token(ChainId.LAYERX, '0xe538905cf8410324e03A5A23C1c177a474D59b2b', 18, 'WOKB', 'Wrapped OKB'), 76 | [ChainId.ETHEREUM]: new Token(ChainId.ETHEREUM, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapped Ether'), 77 | [ChainId.MINATO]: new Token(ChainId.MINATO, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 78 | [ChainId.SONEIUM]: new Token(ChainId.SONEIUM, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 79 | [ChainId.SOMNIA]: new Token(ChainId.SOMNIA, '0x4A3BC48C156384f9564Fd65A53a2f3D534D8f2b7', 18, 'WSTT', 'Wrapped Somnia Testnet Tokens'), 80 | [ChainId.BASE]: new Token(ChainId.BASE, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/entities/trade.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | 3 | import { ChainId, ONE, TradeType, ZERO } from '../constants' 4 | import { sortedInsert } from '../utils' 5 | import { Currency, ETHER } from './currency' 6 | import { CurrencyAmount } from './fractions/currencyAmount' 7 | import { Fraction } from './fractions/fraction' 8 | import { Percent } from './fractions/percent' 9 | import { Price } from './fractions/price' 10 | import { TokenAmount } from './fractions/tokenAmount' 11 | import { Pair } from './pair' 12 | import { Route } from './route' 13 | import { currencyEquals, Token, WETH } from './token' 14 | 15 | /** 16 | * Returns the percent difference between the mid price and the execution price, i.e. price impact. 17 | * @param midPrice mid price before the trade 18 | * @param inputAmount the input amount of the trade 19 | * @param outputAmount the output amount of the trade 20 | */ 21 | function computePriceImpact(midPrice: Price, inputAmount: CurrencyAmount, outputAmount: CurrencyAmount): Percent { 22 | const exactQuote = midPrice.raw.multiply(inputAmount.raw) 23 | // calculate slippage := (exactQuote - outputAmount) / exactQuote 24 | const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote) 25 | return new Percent(slippage.numerator, slippage.denominator) 26 | } 27 | 28 | // minimal interface so the input output comparator may be shared across types 29 | interface InputOutput { 30 | readonly inputAmount: CurrencyAmount 31 | readonly outputAmount: CurrencyAmount 32 | } 33 | 34 | // comparator function that allows sorting trades by their output amounts, in decreasing order, and then input amounts 35 | // in increasing order. i.e. the best trades have the most outputs for the least inputs and are sorted first 36 | export function inputOutputComparator(a: InputOutput, b: InputOutput): number { 37 | // must have same input and output token for comparison 38 | invariant(currencyEquals(a.inputAmount.currency, b.inputAmount.currency), 'INPUT_CURRENCY') 39 | invariant(currencyEquals(a.outputAmount.currency, b.outputAmount.currency), 'OUTPUT_CURRENCY') 40 | if (a.outputAmount.equalTo(b.outputAmount)) { 41 | if (a.inputAmount.equalTo(b.inputAmount)) { 42 | return 0 43 | } 44 | // trade A requires less input than trade B, so A should come first 45 | if (a.inputAmount.lessThan(b.inputAmount)) { 46 | return -1 47 | } else { 48 | return 1 49 | } 50 | } else { 51 | // tradeA has less output than trade B, so should come second 52 | if (a.outputAmount.lessThan(b.outputAmount)) { 53 | return 1 54 | } else { 55 | return -1 56 | } 57 | } 58 | } 59 | 60 | // extension of the input output comparator that also considers other dimensions of the trade in ranking them 61 | export function tradeComparator(a: Trade, b: Trade) { 62 | const ioComp = inputOutputComparator(a, b) 63 | if (ioComp !== 0) { 64 | return ioComp 65 | } 66 | 67 | // consider lowest slippage next, since these are less likely to fail 68 | if (a.priceImpact.lessThan(b.priceImpact)) { 69 | return -1 70 | } else if (a.priceImpact.greaterThan(b.priceImpact)) { 71 | return 1 72 | } 73 | 74 | // finally consider the number of hops since each hop costs gas 75 | return a.route.path.length - b.route.path.length 76 | } 77 | 78 | export interface BestTradeOptions { 79 | // how many results to return 80 | maxNumResults?: number 81 | // the maximum number of hops a trade should contain 82 | maxHops?: number 83 | } 84 | 85 | /** 86 | * Given a currency amount and a chain ID, returns the equivalent representation as the token amount. 87 | * In other words, if the currency is ETHER, returns the WETH token amount for the given chain. Otherwise, returns 88 | * the input currency amount. 89 | */ 90 | function wrappedAmount(currencyAmount: CurrencyAmount, chainId: ChainId): TokenAmount { 91 | if (currencyAmount instanceof TokenAmount) return currencyAmount 92 | if (currencyAmount.currency === ETHER[chainId]) return new TokenAmount(WETH[chainId], currencyAmount.raw) 93 | invariant(false, 'CURRENCY') 94 | } 95 | 96 | function wrappedCurrency(currency: Currency, chainId: ChainId): Token { 97 | if (currency instanceof Token) return currency 98 | if (currency === ETHER[chainId]) return WETH[chainId] 99 | invariant(false, 'CURRENCY') 100 | } 101 | 102 | /** 103 | * Represents a trade executed against a list of pairs. 104 | * Does not account for slippage, i.e. trades that front run this trade and move the price. 105 | */ 106 | export class Trade { 107 | /** 108 | * The route of the trade, i.e. which pairs the trade goes through. 109 | */ 110 | public readonly route: Route 111 | /** 112 | * The type of the trade, either exact in or exact out. 113 | */ 114 | public readonly tradeType: TradeType 115 | /** 116 | * The input amount for the trade assuming no slippage. 117 | */ 118 | public readonly inputAmount: CurrencyAmount 119 | /** 120 | * The output amount for the trade assuming no slippage. 121 | */ 122 | public readonly outputAmount: CurrencyAmount 123 | /** 124 | * The price expressed in terms of output amount/input amount. 125 | */ 126 | public readonly executionPrice: Price 127 | /** 128 | * The mid price after the trade executes assuming no slippage. 129 | */ 130 | public readonly nextMidPrice: Price 131 | /** 132 | * The percent difference between the mid price before the trade and the trade execution price. 133 | */ 134 | public readonly priceImpact: Percent 135 | 136 | /** 137 | * Constructs an exact in trade with the given amount in and route 138 | * @param route route of the exact in trade 139 | * @param amountIn the amount being passed in 140 | */ 141 | public static exactIn(route: Route, amountIn: CurrencyAmount): Trade { 142 | return new Trade(route, amountIn, TradeType.EXACT_INPUT) 143 | } 144 | 145 | /** 146 | * Constructs an exact out trade with the given amount out and route 147 | * @param route route of the exact out trade 148 | * @param amountOut the amount returned by the trade 149 | */ 150 | public static exactOut(route: Route, amountOut: CurrencyAmount): Trade { 151 | return new Trade(route, amountOut, TradeType.EXACT_OUTPUT) 152 | } 153 | 154 | public constructor(route: Route, amount: CurrencyAmount, tradeType: TradeType) { 155 | const amounts: TokenAmount[] = new Array(route.path.length) 156 | const nextPairs: Pair[] = new Array(route.pairs.length) 157 | if (tradeType === TradeType.EXACT_INPUT) { 158 | invariant(currencyEquals(amount.currency, route.input), 'INPUT') 159 | amounts[0] = wrappedAmount(amount, route.chainId) 160 | for (let i = 0; i < route.path.length - 1; i++) { 161 | const pair = route.pairs[i] 162 | const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i]) 163 | amounts[i + 1] = outputAmount 164 | nextPairs[i] = nextPair 165 | } 166 | } else { 167 | invariant(currencyEquals(amount.currency, route.output), 'OUTPUT') 168 | amounts[amounts.length - 1] = wrappedAmount(amount, route.chainId) 169 | for (let i = route.path.length - 1; i > 0; i--) { 170 | const pair = route.pairs[i - 1] 171 | const [inputAmount, nextPair] = pair.getInputAmount(amounts[i]) 172 | amounts[i - 1] = inputAmount 173 | nextPairs[i - 1] = nextPair 174 | } 175 | } 176 | 177 | this.route = route 178 | this.tradeType = tradeType 179 | this.inputAmount = 180 | tradeType === TradeType.EXACT_INPUT 181 | ? amount 182 | : route.input === ETHER[route.pairs[0].chainId] 183 | ? CurrencyAmount.ether(amounts[0].raw, route.pairs[0].chainId) 184 | : amounts[0] 185 | this.outputAmount = 186 | tradeType === TradeType.EXACT_OUTPUT 187 | ? amount 188 | : route.output === ETHER[route.pairs[0].chainId] 189 | ? CurrencyAmount.ether(amounts[amounts.length - 1].raw, route.pairs[0].chainId) 190 | : amounts[amounts.length - 1] 191 | this.executionPrice = new Price( 192 | this.inputAmount.currency, 193 | this.outputAmount.currency, 194 | this.inputAmount.raw, 195 | this.outputAmount.raw 196 | ) 197 | this.nextMidPrice = Price.fromRoute(new Route(nextPairs, route.input)) 198 | this.priceImpact = computePriceImpact(route.midPrice, this.inputAmount, this.outputAmount) 199 | } 200 | 201 | /** 202 | * Get the minimum amount that must be received from this trade for the given slippage tolerance 203 | * @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade 204 | */ 205 | public minimumAmountOut(slippageTolerance: Percent): CurrencyAmount { 206 | invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE') 207 | if (this.tradeType === TradeType.EXACT_OUTPUT) { 208 | return this.outputAmount 209 | } else { 210 | const slippageAdjustedAmountOut = new Fraction(ONE) 211 | .add(slippageTolerance) 212 | .invert() 213 | .multiply(this.outputAmount.raw).quotient 214 | return this.outputAmount instanceof TokenAmount 215 | ? new TokenAmount(this.outputAmount.token, slippageAdjustedAmountOut) 216 | : CurrencyAmount.ether(slippageAdjustedAmountOut, this.route.pairs[0].chainId) 217 | } 218 | } 219 | 220 | /** 221 | * Get the maximum amount in that can be spent via this trade for the given slippage tolerance 222 | * @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade 223 | */ 224 | public maximumAmountIn(slippageTolerance: Percent): CurrencyAmount { 225 | invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE') 226 | if (this.tradeType === TradeType.EXACT_INPUT) { 227 | return this.inputAmount 228 | } else { 229 | const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient 230 | return this.inputAmount instanceof TokenAmount 231 | ? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn) 232 | : CurrencyAmount.ether(slippageAdjustedAmountIn, this.route.pairs[0].chainId) 233 | } 234 | } 235 | 236 | /** 237 | * Given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token 238 | * amount to an output token, making at most `maxHops` hops. 239 | * Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting 240 | * the amount in among multiple routes. 241 | * @param pairs the pairs to consider in finding the best trade 242 | * @param currencyAmountIn exact amount of input currency to spend 243 | * @param currencyOut the desired currency out 244 | * @param maxNumResults maximum number of results to return 245 | * @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair 246 | * @param currentPairs used in recursion; the current list of pairs 247 | * @param originalAmountIn used in recursion; the original value of the currencyAmountIn parameter 248 | * @param bestTrades used in recursion; the current list of best trades 249 | */ 250 | public static bestTradeExactIn( 251 | pairs: Pair[], 252 | currencyAmountIn: CurrencyAmount, 253 | currencyOut: Currency, 254 | { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, 255 | // used in recursion. 256 | currentPairs: Pair[] = [], 257 | originalAmountIn: CurrencyAmount = currencyAmountIn, 258 | bestTrades: Trade[] = [] 259 | ): Trade[] { 260 | invariant(pairs.length > 0, 'PAIRS') 261 | invariant(maxHops > 0, 'MAX_HOPS') 262 | invariant(originalAmountIn === currencyAmountIn || currentPairs.length > 0, 'INVALID_RECURSION') 263 | const chainId: ChainId | undefined = 264 | currencyAmountIn instanceof TokenAmount 265 | ? currencyAmountIn.token.chainId 266 | : currencyOut instanceof Token 267 | ? currencyOut.chainId 268 | : undefined 269 | invariant(chainId !== undefined, 'CHAIN_ID') 270 | 271 | const amountIn = wrappedAmount(currencyAmountIn, chainId) 272 | const tokenOut = wrappedCurrency(currencyOut, chainId) 273 | for (let i = 0; i < pairs.length; i++) { 274 | const pair = pairs[i] 275 | // pair irrelevant 276 | if (!pair.token0.equals(amountIn.token) && !pair.token1.equals(amountIn.token)) continue 277 | if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO)) continue 278 | 279 | let amountOut: TokenAmount 280 | try { 281 | ;[amountOut] = pair.getOutputAmount(amountIn) 282 | } catch (error) { 283 | // input too low 284 | if (error.isInsufficientInputAmountError) { 285 | continue 286 | } 287 | throw error 288 | } 289 | // we have arrived at the output token, so this is the final trade of one of the paths 290 | if (amountOut.token.equals(tokenOut)) { 291 | sortedInsert( 292 | bestTrades, 293 | new Trade( 294 | new Route([...currentPairs, pair], originalAmountIn.currency, currencyOut), 295 | originalAmountIn, 296 | TradeType.EXACT_INPUT 297 | ), 298 | maxNumResults, 299 | tradeComparator 300 | ) 301 | } else if (maxHops > 1 && pairs.length > 1) { 302 | const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length)) 303 | 304 | // otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops 305 | Trade.bestTradeExactIn( 306 | pairsExcludingThisPair, 307 | amountOut, 308 | currencyOut, 309 | { 310 | maxNumResults, 311 | maxHops: maxHops - 1 312 | }, 313 | [...currentPairs, pair], 314 | originalAmountIn, 315 | bestTrades 316 | ) 317 | } 318 | } 319 | 320 | return bestTrades 321 | } 322 | 323 | /** 324 | * similar to the above method but instead targets a fixed output amount 325 | * given a list of pairs, and a fixed amount out, returns the top `maxNumResults` trades that go from an input token 326 | * to an output token amount, making at most `maxHops` hops 327 | * note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting 328 | * the amount in among multiple routes. 329 | * @param pairs the pairs to consider in finding the best trade 330 | * @param currencyIn the currency to spend 331 | * @param currencyAmountOut the exact amount of currency out 332 | * @param maxNumResults maximum number of results to return 333 | * @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair 334 | * @param currentPairs used in recursion; the current list of pairs 335 | * @param originalAmountOut used in recursion; the original value of the currencyAmountOut parameter 336 | * @param bestTrades used in recursion; the current list of best trades 337 | */ 338 | public static bestTradeExactOut( 339 | pairs: Pair[], 340 | currencyIn: Currency, 341 | currencyAmountOut: CurrencyAmount, 342 | { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, 343 | // used in recursion. 344 | currentPairs: Pair[] = [], 345 | originalAmountOut: CurrencyAmount = currencyAmountOut, 346 | bestTrades: Trade[] = [] 347 | ): Trade[] { 348 | invariant(pairs.length > 0, 'PAIRS') 349 | invariant(maxHops > 0, 'MAX_HOPS') 350 | invariant(originalAmountOut === currencyAmountOut || currentPairs.length > 0, 'INVALID_RECURSION') 351 | const chainId: ChainId | undefined = 352 | currencyAmountOut instanceof TokenAmount 353 | ? currencyAmountOut.token.chainId 354 | : currencyIn instanceof Token 355 | ? currencyIn.chainId 356 | : undefined 357 | invariant(chainId !== undefined, 'CHAIN_ID') 358 | 359 | const amountOut = wrappedAmount(currencyAmountOut, chainId) 360 | const tokenIn = wrappedCurrency(currencyIn, chainId) 361 | for (let i = 0; i < pairs.length; i++) { 362 | const pair = pairs[i] 363 | // pair irrelevant 364 | if (!pair.token0.equals(amountOut.token) && !pair.token1.equals(amountOut.token)) continue 365 | if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO)) continue 366 | 367 | let amountIn: TokenAmount 368 | try { 369 | ;[amountIn] = pair.getInputAmount(amountOut) 370 | } catch (error) { 371 | // not enough liquidity in this pair 372 | if (error.isInsufficientReservesError) { 373 | continue 374 | } 375 | throw error 376 | } 377 | // we have arrived at the input token, so this is the first trade of one of the paths 378 | if (amountIn.token.equals(tokenIn)) { 379 | sortedInsert( 380 | bestTrades, 381 | new Trade( 382 | new Route([pair, ...currentPairs], currencyIn, originalAmountOut.currency), 383 | originalAmountOut, 384 | TradeType.EXACT_OUTPUT 385 | ), 386 | maxNumResults, 387 | tradeComparator 388 | ) 389 | } else if (maxHops > 1 && pairs.length > 1) { 390 | const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length)) 391 | 392 | // otherwise, consider all the other paths that arrive at this token as long as we have not exceeded maxHops 393 | Trade.bestTradeExactOut( 394 | pairsExcludingThisPair, 395 | currencyIn, 396 | amountIn, 397 | { 398 | maxNumResults, 399 | maxHops: maxHops - 1 400 | }, 401 | [pair, ...currentPairs], 402 | originalAmountOut, 403 | bestTrades 404 | ) 405 | } 406 | } 407 | 408 | return bestTrades 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | // see https://stackoverflow.com/a/41102306 2 | const CAN_SET_PROTOTYPE = 'setPrototypeOf' in Object 3 | 4 | /** 5 | * Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be 6 | * obtained by sending any amount of input. 7 | */ 8 | export class InsufficientReservesError extends Error { 9 | public readonly isInsufficientReservesError: true = true 10 | 11 | public constructor() { 12 | super() 13 | this.name = this.constructor.name 14 | if (CAN_SET_PROTOTYPE) Object.setPrototypeOf(this, new.target.prototype) 15 | } 16 | } 17 | 18 | /** 19 | * Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less 20 | * than the price of a single unit of output after fees. 21 | */ 22 | export class InsufficientInputAmountError extends Error { 23 | public readonly isInsufficientInputAmountError: true = true 24 | 25 | public constructor() { 26 | super() 27 | this.name = this.constructor.name 28 | if (CAN_SET_PROTOTYPE) Object.setPrototypeOf(this, new.target.prototype) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '@ethersproject/contracts' 2 | import { getNetwork } from '@ethersproject/networks' 3 | import { getDefaultProvider } from '@ethersproject/providers' 4 | import { TokenAmount } from './entities/fractions/tokenAmount' 5 | import { Pair } from './entities/pair' 6 | import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json' 7 | import invariant from 'tiny-invariant' 8 | import ERC20 from './abis/ERC20.json' 9 | import { ChainId } from './constants' 10 | import { Token } from './entities/token' 11 | 12 | let TOKEN_DECIMALS_CACHE: { [chainId: number]: { [address: string]: number } } = { 13 | [ChainId.MUMBAI]: { 14 | '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A': 9 // DGD 15 | } 16 | } 17 | 18 | /** 19 | * Contains methods for constructing instances of pairs and tokens from on-chain data. 20 | */ 21 | export abstract class Fetcher { 22 | /** 23 | * Cannot be constructed. 24 | */ 25 | private constructor() {} 26 | 27 | /** 28 | * Fetch information for a given token on the given chain, using the given ethers provider. 29 | * @param chainId chain of the token 30 | * @param address address of the token on the chain 31 | * @param provider provider used to fetch the token 32 | * @param symbol optional symbol of the token 33 | * @param name optional name of the token 34 | */ 35 | public static async fetchTokenData( 36 | chainId: ChainId, 37 | address: string, 38 | provider = getDefaultProvider(getNetwork(chainId)), 39 | symbol?: string, 40 | name?: string 41 | ): Promise { 42 | const parsedDecimals = 43 | typeof TOKEN_DECIMALS_CACHE?.[chainId]?.[address] === 'number' 44 | ? TOKEN_DECIMALS_CACHE[chainId][address] 45 | : await new Contract(address, ERC20, provider).decimals().then((decimals: number): number => { 46 | TOKEN_DECIMALS_CACHE = { 47 | ...TOKEN_DECIMALS_CACHE, 48 | [chainId]: { 49 | ...TOKEN_DECIMALS_CACHE?.[chainId], 50 | [address]: decimals 51 | } 52 | } 53 | return decimals 54 | }) 55 | return new Token(chainId, address, parsedDecimals, symbol, name) 56 | } 57 | 58 | /** 59 | * Fetches information about a pair and constructs a pair from the given two tokens. 60 | * @param tokenA first token 61 | * @param tokenB second token 62 | * @param provider the provider to use to fetch the data 63 | */ 64 | public static async fetchPairData( 65 | tokenA: Token, 66 | tokenB: Token, 67 | provider = getDefaultProvider(getNetwork(tokenA.chainId)) 68 | ): Promise { 69 | invariant(tokenA.chainId === tokenB.chainId, 'CHAIN_ID') 70 | const address = Pair.getAddress(tokenA, tokenB) 71 | const [reserves0, reserves1] = await new Contract(address, IUniswapV2Pair.abi, provider).getReserves() 72 | const balances = tokenA.sortsBefore(tokenB) ? [reserves0, reserves1] : [reserves1, reserves0] 73 | return new Pair(new TokenAmount(tokenA, balances[0]), new TokenAmount(tokenB, balances[1])) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | export { JSBI } 3 | 4 | export { 5 | BigintIsh, 6 | ChainId, 7 | TradeType, 8 | Rounding, 9 | FACTORY_ADDRESS, 10 | INIT_CODE_HASH, 11 | MINIMUM_LIQUIDITY 12 | } from './constants' 13 | 14 | export * from './errors' 15 | export * from './entities' 16 | export * from './router' 17 | export * from './fetcher' 18 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { TradeType } from './constants' 2 | import invariant from 'tiny-invariant' 3 | import { validateAndParseAddress } from './utils' 4 | import { CurrencyAmount, ETHER, Percent, Trade } from './entities' 5 | 6 | /** 7 | * Options for producing the arguments to send call to the router. 8 | */ 9 | export interface TradeOptions { 10 | /** 11 | * How much the execution price is allowed to move unfavorably from the trade execution price. 12 | */ 13 | allowedSlippage: Percent 14 | /** 15 | * How long the swap is valid until it expires, in seconds. 16 | * This will be used to produce a `deadline` parameter which is computed from when the swap call parameters 17 | * are generated. 18 | */ 19 | ttl: number 20 | /** 21 | * The account that should receive the output of the swap. 22 | */ 23 | recipient: string 24 | 25 | /** 26 | * Whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods 27 | */ 28 | feeOnTransfer?: boolean 29 | } 30 | 31 | /** 32 | * The parameters to use in the call to the Uniswap V2 Router to execute a trade. 33 | */ 34 | export interface SwapParameters { 35 | /** 36 | * The method to call on the Uniswap V2 Router. 37 | */ 38 | methodName: string 39 | /** 40 | * The arguments to pass to the method, all hex encoded. 41 | */ 42 | args: (string | string[])[] 43 | /** 44 | * The amount of wei to send in hex. 45 | */ 46 | value: string 47 | } 48 | 49 | function toHex(currencyAmount: CurrencyAmount) { 50 | return `0x${currencyAmount.raw.toString(16)}` 51 | } 52 | 53 | const ZERO_HEX = '0x0' 54 | 55 | /** 56 | * Represents the Uniswap V2 Router, and has static methods for helping execute trades. 57 | */ 58 | export abstract class Router { 59 | /** 60 | * Cannot be constructed. 61 | */ 62 | private constructor() {} 63 | /** 64 | * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade. 65 | * @param trade to produce call parameters for 66 | * @param options options for the call parameters 67 | */ 68 | public static swapCallParameters(trade: Trade, options: TradeOptions): SwapParameters { 69 | const etherIn = trade.inputAmount.currency === ETHER[trade.route.pairs[0].chainId] 70 | const etherOut = trade.outputAmount.currency === ETHER[trade.route.pairs[0].chainId] 71 | // the router does not support both ether in and out 72 | invariant(!(etherIn && etherOut), 'ETHER_IN_OUT') 73 | invariant(options.ttl > 0, 'TTL') 74 | 75 | const to: string = validateAndParseAddress(options.recipient) 76 | const amountIn: string = toHex(trade.maximumAmountIn(options.allowedSlippage)) 77 | const amountOut: string = toHex(trade.minimumAmountOut(options.allowedSlippage)) 78 | const path: string[] = trade.route.path.map(token => token.address) 79 | const deadline = `0x${(Math.floor(new Date().getTime() / 1000) + options.ttl).toString(16)}` 80 | const useFeeOnTransfer = Boolean(options.feeOnTransfer) 81 | 82 | let methodName: string 83 | let args: (string | string[])[] 84 | let value: string 85 | switch (trade.tradeType) { 86 | case TradeType.EXACT_INPUT: 87 | if (etherIn) { 88 | methodName = useFeeOnTransfer ? 'swapExactETHForTokensSupportingFeeOnTransferTokens' : 'swapExactETHForTokens' 89 | // (uint amountOutMin, address[] calldata path, address to, uint deadline) 90 | args = [amountOut, path, to, deadline] 91 | value = amountIn 92 | } else if (etherOut) { 93 | methodName = useFeeOnTransfer ? 'swapExactTokensForETHSupportingFeeOnTransferTokens' : 'swapExactTokensForETH' 94 | // (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 95 | args = [amountIn, amountOut, path, to, deadline] 96 | value = ZERO_HEX 97 | } else { 98 | methodName = useFeeOnTransfer 99 | ? 'swapExactTokensForTokensSupportingFeeOnTransferTokens' 100 | : 'swapExactTokensForTokens' 101 | // (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 102 | args = [amountIn, amountOut, path, to, deadline] 103 | value = ZERO_HEX 104 | } 105 | break 106 | case TradeType.EXACT_OUTPUT: 107 | invariant(!useFeeOnTransfer, 'EXACT_OUT_FOT') 108 | if (etherIn) { 109 | methodName = 'swapETHForExactTokens' 110 | // (uint amountOut, address[] calldata path, address to, uint deadline) 111 | args = [amountOut, path, to, deadline] 112 | value = amountIn 113 | } else if (etherOut) { 114 | methodName = 'swapTokensForExactETH' 115 | // (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 116 | args = [amountOut, amountIn, path, to, deadline] 117 | value = ZERO_HEX 118 | } else { 119 | methodName = 'swapTokensForExactTokens' 120 | // (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 121 | args = [amountOut, amountIn, path, to, deadline] 122 | value = ZERO_HEX 123 | } 124 | break 125 | } 126 | return { 127 | methodName, 128 | args, 129 | value 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import warning from 'tiny-warning' 3 | import JSBI from 'jsbi' 4 | import { getAddress } from '@ethersproject/address' 5 | 6 | import { BigintIsh, ZERO, ONE, TWO, THREE, SolidityType, SOLIDITY_TYPE_MAXIMA } from './constants' 7 | 8 | export function validateSolidityTypeInstance(value: JSBI, solidityType: SolidityType): void { 9 | invariant(JSBI.greaterThanOrEqual(value, ZERO), `${value} is not a ${solidityType}.`) 10 | invariant(JSBI.lessThanOrEqual(value, SOLIDITY_TYPE_MAXIMA[solidityType]), `${value} is not a ${solidityType}.`) 11 | } 12 | 13 | // warns if addresses are not checksummed 14 | export function validateAndParseAddress(address: string): string { 15 | try { 16 | const checksummedAddress = getAddress(address) 17 | warning(address === checksummedAddress, `${address} is not checksummed.`) 18 | return checksummedAddress 19 | } catch (error) { 20 | invariant(false, `${address} is not a valid address.`) 21 | } 22 | } 23 | 24 | export function parseBigintIsh(bigintIsh: BigintIsh): JSBI { 25 | return bigintIsh instanceof JSBI 26 | ? bigintIsh 27 | : typeof bigintIsh === 'bigint' 28 | ? JSBI.BigInt(bigintIsh.toString()) 29 | : JSBI.BigInt(bigintIsh) 30 | } 31 | 32 | // mock the on-chain sqrt function 33 | export function sqrt(y: JSBI): JSBI { 34 | validateSolidityTypeInstance(y, SolidityType.uint256) 35 | let z: JSBI = ZERO 36 | let x: JSBI 37 | if (JSBI.greaterThan(y, THREE)) { 38 | z = y 39 | x = JSBI.add(JSBI.divide(y, TWO), ONE) 40 | while (JSBI.lessThan(x, z)) { 41 | z = x 42 | x = JSBI.divide(JSBI.add(JSBI.divide(y, x), x), TWO) 43 | } 44 | } else if (JSBI.notEqual(y, ZERO)) { 45 | z = ONE 46 | } 47 | return z 48 | } 49 | 50 | // given an array of items sorted by `comparator`, insert an item into its sort index and constrain the size to 51 | // `maxSize` by removing the last item 52 | export function sortedInsert(items: T[], add: T, maxSize: number, comparator: (a: T, b: T) => number): T | null { 53 | invariant(maxSize > 0, 'MAX_SIZE_ZERO') 54 | // this is an invariant because the interface cannot return multiple removed items if items.length exceeds maxSize 55 | invariant(items.length <= maxSize, 'ITEMS_SIZE') 56 | 57 | // short circuit first item add 58 | if (items.length === 0) { 59 | items.push(add) 60 | return null 61 | } else { 62 | const isFull = items.length === maxSize 63 | // short circuit if full and the additional item does not come before the last item 64 | if (isFull && comparator(items[items.length - 1], add) <= 0) { 65 | return add 66 | } 67 | 68 | let lo = 0, 69 | hi = items.length 70 | 71 | while (lo < hi) { 72 | const mid = (lo + hi) >>> 1 73 | if (comparator(items[mid], add) <= 0) { 74 | lo = mid + 1 75 | } else { 76 | hi = mid 77 | } 78 | } 79 | items.splice(lo, 0, add) 80 | return isFull ? items.pop()! : null 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { INIT_CODE_HASH } from '../src/constants' 2 | 3 | //import { bytecode } from '@uniswap/v2-core/build/UniswapV2Pair.json' 4 | import { keccak256 } from '@ethersproject/solidity' 5 | 6 | const bytecode = "60806040526001600c5534801561001557600080fd5b506040514690806052612d228239604080519182900360520182208282018252600a8352692ab734b9bbb0b8102b1960b11b6020938401528151808301835260018152603160f81b908401528151808401919091527fbfcc8ef98ffbf7b6c3fec7bf5185b566b9863e35a9d83acd49ad6824b5969738818301527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6606082015260808101949094523060a0808601919091528151808603909101815260c09094019052825192019190912060035550600580546001600160a01b03191633179055612c1d806101056000396000f3fe608060405234801561001057600080fd5b50600436106101b95760003560e01c80636a627842116100f9578063ba9a7a5611610097578063d21220a711610071578063d21220a7146105da578063d505accf146105e2578063dd62ed3e14610640578063fff6cae91461067b576101b9565b8063ba9a7a5614610597578063bc25cf771461059f578063c45a0155146105d2576101b9565b80637ecebe00116100d35780637ecebe00146104d757806389afcb441461050a57806395d89b4114610556578063a9059cbb1461055e576101b9565b80636a6278421461046957806370a082311461049c5780637464fc3d146104cf576101b9565b806323b872dd116101665780633644e515116101405780633644e51514610416578063485cc9551461041e5780635909c0d5146104595780635a3d549314610461576101b9565b806323b872dd146103ad57806330adf81f146103f0578063313ce567146103f8576101b9565b8063095ea7b311610197578063095ea7b3146103155780630dfe16811461036257806318160ddd14610393576101b9565b8063022c0d9f146101be57806306fdde03146102595780630902f1ac146102d6575b600080fd5b610257600480360360808110156101d457600080fd5b81359160208101359173ffffffffffffffffffffffffffffffffffffffff604083013516919081019060808101606082013564010000000081111561021857600080fd5b82018360208201111561022a57600080fd5b8035906020019184600183028401116401000000008311171561024c57600080fd5b509092509050610683565b005b610261610d57565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561029b578181015183820152602001610283565b50505050905090810190601f1680156102c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6102de610d90565b604080516dffffffffffffffffffffffffffff948516815292909316602083015263ffffffff168183015290519081900360600190f35b61034e6004803603604081101561032b57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610de5565b604080519115158252519081900360200190f35b61036a610dfc565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b61039b610e18565b60408051918252519081900360200190f35b61034e600480360360608110156103c357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060400135610e1e565b61039b610efd565b610400610f21565b6040805160ff9092168252519081900360200190f35b61039b610f26565b6102576004803603604081101561043457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020013516610f2c565b61039b611005565b61039b61100b565b61039b6004803603602081101561047f57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611011565b61039b600480360360208110156104b257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166113cb565b61039b6113dd565b61039b600480360360208110156104ed57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166113e3565b61053d6004803603602081101561052057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166113f5565b6040805192835260208301919091528051918290030190f35b610261611892565b61034e6004803603604081101561057457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356118cb565b61039b6118d8565b610257600480360360208110156105b557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118de565b61036a611ad4565b61036a611af0565b610257600480360360e08110156105f857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135611b0c565b61039b6004803603604081101561065657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020013516611dd8565b610257611df5565b600c546001146106f457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f556e697377617056323a204c4f434b4544000000000000000000000000000000604482015290519081900360640190fd5b6000600c55841515806107075750600084115b61075c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180612b2f6025913960400191505060405180910390fd5b600080610767610d90565b5091509150816dffffffffffffffffffffffffffff168710801561079a5750806dffffffffffffffffffffffffffff1686105b6107ef576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526021815260200180612b786021913960400191505060405180910390fd5b600654600754600091829173ffffffffffffffffffffffffffffffffffffffff91821691908116908916821480159061085457508073ffffffffffffffffffffffffffffffffffffffff168973ffffffffffffffffffffffffffffffffffffffff1614155b6108bf57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601560248201527f556e697377617056323a20494e56414c49445f544f0000000000000000000000604482015290519081900360640190fd5b8a156108d0576108d0828a8d611fdb565b89156108e1576108e1818a8c611fdb565b86156109c3578873ffffffffffffffffffffffffffffffffffffffff166310d1e85c338d8d8c8c6040518663ffffffff1660e01b8152600401808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001858152602001848152602001806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509650505050505050600060405180830381600087803b1580156109aa57600080fd5b505af11580156109be573d6000803e3d6000fd5b505050505b604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905173ffffffffffffffffffffffffffffffffffffffff8416916370a08231916024808301926020929190829003018186803b158015610a2f57600080fd5b505afa158015610a43573d6000803e3d6000fd5b505050506040513d6020811015610a5957600080fd5b5051604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905191955073ffffffffffffffffffffffffffffffffffffffff8316916370a0823191602480820192602092909190829003018186803b158015610acb57600080fd5b505afa158015610adf573d6000803e3d6000fd5b505050506040513d6020811015610af557600080fd5b5051925060009150506dffffffffffffffffffffffffffff85168a90038311610b1f576000610b35565b89856dffffffffffffffffffffffffffff160383035b9050600089856dffffffffffffffffffffffffffff16038311610b59576000610b6f565b89856dffffffffffffffffffffffffffff160383035b90506000821180610b805750600081115b610bd5576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526024815260200180612b546024913960400191505060405180910390fd5b6000610c09610beb84600363ffffffff6121e816565b610bfd876103e863ffffffff6121e816565b9063ffffffff61226e16565b90506000610c21610beb84600363ffffffff6121e816565b9050610c59620f4240610c4d6dffffffffffffffffffffffffffff8b8116908b1663ffffffff6121e816565b9063ffffffff6121e816565b610c69838363ffffffff6121e816565b1015610cd657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600c60248201527f556e697377617056323a204b0000000000000000000000000000000000000000604482015290519081900360640190fd5b5050610ce4848488886122e0565b60408051838152602081018390528082018d9052606081018c9052905173ffffffffffffffffffffffffffffffffffffffff8b169133917fd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d8229181900360800190a350506001600c55505050505050505050565b6040518060400160405280600a81526020017f556e69737761702056320000000000000000000000000000000000000000000081525081565b6008546dffffffffffffffffffffffffffff808216926e0100000000000000000000000000008304909116917c0100000000000000000000000000000000000000000000000000000000900463ffffffff1690565b6000610df233848461259c565b5060015b92915050565b60065473ffffffffffffffffffffffffffffffffffffffff1681565b60005481565b73ffffffffffffffffffffffffffffffffffffffff831660009081526002602090815260408083203384529091528120547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14610ee85773ffffffffffffffffffffffffffffffffffffffff84166000908152600260209081526040808320338452909152902054610eb6908363ffffffff61226e16565b73ffffffffffffffffffffffffffffffffffffffff851660009081526002602090815260408083203384529091529020555b610ef384848461260b565b5060019392505050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b601281565b60035481565b60055473ffffffffffffffffffffffffffffffffffffffff163314610fb257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f556e697377617056323a20464f5242494444454e000000000000000000000000604482015290519081900360640190fd5b6006805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff00000000000000000000000000000000000000009182161790915560078054929093169116179055565b60095481565b600a5481565b6000600c5460011461108457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f556e697377617056323a204c4f434b4544000000000000000000000000000000604482015290519081900360640190fd5b6000600c81905580611094610d90565b50600654604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905193955091935060009273ffffffffffffffffffffffffffffffffffffffff909116916370a08231916024808301926020929190829003018186803b15801561110e57600080fd5b505afa158015611122573d6000803e3d6000fd5b505050506040513d602081101561113857600080fd5b5051600754604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905192935060009273ffffffffffffffffffffffffffffffffffffffff909216916370a0823191602480820192602092909190829003018186803b1580156111b157600080fd5b505afa1580156111c5573d6000803e3d6000fd5b505050506040513d60208110156111db57600080fd5b505190506000611201836dffffffffffffffffffffffffffff871663ffffffff61226e16565b90506000611225836dffffffffffffffffffffffffffff871663ffffffff61226e16565b9050600061123387876126ec565b600054909150806112705761125c6103e8610bfd611257878763ffffffff6121e816565b612878565b985061126b60006103e86128ca565b6112cd565b6112ca6dffffffffffffffffffffffffffff8916611294868463ffffffff6121e816565b8161129b57fe5b046dffffffffffffffffffffffffffff89166112bd868563ffffffff6121e816565b816112c457fe5b0461297a565b98505b60008911611326576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180612bc16028913960400191505060405180910390fd5b6113308a8a6128ca565b61133c86868a8a6122e0565b811561137e5760085461137a906dffffffffffffffffffffffffffff808216916e01000000000000000000000000000090041663ffffffff6121e816565b600b555b6040805185815260208101859052815133927f4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f928290030190a250506001600c5550949695505050505050565b60016020526000908152604090205481565b600b5481565b60046020526000908152604090205481565b600080600c5460011461146957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f556e697377617056323a204c4f434b4544000000000000000000000000000000604482015290519081900360640190fd5b6000600c81905580611479610d90565b50600654600754604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905194965092945073ffffffffffffffffffffffffffffffffffffffff9182169391169160009184916370a08231916024808301926020929190829003018186803b1580156114fb57600080fd5b505afa15801561150f573d6000803e3d6000fd5b505050506040513d602081101561152557600080fd5b5051604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905191925060009173ffffffffffffffffffffffffffffffffffffffff8516916370a08231916024808301926020929190829003018186803b15801561159957600080fd5b505afa1580156115ad573d6000803e3d6000fd5b505050506040513d60208110156115c357600080fd5b5051306000908152600160205260408120549192506115e288886126ec565b600054909150806115f9848763ffffffff6121e816565b8161160057fe5b049a5080611614848663ffffffff6121e816565b8161161b57fe5b04995060008b11801561162e575060008a115b611683576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180612b996028913960400191505060405180910390fd5b61168d3084612992565b611698878d8d611fdb565b6116a3868d8c611fdb565b604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905173ffffffffffffffffffffffffffffffffffffffff8916916370a08231916024808301926020929190829003018186803b15801561170f57600080fd5b505afa158015611723573d6000803e3d6000fd5b505050506040513d602081101561173957600080fd5b5051604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905191965073ffffffffffffffffffffffffffffffffffffffff8816916370a0823191602480820192602092909190829003018186803b1580156117ab57600080fd5b505afa1580156117bf573d6000803e3d6000fd5b505050506040513d60208110156117d557600080fd5b505193506117e585858b8b6122e0565b811561182757600854611823906dffffffffffffffffffffffffffff808216916e01000000000000000000000000000090041663ffffffff6121e816565b600b555b604080518c8152602081018c9052815173ffffffffffffffffffffffffffffffffffffffff8f169233927fdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496929081900390910190a35050505050505050506001600c81905550915091565b6040518060400160405280600681526020017f554e492d5632000000000000000000000000000000000000000000000000000081525081565b6000610df233848461260b565b6103e881565b600c5460011461194f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f556e697377617056323a204c4f434b4544000000000000000000000000000000604482015290519081900360640190fd5b6000600c55600654600754600854604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905173ffffffffffffffffffffffffffffffffffffffff9485169490931692611a2b9285928792611a26926dffffffffffffffffffffffffffff169185916370a0823191602480820192602092909190829003018186803b1580156119ee57600080fd5b505afa158015611a02573d6000803e3d6000fd5b505050506040513d6020811015611a1857600080fd5b50519063ffffffff61226e16565b611fdb565b600854604080517f70a082310000000000000000000000000000000000000000000000000000000081523060048201529051611aca9284928792611a26926e01000000000000000000000000000090046dffffffffffffffffffffffffffff169173ffffffffffffffffffffffffffffffffffffffff8616916370a0823191602480820192602092909190829003018186803b1580156119ee57600080fd5b50506001600c5550565b60055473ffffffffffffffffffffffffffffffffffffffff1681565b60075473ffffffffffffffffffffffffffffffffffffffff1681565b42841015611b7b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f556e697377617056323a20455850495245440000000000000000000000000000604482015290519081900360640190fd5b60035473ffffffffffffffffffffffffffffffffffffffff80891660008181526004602090815260408083208054600180820190925582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98186015280840196909652958d166060860152608085018c905260a085019590955260c08085018b90528151808603909101815260e0850182528051908301207f19010000000000000000000000000000000000000000000000000000000000006101008601526101028501969096526101228085019690965280518085039096018652610142840180825286519683019690962095839052610162840180825286905260ff89166101828501526101a284018890526101c28401879052519193926101e2808201937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081019281900390910190855afa158015611cdc573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811615801590611d5757508873ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b611dc257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f556e697377617056323a20494e56414c49445f5349474e415455524500000000604482015290519081900360640190fd5b611dcd89898961259c565b505050505050505050565b600260209081526000928352604080842090915290825290205481565b600c54600114611e6657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f556e697377617056323a204c4f434b4544000000000000000000000000000000604482015290519081900360640190fd5b6000600c55600654604080517f70a082310000000000000000000000000000000000000000000000000000000081523060048201529051611fd49273ffffffffffffffffffffffffffffffffffffffff16916370a08231916024808301926020929190829003018186803b158015611edd57600080fd5b505afa158015611ef1573d6000803e3d6000fd5b505050506040513d6020811015611f0757600080fd5b5051600754604080517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152905173ffffffffffffffffffffffffffffffffffffffff909216916370a0823191602480820192602092909190829003018186803b158015611f7a57600080fd5b505afa158015611f8e573d6000803e3d6000fd5b505050506040513d6020811015611fa457600080fd5b50516008546dffffffffffffffffffffffffffff808216916e0100000000000000000000000000009004166122e0565b6001600c55565b604080518082018252601981527f7472616e7366657228616464726573732c75696e743235362900000000000000602091820152815173ffffffffffffffffffffffffffffffffffffffff85811660248301526044808301869052845180840390910181526064909201845291810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb000000000000000000000000000000000000000000000000000000001781529251815160009460609489169392918291908083835b602083106120e157805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016120a4565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114612143576040519150601f19603f3d011682016040523d82523d6000602084013e612148565b606091505b5091509150818015612176575080511580612176575080806020019051602081101561217357600080fd5b50515b6121e157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f556e697377617056323a205452414e534645525f4641494c4544000000000000604482015290519081900360640190fd5b5050505050565b60008115806122035750508082028282828161220057fe5b04145b610df657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f64732d6d6174682d6d756c2d6f766572666c6f77000000000000000000000000604482015290519081900360640190fd5b80820382811115610df657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601560248201527f64732d6d6174682d7375622d756e646572666c6f770000000000000000000000604482015290519081900360640190fd5b6dffffffffffffffffffffffffffff841180159061230c57506dffffffffffffffffffffffffffff8311155b61237757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f556e697377617056323a204f564552464c4f5700000000000000000000000000604482015290519081900360640190fd5b60085463ffffffff428116917c0100000000000000000000000000000000000000000000000000000000900481168203908116158015906123c757506dffffffffffffffffffffffffffff841615155b80156123e257506dffffffffffffffffffffffffffff831615155b15612492578063ffffffff16612425856123fb86612a57565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff169063ffffffff612a7b16565b600980547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff929092169290920201905563ffffffff8116612465846123fb87612a57565b600a80547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff92909216929092020190555b600880547fffffffffffffffffffffffffffffffffffff0000000000000000000000000000166dffffffffffffffffffffffffffff888116919091177fffffffff0000000000000000000000000000ffffffffffffffffffffffffffff166e0100000000000000000000000000008883168102919091177bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167c010000000000000000000000000000000000000000000000000000000063ffffffff871602179283905560408051848416815291909304909116602082015281517f1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1929181900390910190a1505050505050565b73ffffffffffffffffffffffffffffffffffffffff808416600081815260026020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b73ffffffffffffffffffffffffffffffffffffffff8316600090815260016020526040902054612641908263ffffffff61226e16565b73ffffffffffffffffffffffffffffffffffffffff8085166000908152600160205260408082209390935590841681522054612683908263ffffffff612abc16565b73ffffffffffffffffffffffffffffffffffffffff80841660008181526001602090815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b600080600560009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561275757600080fd5b505afa15801561276b573d6000803e3d6000fd5b505050506040513d602081101561278157600080fd5b5051600b5473ffffffffffffffffffffffffffffffffffffffff821615801594509192509061286457801561285f5760006127d86112576dffffffffffffffffffffffffffff88811690881663ffffffff6121e816565b905060006127e583612878565b90508082111561285c576000612813612804848463ffffffff61226e16565b6000549063ffffffff6121e816565b905060006128388361282c86600563ffffffff6121e816565b9063ffffffff612abc16565b9050600081838161284557fe5b04905080156128585761285887826128ca565b5050505b50505b612870565b8015612870576000600b555b505092915050565b600060038211156128bb575080600160028204015b818110156128b5578091506002818285816128a457fe5b0401816128ad57fe5b04905061288d565b506128c5565b81156128c5575060015b919050565b6000546128dd908263ffffffff612abc16565b600090815573ffffffffffffffffffffffffffffffffffffffff8316815260016020526040902054612915908263ffffffff612abc16565b73ffffffffffffffffffffffffffffffffffffffff831660008181526001602090815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b6000818310612989578161298b565b825b9392505050565b73ffffffffffffffffffffffffffffffffffffffff82166000908152600160205260409020546129c8908263ffffffff61226e16565b73ffffffffffffffffffffffffffffffffffffffff831660009081526001602052604081209190915554612a02908263ffffffff61226e16565b600090815560408051838152905173ffffffffffffffffffffffffffffffffffffffff8516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef919081900360200190a35050565b6dffffffffffffffffffffffffffff166e0100000000000000000000000000000290565b60006dffffffffffffffffffffffffffff82167bffffffffffffffffffffffffffffffffffffffffffffffffffffffff841681612ab457fe5b049392505050565b80820182811015610df657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f64732d6d6174682d6164642d6f766572666c6f77000000000000000000000000604482015290519081900360640190fdfe556e697377617056323a20494e53554646494349454e545f4f55545055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f494e5055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f4c4951554944495459556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4255524e4544556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4d494e544544a265627a7a7231582087d08c65e26d74fd696fae59984ecfe804b0ba9d1cc5d12275098414f90b2a1c64736f6c63430005100032454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429" 7 | // this _could_ go in constants, except that it would cost every consumer of the sdk the CPU to compute the hash 8 | // and load the JSON. 9 | const COMPUTED_INIT_CODE_HASH = keccak256(['bytes'], [`0x${bytecode}`]) 10 | console.log(COMPUTED_INIT_CODE_HASH) 11 | describe('constants', () => { 12 | describe('INIT_CODE_HASH', () => { 13 | it('matches computed bytecode hash', () => { 14 | expect(COMPUTED_INIT_CODE_HASH).toEqual(INIT_CODE_HASH) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/data.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, WETH, Token, Fetcher } from '../src' 2 | 3 | // TODO: replace the provider in these tests 4 | describe.skip('data', () => { 5 | it('Token', async () => { 6 | const token = await Fetcher.fetchTokenData(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F') // DAI 7 | expect(token.decimals).toEqual(18) 8 | }) 9 | 10 | it('Token:CACHE', async () => { 11 | const token = await Fetcher.fetchTokenData(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A') // DGD 12 | expect(token.decimals).toEqual(9) 13 | }) 14 | 15 | it('Pair', async () => { 16 | const token = new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18) // DAI 17 | const pair = await Fetcher.fetchPairData(WETH[ChainId.RINKEBY], token) 18 | expect(pair.liquidityToken.address).toEqual('0x8B22F85d0c844Cf793690F6D9DFE9F11Ddb35449') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/entities.test.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { ChainId, WETH as _WETH, TradeType, Rounding, Token, TokenAmount, Pair, Route, Trade } from '../src' 3 | 4 | const ADDRESSES = [ 5 | '0x0000000000000000000000000000000000000001', 6 | '0x0000000000000000000000000000000000000002', 7 | '0x0000000000000000000000000000000000000003' 8 | ] 9 | const CHAIN_ID = ChainId.RINKEBY 10 | const WETH = _WETH[ChainId.RINKEBY] 11 | const DECIMAL_PERMUTATIONS: [number, number, number][] = [ 12 | [0, 0, 0], 13 | [0, 9, 18], 14 | [18, 18, 18] 15 | ] 16 | 17 | function decimalize(amount: number, decimals: number): bigint { 18 | return BigInt(amount) * BigInt(10) ** BigInt(decimals) 19 | } 20 | 21 | describe('entities', () => { 22 | DECIMAL_PERMUTATIONS.forEach(decimals => { 23 | describe(`decimals permutation: ${decimals}`, () => { 24 | let tokens: Token[] 25 | it('Token', () => { 26 | tokens = ADDRESSES.map((address, i) => new Token(CHAIN_ID, address, decimals[i])) 27 | tokens.forEach((token, i) => { 28 | expect(token.chainId).toEqual(CHAIN_ID) 29 | expect(token.address).toEqual(ADDRESSES[i]) 30 | expect(token.decimals).toEqual(decimals[i]) 31 | }) 32 | }) 33 | 34 | let pairs: Pair[] 35 | it('Pair', () => { 36 | pairs = [ 37 | new Pair( 38 | new TokenAmount(tokens[0], decimalize(1, tokens[0].decimals)), 39 | new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)) 40 | ), 41 | new Pair( 42 | new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)), 43 | new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals)) 44 | ), 45 | new Pair( 46 | new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals)), 47 | new TokenAmount(WETH, decimalize(1234, WETH.decimals)) 48 | ) 49 | ] 50 | }) 51 | 52 | let route: Route 53 | it('Route', () => { 54 | route = new Route(pairs, tokens[0]) 55 | expect(route.pairs).toEqual(pairs) 56 | expect(route.path).toEqual(tokens.concat([WETH])) 57 | expect(route.input).toEqual(tokens[0]) 58 | expect(route.output).toEqual(WETH) 59 | }) 60 | 61 | it('Price:Route.midPrice', () => { 62 | invariant(route.input instanceof Token) 63 | invariant(route.output instanceof Token) 64 | expect(route.midPrice.quote(new TokenAmount(route.input, decimalize(1, route.input.decimals)))).toEqual( 65 | new TokenAmount(route.output, decimalize(1234, route.output.decimals)) 66 | ) 67 | expect( 68 | route.midPrice.invert().quote(new TokenAmount(route.output, decimalize(1234, route.output.decimals))) 69 | ).toEqual(new TokenAmount(route.input, decimalize(1, route.input.decimals))) 70 | 71 | expect(route.midPrice.toSignificant(1)).toEqual('1000') 72 | expect(route.midPrice.toSignificant(2)).toEqual('1200') 73 | expect(route.midPrice.toSignificant(3)).toEqual('1230') 74 | expect(route.midPrice.toSignificant(4)).toEqual('1234') 75 | expect(route.midPrice.toSignificant(5)).toEqual('1234') 76 | expect(route.midPrice.toSignificant(5, { groupSeparator: ',' })).toEqual('1,234') 77 | expect(route.midPrice.invert().toSignificant(1)).toEqual('0.0008') 78 | expect(route.midPrice.invert().toSignificant(2)).toEqual('0.00081') 79 | expect(route.midPrice.invert().toSignificant(3)).toEqual('0.00081') 80 | expect(route.midPrice.invert().toSignificant(4)).toEqual('0.0008104') 81 | expect(route.midPrice.invert().toSignificant(4, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103') 82 | expect(route.midPrice.invert().toSignificant(5)).toEqual('0.00081037') 83 | 84 | expect(route.midPrice.toFixed(0)).toEqual('1234') 85 | expect(route.midPrice.toFixed(1)).toEqual('1234.0') 86 | expect(route.midPrice.toFixed(2)).toEqual('1234.00') 87 | expect(route.midPrice.toFixed(2, { groupSeparator: ',' })).toEqual('1,234.00') 88 | expect(route.midPrice.invert().toFixed(0)).toEqual('0') 89 | expect(route.midPrice.invert().toFixed(1)).toEqual('0.0') 90 | expect(route.midPrice.invert().toFixed(2)).toEqual('0.00') 91 | expect(route.midPrice.invert().toFixed(3)).toEqual('0.001') 92 | expect(route.midPrice.invert().toFixed(4)).toEqual('0.0008') 93 | expect(route.midPrice.invert().toFixed(5)).toEqual('0.00081') 94 | expect(route.midPrice.invert().toFixed(6)).toEqual('0.000810') 95 | expect(route.midPrice.invert().toFixed(7)).toEqual('0.0008104') 96 | expect(route.midPrice.invert().toFixed(7, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103') 97 | expect(route.midPrice.invert().toFixed(8)).toEqual('0.00081037') 98 | }) 99 | 100 | describe('Trade', () => { 101 | let route: Route 102 | it('TradeType.EXACT_INPUT', () => { 103 | route = new Route( 104 | [ 105 | new Pair( 106 | new TokenAmount(tokens[1], decimalize(5, tokens[1].decimals)), 107 | new TokenAmount(WETH, decimalize(10, WETH.decimals)) 108 | ) 109 | ], 110 | tokens[1] 111 | ) 112 | const inputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)) 113 | const expectedOutputAmount = new TokenAmount(WETH, '1662497915624478906') 114 | const trade = new Trade(route, inputAmount, TradeType.EXACT_INPUT) 115 | expect(trade.route).toEqual(route) 116 | expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) 117 | expect(trade.inputAmount).toEqual(inputAmount) 118 | expect(trade.outputAmount).toEqual(expectedOutputAmount) 119 | 120 | expect(trade.executionPrice.toSignificant(18)).toEqual('1.66249791562447891') 121 | expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601504513540621866') 122 | expect(trade.executionPrice.quote(inputAmount)).toEqual(expectedOutputAmount) 123 | expect(trade.executionPrice.invert().quote(expectedOutputAmount)).toEqual(inputAmount) 124 | 125 | expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38958368072925352') 126 | expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.71964') 127 | 128 | expect(trade.priceImpact.toSignificant(18)).toEqual('16.8751042187760547') 129 | }) 130 | 131 | it('TradeType.EXACT_OUTPUT', () => { 132 | const outputAmount = new TokenAmount(WETH, '1662497915624478906') 133 | const expectedInputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)) 134 | const trade = new Trade(route, outputAmount, TradeType.EXACT_OUTPUT) 135 | expect(trade.route).toEqual(route) 136 | expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT) 137 | expect(trade.outputAmount).toEqual(outputAmount) 138 | expect(trade.inputAmount).toEqual(expectedInputAmount) 139 | 140 | expect(trade.executionPrice.toSignificant(18)).toEqual('1.66249791562447891') 141 | expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601504513540621866') 142 | expect(trade.executionPrice.quote(expectedInputAmount)).toEqual(outputAmount) 143 | expect(trade.executionPrice.invert().quote(outputAmount)).toEqual(expectedInputAmount) 144 | 145 | expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38958368072925352') 146 | expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.71964') 147 | 148 | expect(trade.priceImpact.toSignificant(18)).toEqual('16.8751042187760547') 149 | }) 150 | 151 | it('minimum TradeType.EXACT_INPUT', () => { 152 | if ([9, 18].includes(tokens[1].decimals)) { 153 | const route = new Route( 154 | [ 155 | new Pair( 156 | new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)), 157 | new TokenAmount( 158 | WETH, 159 | decimalize(10, WETH.decimals) + 160 | (tokens[1].decimals === 9 ? BigInt('30090280812437312') : BigInt('30090270812437322')) 161 | ) 162 | ) 163 | ], 164 | tokens[1] 165 | ) 166 | const outputAmount = new TokenAmount(tokens[1], '1') 167 | const trade = new Trade(route, outputAmount, TradeType.EXACT_INPUT) 168 | 169 | expect(trade.priceImpact.toSignificant(18)).toEqual( 170 | tokens[1].decimals === 9 ? '0.300000099400899902' : '0.3000000000000001' 171 | ) 172 | } 173 | }) 174 | }) 175 | 176 | it('TokenAmount', () => { 177 | const amount = new TokenAmount(WETH, '1234567000000000000000') 178 | expect(amount.toExact()).toEqual('1234.567') 179 | expect(amount.toExact({ groupSeparator: ',' })).toEqual('1,234.567') 180 | }) 181 | }) 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /test/fraction.test.ts: -------------------------------------------------------------------------------- 1 | import { Fraction } from '../src' 2 | import JSBI from 'jsbi' 3 | 4 | describe.only('Fraction', () => { 5 | describe('#quotient', () => { 6 | it('floor division', () => { 7 | expect(new Fraction(JSBI.BigInt(8), JSBI.BigInt(3)).quotient).toEqual(JSBI.BigInt(2)) // one below 8 | expect(new Fraction(JSBI.BigInt(12), JSBI.BigInt(4)).quotient).toEqual(JSBI.BigInt(3)) // exact 9 | expect(new Fraction(JSBI.BigInt(16), JSBI.BigInt(5)).quotient).toEqual(JSBI.BigInt(3)) // one above 10 | }) 11 | }) 12 | describe('#remainder', () => { 13 | it('returns fraction after divison', () => { 14 | expect(new Fraction(JSBI.BigInt(8), JSBI.BigInt(3)).remainder).toEqual( 15 | new Fraction(JSBI.BigInt(2), JSBI.BigInt(3)) 16 | ) 17 | expect(new Fraction(JSBI.BigInt(12), JSBI.BigInt(4)).remainder).toEqual( 18 | new Fraction(JSBI.BigInt(0), JSBI.BigInt(4)) 19 | ) 20 | expect(new Fraction(JSBI.BigInt(16), JSBI.BigInt(5)).remainder).toEqual( 21 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(5)) 22 | ) 23 | }) 24 | }) 25 | describe('#invert', () => { 26 | it('flips num and denom', () => { 27 | expect(new Fraction(JSBI.BigInt(5), JSBI.BigInt(10)).invert().numerator).toEqual(JSBI.BigInt(10)) 28 | expect(new Fraction(JSBI.BigInt(5), JSBI.BigInt(10)).invert().denominator).toEqual(JSBI.BigInt(5)) 29 | }) 30 | }) 31 | describe('#add', () => { 32 | it('multiples denoms and adds nums', () => { 33 | expect(new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).add(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12)))).toEqual( 34 | new Fraction(JSBI.BigInt(52), JSBI.BigInt(120)) 35 | ) 36 | }) 37 | 38 | it('same denom', () => { 39 | expect(new Fraction(JSBI.BigInt(1), JSBI.BigInt(5)).add(new Fraction(JSBI.BigInt(2), JSBI.BigInt(5)))).toEqual( 40 | new Fraction(JSBI.BigInt(3), JSBI.BigInt(5)) 41 | ) 42 | }) 43 | }) 44 | describe('#subtract', () => { 45 | it('multiples denoms and subtracts nums', () => { 46 | expect( 47 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).subtract(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 48 | ).toEqual(new Fraction(JSBI.BigInt(-28), JSBI.BigInt(120))) 49 | }) 50 | it('same denom', () => { 51 | expect( 52 | new Fraction(JSBI.BigInt(3), JSBI.BigInt(5)).subtract(new Fraction(JSBI.BigInt(2), JSBI.BigInt(5))) 53 | ).toEqual(new Fraction(JSBI.BigInt(1), JSBI.BigInt(5))) 54 | }) 55 | }) 56 | describe('#lessThan', () => { 57 | it('correct', () => { 58 | expect( 59 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).lessThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 60 | ).toBe(true) 61 | expect(new Fraction(JSBI.BigInt(1), JSBI.BigInt(3)).lessThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12)))).toBe( 62 | false 63 | ) 64 | expect( 65 | new Fraction(JSBI.BigInt(5), JSBI.BigInt(12)).lessThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 66 | ).toBe(false) 67 | }) 68 | }) 69 | describe('#equalTo', () => { 70 | it('correct', () => { 71 | expect(new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).equalTo(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12)))).toBe( 72 | false 73 | ) 74 | expect(new Fraction(JSBI.BigInt(1), JSBI.BigInt(3)).equalTo(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12)))).toBe( 75 | true 76 | ) 77 | expect(new Fraction(JSBI.BigInt(5), JSBI.BigInt(12)).equalTo(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12)))).toBe( 78 | false 79 | ) 80 | }) 81 | }) 82 | describe('#greaterThan', () => { 83 | it('correct', () => { 84 | expect( 85 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).greaterThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 86 | ).toBe(false) 87 | expect( 88 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(3)).greaterThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 89 | ).toBe(false) 90 | expect( 91 | new Fraction(JSBI.BigInt(5), JSBI.BigInt(12)).greaterThan(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 92 | ).toBe(true) 93 | }) 94 | }) 95 | describe('#multiplty', () => { 96 | it('correct', () => { 97 | expect( 98 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).multiply(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 99 | ).toEqual(new Fraction(JSBI.BigInt(4), JSBI.BigInt(120))) 100 | expect( 101 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(3)).multiply(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 102 | ).toEqual(new Fraction(JSBI.BigInt(4), JSBI.BigInt(36))) 103 | expect( 104 | new Fraction(JSBI.BigInt(5), JSBI.BigInt(12)).multiply(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 105 | ).toEqual(new Fraction(JSBI.BigInt(20), JSBI.BigInt(144))) 106 | }) 107 | }) 108 | describe('#divide', () => { 109 | it('correct', () => { 110 | expect( 111 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(10)).divide(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 112 | ).toEqual(new Fraction(JSBI.BigInt(12), JSBI.BigInt(40))) 113 | expect( 114 | new Fraction(JSBI.BigInt(1), JSBI.BigInt(3)).divide(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 115 | ).toEqual(new Fraction(JSBI.BigInt(12), JSBI.BigInt(12))) 116 | expect( 117 | new Fraction(JSBI.BigInt(5), JSBI.BigInt(12)).divide(new Fraction(JSBI.BigInt(4), JSBI.BigInt(12))) 118 | ).toEqual(new Fraction(JSBI.BigInt(60), JSBI.BigInt(48))) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /test/miscellaneous.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Token, TokenAmount, Pair, InsufficientInputAmountError } from '../src' 2 | import { sortedInsert } from '../src/utils' 3 | 4 | describe('miscellaneous', () => { 5 | it('getLiquidityMinted:0', async () => { 6 | const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18) 7 | const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18) 8 | const pair = new Pair(new TokenAmount(tokenA, '0'), new TokenAmount(tokenB, '0')) 9 | 10 | expect(() => { 11 | pair.getLiquidityMinted( 12 | new TokenAmount(pair.liquidityToken, '0'), 13 | new TokenAmount(tokenA, '1000'), 14 | new TokenAmount(tokenB, '1000') 15 | ) 16 | }).toThrow(InsufficientInputAmountError) 17 | 18 | expect(() => { 19 | pair.getLiquidityMinted( 20 | new TokenAmount(pair.liquidityToken, '0'), 21 | new TokenAmount(tokenA, '1000000'), 22 | new TokenAmount(tokenB, '1') 23 | ) 24 | }).toThrow(InsufficientInputAmountError) 25 | 26 | const liquidity = pair.getLiquidityMinted( 27 | new TokenAmount(pair.liquidityToken, '0'), 28 | new TokenAmount(tokenA, '1001'), 29 | new TokenAmount(tokenB, '1001') 30 | ) 31 | 32 | expect(liquidity.raw.toString()).toEqual('1') 33 | }) 34 | 35 | it('getLiquidityMinted:!0', async () => { 36 | const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18) 37 | const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18) 38 | const pair = new Pair(new TokenAmount(tokenA, '10000'), new TokenAmount(tokenB, '10000')) 39 | 40 | expect( 41 | pair 42 | .getLiquidityMinted( 43 | new TokenAmount(pair.liquidityToken, '10000'), 44 | new TokenAmount(tokenA, '2000'), 45 | new TokenAmount(tokenB, '2000') 46 | ) 47 | .raw.toString() 48 | ).toEqual('2000') 49 | }) 50 | 51 | it('getLiquidityValue:!feeOn', async () => { 52 | const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18) 53 | const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18) 54 | const pair = new Pair(new TokenAmount(tokenA, '1000'), new TokenAmount(tokenB, '1000')) 55 | 56 | { 57 | const liquidityValue = pair.getLiquidityValue( 58 | tokenA, 59 | new TokenAmount(pair.liquidityToken, '1000'), 60 | new TokenAmount(pair.liquidityToken, '1000'), 61 | false 62 | ) 63 | expect(liquidityValue.token.equals(tokenA)).toBe(true) 64 | expect(liquidityValue.raw.toString()).toBe('1000') 65 | } 66 | 67 | // 500 68 | { 69 | const liquidityValue = pair.getLiquidityValue( 70 | tokenA, 71 | new TokenAmount(pair.liquidityToken, '1000'), 72 | new TokenAmount(pair.liquidityToken, '500'), 73 | false 74 | ) 75 | expect(liquidityValue.token.equals(tokenA)).toBe(true) 76 | expect(liquidityValue.raw.toString()).toBe('500') 77 | } 78 | 79 | // tokenB 80 | { 81 | const liquidityValue = pair.getLiquidityValue( 82 | tokenB, 83 | new TokenAmount(pair.liquidityToken, '1000'), 84 | new TokenAmount(pair.liquidityToken, '1000'), 85 | false 86 | ) 87 | expect(liquidityValue.token.equals(tokenB)).toBe(true) 88 | expect(liquidityValue.raw.toString()).toBe('1000') 89 | } 90 | }) 91 | 92 | it('getLiquidityValue:feeOn', async () => { 93 | const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18) 94 | const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18) 95 | const pair = new Pair(new TokenAmount(tokenA, '1000'), new TokenAmount(tokenB, '1000')) 96 | 97 | const liquidityValue = pair.getLiquidityValue( 98 | tokenA, 99 | new TokenAmount(pair.liquidityToken, '500'), 100 | new TokenAmount(pair.liquidityToken, '500'), 101 | true, 102 | '250000' // 500 ** 2 103 | ) 104 | expect(liquidityValue.token.equals(tokenA)).toBe(true) 105 | expect(liquidityValue.raw.toString()).toBe('917') // ceiling(1000 - (500 * (1 / 6))) 106 | }) 107 | 108 | describe('#sortedInsert', () => { 109 | const comp = (a: number, b: number) => a - b 110 | 111 | it('throws if maxSize is 0', () => { 112 | expect(() => sortedInsert([], 1, 0, comp)).toThrow('MAX_SIZE_ZERO') 113 | }) 114 | 115 | it('throws if items.length > maxSize', () => { 116 | expect(() => sortedInsert([1, 2], 1, 1, comp)).toThrow('ITEMS_SIZE') 117 | }) 118 | 119 | it('adds if empty', () => { 120 | const arr: number[] = [] 121 | expect(sortedInsert(arr, 3, 2, comp)).toEqual(null) 122 | expect(arr).toEqual([3]) 123 | }) 124 | 125 | it('adds if not full', () => { 126 | const arr: number[] = [1, 5] 127 | expect(sortedInsert(arr, 3, 3, comp)).toEqual(null) 128 | expect(arr).toEqual([1, 3, 5]) 129 | }) 130 | 131 | it('adds if will not be full after', () => { 132 | const arr: number[] = [1] 133 | expect(sortedInsert(arr, 0, 3, comp)).toEqual(null) 134 | expect(arr).toEqual([0, 1]) 135 | }) 136 | 137 | it('returns add if sorts after last', () => { 138 | const arr = [1, 2, 3] 139 | expect(sortedInsert(arr, 4, 3, comp)).toEqual(4) 140 | expect(arr).toEqual([1, 2, 3]) 141 | }) 142 | 143 | it('removes from end if full', () => { 144 | const arr = [1, 3, 4] 145 | expect(sortedInsert(arr, 2, 3, comp)).toEqual(4) 146 | expect(arr).toEqual([1, 2, 3]) 147 | }) 148 | 149 | it('uses comparator', () => { 150 | const arr = [4, 2, 1] 151 | expect(sortedInsert(arr, 3, 3, (a, b) => comp(a, b) * -1)).toEqual(1) 152 | expect(arr).toEqual([4, 3, 2]) 153 | }) 154 | 155 | describe('maxSize of 1', () => { 156 | it('empty add', () => { 157 | const arr: number[] = [] 158 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(null) 159 | expect(arr).toEqual([3]) 160 | }) 161 | it('full add greater', () => { 162 | const arr: number[] = [2] 163 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(3) 164 | expect(arr).toEqual([2]) 165 | }) 166 | it('full add lesser', () => { 167 | const arr: number[] = [4] 168 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(4) 169 | expect(arr).toEqual([3]) 170 | }) 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /test/pair.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Token, Pair, TokenAmount, WETH, Price } from '../src' 2 | 3 | describe('Pair', () => { 4 | const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 18, 'USDC', 'USD Coin') 5 | const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'DAI Stablecoin') 6 | 7 | describe('constructor', () => { 8 | it('cannot be used for tokens on different chains', () => { 9 | expect(() => new Pair(new TokenAmount(USDC, '100'), new TokenAmount(WETH[ChainId.RINKEBY], '100'))).toThrow( 10 | 'CHAIN_IDS' 11 | ) 12 | }) 13 | }) 14 | 15 | describe('#getAddress', () => { 16 | it('returns the correct address', () => { 17 | expect(Pair.getAddress(USDC, DAI)).toEqual('0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5') 18 | }) 19 | }) 20 | 21 | describe('#token0', () => { 22 | it('always is the token that sorts before', () => { 23 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).token0).toEqual(DAI) 24 | expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).token0).toEqual(DAI) 25 | }) 26 | }) 27 | describe('#token1', () => { 28 | it('always is the token that sorts after', () => { 29 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).token1).toEqual(USDC) 30 | expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).token1).toEqual(USDC) 31 | }) 32 | }) 33 | describe('#reserve0', () => { 34 | it('always comes from the token that sorts before', () => { 35 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserve0).toEqual( 36 | new TokenAmount(DAI, '101') 37 | ) 38 | expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserve0).toEqual( 39 | new TokenAmount(DAI, '101') 40 | ) 41 | }) 42 | }) 43 | describe('#reserve1', () => { 44 | it('always comes from the token that sorts after', () => { 45 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserve1).toEqual( 46 | new TokenAmount(USDC, '100') 47 | ) 48 | expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserve1).toEqual( 49 | new TokenAmount(USDC, '100') 50 | ) 51 | }) 52 | }) 53 | 54 | describe('#token0Price', () => { 55 | it('returns price of token0 in terms of token1', () => { 56 | expect(new Pair(new TokenAmount(USDC, '101'), new TokenAmount(DAI, '100')).token0Price).toEqual( 57 | new Price(DAI, USDC, '100', '101') 58 | ) 59 | expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '101')).token0Price).toEqual( 60 | new Price(DAI, USDC, '100', '101') 61 | ) 62 | }) 63 | }) 64 | 65 | describe('#token1Price', () => { 66 | it('returns price of token1 in terms of token0', () => { 67 | expect(new Pair(new TokenAmount(USDC, '101'), new TokenAmount(DAI, '100')).token1Price).toEqual( 68 | new Price(USDC, DAI, '101', '100') 69 | ) 70 | expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '101')).token1Price).toEqual( 71 | new Price(USDC, DAI, '101', '100') 72 | ) 73 | }) 74 | }) 75 | 76 | describe('#priceOf', () => { 77 | const pair = new Pair(new TokenAmount(USDC, '101'), new TokenAmount(DAI, '100')) 78 | it('returns price of token in terms of other token', () => { 79 | expect(pair.priceOf(DAI)).toEqual(pair.token0Price) 80 | expect(pair.priceOf(USDC)).toEqual(pair.token1Price) 81 | }) 82 | 83 | it('throws if invalid token', () => { 84 | expect(() => pair.priceOf(WETH[ChainId.MAINNET])).toThrow('TOKEN') 85 | }) 86 | }) 87 | 88 | describe('#reserveOf', () => { 89 | it('returns reserves of the given token', () => { 90 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '101')).reserveOf(USDC)).toEqual( 91 | new TokenAmount(USDC, '100') 92 | ) 93 | expect(new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserveOf(USDC)).toEqual( 94 | new TokenAmount(USDC, '100') 95 | ) 96 | }) 97 | 98 | it('throws if not in the pair', () => { 99 | expect(() => 100 | new Pair(new TokenAmount(DAI, '101'), new TokenAmount(USDC, '100')).reserveOf(WETH[ChainId.MAINNET]) 101 | ).toThrow('TOKEN') 102 | }) 103 | }) 104 | 105 | describe('#chainId', () => { 106 | it('returns the token0 chainId', () => { 107 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).chainId).toEqual(ChainId.MAINNET) 108 | expect(new Pair(new TokenAmount(DAI, '100'), new TokenAmount(USDC, '100')).chainId).toEqual(ChainId.MAINNET) 109 | }) 110 | }) 111 | describe('#involvesToken', () => { 112 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(USDC)).toEqual(true) 113 | expect(new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(DAI)).toEqual(true) 114 | expect( 115 | new Pair(new TokenAmount(USDC, '100'), new TokenAmount(DAI, '100')).involvesToken(WETH[ChainId.MAINNET]) 116 | ).toEqual(false) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /test/route.test.ts: -------------------------------------------------------------------------------- 1 | import { Token, WETH, ChainId, Pair, TokenAmount, Route, ETHER } from '../src' 2 | 3 | describe('Route', () => { 4 | const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0') 5 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18, 't1') 6 | const weth = WETH[ChainId.MAINNET] 7 | const pair_0_1 = new Pair(new TokenAmount(token0, '100'), new TokenAmount(token1, '200')) 8 | const pair_0_weth = new Pair(new TokenAmount(token0, '100'), new TokenAmount(weth, '100')) 9 | const pair_1_weth = new Pair(new TokenAmount(token1, '175'), new TokenAmount(weth, '100')) 10 | 11 | it('constructs a path from the tokens', () => { 12 | const route = new Route([pair_0_1], token0) 13 | expect(route.pairs).toEqual([pair_0_1]) 14 | expect(route.path).toEqual([token0, token1]) 15 | expect(route.input).toEqual(token0) 16 | expect(route.output).toEqual(token1) 17 | expect(route.chainId).toEqual(ChainId.MAINNET) 18 | }) 19 | 20 | it('can have a token as both input and output', () => { 21 | const route = new Route([pair_0_weth, pair_0_1, pair_1_weth], weth) 22 | expect(route.pairs).toEqual([pair_0_weth, pair_0_1, pair_1_weth]) 23 | expect(route.input).toEqual(weth) 24 | expect(route.output).toEqual(weth) 25 | }) 26 | 27 | it('supports ether input', () => { 28 | const route = new Route([pair_0_weth], ETHER) 29 | expect(route.pairs).toEqual([pair_0_weth]) 30 | expect(route.input).toEqual(ETHER) 31 | expect(route.output).toEqual(token0) 32 | }) 33 | 34 | it('supports ether output', () => { 35 | const route = new Route([pair_0_weth], token0, ETHER) 36 | expect(route.pairs).toEqual([pair_0_weth]) 37 | expect(route.input).toEqual(token0) 38 | expect(route.output).toEqual(ETHER) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/router.test.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { ChainId, CurrencyAmount, ETHER, Pair, Percent, Route, Router, Token, TokenAmount, Trade, WETH } from '../src' 3 | import JSBI from 'jsbi' 4 | 5 | function checkDeadline(deadline: string[] | string): void { 6 | expect(typeof deadline).toBe('string') 7 | invariant(typeof deadline === 'string') 8 | // less than 5 seconds on the deadline 9 | expect(new Date().getTime() / 1000 - parseInt(deadline)).toBeLessThanOrEqual(5) 10 | } 11 | 12 | describe('Router', () => { 13 | const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0') 14 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18, 't1') 15 | 16 | const pair_0_1 = new Pair(new TokenAmount(token0, JSBI.BigInt(1000)), new TokenAmount(token1, JSBI.BigInt(1000))) 17 | 18 | const pair_weth_0 = new Pair(new TokenAmount(WETH[ChainId.MAINNET], '1000'), new TokenAmount(token0, '1000')) 19 | 20 | describe('#swapCallParameters', () => { 21 | describe('exact in', () => { 22 | it('ether to token1', () => { 23 | const result = Router.swapCallParameters( 24 | Trade.exactIn(new Route([pair_weth_0, pair_0_1], ETHER, token1), CurrencyAmount.ether(JSBI.BigInt(100))), 25 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 26 | ) 27 | expect(result.methodName).toEqual('swapExactETHForTokens') 28 | expect(result.args.slice(0, -1)).toEqual([ 29 | '0x51', 30 | [WETH[ChainId.MAINNET].address, token0.address, token1.address], 31 | '0x0000000000000000000000000000000000000004' 32 | ]) 33 | expect(result.value).toEqual('0x64') 34 | checkDeadline(result.args[result.args.length - 1]) 35 | }) 36 | it('token1 to ether', () => { 37 | const result = Router.swapCallParameters( 38 | Trade.exactIn(new Route([pair_0_1, pair_weth_0], token1, ETHER), new TokenAmount(token1, JSBI.BigInt(100))), 39 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 40 | ) 41 | expect(result.methodName).toEqual('swapExactTokensForETH') 42 | expect(result.args.slice(0, -1)).toEqual([ 43 | '0x64', 44 | '0x51', 45 | [token1.address, token0.address, WETH[ChainId.MAINNET].address], 46 | '0x0000000000000000000000000000000000000004' 47 | ]) 48 | expect(result.value).toEqual('0x0') 49 | checkDeadline(result.args[result.args.length - 1]) 50 | }) 51 | it('token0 to token1', () => { 52 | const result = Router.swapCallParameters( 53 | Trade.exactIn(new Route([pair_0_1], token0, token1), new TokenAmount(token0, JSBI.BigInt(100))), 54 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 55 | ) 56 | expect(result.methodName).toEqual('swapExactTokensForTokens') 57 | expect(result.args.slice(0, -1)).toEqual([ 58 | '0x64', 59 | '0x59', 60 | [token0.address, token1.address], 61 | '0x0000000000000000000000000000000000000004' 62 | ]) 63 | expect(result.value).toEqual('0x0') 64 | checkDeadline(result.args[result.args.length - 1]) 65 | }) 66 | }) 67 | describe('exact out', () => { 68 | it('ether to token1', () => { 69 | const result = Router.swapCallParameters( 70 | Trade.exactOut(new Route([pair_weth_0, pair_0_1], ETHER, token1), new TokenAmount(token1, JSBI.BigInt(100))), 71 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 72 | ) 73 | expect(result.methodName).toEqual('swapETHForExactTokens') 74 | expect(result.args.slice(0, -1)).toEqual([ 75 | '0x64', 76 | [WETH[ChainId.MAINNET].address, token0.address, token1.address], 77 | '0x0000000000000000000000000000000000000004' 78 | ]) 79 | expect(result.value).toEqual('0x80') 80 | checkDeadline(result.args[result.args.length - 1]) 81 | }) 82 | it('token1 to ether', () => { 83 | const result = Router.swapCallParameters( 84 | Trade.exactOut(new Route([pair_0_1, pair_weth_0], token1, ETHER), CurrencyAmount.ether(JSBI.BigInt(100))), 85 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 86 | ) 87 | expect(result.methodName).toEqual('swapTokensForExactETH') 88 | expect(result.args.slice(0, -1)).toEqual([ 89 | '0x64', 90 | '0x80', 91 | [token1.address, token0.address, WETH[ChainId.MAINNET].address], 92 | '0x0000000000000000000000000000000000000004' 93 | ]) 94 | expect(result.value).toEqual('0x0') 95 | checkDeadline(result.args[result.args.length - 1]) 96 | }) 97 | it('token0 to token1', () => { 98 | const result = Router.swapCallParameters( 99 | Trade.exactOut(new Route([pair_0_1], token0, token1), new TokenAmount(token1, JSBI.BigInt(100))), 100 | { ttl: 50, recipient: '0x0000000000000000000000000000000000000004', allowedSlippage: new Percent('1', '100') } 101 | ) 102 | expect(result.methodName).toEqual('swapTokensForExactTokens') 103 | expect(result.args.slice(0, -1)).toEqual([ 104 | '0x64', 105 | '0x71', 106 | [token0.address, token1.address], 107 | '0x0000000000000000000000000000000000000004' 108 | ]) 109 | expect(result.value).toEqual('0x0') 110 | checkDeadline(result.args[result.args.length - 1]) 111 | }) 112 | }) 113 | describe('supporting fee on transfer', () => { 114 | describe('exact in', () => { 115 | it('ether to token1', () => { 116 | const result = Router.swapCallParameters( 117 | Trade.exactIn(new Route([pair_weth_0, pair_0_1], ETHER, token1), CurrencyAmount.ether(JSBI.BigInt(100))), 118 | { 119 | ttl: 50, 120 | recipient: '0x0000000000000000000000000000000000000004', 121 | allowedSlippage: new Percent('1', '100'), 122 | feeOnTransfer: true 123 | } 124 | ) 125 | expect(result.methodName).toEqual('swapExactETHForTokensSupportingFeeOnTransferTokens') 126 | expect(result.args.slice(0, -1)).toEqual([ 127 | '0x51', 128 | [WETH[ChainId.MAINNET].address, token0.address, token1.address], 129 | '0x0000000000000000000000000000000000000004' 130 | ]) 131 | expect(result.value).toEqual('0x64') 132 | checkDeadline(result.args[result.args.length - 1]) 133 | }) 134 | it('token1 to ether', () => { 135 | const result = Router.swapCallParameters( 136 | Trade.exactIn(new Route([pair_0_1, pair_weth_0], token1, ETHER), new TokenAmount(token1, JSBI.BigInt(100))), 137 | { 138 | ttl: 50, 139 | recipient: '0x0000000000000000000000000000000000000004', 140 | allowedSlippage: new Percent('1', '100'), 141 | feeOnTransfer: true 142 | } 143 | ) 144 | expect(result.methodName).toEqual('swapExactTokensForETHSupportingFeeOnTransferTokens') 145 | expect(result.args.slice(0, -1)).toEqual([ 146 | '0x64', 147 | '0x51', 148 | [token1.address, token0.address, WETH[ChainId.MAINNET].address], 149 | '0x0000000000000000000000000000000000000004' 150 | ]) 151 | expect(result.value).toEqual('0x0') 152 | checkDeadline(result.args[result.args.length - 1]) 153 | }) 154 | it('token0 to token1', () => { 155 | const result = Router.swapCallParameters( 156 | Trade.exactIn(new Route([pair_0_1], token0, token1), new TokenAmount(token0, JSBI.BigInt(100))), 157 | { 158 | ttl: 50, 159 | recipient: '0x0000000000000000000000000000000000000004', 160 | allowedSlippage: new Percent('1', '100'), 161 | feeOnTransfer: true 162 | } 163 | ) 164 | expect(result.methodName).toEqual('swapExactTokensForTokensSupportingFeeOnTransferTokens') 165 | expect(result.args.slice(0, -1)).toEqual([ 166 | '0x64', 167 | '0x59', 168 | [token0.address, token1.address], 169 | '0x0000000000000000000000000000000000000004' 170 | ]) 171 | expect(result.value).toEqual('0x0') 172 | checkDeadline(result.args[result.args.length - 1]) 173 | }) 174 | }) 175 | describe('exact out', () => { 176 | it('ether to token1', () => { 177 | expect(() => 178 | Router.swapCallParameters( 179 | Trade.exactOut( 180 | new Route([pair_weth_0, pair_0_1], ETHER, token1), 181 | new TokenAmount(token1, JSBI.BigInt(100)) 182 | ), 183 | { 184 | ttl: 50, 185 | recipient: '0x0000000000000000000000000000000000000004', 186 | allowedSlippage: new Percent('1', '100'), 187 | feeOnTransfer: true 188 | } 189 | ) 190 | ).toThrow('EXACT_OUT_FOT') 191 | }) 192 | it('token1 to ether', () => { 193 | expect(() => 194 | Router.swapCallParameters( 195 | Trade.exactOut(new Route([pair_0_1, pair_weth_0], token1, ETHER), CurrencyAmount.ether(JSBI.BigInt(100))), 196 | { 197 | ttl: 50, 198 | recipient: '0x0000000000000000000000000000000000000004', 199 | allowedSlippage: new Percent('1', '100'), 200 | feeOnTransfer: true 201 | } 202 | ) 203 | ).toThrow('EXACT_OUT_FOT') 204 | }) 205 | it('token0 to token1', () => { 206 | expect(() => 207 | Router.swapCallParameters( 208 | Trade.exactOut(new Route([pair_0_1], token0, token1), new TokenAmount(token1, JSBI.BigInt(100))), 209 | { 210 | ttl: 50, 211 | recipient: '0x0000000000000000000000000000000000000004', 212 | allowedSlippage: new Percent('1', '100'), 213 | feeOnTransfer: true 214 | } 215 | ) 216 | ).toThrow('EXACT_OUT_FOT') 217 | }) 218 | }) 219 | }) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /test/token.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Token } from '../src' 2 | 3 | describe('Token', () => { 4 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 5 | const ADDRESS_TWO = '0x0000000000000000000000000000000000000002' 6 | 7 | describe('#equals', () => { 8 | it('fails if address differs', () => { 9 | expect(new Token(ChainId.MAINNET, ADDRESS_ONE, 18).equals(new Token(ChainId.MAINNET, ADDRESS_TWO, 18))).toBe( 10 | false 11 | ) 12 | }) 13 | 14 | it('false if chain id differs', () => { 15 | expect(new Token(ChainId.ROPSTEN, ADDRESS_ONE, 18).equals(new Token(ChainId.MAINNET, ADDRESS_ONE, 18))).toBe( 16 | false 17 | ) 18 | }) 19 | 20 | it('true if only decimals differs', () => { 21 | expect(new Token(ChainId.MAINNET, ADDRESS_ONE, 9).equals(new Token(ChainId.MAINNET, ADDRESS_ONE, 18))).toBe(true) 22 | }) 23 | 24 | it('true if address is the same', () => { 25 | expect(new Token(ChainId.MAINNET, ADDRESS_ONE, 18).equals(new Token(ChainId.MAINNET, ADDRESS_ONE, 18))).toBe(true) 26 | }) 27 | 28 | it('true on reference equality', () => { 29 | const token = new Token(ChainId.MAINNET, ADDRESS_ONE, 18) 30 | expect(token.equals(token)).toBe(true) 31 | }) 32 | 33 | it('true even if name/symbol/decimals differ', () => { 34 | const tokenA = new Token(ChainId.MAINNET, ADDRESS_ONE, 9, 'abc', 'def') 35 | const tokenB = new Token(ChainId.MAINNET, ADDRESS_ONE, 18, 'ghi', 'jkl') 36 | expect(tokenA.equals(tokenB)).toBe(true) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/trade.test.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { 3 | ChainId, 4 | ETHER, 5 | CurrencyAmount, 6 | Pair, 7 | Percent, 8 | Route, 9 | Token, 10 | TokenAmount, 11 | Trade, 12 | TradeType, 13 | WETH 14 | } from '../src' 15 | 16 | describe('Trade', () => { 17 | const token0 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18, 't0') 18 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18, 't1') 19 | const token2 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000003', 18, 't2') 20 | const token3 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000004', 18, 't3') 21 | 22 | const pair_0_1 = new Pair(new TokenAmount(token0, JSBI.BigInt(1000)), new TokenAmount(token1, JSBI.BigInt(1000))) 23 | const pair_0_2 = new Pair(new TokenAmount(token0, JSBI.BigInt(1000)), new TokenAmount(token2, JSBI.BigInt(1100))) 24 | const pair_0_3 = new Pair(new TokenAmount(token0, JSBI.BigInt(1000)), new TokenAmount(token3, JSBI.BigInt(900))) 25 | const pair_1_2 = new Pair(new TokenAmount(token1, JSBI.BigInt(1200)), new TokenAmount(token2, JSBI.BigInt(1000))) 26 | const pair_1_3 = new Pair(new TokenAmount(token1, JSBI.BigInt(1200)), new TokenAmount(token3, JSBI.BigInt(1300))) 27 | 28 | const pair_weth_0 = new Pair( 29 | new TokenAmount(WETH[ChainId.MAINNET], JSBI.BigInt(1000)), 30 | new TokenAmount(token0, JSBI.BigInt(1000)) 31 | ) 32 | 33 | const empty_pair_0_1 = new Pair(new TokenAmount(token0, JSBI.BigInt(0)), new TokenAmount(token1, JSBI.BigInt(0))) 34 | 35 | it('can be constructed with ETHER as input', () => { 36 | const trade = new Trade( 37 | new Route([pair_weth_0], ETHER), 38 | CurrencyAmount.ether(JSBI.BigInt(100)), 39 | TradeType.EXACT_INPUT 40 | ) 41 | expect(trade.inputAmount.currency).toEqual(ETHER) 42 | expect(trade.outputAmount.currency).toEqual(token0) 43 | }) 44 | it('can be constructed with ETHER as input for exact output', () => { 45 | const trade = new Trade( 46 | new Route([pair_weth_0], ETHER, token0), 47 | new TokenAmount(token0, JSBI.BigInt(100)), 48 | TradeType.EXACT_OUTPUT 49 | ) 50 | expect(trade.inputAmount.currency).toEqual(ETHER) 51 | expect(trade.outputAmount.currency).toEqual(token0) 52 | }) 53 | 54 | it('can be constructed with ETHER as output', () => { 55 | const trade = new Trade( 56 | new Route([pair_weth_0], token0, ETHER), 57 | CurrencyAmount.ether(JSBI.BigInt(100)), 58 | TradeType.EXACT_OUTPUT 59 | ) 60 | expect(trade.inputAmount.currency).toEqual(token0) 61 | expect(trade.outputAmount.currency).toEqual(ETHER) 62 | }) 63 | it('can be constructed with ETHER as output for exact input', () => { 64 | const trade = new Trade( 65 | new Route([pair_weth_0], token0, ETHER), 66 | new TokenAmount(token0, JSBI.BigInt(100)), 67 | TradeType.EXACT_INPUT 68 | ) 69 | expect(trade.inputAmount.currency).toEqual(token0) 70 | expect(trade.outputAmount.currency).toEqual(ETHER) 71 | }) 72 | 73 | describe('#bestTradeExactIn', () => { 74 | it('throws with empty pairs', () => { 75 | expect(() => Trade.bestTradeExactIn([], new TokenAmount(token0, JSBI.BigInt(100)), token2)).toThrow('PAIRS') 76 | }) 77 | it('throws with max hops of 0', () => { 78 | expect(() => 79 | Trade.bestTradeExactIn([pair_0_2], new TokenAmount(token0, JSBI.BigInt(100)), token2, { maxHops: 0 }) 80 | ).toThrow('MAX_HOPS') 81 | }) 82 | 83 | it('provides best route', () => { 84 | const result = Trade.bestTradeExactIn( 85 | [pair_0_1, pair_0_2, pair_1_2], 86 | new TokenAmount(token0, JSBI.BigInt(100)), 87 | token2 88 | ) 89 | expect(result).toHaveLength(2) 90 | expect(result[0].route.pairs).toHaveLength(1) // 0 -> 2 at 10:11 91 | expect(result[0].route.path).toEqual([token0, token2]) 92 | expect(result[0].inputAmount).toEqual(new TokenAmount(token0, JSBI.BigInt(100))) 93 | expect(result[0].outputAmount).toEqual(new TokenAmount(token2, JSBI.BigInt(99))) 94 | expect(result[1].route.pairs).toHaveLength(2) // 0 -> 1 -> 2 at 12:12:10 95 | expect(result[1].route.path).toEqual([token0, token1, token2]) 96 | expect(result[1].inputAmount).toEqual(new TokenAmount(token0, JSBI.BigInt(100))) 97 | expect(result[1].outputAmount).toEqual(new TokenAmount(token2, JSBI.BigInt(69))) 98 | }) 99 | 100 | it('doesnt throw for zero liquidity pairs', () => { 101 | expect(Trade.bestTradeExactIn([empty_pair_0_1], new TokenAmount(token0, JSBI.BigInt(100)), token1)).toHaveLength( 102 | 0 103 | ) 104 | }) 105 | 106 | it('respects maxHops', () => { 107 | const result = Trade.bestTradeExactIn( 108 | [pair_0_1, pair_0_2, pair_1_2], 109 | new TokenAmount(token0, JSBI.BigInt(10)), 110 | token2, 111 | { maxHops: 1 } 112 | ) 113 | expect(result).toHaveLength(1) 114 | expect(result[0].route.pairs).toHaveLength(1) // 0 -> 2 at 10:11 115 | expect(result[0].route.path).toEqual([token0, token2]) 116 | }) 117 | 118 | it('insufficient input for one pair', () => { 119 | const result = Trade.bestTradeExactIn( 120 | [pair_0_1, pair_0_2, pair_1_2], 121 | new TokenAmount(token0, JSBI.BigInt(1)), 122 | token2 123 | ) 124 | expect(result).toHaveLength(1) 125 | expect(result[0].route.pairs).toHaveLength(1) // 0 -> 2 at 10:11 126 | expect(result[0].route.path).toEqual([token0, token2]) 127 | expect(result[0].outputAmount).toEqual(new TokenAmount(token2, JSBI.BigInt(1))) 128 | }) 129 | 130 | it('respects n', () => { 131 | const result = Trade.bestTradeExactIn( 132 | [pair_0_1, pair_0_2, pair_1_2], 133 | new TokenAmount(token0, JSBI.BigInt(10)), 134 | token2, 135 | { maxNumResults: 1 } 136 | ) 137 | 138 | expect(result).toHaveLength(1) 139 | }) 140 | 141 | it('no path', () => { 142 | const result = Trade.bestTradeExactIn( 143 | [pair_0_1, pair_0_3, pair_1_3], 144 | new TokenAmount(token0, JSBI.BigInt(10)), 145 | token2 146 | ) 147 | expect(result).toHaveLength(0) 148 | }) 149 | 150 | it('works for ETHER currency input', () => { 151 | const result = Trade.bestTradeExactIn( 152 | [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], 153 | CurrencyAmount.ether(JSBI.BigInt(100)), 154 | token3 155 | ) 156 | expect(result).toHaveLength(2) 157 | expect(result[0].inputAmount.currency).toEqual(ETHER) 158 | expect(result[0].route.path).toEqual([WETH[ChainId.MAINNET], token0, token1, token3]) 159 | expect(result[0].outputAmount.currency).toEqual(token3) 160 | expect(result[1].inputAmount.currency).toEqual(ETHER) 161 | expect(result[1].route.path).toEqual([WETH[ChainId.MAINNET], token0, token3]) 162 | expect(result[1].outputAmount.currency).toEqual(token3) 163 | }) 164 | it('works for ETHER currency output', () => { 165 | const result = Trade.bestTradeExactIn( 166 | [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], 167 | new TokenAmount(token3, JSBI.BigInt(100)), 168 | ETHER 169 | ) 170 | expect(result).toHaveLength(2) 171 | expect(result[0].inputAmount.currency).toEqual(token3) 172 | expect(result[0].route.path).toEqual([token3, token0, WETH[ChainId.MAINNET]]) 173 | expect(result[0].outputAmount.currency).toEqual(ETHER) 174 | expect(result[1].inputAmount.currency).toEqual(token3) 175 | expect(result[1].route.path).toEqual([token3, token1, token0, WETH[ChainId.MAINNET]]) 176 | expect(result[1].outputAmount.currency).toEqual(ETHER) 177 | }) 178 | }) 179 | 180 | describe('#maximumAmountIn', () => { 181 | describe('tradeType = EXACT_INPUT', () => { 182 | const exactIn = new Trade( 183 | new Route([pair_0_1, pair_1_2], token0), 184 | new TokenAmount(token0, JSBI.BigInt(100)), 185 | TradeType.EXACT_INPUT 186 | ) 187 | it('throws if less than 0', () => { 188 | expect(() => exactIn.maximumAmountIn(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( 189 | 'SLIPPAGE_TOLERANCE' 190 | ) 191 | }) 192 | it('returns exact if 0', () => { 193 | expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.inputAmount) 194 | }) 195 | it('returns exact if nonzero', () => { 196 | expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( 197 | new TokenAmount(token0, JSBI.BigInt(100)) 198 | ) 199 | expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( 200 | new TokenAmount(token0, JSBI.BigInt(100)) 201 | ) 202 | expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( 203 | new TokenAmount(token0, JSBI.BigInt(100)) 204 | ) 205 | }) 206 | }) 207 | describe('tradeType = EXACT_OUTPUT', () => { 208 | const exactOut = new Trade( 209 | new Route([pair_0_1, pair_1_2], token0), 210 | new TokenAmount(token2, JSBI.BigInt(100)), 211 | TradeType.EXACT_OUTPUT 212 | ) 213 | 214 | it('throws if less than 0', () => { 215 | expect(() => exactOut.maximumAmountIn(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( 216 | 'SLIPPAGE_TOLERANCE' 217 | ) 218 | }) 219 | it('returns exact if 0', () => { 220 | expect(exactOut.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactOut.inputAmount) 221 | }) 222 | it('returns slippage amount if nonzero', () => { 223 | expect(exactOut.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( 224 | new TokenAmount(token0, JSBI.BigInt(156)) 225 | ) 226 | expect(exactOut.maximumAmountIn(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( 227 | new TokenAmount(token0, JSBI.BigInt(163)) 228 | ) 229 | expect(exactOut.maximumAmountIn(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( 230 | new TokenAmount(token0, JSBI.BigInt(468)) 231 | ) 232 | }) 233 | }) 234 | }) 235 | 236 | describe('#minimumAmountOut', () => { 237 | describe('tradeType = EXACT_INPUT', () => { 238 | const exactIn = new Trade( 239 | new Route([pair_0_1, pair_1_2], token0), 240 | new TokenAmount(token0, JSBI.BigInt(100)), 241 | TradeType.EXACT_INPUT 242 | ) 243 | it('throws if less than 0', () => { 244 | expect(() => exactIn.minimumAmountOut(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( 245 | 'SLIPPAGE_TOLERANCE' 246 | ) 247 | }) 248 | it('returns exact if 0', () => { 249 | expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.outputAmount) 250 | }) 251 | it('returns exact if nonzero', () => { 252 | expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( 253 | new TokenAmount(token2, JSBI.BigInt(69)) 254 | ) 255 | expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( 256 | new TokenAmount(token2, JSBI.BigInt(65)) 257 | ) 258 | expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( 259 | new TokenAmount(token2, JSBI.BigInt(23)) 260 | ) 261 | }) 262 | }) 263 | describe('tradeType = EXACT_OUTPUT', () => { 264 | const exactOut = new Trade( 265 | new Route([pair_0_1, pair_1_2], token0), 266 | new TokenAmount(token2, JSBI.BigInt(100)), 267 | TradeType.EXACT_OUTPUT 268 | ) 269 | 270 | it('throws if less than 0', () => { 271 | expect(() => exactOut.minimumAmountOut(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( 272 | 'SLIPPAGE_TOLERANCE' 273 | ) 274 | }) 275 | it('returns exact if 0', () => { 276 | expect(exactOut.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactOut.outputAmount) 277 | }) 278 | it('returns slippage amount if nonzero', () => { 279 | expect(exactOut.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( 280 | new TokenAmount(token2, JSBI.BigInt(100)) 281 | ) 282 | expect(exactOut.minimumAmountOut(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( 283 | new TokenAmount(token2, JSBI.BigInt(100)) 284 | ) 285 | expect(exactOut.minimumAmountOut(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( 286 | new TokenAmount(token2, JSBI.BigInt(100)) 287 | ) 288 | }) 289 | }) 290 | }) 291 | 292 | describe('#bestTradeExactOut', () => { 293 | it('throws with empty pairs', () => { 294 | expect(() => Trade.bestTradeExactOut([], token0, new TokenAmount(token2, JSBI.BigInt(100)))).toThrow('PAIRS') 295 | }) 296 | it('throws with max hops of 0', () => { 297 | expect(() => 298 | Trade.bestTradeExactOut([pair_0_2], token0, new TokenAmount(token2, JSBI.BigInt(100)), { maxHops: 0 }) 299 | ).toThrow('MAX_HOPS') 300 | }) 301 | 302 | it('provides best route', () => { 303 | const result = Trade.bestTradeExactOut( 304 | [pair_0_1, pair_0_2, pair_1_2], 305 | token0, 306 | new TokenAmount(token2, JSBI.BigInt(100)) 307 | ) 308 | expect(result).toHaveLength(2) 309 | expect(result[0].route.pairs).toHaveLength(1) // 0 -> 2 at 10:11 310 | expect(result[0].route.path).toEqual([token0, token2]) 311 | expect(result[0].inputAmount).toEqual(new TokenAmount(token0, JSBI.BigInt(101))) 312 | expect(result[0].outputAmount).toEqual(new TokenAmount(token2, JSBI.BigInt(100))) 313 | expect(result[1].route.pairs).toHaveLength(2) // 0 -> 1 -> 2 at 12:12:10 314 | expect(result[1].route.path).toEqual([token0, token1, token2]) 315 | expect(result[1].inputAmount).toEqual(new TokenAmount(token0, JSBI.BigInt(156))) 316 | expect(result[1].outputAmount).toEqual(new TokenAmount(token2, JSBI.BigInt(100))) 317 | }) 318 | 319 | it('doesnt throw for zero liquidity pairs', () => { 320 | expect(Trade.bestTradeExactOut([empty_pair_0_1], token1, new TokenAmount(token1, JSBI.BigInt(100)))).toHaveLength( 321 | 0 322 | ) 323 | }) 324 | 325 | it('respects maxHops', () => { 326 | const result = Trade.bestTradeExactOut( 327 | [pair_0_1, pair_0_2, pair_1_2], 328 | token0, 329 | new TokenAmount(token2, JSBI.BigInt(10)), 330 | { maxHops: 1 } 331 | ) 332 | expect(result).toHaveLength(1) 333 | expect(result[0].route.pairs).toHaveLength(1) // 0 -> 2 at 10:11 334 | expect(result[0].route.path).toEqual([token0, token2]) 335 | }) 336 | 337 | it('insufficient liquidity', () => { 338 | const result = Trade.bestTradeExactOut( 339 | [pair_0_1, pair_0_2, pair_1_2], 340 | token0, 341 | new TokenAmount(token2, JSBI.BigInt(1200)) 342 | ) 343 | expect(result).toHaveLength(0) 344 | }) 345 | 346 | it('insufficient liquidity in one pair but not the other', () => { 347 | const result = Trade.bestTradeExactOut( 348 | [pair_0_1, pair_0_2, pair_1_2], 349 | token0, 350 | new TokenAmount(token2, JSBI.BigInt(1050)) 351 | ) 352 | expect(result).toHaveLength(1) 353 | }) 354 | 355 | it('respects n', () => { 356 | const result = Trade.bestTradeExactOut( 357 | [pair_0_1, pair_0_2, pair_1_2], 358 | token0, 359 | new TokenAmount(token2, JSBI.BigInt(10)), 360 | { maxNumResults: 1 } 361 | ) 362 | 363 | expect(result).toHaveLength(1) 364 | }) 365 | 366 | it('no path', () => { 367 | const result = Trade.bestTradeExactOut( 368 | [pair_0_1, pair_0_3, pair_1_3], 369 | token0, 370 | new TokenAmount(token2, JSBI.BigInt(10)) 371 | ) 372 | expect(result).toHaveLength(0) 373 | }) 374 | 375 | it('works for ETHER currency input', () => { 376 | const result = Trade.bestTradeExactOut( 377 | [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], 378 | ETHER, 379 | new TokenAmount(token3, JSBI.BigInt(100)) 380 | ) 381 | expect(result).toHaveLength(2) 382 | expect(result[0].inputAmount.currency).toEqual(ETHER) 383 | expect(result[0].route.path).toEqual([WETH[ChainId.MAINNET], token0, token1, token3]) 384 | expect(result[0].outputAmount.currency).toEqual(token3) 385 | expect(result[1].inputAmount.currency).toEqual(ETHER) 386 | expect(result[1].route.path).toEqual([WETH[ChainId.MAINNET], token0, token3]) 387 | expect(result[1].outputAmount.currency).toEqual(token3) 388 | }) 389 | it('works for ETHER currency output', () => { 390 | const result = Trade.bestTradeExactOut( 391 | [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], 392 | token3, 393 | CurrencyAmount.ether(JSBI.BigInt(100)) 394 | ) 395 | expect(result).toHaveLength(2) 396 | expect(result[0].inputAmount.currency).toEqual(token3) 397 | expect(result[0].route.path).toEqual([token3, token0, WETH[ChainId.MAINNET]]) 398 | expect(result[0].outputAmount.currency).toEqual(ETHER) 399 | expect(result[1].inputAmount.currency).toEqual(token3) 400 | expect(result[1].route.path).toEqual([token3, token1, token0, WETH[ChainId.MAINNET]]) 401 | expect(result[1].outputAmount.currency).toEqual(ETHER) 402 | }) 403 | }) 404 | }) 405 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "test"], 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "module": "esnext", 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./", 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "strictPropertyInitialization": true, 15 | "noImplicitThis": true, 16 | "alwaysStrict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": ["src/*", "node_modules/*"] 25 | }, 26 | "esModuleInterop": true, 27 | "resolveJsonModule": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------