├── .babelrc.json ├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .yarnrc ├── LICENSE ├── Logos ├── C Crypto.com (green).png ├── Lionhead Crypto.com (blue).png └── README.md ├── README.md ├── cypress.json ├── cypress ├── integration │ ├── add-liquidity.test.ts │ ├── landing.test.ts │ ├── lists.test.ts │ ├── pool.test.ts │ ├── remove-liquidity.test.ts │ ├── send.test.ts │ └── swap.test.ts ├── support │ ├── commands.d.ts │ ├── commands.js │ └── index.js └── tsconfig.json ├── local_modules └── swap-sdk │ ├── .babelrc │ ├── .env │ ├── .yarnrc │ ├── package.json │ ├── src │ ├── abis │ │ ├── CroDefiSwapAbi.ts │ │ ├── CroDefiSwapERC20.json │ │ ├── CroDefiSwapFactory.json │ │ ├── CroDefiSwapPair.json │ │ ├── ICroDefiSwapPair.json │ │ └── mainnet │ │ │ ├── CroDefiSwapERC20.json │ │ │ ├── CroDefiSwapFactory.json │ │ │ ├── CroDefiSwapPair.json │ │ │ └── ICroDefiSwapPair.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 │ └── yarn.lock ├── package.json ├── src ├── 451.html ├── assets │ └── images │ │ ├── arrow-down-blue.svg │ │ ├── arrow-down-grey.svg │ │ ├── arrow-right-white.png │ │ ├── arrow-right.svg │ │ ├── blue-loader.svg │ │ ├── checkmark-green.png │ │ ├── circle-grey.svg │ │ ├── circle.svg │ │ ├── coinbaseWalletIcon.svg │ │ ├── cro-defi-swap.svg │ │ ├── cro-icon.png │ │ ├── deFiSwap-avatar-192px.png │ │ ├── deFiSwap-avatar-512px.png │ │ ├── deFiSwap-avatar.svg │ │ ├── dropdown-blue.svg │ │ ├── dropdown.svg │ │ ├── dropup-blue.svg │ │ ├── ethereum-logo.png │ │ ├── fortmaticIcon.png │ │ ├── info-grey.svg │ │ ├── link.svg │ │ ├── logo-white.svg │ │ ├── logo.webp │ │ ├── magnifying-glass.svg │ │ ├── menu.svg │ │ ├── metamask.png │ │ ├── plus-blue.svg │ │ ├── plus-grey.svg │ │ ├── portisIcon.png │ │ ├── question-mark.svg │ │ ├── question.svg │ │ ├── spinner.svg │ │ ├── ticked.svg │ │ ├── token-list │ │ ├── lists-dark.png │ │ └── lists-light.png │ │ ├── trustWallet.png │ │ ├── walletConnectIcon.svg │ │ └── x.svg ├── components │ ├── AccountDetails │ │ ├── Copy.tsx │ │ ├── Transaction.tsx │ │ └── index.tsx │ ├── AddressInputPanel │ │ └── index.tsx │ ├── Button │ │ └── index.tsx │ ├── CROInputField │ │ └── index.tsx │ ├── Card │ │ └── index.tsx │ ├── Column │ │ └── index.tsx │ ├── CurrencyInputPanel │ │ └── index.tsx │ ├── CurrencyLogo │ │ └── index.tsx │ ├── DoubleLogo │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── Identicon │ │ └── index.tsx │ ├── ListLogo │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── Logo │ │ └── index.tsx │ ├── Menu │ │ └── index.tsx │ ├── Modal │ │ └── index.tsx │ ├── NavigationTabs │ │ └── index.tsx │ ├── NumericalInput │ │ └── index.tsx │ ├── Popover │ │ └── index.tsx │ ├── Popups │ │ ├── ListUpdatePopup.tsx │ │ ├── PopupItem.tsx │ │ ├── TransactionPopup.tsx │ │ └── index.tsx │ ├── PositionCard │ │ └── index.tsx │ ├── QuestionHelper │ │ └── index.tsx │ ├── Row │ │ └── index.tsx │ ├── SearchModal │ │ ├── CommonBases.tsx │ │ ├── CurrencyList.tsx │ │ ├── CurrencySearch.tsx │ │ ├── CurrencySearchModal.tsx │ │ ├── SortButton.tsx │ │ ├── filtering.ts │ │ ├── sorting.ts │ │ └── styleds.tsx │ ├── Select │ │ └── index.tsx │ ├── Settings │ │ └── index.tsx │ ├── Slider │ │ └── index.tsx │ ├── Stake │ │ ├── QuestionTooltip.tsx │ │ ├── StakeModal.tsx │ │ ├── StakeSummary.tsx │ │ ├── StakeSummaryTermItem.tsx │ │ └── UnstakeModal.tsx │ ├── TermsAndConditionsModal │ │ └── index.tsx │ ├── Toggle │ │ └── index.tsx │ ├── TokenWarningModal │ │ └── index.tsx │ ├── Tooltip │ │ └── index.tsx │ ├── TransactionConfirmationModal │ │ └── index.tsx │ ├── TransactionSettings │ │ └── index.tsx │ ├── WalletModal │ │ ├── Icons.tsx │ │ ├── Option.tsx │ │ ├── PendingView.tsx │ │ └── index.tsx │ ├── Web3ReactManager │ │ └── index.tsx │ ├── Web3Status │ │ └── index.tsx │ ├── YieldCroInputPanel │ │ └── index.tsx │ ├── YieldIFiatInputPanel │ │ └── index.tsx │ ├── YieldYearRadio │ │ └── index.tsx │ ├── analytics │ │ └── GoogleAnalyticsReporter.tsx │ └── swap │ │ ├── AdvancedSwapDetails.tsx │ │ ├── AdvancedSwapDetailsDropdown.tsx │ │ ├── ConfirmSwapModal.tsx │ │ ├── FormattedPriceImpact.tsx │ │ ├── SwapModalFooter.tsx │ │ ├── SwapModalHeader.tsx │ │ ├── SwapRoute.tsx │ │ ├── TradePrice.tsx │ │ ├── confirmPriceImpactWithoutFee.ts │ │ └── styleds.tsx ├── connectors │ ├── Fortmatic.ts │ ├── NetworkConnector.ts │ ├── fortmatic.d.ts │ └── index.ts ├── constants │ ├── abis │ │ ├── BasicStakeContract.json │ │ ├── PausableTokenMock.json │ │ ├── cryptoAbi.ts │ │ ├── crypto_router_abi.json │ │ ├── ens-public-resolver.json │ │ ├── ens-registrar.json │ │ ├── erc20.json │ │ ├── erc20.ts │ │ ├── erc20_bytes32.json │ │ ├── mainnet │ │ │ ├── BasicStakeContract.json │ │ │ └── crypto_router_abi.json │ │ ├── migrator.json │ │ ├── migrator.ts │ │ ├── unisocks.json │ │ └── weth.json │ ├── croAddress.ts │ ├── croToken.ts │ ├── index.ts │ ├── lists.ts │ ├── multicall │ │ ├── abi.json │ │ └── index.ts │ ├── stakeContractAddress.ts │ └── v1 │ │ ├── index.ts │ │ ├── v1_exchange.json │ │ └── v1_factory.json ├── data │ ├── Allowances.ts │ ├── Reserves.ts │ ├── TotalSupply.ts │ └── V1.ts ├── favicon.png ├── favicon.webp ├── hooks │ ├── Tokens.ts │ ├── Trades.ts │ ├── index.ts │ ├── useApproveCallback.ts │ ├── useContract.ts │ ├── useCopyClipboard.ts │ ├── useDebounce.ts │ ├── useENS.ts │ ├── useENSAddress.ts │ ├── useENSContentHash.ts │ ├── useENSName.ts │ ├── useFetchCalculatorGraphDataCallback.ts │ ├── useFetchListCallback.ts │ ├── useHttpLocations.ts │ ├── useInterval.ts │ ├── useIsWindowVisible.ts │ ├── useLast.ts │ ├── useOnClickOutside.tsx │ ├── useParsedQueryString.ts │ ├── usePrevious.ts │ ├── useSocksBalance.ts │ ├── useSwapCallback.ts │ ├── useToggle.ts │ ├── useToggledVersion.ts │ └── useWrapCallback.ts ├── i18n.ts ├── index.html ├── index.tsx ├── locales_legacy │ ├── de.json │ ├── es-AR.json │ ├── es-US.json │ ├── it-IT.json │ ├── iw.json │ ├── ro.json │ ├── ru.json │ ├── vi.json │ ├── zh-CN.json │ └── zh-TW.json ├── manifest.webmanifest ├── pages │ ├── AddLiquidity │ │ ├── ConfirmAddModalBottom.tsx │ │ ├── PoolPriceBar.tsx │ │ ├── index.tsx │ │ └── redirects.tsx │ ├── App.tsx │ ├── AppBody.tsx │ ├── Boost │ │ └── index.tsx │ ├── Hero.tsx │ ├── Pool │ │ ├── index.tsx │ │ └── styleds.tsx │ ├── PoolFinder │ │ └── index.tsx │ ├── RemoveLiquidity │ │ ├── index.tsx │ │ └── redirects.tsx │ ├── Stake │ │ └── index.tsx │ ├── Swap │ │ ├── index.tsx │ │ └── redirects.tsx │ └── Yield │ │ └── index.tsx ├── react-app-env.d.ts ├── state │ ├── application │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── burn │ │ ├── actions.ts │ │ ├── hooks.ts │ │ └── reducer.ts │ ├── calculator │ │ ├── actions.ts │ │ ├── domain.test.ts │ │ ├── domain.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── global │ │ └── actions.ts │ ├── index.ts │ ├── lists │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── mint │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ └── reducer.ts │ ├── multicall │ │ ├── actions.test.ts │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ ├── updater.test.ts │ │ └── updater.tsx │ ├── stake │ │ ├── accruedCro.test.ts │ │ ├── accruedCro.ts │ │ └── hooks.tsx │ ├── swap │ │ ├── actions.ts │ │ ├── hooks.test.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ └── reducer.ts │ ├── transactions │ │ ├── actions.ts │ │ ├── hooks.tsx │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ ├── updater.test.ts │ │ └── updater.tsx │ ├── user │ │ ├── actions.ts │ │ ├── hooks.tsx │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.tsx │ └── wallet │ │ └── hooks.ts ├── theme │ ├── DarkModeQueryParamReader.tsx │ ├── components.tsx │ ├── index.tsx │ └── styled.d.ts └── utils │ ├── chunkArray.test.ts │ ├── chunkArray.ts │ ├── contenthashToUri.test.skip.ts │ ├── contenthashToUri.ts │ ├── currencyId.ts │ ├── customTitle.tsx │ ├── formatNumber.ts │ ├── getCalculatorGraphData.ts │ ├── getLibrary.ts │ ├── getTokenList.ts │ ├── graphqlClient.ts │ ├── index.test.ts │ ├── index.ts │ ├── isZero.ts │ ├── listVersionLabel.ts │ ├── maxAmountSpend.ts │ ├── multiplierRange.test.ts │ ├── multiplierRange.ts │ ├── noRobot.tsx │ ├── parseENSAddress.test.ts │ ├── parseENSAddress.ts │ ├── prices.test.ts │ ├── prices.ts │ ├── resolveENSContentHash.ts │ ├── retry.test.ts │ ├── retry.ts │ ├── uriToHttp.test.ts │ ├── uriToHttp.ts │ ├── useDebouncedChangeHandler.tsx │ ├── useUSDCPrice.ts │ ├── v1SwapArgument.test.ts │ ├── v1SwapArguments.ts │ └── wrappedCurrency.ts ├── static └── locales │ └── en.json ├── tsconfig.json └── yarn.lock /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["@babel/preset-typescript"], 5 | "plugins": [ 6 | "@babel/plugin-transform-modules-commonjs", 7 | "@babel/plugin-proposal-optional-chaining", 8 | "@babel/plugin-proposal-nullish-coalescing-operator" 9 | ] 10 | } 11 | }, 12 | "plugins": [ 13 | [ 14 | "@babel/plugin-transform-runtime", 15 | { 16 | "regenerator": true 17 | } 18 | ] 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_CHAIN_ID="1" 2 | REACT_APP_COOKIE_BANNER_KEY="" 3 | REACT_APP_NETWORK_URL="" 4 | REACT_APP_CRYPTO_TOKEN_LIST_URL = "https://crypto.com/tokenlist/mainnet" 5 | REACT_APP_CRO_TOKEN_ADDRESS = "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b" 6 | REACT_APP_ONE_YEAR_STAKE = "0x6aba3E56AEb3b95aD64161103D793fAc5F6ce4F7" 7 | REACT_APP_TWO_YEAR_STAKE = "0x26388d599A677C6A8BCc4c113F0A34e6Ced9493D" 8 | REACT_APP_THREE_YEAR_STAKE = "0x0A3c6EEC8408bdED9000DA65AfdB8a8fDA99E253" 9 | REACT_APP_FOUR_YEAR_STAKE = "0x4f2bC163c8758D7F88771496F7B0Afde767045F3" 10 | REACT_APP_ROUTER_ADDRESS = "0xCeB90E4C17d626BE0fACd78b79c9c87d7ca181b3" 11 | REACT_APP_GOOGLE_ANALYTICS_ID = "" 12 | REACT_APP_GTM = "" 13 | THE_GRAPH_URL="https://api.thegraph.com/subgraphs/name/crypto-com/stake-subgraph" 14 | 15 | PUBLIC_URL="" 16 | FACTORY_ADDRESS="0x9DEB29c9a4c7A88a3C0257393b7f3335338D9A9D" 17 | INIT_CODE_HASH="0x69d637e77615df9f235f642acebbdad8963ef35c5523142078c9b8f9d0ceba7e" 18 | 19 | REACT_APP_STAKE_LIST="" 20 | REACT_APP_LIQUIDITY_LIST="" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "ignorePatterns": [ 11 | "node_modules/**/*" 12 | ], 13 | "settings": { 14 | "react": { 15 | "version": "detect" 16 | } 17 | }, 18 | "extends": [ 19 | "plugin:react/recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:react-hooks/recommended", 22 | "prettier/@typescript-eslint", 23 | "plugin:prettier/recommended" 24 | ], 25 | "rules": { 26 | "@typescript-eslint/explicit-function-return-type": "off", 27 | "prettier/prettier": "error", 28 | "@typescript-eslint/no-explicit-any": "off" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | build 11 | dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | /.netlify 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | notes.txt 27 | .idea/ 28 | 29 | .vscode/ 30 | 31 | package-lock.json 32 | 33 | cypress/videos 34 | cypress/screenshots 35 | cypress/fixtures/example.json 36 | 37 | *.swp 38 | 39 | # parcel 40 | .cache 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DeFi Swap 2 | Copyright 2020 Crypto.com 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, version 3 of the License. 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | You should have received a copy of the GNU General Public License 11 | along with this program. If not, see . 12 | Contact: contact@crypto.com 13 | -------------------------------------------------------------------------------- /Logos/C Crypto.com (green).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/Logos/C Crypto.com (green).png -------------------------------------------------------------------------------- /Logos/Lionhead Crypto.com (blue).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/Logos/Lionhead Crypto.com (blue).png -------------------------------------------------------------------------------- /Logos/README.md: -------------------------------------------------------------------------------- 1 | # Logos 2 | 3 | The logos are registered trademarks of Crypto.com and any unauthorised use of the logos or its elements may constitute a breach of such trademark. The name or logos of Crypto.com may not be used or reproduced without the specific, prior written permission of Crypto.com. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeFi Swap Interface 2 | 3 | [![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/) 4 | 5 | As a new DeFi protocol, DeFi Swap is designed to be the best place to swap and farm DeFi coins at the best available rate, leveraging proven and audited protocols, while offering an outstanding incentive program powered by CRO. 6 | 7 | Please visit https://crypto.com/defi for more information! 8 | 9 | ## Submit a token request 10 | 11 | Please see the [swap-token-list](https://github.com/crypto-com/swap-token-list) repository. 12 | 13 | ## Development 14 | 15 | Please update the .env file to ensure full functionality. 16 | 17 | ### Build local modules 18 | 19 | ```bash 20 | cd local_modules/swap_sdk/ 21 | yarn 22 | yarn build 23 | ``` 24 | 25 | ### Install dependencies 26 | 27 | ```bash 28 | yarn 29 | ``` 30 | 31 | ### Run 32 | 33 | ```bash 34 | yarn start 35 | ``` 36 | 37 | ### Configuring the environment 38 | 39 | 1. Make a copy of `.env` named `.env.local` 40 | 2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"` 41 | 3. Change `REACT_APP_NETWORK_URL` to e.g. `"{YOUR_NETWORK_URL}"` 42 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "pluginsFile": false, 4 | "fixturesFolder": false, 5 | "supportFile": "cypress/support/index.js", 6 | "video": false, 7 | "defaultCommandTimeout": 10000 8 | } 9 | -------------------------------------------------------------------------------- /cypress/integration/landing.test.ts: -------------------------------------------------------------------------------- 1 | import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands' 2 | 3 | describe('Landing Page', () => { 4 | beforeEach(() => cy.visit('/')) 5 | it('loads swap page', () => { 6 | cy.get('#swap-page') 7 | }) 8 | 9 | it('redirects to url /swap', () => { 10 | cy.url().should('include', '/swap') 11 | }) 12 | 13 | it('allows navigation to pool', () => { 14 | cy.get('#pool-nav-link').click() 15 | cy.url().should('include', '/pool') 16 | }) 17 | 18 | it('is connected', () => { 19 | cy.get('#web3-status-connected').click() 20 | cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/integration/lists.test.ts: -------------------------------------------------------------------------------- 1 | describe('Swap', () => { 2 | beforeEach(() => { 3 | cy.visit('/swap') 4 | }) 5 | 6 | it('list introduction and selection should be disabled', () => { 7 | cy.get('#swap-currency-output .open-currency-select-button').click() 8 | cy.get('#list-introduction-choose-a-list').should('not.exist') 9 | cy.get('#list-row-tokens-croswap-eth').should('not.exist') 10 | cy.get('#currency-search-change-list-button').should('not.exist') 11 | }) 12 | 13 | it('list crypto.com list should be used as default', () => { 14 | cy.get('#swap-currency-output .open-currency-select-button').click() 15 | //TODO: change to crypto.com 16 | cy.get('#currency-search-selected-list-name').should('contain', 'Crop') 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/integration/pool.test.ts: -------------------------------------------------------------------------------- 1 | describe('Pool', () => { 2 | beforeEach(() => cy.visit('/pool')) 3 | it('add liquidity links to /add/ETH', () => { 4 | cy.get('#join-pool-button').click() 5 | cy.url().should('contain', '/add/ETH') 6 | }) 7 | 8 | it('import pool links to /import', () => { 9 | cy.get('#import-pool-link').click() 10 | cy.url().should('contain', '/find') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/integration/remove-liquidity.test.ts: -------------------------------------------------------------------------------- 1 | describe('Remove Liquidity', () => { 2 | it('redirects', () => { 3 | cy.visit('/remove/0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F-0xaD6D458402F60fD3Bd25163575031ACDce07538D') 4 | cy.url().should( 5 | 'contain', 6 | '/remove/0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F/0xaD6D458402F60fD3Bd25163575031ACDce07538D' 7 | ) 8 | }) 9 | 10 | it('eth remove', () => { 11 | cy.visit('/remove/ETH/0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F') 12 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH') 13 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'CERU') 14 | }) 15 | 16 | it('eth remove swap order', () => { 17 | cy.visit('/remove/0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F/ETH') 18 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'CERU') 19 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH') 20 | }) 21 | 22 | it('loads the two correct tokens', () => { 23 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F') 24 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH') 25 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'CERU') 26 | }) 27 | 28 | it('does not crash if ETH is duplicated', () => { 29 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') 30 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH') 31 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH') 32 | }) 33 | 34 | it('token not in storage is loaded', () => { 35 | cy.visit('/remove/0x5284fAB1638D281ECC18A8d6645aE2D4af6ebe8F-0xaD6D458402F60fD3Bd25163575031ACDce07538D') 36 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'CERU') 37 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'DAI') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /cypress/integration/send.test.ts: -------------------------------------------------------------------------------- 1 | describe('Send', () => { 2 | it('should redirect', () => { 3 | cy.visit('/send') 4 | cy.url().should('include', '/swap') 5 | }) 6 | 7 | it('should redirect with url params', () => { 8 | cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz') 9 | cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/support/commands.d.ts: -------------------------------------------------------------------------------- 1 | export const TEST_ADDRESS_NEVER_USE: string 2 | 3 | export const TEST_ADDRESS_NEVER_USE_SHORTENED: string 4 | 5 | // declare namespace Cypress { 6 | // // eslint-disable-next-line @typescript-eslint/class-name-casing 7 | // interface cy { 8 | // additionalCommands(): void 9 | // } 10 | // } 11 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This file is processed and loaded automatically before your test files. 3 | // 4 | // You can read more here: 5 | // https://on.cypress.io/configuration 6 | // *********************************************************** 7 | 8 | // Import commands.ts using ES2015 syntax: 9 | import './commands' 10 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "lib": [ 7 | "es5", 8 | "dom" 9 | ], 10 | "types": [ 11 | "cypress" 12 | ] 13 | }, 14 | "include": [ 15 | "**/*.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /local_modules/swap-sdk/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [] // "transform-jsbi-to-bigint" 3 | } 4 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/.env: -------------------------------------------------------------------------------- 1 | FACTORY_ADDRESS="0x9DEB29c9a4c7A88a3C0257393b7f3335338D9A9D" 2 | INIT_CODE_HASH="0x69d637e77615df9f235f642acebbdad8963ef35c5523142078c9b8f9d0ceba7e" 3 | REACT_APP_CHAIN_ID = "1" -------------------------------------------------------------------------------- /local_modules/swap-sdk/.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crypto-com-swap/sdk", 3 | "license": "MIT", 4 | "version": "3.0.3", 5 | "description": "🛠 An SDK for building applications on top of Uniswap.", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": "https://github.com/crypto-com/swap-sdk.git", 12 | "keywords": [ 13 | "uniswap", 14 | "ethereum" 15 | ], 16 | "module": "dist/sdk.esm.js", 17 | "scripts": { 18 | "lint": "tsdx lint src test", 19 | "build": "tsdx build", 20 | "start": "tsdx watch", 21 | "test": "tsdx test", 22 | "prepublishOnly": "tsdx build" 23 | }, 24 | "dependencies": { 25 | "big.js": "^5.2.2", 26 | "decimal.js-light": "^2.5.0", 27 | "jsbi": "^3.1.1", 28 | "tiny-invariant": "^1.1.0", 29 | "tiny-warning": "^1.0.3", 30 | "toformat": "^2.0.0" 31 | }, 32 | "peerDependencies": { 33 | "@ethersproject/address": "^5.0.0-beta", 34 | "@ethersproject/contracts": "^5.0.0-beta", 35 | "@ethersproject/networks": "^5.0.0-beta", 36 | "@ethersproject/providers": "^5.0.0-beta", 37 | "@ethersproject/solidity": "^5.0.0-beta" 38 | }, 39 | "devDependencies": { 40 | "@ethersproject/address": "^5.0.2", 41 | "@ethersproject/contracts": "^5.0.2", 42 | "@ethersproject/networks": "^5.0.2", 43 | "@ethersproject/providers": "^5.0.5", 44 | "@ethersproject/solidity": "^5.0.2", 45 | "@types/big.js": "^4.0.5", 46 | "@types/jest": "^24.0.25", 47 | "babel-plugin-transform-jsbi-to-bigint": "^1.3.1", 48 | "tsdx": "^0.12.3" 49 | }, 50 | "engines": { 51 | "node": ">=10" 52 | }, 53 | "prettier": { 54 | "printWidth": 120, 55 | "semi": false, 56 | "singleQuote": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/src/abis/CroDefiSwapAbi.ts: -------------------------------------------------------------------------------- 1 | import CroDefiSwapPair from './CroDefiSwapPair.json' 2 | import CroDefiSwapFactory from './CroDefiSwapFactory.json' 3 | import CroDefiSwapERC20 from './CroDefiSwapERC20.json' 4 | import ICroDefiSwapPair from './ICroDefiSwapPair.json' 5 | 6 | import MainnetCroDefiSwapPair from './mainnet/CroDefiSwapPair.json' 7 | import MainnetCroDefiSwapFactory from './mainnet/CroDefiSwapFactory.json' 8 | import MainnetCroDefiSwapERC20 from './mainnet/CroDefiSwapERC20.json' 9 | import MainnetICroDefiSwapPair from './mainnet/ICroDefiSwapPair.json' 10 | 11 | export const ERC20 = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetCroDefiSwapERC20 : CroDefiSwapERC20 12 | export const CroDefiSwapPairAbi = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetCroDefiSwapPair : CroDefiSwapPair 13 | export const CroDefiSwapFactoryAbi = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetCroDefiSwapFactory : CroDefiSwapFactory 14 | export const ICroDefiSwapPairInterface = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetICroDefiSwapPair : ICroDefiSwapPair 15 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | MAINNET = 1, 8 | ROPSTEN = 3, 9 | RINKEBY = 4, 10 | GÖRLI = 5, 11 | KOVAN = 42 12 | } 13 | 14 | export enum TradeType { 15 | EXACT_INPUT, 16 | EXACT_OUTPUT 17 | } 18 | 19 | export enum Rounding { 20 | ROUND_DOWN, 21 | ROUND_HALF_UP, 22 | ROUND_UP 23 | } 24 | 25 | export const FACTORY_ADDRESS = process.env.FACTORY_ADDRESS || '' 26 | 27 | export const INIT_CODE_HASH = process.env.INIT_CODE_HASH || '' 28 | 29 | export const MINIMUM_LIQUIDITY = JSBI.BigInt(1000) 30 | 31 | // exports for internal consumption 32 | export const ZERO = JSBI.BigInt(0) 33 | export const ONE = JSBI.BigInt(1) 34 | export const TWO = JSBI.BigInt(2) 35 | export const THREE = JSBI.BigInt(3) 36 | export const FIVE = JSBI.BigInt(5) 37 | export const TEN = JSBI.BigInt(10) 38 | export const _100 = JSBI.BigInt(100) 39 | export const _997 = JSBI.BigInt(997) 40 | export const _1000 = JSBI.BigInt(1000) 41 | 42 | export enum SolidityType { 43 | uint8 = 'uint8', 44 | uint256 = 'uint256' 45 | } 46 | 47 | export const SOLIDITY_TYPE_MAXIMA = { 48 | [SolidityType.uint8]: JSBI.BigInt('0xff'), 49 | [SolidityType.uint256]: JSBI.BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 50 | } 51 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'toformat' 2 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/src/entities/currency.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | 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 | public static readonly ETHER: Currency = new Currency(18, 'ETH', 'Ether') 20 | 21 | /** 22 | * Constructs an instance of the base class `Currency`. The only instance of the base class `Currency` is `Currency.ETHER`. 23 | * @param decimals decimals of the currency 24 | * @param symbol symbol of the currency 25 | * @param name of the currency 26 | */ 27 | protected constructor(decimals: number, symbol?: string, name?: string) { 28 | validateSolidityTypeInstance(JSBI.BigInt(decimals), SolidityType.uint8) 29 | 30 | this.decimals = decimals 31 | this.symbol = symbol 32 | this.name = name 33 | } 34 | } 35 | 36 | const ETHER = Currency.ETHER 37 | export { ETHER } 38 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 } 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): CurrencyAmount { 22 | return new CurrencyAmount(ETHER, 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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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].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[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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | export * from './abis/CroDefiSwapAbi' 19 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/test/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { INIT_CODE_HASH } from '../src/constants' 2 | import { CroDefiSwapPairAbi } from '../src/abis/CroDefiSwapAbi' 3 | 4 | import { keccak256 } from '@ethersproject/solidity' 5 | 6 | // this _could_ go in constants, except that it would cost every consumer of the sdk the CPU to compute the hash 7 | // and load the JSON. 8 | const COMPUTED_INIT_CODE_HASH = keccak256(['bytes'], [`0x${CroDefiSwapPairAbi.bytecode}`]) 9 | 10 | describe('constants', () => { 11 | describe('INIT_CODE_HASH', () => { 12 | it('matches computed bytecode hash', () => { 13 | expect(COMPUTED_INIT_CODE_HASH).toEqual(INIT_CODE_HASH) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /local_modules/swap-sdk/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 | -------------------------------------------------------------------------------- /src/451.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unavailable For Legal Reasons 6 | 7 | 8 |

Unavailable For Legal Reasons

9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/arrow-down-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-down-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-right-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/arrow-right-white.png -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/blue-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/checkmark-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/checkmark-green.png -------------------------------------------------------------------------------- /src/assets/images/circle-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/cro-defi-swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/images/cro-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/cro-icon.png -------------------------------------------------------------------------------- /src/assets/images/deFiSwap-avatar-192px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/deFiSwap-avatar-192px.png -------------------------------------------------------------------------------- /src/assets/images/deFiSwap-avatar-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/deFiSwap-avatar-512px.png -------------------------------------------------------------------------------- /src/assets/images/dropdown-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropup-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ethereum-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/ethereum-logo.png -------------------------------------------------------------------------------- /src/assets/images/fortmaticIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/fortmaticIcon.png -------------------------------------------------------------------------------- /src/assets/images/info-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/images/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/logo.webp -------------------------------------------------------------------------------- /src/assets/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/metamask.png -------------------------------------------------------------------------------- /src/assets/images/plus-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/plus-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/portisIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/portisIcon.png -------------------------------------------------------------------------------- /src/assets/images/question-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/images/ticked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | A3336E24-A8A8-4946-B2C8-10D8DD7B9B01 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/token-list/lists-dark.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/token-list/lists-light.png -------------------------------------------------------------------------------- /src/assets/images/trustWallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/assets/images/trustWallet.png -------------------------------------------------------------------------------- /src/assets/images/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AccountDetails/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import useCopyClipboard from '../../hooks/useCopyClipboard' 4 | 5 | import { LinkStyledButton } from '../../theme' 6 | import { CheckCircle, Copy } from 'react-feather' 7 | 8 | const CopyIcon = styled(LinkStyledButton)` 9 | color: ${({ theme }) => theme.text3}; 10 | flex-shrink: 0; 11 | display: flex; 12 | text-decoration: none; 13 | font-size: 0.825rem; 14 | :hover, 15 | :active, 16 | :focus { 17 | text-decoration: none; 18 | color: ${({ theme }) => theme.text2}; 19 | } 20 | ` 21 | const TransactionStatusText = styled.span` 22 | margin-left: 0.25rem; 23 | font-size: 0.825rem; 24 | ${({ theme }) => theme.flexRowNoWrap}; 25 | align-items: center; 26 | ` 27 | 28 | export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { 29 | const [isCopied, setCopied] = useCopyClipboard() 30 | 31 | return ( 32 | setCopied(props.toCopy)}> 33 | {isCopied ? ( 34 | 35 | 36 | Copied 37 | 38 | ) : ( 39 | 40 | 41 | 42 | )} 43 | {isCopied ? '' : props.children} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/AccountDetails/Transaction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { CheckCircle, Triangle } from 'react-feather' 4 | 5 | import { useActiveWeb3React } from '../../hooks' 6 | import { getEtherscanLink } from '../../utils' 7 | import { ExternalLink } from '../../theme' 8 | import { useAllTransactions } from '../../state/transactions/hooks' 9 | import { RowFixed } from '../Row' 10 | import Loader from '../Loader' 11 | 12 | const TransactionWrapper = styled.div`` 13 | 14 | const TransactionStatusText = styled.div` 15 | margin-right: 0.5rem; 16 | display: flex; 17 | align-items: center; 18 | :hover { 19 | text-decoration: underline; 20 | } 21 | ` 22 | 23 | const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | text-decoration: none !important; 28 | border-radius: 0.5rem; 29 | padding: 0.25rem 0rem; 30 | font-weight: 500; 31 | font-size: 0.825rem; 32 | color: ${({ theme }) => theme.primary1}; 33 | ` 34 | 35 | const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` 36 | color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)}; 37 | ` 38 | 39 | export default function Transaction({ hash }: { hash: string }) { 40 | const { chainId } = useActiveWeb3React() 41 | const allTransactions = useAllTransactions() 42 | 43 | const tx = allTransactions?.[hash] 44 | const summary = tx?.summary 45 | const pending = !tx?.receipt 46 | const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined') 47 | 48 | if (!chainId) return null 49 | 50 | return ( 51 | 52 | 53 | 54 | {summary ?? hash} ↗ 55 | 56 | 57 | {pending ? : success ? : } 58 | 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { CardProps, Text } from 'rebass' 4 | import { Box } from 'rebass/styled-components' 5 | 6 | const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>` 7 | width: 100%; 8 | border-radius: 16px; 9 | padding: 1.25rem; 10 | padding: ${({ padding }) => padding}; 11 | border: ${({ border }) => border}; 12 | border-radius: ${({ borderRadius }) => borderRadius}; 13 | ` 14 | export default Card 15 | 16 | export const LightCard = styled(Card)` 17 | border: 1px solid ${({ theme }) => theme.bg2}; 18 | background-color: ${({ theme }) => theme.bg1}; 19 | ` 20 | 21 | export const GreyCard = styled(Card)` 22 | background-color: ${({ theme }) => theme.bg3}; 23 | ` 24 | 25 | export const OutlineCard = styled(Card)` 26 | border: 1px solid ${({ theme }) => theme.bg3}; 27 | ` 28 | 29 | export const YellowCard = styled(Card)` 30 | background-color: rgba(243, 132, 30, 0.05); 31 | color: ${({ theme }) => theme.yellow2}; 32 | font-weight: 500; 33 | ` 34 | 35 | export const PinkCard = styled(Card)` 36 | background-color: rgba(255, 0, 122, 0.03); 37 | color: ${({ theme }) => theme.primary1}; 38 | font-weight: 500; 39 | ` 40 | 41 | const BlueCardStyled = styled(Card)` 42 | background-color: ${({ theme }) => theme.primary5}; 43 | color: ${({ theme }) => theme.primary1}; 44 | border-radius: 12px; 45 | width: fit-content; 46 | ` 47 | 48 | export const BlueCard = ({ children, ...rest }: CardProps) => { 49 | return ( 50 | 51 | 52 | {children} 53 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Column/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Column = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | ` 8 | export const ColumnCenter = styled(Column)` 9 | width: 100%; 10 | align-items: center; 11 | ` 12 | 13 | export const AutoColumn = styled.div<{ 14 | gap?: 'sm' | 'md' | 'lg' | string 15 | justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between' 16 | }>` 17 | display: grid; 18 | grid-auto-rows: auto; 19 | grid-row-gap: ${({ gap }) => (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap}; 20 | justify-items: ${({ justify }) => justify && justify}; 21 | ` 22 | 23 | export default Column 24 | -------------------------------------------------------------------------------- /src/components/CurrencyLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Currency, ETHER, Token } from 'swap-sdk' 2 | import React, { useMemo } from 'react' 3 | import styled from 'styled-components' 4 | 5 | import EthereumLogo from '../../assets/images/ethereum-logo.png' 6 | import useHttpLocations from '../../hooks/useHttpLocations' 7 | import { WrappedTokenInfo } from '../../state/lists/hooks' 8 | import Logo from '../Logo' 9 | 10 | const getTokenLogoURL = (symbol: string) => { 11 | if (symbol == 'WETH') return `https://crypto.com/price/coin-data/icon/ETH/color_icon.png` 12 | else return `https://crypto.com/price/coin-data/icon/${symbol}/color_icon.png` 13 | } 14 | 15 | const StyledEthereumLogo = styled.img<{ size: string }>` 16 | width: ${({ size }) => size}; 17 | height: ${({ size }) => size}; 18 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); 19 | border-radius: 24px; 20 | ` 21 | 22 | const StyledLogo = styled(Logo)<{ size: string }>` 23 | width: ${({ size }) => size}; 24 | height: ${({ size }) => size}; 25 | ` 26 | 27 | export default function CurrencyLogo({ 28 | currency, 29 | size = '24px', 30 | style, 31 | logUrl 32 | }: { 33 | currency?: Currency 34 | size?: string 35 | style?: React.CSSProperties 36 | logUrl?: string | undefined 37 | }) { 38 | const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined) 39 | 40 | const srcs: string[] = useMemo(() => { 41 | if (currency === ETHER) return [] 42 | if (currency instanceof Token) { 43 | if (currency instanceof WrappedTokenInfo) { 44 | return [...uriLocations, getTokenLogoURL(currency.symbol)] 45 | } 46 | 47 | return [getTokenLogoURL(currency.symbol)] 48 | } 49 | return [] 50 | }, [currency, uriLocations]) 51 | if (currency === ETHER) { 52 | return 53 | } 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /src/components/DoubleLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Currency } from 'swap-sdk' 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import CurrencyLogo from '../CurrencyLogo' 5 | 6 | const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>` 7 | position: relative; 8 | display: flex; 9 | flex-direction: row; 10 | margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'}; 11 | ` 12 | 13 | interface DoubleCurrencyLogoProps { 14 | margin?: boolean 15 | size?: number 16 | currency0?: Currency 17 | currency1?: Currency 18 | } 19 | 20 | const HigherLogo = styled(CurrencyLogo)` 21 | z-index: 2; 22 | ` 23 | const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>` 24 | position: absolute; 25 | left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'}; 26 | ` 27 | 28 | export default function DoubleCurrencyLogo({ 29 | currency0, 30 | currency1, 31 | size = 16, 32 | margin = false 33 | }: DoubleCurrencyLogoProps) { 34 | return ( 35 | 36 | {currency0 && } 37 | {currency1 && } 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Identicon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | 3 | import styled from 'styled-components' 4 | 5 | import { useActiveWeb3React } from '../../hooks' 6 | import Jazzicon from 'jazzicon' 7 | 8 | const StyledIdenticonContainer = styled.div` 9 | height: 1rem; 10 | width: 1rem; 11 | border-radius: 1.125rem; 12 | background-color: ${({ theme }) => theme.bg4}; 13 | ` 14 | 15 | export default function Identicon() { 16 | const ref = useRef() 17 | 18 | const { account } = useActiveWeb3React() 19 | 20 | useEffect(() => { 21 | if (account && ref.current) { 22 | ref.current.innerHTML = '' 23 | ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16))) 24 | } 25 | }, [account]) 26 | 27 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ListLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import useHttpLocations from '../../hooks/useHttpLocations' 4 | 5 | import Logo from '../Logo' 6 | 7 | const StyledListLogo = styled(Logo)<{ size: string }>` 8 | width: ${({ size }) => size}; 9 | height: ${({ size }) => size}; 10 | ` 11 | 12 | export default function ListLogo({ 13 | logoURI, 14 | style, 15 | size = '24px', 16 | alt 17 | }: { 18 | logoURI: string 19 | size?: string 20 | style?: React.CSSProperties 21 | alt?: string 22 | }) { 23 | const srcs: string[] = useHttpLocations(logoURI) 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styled, { keyframes } from 'styled-components' 4 | 5 | const rotate = keyframes` 6 | from { 7 | transform: rotate(0deg); 8 | } 9 | to { 10 | transform: rotate(360deg); 11 | } 12 | ` 13 | 14 | const StyledSVG = styled.svg<{ size: string; stroke?: string }>` 15 | animation: 2s ${rotate} linear infinite; 16 | height: ${({ size }) => size}; 17 | width: ${({ size }) => size}; 18 | path { 19 | stroke: ${({ stroke, theme }) => stroke ?? theme.primary1}; 20 | } 21 | ` 22 | 23 | /** 24 | * Takes in custom size and stroke for circle color, default to primary color as fill, 25 | * need ...rest for layered styles on top 26 | */ 27 | export default function Loader({ size = '16px', stroke, ...rest }: { size?: string; stroke?: string }) { 28 | return ( 29 | 30 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { HelpCircle } from 'react-feather' 3 | import { ImageProps } from 'rebass' 4 | 5 | const BAD_SRCS: { [tokenAddress: string]: true } = {} 6 | 7 | export interface LogoProps extends Pick { 8 | srcs: string[] 9 | } 10 | 11 | /** 12 | * Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert 13 | */ 14 | export default function Logo({ srcs, alt, ...rest }: LogoProps) { 15 | const [, refresh] = useState(0) 16 | 17 | const src: string | undefined = srcs.find(src => !BAD_SRCS[src]) 18 | 19 | if (src) { 20 | return ( 21 | {alt} { 26 | if (src) BAD_SRCS[src] = true 27 | refresh(i => i + 1) 28 | }} 29 | /> 30 | ) 31 | } 32 | 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Popups/TransactionPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { AlertCircle, CheckCircle } from 'react-feather' 3 | import styled, { ThemeContext } from 'styled-components' 4 | import { useActiveWeb3React } from '../../hooks' 5 | import { TYPE } from '../../theme' 6 | import { ExternalLink } from '../../theme/components' 7 | import { getEtherscanLink } from '../../utils' 8 | import { AutoColumn } from '../Column' 9 | import { AutoRow } from '../Row' 10 | 11 | const RowNoFlex = styled(AutoRow)` 12 | flex-wrap: nowrap; 13 | ` 14 | 15 | export default function TransactionPopup({ 16 | hash, 17 | success, 18 | summary 19 | }: { 20 | hash: string 21 | success?: boolean 22 | summary?: string 23 | }) { 24 | const { chainId } = useActiveWeb3React() 25 | 26 | const theme = useContext(ThemeContext) 27 | 28 | return ( 29 | 30 |
31 | {success ? : } 32 |
33 | 34 | {summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)} 35 | {chainId && ( 36 | View on Etherscan 37 | )} 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Popups/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useActivePopups } from '../../state/application/hooks' 4 | import { AutoColumn } from '../Column' 5 | import PopupItem from './PopupItem' 6 | 7 | const MobilePopupWrapper = styled.div<{ height: string | number }>` 8 | position: relative; 9 | max-width: 100%; 10 | height: ${({ height }) => height}; 11 | margin: ${({ height }) => (height ? '0 auto;' : 0)}; 12 | margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; 13 | 14 | display: none; 15 | ${({ theme }) => theme.mediaWidth.upToSmall` 16 | display: block; 17 | `}; 18 | ` 19 | 20 | const MobilePopupInner = styled.div` 21 | height: 99%; 22 | overflow-x: auto; 23 | overflow-y: hidden; 24 | display: flex; 25 | flex-direction: row; 26 | -webkit-overflow-scrolling: touch; 27 | ::-webkit-scrollbar { 28 | display: none; 29 | } 30 | ` 31 | 32 | const FixedPopupColumn = styled(AutoColumn)` 33 | position: fixed; 34 | top: 64px; 35 | right: 1rem; 36 | max-width: 355px !important; 37 | width: 100%; 38 | z-index: 2; 39 | 40 | ${({ theme }) => theme.mediaWidth.upToSmall` 41 | display: none; 42 | `}; 43 | ` 44 | 45 | export default function Popups() { 46 | // get all popups 47 | const activePopups = useActivePopups() 48 | 49 | return ( 50 | <> 51 | 52 | {activePopups.map(item => ( 53 | 54 | ))} 55 | 56 | 0 ? 'fit-content' : 0}> 57 | 58 | {activePopups // reverse so new items up front 59 | .slice(0) 60 | .reverse() 61 | .map(item => ( 62 | 63 | ))} 64 | 65 | 66 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/QuestionHelper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import { HelpCircle as Question } from 'react-feather' 3 | import styled from 'styled-components' 4 | import Tooltip from '../Tooltip' 5 | 6 | const QuestionWrapper = styled.div` 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 0.2rem; 11 | border: none; 12 | background: none; 13 | outline: none; 14 | cursor: default; 15 | border-radius: 36px; 16 | background-color: ${({ theme }) => theme.bg2}; 17 | color: ${({ theme }) => theme.text2}; 18 | 19 | :hover, 20 | :focus { 21 | opacity: 0.7; 22 | } 23 | ` 24 | 25 | export default function QuestionHelper({ text }: { text: string }) { 26 | const [show, setShow] = useState(false) 27 | 28 | const open = useCallback(() => setShow(true), [setShow]) 29 | const close = useCallback(() => setShow(false), [setShow]) 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Row/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Box } from 'rebass/styled-components' 3 | 4 | const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>` 5 | width: 100%; 6 | display: flex; 7 | padding: 0; 8 | align-items: ${({ align }) => (align ? align : 'center')}; 9 | padding: ${({ padding }) => padding}; 10 | border: ${({ border }) => border}; 11 | border-radius: ${({ borderRadius }) => borderRadius}; 12 | ` 13 | 14 | export const RowBetween = styled(Row)` 15 | justify-content: space-between; 16 | ` 17 | 18 | export const RowFlat = styled.div` 19 | display: flex; 20 | align-items: flex-end; 21 | ` 22 | 23 | export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` 24 | flex-wrap: wrap; 25 | margin: ${({ gap }) => gap && `-${gap}`}; 26 | justify-content: ${({ justify }) => justify && justify}; 27 | 28 | & > * { 29 | margin: ${({ gap }) => gap} !important; 30 | } 31 | ` 32 | 33 | export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` 34 | width: fit-content; 35 | margin: ${({ gap }) => gap && `-${gap}`}; 36 | ` 37 | 38 | export default Row 39 | -------------------------------------------------------------------------------- /src/components/SearchModal/CurrencySearchModal.tsx: -------------------------------------------------------------------------------- 1 | import { Currency } from 'swap-sdk' 2 | import React, { useCallback, useEffect, useState } from 'react' 3 | import { useDispatch } from 'react-redux' 4 | import { selectList } from '../../state/lists/actions' 5 | import { AppDispatch } from '../../state' 6 | import { DEFAULT_TOKEN_LIST_URL } from '../../constants/lists' 7 | 8 | import useLast from '../../hooks/useLast' 9 | import Modal from '../Modal' 10 | import { CurrencySearch } from './CurrencySearch' 11 | 12 | interface CurrencySearchModalProps { 13 | isOpen: boolean 14 | onDismiss: () => void 15 | selectedCurrency?: Currency | null 16 | onCurrencySelect: (currency: Currency) => void 17 | otherSelectedCurrency?: Currency | null 18 | showCommonBases?: boolean 19 | } 20 | 21 | export default function CurrencySearchModal({ 22 | isOpen, 23 | onDismiss, 24 | onCurrencySelect, 25 | selectedCurrency, 26 | otherSelectedCurrency, 27 | showCommonBases = false 28 | }: CurrencySearchModalProps) { 29 | const [listView, setListView] = useState(false) 30 | const dispatch = useDispatch() 31 | const lastOpen = useLast(isOpen) 32 | 33 | useEffect(() => { 34 | if (dispatch && selectList && isOpen && !lastOpen) { 35 | dispatch(selectList(DEFAULT_TOKEN_LIST_URL)) 36 | setListView(false) 37 | } 38 | }, [dispatch, selectList, isOpen, lastOpen]) 39 | 40 | const handleCurrencySelect = useCallback( 41 | (currency: Currency) => { 42 | onCurrencySelect(currency) 43 | onDismiss() 44 | }, 45 | [onDismiss, onCurrencySelect] 46 | ) 47 | 48 | return ( 49 | 50 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/SearchModal/SortButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text } from 'rebass' 3 | import styled from 'styled-components' 4 | import { RowFixed } from '../Row' 5 | 6 | export const FilterWrapper = styled(RowFixed)` 7 | padding: 8px; 8 | background-color: ${({ theme }) => theme.bg2}; 9 | color: ${({ theme }) => theme.text1}; 10 | border-radius: 8px; 11 | user-select: none; 12 | & > * { 13 | user-select: none; 14 | } 15 | :hover { 16 | cursor: pointer; 17 | } 18 | ` 19 | 20 | export default function SortButton({ 21 | toggleSortOrder, 22 | ascending 23 | }: { 24 | toggleSortOrder: () => void 25 | ascending: boolean 26 | }) { 27 | return ( 28 | 29 | 30 | {ascending ? '↑' : '↓'} 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/SearchModal/filtering.ts: -------------------------------------------------------------------------------- 1 | import { isAddress } from '../../utils' 2 | import { Token } from 'swap-sdk' 3 | 4 | export function filterTokens(tokens: Token[], search: string): Token[] { 5 | if (search.length === 0) return tokens 6 | 7 | const searchingAddress = isAddress(search) 8 | 9 | if (searchingAddress) { 10 | return tokens.filter(token => token.address === searchingAddress) 11 | } 12 | 13 | const lowerSearchParts = search 14 | .toLowerCase() 15 | .split(/\s+/) 16 | .filter(s => s.length > 0) 17 | 18 | if (lowerSearchParts.length === 0) { 19 | return tokens 20 | } 21 | 22 | const matchesSearch = (s: string): boolean => { 23 | const sParts = s 24 | .toLowerCase() 25 | .split(/\s+/) 26 | .filter(s => s.length > 0) 27 | 28 | return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p))) 29 | } 30 | 31 | return tokens.filter(token => { 32 | const { symbol, name } = token 33 | 34 | return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name)) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/SearchModal/sorting.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenAmount } from 'swap-sdk' 2 | import { useMemo } from 'react' 3 | import { useAllTokenBalances } from '../../state/wallet/hooks' 4 | 5 | // compare two token amounts with highest one coming first 6 | function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) { 7 | if (balanceA && balanceB) { 8 | return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1 9 | } else if (balanceA && balanceA.greaterThan('0')) { 10 | return -1 11 | } else if (balanceB && balanceB.greaterThan('0')) { 12 | return 1 13 | } 14 | return 0 15 | } 16 | 17 | function getTokenComparator(balances: { 18 | [tokenAddress: string]: TokenAmount | undefined 19 | }): (tokenA: Token, tokenB: Token) => number { 20 | return function sortTokens(tokenA: Token, tokenB: Token): number { 21 | // -1 = a is first 22 | // 1 = b is first 23 | 24 | // sort by balances 25 | const balanceA = balances[tokenA.address] 26 | const balanceB = balances[tokenB.address] 27 | 28 | const balanceComp = balanceComparator(balanceA, balanceB) 29 | if (balanceComp !== 0) return balanceComp 30 | 31 | if (tokenA.symbol && tokenB.symbol) { 32 | // sort by symbol 33 | return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1 34 | } else { 35 | return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0 36 | } 37 | } 38 | } 39 | 40 | export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number { 41 | const balances = useAllTokenBalances() 42 | const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances]) 43 | return useMemo(() => { 44 | if (inverted) { 45 | return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1 46 | } else { 47 | return comparator 48 | } 49 | }, [inverted, comparator]) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/SearchModal/styleds.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { AutoColumn } from '../Column' 3 | import { RowBetween, RowFixed } from '../Row' 4 | 5 | export const ModalInfo = styled.div` 6 | ${({ theme }) => theme.flexRowNoWrap} 7 | align-items: center; 8 | padding: 1rem 1rem; 9 | margin: 0.25rem 0.5rem; 10 | justify-content: center; 11 | flex: 1; 12 | user-select: none; 13 | ` 14 | 15 | export const FadedSpan = styled(RowFixed)` 16 | color: ${({ theme }) => theme.primary1}; 17 | font-size: 14px; 18 | ` 19 | 20 | export const PaddedColumn = styled(AutoColumn)` 21 | padding: 20px; 22 | padding-bottom: 12px; 23 | ` 24 | 25 | export const MenuItem = styled(RowBetween)` 26 | padding: 4px 20px; 27 | height: 56px; 28 | display: grid; 29 | grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px); 30 | grid-gap: 16px; 31 | cursor: ${({ disabled }) => !disabled && 'pointer'}; 32 | pointer-events: ${({ disabled }) => disabled && 'none'}; 33 | :hover { 34 | background-color: ${({ theme, disabled }) => !disabled && theme.bg2}; 35 | } 36 | opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)}; 37 | ` 38 | 39 | export const SearchInput = styled.input` 40 | position: relative; 41 | display: flex; 42 | padding: 16px; 43 | align-items: center; 44 | width: 100%; 45 | white-space: nowrap; 46 | background: none; 47 | border: none; 48 | outline: none; 49 | border-radius: 20px; 50 | color: ${({ theme }) => theme.text1}; 51 | border-style: solid; 52 | border: 1px solid ${({ theme }) => theme.bg3}; 53 | -webkit-appearance: none; 54 | 55 | font-size: 18px; 56 | 57 | ::placeholder { 58 | color: ${({ theme }) => theme.text3}; 59 | } 60 | transition: border 100ms; 61 | :focus { 62 | border: 1px solid ${({ theme }) => theme.primary1}; 63 | outline: none; 64 | } 65 | ` 66 | export const Separator = styled.div` 67 | width: 100%; 68 | height: 1px; 69 | background-color: ${({ theme }) => theme.bg2}; 70 | ` 71 | 72 | export const SeparatorDark = styled.div` 73 | width: 100%; 74 | height: 1px; 75 | background-color: ${({ theme }) => theme.bg3}; 76 | ` 77 | -------------------------------------------------------------------------------- /src/components/Stake/QuestionTooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import { HelpCircle as Question } from 'react-feather' 3 | import styled from 'styled-components' 4 | import Tooltip from '../Tooltip' 5 | 6 | const QuestionWrapper = styled.div` 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 0.2rem; 11 | border: none; 12 | background: none; 13 | outline: none; 14 | cursor: default; 15 | border-radius: 36px; 16 | background-color: ${({ theme }) => theme.bg2}; 17 | color: ${({ theme }) => theme.text2}; 18 | 19 | :hover, 20 | :focus { 21 | opacity: 0.7; 22 | } 23 | ` 24 | 25 | export default function QuestionHelper({ text }: { text: string | React.ReactNode }) { 26 | const [show, setShow] = useState(false) 27 | 28 | const open = useCallback(() => setShow(true), [setShow]) 29 | const close = useCallback(() => setShow(false), [setShow]) 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>` 5 | padding: 0.25rem 0.5rem; 6 | border-radius: 14px; 7 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')}; 8 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 9 | font-size: 0.825rem; 10 | font-weight: 400; 11 | ` 12 | 13 | const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 14 | border-radius: 16px; 15 | border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; 16 | display: flex; 17 | width: fit-content; 18 | cursor: pointer; 19 | outline: none; 20 | padding: 0; 21 | background-color: transparent; 22 | ` 23 | 24 | export interface ToggleProps { 25 | id?: string 26 | isActive: boolean 27 | toggle: () => void 28 | } 29 | 30 | export default function Toggle({ id, isActive, toggle }: ToggleProps) { 31 | return ( 32 | 33 | 34 | On 35 | 36 | 37 | Off 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import styled from 'styled-components' 3 | import Popover, { PopoverProps } from '../Popover' 4 | 5 | const TooltipContainer = styled.div` 6 | width: 228px; 7 | padding: 0.6rem 1rem; 8 | line-height: 150%; 9 | font-weight: 400; 10 | ` 11 | 12 | interface TooltipProps extends Omit { 13 | text: string | React.ReactNode 14 | } 15 | 16 | export default function Tooltip({ text, ...rest }: TooltipProps) { 17 | return {text}} {...rest} /> 18 | } 19 | 20 | export function MouseoverTooltip({ children, ...rest }: Omit) { 21 | const [show, setShow] = useState(false) 22 | const open = useCallback(() => setShow(true), [setShow]) 23 | const close = useCallback(() => setShow(false), [setShow]) 24 | return ( 25 | 26 |
27 | {children} 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/WalletModal/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MetamaskIcon from '../../assets/images/metamask.png' 3 | import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' 4 | import FortmaticIcon from '../../assets/images/fortmaticIcon.png' 5 | import PortisIcon from '../../assets/images/portisIcon.png' 6 | import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' 7 | import ArrowRight from '../../assets/images/walletConnectIcon.svg' 8 | 9 | const Icons: FC = ({ name }: { name: string }) => { 10 | switch (name) { 11 | case 'coinbase': 12 | return 13 | case 'metamask': 14 | return 15 | case 'portis': 16 | return 17 | case 'fortmatic': 18 | return 19 | case 'walletConnect': 20 | return 21 | case 'injected': 22 | return 23 | default: 24 | null 25 | } 26 | } 27 | 28 | export default Icons 29 | -------------------------------------------------------------------------------- /src/components/YieldCroInputPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import styled, { ThemeContext } from 'styled-components' 3 | import { RowBetween } from '../Row' 4 | import { darken } from 'polished' 5 | import { TYPE } from '../../theme' 6 | import { Text } from 'rebass' 7 | import { Input as NumericalInput } from '../NumericalInput' 8 | 9 | const InputPanel = styled.div<{ hideInput?: boolean }>` 10 | ${({ theme }) => theme.flexColumnNoWrap} 11 | position: relative; 12 | border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')}; 13 | background-color: ${({ theme }) => theme.bg2}; 14 | z-index: 1; 15 | ` 16 | 17 | const Container = styled.div<{ hideInput: boolean }>` 18 | border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')}; 19 | border: 1px solid ${({ theme }) => theme.bg2}; 20 | background-color: ${({ theme }) => theme.bg1}; 21 | ` 22 | 23 | const LabelRow = styled.div` 24 | ${({ theme }) => theme.flexRowNoWrap} 25 | align-items: center; 26 | font-size: 0.75rem; 27 | line-height: 1rem; 28 | padding: 0.75rem 1rem 0 1rem; 29 | span:hover { 30 | cursor: pointer; 31 | color: ${({ theme }) => darken(0.2, theme.text2)}; 32 | } 33 | ` 34 | 35 | const InputRow = styled.div` 36 | ${({ theme }) => theme.flexRowNoWrap} 37 | align-items: center; 38 | padding: 0.75rem 0.5rem 0.75rem 1rem; 39 | ` 40 | 41 | const Aligner = styled.span` 42 | display: flex; 43 | align-items: center; 44 | justify-content: space-between; 45 | ` 46 | 47 | interface InputPanelProps { 48 | value: string 49 | onUserInput: (value: string) => void 50 | id: string 51 | label: string 52 | } 53 | 54 | export default function YieldCroInputPanel({ id, label, value, onUserInput }: InputPanelProps) { 55 | const theme = useContext(ThemeContext) 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | {label} 63 | 64 | 65 | 66 | 67 | { 71 | onUserInput(val) 72 | }} 73 | /> 74 | 75 | 76 | CRO 77 | 78 | 79 | 80 | 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/components/YieldIFiatInputPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import styled, { ThemeContext } from 'styled-components' 3 | import { RowBetween } from '../Row' 4 | import { darken } from 'polished' 5 | import { TYPE } from '../../theme' 6 | import { Text } from 'rebass' 7 | import { Input as NumericalInput } from '../NumericalInput' 8 | 9 | const InputPanel = styled.div<{ hideInput?: boolean }>` 10 | ${({ theme }) => theme.flexColumnNoWrap} 11 | position: relative; 12 | border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')}; 13 | background-color: ${({ theme }) => theme.bg2}; 14 | z-index: 1; 15 | ` 16 | 17 | const Container = styled.div<{ hideInput: boolean }>` 18 | border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')}; 19 | border: 1px solid ${({ theme }) => theme.bg2}; 20 | background-color: ${({ theme }) => theme.bg1}; 21 | ` 22 | 23 | const LabelRow = styled.div` 24 | ${({ theme }) => theme.flexRowNoWrap} 25 | align-items: center; 26 | font-size: 0.75rem; 27 | line-height: 1rem; 28 | padding: 0.75rem 1rem 0 1rem; 29 | ` 30 | 31 | const InputRow = styled.div` 32 | ${({ theme }) => theme.flexRowNoWrap} 33 | align-items: center; 34 | padding: 0.75rem 0.5rem 0.75rem 1rem; 35 | ` 36 | 37 | const Aligner = styled.span` 38 | display: flex; 39 | align-items: center; 40 | justify-content: space-between; 41 | ` 42 | 43 | interface InputPanelProps { 44 | value: string 45 | onUserInput: (value: string) => void 46 | id: string 47 | label: string 48 | currency: string 49 | } 50 | 51 | export default function YieldFiatInputPanel({ id, label, value, onUserInput, currency }: InputPanelProps) { 52 | const theme = useContext(ThemeContext) 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | {label} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {currency} 68 | 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/components/analytics/GoogleAnalyticsReporter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import ReactGA from 'react-ga' 3 | import { RouteComponentProps } from 'react-router-dom' 4 | 5 | // fires a GA pageview every time the route changes 6 | export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null { 7 | useEffect(() => { 8 | ReactGA.pageview(`${pathname}${search}`) 9 | }, [pathname, search]) 10 | return null 11 | } 12 | -------------------------------------------------------------------------------- /src/components/swap/AdvancedSwapDetailsDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useLastTruthy } from '../../hooks/useLast' 4 | import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails' 5 | 6 | const AdvancedDetailsFooter = styled.div<{ show: boolean }>` 7 | padding-top: calc(16px + 2rem); 8 | padding-bottom: 20px; 9 | margin-top: -2rem; 10 | width: 100%; 11 | max-width: 400px; 12 | border-bottom-left-radius: 20px; 13 | border-bottom-right-radius: 20px; 14 | color: ${({ theme }) => theme.text2}; 15 | background-color: ${({ theme }) => theme.advancedBG}; 16 | z-index: -1; 17 | 18 | transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')}; 19 | transition: transform 300ms ease-in-out; 20 | ` 21 | 22 | export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) { 23 | const lastTrade = useLastTruthy(trade) 24 | 25 | return ( 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/swap/FormattedPriceImpact.tsx: -------------------------------------------------------------------------------- 1 | import { Percent } from 'swap-sdk' 2 | import React from 'react' 3 | import { ONE_BIPS } from '../../constants' 4 | import { warningSeverity } from '../../utils/prices' 5 | import { ErrorText } from './styleds' 6 | 7 | /** 8 | * Formatted version of price impact text with warning colors 9 | */ 10 | export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { 11 | return ( 12 | 13 | {priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/swap/SwapRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Trade } from 'swap-sdk' 2 | import React, { Fragment, memo, useContext } from 'react' 3 | import { ChevronRight } from 'react-feather' 4 | import { Flex } from 'rebass' 5 | import { ThemeContext } from 'styled-components' 6 | import { TYPE } from '../../theme' 7 | import CurrencyLogo from '../CurrencyLogo' 8 | 9 | export default memo(function SwapRoute({ trade }: { trade: Trade }) { 10 | const theme = useContext(ThemeContext) 11 | return ( 12 | 22 | {trade.route.path.map((token, i, path) => { 23 | const isLastItem: boolean = i === path.length - 1 24 | return ( 25 | 26 | 27 | 28 | 29 | {token.symbol} 30 | 31 | 32 | {isLastItem ? null : } 33 | 34 | ) 35 | })} 36 | 37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/swap/TradePrice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Currency, Price } from 'swap-sdk' 3 | import { useContext } from 'react' 4 | import { Repeat } from 'react-feather' 5 | import { Text } from 'rebass' 6 | import { ThemeContext } from 'styled-components' 7 | import { StyledBalanceMaxMini } from './styleds' 8 | 9 | interface TradePriceProps { 10 | price?: Price 11 | inputCurrency?: Currency 12 | outputCurrency?: Currency 13 | showInverted: boolean 14 | setShowInverted: (showInverted: boolean) => void 15 | } 16 | 17 | export default function TradePrice({ 18 | price, 19 | inputCurrency, 20 | outputCurrency, 21 | showInverted, 22 | setShowInverted 23 | }: TradePriceProps) { 24 | const theme = useContext(ThemeContext) 25 | 26 | const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6) 27 | 28 | const show = Boolean(inputCurrency && outputCurrency) 29 | const label = showInverted 30 | ? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}` 31 | : `${inputCurrency?.symbol} per ${outputCurrency?.symbol}` 32 | 33 | return ( 34 | 40 | {show ? ( 41 | <> 42 | {formattedPrice ?? '-'} {label} 43 | setShowInverted(!showInverted)}> 44 | 45 | 46 | 47 | ) : ( 48 | '-' 49 | )} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/swap/confirmPriceImpactWithoutFee.ts: -------------------------------------------------------------------------------- 1 | import { Percent } from 'swap-sdk' 2 | import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants' 3 | 4 | /** 5 | * Given the price impact, get user confirmation. 6 | * 7 | * @param priceImpactWithoutFee price impact of the trade without the fee. 8 | */ 9 | export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean { 10 | if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) { 11 | return ( 12 | window.prompt( 13 | `This swap has a price impact of at least ${PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN.toFixed( 14 | 0 15 | )}%. Please type the word "confirm" to continue with this swap.` 16 | ) === 'confirm' 17 | ) 18 | } else if (!priceImpactWithoutFee.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) { 19 | return window.confirm( 20 | `This swap has a price impact of at least ${ALLOWED_PRICE_IMPACT_HIGH.toFixed( 21 | 0 22 | )}%. Please confirm that you would like to continue with this swap.` 23 | ) 24 | } 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /src/connectors/Fortmatic.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from 'swap-sdk' 2 | import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector' 3 | 4 | export const OVERLAY_READY = 'OVERLAY_READY' 5 | 6 | type FormaticSupportedChains = Extract 7 | 8 | const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = { 9 | [ChainId.MAINNET]: undefined, 10 | [ChainId.ROPSTEN]: 'ropsten', 11 | [ChainId.RINKEBY]: 'rinkeby', 12 | [ChainId.KOVAN]: 'kovan' 13 | } 14 | 15 | export class FortmaticConnector extends FortmaticConnectorCore { 16 | async activate() { 17 | if (!this.fortmatic) { 18 | const { default: Fortmatic } = await import('fortmatic') 19 | 20 | const { apiKey, chainId } = this as any 21 | if (chainId in CHAIN_ID_NETWORK_ARGUMENT) { 22 | this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains]) 23 | } else { 24 | throw new Error(`Unsupported network ID: ${chainId}`) 25 | } 26 | } 27 | 28 | const provider = this.fortmatic.getProvider() 29 | 30 | const pollForOverlayReady = new Promise(resolve => { 31 | const interval = setInterval(() => { 32 | if (provider.overlayReady) { 33 | clearInterval(interval) 34 | this.emit(OVERLAY_READY) 35 | resolve() 36 | } 37 | }, 200) 38 | }) 39 | 40 | const [account] = await Promise.all([ 41 | provider.enable().then((accounts: string[]) => accounts[0]), 42 | pollForOverlayReady 43 | ]) 44 | 45 | return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/connectors/fortmatic.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'formatic' 2 | -------------------------------------------------------------------------------- /src/connectors/index.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from '@ethersproject/providers' 2 | import { InjectedConnector } from '@web3-react/injected-connector' 3 | import { WalletConnectConnector } from '@web3-react/walletconnect-connector' 4 | import { WalletLinkConnector } from '@web3-react/walletlink-connector' 5 | import { PortisConnector } from '@web3-react/portis-connector' 6 | 7 | import { FortmaticConnector } from './Fortmatic' 8 | import { NetworkConnector } from './NetworkConnector' 9 | 10 | const NETWORK_URL = process.env.REACT_APP_NETWORK_URL 11 | const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY 12 | const PORTIS_ID = process.env.REACT_APP_PORTIS_ID 13 | 14 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1') 15 | 16 | if (typeof NETWORK_URL === 'undefined') { 17 | throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`) 18 | } 19 | 20 | export const network = new NetworkConnector({ 21 | urls: { [NETWORK_CHAIN_ID]: NETWORK_URL } 22 | }) 23 | 24 | let networkLibrary: Web3Provider | undefined 25 | export function getNetworkLibrary(): Web3Provider { 26 | return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any)) 27 | } 28 | 29 | export const injected = new InjectedConnector({ 30 | supportedChainIds: [NETWORK_CHAIN_ID] 31 | }) 32 | 33 | export const walletconnect = new WalletConnectConnector({ 34 | rpc: { [NETWORK_CHAIN_ID]: NETWORK_URL }, 35 | bridge: 'https://bridge.walletconnect.org', 36 | qrcode: true, 37 | pollingInterval: 15000 38 | }) 39 | 40 | // mainnet only 41 | export const fortmatic = new FortmaticConnector({ 42 | apiKey: FORMATIC_KEY ?? '', 43 | chainId: 1 44 | }) 45 | 46 | // mainnet only 47 | export const portis = new PortisConnector({ 48 | dAppId: PORTIS_ID ?? '', 49 | networks: [1] 50 | }) 51 | 52 | // mainnet only 53 | export const walletlink = new WalletLinkConnector({ 54 | url: NETWORK_URL, 55 | appName: 'Crypto.com | DeFi Swap', 56 | appLogoUrl: 'https://dnkplacumu1sr.cloudfront.net/crypto_com_logo.svg' 57 | }) 58 | -------------------------------------------------------------------------------- /src/constants/abis/cryptoAbi.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import PausableTokenABI from './PausableTokenMock.json' 3 | import BasicStakeABI from './BasicStakeContract.json' 4 | import CryptoRouter from './crypto_router_abi.json' 5 | import MainnetBasicStakeABI from './mainnet/BasicStakeContract.json' 6 | import MainnetCryptoRouter from './mainnet/crypto_router_abi.json' 7 | 8 | const PAUSEABLE_STAKE_ABI = PausableTokenABI.abi 9 | const BASIC_STAKE_ABI = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetBasicStakeABI.abi : BasicStakeABI.abi 10 | const CRYPTO_ROUTER_ABI = process.env.REACT_APP_CHAIN_ID === '1' ? MainnetCryptoRouter : CryptoRouter 11 | const BASIC_STAKE_INTERFACE = new Interface(BASIC_STAKE_ABI) 12 | const PAUSEABLE_STAKE_INTERFACE = new Interface(PAUSEABLE_STAKE_ABI) 13 | 14 | export { BASIC_STAKE_INTERFACE, PAUSEABLE_STAKE_INTERFACE, PAUSEABLE_STAKE_ABI, BASIC_STAKE_ABI, CRYPTO_ROUTER_ABI } 15 | -------------------------------------------------------------------------------- /src/constants/abis/erc20.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import ERC20_ABI from './erc20.json' 3 | import ERC20_BYTES32_ABI from './erc20_bytes32.json' 4 | 5 | const ERC20_INTERFACE = new Interface(ERC20_ABI) 6 | 7 | const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI) 8 | 9 | export default ERC20_INTERFACE 10 | export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI } 11 | -------------------------------------------------------------------------------- /src/constants/abis/erc20_bytes32.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "bytes32" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": true, 18 | "inputs": [], 19 | "name": "symbol", 20 | "outputs": [ 21 | { 22 | "name": "", 23 | "type": "bytes32" 24 | } 25 | ], 26 | "payable": false, 27 | "stateMutability": "view", 28 | "type": "function" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /src/constants/abis/migrator.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_factoryV1", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_router", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "inputs": [ 20 | { 21 | "internalType": "address", 22 | "name": "token", 23 | "type": "address" 24 | }, 25 | { 26 | "internalType": "uint256", 27 | "name": "amountTokenMin", 28 | "type": "uint256" 29 | }, 30 | { 31 | "internalType": "uint256", 32 | "name": "amountETHMin", 33 | "type": "uint256" 34 | }, 35 | { 36 | "internalType": "address", 37 | "name": "to", 38 | "type": "address" 39 | }, 40 | { 41 | "internalType": "uint256", 42 | "name": "deadline", 43 | "type": "uint256" 44 | } 45 | ], 46 | "name": "migrate", 47 | "outputs": [], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "stateMutability": "payable", 53 | "type": "receive" 54 | } 55 | ] -------------------------------------------------------------------------------- /src/constants/abis/migrator.ts: -------------------------------------------------------------------------------- 1 | import MIGRATOR_ABI from './migrator.json' 2 | 3 | const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b' 4 | 5 | export { MIGRATOR_ADDRESS, MIGRATOR_ABI } 6 | -------------------------------------------------------------------------------- /src/constants/croAddress.ts: -------------------------------------------------------------------------------- 1 | const ROUTER_ADDRESS = process.env.REACT_APP_ROUTER_ADDRESS 2 | 3 | if (typeof ROUTER_ADDRESS === 'undefined') throw new Error('ROUTER_ADDRESS is undefined') 4 | 5 | export { ROUTER_ADDRESS } 6 | -------------------------------------------------------------------------------- /src/constants/croToken.ts: -------------------------------------------------------------------------------- 1 | import { Token, ChainId } from 'swap-sdk' 2 | 3 | export const CRO_ADDRESS = process.env.REACT_APP_CRO_TOKEN_ADDRESS 4 | if (typeof CRO_ADDRESS === 'undefined') throw new Error('CRO address is not configured') 5 | 6 | const CRO_TOKEN = 7 | process.env.REACT_APP_CHAIN_ID === String(ChainId.MAINNET) 8 | ? new Token(ChainId.MAINNET, CRO_ADDRESS, 8, 'CRO', 'Crypto.com Coin') 9 | : new Token(ChainId.ROPSTEN, CRO_ADDRESS, 8, 'CRO', 'Crypto.com Coin') 10 | 11 | export default CRO_TOKEN 12 | -------------------------------------------------------------------------------- /src/constants/lists.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TOKEN_LIST_URL = String(process.env.REACT_APP_CRYPTO_TOKEN_LIST_URL) 2 | 3 | if (typeof DEFAULT_TOKEN_LIST_URL === 'undefined') { 4 | throw new Error(`REACT_APP_CRYPTO_TOKEN_LIST_URL is missing`) 5 | } 6 | 7 | export const DEFAULT_LIST_OF_LISTS: string[] = [ 8 | DEFAULT_TOKEN_LIST_URL 9 | /* 10 | 't2crtokens.eth', // kleros 11 | 'tokens.1inch.eth', // 1inch 12 | 'synths.snx.eth', 13 | 'tokenlist.dharma.eth', 14 | 'defi.cmc.eth', 15 | 'erc20.cmc.eth', 16 | 'stablecoin.cmc.eth', 17 | 'tokenlist.zerion.eth', 18 | 'tokenlist.aave.eth', 19 | 'https://app.tryroll.com/tokens.json', 20 | 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json', 21 | 'https://defiprime.com/defiprime.tokenlist.json', 22 | 'https://umaproject.org/uma.tokenlist.json' 23 | */ 24 | ] 25 | -------------------------------------------------------------------------------- /src/constants/multicall/index.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from 'swap-sdk' 2 | import MULTICALL_ABI from './abi.json' 3 | 4 | const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = { 5 | [ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441', 6 | [ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed', 7 | [ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A', 8 | [ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821', 9 | [ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e' 10 | } 11 | 12 | export { MULTICALL_ABI, MULTICALL_NETWORKS } 13 | -------------------------------------------------------------------------------- /src/constants/stakeContractAddress.ts: -------------------------------------------------------------------------------- 1 | const ONE_YEAR = process.env.REACT_APP_ONE_YEAR_STAKE 2 | const TWO_YEAR = process.env.REACT_APP_TWO_YEAR_STAKE 3 | const THREE_YEAR = process.env.REACT_APP_THREE_YEAR_STAKE 4 | const FOUR_YEAR = process.env.REACT_APP_FOUR_YEAR_STAKE 5 | 6 | if ( 7 | typeof ONE_YEAR === 'undefined' || 8 | typeof TWO_YEAR === 'undefined' || 9 | typeof THREE_YEAR === 'undefined' || 10 | typeof FOUR_YEAR === 'undefined' 11 | ) 12 | throw new Error('Stake addresses are not well configured') 13 | 14 | export enum Field { 15 | ONE_YEAR = 'ONE_YEAR', 16 | TWO_YEAR = 'TWO_YEAR', 17 | THREE_YEAR = 'THREE_YEAR', 18 | FOUR_YEAR = 'FOUR_YEAR' 19 | } 20 | 21 | export const StakeContractAddress = Object.freeze({ 22 | [Field.ONE_YEAR]: ONE_YEAR, 23 | [Field.TWO_YEAR]: TWO_YEAR, 24 | [Field.THREE_YEAR]: THREE_YEAR, 25 | [Field.FOUR_YEAR]: FOUR_YEAR 26 | }) 27 | -------------------------------------------------------------------------------- /src/constants/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import { ChainId } from 'swap-sdk' 3 | import V1_EXCHANGE_ABI from './v1_exchange.json' 4 | import V1_FACTORY_ABI from './v1_factory.json' 5 | 6 | const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = { 7 | [ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95', 8 | [ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351', 9 | [ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36', 10 | [ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA', 11 | [ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30' 12 | } 13 | 14 | const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI) 15 | const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI) 16 | 17 | export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI } 18 | -------------------------------------------------------------------------------- /src/constants/v1/v1_factory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "NewExchange", 4 | "inputs": [ 5 | { "type": "address", "name": "token", "indexed": true }, 6 | { "type": "address", "name": "exchange", "indexed": true } 7 | ], 8 | "anonymous": false, 9 | "type": "event" 10 | }, 11 | { 12 | "name": "initializeFactory", 13 | "outputs": [], 14 | "inputs": [{ "type": "address", "name": "template" }], 15 | "constant": false, 16 | "payable": false, 17 | "type": "function" 18 | }, 19 | { 20 | "name": "createExchange", 21 | "outputs": [{ "type": "address", "name": "out" }], 22 | "inputs": [{ "type": "address", "name": "token" }], 23 | "constant": false, 24 | "payable": false, 25 | "type": "function" 26 | }, 27 | { 28 | "name": "getExchange", 29 | "outputs": [{ "type": "address", "name": "out" }], 30 | "inputs": [{ "type": "address", "name": "token" }], 31 | "constant": true, 32 | "payable": false, 33 | "type": "function" 34 | }, 35 | { 36 | "name": "getToken", 37 | "outputs": [{ "type": "address", "name": "out" }], 38 | "inputs": [{ "type": "address", "name": "exchange" }], 39 | "constant": true, 40 | "payable": false, 41 | "type": "function" 42 | }, 43 | { 44 | "name": "getTokenWithId", 45 | "outputs": [{ "type": "address", "name": "out" }], 46 | "inputs": [{ "type": "uint256", "name": "token_id" }], 47 | "constant": true, 48 | "payable": false, 49 | "type": "function" 50 | }, 51 | { 52 | "name": "exchangeTemplate", 53 | "outputs": [{ "type": "address", "name": "out" }], 54 | "inputs": [], 55 | "constant": true, 56 | "payable": false, 57 | "type": "function" 58 | }, 59 | { 60 | "name": "tokenCount", 61 | "outputs": [{ "type": "uint256", "name": "out" }], 62 | "inputs": [], 63 | "constant": true, 64 | "payable": false, 65 | "type": "function" 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /src/data/Allowances.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenAmount } from 'swap-sdk' 2 | import { useMemo } from 'react' 3 | 4 | import { useTokenContract } from '../hooks/useContract' 5 | import { useSingleCallResult } from '../state/multicall/hooks' 6 | 7 | export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount | undefined { 8 | const contract = useTokenContract(token?.address, false) 9 | 10 | const inputs = useMemo(() => [owner, spender], [owner, spender]) 11 | const allowance = useSingleCallResult(contract, 'allowance', inputs).result 12 | 13 | return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [ 14 | token, 15 | allowance 16 | ]) 17 | } 18 | -------------------------------------------------------------------------------- /src/data/Reserves.ts: -------------------------------------------------------------------------------- 1 | import { TokenAmount, Pair, Currency, ICroDefiSwapPairInterface } from 'swap-sdk' 2 | import { useMemo } from 'react' 3 | import { Interface } from '@ethersproject/abi' 4 | import { useActiveWeb3React } from '../hooks' 5 | 6 | import { useMultipleContractSingleData } from '../state/multicall/hooks' 7 | import { wrappedCurrency } from '../utils/wrappedCurrency' 8 | 9 | const PAIR_INTERFACE = new Interface(ICroDefiSwapPairInterface.abi) 10 | 11 | export enum PairState { 12 | LOADING, 13 | NOT_EXISTS, 14 | EXISTS, 15 | INVALID 16 | } 17 | 18 | export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] { 19 | const { chainId } = useActiveWeb3React() 20 | 21 | const tokens = useMemo( 22 | () => 23 | currencies.map(([currencyA, currencyB]) => [ 24 | wrappedCurrency(currencyA, chainId), 25 | wrappedCurrency(currencyB, chainId) 26 | ]), 27 | [chainId, currencies] 28 | ) 29 | 30 | const pairAddresses = useMemo( 31 | () => 32 | tokens.map(([tokenA, tokenB]) => { 33 | return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined 34 | }), 35 | [tokens] 36 | ) 37 | 38 | const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves') 39 | 40 | return useMemo(() => { 41 | return results.map((result, i) => { 42 | const { result: reserves, loading } = result 43 | const tokenA = tokens[i][0] 44 | const tokenB = tokens[i][1] 45 | 46 | if (loading) return [PairState.LOADING, null] 47 | if (tokenA && tokenB && tokenA.equals(tokenB)) return [PairState.INVALID, null] 48 | if (!reserves) return [PairState.NOT_EXISTS, null] 49 | const { reserve0, reserve1 } = reserves 50 | const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] 51 | return [ 52 | PairState.EXISTS, 53 | new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString())) 54 | ] 55 | }) 56 | }, [results, tokens]) 57 | } 58 | 59 | export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] { 60 | return usePairs([[tokenA, tokenB]])[0] 61 | } 62 | -------------------------------------------------------------------------------- /src/data/TotalSupply.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import { Token, TokenAmount } from 'swap-sdk' 3 | import { useTokenContract } from '../hooks/useContract' 4 | import { useSingleCallResult } from '../state/multicall/hooks' 5 | 6 | // returns undefined if input token is undefined, or fails to get token contract, 7 | // or contract total supply cannot be fetched 8 | export function useTotalSupply(token?: Token): TokenAmount | undefined { 9 | const contract = useTokenContract(token?.address, false) 10 | 11 | const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0] 12 | 13 | return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined 14 | } 15 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/favicon.png -------------------------------------------------------------------------------- /src/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/swap-interface/af9bf07fb73a5273ad14ed11e20951e349d1cc8c/src/favicon.webp -------------------------------------------------------------------------------- /src/hooks/useCopyClipboard.ts: -------------------------------------------------------------------------------- 1 | import copy from 'copy-to-clipboard' 2 | import { useCallback, useEffect, useState } from 'react' 3 | 4 | export default function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] { 5 | const [isCopied, setIsCopied] = useState(false) 6 | 7 | const staticCopy = useCallback(text => { 8 | const didCopy = copy(text) 9 | setIsCopied(didCopy) 10 | }, []) 11 | 12 | useEffect(() => { 13 | if (isCopied) { 14 | const hide = setTimeout(() => { 15 | setIsCopied(false) 16 | }, timeout) 17 | 18 | return () => { 19 | clearTimeout(hide) 20 | } 21 | } 22 | return undefined 23 | }, [isCopied, setIsCopied, timeout]) 24 | 25 | return [isCopied, staticCopy] 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | // modified from https://usehooks.com/useDebounce/ 4 | export default function useDebounce(value: T, delay: number): T { 5 | const [debouncedValue, setDebouncedValue] = useState(value) 6 | 7 | useEffect(() => { 8 | // Update debounced value after delay 9 | const handler = setTimeout(() => { 10 | setDebouncedValue(value) 11 | }, delay) 12 | 13 | // Cancel the timeout if value changes (also on delay change or unmount) 14 | // This is how we prevent debounced value from updating if value is changed ... 15 | // .. within the delay period. Timeout gets cleared and restarted. 16 | return () => { 17 | clearTimeout(handler) 18 | } 19 | }, [value, delay]) 20 | 21 | return debouncedValue 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useENS.ts: -------------------------------------------------------------------------------- 1 | import { isAddress } from '../utils' 2 | import useENSAddress from './useENSAddress' 3 | import useENSName from './useENSName' 4 | 5 | /** 6 | * Given a name or address, does a lookup to resolve to an address and name 7 | * @param nameOrAddress ENS name or address 8 | */ 9 | export default function useENS( 10 | nameOrAddress?: string | null 11 | ): { loading: boolean; address: string | null; name: string | null } { 12 | const validated = isAddress(nameOrAddress) 13 | const reverseLookup = useENSName(validated ? validated : undefined) 14 | const lookup = useENSAddress(nameOrAddress) 15 | 16 | return { 17 | loading: reverseLookup.loading || lookup.loading, 18 | address: validated ? validated : lookup.address, 19 | name: reverseLookup.ENSName ? reverseLookup.ENSName : !validated && lookup.address ? nameOrAddress || null : null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useENSAddress.ts: -------------------------------------------------------------------------------- 1 | import { namehash } from 'ethers/lib/utils' 2 | import { useMemo } from 'react' 3 | import { useSingleCallResult } from '../state/multicall/hooks' 4 | import isZero from '../utils/isZero' 5 | import { useENSRegistrarContract, useENSResolverContract } from './useContract' 6 | import useDebounce from './useDebounce' 7 | 8 | /** 9 | * Does a lookup for an ENS name to find its address. 10 | */ 11 | export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } { 12 | const debouncedName = useDebounce(ensName, 200) 13 | const ensNodeArgument = useMemo(() => { 14 | if (!debouncedName) return [undefined] 15 | try { 16 | return debouncedName ? [namehash(debouncedName)] : [undefined] 17 | } catch (error) { 18 | return [undefined] 19 | } 20 | }, [debouncedName]) 21 | const registrarContract = useENSRegistrarContract(false) 22 | const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument) 23 | const resolverAddressResult = resolverAddress.result?.[0] 24 | const resolverContract = useENSResolverContract( 25 | resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined, 26 | false 27 | ) 28 | const addr = useSingleCallResult(resolverContract, 'addr', ensNodeArgument) 29 | 30 | const changed = debouncedName !== ensName 31 | return { 32 | address: changed ? null : addr.result?.[0] ?? null, 33 | loading: changed || resolverAddress.loading || addr.loading 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/hooks/useENSContentHash.ts: -------------------------------------------------------------------------------- 1 | import { namehash } from 'ethers/lib/utils' 2 | import { useMemo } from 'react' 3 | import { useSingleCallResult } from '../state/multicall/hooks' 4 | import isZero from '../utils/isZero' 5 | import { useENSRegistrarContract, useENSResolverContract } from './useContract' 6 | 7 | /** 8 | * Does a lookup for an ENS name to find its contenthash. 9 | */ 10 | export default function useENSContentHash(ensName?: string | null): { loading: boolean; contenthash: string | null } { 11 | const ensNodeArgument = useMemo(() => { 12 | if (!ensName) return [undefined] 13 | try { 14 | return ensName ? [namehash(ensName)] : [undefined] 15 | } catch (error) { 16 | return [undefined] 17 | } 18 | }, [ensName]) 19 | const registrarContract = useENSRegistrarContract(false) 20 | const resolverAddressResult = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument) 21 | const resolverAddress = resolverAddressResult.result?.[0] 22 | const resolverContract = useENSResolverContract( 23 | resolverAddress && isZero(resolverAddress) ? undefined : resolverAddress, 24 | false 25 | ) 26 | const contenthash = useSingleCallResult(resolverContract, 'contenthash', ensNodeArgument) 27 | 28 | return { 29 | contenthash: contenthash.result?.[0] ?? null, 30 | loading: resolverAddressResult.loading || contenthash.loading 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/useENSName.ts: -------------------------------------------------------------------------------- 1 | import { namehash } from 'ethers/lib/utils' 2 | import { useMemo } from 'react' 3 | import { useSingleCallResult } from '../state/multicall/hooks' 4 | import { isAddress } from '../utils' 5 | import isZero from '../utils/isZero' 6 | import { useENSRegistrarContract, useENSResolverContract } from './useContract' 7 | import useDebounce from './useDebounce' 8 | 9 | /** 10 | * Does a reverse lookup for an address to find its ENS name. 11 | * Note this is not the same as looking up an ENS name to find an address. 12 | */ 13 | export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } { 14 | const debouncedAddress = useDebounce(address, 200) 15 | const ensNodeArgument = useMemo(() => { 16 | if (!debouncedAddress || !isAddress(debouncedAddress)) return [undefined] 17 | try { 18 | return debouncedAddress ? [namehash(`${debouncedAddress.toLowerCase().substr(2)}.addr.reverse`)] : [undefined] 19 | } catch (error) { 20 | return [undefined] 21 | } 22 | }, [debouncedAddress]) 23 | const registrarContract = useENSRegistrarContract(false) 24 | const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument) 25 | const resolverAddressResult = resolverAddress.result?.[0] 26 | const resolverContract = useENSResolverContract( 27 | resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined, 28 | false 29 | ) 30 | const name = useSingleCallResult(resolverContract, 'name', ensNodeArgument) 31 | 32 | const changed = debouncedAddress !== address 33 | return { 34 | ENSName: changed ? null : name.result?.[0] ?? null, 35 | loading: changed || resolverAddress.loading || name.loading 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/useFetchCalculatorGraphDataCallback.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@reduxjs/toolkit' 2 | import { useCallback } from 'react' 3 | import { useDispatch } from 'react-redux' 4 | import { AppDispatch } from '../state' 5 | import { CalculatorGraphData } from '../state/calculator/reducer' 6 | import getCalculatorGraphData from '../utils/getCalculatorGraphData' 7 | import { fetchCalculatorGraphData } from '../state/calculator/actions' 8 | 9 | export function useFetchCalculatorGraphDataCallback(): (listUrl: string) => Promise { 10 | const dispatch = useDispatch() 11 | 12 | return useCallback( 13 | async (listUrl: string) => { 14 | const requestId = nanoid() 15 | dispatch(fetchCalculatorGraphData.pending({ requestId, url: listUrl })) 16 | return getCalculatorGraphData(listUrl) 17 | .then(graphData => { 18 | dispatch(fetchCalculatorGraphData.fulfilled({ url: listUrl, graphData, requestId })) 19 | return graphData 20 | }) 21 | .catch(error => { 22 | console.debug(`Failed to get list at url ${listUrl}`, error) 23 | dispatch(fetchCalculatorGraphData.rejected({ url: listUrl, requestId, errorMessage: error.message })) 24 | throw error 25 | }) 26 | }, 27 | [dispatch] 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/useFetchListCallback.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@reduxjs/toolkit' 2 | import { ChainId } from 'swap-sdk' 3 | import { TokenList } from '@uniswap/token-lists' 4 | import { useCallback } from 'react' 5 | import { useDispatch } from 'react-redux' 6 | import { getNetworkLibrary, NETWORK_CHAIN_ID } from '../connectors' 7 | import { AppDispatch } from '../state' 8 | import { fetchTokenList } from '../state/lists/actions' 9 | import getTokenList from '../utils/getTokenList' 10 | import resolveENSContentHash from '../utils/resolveENSContentHash' 11 | import { useActiveWeb3React } from './index' 12 | 13 | export function useFetchListCallback(): (listUrl: string) => Promise { 14 | const { chainId, library } = useActiveWeb3React() 15 | const dispatch = useDispatch() 16 | 17 | const ensResolver = useCallback( 18 | (ensName: string) => { 19 | if (!library || chainId !== ChainId.MAINNET) { 20 | if (NETWORK_CHAIN_ID === ChainId.MAINNET) { 21 | const networkLibrary = getNetworkLibrary() 22 | if (networkLibrary) { 23 | return resolveENSContentHash(ensName, networkLibrary) 24 | } 25 | } 26 | throw new Error('Could not construct mainnet ENS resolver') 27 | } 28 | return resolveENSContentHash(ensName, library) 29 | }, 30 | [chainId, library] 31 | ) 32 | 33 | return useCallback( 34 | async (listUrl: string) => { 35 | const requestId = nanoid() 36 | dispatch(fetchTokenList.pending({ requestId, url: listUrl })) 37 | return getTokenList(listUrl, ensResolver) 38 | .then(tokenList => { 39 | dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })) 40 | return tokenList 41 | }) 42 | .catch(error => { 43 | console.debug(`Failed to get list at url ${listUrl}`, error) 44 | dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message })) 45 | throw error 46 | }) 47 | }, 48 | [dispatch, ensResolver] 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/hooks/useHttpLocations.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import contenthashToUri from '../utils/contenthashToUri' 3 | import { parseENSAddress } from '../utils/parseENSAddress' 4 | import uriToHttp from '../utils/uriToHttp' 5 | import useENSContentHash from './useENSContentHash' 6 | 7 | export default function useHttpLocations(uri: string | undefined): string[] { 8 | const ens = useMemo(() => (uri ? parseENSAddress(uri) : undefined), [uri]) 9 | const resolvedContentHash = useENSContentHash(ens?.ensName) 10 | return useMemo(() => { 11 | if (ens) { 12 | return resolvedContentHash.contenthash ? uriToHttp(contenthashToUri(resolvedContentHash.contenthash)) : [] 13 | } else { 14 | return uri ? uriToHttp(uri) : [] 15 | } 16 | }, [ens, resolvedContentHash.contenthash, uri]) 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export default function useInterval(callback: () => void, delay: null | number, leading = true) { 4 | const savedCallback = useRef<() => void>() 5 | 6 | // Remember the latest callback. 7 | useEffect(() => { 8 | savedCallback.current = callback 9 | }, [callback]) 10 | 11 | // Set up the interval. 12 | useEffect(() => { 13 | function tick() { 14 | const current = savedCallback.current 15 | current && current() 16 | } 17 | 18 | if (delay !== null) { 19 | if (leading) tick() 20 | const id = setInterval(tick, delay) 21 | return () => clearInterval(id) 22 | } 23 | return undefined 24 | }, [delay, leading]) 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useIsWindowVisible.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document 4 | 5 | function isWindowVisible() { 6 | return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden' 7 | } 8 | 9 | /** 10 | * Returns whether the window is currently visible to the user. 11 | */ 12 | export default function useIsWindowVisible(): boolean { 13 | const [focused, setFocused] = useState(isWindowVisible()) 14 | const listener = useCallback(() => { 15 | setFocused(isWindowVisible()) 16 | }, [setFocused]) 17 | 18 | useEffect(() => { 19 | if (!VISIBILITY_STATE_SUPPORTED) return undefined 20 | 21 | document.addEventListener('visibilitychange', listener) 22 | return () => { 23 | document.removeEventListener('visibilitychange', listener) 24 | } 25 | }, [listener]) 26 | 27 | return focused 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useLast.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | /** 4 | * Returns the last value of type T that passes a filter function 5 | * @param value changing value 6 | * @param filterFn function that determines whether a given value should be considered for the last value 7 | */ 8 | export default function useLast( 9 | value: T | undefined | null, 10 | filterFn?: (value: T | null | undefined) => boolean 11 | ): T | null | undefined { 12 | const [last, setLast] = useState(filterFn && filterFn(value) ? value : undefined) 13 | useEffect(() => { 14 | setLast(last => { 15 | const shouldUse: boolean = filterFn ? filterFn(value) : true 16 | if (shouldUse) return value 17 | return last 18 | }) 19 | }, [filterFn, value]) 20 | return last 21 | } 22 | 23 | function isDefined(x: T | null | undefined): x is T { 24 | return x !== null && x !== undefined 25 | } 26 | 27 | /** 28 | * Returns the last truthy value of type T 29 | * @param value changing value 30 | */ 31 | export function useLastTruthy(value: T | undefined | null): T | null | undefined { 32 | return useLast(value, isDefined) 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/useOnClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react' 2 | 3 | export function useOnClickOutside( 4 | node: RefObject, 5 | handler: undefined | (() => void) 6 | ) { 7 | const handlerRef = useRef void)>(handler) 8 | useEffect(() => { 9 | handlerRef.current = handler 10 | }, [handler]) 11 | 12 | useEffect(() => { 13 | const handleClickOutside = (e: MouseEvent) => { 14 | if (node.current?.contains(e.target as Node) ?? false) { 15 | return 16 | } 17 | if (handlerRef.current) handlerRef.current() 18 | } 19 | 20 | document.addEventListener('mousedown', handleClickOutside) 21 | 22 | return () => { 23 | document.removeEventListener('mousedown', handleClickOutside) 24 | } 25 | }, [node]) 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useParsedQueryString.ts: -------------------------------------------------------------------------------- 1 | import { parse, ParsedQs } from 'qs' 2 | import { useMemo } from 'react' 3 | import { useLocation } from 'react-router-dom' 4 | 5 | export default function useParsedQueryString(): ParsedQs { 6 | const { search } = useLocation() 7 | return useMemo( 8 | () => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}), 9 | [search] 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | // modified from https://usehooks.com/usePrevious/ 4 | export default function usePrevious(value: T) { 5 | // The ref object is a generic container whose current property is mutable ... 6 | // ... and can hold any value, similar to an instance property on a class 7 | const ref = useRef() 8 | 9 | // Store current value in ref 10 | useEffect(() => { 11 | ref.current = value 12 | }, [value]) // Only re-run if value changes 13 | 14 | // Return previous value (happens before update in useEffect above) 15 | return ref.current 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useSocksBalance.ts: -------------------------------------------------------------------------------- 1 | import { JSBI } from 'swap-sdk' 2 | import { useMemo } from 'react' 3 | import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks' 4 | import { useActiveWeb3React } from './index' 5 | import { useSocksController } from './useContract' 6 | 7 | export default function useSocksBalance(): JSBI | undefined { 8 | const { account } = useActiveWeb3React() 9 | const socksContract = useSocksController() 10 | 11 | const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD) 12 | const data = result?.[0] 13 | return data ? JSBI.BigInt(data.toString()) : undefined 14 | } 15 | 16 | export function useHasSocks(): boolean | undefined { 17 | const balance = useSocksBalance() 18 | return useMemo(() => balance && JSBI.greaterThan(balance, JSBI.BigInt(0)), [balance]) 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export default function useToggle(initialState = false): [boolean, () => void] { 4 | const [state, setState] = useState(initialState) 5 | const toggle = useCallback(() => setState(state => !state), []) 6 | return [state, toggle] 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/useToggledVersion.ts: -------------------------------------------------------------------------------- 1 | export enum Version { 2 | v1 = 'v1', 3 | v2 = 'v2' 4 | } 5 | 6 | export const DEFAULT_VERSION: Version = Version.v2 7 | 8 | export default function useToggledVersion(): Version { 9 | return DEFAULT_VERSION 10 | } 11 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import XHR from 'i18next-xhr-backend' 4 | import LanguageDetector from 'i18next-browser-languagedetector' 5 | 6 | i18next 7 | .use(XHR) 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | backend: { 12 | loadPath: `${process.env.PUBLIC_URL || ''}/locales/{{lng}}.json` 13 | }, 14 | react: { 15 | useSuspense: true 16 | }, 17 | fallbackLng: 'en', 18 | preload: ['en'], 19 | keySeparator: false, 20 | interpolation: { escapeValue: false } 21 | }) 22 | 23 | export default i18next 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | DeFi Swap 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/locales_legacy/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "noWallet": "未发现以太钱包", 3 | "wrongNetwork": "网络错误", 4 | "switchNetwork": "请切换到 {{ correctNetwork }}", 5 | "installWeb3MobileBrowser": "请从支持web3的移动端浏览器,如 Trust Wallet 或 Coinbase Wallet 访问。", 6 | "installMetamask": "请从安装了 Metamask 插件的 Chrome 或 Brave 访问。", 7 | "disconnected": "未连接", 8 | "swap": "兑换", 9 | "send": "发送", 10 | "pool": "资金池", 11 | "betaWarning": "项目尚处于beta阶段。使用需自行承担风险。", 12 | "input": "输入", 13 | "output": "输出", 14 | "estimated": "估计", 15 | "balance": "余额: {{ balanceInput }}", 16 | "unlock": "解锁", 17 | "pending": "处理中", 18 | "selectToken": "选择通证", 19 | "searchOrPaste": "搜索通证或粘贴地址", 20 | "noExchange": "未找到交易所", 21 | "exchangeRate": "兑换率", 22 | "enterValueCont": "输入{{ missingCurrencyValue }}值并继续。", 23 | "selectTokenCont": "选取通证继续。", 24 | "noLiquidity": "没有流动金。", 25 | "unlockTokenCont": "请解锁通证并继续。", 26 | "transactionDetails": "交易明细", 27 | "hideDetails": "隐藏明细", 28 | "youAreSelling": "你正在出售", 29 | "orTransFail": "或交易失败。", 30 | "youWillReceive": "你将至少收到", 31 | "youAreBuying": "你正在购买", 32 | "itWillCost": "它将至少花费", 33 | "insufficientBalance": "余额不足", 34 | "inputNotValid": "无效的输入值", 35 | "differentToken": "必须是不同的通证。", 36 | "noRecipient": "输入接收钱包地址。", 37 | "invalidRecipient": "请输入有效的收钱地址。", 38 | "recipientAddress": "接收地址", 39 | "youAreSending": "你正在发送", 40 | "willReceive": "将至少收到", 41 | "to": "至", 42 | "addLiquidity": "添加流动金", 43 | "deposit": "存入", 44 | "currentPoolSize": "当前资金池大小", 45 | "yourPoolShare": "你的资金池份额", 46 | "noZero": "金额不能为零。", 47 | "mustBeETH": "输入中必须有一个是 ETH。", 48 | "enterCurrencyOrLabelCont": "输入 {{ inputCurrency }} 或 {{ label }} 值并继续。", 49 | "youAreAdding": "你将添加", 50 | "and": "和", 51 | "intoPool": "入流动资金池。", 52 | "outPool": "出流动资金池。", 53 | "youWillMint": "你将铸造", 54 | "liquidityTokens": "流动通证。", 55 | "totalSupplyIs": "当前流动通证的总量是", 56 | "youAreSettingExRate": "你将初始兑换率设置为", 57 | "totalSupplyIs0": "当前流动通证的总量是0。", 58 | "tokenWorth": "当前兑换率下,每个资金池通证价值", 59 | "firstLiquidity": "你是第一个添加流动金的人!", 60 | "initialExchangeRate": "初始兑换率将由你的存入情况决定。请确保你存入的 ETH 和 {{ label }} 具有相同的总市值。", 61 | "removeLiquidity": "删除流动金", 62 | "poolTokens": "资金池通证", 63 | "enterLabelCont": "输入 {{ label }} 值并继续。", 64 | "youAreRemoving": "你正在移除", 65 | "youWillRemove": "你将移除", 66 | "createExchange": "创建交易所", 67 | "invalidTokenAddress": "通证地址无效", 68 | "exchangeExists": "{{ label }} 交易所已存在!", 69 | "invalidSymbol": "通证符号无效", 70 | "invalidDecimals": "小数位数无效", 71 | "tokenAddress": "通证地址", 72 | "label": "通证符号", 73 | "decimals": "小数位数", 74 | "enterTokenCont": "输入通证地址并继续" 75 | } 76 | -------------------------------------------------------------------------------- /src/locales_legacy/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "noWallet": "未偵測到以太坊錢包", 3 | "wrongNetwork": "你位在錯誤的網路", 4 | "switchNetwork": "請切換到 {{ correctNetwork }}", 5 | "installWeb3MobileBrowser": "請安裝含有 web3 瀏覽器的手機錢包,如 Trust Wallet 或 Coinbase Wallet。", 6 | "installMetamask": "請使用 Chrome 或 Brave 瀏覽器安裝 Metamask。", 7 | "disconnected": "未連接", 8 | "swap": "兌換", 9 | "send": "發送", 10 | "pool": "資金池", 11 | "betaWarning": "本產品仍在測試階段。使用者需自負風險。", 12 | "input": "輸入", 13 | "output": "輸出", 14 | "estimated": "估計", 15 | "balance": "餘額: {{ balanceInput }}", 16 | "unlock": "解鎖", 17 | "pending": "處理中", 18 | "selectToken": "選擇代幣", 19 | "searchOrPaste": "選擇代幣或輸入地址", 20 | "noExchange": "找不到交易所", 21 | "exchangeRate": "匯率", 22 | "enterValueCont": "輸入 {{ missingCurrencyValue }} 以繼續。", 23 | "selectTokenCont": "選擇代幣以繼續。", 24 | "noLiquidity": "沒有流動性資金。", 25 | "unlockTokenCont": "解鎖代幣以繼續。", 26 | "transactionDetails": "交易明細", 27 | "hideDetails": "隱藏明細", 28 | "youAreSelling": "你正在出售", 29 | "orTransFail": "或交易失敗。", 30 | "youWillReceive": "你將至少收到", 31 | "youAreBuying": "你正在購買", 32 | "itWillCost": "這將花費至多", 33 | "insufficientBalance": "餘額不足", 34 | "inputNotValid": "無效的輸入值", 35 | "differentToken": "必須是不同的代幣。", 36 | "noRecipient": "請輸入收款人錢包地址。", 37 | "invalidRecipient": "請輸入有效的錢包地址。", 38 | "recipientAddress": "收款人錢包地址", 39 | "youAreSending": "你正在發送", 40 | "willReceive": "將至少收到", 41 | "to": "至", 42 | "addLiquidity": "增加流動性資金", 43 | "deposit": "存入", 44 | "currentPoolSize": "目前的資金池總量", 45 | "yourPoolShare": "你在資金池中的佔比", 46 | "noZero": "金額不能為零。", 47 | "mustBeETH": "輸入中必須包含 ETH。", 48 | "enterCurrencyOrLabelCont": "輸入 {{ inputCurrency }} 或 {{ label }} 以繼續。", 49 | "youAreAdding": "你將把", 50 | "and": "和", 51 | "intoPool": "加入資金池。", 52 | "outPool": "領出資金池。", 53 | "youWillMint": "你將產生", 54 | "liquidityTokens": "流動性代幣。", 55 | "totalSupplyIs": "目前流動性代幣供給總量為", 56 | "youAreSettingExRate": "初始的匯率將被設定為", 57 | "totalSupplyIs0": "目前流動性代幣供給為零。", 58 | "tokenWorth": "依據目前的匯率,每個流動性代幣價值", 59 | "firstLiquidity": "您是第一個提供流動性資金的人!", 60 | "initialExchangeRate": "初始的匯率將取決於你存入的資金。請確保存入的 ETH 和 {{ label }} 的價值相等。", 61 | "removeLiquidity": "領出流動性資金", 62 | "poolTokens": "資金池代幣", 63 | "enterLabelCont": "輸入 {{ label }} 以繼續。", 64 | "youAreRemoving": "您正在移除", 65 | "youWillRemove": "您即將移除", 66 | "createExchange": "創建交易所", 67 | "invalidTokenAddress": "無效的代幣地址", 68 | "exchangeExists": "{{ label }} 的交易所已經存在!", 69 | "invalidSymbol": "代幣符號錯誤", 70 | "invalidDecimals": "小數位數錯誤", 71 | "tokenAddress": "代幣地址", 72 | "label": "代幣符號", 73 | "decimals": "小數位數", 74 | "enterTokenCont": "輸入代幣地址" 75 | } 76 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Croswap", 3 | "name": "Croswap", 4 | "icons": [ 5 | { 6 | "src": "./assets/images/deFiSwap-avatar-192px.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "./assets/images/deFiSwap-avatar-512px.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | } 17 | ], 18 | "orientation": "portrait", 19 | "display": "standalone", 20 | "theme_color": "#ff007a", 21 | "background_color": "#fff" 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/AddLiquidity/PoolPriceBar.tsx: -------------------------------------------------------------------------------- 1 | import { Currency, Percent, Price } from 'swap-sdk' 2 | import React, { useContext } from 'react' 3 | import { Text } from 'rebass' 4 | import { ThemeContext } from 'styled-components' 5 | import { AutoColumn } from '../../components/Column' 6 | import { AutoRow } from '../../components/Row' 7 | import { ONE_BIPS } from '../../constants' 8 | import { Field } from '../../state/mint/actions' 9 | import { TYPE } from '../../theme' 10 | 11 | export function PoolPriceBar({ 12 | currencies, 13 | noLiquidity, 14 | poolTokenPercentage, 15 | price 16 | }: { 17 | currencies: { [field in Field]?: Currency } 18 | noLiquidity?: boolean 19 | poolTokenPercentage?: Percent 20 | price?: Price 21 | }) { 22 | const theme = useContext(ThemeContext) 23 | return ( 24 | 25 | 26 | 27 | {price?.toSignificant(6) ?? '-'} 28 | 29 | {currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol} 30 | 31 | 32 | 33 | {price?.invert()?.toSignificant(6) ?? '-'} 34 | 35 | {currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol} 36 | 37 | 38 | 39 | 40 | {noLiquidity && price 41 | ? '100' 42 | : (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'} 43 | % 44 | 45 | 46 | Share of Pool 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/AddLiquidity/redirects.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect, RouteComponentProps } from 'react-router-dom' 3 | import AddLiquidity from './index' 4 | 5 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/ 6 | export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) { 7 | const { 8 | match: { 9 | params: { currencyIdA } 10 | } 11 | } = props 12 | const match = currencyIdA.match(OLD_PATH_STRUCTURE) 13 | if (match?.length) { 14 | return 15 | } 16 | 17 | return 18 | } 19 | 20 | export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) { 21 | const { 22 | match: { 23 | params: { currencyIdA, currencyIdB } 24 | } 25 | } = props 26 | if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) { 27 | return 28 | } 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/AppBody.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | export const BodyWrapper = styled.div` 5 | position: relative; 6 | max-width: 420px; 7 | width: 100%; 8 | background: ${({ theme }) => theme.bg1}; 9 | box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 10 | 0px 24px 32px rgba(0, 0, 0, 0.01); 11 | border-radius: 30px; 12 | padding: 1rem; 13 | ` 14 | 15 | /** 16 | * The styled container element that wraps the content of most pages and the tabs. 17 | */ 18 | export default function AppBody({ children }: { children: React.ReactNode }) { 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/Hero.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Switch, Route } from 'react-router-dom' 3 | import CRODefiSwap from '../assets/images/cro-defi-swap.svg' 4 | import styled from 'styled-components' 5 | import { Text } from 'rebass' 6 | 7 | const HeroWrapper = styled.div` 8 | position: relative; 9 | max-width: 420px; 10 | width: 100%; 11 | height: auto; 12 | display: flex; 13 | justify-content: space-between; 14 | flex-direction: column; 15 | align-items: center; 16 | margin: 25px 0; 17 | gap: 9px; 18 | ` 19 | 20 | const HeroText = styled(Text)` 21 | color: ${({ theme }) => theme.text1}; 22 | ` 23 | 24 | const DefaultHero: FC = () => ( 25 | 26 | 27 | 28 | The Best Place to Swap & Farm DeFi Coins 29 | 30 | 31 | ) 32 | 33 | const Hero: FC = () => ( 34 | 35 | 36 | 37 | ) 38 | 39 | export default Hero 40 | -------------------------------------------------------------------------------- /src/pages/Pool/styleds.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'rebass' 2 | import styled from 'styled-components' 3 | 4 | export const Wrapper = styled.div` 5 | position: relative; 6 | ` 7 | 8 | export const ClickableText = styled(Text)` 9 | :hover { 10 | cursor: pointer; 11 | } 12 | color: ${({ theme }) => theme.primary1}; 13 | ` 14 | export const MaxButton = styled.button<{ width: string }>` 15 | padding: 0.5rem 1rem; 16 | background-color: ${({ theme }) => theme.primary2}; 17 | border: none; 18 | border-radius: 8px; 19 | font-size: 0.875rem; 20 | ${({ theme }) => theme.mediaWidth.upToSmall` 21 | padding: 0.25rem 0.5rem; 22 | `}; 23 | font-weight: 500; 24 | cursor: pointer; 25 | margin: 0.25rem; 26 | overflow: hidden; 27 | color: ${({ theme }) => theme.primary1}; 28 | :hover { 29 | border: 1px solid ${({ theme }) => theme.primary1}; 30 | } 31 | :focus { 32 | border: 1px solid ${({ theme }) => theme.primary1}; 33 | outline: none; 34 | } 35 | ` 36 | 37 | export const Dots = styled.span` 38 | &::after { 39 | display: inline-block; 40 | animation: ellipsis 1.25s infinite; 41 | content: '.'; 42 | width: 1em; 43 | text-align: left; 44 | } 45 | @keyframes ellipsis { 46 | 0% { 47 | content: '.'; 48 | } 49 | 33% { 50 | content: '..'; 51 | } 52 | 66% { 53 | content: '...'; 54 | } 55 | } 56 | ` 57 | -------------------------------------------------------------------------------- /src/pages/RemoveLiquidity/redirects.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RouteComponentProps, Redirect } from 'react-router-dom' 3 | 4 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/ 5 | 6 | export function RedirectOldRemoveLiquidityPathStructure({ 7 | match: { 8 | params: { tokens } 9 | } 10 | }: RouteComponentProps<{ tokens: string }>) { 11 | if (!OLD_PATH_STRUCTURE.test(tokens)) { 12 | return 13 | } 14 | const [currency0, currency1] = tokens.split('-') 15 | 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/Swap/redirects.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect, RouteComponentProps } from 'react-router-dom' 3 | 4 | // Redirects to swap but only replace the pathname 5 | export function RedirectPathToSwapOnly({ location }: RouteComponentProps) { 6 | return 7 | } 8 | 9 | // Redirects from the /swap/:outputCurrency path to the /swap?outputCurrency=:outputCurrency format 10 | export function RedirectToSwap(props: RouteComponentProps<{ outputCurrency: string }>) { 11 | const { 12 | location: { search }, 13 | match: { 14 | params: { outputCurrency } 15 | } 16 | } = props 17 | 18 | return ( 19 | 1 25 | ? `${search}&outputCurrency=${outputCurrency}` 26 | : `?outputCurrency=${outputCurrency}` 27 | }} 28 | /> 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'jazzicon' { 4 | export default function(diameter: number, seed: number): HTMLElement 5 | } 6 | 7 | declare module 'fortmatic' 8 | 9 | interface Window { 10 | ethereum?: { 11 | isMetaMask?: true 12 | on?: (...args: any[]) => void 13 | removeListener?: (...args: any[]) => void 14 | } 15 | web3?: {} 16 | } 17 | 18 | declare module 'content-hash' { 19 | declare function decode(x: string): string 20 | declare function getCodec(x: string): string 21 | } 22 | 23 | declare module 'multihashes' { 24 | declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array } 25 | declare function toB58String(hash: Uint8Array): string 26 | } 27 | -------------------------------------------------------------------------------- /src/state/application/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | import { TokenList } from '@uniswap/token-lists' 3 | 4 | export type PopupContent = 5 | | { 6 | txn: { 7 | hash: string 8 | success: boolean 9 | summary?: string 10 | } 11 | } 12 | | { 13 | listUpdate: { 14 | listUrl: string 15 | oldList: TokenList 16 | newList: TokenList 17 | auto: boolean 18 | } 19 | } 20 | 21 | export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('app/updateBlockNumber') 22 | export const toggleWalletModal = createAction('app/toggleWalletModal') 23 | export const toggleSettingsMenu = createAction('app/toggleSettingsMenu') 24 | export const addPopup = createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>( 25 | 'app/addPopup' 26 | ) 27 | export const removePopup = createAction<{ key: string }>('app/removePopup') 28 | -------------------------------------------------------------------------------- /src/state/application/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | import { useActiveWeb3React } from '../../hooks' 3 | import { addPopup, PopupContent, removePopup, toggleWalletModal, toggleSettingsMenu } from './actions' 4 | import { useSelector, useDispatch } from 'react-redux' 5 | import { AppState } from '../index' 6 | 7 | export function useBlockNumber(): number | undefined { 8 | const { chainId } = useActiveWeb3React() 9 | 10 | return useSelector((state: AppState) => state.application.blockNumber[chainId ?? -1]) 11 | } 12 | 13 | export function useWalletModalOpen(): boolean { 14 | return useSelector((state: AppState) => state.application.walletModalOpen) 15 | } 16 | 17 | export function useWalletModalToggle(): () => void { 18 | const dispatch = useDispatch() 19 | return useCallback(() => dispatch(toggleWalletModal()), [dispatch]) 20 | } 21 | 22 | export function useSettingsMenuOpen(): boolean { 23 | return useSelector((state: AppState) => state.application.settingsMenuOpen) 24 | } 25 | 26 | export function useToggleSettingsMenu(): () => void { 27 | const dispatch = useDispatch() 28 | return useCallback(() => dispatch(toggleSettingsMenu()), [dispatch]) 29 | } 30 | 31 | // returns a function that allows adding a popup 32 | export function useAddPopup(): (content: PopupContent, key?: string) => void { 33 | const dispatch = useDispatch() 34 | 35 | return useCallback( 36 | (content: PopupContent, key?: string) => { 37 | dispatch(addPopup({ content, key })) 38 | }, 39 | [dispatch] 40 | ) 41 | } 42 | 43 | // returns a function that allows removing a popup via its key 44 | export function useRemovePopup(): (key: string) => void { 45 | const dispatch = useDispatch() 46 | return useCallback( 47 | (key: string) => { 48 | dispatch(removePopup({ key })) 49 | }, 50 | [dispatch] 51 | ) 52 | } 53 | 54 | // get the list of active popups 55 | export function useActivePopups(): AppState['application']['popupList'] { 56 | const list = useSelector((state: AppState) => state.application.popupList) 57 | return useMemo(() => list.filter(item => item.show), [list]) 58 | } 59 | -------------------------------------------------------------------------------- /src/state/application/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, nanoid } from '@reduxjs/toolkit' 2 | import { 3 | addPopup, 4 | PopupContent, 5 | removePopup, 6 | toggleWalletModal, 7 | toggleSettingsMenu, 8 | updateBlockNumber 9 | } from './actions' 10 | 11 | type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }> 12 | 13 | export interface ApplicationState { 14 | blockNumber: { [chainId: number]: number } 15 | popupList: PopupList 16 | walletModalOpen: boolean 17 | settingsMenuOpen: boolean 18 | } 19 | 20 | const initialState: ApplicationState = { 21 | blockNumber: {}, 22 | popupList: [], 23 | walletModalOpen: false, 24 | settingsMenuOpen: false 25 | } 26 | 27 | export default createReducer(initialState, builder => 28 | builder 29 | .addCase(updateBlockNumber, (state, action) => { 30 | const { chainId, blockNumber } = action.payload 31 | if (typeof state.blockNumber[chainId] !== 'number') { 32 | state.blockNumber[chainId] = blockNumber 33 | } else { 34 | state.blockNumber[chainId] = Math.max(blockNumber, state.blockNumber[chainId]) 35 | } 36 | }) 37 | .addCase(toggleWalletModal, state => { 38 | state.walletModalOpen = !state.walletModalOpen 39 | }) 40 | .addCase(toggleSettingsMenu, state => { 41 | state.settingsMenuOpen = !state.settingsMenuOpen 42 | }) 43 | .addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000 } }) => { 44 | state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([ 45 | { 46 | key: key || nanoid(), 47 | show: true, 48 | content, 49 | removeAfterMs 50 | } 51 | ]) 52 | }) 53 | .addCase(removePopup, (state, { payload: { key } }) => { 54 | state.popupList.forEach(p => { 55 | if (p.key === key) { 56 | p.show = false 57 | } 58 | }) 59 | }) 60 | ) 61 | -------------------------------------------------------------------------------- /src/state/application/updater.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | import { useActiveWeb3React } from '../../hooks' 3 | import useDebounce from '../../hooks/useDebounce' 4 | import useIsWindowVisible from '../../hooks/useIsWindowVisible' 5 | import { updateBlockNumber } from './actions' 6 | import { useDispatch } from 'react-redux' 7 | 8 | export default function Updater(): null { 9 | const { library, chainId } = useActiveWeb3React() 10 | const dispatch = useDispatch() 11 | 12 | const windowVisible = useIsWindowVisible() 13 | 14 | const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({ 15 | chainId, 16 | blockNumber: null 17 | }) 18 | 19 | const blockNumberCallback = useCallback( 20 | (blockNumber: number) => { 21 | setState(state => { 22 | if (chainId === state.chainId) { 23 | if (typeof state.blockNumber !== 'number') return { chainId, blockNumber } 24 | return { chainId, blockNumber: Math.max(blockNumber, state.blockNumber) } 25 | } 26 | return state 27 | }) 28 | }, 29 | [chainId, setState] 30 | ) 31 | 32 | // attach/detach listeners 33 | useEffect(() => { 34 | if (!library || !chainId || !windowVisible) return undefined 35 | 36 | setState({ chainId, blockNumber: null }) 37 | 38 | library 39 | .getBlockNumber() 40 | .then(blockNumberCallback) 41 | .catch(error => console.error(`Failed to get block number for chainId: ${chainId}`, error)) 42 | 43 | library.on('block', blockNumberCallback) 44 | return () => { 45 | library.removeListener('block', blockNumberCallback) 46 | } 47 | }, [dispatch, chainId, library, blockNumberCallback, windowVisible]) 48 | 49 | const debouncedState = useDebounce(state, 100) 50 | 51 | useEffect(() => { 52 | if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return 53 | dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber })) 54 | }, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId]) 55 | 56 | return null 57 | } 58 | -------------------------------------------------------------------------------- /src/state/burn/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export enum Field { 4 | LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT', 5 | LIQUIDITY = 'LIQUIDITY', 6 | CURRENCY_A = 'CURRENCY_A', 7 | CURRENCY_B = 'CURRENCY_B' 8 | } 9 | 10 | export const typeInput = createAction<{ field: Field; typedValue: string }>('burn/typeInputBurn') 11 | -------------------------------------------------------------------------------- /src/state/burn/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit' 2 | import { Field, typeInput } from './actions' 3 | 4 | export interface BurnState { 5 | readonly independentField: Field 6 | readonly typedValue: string 7 | } 8 | 9 | const initialState: BurnState = { 10 | independentField: Field.LIQUIDITY_PERCENT, 11 | typedValue: '0' 12 | } 13 | 14 | export default createReducer(initialState, builder => 15 | builder.addCase(typeInput, (state, { payload: { field, typedValue } }) => { 16 | return { 17 | ...state, 18 | independentField: field, 19 | typedValue 20 | } 21 | }) 22 | ) 23 | -------------------------------------------------------------------------------- /src/state/calculator/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorWithPayload, createAction } from '@reduxjs/toolkit' 2 | import { CalculatorGraphData } from './reducer' 3 | 4 | export enum Field { 5 | STAKE_YEAR = 'STAKE_YEAR', 6 | TOTAL_LIQUIDITY_PROVIDED_USD = 'TOTAL_LIQUIDITY_PROVIDED_USD', 7 | TOTAL_STAKED_AMOUNT_CRO = 'TOTAL_STAKED_AMOUNT_CRO' 8 | } 9 | 10 | export const resetYieldState = createAction('calculator/resetYieldState') 11 | export const typeInput = createAction<{ field: Field; typedValue: string }>('calculator/typeInputYield') 12 | 13 | export const fetchCalculatorGraphData: Readonly<{ 14 | pending: ActionCreatorWithPayload<{ url: string; requestId: string }> 15 | fulfilled: ActionCreatorWithPayload<{ url: string; graphData: CalculatorGraphData; requestId: string }> 16 | rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }> 17 | }> = { 18 | pending: createAction('calculator/fetchCalculatorGraphData/pending'), 19 | fulfilled: createAction('calculator/fetchCalculatorGraphData/fulfilled'), 20 | rejected: createAction('calculator/fetchCalculatorGraphData/rejected') 21 | } 22 | -------------------------------------------------------------------------------- /src/state/calculator/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | 4 | import { AppDispatch, AppState } from '../index' 5 | import { Field, typeInput } from './actions' 6 | 7 | export function useCalculatorState(): AppState['calculator'] { 8 | return useSelector(state => state.calculator) 9 | } 10 | 11 | export function useCalculatorActionHandlers(): { 12 | onStakedCroAmountInput: (typedValue: string) => void 13 | onLiquidityProvidedUsdAmount: (typedValue: string) => void 14 | onStakeYear: (typedValue: string) => void 15 | } { 16 | const dispatch = useDispatch() 17 | 18 | const onLiquidityProvidedUsdAmount = useCallback( 19 | (typedValue: string) => { 20 | if (!typedValue || typedValue.match(/^\d{1,12}(\.\d{0,2})?$/)) { 21 | dispatch(typeInput({ field: Field.TOTAL_LIQUIDITY_PROVIDED_USD, typedValue })) 22 | } 23 | }, 24 | [dispatch] 25 | ) 26 | const onStakedCroAmountInput = useCallback( 27 | (typedValue: string) => { 28 | if (!typedValue || typedValue.match(/^\d{1,12}(\.\d{0,18})?$/)) { 29 | dispatch(typeInput({ field: Field.TOTAL_STAKED_AMOUNT_CRO, typedValue })) 30 | } 31 | }, 32 | [dispatch] 33 | ) 34 | const onStakeYear = useCallback( 35 | (typedValue: string) => { 36 | dispatch(typeInput({ field: Field.STAKE_YEAR, typedValue })) 37 | }, 38 | [dispatch] 39 | ) 40 | 41 | return { 42 | onStakedCroAmountInput, 43 | onLiquidityProvidedUsdAmount, 44 | onStakeYear 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/state/calculator/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit' 2 | import { Field, resetYieldState, typeInput, fetchCalculatorGraphData } from './actions' 3 | 4 | export interface CalculatorGraphData { 5 | readonly liquidityProvidedUsd: string 6 | readonly existingStakeList: { 7 | readonly stakeCroAmount: string 8 | readonly existingStakeYear: number 9 | }[] 10 | readonly totalCropWeight: string 11 | readonly allPoolStakedCroAmount: string 12 | readonly croToUsdRate: string 13 | readonly totalPoolLiquidityUsd: string 14 | readonly averageMultiplier: string 15 | } 16 | 17 | export interface CalculatorResult { 18 | originalApyPercent: string 19 | newApyPercent: string 20 | annualizedCroRewards: string 21 | } 22 | 23 | export interface CalculatorState { 24 | readonly graphData: CalculatorGraphData 25 | readonly constants: { 26 | ratioCroStakedToDailyRewardPoolPercent: number 27 | minimumDailyRewardPool: number 28 | } 29 | readonly [Field.TOTAL_LIQUIDITY_PROVIDED_USD]: string 30 | readonly [Field.TOTAL_STAKED_AMOUNT_CRO]: string 31 | readonly [Field.STAKE_YEAR]: string 32 | } 33 | 34 | export const initialState: CalculatorState = { 35 | constants: { 36 | ratioCroStakedToDailyRewardPoolPercent: 0.1, 37 | minimumDailyRewardPool: 1000000 38 | }, 39 | graphData: { 40 | liquidityProvidedUsd: '0', 41 | existingStakeList: [], 42 | totalCropWeight: '0', 43 | allPoolStakedCroAmount: '54000000', 44 | croToUsdRate: '0.18', 45 | totalPoolLiquidityUsd: '75000000', 46 | averageMultiplier: '1' 47 | }, 48 | [Field.TOTAL_LIQUIDITY_PROVIDED_USD]: '', 49 | [Field.TOTAL_STAKED_AMOUNT_CRO]: '', 50 | [Field.STAKE_YEAR]: '2' 51 | } 52 | 53 | export default createReducer(initialState, builder => 54 | builder 55 | .addCase(resetYieldState, state => { 56 | return { 57 | ...initialState, 58 | graphData: state.graphData 59 | } 60 | }) 61 | .addCase(typeInput, (state, { payload: { field, typedValue } }) => { 62 | state[field] = typedValue 63 | }) 64 | .addCase(fetchCalculatorGraphData.pending, (state, { payload: { requestId, url } }) => { 65 | state 66 | }) 67 | .addCase(fetchCalculatorGraphData.fulfilled, (state, { payload: { requestId, graphData, url } }) => { 68 | state.graphData = graphData 69 | }) 70 | .addCase(fetchCalculatorGraphData.rejected, (state, { payload: { url, requestId, errorMessage } }) => { 71 | state 72 | }) 73 | ) 74 | -------------------------------------------------------------------------------- /src/state/calculator/updater.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { useActiveWeb3React } from '../../hooks' 4 | import useInterval from '../../hooks/useInterval' 5 | import useIsWindowVisible from '../../hooks/useIsWindowVisible' 6 | import { AppDispatch, AppState } from '../index' 7 | import { useFetchCalculatorGraphDataCallback } from '../../hooks/useFetchCalculatorGraphDataCallback' 8 | 9 | export default function Updater(): null { 10 | const { library } = useActiveWeb3React() 11 | const dispatch = useDispatch() 12 | const graphData = useSelector(state => state.calculator.graphData) 13 | 14 | const isWindowVisible = useIsWindowVisible() 15 | 16 | const fetchCalculatorGraphData = useFetchCalculatorGraphDataCallback() 17 | 18 | const fetchAllCalculatorGraphData = useCallback(() => { 19 | if (!isWindowVisible) return 20 | 21 | fetchCalculatorGraphData('').catch(error => console.debug('interval list fetching error', error)) 22 | }, [fetchCalculatorGraphData, isWindowVisible, graphData]) 23 | 24 | // fetch all lists every 10 minutes, but only after we initialize library 25 | useInterval(fetchAllCalculatorGraphData, library ? 1000 * 60 * 10 : null) 26 | 27 | return null 28 | } 29 | -------------------------------------------------------------------------------- /src/state/global/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | // fired once when the app reloads but before the app renders 4 | // allows any updates to be applied to store data loaded from localStorage 5 | export const updateVersion = createAction('global/updateVersion') 6 | -------------------------------------------------------------------------------- /src/state/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit' 2 | import { save, load } from 'redux-localstorage-simple' 3 | 4 | import application from './application/reducer' 5 | import { updateVersion } from './global/actions' 6 | import user from './user/reducer' 7 | import transactions from './transactions/reducer' 8 | import swap from './swap/reducer' 9 | import mint from './mint/reducer' 10 | import lists from './lists/reducer' 11 | import burn from './burn/reducer' 12 | import multicall from './multicall/reducer' 13 | import calculator from './calculator/reducer' 14 | 15 | const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists'] 16 | 17 | const store = configureStore({ 18 | reducer: { 19 | application, 20 | user, 21 | transactions, 22 | swap, 23 | mint, 24 | burn, 25 | multicall, 26 | lists, 27 | calculator 28 | }, 29 | middleware: [...getDefaultMiddleware({ thunk: false }), save({ states: PERSISTED_KEYS })], 30 | preloadedState: load({ states: PERSISTED_KEYS }) 31 | }) 32 | 33 | store.dispatch(updateVersion()) 34 | 35 | export default store 36 | 37 | export type AppState = ReturnType 38 | export type AppDispatch = typeof store.dispatch 39 | -------------------------------------------------------------------------------- /src/state/lists/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorWithPayload, createAction } from '@reduxjs/toolkit' 2 | import { TokenList, Version } from '@uniswap/token-lists' 3 | 4 | export const fetchTokenList: Readonly<{ 5 | pending: ActionCreatorWithPayload<{ url: string; requestId: string }> 6 | fulfilled: ActionCreatorWithPayload<{ url: string; tokenList: TokenList; requestId: string }> 7 | rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }> 8 | }> = { 9 | pending: createAction('lists/fetchTokenList/pending'), 10 | fulfilled: createAction('lists/fetchTokenList/fulfilled'), 11 | rejected: createAction('lists/fetchTokenList/rejected') 12 | } 13 | 14 | export const acceptListUpdate = createAction('lists/acceptListUpdate') 15 | export const addList = createAction('lists/addList') 16 | export const removeList = createAction('lists/removeList') 17 | export const selectList = createAction('lists/selectList') 18 | export const rejectVersionUpdate = createAction('lists/rejectVersionUpdate') 19 | -------------------------------------------------------------------------------- /src/state/mint/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export enum Field { 4 | CURRENCY_A = 'CURRENCY_A', 5 | CURRENCY_B = 'CURRENCY_B' 6 | } 7 | 8 | export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('mint/typeInputMint') 9 | export const resetMintState = createAction('mint/resetMintState') 10 | -------------------------------------------------------------------------------- /src/state/mint/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'redux' 2 | 3 | import { Field, typeInput } from './actions' 4 | import reducer, { MintState } from './reducer' 5 | 6 | describe('mint reducer', () => { 7 | let store: Store 8 | 9 | beforeEach(() => { 10 | store = createStore(reducer, { 11 | independentField: Field.CURRENCY_A, 12 | typedValue: '', 13 | otherTypedValue: '' 14 | }) 15 | }) 16 | 17 | describe('typeInput', () => { 18 | it('sets typed value', () => { 19 | store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false })) 20 | expect(store.getState()).toEqual({ independentField: Field.CURRENCY_A, typedValue: '1.0', otherTypedValue: '' }) 21 | }) 22 | it('clears other value', () => { 23 | store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false })) 24 | store.dispatch(typeInput({ field: Field.CURRENCY_B, typedValue: '1.0', noLiquidity: false })) 25 | expect(store.getState()).toEqual({ independentField: Field.CURRENCY_B, typedValue: '1.0', otherTypedValue: '' }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/state/mint/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit' 2 | import { Field, resetMintState, typeInput } from './actions' 3 | 4 | export interface MintState { 5 | readonly independentField: Field 6 | readonly typedValue: string 7 | readonly otherTypedValue: string // for the case when there's no liquidity 8 | } 9 | 10 | const initialState: MintState = { 11 | independentField: Field.CURRENCY_A, 12 | typedValue: '', 13 | otherTypedValue: '' 14 | } 15 | 16 | export default createReducer(initialState, builder => 17 | builder 18 | .addCase(resetMintState, () => initialState) 19 | .addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => { 20 | if (noLiquidity) { 21 | // they're typing into the field they've last typed in 22 | if (field === state.independentField) { 23 | return { 24 | ...state, 25 | independentField: field, 26 | typedValue 27 | } 28 | } 29 | // they're typing into a new field, store the other value 30 | else { 31 | return { 32 | ...state, 33 | independentField: field, 34 | typedValue, 35 | otherTypedValue: state.typedValue 36 | } 37 | } 38 | } else { 39 | return { 40 | ...state, 41 | independentField: field, 42 | typedValue, 43 | otherTypedValue: '' 44 | } 45 | } 46 | }) 47 | ) 48 | -------------------------------------------------------------------------------- /src/state/multicall/actions.test.ts: -------------------------------------------------------------------------------- 1 | import { parseCallKey, toCallKey } from './actions' 2 | 3 | describe('actions', () => { 4 | describe('#parseCallKey', () => { 5 | it('does not throw for invalid address', () => { 6 | expect(parseCallKey('0x-0x')).toEqual({ address: '0x', callData: '0x' }) 7 | }) 8 | it('does not throw for invalid calldata', () => { 9 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toEqual({ 10 | address: '0x6b175474e89094c44da98b954eedeac495271d0f', 11 | callData: 'abc' 12 | }) 13 | }) 14 | it('throws for invalid format', () => { 15 | expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc') 16 | }) 17 | it('throws for uppercase calldata', () => { 18 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({ 19 | address: '0x6b175474e89094c44da98b954eedeac495271d0f', 20 | callData: '0xabcD' 21 | }) 22 | }) 23 | it('parses pieces into address', () => { 24 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({ 25 | address: '0x6b175474e89094c44da98b954eedeac495271d0f', 26 | callData: '0xabcd' 27 | }) 28 | }) 29 | }) 30 | 31 | describe('#toCallKey', () => { 32 | it('throws for invalid address', () => { 33 | expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x') 34 | }) 35 | it('throws for invalid calldata', () => { 36 | expect(() => 37 | toCallKey({ 38 | address: '0x6b175474e89094c44da98b954eedeac495271d0f', 39 | callData: 'abc' 40 | }) 41 | ).toThrow('Invalid hex: abc') 42 | }) 43 | it('throws for uppercase hex', () => { 44 | expect(() => 45 | toCallKey({ 46 | address: '0x6b175474e89094c44da98b954eedeac495271d0f', 47 | callData: '0xabcD' 48 | }) 49 | ).toThrow('Invalid hex: 0xabcD') 50 | }) 51 | it('concatenates address to data', () => { 52 | expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual( 53 | '0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd' 54 | ) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/state/multicall/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export interface Call { 4 | address: string 5 | callData: string 6 | } 7 | 8 | const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/ 9 | const LOWER_HEX_REGEX = /^0x[a-f0-9]*$/ 10 | export function toCallKey(call: Call): string { 11 | if (!ADDRESS_REGEX.test(call.address)) { 12 | throw new Error(`Invalid address: ${call.address}`) 13 | } 14 | if (!LOWER_HEX_REGEX.test(call.callData)) { 15 | throw new Error(`Invalid hex: ${call.callData}`) 16 | } 17 | return `${call.address}-${call.callData}` 18 | } 19 | 20 | export function parseCallKey(callKey: string): Call { 21 | const pcs = callKey.split('-') 22 | if (pcs.length !== 2) { 23 | throw new Error(`Invalid call key: ${callKey}`) 24 | } 25 | return { 26 | address: pcs[0], 27 | callData: pcs[1] 28 | } 29 | } 30 | 31 | export interface ListenerOptions { 32 | // how often this data should be fetched, by default 1 33 | readonly blocksPerFetch?: number 34 | } 35 | 36 | export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>( 37 | 'multicall/addMulticallListeners' 38 | ) 39 | export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>( 40 | 'multicall/removeMulticallListeners' 41 | ) 42 | export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>( 43 | 'multicall/fetchingMulticallResults' 44 | ) 45 | export const errorFetchingMulticallResults = createAction<{ 46 | chainId: number 47 | calls: Call[] 48 | fetchingBlockNumber: number 49 | }>('multicall/errorFetchingMulticallResults') 50 | export const updateMulticallResults = createAction<{ 51 | chainId: number 52 | blockNumber: number 53 | results: { 54 | [callKey: string]: string | null 55 | } 56 | }>('multicall/updateMulticallResults') 57 | -------------------------------------------------------------------------------- /src/state/stake/accruedCro.test.ts: -------------------------------------------------------------------------------- 1 | import { computeAccruedCro } from './accruedCro' 2 | 3 | describe('accruedCro domain', () => { 4 | describe('computeAccruedCro', () => { 5 | it('get empty default result', () => { 6 | expect(computeAccruedCro([])).toEqual({ 7 | totalAccruedCro: '0.00' 8 | }) 9 | }) 10 | 11 | it('get some result with 1 item in reward', () => { 12 | expect(computeAccruedCro([{ reward: '1000000', timestamp: '1599591600' }])).toEqual({ 13 | totalAccruedCro: '1000000' 14 | }) 15 | }) 16 | 17 | it('get some result for multiple days', () => { 18 | expect( 19 | computeAccruedCro([ 20 | { reward: '1000000', timestamp: '1599591600' }, 21 | { reward: '1000000', timestamp: '1599620400' }, 22 | { reward: '1000000', timestamp: '1599624000' }, 23 | { reward: '981306.4876033951467473046839320509', timestamp: '1599627600' }, 24 | { reward: '977471.0348374786514495211287361187', timestamp: '1599631200' } 25 | ]) 26 | ).toEqual({ 27 | totalAccruedCro: '1977471.0348374786514495211287361187' 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/state/stake/accruedCro.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | 3 | export interface HourlyRewardRaw { 4 | reward: string 5 | timestamp: string 6 | } 7 | 8 | interface HourlyReward { 9 | reward: BigNumber 10 | date: string 11 | time: string 12 | } 13 | 14 | const groupBy = (array: {}[], key: string) => { 15 | return array.reduce((result, currentValue) => { 16 | ;(result[currentValue[key]] = result[currentValue[key]] || []).push(currentValue) 17 | return result 18 | }, {}) 19 | } 20 | 21 | function computeDailyReward(rewardInDay: HourlyReward[]): BigNumber { 22 | return rewardInDay.reduce((previousValue: BigNumber, { reward }: HourlyReward) => { 23 | return reward.lt(previousValue) ? reward : previousValue 24 | }, new BigNumber(Infinity)) 25 | } 26 | 27 | export function computeAccruedCro(rewardsPerHour: HourlyRewardRaw[]): { totalAccruedCro: string } { 28 | const emptyResult = { totalAccruedCro: '0.00' } 29 | const rewards: HourlyReward[] = rewardsPerHour.map(({ reward, timestamp }) => { 30 | return { 31 | reward: new BigNumber(reward), 32 | date: new Date(+timestamp * 1000).toISOString().split('T')[0], 33 | time: new Date(+timestamp * 1000).toISOString().split('T')[1] 34 | } 35 | }) 36 | if (rewards.length == 0) return emptyResult 37 | const rewardGroupByDate = groupBy(rewards, 'date') as { [key: string]: HourlyReward[] } 38 | 39 | const totalAccruedCro = Object.keys(rewardGroupByDate) 40 | .sort() 41 | .reduce((previousValue: BigNumber, currentValue: string) => { 42 | const rewardsListPerDay = rewardGroupByDate[currentValue] 43 | const reward = computeDailyReward(rewardsListPerDay) 44 | return previousValue.plus(reward) 45 | }, new BigNumber(0)) 46 | 47 | return { totalAccruedCro: totalAccruedCro.toString() } 48 | } 49 | -------------------------------------------------------------------------------- /src/state/swap/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export enum Field { 4 | INPUT = 'INPUT', 5 | OUTPUT = 'OUTPUT' 6 | } 7 | 8 | export const selectCurrency = createAction<{ field: Field; currencyId: string }>('swap/selectCurrency') 9 | export const switchCurrencies = createAction('swap/switchCurrencies') 10 | export const typeInput = createAction<{ field: Field; typedValue: string }>('swap/typeInput') 11 | export const replaceSwapState = createAction<{ 12 | field: Field 13 | typedValue: string 14 | inputCurrencyId?: string 15 | outputCurrencyId?: string 16 | recipient: string | null 17 | }>('swap/replaceSwapState') 18 | export const setRecipient = createAction<{ recipient: string | null }>('swap/setRecipient') 19 | -------------------------------------------------------------------------------- /src/state/swap/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'redux' 2 | import { Field, selectCurrency } from './actions' 3 | import reducer, { SwapState } from './reducer' 4 | 5 | describe('swap reducer', () => { 6 | let store: Store 7 | 8 | beforeEach(() => { 9 | store = createStore(reducer, { 10 | [Field.OUTPUT]: { currencyId: '' }, 11 | [Field.INPUT]: { currencyId: '' }, 12 | typedValue: '', 13 | independentField: Field.INPUT, 14 | recipient: null 15 | }) 16 | }) 17 | 18 | describe('selectToken', () => { 19 | it('changes token', () => { 20 | store.dispatch( 21 | selectCurrency({ 22 | field: Field.OUTPUT, 23 | currencyId: '0x0000' 24 | }) 25 | ) 26 | 27 | expect(store.getState()).toEqual({ 28 | [Field.OUTPUT]: { currencyId: '0x0000' }, 29 | [Field.INPUT]: { currencyId: '' }, 30 | typedValue: '', 31 | independentField: Field.INPUT, 32 | recipient: null 33 | }) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/state/transactions/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | import { ChainId } from 'swap-sdk' 3 | 4 | export interface SerializableTransactionReceipt { 5 | to: string 6 | from: string 7 | contractAddress: string 8 | transactionIndex: number 9 | blockHash: string 10 | transactionHash: string 11 | blockNumber: number 12 | status?: number 13 | } 14 | 15 | export const addTransaction = createAction<{ 16 | chainId: ChainId 17 | hash: string 18 | from: string 19 | approval?: { tokenAddress: string; spender: string } 20 | summary?: string 21 | }>('transactions/addTransaction') 22 | export const clearAllTransactions = createAction<{ chainId: ChainId }>('transactions/clearAllTransactions') 23 | export const finalizeTransaction = createAction<{ 24 | chainId: ChainId 25 | hash: string 26 | receipt: SerializableTransactionReceipt 27 | }>('transactions/finalizeTransaction') 28 | export const checkedTransaction = createAction<{ 29 | chainId: ChainId 30 | hash: string 31 | blockNumber: number 32 | }>('transactions/checkedTransaction') 33 | -------------------------------------------------------------------------------- /src/state/transactions/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit' 2 | import { 3 | addTransaction, 4 | checkedTransaction, 5 | clearAllTransactions, 6 | finalizeTransaction, 7 | SerializableTransactionReceipt 8 | } from './actions' 9 | 10 | const now = () => new Date().getTime() 11 | 12 | export interface TransactionDetails { 13 | hash: string 14 | approval?: { tokenAddress: string; spender: string } 15 | summary?: string 16 | receipt?: SerializableTransactionReceipt 17 | lastCheckedBlockNumber?: number 18 | addedTime: number 19 | confirmedTime?: number 20 | from: string 21 | } 22 | 23 | export interface TransactionState { 24 | [chainId: number]: { 25 | [txHash: string]: TransactionDetails 26 | } 27 | } 28 | 29 | export const initialState: TransactionState = {} 30 | 31 | export default createReducer(initialState, builder => 32 | builder 33 | .addCase(addTransaction, (transactions, { payload: { chainId, from, hash, approval, summary } }) => { 34 | if (transactions[chainId]?.[hash]) { 35 | throw Error('Attempted to add existing transaction.') 36 | } 37 | const txs = transactions[chainId] ?? {} 38 | txs[hash] = { hash, approval, summary, from, addedTime: now() } 39 | transactions[chainId] = txs 40 | }) 41 | .addCase(clearAllTransactions, (transactions, { payload: { chainId } }) => { 42 | if (!transactions[chainId]) return 43 | transactions[chainId] = {} 44 | }) 45 | .addCase(checkedTransaction, (transactions, { payload: { chainId, hash, blockNumber } }) => { 46 | const tx = transactions[chainId]?.[hash] 47 | if (!tx) { 48 | return 49 | } 50 | if (!tx.lastCheckedBlockNumber) { 51 | tx.lastCheckedBlockNumber = blockNumber 52 | } else { 53 | tx.lastCheckedBlockNumber = Math.max(blockNumber, tx.lastCheckedBlockNumber) 54 | } 55 | }) 56 | .addCase(finalizeTransaction, (transactions, { payload: { hash, chainId, receipt } }) => { 57 | const tx = transactions[chainId]?.[hash] 58 | if (!tx) { 59 | return 60 | } 61 | tx.receipt = receipt 62 | tx.confirmedTime = now() 63 | }) 64 | ) 65 | -------------------------------------------------------------------------------- /src/state/transactions/updater.test.ts: -------------------------------------------------------------------------------- 1 | import { shouldCheck } from './updater' 2 | 3 | describe('transactions updater', () => { 4 | describe('shouldCheck', () => { 5 | it('returns true if no receipt and never checked', () => { 6 | expect(shouldCheck(10, { addedTime: 100 })).toEqual(true) 7 | }) 8 | it('returns false if has receipt and never checked', () => { 9 | expect(shouldCheck(10, { addedTime: 100, receipt: {} })).toEqual(false) 10 | }) 11 | it('returns true if has not been checked in 1 blocks', () => { 12 | expect(shouldCheck(10, { addedTime: new Date().getTime(), lastCheckedBlockNumber: 9 })).toEqual(true) 13 | }) 14 | it('returns false if checked in last 3 blocks and greater than 20 minutes old', () => { 15 | expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 8 })).toEqual( 16 | false 17 | ) 18 | }) 19 | it('returns true if not checked in last 5 blocks and greater than 20 minutes old', () => { 20 | expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 5 })).toEqual( 21 | true 22 | ) 23 | }) 24 | it('returns false if checked in last 10 blocks and greater than 60 minutes old', () => { 25 | expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 11 })).toEqual( 26 | false 27 | ) 28 | }) 29 | it('returns true if checked in last 3 blocks and greater than 20 minutes old', () => { 30 | expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 10 })).toEqual( 31 | true 32 | ) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/state/user/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export interface SerializedToken { 4 | chainId: number 5 | address: string 6 | decimals: number 7 | symbol?: string 8 | name?: string 9 | } 10 | 11 | export interface SerializedPair { 12 | token0: SerializedToken 13 | token1: SerializedToken 14 | } 15 | 16 | export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode') 17 | export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') 18 | export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode') 19 | export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>( 20 | 'user/updateUserSlippageTolerance' 21 | ) 22 | export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline') 23 | export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken') 24 | export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken') 25 | export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair') 26 | export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>( 27 | 'user/removeSerializedPair' 28 | ) 29 | -------------------------------------------------------------------------------- /src/state/user/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'redux' 2 | import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants' 3 | import { updateVersion } from '../global/actions' 4 | import reducer, { initialState, UserState } from './reducer' 5 | 6 | describe('swap reducer', () => { 7 | let store: Store 8 | 9 | beforeEach(() => { 10 | store = createStore(reducer, initialState) 11 | }) 12 | 13 | describe('updateVersion', () => { 14 | it('has no timestamp originally', () => { 15 | expect(store.getState().lastUpdateVersionTimestamp).toBeUndefined() 16 | }) 17 | it('sets the lastUpdateVersionTimestamp', () => { 18 | const time = new Date().getTime() 19 | store.dispatch(updateVersion()) 20 | expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time) 21 | }) 22 | it('sets allowed slippage and deadline', () => { 23 | store = createStore(reducer, { 24 | ...initialState, 25 | userDeadline: undefined, 26 | userSlippageTolerance: undefined 27 | } as any) 28 | store.dispatch(updateVersion()) 29 | expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW) 30 | expect(store.getState().userSlippageTolerance).toEqual(INITIAL_ALLOWED_SLIPPAGE) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/state/user/updater.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { AppDispatch } from '../index' 4 | import { updateMatchesDarkMode } from './actions' 5 | 6 | export default function Updater(): null { 7 | const dispatch = useDispatch() 8 | 9 | // keep dark mode in sync with the system 10 | useEffect(() => { 11 | const darkHandler = (match: MediaQueryListEvent) => { 12 | dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches })) 13 | } 14 | 15 | const match = window?.matchMedia('(prefers-color-scheme: dark)') 16 | dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches })) 17 | 18 | if (match?.addListener) { 19 | match?.addListener(darkHandler) 20 | } else if (match?.addEventListener) { 21 | match?.addEventListener('change', darkHandler) 22 | } 23 | 24 | return () => { 25 | if (match?.removeListener) { 26 | match?.removeListener(darkHandler) 27 | } else if (match?.removeEventListener) { 28 | match?.removeEventListener('change', darkHandler) 29 | } 30 | } 31 | }, [dispatch]) 32 | 33 | return null 34 | } 35 | -------------------------------------------------------------------------------- /src/theme/DarkModeQueryParamReader.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { RouteComponentProps } from 'react-router-dom' 4 | import { parse } from 'qs' 5 | import { AppDispatch } from '../state' 6 | import { updateUserDarkMode } from '../state/user/actions' 7 | 8 | export default function DarkModeQueryParamReader({ location: { search } }: RouteComponentProps): null { 9 | const dispatch = useDispatch() 10 | 11 | useEffect(() => { 12 | if (!search) return 13 | if (search.length < 2) return 14 | 15 | const parsed = parse(search, { 16 | parseArrays: false, 17 | ignoreQueryPrefix: true 18 | }) 19 | 20 | const theme = parsed.theme 21 | 22 | if (typeof theme !== 'string') return 23 | 24 | if (theme.toLowerCase() === 'light') { 25 | dispatch(updateUserDarkMode({ userDarkMode: false })) 26 | } else if (theme.toLowerCase() === 'dark') { 27 | dispatch(updateUserDarkMode({ userDarkMode: true })) 28 | } 29 | }, [dispatch, search]) 30 | 31 | return null 32 | } 33 | -------------------------------------------------------------------------------- /src/theme/styled.d.ts: -------------------------------------------------------------------------------- 1 | import { FlattenSimpleInterpolation, ThemedCssFunction } from 'styled-components' 2 | 3 | export type Color = string 4 | export interface Colors { 5 | // base 6 | white: Color 7 | black: Color 8 | 9 | // text 10 | text1: Color 11 | text2: Color 12 | text3: Color 13 | text4: Color 14 | text5: Color 15 | 16 | // backgrounds / greys 17 | bg1: Color 18 | bg2: Color 19 | bg3: Color 20 | bg4: Color 21 | bg5: Color 22 | 23 | modalBG: Color 24 | advancedBG: Color 25 | 26 | //blues 27 | primary1: Color 28 | primary2: Color 29 | primary3: Color 30 | primary4: Color 31 | primary5: Color 32 | 33 | primaryText1: Color 34 | 35 | // pinks 36 | secondary1: Color 37 | secondary2: Color 38 | secondary3: Color 39 | 40 | // other 41 | red1: Color 42 | red2: Color 43 | green1: Color 44 | yellow1: Color 45 | yellow2: Color 46 | } 47 | 48 | export interface Grids { 49 | sm: number 50 | md: number 51 | lg: number 52 | } 53 | 54 | declare module 'styled-components' { 55 | export interface DefaultTheme extends Colors { 56 | grids: Grids 57 | 58 | // shadows 59 | shadow1: string 60 | 61 | // media queries 62 | mediaWidth: { 63 | upToExtraSmall: ThemedCssFunction 64 | upToSmall: ThemedCssFunction 65 | upToMedium: ThemedCssFunction 66 | upToLarge: ThemedCssFunction 67 | } 68 | 69 | // css snippets 70 | flexColumnNoWrap: FlattenSimpleInterpolation 71 | flexRowNoWrap: FlattenSimpleInterpolation 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/chunkArray.test.ts: -------------------------------------------------------------------------------- 1 | import chunkArray from './chunkArray' 2 | 3 | describe('#chunkArray', () => { 4 | it('size 1', () => { 5 | expect(chunkArray([1, 2, 3], 1)).toEqual([[1], [2], [3]]) 6 | }) 7 | it('size 0 throws', () => { 8 | expect(() => chunkArray([1, 2, 3], 0)).toThrow('maxChunkSize must be gte 1') 9 | }) 10 | it('size gte items', () => { 11 | expect(chunkArray([1, 2, 3], 3)).toEqual([[1, 2, 3]]) 12 | expect(chunkArray([1, 2, 3], 4)).toEqual([[1, 2, 3]]) 13 | }) 14 | it('size exact half', () => { 15 | expect(chunkArray([1, 2, 3, 4], 2)).toEqual([ 16 | [1, 2], 17 | [3, 4] 18 | ]) 19 | }) 20 | it('evenly distributes', () => { 21 | const chunked = chunkArray([...Array(100).keys()], 40) 22 | 23 | expect(chunked).toEqual([ 24 | [...Array(34).keys()], 25 | [...Array(34).keys()].map(i => i + 34), 26 | [...Array(32).keys()].map(i => i + 68) 27 | ]) 28 | 29 | expect(chunked[0][0]).toEqual(0) 30 | expect(chunked[2][31]).toEqual(99) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utils/chunkArray.ts: -------------------------------------------------------------------------------- 1 | // chunks array into chunks of maximum size 2 | // evenly distributes items among the chunks 3 | export default function chunkArray(items: T[], maxChunkSize: number): T[][] { 4 | if (maxChunkSize < 1) throw new Error('maxChunkSize must be gte 1') 5 | if (items.length <= maxChunkSize) return [items] 6 | 7 | const numChunks: number = Math.ceil(items.length / maxChunkSize) 8 | const chunkSize = Math.ceil(items.length / numChunks) 9 | 10 | return [...Array(numChunks).keys()].map(ix => items.slice(ix * chunkSize, ix * chunkSize + chunkSize)) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/contenthashToUri.test.skip.ts: -------------------------------------------------------------------------------- 1 | import contenthashToUri, { hexToUint8Array } from './contenthashToUri' 2 | 3 | // this test is skipped for now because importing CID results in 4 | // TypeError: TextDecoder is not a constructor 5 | 6 | describe('#contenthashToUri', () => { 7 | it('1inch.tokens.eth contenthash', () => { 8 | expect(contenthashToUri('0xe3010170122013e051d1cfff20606de36845d4fe28deb9861a319a5bc8596fa4e610e8803918')).toEqual( 9 | 'ipfs://QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1' 10 | ) 11 | }) 12 | it('cro swap contenthash', () => { 13 | expect(contenthashToUri('0xe5010170000f6170702e756e69737761702e6f7267')).toEqual('ipns://app.uniswap.org') 14 | }) 15 | }) 16 | 17 | describe('#hexToUint8Array', () => { 18 | it('common case', () => { 19 | expect(hexToUint8Array('0x010203fdfeff')).toEqual(new Uint8Array([1, 2, 3, 253, 254, 255])) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/contenthashToUri.ts: -------------------------------------------------------------------------------- 1 | import CID from 'cids' 2 | import { getCodec, rmPrefix } from 'multicodec' 3 | import { decode, toB58String } from 'multihashes' 4 | 5 | export function hexToUint8Array(hex: string): Uint8Array { 6 | hex = hex.startsWith('0x') ? hex.substr(2) : hex 7 | if (hex.length % 2 !== 0) throw new Error('hex must have length that is multiple of 2') 8 | const arr = new Uint8Array(hex.length / 2) 9 | for (let i = 0; i < arr.length; i++) { 10 | arr[i] = parseInt(hex.substr(i * 2, 2), 16) 11 | } 12 | return arr 13 | } 14 | 15 | const UTF_8_DECODER = new TextDecoder() 16 | 17 | /** 18 | * Returns the URI representation of the content hash for supported codecs 19 | * @param contenthash to decode 20 | */ 21 | export default function contenthashToUri(contenthash: string): string { 22 | const buff = hexToUint8Array(contenthash) 23 | const codec = getCodec(buff as Buffer) // the typing is wrong for @types/multicodec 24 | switch (codec) { 25 | case 'ipfs-ns': { 26 | const data = rmPrefix(buff as Buffer) 27 | const cid = new CID(data) 28 | return `ipfs://${toB58String(cid.multihash)}` 29 | } 30 | case 'ipns-ns': { 31 | const data = rmPrefix(buff as Buffer) 32 | const cid = new CID(data) 33 | const multihash = decode(cid.multihash) 34 | if (multihash.name === 'identity') { 35 | return `ipns://${UTF_8_DECODER.decode(multihash.digest).trim()}` 36 | } else { 37 | return `ipns://${toB58String(cid.multihash)}` 38 | } 39 | } 40 | default: 41 | throw new Error(`Unrecognized codec: ${codec}`) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/currencyId.ts: -------------------------------------------------------------------------------- 1 | import { Currency, ETHER, Token } from 'swap-sdk' 2 | 3 | export function currencyId(currency: Currency): string { 4 | if (currency === ETHER) return 'ETH' 5 | if (currency instanceof Token) return currency.address 6 | throw new Error('invalid currency') 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/customTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | 4 | export const CustomTitle = ({ titleStr }: { titleStr: string }) => { 5 | return ( 6 | <> 7 | 8 | {titleStr} | Crypto.com 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/formatNumber.ts: -------------------------------------------------------------------------------- 1 | export default function formatNumber(num: string | number) { 2 | return (+num).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 8 }) 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getCalculatorGraphData.ts: -------------------------------------------------------------------------------- 1 | import { CalculatorGraphData } from '../state/calculator/reducer' 2 | import { gql } from 'graphql-request' 3 | import graphQLClient from './graphqlClient' 4 | import { FACTORY_ADDRESS } from 'swap-sdk' 5 | 6 | const calculatorDataQuery = gql` 7 | query data($factoryId: String) { 8 | staking(id: "1") { 9 | totalCrops 10 | totalTokenStaked 11 | averageMultiplier 12 | } 13 | prices(id: "1") { 14 | cro 15 | } 16 | factory(id: $factoryId) { 17 | totalLiquidityUSD 18 | } 19 | } 20 | ` 21 | 22 | function getCroToUsdRate(prices: { cro: string }[]) { 23 | const { cro } = prices[0] 24 | return cro 25 | } 26 | 27 | /** 28 | * Contains the logic for resolving a list URL to a validated token list 29 | * @param listUrl list url 30 | */ 31 | export default async function getCalculatorGraphData(listUrl: string): Promise { 32 | const data = await graphQLClient.request(calculatorDataQuery, { 33 | factoryId: FACTORY_ADDRESS 34 | }) 35 | return { 36 | liquidityProvidedUsd: '0', 37 | totalCropWeight: data.staking.totalCrops, 38 | existingStakeList: [], 39 | allPoolStakedCroAmount: data.staking.totalTokenStaked, 40 | croToUsdRate: getCroToUsdRate(data.prices), 41 | totalPoolLiquidityUsd: data.factory.totalLiquidityUSD, 42 | averageMultiplier: data.staking.averageMultiplier 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/getLibrary.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from '@ethersproject/providers' 2 | 3 | export default function getLibrary(provider: any): Web3Provider { 4 | const library = new Web3Provider(provider) 5 | library.pollingInterval = 15000 6 | return library 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/graphqlClient.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request' 2 | 3 | const endpoint = process.env.THE_GRAPH_URL || '' 4 | const graphQLClient = new GraphQLClient(endpoint) 5 | export default graphQLClient 6 | -------------------------------------------------------------------------------- /src/utils/isZero.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the string value is zero in hex 3 | * @param hexNumberString 4 | */ 5 | export default function isZero(hexNumberString: string) { 6 | return /^0x0*$/.test(hexNumberString) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/listVersionLabel.ts: -------------------------------------------------------------------------------- 1 | import { Version } from '@uniswap/token-lists' 2 | 3 | export default function listVersionLabel(version: Version): string { 4 | return `v${version.major}.${version.minor}.${version.patch}` 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/maxAmountSpend.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount, ETHER, JSBI } from 'swap-sdk' 2 | import { MIN_ETH } from '../constants' 3 | 4 | /** 5 | * Given some token amount, return the max that can be spent of it 6 | * @param currencyAmount to return max of 7 | */ 8 | export function maxAmountSpend(currencyAmount?: CurrencyAmount): CurrencyAmount | undefined { 9 | if (!currencyAmount) return undefined 10 | if (currencyAmount.currency === ETHER) { 11 | if (JSBI.greaterThan(currencyAmount.raw, MIN_ETH)) { 12 | return CurrencyAmount.ether(JSBI.subtract(currencyAmount.raw, MIN_ETH)) 13 | } else { 14 | return CurrencyAmount.ether(JSBI.BigInt(0)) 15 | } 16 | } 17 | return currencyAmount 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/multiplierRange.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | export type StakeYear = '1' | '2' | '3' | '4' 3 | 4 | enum Field { 5 | RANGE = 'range', 6 | ONE = 'one', 7 | TWO = 'two', 8 | THREE = 'three', 9 | FOUR = 'four' 10 | } 11 | 12 | interface RangeRecord { 13 | [Field.RANGE]: string 14 | [Field.ONE]: string 15 | [Field.TWO]: string 16 | [Field.THREE]: string 17 | [Field.FOUR]: string 18 | } 19 | 20 | function csvJSON(csv: string): RangeRecord[] { 21 | const lines = csv.split('\n') 22 | const result: RangeRecord[] = [] 23 | const headers = lines[0].split(',') 24 | 25 | for (let i = 1; i < lines.length; i++) { 26 | const obj: RangeRecord = { 27 | [Field.RANGE]: '', 28 | [Field.ONE]: '', 29 | [Field.TWO]: '', 30 | [Field.THREE]: '', 31 | [Field.FOUR]: '' 32 | } 33 | const currentline = lines[i].split(',') 34 | 35 | for (let j = 0; j < headers.length; j++) { 36 | obj[headers[j]] = currentline[j].trim() 37 | } 38 | 39 | result.push(obj) 40 | } 41 | 42 | return result 43 | } 44 | 45 | const csvString = `range,one,two,three,four 46 | 0,0.0,0.0,0.0,0.0 47 | 1000,1.0,1.2,1.4,2.0 48 | 5000,1.3,1.4,1.8,2.5 49 | 10000,1.5,1.7,2.1,3.0 50 | 50000,2.0,2.3,2.9,4.0 51 | 100000,3.0,3.5,4.3,6.0 52 | 500000,4.0,4.6,5.7,8.0 53 | 1000000,6.0,6.9,8.6,12.0 54 | 5000000,8.0,9.2,11.4,16.0 55 | 50000000,10.0,11.5,14.3,20.0` 56 | 57 | const rangeYearMultiplierRange = csvJSON(csvString) as RangeRecord[] 58 | 59 | export default function getMultiplierRange(croStake: BigNumber, stakeYear: StakeYear): string { 60 | return rangeYearMultiplierRange.reduce((previousValue: string, range: RangeRecord) => { 61 | const rangeNum = new BigNumber(range[Field.RANGE]) 62 | const croStakeNum = new BigNumber(croStake) 63 | if (croStakeNum.lt(rangeNum)) { 64 | return previousValue 65 | } 66 | return stakeYear === '1' ? range.one : stakeYear === '2' ? range.two : stakeYear === '3' ? range.three : range.four 67 | }, '0.0') 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/noRobot.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | 4 | export const NoRobot = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/parseENSAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { parseENSAddress } from './parseENSAddress' 2 | 3 | describe('parseENSAddress', () => { 4 | it('test cases', () => { 5 | expect(parseENSAddress('hello.eth')).toEqual({ ensName: 'hello.eth', ensPath: undefined }) 6 | expect(parseENSAddress('hello.eth/')).toEqual({ ensName: 'hello.eth', ensPath: '/' }) 7 | expect(parseENSAddress('hello.world.eth/')).toEqual({ ensName: 'hello.world.eth', ensPath: '/' }) 8 | expect(parseENSAddress('hello.world.eth/abcdef')).toEqual({ ensName: 'hello.world.eth', ensPath: '/abcdef' }) 9 | expect(parseENSAddress('abso.lutely')).toEqual(undefined) 10 | expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined }) 11 | expect(parseENSAddress('eth')).toEqual(undefined) 12 | expect(parseENSAddress('eth/hello-world')).toEqual(undefined) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/utils/parseENSAddress.ts: -------------------------------------------------------------------------------- 1 | const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/ 2 | 3 | export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined { 4 | const match = ensAddress.match(ENS_NAME_REGEX) 5 | if (!match) return undefined 6 | return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[3] } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/prices.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, JSBI, Pair, Route, Token, TokenAmount, Trade, TradeType } from 'swap-sdk' 2 | import { computeTradePriceBreakdown } from './prices' 3 | 4 | describe('prices', () => { 5 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18) 6 | const token2 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18) 7 | const token3 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000003', 18) 8 | 9 | const pair12 = new Pair(new TokenAmount(token1, JSBI.BigInt(10000)), new TokenAmount(token2, JSBI.BigInt(20000))) 10 | const pair23 = new Pair(new TokenAmount(token2, JSBI.BigInt(20000)), new TokenAmount(token3, JSBI.BigInt(30000))) 11 | 12 | describe('computeTradePriceBreakdown', () => { 13 | it('returns undefined for undefined', () => { 14 | expect(computeTradePriceBreakdown(undefined)).toEqual({ 15 | priceImpactWithoutFee: undefined, 16 | realizedLPFee: undefined 17 | }) 18 | }) 19 | 20 | it('correct realized lp fee for single hop', () => { 21 | expect( 22 | computeTradePriceBreakdown( 23 | new Trade(new Route([pair12], token1), new TokenAmount(token1, JSBI.BigInt(1000)), TradeType.EXACT_INPUT) 24 | ).realizedLPFee 25 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(3))) 26 | }) 27 | 28 | it('correct realized lp fee for double hop', () => { 29 | expect( 30 | computeTradePriceBreakdown( 31 | new Trade( 32 | new Route([pair12, pair23], token1), 33 | new TokenAmount(token1, JSBI.BigInt(1000)), 34 | TradeType.EXACT_INPUT 35 | ) 36 | ).realizedLPFee 37 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(5))) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/utils/resolveENSContentHash.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '@ethersproject/contracts' 2 | import { Provider } from '@ethersproject/abstract-provider' 3 | import { namehash } from 'ethers/lib/utils' 4 | 5 | const REGISTRAR_ABI = [ 6 | { 7 | constant: true, 8 | inputs: [ 9 | { 10 | name: 'node', 11 | type: 'bytes32' 12 | } 13 | ], 14 | name: 'resolver', 15 | outputs: [ 16 | { 17 | name: 'resolverAddress', 18 | type: 'address' 19 | } 20 | ], 21 | payable: false, 22 | stateMutability: 'view', 23 | type: 'function' 24 | } 25 | ] 26 | const REGISTRAR_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' 27 | 28 | const RESOLVER_ABI = [ 29 | { 30 | constant: true, 31 | inputs: [ 32 | { 33 | internalType: 'bytes32', 34 | name: 'node', 35 | type: 'bytes32' 36 | } 37 | ], 38 | name: 'contenthash', 39 | outputs: [ 40 | { 41 | internalType: 'bytes', 42 | name: '', 43 | type: 'bytes' 44 | } 45 | ], 46 | payable: false, 47 | stateMutability: 'view', 48 | type: 'function' 49 | } 50 | ] 51 | 52 | // cache the resolver contracts since most of them are the public resolver 53 | function resolverContract(resolverAddress: string, provider: Provider): Contract { 54 | return new Contract(resolverAddress, RESOLVER_ABI, provider) 55 | } 56 | 57 | /** 58 | * Fetches and decodes the result of an ENS contenthash lookup on mainnet to a URI 59 | * @param ensName to resolve 60 | * @param provider provider to use to fetch the data 61 | */ 62 | export default async function resolveENSContentHash(ensName: string, provider: Provider): Promise { 63 | const ensRegistrarContract = new Contract(REGISTRAR_ADDRESS, REGISTRAR_ABI, provider) 64 | const hash = namehash(ensName) 65 | const resolverAddress = await ensRegistrarContract.resolver(hash) 66 | return resolverContract(resolverAddress, provider).contenthash(hash) 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/retry.test.ts: -------------------------------------------------------------------------------- 1 | import { retry, RetryableError } from './retry' 2 | 3 | describe('retry', () => { 4 | function makeFn(fails: number, result: T, retryable = true): () => Promise { 5 | return async () => { 6 | if (fails > 0) { 7 | fails-- 8 | throw retryable ? new RetryableError('failure') : new Error('bad failure') 9 | } 10 | return result 11 | } 12 | } 13 | 14 | it('fails for non-retryable error', async () => { 15 | await expect(retry(makeFn(1, 'abc', false), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow( 16 | 'bad failure' 17 | ) 18 | }) 19 | 20 | it('works after one fail', async () => { 21 | await expect(retry(makeFn(1, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc') 22 | }) 23 | 24 | it('works after two fails', async () => { 25 | await expect(retry(makeFn(2, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc') 26 | }) 27 | 28 | it('throws if too many fails', async () => { 29 | await expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow('failure') 30 | }) 31 | 32 | it('cancel causes promise to reject', async () => { 33 | const { promise, cancel } = retry(makeFn(2, 'abc'), { n: 3, minWait: 100, maxWait: 100 }) 34 | cancel() 35 | await expect(promise).rejects.toThrow('Cancelled') 36 | }) 37 | 38 | it('cancel no-op after complete', async () => { 39 | const { promise, cancel } = retry(makeFn(0, 'abc'), { n: 3, minWait: 100, maxWait: 100 }) 40 | // defer 41 | setTimeout(cancel, 0) 42 | await expect(promise).resolves.toEqual('abc') 43 | }) 44 | 45 | async function checkTime(fn: () => Promise, min: number, max: number) { 46 | const time = new Date().getTime() 47 | await fn() 48 | const diff = new Date().getTime() - time 49 | expect(diff).toBeGreaterThanOrEqual(min) 50 | expect(diff).toBeLessThanOrEqual(max) 51 | } 52 | 53 | it('waits random amount of time between min and max', async () => { 54 | const promises = [] 55 | for (let i = 0; i < 10; i++) { 56 | promises.push( 57 | checkTime( 58 | () => expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 100, minWait: 50 }).promise).rejects.toThrow('failure'), 59 | 150, 60 | 400 61 | ) 62 | ) 63 | } 64 | await Promise.all(promises) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | function wait(ms: number): Promise { 2 | return new Promise(resolve => setTimeout(resolve, ms)) 3 | } 4 | 5 | function waitRandom(min: number, max: number): Promise { 6 | return wait(min + Math.round(Math.random() * Math.max(0, max - min))) 7 | } 8 | 9 | /** 10 | * This error is thrown if the function is cancelled before completing 11 | */ 12 | export class CancelledError extends Error { 13 | __proto__: Error 14 | constructor() { 15 | const trueProto = new.target.prototype 16 | super('Cancelled') 17 | 18 | this.__proto__ = trueProto 19 | } 20 | } 21 | 22 | /** 23 | * Throw this error if the function should retry 24 | */ 25 | export class RetryableError extends Error { 26 | __proto__: Error 27 | constructor(message?: string) { 28 | const trueProto = new.target.prototype 29 | super(message) 30 | 31 | this.__proto__ = trueProto 32 | } 33 | } 34 | 35 | /** 36 | * Retries the function that returns the promise until the promise successfully resolves up to n retries 37 | * @param fn function to retry 38 | * @param n how many times to retry 39 | * @param minWait min wait between retries in ms 40 | * @param maxWait max wait between retries in ms 41 | */ 42 | export function retry( 43 | fn: () => Promise, 44 | { n, minWait, maxWait }: { n: number; minWait: number; maxWait: number } 45 | ): { promise: Promise; cancel: () => void } { 46 | let completed = false 47 | let rejectCancelled: (error: Error) => void 48 | const promise = new Promise(async (resolve, reject) => { 49 | rejectCancelled = reject 50 | while (true) { 51 | let result: T 52 | try { 53 | result = await fn() 54 | if (!completed) { 55 | resolve(result) 56 | completed = true 57 | } 58 | break 59 | } catch (error) { 60 | if (completed) { 61 | break 62 | } 63 | if (n <= 0 || !(error instanceof RetryableError)) { 64 | reject(error) 65 | completed = true 66 | break 67 | } 68 | n-- 69 | } 70 | await waitRandom(minWait, maxWait) 71 | } 72 | }) 73 | return { 74 | promise, 75 | cancel: () => { 76 | if (completed) return 77 | completed = true 78 | rejectCancelled(new CancelledError()) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/uriToHttp.test.ts: -------------------------------------------------------------------------------- 1 | import uriToHttp from './uriToHttp' 2 | 3 | describe('uriToHttp', () => { 4 | it('returns .eth.link for ens names', () => { 5 | expect(uriToHttp('t2crtokens.eth')).toEqual([]) 6 | }) 7 | it('returns https first for http', () => { 8 | expect(uriToHttp('http://test.com')).toEqual(['https://test.com', 'http://test.com']) 9 | }) 10 | it('returns https for https', () => { 11 | expect(uriToHttp('https://test.com')).toEqual(['https://test.com']) 12 | }) 13 | it('returns ipfs gateways for ipfs:// urls', () => { 14 | expect(uriToHttp('ipfs://QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ')).toEqual([ 15 | 'https://cloudflare-ipfs.com/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/', 16 | 'https://ipfs.io/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/' 17 | ]) 18 | }) 19 | it('returns empty array for invalid scheme', () => { 20 | expect(uriToHttp('blah:test')).toEqual([]) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/utils/uriToHttp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a URI that may be ipfs, ipns, http, or https protocol, return the fetch-able http(s) URLs for the same content 3 | * @param uri to convert to fetch-able http url 4 | */ 5 | export default function uriToHttp(uri: string): string[] { 6 | const protocol = uri.split(':')[0].toLowerCase() 7 | switch (protocol) { 8 | case 'https': 9 | return [uri] 10 | case 'http': 11 | return ['https' + uri.substr(4), uri] 12 | case 'ipfs': 13 | const hash = uri.match(/^ipfs:(\/\/)?(.*)$/i)?.[2] 14 | return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`] 15 | case 'ipns': 16 | const name = uri.match(/^ipns:(\/\/)?(.*)$/i)?.[2] 17 | return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`] 18 | default: 19 | return [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/useDebouncedChangeHandler.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react' 2 | 3 | /** 4 | * Easy way to debounce the handling of a rapidly changing value, e.g. a changing slider input 5 | * @param value value that is rapidly changing 6 | * @param onChange change handler that should receive the debounced updates to the value 7 | * @param debouncedMs how long we should wait for changes to be applied 8 | */ 9 | export default function useDebouncedChangeHandler( 10 | value: T, 11 | onChange: (newValue: T) => void, 12 | debouncedMs = 100 13 | ): [T, (value: T) => void] { 14 | const [inner, setInner] = useState(() => value) 15 | const timer = useRef>() 16 | 17 | const onChangeInner = useCallback( 18 | (newValue: T) => { 19 | setInner(newValue) 20 | if (timer.current) { 21 | clearTimeout(timer.current) 22 | } 23 | timer.current = setTimeout(() => { 24 | onChange(newValue) 25 | timer.current = undefined 26 | }, debouncedMs) 27 | }, 28 | [debouncedMs, onChange] 29 | ) 30 | 31 | useEffect(() => { 32 | if (timer.current) { 33 | clearTimeout(timer.current) 34 | timer.current = undefined 35 | } 36 | setInner(value) 37 | }, [value]) 38 | 39 | return [inner, onChangeInner] 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/wrappedCurrency.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Currency, CurrencyAmount, ETHER, Token, TokenAmount, WETH } from 'swap-sdk' 2 | 3 | export function wrappedCurrency(currency: Currency | undefined, chainId: ChainId | undefined): Token | undefined { 4 | return chainId && currency === ETHER ? WETH[chainId] : currency instanceof Token ? currency : undefined 5 | } 6 | 7 | export function wrappedCurrencyAmount( 8 | currencyAmount: CurrencyAmount | undefined, 9 | chainId: ChainId | undefined 10 | ): TokenAmount | undefined { 11 | const token = currencyAmount && chainId ? wrappedCurrency(currencyAmount.currency, chainId) : undefined 12 | return token && currencyAmount ? new TokenAmount(token, currencyAmount.raw) : undefined 13 | } 14 | 15 | export function unwrappedToken(token: Token): Currency { 16 | if (token.equals(WETH[token.chainId])) return ETHER 17 | return token 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "strict": true, 16 | "alwaysStrict": true, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "noImplicitReturns": true, 23 | "moduleResolution": "node", 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "jsx": "preserve", 27 | "downlevelIteration": true, 28 | "allowSyntheticDefaultImports": true, 29 | "suppressImplicitAnyIndexErrors": true, 30 | "types": [ 31 | "react-spring", 32 | "jest" 33 | ] 34 | }, 35 | "exclude": [ 36 | "node_modules", 37 | "cypress" 38 | ], 39 | "include": [ 40 | "./src/**/*.ts", 41 | "./src/**/*.tsx" 42 | ] 43 | } 44 | --------------------------------------------------------------------------------