├── .github ├── stale.yml └── workflows │ ├── lint.yml │ └── unit-tests.yml ├── .gitignore ├── .prettierignore ├── .yarnrc ├── LICENSE ├── README.md ├── package.json ├── src ├── addresses.test.ts ├── addresses.ts ├── chains.ts ├── constants.ts ├── declarations.d.ts ├── entities │ ├── baseCurrency.ts │ ├── currency.test.ts │ ├── currency.ts │ ├── ether.test.ts │ ├── ether.ts │ ├── fractions │ │ ├── currencyAmount.test.ts │ │ ├── currencyAmount.ts │ │ ├── fraction.test.ts │ │ ├── fraction.ts │ │ ├── index.ts │ │ ├── percent.test.ts │ │ ├── percent.ts │ │ ├── price.test.ts │ │ └── price.ts │ ├── index.ts │ ├── nativeCurrency.ts │ ├── token.test.ts │ ├── token.ts │ └── weth9.ts ├── index.ts └── utils │ ├── computePriceImpact.test.ts │ ├── computePriceImpact.ts │ ├── index.ts │ ├── sortedInsert.test.ts │ ├── sortedInsert.ts │ ├── sqrt.test.ts │ ├── sqrt.ts │ ├── validateAndParseAddress.test.ts │ └── validateAndParseAddress.ts ├── tsconfig.json └── yarn.lock /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | issues: 4 | # Number of days of inactivity before an Issue or Pull Request becomes stale 5 | daysUntilStale: 7 6 | 7 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 8 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 9 | daysUntilClose: 7 10 | 11 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 12 | onlyLabels: 13 | - question 14 | - autoclose 15 | 16 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 17 | exemptLabels: 18 | - p0 19 | - bug 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as stale because it has not had 24 | recent activity. It will be closed if no further activity occurs. Thank you 25 | for your contributions. 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | run-linters: 9 | name: Run linters 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 12 20 | 21 | - name: Install dependencies 22 | run: yarn install --frozen-lockfile 23 | 24 | - name: Run linters 25 | uses: wearerequired/lint-action@a8497ddb33fb1205941fd40452ca9fff07e0770d 26 | with: 27 | github_token: ${{ secrets.github_token }} 28 | prettier: true 29 | auto_fix: true 30 | prettier_extensions: 'css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml,yml' 31 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | tests: 13 | name: Unit tests 14 | strategy: 15 | matrix: 16 | node: ['10.x', '12.x', '14.x'] 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - name: Get cache directory 30 | id: yarn-cache 31 | run: echo "::set-output name=dir::$(yarn cache dir)" 32 | 33 | - name: Cache yarn dependencies 34 | uses: actions/cache@v1 35 | with: 36 | path: ${{ steps.yarn-cache.outputs.dir }} 37 | key: yarn-${{ hashFiles('**/yarn.lock') }} 38 | restore-keys: | 39 | yarn- 40 | 41 | - name: Install dependencies 42 | run: yarn install --frozen-lockfile 43 | 44 | - name: Run tests 45 | run: yarn test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Uniswap Labs 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @uniswap/sdk-core - Now at `Uniswap/sdks` 2 | 3 | All versions after 4.2.0 of this SDK can be found in the [SDK monorepo](https://github.com/Uniswap/sdks/tree/main/sdks/sdk-core)! Please file all future issues, PR’s, and discussions there. 4 | 5 | ### Old Issues and PR’s 6 | 7 | If you have an issue or open PR that is still active on this SDK in this repository, please recreate it in the new repository. Some existing issues and PR’s may be automatically migrated by the Uniswap Labs team. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/sdk-core", 3 | "license": "MIT", 4 | "version": "4.2.0", 5 | "description": "⚒️ An SDK for building applications on top of Uniswap V3", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": "https://github.com/Uniswap/uniswap-sdk-core.git", 12 | "keywords": [ 13 | "uniswap", 14 | "ethereum" 15 | ], 16 | "module": "dist/sdk-core.esm.js", 17 | "scripts": { 18 | "build": "tsdx build", 19 | "start": "tsdx watch", 20 | "test": "tsdx test", 21 | "prepublishOnly": "tsdx build" 22 | }, 23 | "dependencies": { 24 | "@ethersproject/address": "^5.0.2", 25 | "big.js": "^5.2.2", 26 | "decimal.js-light": "^2.5.0", 27 | "jsbi": "^3.1.4", 28 | "tiny-invariant": "^1.1.0", 29 | "toformat": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/big.js": "^4.0.5", 33 | "@types/jest": "^24.0.25", 34 | "tsdx": "^0.14.1" 35 | }, 36 | "engines": { 37 | "node": ">=10" 38 | }, 39 | "prettier": { 40 | "printWidth": 120, 41 | "semi": false, 42 | "singleQuote": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/addresses.test.ts: -------------------------------------------------------------------------------- 1 | import { SWAP_ROUTER_02_ADDRESSES } from './addresses' 2 | import { ChainId } from './chains' 3 | 4 | describe('addresses', () => { 5 | describe('swap router 02 addresses', () => { 6 | it('should return the correct address for base', () => { 7 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.BASE) 8 | expect(address).toEqual('0x2626664c2603336E57B271c5C0b26F421741e481') 9 | }) 10 | 11 | it('should return the correct address for base goerli', () => { 12 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.BASE_GOERLI) 13 | expect(address).toEqual('0x8357227D4eDc78991Db6FDB9bD6ADE250536dE1d') 14 | }) 15 | 16 | it('should return the correct address for avalanche', () => { 17 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.AVALANCHE) 18 | expect(address).toEqual('0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE') 19 | }) 20 | 21 | it('should return the correct address for BNB', () => { 22 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.BNB) 23 | expect(address).toEqual('0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2') 24 | }) 25 | 26 | it('should return the correct address for arbitrum goerli', () => { 27 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.ARBITRUM_GOERLI) 28 | expect(address).toEqual('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45') 29 | }) 30 | 31 | it('should return the correct address for optimism sepolia', () => { 32 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.OPTIMISM_SEPOLIA) 33 | expect(address).toEqual('0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4') 34 | }) 35 | 36 | it('should return the correct address for sepolia', () => { 37 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.SEPOLIA) 38 | expect(address).toEqual('0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E') 39 | }) 40 | 41 | it('should return the correct address for bast', () => { 42 | const address = SWAP_ROUTER_02_ADDRESSES(ChainId.BLAST) 43 | expect(address).toEqual('0x549FEB8c9bd4c12Ad2AB27022dA12492aC452B66') 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/addresses.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, SUPPORTED_CHAINS, SupportedChainsType } from './chains' 2 | 3 | type AddressMap = { [chainId: number]: string } 4 | 5 | type ChainAddresses = { 6 | v3CoreFactoryAddress: string 7 | multicallAddress: string 8 | quoterAddress: string 9 | v3MigratorAddress?: string 10 | nonfungiblePositionManagerAddress?: string 11 | tickLensAddress?: string 12 | swapRouter02Address?: string 13 | v1MixedRouteQuoterAddress?: string 14 | } 15 | 16 | const DEFAULT_NETWORKS = [ChainId.MAINNET, ChainId.GOERLI, ChainId.SEPOLIA] 17 | 18 | function constructSameAddressMap(address: string, additionalNetworks: ChainId[] = []): AddressMap { 19 | return DEFAULT_NETWORKS.concat(additionalNetworks).reduce((memo, chainId) => { 20 | memo[chainId] = address 21 | return memo 22 | }, {}) 23 | } 24 | 25 | export const UNI_ADDRESSES: AddressMap = constructSameAddressMap('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', [ 26 | ChainId.OPTIMISM, 27 | ChainId.ARBITRUM_ONE, 28 | ChainId.POLYGON, 29 | ChainId.POLYGON_MUMBAI, 30 | ChainId.SEPOLIA 31 | ]) 32 | 33 | export const UNISWAP_NFT_AIRDROP_CLAIM_ADDRESS = '0x8B799381ac40b838BBA4131ffB26197C432AFe78' 34 | 35 | /** 36 | * @deprecated use V2_FACTORY_ADDRESSES instead 37 | */ 38 | export const V2_FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 39 | export const V2_FACTORY_ADDRESSES: AddressMap = { 40 | [ChainId.MAINNET]: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', 41 | [ChainId.GOERLI]: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', 42 | [ChainId.SEPOLIA]: '0xB7f907f7A9eBC822a80BD25E224be42Ce0A698A0', 43 | [ChainId.OPTIMISM]: '0x0c3c1c532F1e39EdF36BE9Fe0bE1410313E074Bf', 44 | [ChainId.ARBITRUM_ONE]: '0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9', 45 | [ChainId.AVALANCHE]: '0x9e5A52f57b3038F1B8EeE45F28b3C1967e22799C', 46 | [ChainId.BASE]: '0x8909dc15e40173ff4699343b6eb8132c65e18ec6', 47 | [ChainId.BNB]: '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', 48 | [ChainId.POLYGON]: '0x9e5A52f57b3038F1B8EeE45F28b3C1967e22799C', 49 | [ChainId.CELO]: '0x79a530c8e2fA8748B7B40dd3629C0520c2cCf03f', 50 | [ChainId.BLAST]: '0x5C346464d33F90bABaf70dB6388507CC889C1070' 51 | } 52 | /** 53 | * @deprecated use V2_ROUTER_ADDRESSES instead 54 | */ 55 | export const V2_ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' 56 | export const V2_ROUTER_ADDRESSES: AddressMap = { 57 | [ChainId.MAINNET]: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 58 | [ChainId.GOERLI]: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 59 | [ChainId.ARBITRUM_ONE]: '0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24', 60 | [ChainId.OPTIMISM]: '0x4a7b5da61326a6379179b40d00f57e5bbdc962c2', 61 | [ChainId.BASE]: '0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24', 62 | [ChainId.AVALANCHE]: '0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24', 63 | [ChainId.BNB]: '0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24', 64 | [ChainId.POLYGON]: '0xedf6066a2b290c185783862c7f4776a2c8077ad1', 65 | [ChainId.BLAST]: '0xBB66Eb1c5e875933D44DAe661dbD80e5D9B03035' 66 | } 67 | 68 | // Networks that share most of the same addresses i.e. Mainnet, Goerli, Optimism, Arbitrum, Polygon 69 | const DEFAULT_ADDRESSES: ChainAddresses = { 70 | v3CoreFactoryAddress: '0x1F98431c8aD98523631AE4a59f267346ea31F984', 71 | multicallAddress: '0x1F98415757620B543A52E61c46B32eB19261F984', 72 | quoterAddress: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 73 | v3MigratorAddress: '0xA5644E29708357803b5A882D272c41cC0dF92B34', 74 | nonfungiblePositionManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' 75 | } 76 | const MAINNET_ADDRESSES: ChainAddresses = { 77 | ...DEFAULT_ADDRESSES, 78 | v1MixedRouteQuoterAddress: '0x84E44095eeBfEC7793Cd7d5b57B7e401D7f1cA2E' 79 | } 80 | const GOERLI_ADDRESSES: ChainAddresses = { 81 | ...DEFAULT_ADDRESSES, 82 | v1MixedRouteQuoterAddress: '0xBa60b6e6fF25488308789E6e0A65D838be34194e' 83 | } 84 | 85 | const OPTIMISM_ADDRESSES: ChainAddresses = DEFAULT_ADDRESSES 86 | const ARBITRUM_ONE_ADDRESSES: ChainAddresses = { 87 | ...DEFAULT_ADDRESSES, 88 | multicallAddress: '0xadF885960B47eA2CD9B55E6DAc6B42b7Cb2806dB', 89 | tickLensAddress: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573' 90 | } 91 | const POLYGON_ADDRESSES: ChainAddresses = DEFAULT_ADDRESSES 92 | 93 | // celo v3 addresses 94 | const CELO_ADDRESSES: ChainAddresses = { 95 | v3CoreFactoryAddress: '0xAfE208a311B21f13EF87E33A90049fC17A7acDEc', 96 | multicallAddress: '0x633987602DE5C4F337e3DbF265303A1080324204', 97 | quoterAddress: '0x82825d0554fA07f7FC52Ab63c961F330fdEFa8E8', 98 | v3MigratorAddress: '0x3cFd4d48EDfDCC53D3f173F596f621064614C582', 99 | nonfungiblePositionManagerAddress: '0x3d79EdAaBC0EaB6F08ED885C05Fc0B014290D95A', 100 | tickLensAddress: '0x5f115D9113F88e0a0Db1b5033D90D4a9690AcD3D' 101 | } 102 | 103 | // BNB v3 addresses 104 | const BNB_ADDRESSES: ChainAddresses = { 105 | v3CoreFactoryAddress: '0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7', 106 | multicallAddress: '0x963Df249eD09c358A4819E39d9Cd5736c3087184', 107 | quoterAddress: '0x78D78E420Da98ad378D7799bE8f4AF69033EB077', 108 | v3MigratorAddress: '0x32681814957e0C13117ddc0c2aba232b5c9e760f', 109 | nonfungiblePositionManagerAddress: '0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613', 110 | tickLensAddress: '0xD9270014D396281579760619CCf4c3af0501A47C', 111 | swapRouter02Address: '0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2' 112 | } 113 | 114 | // optimism goerli addresses 115 | const OPTIMISM_GOERLI_ADDRESSES: ChainAddresses = { 116 | v3CoreFactoryAddress: '0xB656dA17129e7EB733A557f4EBc57B76CFbB5d10', 117 | multicallAddress: '0x07F2D8a2a02251B62af965f22fC4744A5f96BCCd', 118 | quoterAddress: '0x9569CbA925c8ca2248772A9A4976A516743A246F', 119 | v3MigratorAddress: '0xf6c55fBe84B1C8c3283533c53F51bC32F5C7Aba8', 120 | nonfungiblePositionManagerAddress: '0x39Ca85Af2F383190cBf7d7c41ED9202D27426EF6', 121 | tickLensAddress: '0xe6140Bd164b63E8BfCfc40D5dF952f83e171758e' 122 | } 123 | 124 | // optimism sepolia addresses 125 | const OPTIMISM_SEPOLIA_ADDRESSES: ChainAddresses = { 126 | v3CoreFactoryAddress: '0x8CE191193D15ea94e11d327b4c7ad8bbE520f6aF', 127 | multicallAddress: '0x80e4e06841bb76AA9735E0448cB8d003C0EF009a', 128 | quoterAddress: '0x0FBEa6cf957d95ee9313490050F6A0DA68039404', 129 | v3MigratorAddress: '0xE7EcbAAaA54D007A00dbb6c1d2f150066D69dA07', 130 | nonfungiblePositionManagerAddress: '0xdA75cEf1C93078e8b736FCA5D5a30adb97C8957d', 131 | tickLensAddress: '0xCb7f54747F58F8944973cea5b8f4ac2209BadDC5', 132 | swapRouter02Address: '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4' 133 | } 134 | 135 | // arbitrum goerli v3 addresses 136 | const ARBITRUM_GOERLI_ADDRESSES: ChainAddresses = { 137 | v3CoreFactoryAddress: '0x4893376342d5D7b3e31d4184c08b265e5aB2A3f6', 138 | multicallAddress: '0x8260CB40247290317a4c062F3542622367F206Ee', 139 | quoterAddress: '0x1dd92b83591781D0C6d98d07391eea4b9a6008FA', 140 | v3MigratorAddress: '0xA815919D2584Ac3F76ea9CB62E6Fd40a43BCe0C3', 141 | nonfungiblePositionManagerAddress: '0x622e4726a167799826d1E1D150b076A7725f5D81', 142 | tickLensAddress: '0xb52429333da969a0C79a60930a4Bf0020E5D1DE8' 143 | } 144 | 145 | // arbitrum sepolia v3 addresses 146 | const ARBITRUM_SEPOLIA_ADDRESSES: ChainAddresses = { 147 | v3CoreFactoryAddress: '0x248AB79Bbb9bC29bB72f7Cd42F17e054Fc40188e', 148 | multicallAddress: '0x2B718b475e385eD29F56775a66aAB1F5cC6B2A0A', 149 | quoterAddress: '0x2779a0CC1c3e0E44D2542EC3e79e3864Ae93Ef0B', 150 | v3MigratorAddress: '0x398f43ef2c67B941147157DA1c5a868E906E043D', 151 | nonfungiblePositionManagerAddress: '0x6b2937Bde17889EDCf8fbD8dE31C3C2a70Bc4d65', 152 | tickLensAddress: '0x0fd18587734e5C2dcE2dccDcC7DD1EC89ba557d9', 153 | swapRouter02Address: '0x101F443B4d1b059569D643917553c771E1b9663E' 154 | } 155 | 156 | // sepolia v3 addresses 157 | const SEPOLIA_ADDRESSES: ChainAddresses = { 158 | v3CoreFactoryAddress: '0x0227628f3F023bb0B980b67D528571c95c6DaC1c', 159 | multicallAddress: '0xD7F33bCdb21b359c8ee6F0251d30E94832baAd07', 160 | quoterAddress: '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3', 161 | v3MigratorAddress: '0x729004182cF005CEC8Bd85df140094b6aCbe8b15', 162 | nonfungiblePositionManagerAddress: '0x1238536071E1c677A632429e3655c799b22cDA52', 163 | tickLensAddress: '0xd7f33bcdb21b359c8ee6f0251d30e94832baad07', 164 | swapRouter02Address: '0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E' 165 | } 166 | 167 | // Avalanche v3 addresses 168 | const AVALANCHE_ADDRESSES: ChainAddresses = { 169 | v3CoreFactoryAddress: '0x740b1c1de25031C31FF4fC9A62f554A55cdC1baD', 170 | multicallAddress: '0x0139141Cd4Ee88dF3Cdb65881D411bAE271Ef0C2', 171 | quoterAddress: '0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F', 172 | v3MigratorAddress: '0x44f5f1f5E452ea8d29C890E8F6e893fC0f1f0f97', 173 | nonfungiblePositionManagerAddress: '0x655C406EBFa14EE2006250925e54ec43AD184f8B', 174 | tickLensAddress: '0xEB9fFC8bf81b4fFd11fb6A63a6B0f098c6e21950', 175 | swapRouter02Address: '0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE' 176 | } 177 | 178 | const BASE_ADDRESSES: ChainAddresses = { 179 | v3CoreFactoryAddress: '0x33128a8fC17869897dcE68Ed026d694621f6FDfD', 180 | multicallAddress: '0x091e99cb1C49331a94dD62755D168E941AbD0693', 181 | quoterAddress: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a', 182 | v3MigratorAddress: '0x23cF10b1ee3AdfCA73B0eF17C07F7577e7ACd2d7', 183 | nonfungiblePositionManagerAddress: '0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1', 184 | tickLensAddress: '0x0CdeE061c75D43c82520eD998C23ac2991c9ac6d', 185 | swapRouter02Address: '0x2626664c2603336E57B271c5C0b26F421741e481' 186 | } 187 | 188 | // Base Goerli v3 addresses 189 | const BASE_GOERLI_ADDRESSES: ChainAddresses = { 190 | v3CoreFactoryAddress: '0x9323c1d6D800ed51Bd7C6B216cfBec678B7d0BC2', 191 | multicallAddress: '0xB206027a9E0E13F05eBEFa5D2402Bab3eA716439', 192 | quoterAddress: '0xedf539058e28E5937dAef3f69cEd0b25fbE66Ae9', 193 | v3MigratorAddress: '0x3efe5d02a04b7351D671Db7008ec6eBA9AD9e3aE', 194 | nonfungiblePositionManagerAddress: '0x3c61369ef0D1D2AFa70d8feC2F31C5D6Ce134F30', 195 | tickLensAddress: '0x1acB873Ee909D0c98adB18e4474943249F931b92', 196 | swapRouter02Address: '0x8357227D4eDc78991Db6FDB9bD6ADE250536dE1d' 197 | } 198 | 199 | const ZORA_ADDRESSES: ChainAddresses = { 200 | v3CoreFactoryAddress: '0x7145F8aeef1f6510E92164038E1B6F8cB2c42Cbb', 201 | multicallAddress: '0xA51c76bEE6746cB487a7e9312E43e2b8f4A37C15', 202 | quoterAddress: '0x11867e1b3348F3ce4FcC170BC5af3d23E07E64Df', 203 | v3MigratorAddress: '0x048352d8dCF13686982C799da63fA6426a9D0b60', 204 | nonfungiblePositionManagerAddress: '0xbC91e8DfA3fF18De43853372A3d7dfe585137D78', 205 | tickLensAddress: '0x209AAda09D74Ad3B8D0E92910Eaf85D2357e3044', 206 | swapRouter02Address: '0x7De04c96BE5159c3b5CeffC82aa176dc81281557' 207 | } 208 | 209 | const ZORA_SEPOLIA_ADDRESSES: ChainAddresses = { 210 | v3CoreFactoryAddress: '0x4324A677D74764f46f33ED447964252441aA8Db6', 211 | multicallAddress: '0xA1E7e3A69671C4494EC59Dbd442de930a93F911A', 212 | quoterAddress: '0xC195976fEF0985886E37036E2DF62bF371E12Df0', 213 | v3MigratorAddress: '0x65ef259b31bf1d977c37e9434658694267674897', 214 | nonfungiblePositionManagerAddress: '0xB8458EaAe43292e3c1F7994EFd016bd653d23c20', 215 | tickLensAddress: '0x23C0F71877a1Fc4e20A78018f9831365c85f3064' 216 | } 217 | 218 | const ROOTSTOCK_ADDRESSES: ChainAddresses = { 219 | v3CoreFactoryAddress: '0xaF37EC98A00FD63689CF3060BF3B6784E00caD82', 220 | multicallAddress: '0x996a9858cDfa45Ad68E47c9A30a7201E29c6a386', 221 | quoterAddress: '0xb51727c996C68E60F598A923a5006853cd2fEB31', 222 | v3MigratorAddress: '0x16678977CA4ec3DAD5efc7b15780295FE5f56162', 223 | nonfungiblePositionManagerAddress: '0x9d9386c042F194B460Ec424a1e57ACDE25f5C4b1', 224 | tickLensAddress: '0x55B9dF5bF68ADe972191a91980459f48ecA16afC', 225 | swapRouter02Address: '0x0B14ff67f0014046b4b99057Aec4509640b3947A' 226 | } 227 | 228 | const BLAST_ADDRESSES: ChainAddresses = { 229 | v3CoreFactoryAddress: '0x792edAdE80af5fC680d96a2eD80A44247D2Cf6Fd', 230 | multicallAddress: '0xdC7f370de7631cE9e2c2e1DCDA6B3B5744Cf4705', 231 | quoterAddress: '0x6Cdcd65e03c1CEc3730AeeCd45bc140D57A25C77', 232 | v3MigratorAddress: '0x15CA7043CD84C5D21Ae76Ba0A1A967d42c40ecE0', 233 | nonfungiblePositionManagerAddress: '0xB218e4f7cF0533d4696fDfC419A0023D33345F28', 234 | tickLensAddress: '0x2E95185bCdD928a3e984B7e2D6560Ab1b17d7274', 235 | swapRouter02Address: '0x549FEB8c9bd4c12Ad2AB27022dA12492aC452B66' 236 | } 237 | 238 | export const CHAIN_TO_ADDRESSES_MAP: Record = { 239 | [ChainId.MAINNET]: MAINNET_ADDRESSES, 240 | [ChainId.OPTIMISM]: OPTIMISM_ADDRESSES, 241 | [ChainId.ARBITRUM_ONE]: ARBITRUM_ONE_ADDRESSES, 242 | [ChainId.POLYGON]: POLYGON_ADDRESSES, 243 | [ChainId.POLYGON_MUMBAI]: POLYGON_ADDRESSES, 244 | [ChainId.GOERLI]: GOERLI_ADDRESSES, 245 | [ChainId.CELO]: CELO_ADDRESSES, 246 | [ChainId.CELO_ALFAJORES]: CELO_ADDRESSES, 247 | [ChainId.BNB]: BNB_ADDRESSES, 248 | [ChainId.OPTIMISM_GOERLI]: OPTIMISM_GOERLI_ADDRESSES, 249 | [ChainId.OPTIMISM_SEPOLIA]: OPTIMISM_SEPOLIA_ADDRESSES, 250 | [ChainId.ARBITRUM_GOERLI]: ARBITRUM_GOERLI_ADDRESSES, 251 | [ChainId.ARBITRUM_SEPOLIA]: ARBITRUM_SEPOLIA_ADDRESSES, 252 | [ChainId.SEPOLIA]: SEPOLIA_ADDRESSES, 253 | [ChainId.AVALANCHE]: AVALANCHE_ADDRESSES, 254 | [ChainId.BASE]: BASE_ADDRESSES, 255 | [ChainId.BASE_GOERLI]: BASE_GOERLI_ADDRESSES, 256 | [ChainId.ZORA]: ZORA_ADDRESSES, 257 | [ChainId.ZORA_SEPOLIA]: ZORA_SEPOLIA_ADDRESSES, 258 | [ChainId.ROOTSTOCK]: ROOTSTOCK_ADDRESSES, 259 | [ChainId.BLAST]: BLAST_ADDRESSES 260 | } 261 | 262 | /* V3 Contract Addresses */ 263 | export const V3_CORE_FACTORY_ADDRESSES: AddressMap = { 264 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 265 | memo[chainId] = CHAIN_TO_ADDRESSES_MAP[chainId].v3CoreFactoryAddress 266 | return memo 267 | }, {}) 268 | } 269 | 270 | export const V3_MIGRATOR_ADDRESSES: AddressMap = { 271 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 272 | const v3MigratorAddress = CHAIN_TO_ADDRESSES_MAP[chainId].v3MigratorAddress 273 | if (v3MigratorAddress) { 274 | memo[chainId] = v3MigratorAddress 275 | } 276 | return memo 277 | }, {}) 278 | } 279 | 280 | export const MULTICALL_ADDRESSES: AddressMap = { 281 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 282 | memo[chainId] = CHAIN_TO_ADDRESSES_MAP[chainId].multicallAddress 283 | return memo 284 | }, {}) 285 | } 286 | 287 | /** 288 | * The oldest V0 governance address 289 | */ 290 | export const GOVERNANCE_ALPHA_V0_ADDRESSES: AddressMap = constructSameAddressMap( 291 | '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F' 292 | ) 293 | /** 294 | * The older V1 governance address 295 | */ 296 | export const GOVERNANCE_ALPHA_V1_ADDRESSES: AddressMap = { 297 | [ChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6' 298 | } 299 | /** 300 | * The latest governor bravo that is currently admin of timelock 301 | */ 302 | export const GOVERNANCE_BRAVO_ADDRESSES: AddressMap = { 303 | [ChainId.MAINNET]: '0x408ED6354d4973f66138C91495F2f2FCbd8724C3' 304 | } 305 | 306 | export const TIMELOCK_ADDRESSES: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC') 307 | 308 | export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = { 309 | [ChainId.MAINNET]: '0x090D4613473dEE047c3f2706764f49E0821D256e' 310 | } 311 | 312 | export const ARGENT_WALLET_DETECTOR_ADDRESS: AddressMap = { 313 | [ChainId.MAINNET]: '0xeca4B0bDBf7c55E9b7925919d03CbF8Dc82537E8' 314 | } 315 | 316 | export const QUOTER_ADDRESSES: AddressMap = { 317 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 318 | memo[chainId] = CHAIN_TO_ADDRESSES_MAP[chainId].quoterAddress 319 | return memo 320 | }, {}) 321 | } 322 | 323 | export const NONFUNGIBLE_POSITION_MANAGER_ADDRESSES: AddressMap = { 324 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 325 | const nonfungiblePositionManagerAddress = CHAIN_TO_ADDRESSES_MAP[chainId].nonfungiblePositionManagerAddress 326 | if (nonfungiblePositionManagerAddress) { 327 | memo[chainId] = nonfungiblePositionManagerAddress 328 | } 329 | return memo 330 | }, {}) 331 | } 332 | 333 | export const ENS_REGISTRAR_ADDRESSES: AddressMap = { 334 | ...constructSameAddressMap('0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e') 335 | } 336 | 337 | export const SOCKS_CONTROLLER_ADDRESSES: AddressMap = { 338 | [ChainId.MAINNET]: '0x65770b5283117639760beA3F867b69b3697a91dd' 339 | } 340 | 341 | export const TICK_LENS_ADDRESSES: AddressMap = { 342 | ...SUPPORTED_CHAINS.reduce((memo, chainId) => { 343 | const tickLensAddress = CHAIN_TO_ADDRESSES_MAP[chainId].tickLensAddress 344 | if (tickLensAddress) { 345 | memo[chainId] = tickLensAddress 346 | } 347 | return memo 348 | }, {}) 349 | } 350 | 351 | export const MIXED_ROUTE_QUOTER_V1_ADDRESSES: AddressMap = SUPPORTED_CHAINS.reduce((memo, chainId) => { 352 | const v1MixedRouteQuoterAddress = CHAIN_TO_ADDRESSES_MAP[chainId].v1MixedRouteQuoterAddress 353 | if (v1MixedRouteQuoterAddress) { 354 | memo[chainId] = v1MixedRouteQuoterAddress 355 | } 356 | return memo 357 | }, {}) 358 | 359 | export const SWAP_ROUTER_02_ADDRESSES = (chainId: number) => { 360 | if (SUPPORTED_CHAINS.includes(chainId)) { 361 | const id = chainId as SupportedChainsType 362 | return CHAIN_TO_ADDRESSES_MAP[id].swapRouter02Address ?? '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' 363 | } 364 | return '' 365 | } 366 | -------------------------------------------------------------------------------- /src/chains.ts: -------------------------------------------------------------------------------- 1 | export enum ChainId { 2 | MAINNET = 1, 3 | GOERLI = 5, 4 | SEPOLIA = 11155111, 5 | OPTIMISM = 10, 6 | OPTIMISM_GOERLI = 420, 7 | OPTIMISM_SEPOLIA = 11155420, 8 | ARBITRUM_ONE = 42161, 9 | ARBITRUM_GOERLI = 421613, 10 | ARBITRUM_SEPOLIA = 421614, 11 | POLYGON = 137, 12 | POLYGON_MUMBAI = 80001, 13 | CELO = 42220, 14 | CELO_ALFAJORES = 44787, 15 | GNOSIS = 100, 16 | MOONBEAM = 1284, 17 | BNB = 56, 18 | AVALANCHE = 43114, 19 | BASE_GOERLI = 84531, 20 | BASE = 8453, 21 | ZORA = 7777777, 22 | ZORA_SEPOLIA = 999999999, 23 | ROOTSTOCK = 30, 24 | BLAST = 81457 25 | } 26 | 27 | export const SUPPORTED_CHAINS = [ 28 | ChainId.MAINNET, 29 | ChainId.OPTIMISM, 30 | ChainId.OPTIMISM_GOERLI, 31 | ChainId.OPTIMISM_SEPOLIA, 32 | ChainId.ARBITRUM_ONE, 33 | ChainId.ARBITRUM_GOERLI, 34 | ChainId.ARBITRUM_SEPOLIA, 35 | ChainId.POLYGON, 36 | ChainId.POLYGON_MUMBAI, 37 | ChainId.GOERLI, 38 | ChainId.SEPOLIA, 39 | ChainId.CELO_ALFAJORES, 40 | ChainId.CELO, 41 | ChainId.BNB, 42 | ChainId.AVALANCHE, 43 | ChainId.BASE, 44 | ChainId.BASE_GOERLI, 45 | ChainId.ZORA, 46 | ChainId.ZORA_SEPOLIA, 47 | ChainId.ROOTSTOCK, 48 | ChainId.BLAST 49 | ] as const 50 | export type SupportedChainsType = typeof SUPPORTED_CHAINS[number] 51 | 52 | export enum NativeCurrencyName { 53 | // Strings match input for CLI 54 | ETHER = 'ETH', 55 | MATIC = 'MATIC', 56 | CELO = 'CELO', 57 | GNOSIS = 'XDAI', 58 | MOONBEAM = 'GLMR', 59 | BNB = 'BNB', 60 | AVAX = 'AVAX', 61 | ROOTSTOCK = 'RBTC' 62 | } 63 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | 3 | // exports for external consumption 4 | export type BigintIsh = JSBI | string | number 5 | 6 | export enum TradeType { 7 | EXACT_INPUT, 8 | EXACT_OUTPUT 9 | } 10 | 11 | export enum Rounding { 12 | ROUND_DOWN, 13 | ROUND_HALF_UP, 14 | ROUND_UP 15 | } 16 | 17 | export const MaxUint256 = JSBI.BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 18 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'toformat' 2 | -------------------------------------------------------------------------------- /src/entities/baseCurrency.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { Currency } from './currency' 3 | import { Token } from './token' 4 | 5 | /** 6 | * A currency is any fungible financial instrument, including Ether, all ERC20 tokens, and other chain-native currencies 7 | */ 8 | export abstract class BaseCurrency { 9 | /** 10 | * Returns whether the currency is native to the chain and must be wrapped (e.g. Ether) 11 | */ 12 | public abstract readonly isNative: boolean 13 | /** 14 | * Returns whether the currency is a token that is usable in Uniswap without wrapping 15 | */ 16 | public abstract readonly isToken: boolean 17 | 18 | /** 19 | * The chain ID on which this currency resides 20 | */ 21 | public readonly chainId: number 22 | /** 23 | * The decimals used in representing currency amounts 24 | */ 25 | public readonly decimals: number 26 | /** 27 | * The symbol of the currency, i.e. a short textual non-unique identifier 28 | */ 29 | public readonly symbol?: string 30 | /** 31 | * The name of the currency, i.e. a descriptive textual non-unique identifier 32 | */ 33 | public readonly name?: string 34 | 35 | /** 36 | * Constructs an instance of the base class `BaseCurrency`. 37 | * @param chainId the chain ID on which this currency resides 38 | * @param decimals decimals of the currency 39 | * @param symbol symbol of the currency 40 | * @param name of the currency 41 | */ 42 | protected constructor(chainId: number, decimals: number, symbol?: string, name?: string) { 43 | invariant(Number.isSafeInteger(chainId), 'CHAIN_ID') 44 | invariant(decimals >= 0 && decimals < 255 && Number.isInteger(decimals), 'DECIMALS') 45 | 46 | this.chainId = chainId 47 | this.decimals = decimals 48 | this.symbol = symbol 49 | this.name = name 50 | } 51 | 52 | /** 53 | * Returns whether this currency is functionally equivalent to the other currency 54 | * @param other the other currency 55 | */ 56 | public abstract equals(other: Currency): boolean 57 | 58 | /** 59 | * Return the wrapped version of this currency that can be used with the Uniswap contracts. Currencies must 60 | * implement this to be used in Uniswap 61 | */ 62 | public abstract get wrapped(): Token 63 | } 64 | -------------------------------------------------------------------------------- /src/entities/currency.test.ts: -------------------------------------------------------------------------------- 1 | import { Ether, Token } from './index' 2 | 3 | describe('Currency', () => { 4 | const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' 5 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 6 | 7 | const t0 = new Token(1, ADDRESS_ZERO, 18) 8 | const t1 = new Token(1, ADDRESS_ONE, 18) 9 | 10 | describe('#equals', () => { 11 | it('ether on same chains is ether', () => { 12 | expect(Ether.onChain(1).equals(Ether.onChain(1))) 13 | }) 14 | it('ether is not token0', () => { 15 | expect(Ether.onChain(1).equals(t0)).toStrictEqual(false) 16 | }) 17 | it('token1 is not token0', () => { 18 | expect(t1.equals(t0)).toStrictEqual(false) 19 | }) 20 | it('token0 is token0', () => { 21 | expect(t0.equals(t0)).toStrictEqual(true) 22 | }) 23 | it('token0 is equal to another token0', () => { 24 | expect(t0.equals(new Token(1, ADDRESS_ZERO, 18, 'symbol', 'name'))).toStrictEqual(true) 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/entities/currency.ts: -------------------------------------------------------------------------------- 1 | import { NativeCurrency } from './nativeCurrency' 2 | import { Token } from './token' 3 | 4 | export type Currency = NativeCurrency | Token 5 | -------------------------------------------------------------------------------- /src/entities/ether.test.ts: -------------------------------------------------------------------------------- 1 | import { Ether } from './ether' 2 | 3 | describe('Ether', () => { 4 | it('static constructor uses cache', () => { 5 | expect(Ether.onChain(1) === Ether.onChain(1)).toEqual(true) 6 | }) 7 | it('caches once per chain ID', () => { 8 | expect(Ether.onChain(1) !== Ether.onChain(2)).toEqual(true) 9 | }) 10 | it('#equals returns false for diff chains', () => { 11 | expect(Ether.onChain(1).equals(Ether.onChain(2))).toEqual(false) 12 | }) 13 | it('#equals returns true for same chains', () => { 14 | expect(Ether.onChain(1).equals(Ether.onChain(1))).toEqual(true) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/entities/ether.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { Currency } from './currency' 3 | import { NativeCurrency } from './nativeCurrency' 4 | import { Token } from './token' 5 | import { WETH9 } from './weth9' 6 | 7 | /** 8 | * Ether is the main usage of a 'native' currency, i.e. for Ethereum mainnet and all testnets 9 | */ 10 | export class Ether extends NativeCurrency { 11 | protected constructor(chainId: number) { 12 | super(chainId, 18, 'ETH', 'Ether') 13 | } 14 | 15 | public get wrapped(): Token { 16 | const weth9 = WETH9[this.chainId] 17 | invariant(!!weth9, 'WRAPPED') 18 | return weth9 19 | } 20 | 21 | private static _etherCache: { [chainId: number]: Ether } = {} 22 | 23 | public static onChain(chainId: number): Ether { 24 | return this._etherCache[chainId] ?? (this._etherCache[chainId] = new Ether(chainId)) 25 | } 26 | 27 | public equals(other: Currency): boolean { 28 | return other.isNative && other.chainId === this.chainId 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/entities/fractions/currencyAmount.test.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { MaxUint256 } from '../../constants' 3 | import { Ether } from '../ether' 4 | import { Token } from '../token' 5 | import { CurrencyAmount } from './currencyAmount' 6 | import { Percent } from './percent' 7 | 8 | describe('CurrencyAmount', () => { 9 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 10 | 11 | describe('constructor', () => { 12 | it('works', () => { 13 | const token = new Token(1, ADDRESS_ONE, 18) 14 | const amount = CurrencyAmount.fromRawAmount(token, 100) 15 | expect(amount.quotient).toEqual(JSBI.BigInt(100)) 16 | }) 17 | }) 18 | 19 | describe('#quotient', () => { 20 | it('returns the amount after multiplication', () => { 21 | const token = new Token(1, ADDRESS_ONE, 18) 22 | const amount = CurrencyAmount.fromRawAmount(token, 100).multiply(new Percent(15, 100)) 23 | expect(amount.quotient).toEqual(JSBI.BigInt(15)) 24 | }) 25 | }) 26 | 27 | describe('#ether', () => { 28 | it('produces ether amount', () => { 29 | const amount = CurrencyAmount.fromRawAmount(Ether.onChain(1), 100) 30 | expect(amount.quotient).toEqual(JSBI.BigInt(100)) 31 | expect(amount.currency).toEqual(Ether.onChain(1)) 32 | }) 33 | }) 34 | 35 | it('token amount can be max uint256', () => { 36 | const amount = CurrencyAmount.fromRawAmount(new Token(1, ADDRESS_ONE, 18), MaxUint256) 37 | expect(amount.quotient).toEqual(MaxUint256) 38 | }) 39 | it('token amount cannot exceed max uint256', () => { 40 | expect(() => 41 | CurrencyAmount.fromRawAmount(new Token(1, ADDRESS_ONE, 18), JSBI.add(MaxUint256, JSBI.BigInt(1))) 42 | ).toThrow('AMOUNT') 43 | }) 44 | it('token amount quotient cannot exceed max uint256', () => { 45 | expect(() => 46 | CurrencyAmount.fromFractionalAmount( 47 | new Token(1, ADDRESS_ONE, 18), 48 | JSBI.add(JSBI.multiply(MaxUint256, JSBI.BigInt(2)), JSBI.BigInt(2)), 49 | JSBI.BigInt(2) 50 | ) 51 | ).toThrow('AMOUNT') 52 | }) 53 | it('token amount numerator can be gt. uint256 if denominator is gt. 1', () => { 54 | const amount = CurrencyAmount.fromFractionalAmount( 55 | new Token(1, ADDRESS_ONE, 18), 56 | JSBI.add(MaxUint256, JSBI.BigInt(2)), 57 | 2 58 | ) 59 | expect(amount.numerator).toEqual(JSBI.add(JSBI.BigInt(2), MaxUint256)) 60 | }) 61 | 62 | describe('#toFixed', () => { 63 | it('throws for decimals > currency.decimals', () => { 64 | const token = new Token(1, ADDRESS_ONE, 0) 65 | const amount = CurrencyAmount.fromRawAmount(token, 1000) 66 | expect(() => amount.toFixed(3)).toThrow('DECIMALS') 67 | }) 68 | it('is correct for 0 decimals', () => { 69 | const token = new Token(1, ADDRESS_ONE, 0) 70 | const amount = CurrencyAmount.fromRawAmount(token, 123456) 71 | expect(amount.toFixed(0)).toEqual('123456') 72 | }) 73 | it('is correct for 18 decimals', () => { 74 | const token = new Token(1, ADDRESS_ONE, 18) 75 | const amount = CurrencyAmount.fromRawAmount(token, 1e15) 76 | expect(amount.toFixed(9)).toEqual('0.001000000') 77 | }) 78 | }) 79 | 80 | describe('#toSignificant', () => { 81 | it('does not throw for sig figs > currency.decimals', () => { 82 | const token = new Token(1, ADDRESS_ONE, 0) 83 | const amount = CurrencyAmount.fromRawAmount(token, 1000) 84 | expect(amount.toSignificant(3)).toEqual('1000') 85 | }) 86 | it('is correct for 0 decimals', () => { 87 | const token = new Token(1, ADDRESS_ONE, 0) 88 | const amount = CurrencyAmount.fromRawAmount(token, 123456) 89 | expect(amount.toSignificant(4)).toEqual('123400') 90 | }) 91 | it('is correct for 18 decimals', () => { 92 | const token = new Token(1, ADDRESS_ONE, 18) 93 | const amount = CurrencyAmount.fromRawAmount(token, 1e15) 94 | expect(amount.toSignificant(9)).toEqual('0.001') 95 | }) 96 | }) 97 | 98 | describe('#toExact', () => { 99 | it('does not throw for sig figs > currency.decimals', () => { 100 | const token = new Token(1, ADDRESS_ONE, 0) 101 | const amount = CurrencyAmount.fromRawAmount(token, 1000) 102 | expect(amount.toExact()).toEqual('1000') 103 | }) 104 | it('is correct for 0 decimals', () => { 105 | const token = new Token(1, ADDRESS_ONE, 0) 106 | const amount = CurrencyAmount.fromRawAmount(token, 123456) 107 | expect(amount.toExact()).toEqual('123456') 108 | }) 109 | it('is correct for 18 decimals', () => { 110 | const token = new Token(1, ADDRESS_ONE, 18) 111 | const amount = CurrencyAmount.fromRawAmount(token, 123e13) 112 | expect(amount.toExact()).toEqual('0.00123') 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /src/entities/fractions/currencyAmount.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import JSBI from 'jsbi' 3 | import { Currency } from '../currency' 4 | import { Token } from '../token' 5 | import { Fraction } from './fraction' 6 | import _Big from 'big.js' 7 | 8 | import toFormat from 'toformat' 9 | import { BigintIsh, Rounding, MaxUint256 } from '../../constants' 10 | 11 | const Big = toFormat(_Big) 12 | 13 | export class CurrencyAmount extends Fraction { 14 | public readonly currency: T 15 | public readonly decimalScale: JSBI 16 | 17 | /** 18 | * Returns a new currency amount instance from the unitless amount of token, i.e. the raw amount 19 | * @param currency the currency in the amount 20 | * @param rawAmount the raw token or ether amount 21 | */ 22 | public static fromRawAmount(currency: T, rawAmount: BigintIsh): CurrencyAmount { 23 | return new CurrencyAmount(currency, rawAmount) 24 | } 25 | 26 | /** 27 | * Construct a currency amount with a denominator that is not equal to 1 28 | * @param currency the currency 29 | * @param numerator the numerator of the fractional token amount 30 | * @param denominator the denominator of the fractional token amount 31 | */ 32 | public static fromFractionalAmount( 33 | currency: T, 34 | numerator: BigintIsh, 35 | denominator: BigintIsh 36 | ): CurrencyAmount { 37 | return new CurrencyAmount(currency, numerator, denominator) 38 | } 39 | 40 | protected constructor(currency: T, numerator: BigintIsh, denominator?: BigintIsh) { 41 | super(numerator, denominator) 42 | invariant(JSBI.lessThanOrEqual(this.quotient, MaxUint256), 'AMOUNT') 43 | this.currency = currency 44 | this.decimalScale = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(currency.decimals)) 45 | } 46 | 47 | public add(other: CurrencyAmount): CurrencyAmount { 48 | invariant(this.currency.equals(other.currency), 'CURRENCY') 49 | const added = super.add(other) 50 | return CurrencyAmount.fromFractionalAmount(this.currency, added.numerator, added.denominator) 51 | } 52 | 53 | public subtract(other: CurrencyAmount): CurrencyAmount { 54 | invariant(this.currency.equals(other.currency), 'CURRENCY') 55 | const subtracted = super.subtract(other) 56 | return CurrencyAmount.fromFractionalAmount(this.currency, subtracted.numerator, subtracted.denominator) 57 | } 58 | 59 | public multiply(other: Fraction | BigintIsh): CurrencyAmount { 60 | const multiplied = super.multiply(other) 61 | return CurrencyAmount.fromFractionalAmount(this.currency, multiplied.numerator, multiplied.denominator) 62 | } 63 | 64 | public divide(other: Fraction | BigintIsh): CurrencyAmount { 65 | const divided = super.divide(other) 66 | return CurrencyAmount.fromFractionalAmount(this.currency, divided.numerator, divided.denominator) 67 | } 68 | 69 | public toSignificant( 70 | significantDigits: number = 6, 71 | format?: object, 72 | rounding: Rounding = Rounding.ROUND_DOWN 73 | ): string { 74 | return super.divide(this.decimalScale).toSignificant(significantDigits, format, rounding) 75 | } 76 | 77 | public toFixed( 78 | decimalPlaces: number = this.currency.decimals, 79 | format?: object, 80 | rounding: Rounding = Rounding.ROUND_DOWN 81 | ): string { 82 | invariant(decimalPlaces <= this.currency.decimals, 'DECIMALS') 83 | return super.divide(this.decimalScale).toFixed(decimalPlaces, format, rounding) 84 | } 85 | 86 | public toExact(format: object = { groupSeparator: '' }): string { 87 | Big.DP = this.currency.decimals 88 | return new Big(this.quotient.toString()).div(this.decimalScale.toString()).toFormat(format) 89 | } 90 | 91 | public get wrapped(): CurrencyAmount { 92 | if (this.currency.isToken) return this as CurrencyAmount 93 | return CurrencyAmount.fromFractionalAmount(this.currency.wrapped, this.numerator, this.denominator) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/entities/fractions/fraction.test.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { Fraction } from './fraction' 3 | 4 | describe('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 | describe('#asFraction', () => { 122 | it('returns an equivalent but not the same reference fraction', () => { 123 | const f = new Fraction(1, 2) 124 | expect(f.asFraction).toEqual(f) 125 | expect(f === f.asFraction).toEqual(false) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /src/entities/fractions/fraction.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import invariant from 'tiny-invariant' 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 | 9 | const Decimal = toFormat(_Decimal) 10 | const Big = toFormat(_Big) 11 | 12 | const toSignificantRounding = { 13 | [Rounding.ROUND_DOWN]: Decimal.ROUND_DOWN, 14 | [Rounding.ROUND_HALF_UP]: Decimal.ROUND_HALF_UP, 15 | [Rounding.ROUND_UP]: Decimal.ROUND_UP 16 | } 17 | 18 | const toFixedRounding = { 19 | [Rounding.ROUND_DOWN]: RoundingMode.RoundDown, 20 | [Rounding.ROUND_HALF_UP]: RoundingMode.RoundHalfUp, 21 | [Rounding.ROUND_UP]: RoundingMode.RoundUp 22 | } 23 | 24 | export class Fraction { 25 | public readonly numerator: JSBI 26 | public readonly denominator: JSBI 27 | 28 | public constructor(numerator: BigintIsh, denominator: BigintIsh = JSBI.BigInt(1)) { 29 | this.numerator = JSBI.BigInt(numerator) 30 | this.denominator = JSBI.BigInt(denominator) 31 | } 32 | 33 | private static tryParseFraction(fractionish: BigintIsh | Fraction): Fraction { 34 | if (fractionish instanceof JSBI || typeof fractionish === 'number' || typeof fractionish === 'string') 35 | return new Fraction(fractionish) 36 | 37 | if ('numerator' in fractionish && 'denominator' in fractionish) return fractionish 38 | throw new Error('Could not parse fraction') 39 | } 40 | 41 | // performs floor division 42 | public get quotient(): JSBI { 43 | return JSBI.divide(this.numerator, this.denominator) 44 | } 45 | 46 | // remainder after floor division 47 | public get remainder(): Fraction { 48 | return new Fraction(JSBI.remainder(this.numerator, this.denominator), this.denominator) 49 | } 50 | 51 | public invert(): Fraction { 52 | return new Fraction(this.denominator, this.numerator) 53 | } 54 | 55 | public add(other: Fraction | BigintIsh): Fraction { 56 | const otherParsed = Fraction.tryParseFraction(other) 57 | if (JSBI.equal(this.denominator, otherParsed.denominator)) { 58 | return new Fraction(JSBI.add(this.numerator, otherParsed.numerator), this.denominator) 59 | } 60 | return new Fraction( 61 | JSBI.add( 62 | JSBI.multiply(this.numerator, otherParsed.denominator), 63 | JSBI.multiply(otherParsed.numerator, this.denominator) 64 | ), 65 | JSBI.multiply(this.denominator, otherParsed.denominator) 66 | ) 67 | } 68 | 69 | public subtract(other: Fraction | BigintIsh): Fraction { 70 | const otherParsed = Fraction.tryParseFraction(other) 71 | if (JSBI.equal(this.denominator, otherParsed.denominator)) { 72 | return new Fraction(JSBI.subtract(this.numerator, otherParsed.numerator), this.denominator) 73 | } 74 | return new Fraction( 75 | JSBI.subtract( 76 | JSBI.multiply(this.numerator, otherParsed.denominator), 77 | JSBI.multiply(otherParsed.numerator, this.denominator) 78 | ), 79 | JSBI.multiply(this.denominator, otherParsed.denominator) 80 | ) 81 | } 82 | 83 | public lessThan(other: Fraction | BigintIsh): boolean { 84 | const otherParsed = Fraction.tryParseFraction(other) 85 | return JSBI.lessThan( 86 | JSBI.multiply(this.numerator, otherParsed.denominator), 87 | JSBI.multiply(otherParsed.numerator, this.denominator) 88 | ) 89 | } 90 | 91 | public equalTo(other: Fraction | BigintIsh): boolean { 92 | const otherParsed = Fraction.tryParseFraction(other) 93 | return JSBI.equal( 94 | JSBI.multiply(this.numerator, otherParsed.denominator), 95 | JSBI.multiply(otherParsed.numerator, this.denominator) 96 | ) 97 | } 98 | 99 | public greaterThan(other: Fraction | BigintIsh): boolean { 100 | const otherParsed = Fraction.tryParseFraction(other) 101 | return JSBI.greaterThan( 102 | JSBI.multiply(this.numerator, otherParsed.denominator), 103 | JSBI.multiply(otherParsed.numerator, this.denominator) 104 | ) 105 | } 106 | 107 | public multiply(other: Fraction | BigintIsh): Fraction { 108 | const otherParsed = Fraction.tryParseFraction(other) 109 | return new Fraction( 110 | JSBI.multiply(this.numerator, otherParsed.numerator), 111 | JSBI.multiply(this.denominator, otherParsed.denominator) 112 | ) 113 | } 114 | 115 | public divide(other: Fraction | BigintIsh): Fraction { 116 | const otherParsed = Fraction.tryParseFraction(other) 117 | return new Fraction( 118 | JSBI.multiply(this.numerator, otherParsed.denominator), 119 | JSBI.multiply(this.denominator, otherParsed.numerator) 120 | ) 121 | } 122 | 123 | public toSignificant( 124 | significantDigits: number, 125 | format: object = { groupSeparator: '' }, 126 | rounding: Rounding = Rounding.ROUND_HALF_UP 127 | ): string { 128 | invariant(Number.isInteger(significantDigits), `${significantDigits} is not an integer.`) 129 | invariant(significantDigits > 0, `${significantDigits} is not positive.`) 130 | 131 | Decimal.set({ precision: significantDigits + 1, rounding: toSignificantRounding[rounding] }) 132 | const quotient = new Decimal(this.numerator.toString()) 133 | .div(this.denominator.toString()) 134 | .toSignificantDigits(significantDigits) 135 | return quotient.toFormat(quotient.decimalPlaces(), format) 136 | } 137 | 138 | public toFixed( 139 | decimalPlaces: number, 140 | format: object = { groupSeparator: '' }, 141 | rounding: Rounding = Rounding.ROUND_HALF_UP 142 | ): string { 143 | invariant(Number.isInteger(decimalPlaces), `${decimalPlaces} is not an integer.`) 144 | invariant(decimalPlaces >= 0, `${decimalPlaces} is negative.`) 145 | 146 | Big.DP = decimalPlaces 147 | Big.RM = toFixedRounding[rounding] 148 | return new Big(this.numerator.toString()).div(this.denominator.toString()).toFormat(decimalPlaces, format) 149 | } 150 | 151 | /** 152 | * Helper method for converting any super class back to a fraction 153 | */ 154 | public get asFraction(): Fraction { 155 | return new Fraction(this.numerator, this.denominator) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/entities/fractions/index.ts: -------------------------------------------------------------------------------- 1 | export { CurrencyAmount } from './currencyAmount' 2 | export { Fraction } from './fraction' 3 | export { Percent } from './percent' 4 | export { Price } from './price' 5 | -------------------------------------------------------------------------------- /src/entities/fractions/percent.test.ts: -------------------------------------------------------------------------------- 1 | import { Percent } from './percent' 2 | 3 | describe('Percent', () => { 4 | describe('constructor', () => { 5 | it('defaults to 1 denominator', () => { 6 | expect(new Percent(1)).toEqual(new Percent(1, 1)) 7 | }) 8 | }) 9 | describe('#add', () => { 10 | it('returns a percent', () => { 11 | expect(new Percent(1, 100).add(new Percent(2, 100))).toEqual(new Percent(3, 100)) 12 | }) 13 | it('different denominators', () => { 14 | expect(new Percent(1, 25).add(new Percent(2, 100))).toEqual(new Percent(150, 2500)) 15 | }) 16 | }) 17 | describe('#subtract', () => { 18 | it('returns a percent', () => { 19 | expect(new Percent(1, 100).subtract(new Percent(2, 100))).toEqual(new Percent(-1, 100)) 20 | }) 21 | it('different denominators', () => { 22 | expect(new Percent(1, 25).subtract(new Percent(2, 100))).toEqual(new Percent(50, 2500)) 23 | }) 24 | }) 25 | describe('#multiply', () => { 26 | it('returns a percent', () => { 27 | expect(new Percent(1, 100).multiply(new Percent(2, 100))).toEqual(new Percent(2, 10000)) 28 | }) 29 | it('different denominators', () => { 30 | expect(new Percent(1, 25).multiply(new Percent(2, 100))).toEqual(new Percent(2, 2500)) 31 | }) 32 | }) 33 | describe('#divide', () => { 34 | it('returns a percent', () => { 35 | expect(new Percent(1, 100).divide(new Percent(2, 100))).toEqual(new Percent(100, 200)) 36 | }) 37 | it('different denominators', () => { 38 | expect(new Percent(1, 25).divide(new Percent(2, 100))).toEqual(new Percent(100, 50)) 39 | }) 40 | }) 41 | 42 | describe('#toSignificant', () => { 43 | it('returns the value scaled by 100', () => { 44 | expect(new Percent(154, 10_000).toSignificant(3)).toEqual('1.54') 45 | }) 46 | }) 47 | describe('#toFixed', () => { 48 | it('returns the value scaled by 100', () => { 49 | expect(new Percent(154, 10_000).toFixed(2)).toEqual('1.54') 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/entities/fractions/percent.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { BigintIsh, Rounding } from '../../constants' 3 | import { Fraction } from './fraction' 4 | 5 | const ONE_HUNDRED = new Fraction(JSBI.BigInt(100)) 6 | 7 | /** 8 | * Converts a fraction to a percent 9 | * @param fraction the fraction to convert 10 | */ 11 | function toPercent(fraction: Fraction): Percent { 12 | return new Percent(fraction.numerator, fraction.denominator) 13 | } 14 | 15 | export class Percent extends Fraction { 16 | /** 17 | * This boolean prevents a fraction from being interpreted as a Percent 18 | */ 19 | public readonly isPercent: true = true 20 | 21 | add(other: Fraction | BigintIsh): Percent { 22 | return toPercent(super.add(other)) 23 | } 24 | 25 | subtract(other: Fraction | BigintIsh): Percent { 26 | return toPercent(super.subtract(other)) 27 | } 28 | 29 | multiply(other: Fraction | BigintIsh): Percent { 30 | return toPercent(super.multiply(other)) 31 | } 32 | 33 | divide(other: Fraction | BigintIsh): Percent { 34 | return toPercent(super.divide(other)) 35 | } 36 | 37 | public toSignificant(significantDigits: number = 5, format?: object, rounding?: Rounding): string { 38 | return super.multiply(ONE_HUNDRED).toSignificant(significantDigits, format, rounding) 39 | } 40 | 41 | public toFixed(decimalPlaces: number = 2, format?: object, rounding?: Rounding): string { 42 | return super.multiply(ONE_HUNDRED).toFixed(decimalPlaces, format, rounding) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/entities/fractions/price.test.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../token' 2 | import { CurrencyAmount } from './currencyAmount' 3 | import { Price } from './price' 4 | 5 | describe('Price', () => { 6 | const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' 7 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 8 | 9 | const t0 = new Token(1, ADDRESS_ZERO, 18) 10 | const t0_6 = new Token(1, ADDRESS_ZERO, 6) 11 | const t1 = new Token(1, ADDRESS_ONE, 18) 12 | 13 | describe('#constructor', () => { 14 | it('array format works', () => { 15 | const price = new Price(t0, t1, 1, 54321) 16 | expect(price.toSignificant(5)).toEqual('54321') 17 | expect(price.baseCurrency.equals(t0)) 18 | expect(price.quoteCurrency.equals(t1)) 19 | }) 20 | it('object format works', () => { 21 | const price = new Price({ 22 | baseAmount: CurrencyAmount.fromRawAmount(t0, 1), 23 | quoteAmount: CurrencyAmount.fromRawAmount(t1, 54321) 24 | }) 25 | expect(price.toSignificant(5)).toEqual('54321') 26 | expect(price.baseCurrency.equals(t0)) 27 | expect(price.quoteCurrency.equals(t1)) 28 | }) 29 | }) 30 | 31 | describe('#quote', () => { 32 | it('returns correct value', () => { 33 | const price = new Price(t0, t1, 1, 5) 34 | expect(price.quote(CurrencyAmount.fromRawAmount(t0, 10))).toEqual(CurrencyAmount.fromRawAmount(t1, 50)) 35 | }) 36 | }) 37 | 38 | describe('#toSignificant', () => { 39 | it('no decimals', () => { 40 | const p = new Price(t0, t1, 123, 456) 41 | expect(p.toSignificant(4)).toEqual('3.707') 42 | }) 43 | it('no decimals flip ratio', () => { 44 | const p = new Price(t0, t1, 456, 123) 45 | expect(p.toSignificant(4)).toEqual('0.2697') 46 | }) 47 | it('with decimal difference', () => { 48 | const p = new Price(t0_6, t1, 123, 456) 49 | expect(p.toSignificant(4)).toEqual('0.000000000003707') 50 | }) 51 | it('with decimal difference flipped', () => { 52 | const p = new Price(t0_6, t1, 456, 123) 53 | expect(p.toSignificant(4)).toEqual('0.0000000000002697') 54 | }) 55 | it('with decimal difference flipped base quote flipped', () => { 56 | const p = new Price(t1, t0_6, 456, 123) 57 | expect(p.toSignificant(4)).toEqual('269700000000') 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/entities/fractions/price.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import invariant from 'tiny-invariant' 3 | 4 | import { BigintIsh, Rounding } from '../../constants' 5 | import { Currency } from '../currency' 6 | import { Fraction } from './fraction' 7 | import { CurrencyAmount } from './currencyAmount' 8 | 9 | export class Price extends Fraction { 10 | public readonly baseCurrency: TBase // input i.e. denominator 11 | public readonly quoteCurrency: TQuote // output i.e. numerator 12 | public readonly scalar: Fraction // used to adjust the raw fraction w/r/t the decimals of the {base,quote}Token 13 | 14 | /** 15 | * Construct a price, either with the base and quote currency amount, or the 16 | * @param args 17 | */ 18 | public constructor( 19 | ...args: 20 | | [TBase, TQuote, BigintIsh, BigintIsh] 21 | | [{ baseAmount: CurrencyAmount; quoteAmount: CurrencyAmount }] 22 | ) { 23 | let baseCurrency: TBase, quoteCurrency: TQuote, denominator: BigintIsh, numerator: BigintIsh 24 | 25 | if (args.length === 4) { 26 | ;[baseCurrency, quoteCurrency, denominator, numerator] = args 27 | } else { 28 | const result = args[0].quoteAmount.divide(args[0].baseAmount) 29 | ;[baseCurrency, quoteCurrency, denominator, numerator] = [ 30 | args[0].baseAmount.currency, 31 | args[0].quoteAmount.currency, 32 | result.denominator, 33 | result.numerator 34 | ] 35 | } 36 | super(numerator, denominator) 37 | 38 | this.baseCurrency = baseCurrency 39 | this.quoteCurrency = quoteCurrency 40 | this.scalar = new Fraction( 41 | JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(baseCurrency.decimals)), 42 | JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(quoteCurrency.decimals)) 43 | ) 44 | } 45 | 46 | /** 47 | * Flip the price, switching the base and quote currency 48 | */ 49 | public invert(): Price { 50 | return new Price(this.quoteCurrency, this.baseCurrency, this.numerator, this.denominator) 51 | } 52 | 53 | /** 54 | * Multiply the price by another price, returning a new price. The other price must have the same base currency as this price's quote currency 55 | * @param other the other price 56 | */ 57 | public multiply(other: Price): Price { 58 | invariant(this.quoteCurrency.equals(other.baseCurrency), 'TOKEN') 59 | const fraction = super.multiply(other) 60 | return new Price(this.baseCurrency, other.quoteCurrency, fraction.denominator, fraction.numerator) 61 | } 62 | 63 | /** 64 | * Return the amount of quote currency corresponding to a given amount of the base currency 65 | * @param currencyAmount the amount of base currency to quote against the price 66 | */ 67 | public quote(currencyAmount: CurrencyAmount): CurrencyAmount { 68 | invariant(currencyAmount.currency.equals(this.baseCurrency), 'TOKEN') 69 | const result = super.multiply(currencyAmount) 70 | return CurrencyAmount.fromFractionalAmount(this.quoteCurrency, result.numerator, result.denominator) 71 | } 72 | 73 | /** 74 | * Get the value scaled by decimals for formatting 75 | * @private 76 | */ 77 | private get adjustedForDecimals(): Fraction { 78 | return super.multiply(this.scalar) 79 | } 80 | 81 | public toSignificant(significantDigits: number = 6, format?: object, rounding?: Rounding): string { 82 | return this.adjustedForDecimals.toSignificant(significantDigits, format, rounding) 83 | } 84 | 85 | public toFixed(decimalPlaces: number = 4, format?: object, rounding?: Rounding): string { 86 | return this.adjustedForDecimals.toFixed(decimalPlaces, format, rounding) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fractions' 2 | 3 | export * from './currency' 4 | export * from './ether' 5 | export * from './nativeCurrency' 6 | export * from './token' 7 | export * from './weth9' 8 | -------------------------------------------------------------------------------- /src/entities/nativeCurrency.ts: -------------------------------------------------------------------------------- 1 | import { BaseCurrency } from './baseCurrency' 2 | 3 | /** 4 | * Represents the native currency of the chain on which it resides, e.g. 5 | */ 6 | export abstract class NativeCurrency extends BaseCurrency { 7 | public readonly isNative: true = true 8 | public readonly isToken: false = false 9 | } 10 | -------------------------------------------------------------------------------- /src/entities/token.test.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './token' 2 | import { BigNumber } from '@ethersproject/bignumber' 3 | 4 | describe('Token', () => { 5 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 6 | const ADDRESS_TWO = '0x0000000000000000000000000000000000000002' 7 | const DAI_MAINNET = '0x6B175474E89094C44Da98b954EedeAC495271d0F' 8 | 9 | describe('#constructor', () => { 10 | it('fails with invalid address', () => { 11 | expect(() => new Token(3, '0xhello00000000000000000000000000000000002', 18).address).toThrow( 12 | '0xhello00000000000000000000000000000000002 is not a valid address' 13 | ) 14 | }) 15 | it('fails with negative decimals', () => { 16 | expect(() => new Token(3, ADDRESS_ONE, -1).address).toThrow('DECIMALS') 17 | }) 18 | it('fails with 256 decimals', () => { 19 | expect(() => new Token(3, ADDRESS_ONE, 256).address).toThrow('DECIMALS') 20 | }) 21 | it('fails with non-integer decimals', () => { 22 | expect(() => new Token(3, ADDRESS_ONE, 1.5).address).toThrow('DECIMALS') 23 | }) 24 | it('fails with negative FOT fees', () => { 25 | expect( 26 | () => new Token(3, ADDRESS_ONE, 18, undefined, undefined, undefined, BigNumber.from(-1), undefined) 27 | ).toThrow('NON-NEGATIVE FOT FEES') 28 | expect( 29 | () => new Token(3, ADDRESS_ONE, 18, undefined, undefined, undefined, undefined, BigNumber.from(-1)) 30 | ).toThrow('NON-NEGATIVE FOT FEES') 31 | }) 32 | }) 33 | 34 | describe('#constructor with bypassChecksum = true', () => { 35 | const bypassChecksum = true 36 | 37 | it('creates the token with a valid address', () => { 38 | expect(new Token(3, ADDRESS_TWO, 18, undefined, undefined, bypassChecksum).address).toBe(ADDRESS_TWO) 39 | }) 40 | it('fails with invalid address', () => { 41 | expect( 42 | () => 43 | new Token(3, '0xhello00000000000000000000000000000000002', 18, undefined, undefined, bypassChecksum).address 44 | ).toThrow('0xhello00000000000000000000000000000000002 is not a valid address') 45 | }) 46 | it('fails with negative decimals', () => { 47 | expect(() => new Token(3, ADDRESS_ONE, -1, undefined, undefined, bypassChecksum).address).toThrow('DECIMALS') 48 | }) 49 | it('fails with 256 decimals', () => { 50 | expect(() => new Token(3, ADDRESS_ONE, 256, undefined, undefined, bypassChecksum).address).toThrow('DECIMALS') 51 | }) 52 | it('fails with non-integer decimals', () => { 53 | expect(() => new Token(3, ADDRESS_ONE, 1.5, undefined, undefined, bypassChecksum).address).toThrow('DECIMALS') 54 | }) 55 | }) 56 | 57 | describe('#equals', () => { 58 | it('fails if address differs', () => { 59 | expect(new Token(1, ADDRESS_ONE, 18).equals(new Token(1, ADDRESS_TWO, 18))).toBe(false) 60 | }) 61 | 62 | it('false if chain id differs', () => { 63 | expect(new Token(3, ADDRESS_ONE, 18).equals(new Token(1, ADDRESS_ONE, 18))).toBe(false) 64 | }) 65 | 66 | it('true if only decimals differs', () => { 67 | expect(new Token(1, ADDRESS_ONE, 9).equals(new Token(1, ADDRESS_ONE, 18))).toBe(true) 68 | }) 69 | 70 | it('true if address is the same', () => { 71 | expect(new Token(1, ADDRESS_ONE, 18).equals(new Token(1, ADDRESS_ONE, 18))).toBe(true) 72 | }) 73 | 74 | it('true on reference equality', () => { 75 | const token = new Token(1, ADDRESS_ONE, 18) 76 | expect(token.equals(token)).toBe(true) 77 | }) 78 | 79 | it('true even if name/symbol/decimals differ', () => { 80 | const tokenA = new Token(1, ADDRESS_ONE, 9, 'abc', 'def') 81 | const tokenB = new Token(1, ADDRESS_ONE, 18, 'ghi', 'jkl') 82 | expect(tokenA.equals(tokenB)).toBe(true) 83 | }) 84 | 85 | it('true even if one token is checksummed and the other is not', () => { 86 | const tokenA = new Token(1, DAI_MAINNET, 18, 'DAI', undefined, false) 87 | const tokenB = new Token(1, DAI_MAINNET.toLowerCase(), 18, 'DAI', undefined, true) 88 | expect(tokenA.equals(tokenB)).toBe(true) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/entities/token.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import invariant from 'tiny-invariant' 3 | import { checkValidAddress, validateAndParseAddress } from '../utils/validateAndParseAddress' 4 | import { BaseCurrency } from './baseCurrency' 5 | import { Currency } from './currency' 6 | 7 | /** 8 | * Represents an ERC20 token with a unique address and some metadata. 9 | */ 10 | export class Token extends BaseCurrency { 11 | public readonly isNative: false = false 12 | public readonly isToken: true = true 13 | 14 | /** 15 | * The contract address on the chain on which this token lives 16 | */ 17 | public readonly address: string 18 | 19 | /** 20 | * Relevant for fee-on-transfer (FOT) token taxes, 21 | * Not every ERC20 token is FOT token, so this field is optional 22 | */ 23 | public readonly buyFeeBps?: BigNumber 24 | public readonly sellFeeBps?: BigNumber 25 | 26 | /** 27 | * 28 | * @param chainId {@link BaseCurrency#chainId} 29 | * @param address The contract address on the chain on which this token lives 30 | * @param decimals {@link BaseCurrency#decimals} 31 | * @param symbol {@link BaseCurrency#symbol} 32 | * @param name {@link BaseCurrency#name} 33 | * @param bypassChecksum If true it only checks for length === 42, startsWith 0x and contains only hex characters 34 | * @param buyFeeBps Buy fee tax for FOT tokens, in basis points 35 | * @param sellFeeBps Sell fee tax for FOT tokens, in basis points 36 | */ 37 | public constructor( 38 | chainId: number, 39 | address: string, 40 | decimals: number, 41 | symbol?: string, 42 | name?: string, 43 | bypassChecksum?: boolean, 44 | buyFeeBps?: BigNumber, 45 | sellFeeBps?: BigNumber 46 | ) { 47 | super(chainId, decimals, symbol, name) 48 | if (bypassChecksum) { 49 | this.address = checkValidAddress(address) 50 | } else { 51 | this.address = validateAndParseAddress(address) 52 | } 53 | if (buyFeeBps) { 54 | invariant(buyFeeBps.gte(BigNumber.from(0)), 'NON-NEGATIVE FOT FEES') 55 | } 56 | if (sellFeeBps) { 57 | invariant(sellFeeBps.gte(BigNumber.from(0)), 'NON-NEGATIVE FOT FEES') 58 | } 59 | this.buyFeeBps = buyFeeBps 60 | this.sellFeeBps = sellFeeBps 61 | } 62 | 63 | /** 64 | * Returns true if the two tokens are equivalent, i.e. have the same chainId and address. 65 | * @param other other token to compare 66 | */ 67 | public equals(other: Currency): boolean { 68 | return other.isToken && this.chainId === other.chainId && this.address.toLowerCase() === other.address.toLowerCase() 69 | } 70 | 71 | /** 72 | * Returns true if the address of this token sorts before the address of the other token 73 | * @param other other token to compare 74 | * @throws if the tokens have the same address 75 | * @throws if the tokens are on different chains 76 | */ 77 | public sortsBefore(other: Token): boolean { 78 | invariant(this.chainId === other.chainId, 'CHAIN_IDS') 79 | invariant(this.address.toLowerCase() !== other.address.toLowerCase(), 'ADDRESSES') 80 | return this.address.toLowerCase() < other.address.toLowerCase() 81 | } 82 | 83 | /** 84 | * Return this token, which does not need to be wrapped 85 | */ 86 | public get wrapped(): Token { 87 | return this 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/entities/weth9.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './token' 2 | 3 | /** 4 | * Known WETH9 implementation addresses, used in our implementation of Ether#wrapped 5 | */ 6 | export const WETH9: { [chainId: number]: Token } = { 7 | [1]: new Token(1, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapped Ether'), 8 | [3]: new Token(3, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), 9 | [4]: new Token(4, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), 10 | [5]: new Token(5, '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', 18, 'WETH', 'Wrapped Ether'), 11 | [42]: new Token(42, '0xd0A1E359811322d97991E03f863a0C30C2cF029C', 18, 'WETH', 'Wrapped Ether'), 12 | 13 | [10]: new Token(10, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 14 | [69]: new Token(69, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 15 | [11155420]: new Token(11155420, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 16 | 17 | [42161]: new Token(42161, '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 18, 'WETH', 'Wrapped Ether'), 18 | [421611]: new Token(421611, '0xB47e6A5f8b33b3F17603C83a0535A9dcD7E32681', 18, 'WETH', 'Wrapped Ether'), 19 | [421614]: new Token(421614, '0x980B62Da83eFf3D4576C647993b0c1D7faf17c73', 18, 'WETH', 'Wrapped Ether'), 20 | 21 | [8453]: new Token(8453, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'), 22 | 23 | [56]: new Token(56, '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', 18, 'WBNB', 'Wrapped BNB'), 24 | [137]: new Token(137, '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 18, 'WMATIC', 'Wrapped MATIC'), 25 | [43114]: new Token(43114, '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', 18, 'WAVAX', 'Wrapped AVAX') 26 | } 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addresses' 2 | export * from './chains' 3 | export * from './constants' 4 | export * from './entities' 5 | export * from './utils' 6 | -------------------------------------------------------------------------------- /src/utils/computePriceImpact.test.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount, Ether, Percent, Price, Token } from '../entities' 2 | import { computePriceImpact } from './computePriceImpact' 3 | 4 | describe('#computePriceImpact', () => { 5 | const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' 6 | const ADDRESS_ONE = '0x0000000000000000000000000000000000000001' 7 | 8 | const t0 = new Token(1, ADDRESS_ZERO, 18) 9 | const t1 = new Token(1, ADDRESS_ONE, 18) 10 | 11 | it('is correct for zero', () => { 12 | expect( 13 | computePriceImpact( 14 | new Price(Ether.onChain(1), t0, 10, 100), 15 | CurrencyAmount.fromRawAmount(Ether.onChain(1), 10), 16 | CurrencyAmount.fromRawAmount(t0, 100) 17 | ) 18 | ).toEqual(new Percent(0, 10000)) 19 | }) 20 | it('is correct for half output', () => { 21 | expect( 22 | computePriceImpact( 23 | new Price(t0, t1, 10, 100), 24 | CurrencyAmount.fromRawAmount(t0, 10), 25 | CurrencyAmount.fromRawAmount(t1, 50) 26 | ) 27 | ).toEqual(new Percent(5000, 10000)) 28 | }) 29 | it('is negative for more output', () => { 30 | expect( 31 | computePriceImpact( 32 | new Price(t0, t1, 10, 100), 33 | CurrencyAmount.fromRawAmount(t0, 10), 34 | CurrencyAmount.fromRawAmount(t1, 200) 35 | ) 36 | ).toEqual(new Percent(-10000, 10000)) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/utils/computePriceImpact.ts: -------------------------------------------------------------------------------- 1 | import { Currency, CurrencyAmount, Percent, Price } from '../entities' 2 | 3 | /** 4 | * Returns the percent difference between the mid price and the execution price, i.e. price impact. 5 | * @param midPrice mid price before the trade 6 | * @param inputAmount the input amount of the trade 7 | * @param outputAmount the output amount of the trade 8 | */ 9 | export function computePriceImpact( 10 | midPrice: Price, 11 | inputAmount: CurrencyAmount, 12 | outputAmount: CurrencyAmount 13 | ): Percent { 14 | const quotedOutputAmount = midPrice.quote(inputAmount) 15 | // calculate price impact := (exactQuote - outputAmount) / exactQuote 16 | const priceImpact = quotedOutputAmount.subtract(outputAmount).divide(quotedOutputAmount) 17 | return new Percent(priceImpact.numerator, priceImpact.denominator) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { computePriceImpact } from './computePriceImpact' 2 | export { sortedInsert } from './sortedInsert' 3 | export { sqrt } from './sqrt' 4 | export { validateAndParseAddress } from './validateAndParseAddress' 5 | -------------------------------------------------------------------------------- /src/utils/sortedInsert.test.ts: -------------------------------------------------------------------------------- 1 | import { sortedInsert } from './sortedInsert' 2 | 3 | describe('#sortedInsert', () => { 4 | const comp = (a: number, b: number) => a - b 5 | 6 | it('throws if maxSize is 0', () => { 7 | expect(() => sortedInsert([], 1, 0, comp)).toThrow('MAX_SIZE_ZERO') 8 | }) 9 | 10 | it('throws if items.length > maxSize', () => { 11 | expect(() => sortedInsert([1, 2], 1, 1, comp)).toThrow('ITEMS_SIZE') 12 | }) 13 | 14 | it('adds if empty', () => { 15 | const arr: number[] = [] 16 | expect(sortedInsert(arr, 3, 2, comp)).toEqual(null) 17 | expect(arr).toEqual([3]) 18 | }) 19 | 20 | it('adds if not full', () => { 21 | const arr: number[] = [1, 5] 22 | expect(sortedInsert(arr, 3, 3, comp)).toEqual(null) 23 | expect(arr).toEqual([1, 3, 5]) 24 | }) 25 | 26 | it('adds if will not be full after', () => { 27 | const arr: number[] = [1] 28 | expect(sortedInsert(arr, 0, 3, comp)).toEqual(null) 29 | expect(arr).toEqual([0, 1]) 30 | }) 31 | 32 | it('returns add if sorts after last', () => { 33 | const arr = [1, 2, 3] 34 | expect(sortedInsert(arr, 4, 3, comp)).toEqual(4) 35 | expect(arr).toEqual([1, 2, 3]) 36 | }) 37 | 38 | it('removes from end if full', () => { 39 | const arr = [1, 3, 4] 40 | expect(sortedInsert(arr, 2, 3, comp)).toEqual(4) 41 | expect(arr).toEqual([1, 2, 3]) 42 | }) 43 | 44 | it('uses comparator', () => { 45 | const arr = [4, 2, 1] 46 | expect(sortedInsert(arr, 3, 3, (a, b) => comp(a, b) * -1)).toEqual(1) 47 | expect(arr).toEqual([4, 3, 2]) 48 | }) 49 | 50 | describe('maxSize of 1', () => { 51 | it('empty add', () => { 52 | const arr: number[] = [] 53 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(null) 54 | expect(arr).toEqual([3]) 55 | }) 56 | it('full add greater', () => { 57 | const arr: number[] = [2] 58 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(3) 59 | expect(arr).toEqual([2]) 60 | }) 61 | it('full add lesser', () => { 62 | const arr: number[] = [4] 63 | expect(sortedInsert(arr, 3, 1, comp)).toEqual(4) 64 | expect(arr).toEqual([3]) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/utils/sortedInsert.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | 3 | // given an array of items sorted by `comparator`, insert an item into its sort index and constrain the size to 4 | // `maxSize` by removing the last item 5 | export function sortedInsert(items: T[], add: T, maxSize: number, comparator: (a: T, b: T) => number): T | null { 6 | invariant(maxSize > 0, 'MAX_SIZE_ZERO') 7 | // this is an invariant because the interface cannot return multiple removed items if items.length exceeds maxSize 8 | invariant(items.length <= maxSize, 'ITEMS_SIZE') 9 | 10 | // short circuit first item add 11 | if (items.length === 0) { 12 | items.push(add) 13 | return null 14 | } else { 15 | const isFull = items.length === maxSize 16 | // short circuit if full and the additional item does not come before the last item 17 | if (isFull && comparator(items[items.length - 1], add) <= 0) { 18 | return add 19 | } 20 | 21 | let lo = 0, 22 | hi = items.length 23 | 24 | while (lo < hi) { 25 | const mid = (lo + hi) >>> 1 26 | if (comparator(items[mid], add) <= 0) { 27 | lo = mid + 1 28 | } else { 29 | hi = mid 30 | } 31 | } 32 | items.splice(lo, 0, add) 33 | return isFull ? items.pop()! : null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/sqrt.test.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { MaxUint256 } from '../constants' 3 | import { sqrt } from './sqrt' 4 | 5 | describe('#sqrt', () => { 6 | it('correct for 0-1000', () => { 7 | for (let i = 0; i < 1000; i++) { 8 | expect(sqrt(JSBI.BigInt(i))).toEqual(JSBI.BigInt(Math.floor(Math.sqrt(i)))) 9 | } 10 | }) 11 | 12 | describe('correct for all even powers of 2', () => { 13 | for (let i = 0; i < 256; i++) { 14 | it(`2^${i * 2}`, () => { 15 | const root = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(i)) 16 | const rootSquared = JSBI.multiply(root, root) 17 | 18 | expect(sqrt(rootSquared)).toEqual(root) 19 | }) 20 | } 21 | }) 22 | 23 | it('correct for MaxUint256', () => { 24 | expect(sqrt(MaxUint256)).toEqual(JSBI.BigInt('340282366920938463463374607431768211455')) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/sqrt.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import invariant from 'tiny-invariant' 3 | 4 | export const MAX_SAFE_INTEGER = JSBI.BigInt(Number.MAX_SAFE_INTEGER) 5 | 6 | const ZERO = JSBI.BigInt(0) 7 | const ONE = JSBI.BigInt(1) 8 | const TWO = JSBI.BigInt(2) 9 | 10 | /** 11 | * Computes floor(sqrt(value)) 12 | * @param value the value for which to compute the square root, rounded down 13 | */ 14 | export function sqrt(value: JSBI): JSBI { 15 | invariant(JSBI.greaterThanOrEqual(value, ZERO), 'NEGATIVE') 16 | 17 | // rely on built in sqrt if possible 18 | if (JSBI.lessThan(value, MAX_SAFE_INTEGER)) { 19 | return JSBI.BigInt(Math.floor(Math.sqrt(JSBI.toNumber(value)))) 20 | } 21 | 22 | let z: JSBI 23 | let x: JSBI 24 | z = value 25 | x = JSBI.add(JSBI.divide(value, TWO), ONE) 26 | while (JSBI.lessThan(x, z)) { 27 | z = x 28 | x = JSBI.divide(JSBI.add(JSBI.divide(value, x), x), TWO) 29 | } 30 | return z 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/validateAndParseAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { checkValidAddress, validateAndParseAddress } from './validateAndParseAddress' 2 | 3 | describe('#validateAndParseAddress', () => { 4 | it('returns same address if already checksummed', () => { 5 | expect(validateAndParseAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f')).toEqual( 6 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 7 | ) 8 | }) 9 | 10 | it('returns checksummed address if not checksummed', () => { 11 | expect(validateAndParseAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'.toLowerCase())).toEqual( 12 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 13 | ) 14 | }) 15 | 16 | it('throws if not valid', () => { 17 | expect(() => validateAndParseAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6')).toThrow( 18 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6 is not a valid address.' 19 | ) 20 | }) 21 | }) 22 | 23 | describe('#checkValidAddress', () => { 24 | it('returns same address if valid', () => { 25 | expect(checkValidAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f')).toEqual( 26 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 27 | ) 28 | }) 29 | 30 | it('throws if length < 42', () => { 31 | expect(() => checkValidAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6')).toThrow( 32 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6 is not a valid address.' 33 | ) 34 | }) 35 | 36 | it('throws if length > 42', () => { 37 | expect(() => checkValidAddress('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6fA')).toThrow( 38 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6fA is not a valid address.' 39 | ) 40 | }) 41 | 42 | it('throws if it does not start with 0x', () => { 43 | expect(() => checkValidAddress('5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f')).toThrow( 44 | '5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f is not a valid address.' 45 | ) 46 | }) 47 | 48 | it('throws if it is not a HEX string', () => { 49 | expect(() => checkValidAddress('0x5C69bEe701ef814a2X6a3EDD4B1652CB9cc5aA6f')).toThrow( 50 | '0x5C69bEe701ef814a2X6a3EDD4B1652CB9cc5aA6f is not a valid address.' 51 | ) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/utils/validateAndParseAddress.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from '@ethersproject/address' 2 | 3 | /** 4 | * Validates an address and returns the parsed (checksummed) version of that address 5 | * @param address the unchecksummed hex address 6 | */ 7 | export function validateAndParseAddress(address: string): string { 8 | try { 9 | return getAddress(address) 10 | } catch (error) { 11 | throw new Error(`${address} is not a valid address.`) 12 | } 13 | } 14 | 15 | // Checks a string starts with 0x, is 42 characters long and contains only hex characters after 0x 16 | const startsWith0xLen42HexRegex = /^0x[0-9a-fA-F]{40}$/ 17 | 18 | /** 19 | * Checks if an address is valid by checking 0x prefix, length === 42 and hex encoding. 20 | * @param address the unchecksummed hex address 21 | */ 22 | export function checkValidAddress(address: string): string { 23 | if (startsWith0xLen42HexRegex.test(address)) { 24 | return address 25 | } 26 | 27 | throw new Error(`${address} is not a valid address.`) 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "module": "esnext", 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "esModuleInterop": true 22 | } 23 | } 24 | --------------------------------------------------------------------------------