├── .env ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md └── workflows │ └── semgrep.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .yarnrc ├── LICENSE ├── README.md ├── cypress.json ├── package.json ├── public ├── 451.html ├── favicon.png ├── images │ ├── 192x192_App_Icon.png │ └── 512x512_App_Icon.png ├── index.html ├── locales │ ├── de.json │ ├── en.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.json ├── schema.json ├── src ├── apollo │ └── client.ts ├── assets │ ├── images │ │ ├── arbitrum.svg │ │ ├── arrow-down-blue.svg │ │ ├── arrow-down-grey.svg │ │ ├── arrow-right-white.png │ │ ├── arrow-right.svg │ │ ├── avalanche-logo.png │ │ ├── base-logo.svg │ │ ├── big_unicorn.png │ │ ├── blue-loader.svg │ │ ├── bnb-logo.svg │ │ ├── celo-logo.svg │ │ ├── circle-grey.svg │ │ ├── circle.svg │ │ ├── cmc.png │ │ ├── coinbaseWalletIcon.svg │ │ ├── dropdown-blue.svg │ │ ├── dropdown.svg │ │ ├── dropup-blue.svg │ │ ├── ethereum-logo.png │ │ ├── link.svg │ │ ├── lm-card-bg.png │ │ ├── magnifying-glass.svg │ │ ├── menu.svg │ │ ├── metamask.png │ │ ├── noise.png │ │ ├── optimism.png │ │ ├── optimism.svg │ │ ├── plus-blue.svg │ │ ├── plus-grey.svg │ │ ├── polygon-logo.png │ │ ├── question-mark.svg │ │ ├── question.svg │ │ ├── spinner.svg │ │ ├── squiggle.png │ │ ├── token-list-logo.png │ │ ├── token-list │ │ │ ├── lists-dark.png │ │ │ └── lists-light.png │ │ ├── token-logo.png │ │ ├── tokenlistsgrouped.png │ │ ├── trustWallet.png │ │ ├── walletConnectIcon.svg │ │ ├── whitev3.png │ │ ├── whitev3.svg │ │ ├── x.svg │ │ └── xl_uni.png │ ├── mp3 │ │ └── uni.mp3 │ └── svg │ │ ├── QR.svg │ │ ├── lightcircle.svg │ │ ├── logo.svg │ │ ├── logo_pink.svg │ │ ├── logo_white.svg │ │ ├── optimism-plain.svg │ │ ├── tokenlist.svg │ │ ├── wordmark.svg │ │ ├── wordmark_pink.svg │ │ └── wordmark_white.svg ├── components │ ├── BarChart │ │ ├── alt.tsx │ │ └── index.tsx │ ├── Button │ │ └── index.tsx │ ├── CandleChart │ │ └── index.tsx │ ├── Card │ │ └── index.tsx │ ├── Column │ │ └── index.tsx │ ├── Confetti │ │ └── index.tsx │ ├── CurrencyLogo │ │ └── index.tsx │ ├── DensityChart │ │ ├── CurrentPriceLabel.tsx │ │ ├── CustomToolTip.tsx │ │ └── index.tsx │ ├── DoubleLogo │ │ └── index.tsx │ ├── FormattedCurrencyAmount │ │ └── index.tsx │ ├── Header │ │ ├── Polling.tsx │ │ ├── TopBar.tsx │ │ ├── URLWarning.tsx │ │ └── index.tsx │ ├── HoverInlineText │ │ └── index.tsx │ ├── LineChart │ │ ├── alt.tsx │ │ └── index.tsx │ ├── ListLogo │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── Logo │ │ └── index.tsx │ ├── Menu │ │ ├── NetworkDropdown.tsx │ │ └── index.tsx │ ├── Modal │ │ └── index.tsx │ ├── NumericalInput │ │ └── index.tsx │ ├── Percent │ │ └── index.tsx │ ├── Popover │ │ └── index.tsx │ ├── Popups │ │ ├── ListUpdatePopup.tsx │ │ ├── PopupItem.tsx │ │ └── index.tsx │ ├── QuestionHelper │ │ └── index.tsx │ ├── Row │ │ └── index.tsx │ ├── Search │ │ └── index.tsx │ ├── Text │ │ └── index.ts │ ├── Toggle │ │ ├── ListToggle.tsx │ │ ├── MultiToggle.tsx │ │ └── index.tsx │ ├── Tooltip │ │ └── index.tsx │ ├── TransactionsTable │ │ └── index.tsx │ ├── pools │ │ ├── PoolTable.tsx │ │ └── TopPoolMovers.tsx │ ├── shared │ │ └── index.tsx │ └── tokens │ │ ├── TokenTable.tsx │ │ └── TopTokenMovers.tsx ├── constants │ ├── abis │ │ ├── argent-wallet-detector.json │ │ ├── argent-wallet-detector.ts │ │ ├── ens-public-resolver.json │ │ ├── ens-registrar.json │ │ ├── erc20.json │ │ ├── erc20.ts │ │ ├── erc20_bytes32.json │ │ ├── migrator.json │ │ ├── migrator.ts │ │ ├── staking-rewards.ts │ │ ├── unisocks.json │ │ └── weth.json │ ├── chains.ts │ ├── index.ts │ ├── intervals.ts │ ├── lists.ts │ ├── multicall │ │ ├── abi.json │ │ └── index.ts │ ├── networks.ts │ └── tokenLists │ │ └── uniswap-v2-unsupported.tokenlist.json ├── data │ ├── application │ │ └── index.ts │ ├── combined │ │ └── pools.ts │ ├── pools │ │ ├── chartData.ts │ │ ├── poolData.ts │ │ ├── tickData.ts │ │ ├── topPools.ts │ │ └── transactions.ts │ ├── protocol │ │ ├── chart.ts │ │ ├── derived.ts │ │ ├── overview.ts │ │ └── transactions.ts │ ├── search │ │ └── index.ts │ └── tokens │ │ ├── chartData.ts │ │ ├── poolsForToken.ts │ │ ├── priceData.ts │ │ ├── tokenData.ts │ │ ├── topTokens.ts │ │ └── transactions.ts ├── hooks │ ├── chart.ts │ ├── useAppDispatch.ts │ ├── useBlocksFromTimestamps.ts │ ├── useCMCLink.ts │ ├── useColor.ts │ ├── useCopyClipboard.ts │ ├── useDebounce.ts │ ├── useEthPrices.ts │ ├── useFetchListCallback.ts │ ├── useHttpLocations.ts │ ├── useInterval.ts │ ├── useIsWindowVisible.ts │ ├── useLast.ts │ ├── useOnClickOutside.tsx │ ├── useParsedQueryString.ts │ ├── usePrevious.ts │ ├── useTheme.ts │ ├── useToggle.ts │ ├── useToggledVersion.ts │ └── useWindowSize.ts ├── i18n.ts ├── index.tsx ├── pages │ ├── App.tsx │ ├── Home │ │ └── index.tsx │ ├── Pool │ │ ├── PoolPage.tsx │ │ └── PoolsOverview.tsx │ ├── Protocol │ │ └── index.tsx │ ├── Token │ │ ├── TokenPage.tsx │ │ ├── TokensOverview.tsx │ │ └── redirects.tsx │ ├── Wallets │ │ └── index.tsx │ └── styled.ts ├── react-app-env.d.ts ├── state │ ├── application │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── global │ │ └── actions.ts │ ├── index.ts │ ├── lists │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ ├── updater.ts │ │ └── wrappedTokenInfo.ts │ ├── pools │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── protocol │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── tokens │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.ts │ │ └── updater.ts │ └── user │ │ ├── actions.ts │ │ ├── hooks.tsx │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.tsx ├── theme │ ├── DarkModeQueryParamReader.tsx │ ├── components.tsx │ ├── index.tsx │ ├── rebass.d.ts │ └── styled.d.ts ├── types │ └── index.ts └── utils │ ├── chunkArray.test.ts │ ├── chunkArray.ts │ ├── contenthashToUri.test.skip.ts │ ├── contenthashToUri.ts │ ├── currencyId.ts │ ├── data.ts │ ├── date.ts │ ├── getLibrary.ts │ ├── getTokenList.ts │ ├── index.ts │ ├── isZero.ts │ ├── listSort.ts │ ├── listVersionLabel.ts │ ├── networkPrefix.ts │ ├── numbers.ts │ ├── parseENSAddress.test.ts │ ├── parseENSAddress.ts │ ├── queries.ts │ ├── resolveENSContentHash.ts │ ├── retry.test.ts │ ├── retry.ts │ ├── tokens.ts │ ├── uriToHttp.test.ts │ ├── uriToHttp.ts │ └── useDebouncedChangeHandler.tsx ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_CHAIN_ID="1" 2 | REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847" 3 | REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | // Allows for the parsing of JSX 8 | "jsx": true 9 | } 10 | }, 11 | "ignorePatterns": ["node_modules/**/*"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "extends": [ 18 | "plugin:react/recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react-hooks/recommended", 21 | "plugin:prettier/recommended" 22 | ], 23 | "rules": { 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "prettier/prettier": "error", 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "@typescript-eslint/ban-ts-comment": "off", 28 | "@typescript-eslint/ban-ts-ignore": "off" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Describe an issue in the Uniswap Interface 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Bug Description** 10 | A clear and concise description of the bug. 11 | 12 | **Steps to Reproduce** 13 | 14 | 1. Go to ... 15 | 2. Click on ... 16 | ... 17 | 18 | **Expected Behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Additional Context** 22 | Add any other context about the problem here (screenshots, whether the bug only occurs only in certain mobile/desktop/browser environments, etc.) 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support 4 | url: https://discord.gg/FCfyBSbCU5 5 | about: Please ask and answer questions here 6 | - name: List a token 7 | url: https://github.com/Uniswap/default-token-list#adding-a-token 8 | about: Any requests to add a token to Uniswap should go here 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for improving the UX of the Uniswap Interface 4 | title: '' 5 | labels: 'improvement' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | on: 3 | workflow_dispatch: {} 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 11 | - cron: '35 11 * * *' 12 | jobs: 13 | semgrep: 14 | name: semgrep/ci 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | container: 19 | image: returntocorp/semgrep 20 | if: (github.actor != 'dependabot[bot]') 21 | steps: 22 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 23 | - run: semgrep ci 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Uniswap Info V3 2 | 3 | An open sourced interface for Uniswap V3 analytics. 4 | 5 | Info URL: https://info.uniswap.org/#/ 6 | 7 | ## Development 8 | 9 | ### Install Dependencies 10 | 11 | ```bash 12 | yarn 13 | ``` 14 | 15 | ### Run 16 | 17 | ```bash 18 | yarn start 19 | ``` 20 | 21 | ## Contributions 22 | 23 | **Please open all pull requests against the `master` branch.** 24 | CI checks will run against all PRs. 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/451.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unavailable For Legal Reasons 6 | 7 | 8 |

Unavailable For Legal Reasons

9 | 10 | 11 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/public/favicon.png -------------------------------------------------------------------------------- /public/images/192x192_App_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/public/images/192x192_App_Icon.png -------------------------------------------------------------------------------- /public/images/512x512_App_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/public/images/512x512_App_Icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | 27 | Uniswap Info 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/locales/iw.json: -------------------------------------------------------------------------------- 1 | { 2 | "noWallet": "לא נמצא ארנק", 3 | "wrongNetwork": "נבחרה רשת לא נכונה", 4 | "switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל", 5 | "installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה", 6 | "installMetamask": " 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": "ושל האית'ר הינן בערך שווה {{ 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 | -------------------------------------------------------------------------------- /public/locales/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 | -------------------------------------------------------------------------------- /public/locales/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 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Uniswap", 3 | "name": "Uniswap", 4 | "icons": [ 5 | { 6 | "src": "./images/192x192_App_Icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "./images/512x512_App_Icon.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/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/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/arrow-right-white.png -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/avalanche-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/avalanche-logo.png -------------------------------------------------------------------------------- /src/assets/images/base-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/big_unicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/big_unicorn.png -------------------------------------------------------------------------------- /src/assets/images/blue-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/bnb-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/images/celo-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/images/circle-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/cmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/cmc.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/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/ethereum-logo.png -------------------------------------------------------------------------------- /src/assets/images/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/lm-card-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/lm-card-bg.png -------------------------------------------------------------------------------- /src/assets/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/metamask.png -------------------------------------------------------------------------------- /src/assets/images/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/noise.png -------------------------------------------------------------------------------- /src/assets/images/optimism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/optimism.png -------------------------------------------------------------------------------- /src/assets/images/optimism.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/plus-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/plus-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/polygon-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/polygon-logo.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/squiggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/squiggle.png -------------------------------------------------------------------------------- /src/assets/images/token-list-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/token-list-logo.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/token-list/lists-dark.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/token-list/lists-light.png -------------------------------------------------------------------------------- /src/assets/images/token-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/token-logo.png -------------------------------------------------------------------------------- /src/assets/images/tokenlistsgrouped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/tokenlistsgrouped.png -------------------------------------------------------------------------------- /src/assets/images/trustWallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/trustWallet.png -------------------------------------------------------------------------------- /src/assets/images/whitev3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/whitev3.png -------------------------------------------------------------------------------- /src/assets/images/whitev3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/xl_uni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/images/xl_uni.png -------------------------------------------------------------------------------- /src/assets/mp3/uni.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uniswap/v3-info/43e5b31f1a8a2e47137dd7d08398be7043ccaf98/src/assets/mp3/uni.mp3 -------------------------------------------------------------------------------- /src/assets/svg/QR.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/lightcircle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/optimism-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Box } from 'rebass/styled-components' 3 | 4 | const Card = styled(Box)<{ 5 | width?: string 6 | padding?: string 7 | border?: string 8 | borderRadius?: string 9 | $minHeight?: number 10 | }>` 11 | width: ${({ width }) => width ?? '100%'}; 12 | border-radius: 16px; 13 | padding: 1rem; 14 | padding: ${({ padding }) => padding}; 15 | border: ${({ border }) => border}; 16 | border-radius: ${({ borderRadius }) => borderRadius}; 17 | min-height: ${({ $minHeight }) => `${$minHeight}px`}; 18 | ` 19 | export default Card 20 | 21 | export const LightCard = styled(Card)` 22 | border: 1px solid ${({ theme }) => theme.bg2}; 23 | background-color: ${({ theme }) => theme.bg1}; 24 | ` 25 | 26 | export const LightGreyCard = styled(Card)` 27 | background-color: ${({ theme }) => theme.bg3}; 28 | ` 29 | 30 | export const GreyCard = styled(Card)` 31 | background-color: ${({ theme }) => theme.bg2}; 32 | ` 33 | 34 | export const DarkGreyCard = styled(Card)` 35 | background-color: ${({ theme }) => theme.bg0}; 36 | ` 37 | 38 | export const OutlineCard = styled(Card)` 39 | border: 1px solid ${({ theme }) => theme.bg3}; 40 | ` 41 | 42 | export const YellowCard = styled(Card)` 43 | background-color: rgba(243, 132, 30, 0.05); 44 | color: ${({ theme }) => theme.yellow3}; 45 | font-weight: 500; 46 | ` 47 | 48 | export const PinkCard = styled(Card)` 49 | background-color: rgba(255, 0, 122, 0.03); 50 | color: ${({ theme }) => theme.primary1}; 51 | font-weight: 500; 52 | ` 53 | 54 | export const BlueCard = styled(Card)` 55 | background-color: ${({ theme }) => theme.primary5}; 56 | color: ${({ theme }) => theme.blue2}; 57 | border-radius: 12px; 58 | width: fit-content; 59 | ` 60 | 61 | export const ScrollableX = styled.div` 62 | display: flex; 63 | flex-direction: row; 64 | width: 100%; 65 | overflow-x: auto; 66 | overflow-y: hidden; 67 | white-space: nowrap; 68 | 69 | ::-webkit-scrollbar { 70 | display: none; 71 | } 72 | ` 73 | 74 | export const GreyBadge = styled(Card)` 75 | width: fit-content; 76 | border-radius: 8px; 77 | background: ${({ theme }) => theme.bg3}; 78 | color: ${({ theme }) => theme.text1}; 79 | padding: 4px 6px; 80 | font-weight: 400; 81 | ` 82 | -------------------------------------------------------------------------------- /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 }) => 20 | ($gap === 'sm' && '8px') || ($gap === 'md' && '12px') || ($gap === 'lg' && '24px') || $gap}; 21 | justify-items: ${({ justify }) => justify && justify}; 22 | ` 23 | 24 | export default Column 25 | -------------------------------------------------------------------------------- /src/components/Confetti/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactConfetti from 'react-confetti' 3 | import { useWindowSize } from '../../hooks/useWindowSize' 4 | 5 | // eslint-disable-next-line react/prop-types 6 | export default function Confetti({ start, variant }: { start: boolean; variant?: string }) { 7 | const { width, height } = useWindowSize() 8 | 9 | const _variant = variant ? variant : height && width && height > 1.5 * width ? 'bottom' : variant 10 | 11 | return start && width && height ? ( 12 | 31 | ) : null 32 | } 33 | -------------------------------------------------------------------------------- /src/components/DensityChart/CurrentPriceLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChartEntry } from './index' 3 | import { PoolData } from 'state/pools/reducer' 4 | import useTheme from 'hooks/useTheme' 5 | import styled from 'styled-components' 6 | import { AutoColumn } from 'components/Column' 7 | import { RowFixed } from 'components/Row' 8 | import { TYPE } from 'theme' 9 | 10 | const Wrapper = styled.div` 11 | border-radius: 8px; 12 | padding: 6px 12px; 13 | color: white; 14 | width: fit-content; 15 | font-size: 14px; 16 | background-color: ${({ theme }) => theme.bg2}; 17 | ` 18 | 19 | interface LabelProps { 20 | x: number 21 | y: number 22 | index: number 23 | } 24 | 25 | interface CurrentPriceLabelProps { 26 | data: ChartEntry[] | undefined 27 | chartProps: any 28 | poolData: PoolData 29 | } 30 | 31 | export function CurrentPriceLabel({ data, chartProps, poolData }: CurrentPriceLabelProps) { 32 | const theme = useTheme() 33 | const labelData = chartProps as LabelProps 34 | const entryData = data?.[labelData.index] 35 | if (entryData?.isCurrent) { 36 | const price0 = entryData.price0 37 | const price1 = entryData.price1 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | Current Price 45 |
54 |
55 | {`1 ${poolData.token0.symbol} = ${Number(price0).toLocaleString(undefined, { 56 | minimumSignificantDigits: 1, 57 | })} ${poolData.token1.symbol}`} 58 | {`1 ${poolData.token1.symbol} = ${Number(price1).toLocaleString(undefined, { 59 | minimumSignificantDigits: 1, 60 | })} ${poolData.token0.symbol}`} 61 |
62 |
63 |
64 |
65 | ) 66 | } 67 | return null 68 | } 69 | -------------------------------------------------------------------------------- /src/components/DensityChart/CustomToolTip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PoolData } from 'state/pools/reducer' 3 | import styled from 'styled-components' 4 | import { LightCard } from 'components/Card' 5 | import useTheme from 'hooks/useTheme' 6 | import { AutoColumn } from 'components/Column' 7 | import { TYPE } from 'theme' 8 | import { RowBetween } from 'components/Row' 9 | import { formatAmount } from 'utils/numbers' 10 | 11 | const TooltipWrapper = styled(LightCard)` 12 | padding: 12px; 13 | width: 320px; 14 | opacity: 0.6; 15 | font-size: 12px; 16 | z-index: 10; 17 | ` 18 | 19 | interface CustomToolTipProps { 20 | chartProps: any 21 | poolData: PoolData 22 | currentPrice: number | undefined 23 | } 24 | 25 | export function CustomToolTip({ chartProps, poolData, currentPrice }: CustomToolTipProps) { 26 | const theme = useTheme() 27 | const price0 = chartProps?.payload?.[0]?.payload.price0 28 | const price1 = chartProps?.payload?.[0]?.payload.price1 29 | const tvlToken0 = chartProps?.payload?.[0]?.payload.tvlToken0 30 | const tvlToken1 = chartProps?.payload?.[0]?.payload.tvlToken1 31 | 32 | return ( 33 | 34 | 35 | Tick stats 36 | 37 | {poolData?.token0?.symbol} Price: 38 | 39 | {price0 40 | ? Number(price0).toLocaleString(undefined, { 41 | minimumSignificantDigits: 1, 42 | }) 43 | : ''}{' '} 44 | {poolData?.token1?.symbol} 45 | 46 | 47 | 48 | {poolData?.token1?.symbol} Price: 49 | 50 | {price1 51 | ? Number(price1).toLocaleString(undefined, { 52 | minimumSignificantDigits: 1, 53 | }) 54 | : ''}{' '} 55 | {poolData?.token0?.symbol} 56 | 57 | 58 | {currentPrice && price0 && currentPrice > price1 ? ( 59 | 60 | {poolData?.token0?.symbol} Locked: 61 | 62 | {tvlToken0 ? formatAmount(tvlToken0) : ''} {poolData?.token0?.symbol} 63 | 64 | 65 | ) : ( 66 | 67 | {poolData?.token1?.symbol} Locked: 68 | 69 | {tvlToken1 ? formatAmount(tvlToken1) : ''} {poolData?.token1?.symbol} 70 | 71 | 72 | )} 73 | 74 | 75 | ) 76 | } 77 | 78 | export default CustomToolTip 79 | -------------------------------------------------------------------------------- /src/components/DoubleLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import CurrencyLogo from '../CurrencyLogo' 4 | 5 | const Wrapper = styled.div<{ $margin: boolean; $sizeraw: number }>` 6 | position: relative; 7 | display: flex; 8 | flex-direction: row; 9 | margin-right: ${({ $sizeraw, $margin }) => $margin && ($sizeraw / 3 + 8).toString() + 'px'}; 10 | ` 11 | 12 | interface DoubleCurrencyLogoProps { 13 | margin?: boolean 14 | size?: number 15 | address0?: string 16 | address1?: string 17 | } 18 | 19 | const HigherLogo = styled(CurrencyLogo)` 20 | z-index: 2; 21 | ` 22 | 23 | export default function DoubleCurrencyLogo({ address0, address1, size = 16, margin = false }: DoubleCurrencyLogoProps) { 24 | return ( 25 | 26 | {address0 && } 27 | {address1 && } 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/FormattedCurrencyAmount/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core' 3 | import JSBI from 'jsbi' 4 | 5 | const CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000)) 6 | 7 | export default function FormattedCurrencyAmount({ 8 | currencyAmount, 9 | significantDigits = 4, 10 | }: { 11 | currencyAmount: CurrencyAmount 12 | significantDigits?: number 13 | }) { 14 | return ( 15 | <> 16 | {currencyAmount.equalTo(JSBI.BigInt(0)) 17 | ? '0' 18 | : currencyAmount.greaterThan(CURRENCY_AMOUNT_MIN) 19 | ? currencyAmount.toSignificant(significantDigits) 20 | : `<${CURRENCY_AMOUNT_MIN.toSignificant(1)}`} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Header/Polling.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled, { keyframes } from 'styled-components' 3 | import { TYPE, ExternalLink } from '../../theme' 4 | 5 | import { useActiveNetworkVersion, useSubgraphStatus } from '../../state/application/hooks' 6 | import { ExplorerDataType, getExplorerLink } from '../../utils' 7 | import useTheme from 'hooks/useTheme' 8 | import { EthereumNetworkInfo } from 'constants/networks' 9 | import { ChainId } from '@uniswap/sdk-core' 10 | 11 | const StyledPolling = styled.div` 12 | display: flex; 13 | color: white; 14 | margin-right: 1rem; 15 | border-radius: 4px; 16 | width: 192px; 17 | padding: 4px; 18 | background-color: ${({ theme }) => theme.bg2}; 19 | transition: opacity 0.25s ease; 20 | color: ${({ theme }) => theme.green1}; 21 | :hover { 22 | opacity: 1; 23 | } 24 | z-index: 9999; 25 | 26 | ${({ theme }) => theme.mediaWidth.upToMedium` 27 | display: none; 28 | `} 29 | ` 30 | const StyledPollingDot = styled.div` 31 | width: 8px; 32 | height: 8px; 33 | min-height: 8px; 34 | min-width: 8px; 35 | margin-left: 0.4rem; 36 | margin-top: 3px; 37 | border-radius: 50%; 38 | position: relative; 39 | background-color: ${({ theme }) => theme.green1}; 40 | ` 41 | 42 | const rotate360 = keyframes` 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | ` 50 | 51 | const Spinner = styled.div` 52 | animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite; 53 | transform: translateZ(0); 54 | border-top: 1px solid transparent; 55 | border-right: 1px solid transparent; 56 | border-bottom: 1px solid transparent; 57 | border-left: 2px solid ${({ theme }) => theme.green1}; 58 | background: transparent; 59 | width: 14px; 60 | height: 14px; 61 | border-radius: 50%; 62 | position: relative; 63 | left: -3px; 64 | top: -3px; 65 | ` 66 | 67 | export default function Polling() { 68 | const theme = useTheme() 69 | const [activeNetwork] = useActiveNetworkVersion() 70 | const [status] = useSubgraphStatus() 71 | const [isMounted, setIsMounted] = useState(true) 72 | const latestBlock = activeNetwork === EthereumNetworkInfo ? status.headBlock : status.syncedBlock 73 | 74 | useEffect( 75 | () => { 76 | const timer1 = setTimeout(() => setIsMounted(true), 1000) 77 | 78 | // this will clear Timeout when component unmount like in willComponentUnmount 79 | return () => { 80 | setIsMounted(false) 81 | clearTimeout(timer1) 82 | } 83 | }, 84 | [status], //useEffect will run only one time 85 | //if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run) 86 | ) 87 | 88 | return ( 89 | 92 | 93 | 94 | Latest synced block:{' '} 95 | 96 | {latestBlock} 97 | {!isMounted && } 98 | 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Header/TopBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { AutoRow, RowBetween, RowFixed } from 'components/Row' 4 | import { ExternalLink, TYPE } from 'theme' 5 | import { useEthPrices } from 'hooks/useEthPrices' 6 | import { formatDollarAmount } from 'utils/numbers' 7 | import Polling from './Polling' 8 | import { useActiveNetworkVersion } from '../../state/application/hooks' 9 | import { SupportedNetwork } from '../../constants/networks' 10 | 11 | const Wrapper = styled.div` 12 | width: 100%; 13 | background-color: ${({ theme }) => theme.black}; 14 | padding: 10px 20px; 15 | ` 16 | 17 | const Item = styled(TYPE.main)` 18 | font-size: 12px; 19 | ` 20 | 21 | const StyledLink = styled(ExternalLink)` 22 | font-size: 12px; 23 | color: ${({ theme }) => theme.text1}; 24 | ` 25 | 26 | const TopBar = () => { 27 | const ethPrices = useEthPrices() 28 | const [activeNetwork] = useActiveNetworkVersion() 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | {activeNetwork.id === SupportedNetwork.CELO ? ( 36 | Celo Price: 37 | ) : activeNetwork.id === SupportedNetwork.BNB ? ( 38 | BNB Price: 39 | ) : activeNetwork.id === SupportedNetwork.AVALANCHE ? ( 40 | AVAX Price: 41 | ) : ( 42 | Eth Price: 43 | )} 44 | 45 | {formatDollarAmount(ethPrices?.current)} 46 | 47 | 48 | 49 | 50 | V2 Analytics 51 | Docs 52 | App 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export default TopBar 60 | -------------------------------------------------------------------------------- /src/components/Header/URLWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { AlertTriangle, X } from 'react-feather' 5 | import { useURLWarningToggle, useURLWarningVisible } from '../../state/user/hooks' 6 | import { isMobile } from 'react-device-detect' 7 | 8 | const PhishAlert = styled.div<{ isActive: any }>` 9 | width: 100%; 10 | padding: 6px 6px; 11 | background-color: ${({ theme }) => theme.blue1}; 12 | color: white; 13 | font-size: 11px; 14 | justify-content: space-between; 15 | align-items: center; 16 | display: ${({ isActive }) => (isActive ? 'flex' : 'none')}; 17 | ` 18 | 19 | export const StyledClose = styled(X)` 20 | :hover { 21 | cursor: pointer; 22 | } 23 | ` 24 | 25 | export default function URLWarning() { 26 | const toggleURLWarning = useURLWarningToggle() 27 | const showURLWarning = useURLWarningVisible() 28 | 29 | return isMobile ? ( 30 | 31 |
32 | Make sure the URL is 33 | app.uniswap.org 34 |
35 | 36 |
37 | ) : window.location.hostname === 'app.uniswap.org' ? ( 38 | 39 |
40 | Always make sure the URL is 41 | app.uniswap.org - bookmark it 42 | to be safe. 43 |
44 | 45 |
46 | ) : null 47 | } 48 | -------------------------------------------------------------------------------- /src/components/HoverInlineText/index.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from 'components/Tooltip' 2 | import React, { useState } from 'react' 3 | import styled from 'styled-components' 4 | 5 | const TextWrapper = styled.div<{ 6 | $margin: boolean 7 | $link: boolean 8 | color?: string 9 | fontSize?: string 10 | $adjustSize?: boolean 11 | }>` 12 | position: relative; 13 | margin-left: ${({ $margin }) => $margin && '4px'}; 14 | color: ${({ theme, $link, color }) => ($link ? theme.blue1 : color ?? theme.text1)}; 15 | font-size: ${({ fontSize }) => fontSize ?? 'inherit'}; 16 | 17 | :hover { 18 | cursor: pointer; 19 | } 20 | 21 | @media screen and (max-width: 600px) { 22 | font-size: ${({ $adjustSize }) => $adjustSize && '12px'}; 23 | } 24 | ` 25 | 26 | const HoverInlineText = ({ 27 | text, 28 | maxCharacters = 20, 29 | margin = false, 30 | adjustSize = false, 31 | fontSize, 32 | color, 33 | link, 34 | ...rest 35 | }: { 36 | text: string 37 | maxCharacters?: number 38 | margin?: boolean 39 | adjustSize?: boolean 40 | fontSize?: string 41 | color?: string 42 | link?: boolean 43 | }) => { 44 | const [showHover, setShowHover] = useState(false) 45 | 46 | if (!text) { 47 | return 48 | } 49 | 50 | if (text.length > maxCharacters) { 51 | return ( 52 | 53 | setShowHover(true)} 55 | onMouseLeave={() => setShowHover(false)} 56 | $margin={margin} 57 | $adjustSize={adjustSize} 58 | $link={!!link} 59 | color={color} 60 | fontSize={fontSize} 61 | {...rest} 62 | > 63 | {' ' + text.slice(0, maxCharacters - 1) + '...'} 64 | 65 | 66 | ) 67 | } 68 | 69 | return ( 70 | 71 | {text} 72 | 73 | ) 74 | } 75 | 76 | export default HoverInlineText 77 | -------------------------------------------------------------------------------- /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 | import v3 from '../../assets/images/whitev3.svg' 3 | import styled, { keyframes, css } 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({ 28 | size = '16px', 29 | stroke, 30 | ...rest 31 | }: { 32 | size?: string 33 | stroke?: string 34 | [k: string]: any 35 | }) { 36 | return ( 37 | 38 | 44 | 45 | ) 46 | } 47 | 48 | const pulse = keyframes` 49 | 0% { transform: scale(1); } 50 | 60% { transform: scale(1.1); } 51 | 100% { transform: scale(1); } 52 | ` 53 | 54 | const Wrapper = styled.div<{ fill: number; height?: string }>` 55 | pointer-events: none; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | background-color: ${({ theme, fill }) => (fill ? 'black' : theme.bg0)}; 60 | height: 100%; 61 | width: 100%; 62 | ${(props) => 63 | props.fill && !props.height 64 | ? css` 65 | height: 100vh; 66 | ` 67 | : css` 68 | height: 180px; 69 | `} 70 | ` 71 | 72 | const AnimatedImg = styled.div` 73 | animation: ${pulse} 800ms linear infinite; 74 | & > * { 75 | width: 72px; 76 | } 77 | ` 78 | 79 | export const LocalLoader = ({ fill }: { fill: boolean }) => { 80 | return ( 81 | 82 | 83 | loading-icon 84 | 85 | 86 | ) 87 | } 88 | 89 | const loadingAnimation = keyframes` 90 | 0% { 91 | background-position: 100% 50%; 92 | } 93 | 100% { 94 | background-position: 0% 50%; 95 | } 96 | ` 97 | 98 | export const LoadingRows = styled.div` 99 | display: grid; 100 | min-width: 75%; 101 | max-width: 100%; 102 | grid-column-gap: 0.5em; 103 | grid-row-gap: 0.8em; 104 | grid-template-columns: repeat(1, 1fr); 105 | & > div { 106 | animation: ${loadingAnimation} 1.5s infinite; 107 | animation-fill-mode: both; 108 | background: linear-gradient( 109 | to left, 110 | ${({ theme }) => theme.bg1} 25%, 111 | ${({ theme }) => theme.bg2} 50%, 112 | ${({ theme }) => theme.bg1} 75% 113 | ); 114 | background-size: 400%; 115 | border-radius: 12px; 116 | height: 2.4em; 117 | will-change: background-position; 118 | } 119 | & > div:nth-child(4n + 1) { 120 | grid-column: 1 / 3; 121 | } 122 | & > div:nth-child(4n) { 123 | grid-column: 3 / 4; 124 | margin-bottom: 2em; 125 | } 126 | ` 127 | -------------------------------------------------------------------------------- /src/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { HelpCircle } from 'react-feather' 3 | import { ImageProps } from 'rebass' 4 | import styled from 'styled-components' 5 | 6 | const BAD_SRCS: { [tokenAddress: string]: true } = {} 7 | 8 | export interface LogoProps extends Pick { 9 | srcs: string[] 10 | } 11 | 12 | /** 13 | * Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert 14 | */ 15 | export default function Logo({ srcs, alt, ...rest }: LogoProps) { 16 | const [, refresh] = useState(0) 17 | 18 | const src: string | undefined = srcs.find((src) => !BAD_SRCS[src]) 19 | 20 | if (src) { 21 | return ( 22 | {alt} { 27 | if (src) BAD_SRCS[src] = true 28 | refresh((i) => i + 1) 29 | }} 30 | /> 31 | ) 32 | } 33 | 34 | return 35 | } 36 | 37 | export const GenericImageWrapper = styled.img<{ size?: string }>` 38 | width: ${({ size }) => size ?? '20px'}; 39 | height: ${({ size }) => size ?? '20px'}; 40 | ` 41 | -------------------------------------------------------------------------------- /src/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { BookOpen, Code, Info, MessageCircle } from 'react-feather' 3 | import styled from 'styled-components' 4 | import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg' 5 | import { useOnClickOutside } from '../../hooks/useOnClickOutside' 6 | 7 | import { ExternalLink } from '../../theme' 8 | 9 | const StyledMenuIcon = styled(MenuIcon)` 10 | path { 11 | stroke: ${({ theme }) => theme.text1}; 12 | } 13 | ` 14 | 15 | const StyledMenuButton = styled.button` 16 | width: 100%; 17 | height: 100%; 18 | border: none; 19 | background-color: transparent; 20 | margin: 0; 21 | padding: 0; 22 | height: 35px; 23 | background-color: ${({ theme }) => theme.bg3}; 24 | 25 | padding: 0.15rem 0.5rem; 26 | border-radius: 0.5rem; 27 | 28 | :hover, 29 | :focus { 30 | cursor: pointer; 31 | outline: none; 32 | background-color: ${({ theme }) => theme.bg4}; 33 | } 34 | 35 | svg { 36 | margin-top: 2px; 37 | } 38 | ` 39 | 40 | const StyledMenu = styled.div` 41 | margin-left: 0.5rem; 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | position: relative; 46 | border: none; 47 | text-align: left; 48 | ` 49 | 50 | const MenuFlyout = styled.span` 51 | min-width: 8.125rem; 52 | background-color: ${({ theme }) => theme.bg3}; 53 | box-shadow: 54 | 0px 0px 1px rgba(0, 0, 0, 0.01), 55 | 0px 4px 8px rgba(0, 0, 0, 0.04), 56 | 0px 16px 24px rgba(0, 0, 0, 0.04), 57 | 0px 24px 32px rgba(0, 0, 0, 0.01); 58 | border-radius: 12px; 59 | padding: 0.5rem; 60 | display: flex; 61 | flex-direction: column; 62 | font-size: 1rem; 63 | position: absolute; 64 | top: 2.6rem; 65 | right: 0rem; 66 | z-index: 1000; 67 | ` 68 | 69 | const MenuItem = styled(ExternalLink)` 70 | flex: 1; 71 | padding: 0.5rem 0.5rem; 72 | color: ${({ theme }) => theme.text2}; 73 | :hover { 74 | color: ${({ theme }) => theme.text1}; 75 | cursor: pointer; 76 | text-decoration: none; 77 | opacity: 0.6; 78 | } 79 | > svg { 80 | margin-right: 8px; 81 | } 82 | ` 83 | 84 | const CODE_LINK = 'https://github.com/Uniswap/uniswap-v3-info' 85 | 86 | export default function Menu() { 87 | const node = useRef(null) 88 | const [isOpen, setOpen] = useState(false) 89 | 90 | useOnClickOutside(node, isOpen ? () => setOpen(false) : undefined) 91 | 92 | return ( 93 | 94 | setOpen((open) => !open)}> 95 | 96 | 97 | 98 | {isOpen && ( 99 | 100 | 101 | 102 | About 103 | 104 | 105 | 106 | Docs 107 | 108 | 109 | 110 | Github 111 | 112 | 113 | 114 | Discord 115 | 116 | 117 | )} 118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/components/NumericalInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { escapeRegExp } from '../../utils' 4 | 5 | const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>` 6 | color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)}; 7 | width: 0; 8 | position: relative; 9 | font-weight: 500; 10 | outline: none; 11 | border: none; 12 | flex: 1 1 auto; 13 | background-color: ${({ theme }) => theme.bg1}; 14 | font-size: ${({ fontSize }) => fontSize ?? '24px'}; 15 | text-align: ${({ align }) => align && align}; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | padding: 0px; 20 | -webkit-appearance: textfield; 21 | 22 | ::-webkit-search-decoration { 23 | -webkit-appearance: none; 24 | } 25 | 26 | [type='number'] { 27 | -moz-appearance: textfield; 28 | } 29 | 30 | ::-webkit-outer-spin-button, 31 | ::-webkit-inner-spin-button { 32 | -webkit-appearance: none; 33 | } 34 | 35 | ::placeholder { 36 | color: ${({ theme }) => theme.text4}; 37 | } 38 | ` 39 | 40 | const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group 41 | 42 | export const Input = React.memo(function InnerInput({ 43 | value, 44 | onUserInput, 45 | placeholder, 46 | prependSymbol, 47 | ...rest 48 | }: { 49 | value: string | number 50 | onUserInput: (input: string) => void 51 | error?: boolean 52 | fontSize?: string 53 | align?: 'right' | 'left' 54 | prependSymbol?: string | undefined 55 | } & Omit, 'ref' | 'onChange' | 'as'>) { 56 | const enforcer = (nextUserInput: string) => { 57 | if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { 58 | onUserInput(nextUserInput) 59 | } 60 | } 61 | 62 | return ( 63 | { 67 | if (prependSymbol) { 68 | const value = event.target.value 69 | 70 | // cut off prepended symbol 71 | const formattedValue = value.toString().includes(prependSymbol) 72 | ? value.toString().slice(1, value.toString().length + 1) 73 | : value 74 | 75 | // replace commas with periods, because uniswap exclusively uses period as the decimal separator 76 | enforcer(formattedValue.replace(/,/g, '.')) 77 | } else { 78 | enforcer(event.target.value.replace(/,/g, '.')) 79 | } 80 | }} 81 | // universal input options 82 | inputMode="decimal" 83 | title="Token Amount" 84 | autoComplete="off" 85 | autoCorrect="off" 86 | // text-specific options 87 | type="text" 88 | pattern="^[0-9]*[.,]?[0-9]*$" 89 | placeholder={placeholder || '0.0'} 90 | minLength={1} 91 | maxLength={79} 92 | spellCheck="false" 93 | /> 94 | ) 95 | }) 96 | 97 | export default Input 98 | 99 | // const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group 100 | -------------------------------------------------------------------------------- /src/components/Percent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TYPE } from 'theme' 3 | import styled from 'styled-components' 4 | 5 | const Wrapper = styled(TYPE.main)<{ fontWeight: number; fontSize: string; negative: boolean; neutral: boolean }>` 6 | font-size: ${({ fontSize }) => fontSize}; 7 | font-weight: ${({ fontWeight }) => fontWeight}; 8 | color: ${({ theme, negative }) => (negative ? theme.red1 : theme.green1)}; 9 | ` 10 | 11 | export interface LogoProps { 12 | value: number | undefined 13 | decimals?: number 14 | fontSize?: string 15 | fontWeight?: number 16 | wrap?: boolean 17 | simple?: boolean 18 | } 19 | 20 | export default function Percent({ 21 | value, 22 | decimals = 2, 23 | fontSize = '16px', 24 | fontWeight = 500, 25 | wrap = false, 26 | simple = false, 27 | ...rest 28 | }: LogoProps) { 29 | if (value === undefined || value === null) { 30 | return ( 31 | 32 | - 33 | 34 | ) 35 | } 36 | 37 | const truncated = parseFloat(value.toFixed(decimals)) 38 | 39 | if (simple) { 40 | return ( 41 | 42 | {Math.abs(value).toFixed(decimals)}% 43 | 44 | ) 45 | } 46 | 47 | return ( 48 | 49 | {wrap && '('} 50 | {truncated < 0 && '↓'} 51 | {truncated > 0 && '↑'} 52 | {Math.abs(value).toFixed(decimals)}%{wrap && ')'} 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Popover/index.tsx: -------------------------------------------------------------------------------- 1 | import { Placement } from '@popperjs/core' 2 | import { transparentize } from 'polished' 3 | import React, { useCallback, useState } from 'react' 4 | import { usePopper } from 'react-popper' 5 | import styled from 'styled-components' 6 | import useInterval from '../../hooks/useInterval' 7 | import Portal from '@reach/portal' 8 | 9 | const PopoverContainer = styled.div<{ $show: boolean }>` 10 | z-index: 9999; 11 | 12 | visibility: ${(props) => (props.$show ? 'visible' : 'hidden')}; 13 | opacity: ${(props) => (props.$show ? 1 : 0)}; 14 | transition: 15 | visibility 150ms linear, 16 | opacity 150ms linear; 17 | 18 | background: ${({ theme }) => theme.bg2}; 19 | border: 1px solid ${({ theme }) => theme.bg3}; 20 | box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)}; 21 | color: ${({ theme }) => theme.text2}; 22 | border-radius: 8px; 23 | ` 24 | 25 | const ReferenceElement = styled.div` 26 | display: inline-block; 27 | ` 28 | 29 | const Arrow = styled.div` 30 | width: 8px; 31 | height: 8px; 32 | z-index: 9998; 33 | 34 | ::before { 35 | position: absolute; 36 | width: 8px; 37 | height: 8px; 38 | z-index: 9998; 39 | 40 | content: ''; 41 | border: 1px solid ${({ theme }) => theme.bg3}; 42 | transform: rotate(45deg); 43 | background: ${({ theme }) => theme.bg2}; 44 | } 45 | 46 | &.arrow-top { 47 | bottom: -5px; 48 | ::before { 49 | border-top: none; 50 | border-left: none; 51 | } 52 | } 53 | 54 | &.arrow-bottom { 55 | top: -5px; 56 | ::before { 57 | border-bottom: none; 58 | border-right: none; 59 | } 60 | } 61 | 62 | &.arrow-left { 63 | right: -5px; 64 | 65 | ::before { 66 | border-bottom: none; 67 | border-left: none; 68 | } 69 | } 70 | 71 | &.arrow-right { 72 | left: -5px; 73 | ::before { 74 | border-right: none; 75 | border-top: none; 76 | } 77 | } 78 | ` 79 | 80 | export interface PopoverProps { 81 | content: React.ReactNode 82 | show: boolean 83 | children: React.ReactNode 84 | placement?: Placement 85 | } 86 | 87 | export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) { 88 | const [referenceElement, setReferenceElement] = useState(null) 89 | const [popperElement, setPopperElement] = useState(null) 90 | const [arrowElement, setArrowElement] = useState(null) 91 | const { styles, update, attributes } = usePopper(referenceElement, popperElement, { 92 | placement, 93 | strategy: 'fixed', 94 | modifiers: [ 95 | { name: 'offset', options: { offset: [8, 8] } }, 96 | { name: 'arrow', options: { element: arrowElement } }, 97 | ], 98 | }) 99 | const updateCallback = useCallback(() => { 100 | update && update() 101 | }, [update]) 102 | useInterval(updateCallback, show ? 100 : null) 103 | 104 | return ( 105 | <> 106 | {children} 107 | 108 | 109 | {content} 110 | 116 | 117 | 118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/components/Popups/PopupItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useEffect } from 'react' 2 | import { X } from 'react-feather' 3 | import { useSpring } from 'react-spring/web' 4 | import styled, { ThemeContext } from 'styled-components' 5 | import { animated } from 'react-spring' 6 | import { PopupContent } from '../../state/application/actions' 7 | import { useRemovePopup } from '../../state/application/hooks' 8 | import ListUpdatePopup from './ListUpdatePopup' 9 | 10 | export const StyledClose = styled(X)` 11 | position: absolute; 12 | right: 10px; 13 | top: 10px; 14 | 15 | :hover { 16 | cursor: pointer; 17 | } 18 | ` 19 | export const Popup = styled.div` 20 | display: inline-block; 21 | width: 100%; 22 | padding: 1em; 23 | background-color: ${({ theme }) => theme.bg1}; 24 | position: relative; 25 | border-radius: 10px; 26 | padding: 20px; 27 | padding-right: 35px; 28 | overflow: hidden; 29 | 30 | ${({ theme }) => theme.mediaWidth.upToSmall` 31 | min-width: 290px; 32 | &:not(:last-of-type) { 33 | margin-right: 20px; 34 | } 35 | `} 36 | ` 37 | const Fader = styled.div` 38 | position: absolute; 39 | bottom: 0px; 40 | left: 0px; 41 | width: 100%; 42 | height: 2px; 43 | background-color: ${({ theme }) => theme.bg3}; 44 | ` 45 | 46 | const AnimatedFader = animated(Fader) 47 | 48 | export default function PopupItem({ 49 | removeAfterMs, 50 | content, 51 | popKey, 52 | }: { 53 | removeAfterMs: number | null 54 | content: PopupContent 55 | popKey: string 56 | }) { 57 | const removePopup = useRemovePopup() 58 | const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup]) 59 | useEffect(() => { 60 | if (removeAfterMs === null) return undefined 61 | 62 | const timeout = setTimeout(() => { 63 | removeThisPopup() 64 | }, removeAfterMs) 65 | 66 | return () => { 67 | clearTimeout(timeout) 68 | } 69 | }, [removeAfterMs, removeThisPopup]) 70 | 71 | const theme = useContext(ThemeContext) 72 | 73 | let popupContent 74 | if ('listUpdate' in content) { 75 | const { 76 | listUpdate: { listUrl, oldList, newList, auto }, 77 | } = content 78 | popupContent = 79 | } 80 | 81 | const faderStyle = useSpring({ 82 | from: { width: '100%' }, 83 | to: { width: '0%' }, 84 | config: { duration: removeAfterMs ?? undefined }, 85 | }) 86 | 87 | return ( 88 | 89 | 90 | {popupContent} 91 | {removeAfterMs !== null ? : null} 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /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 | import { useURLWarningVisible } from '../../state/user/hooks' 7 | 8 | const MobilePopupWrapper = styled.div<{ height: string | number }>` 9 | position: relative; 10 | max-width: 100%; 11 | height: ${({ height }) => height}; 12 | margin: ${({ height }) => (height ? '0 auto;' : 0)}; 13 | margin-bottom: ${({ height }) => (height ? '20px' : 0)}; 14 | 15 | display: none; 16 | ${({ theme }) => theme.mediaWidth.upToSmall` 17 | display: block; 18 | `}; 19 | ` 20 | 21 | const MobilePopupInner = styled.div` 22 | height: 99%; 23 | overflow-x: auto; 24 | overflow-y: hidden; 25 | display: flex; 26 | flex-direction: row; 27 | -webkit-overflow-scrolling: touch; 28 | ::-webkit-scrollbar { 29 | display: none; 30 | } 31 | ` 32 | 33 | const FixedPopupColumn = styled(AutoColumn)<{ $extraPadding: boolean }>` 34 | position: fixed; 35 | top: ${({ $extraPadding }) => ($extraPadding ? '108px' : '88px')}; 36 | right: 1rem; 37 | max-width: 355px !important; 38 | width: 100%; 39 | z-index: 3; 40 | 41 | ${({ theme }) => theme.mediaWidth.upToSmall` 42 | display: none; 43 | `}; 44 | ` 45 | 46 | export default function Popups() { 47 | // get all popups 48 | const activePopups = useActivePopups() 49 | 50 | const urlWarningActive = useURLWarningVisible() 51 | 52 | return ( 53 | <> 54 | 55 | {activePopups.map((item) => ( 56 | 57 | ))} 58 | 59 | 0 ? 'fit-content' : 0}> 60 | 61 | {activePopups // reverse so new items up front 62 | .slice(0) 63 | .reverse() 64 | .map((item) => ( 65 | 66 | ))} 67 | 68 | 69 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /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 | const LightQuestionWrapper = styled.div` 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | padding: 0.2rem; 30 | border: none; 31 | background: none; 32 | outline: none; 33 | cursor: default; 34 | border-radius: 36px; 35 | width: 24px; 36 | height: 24px; 37 | background-color: rgba(255, 255, 255, 0.1); 38 | color: ${({ theme }) => theme.white}; 39 | 40 | :hover, 41 | :focus { 42 | opacity: 0.7; 43 | } 44 | ` 45 | 46 | const QuestionMark = styled.span` 47 | font-size: 1rem; 48 | ` 49 | 50 | export default function QuestionHelper({ text }: { text: string }) { 51 | const [show, setShow] = useState(false) 52 | 53 | const open = useCallback(() => setShow(true), [setShow]) 54 | const close = useCallback(() => setShow(false), [setShow]) 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export function LightQuestionHelper({ text }: { text: string }) { 68 | const [show, setShow] = useState(false) 69 | 70 | const open = useCallback(() => setShow(true), [setShow]) 71 | const close = useCallback(() => setShow(false), [setShow]) 72 | 73 | return ( 74 | 75 | 76 | 77 | ? 78 | 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Row/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Box } from 'rebass/styled-components' 3 | 4 | const Row = styled(Box)<{ 5 | width?: string 6 | align?: string 7 | justify?: string 8 | padding?: string 9 | border?: string 10 | borderRadius?: string 11 | gap?: string 12 | }>` 13 | width: ${({ width }) => width ?? '100%'}; 14 | display: flex; 15 | padding: 0; 16 | align-items: ${({ align }) => align ?? 'center'}; 17 | justify-content: ${({ justify }) => justify ?? 'flex-start'}; 18 | padding: ${({ padding }) => padding}; 19 | border: ${({ border }) => border}; 20 | border-radius: ${({ borderRadius }) => borderRadius}; 21 | gap: ${({ gap }) => gap}; 22 | ` 23 | 24 | export const RowBetween = styled(Row)` 25 | justify-content: space-between; 26 | ` 27 | 28 | export const RowFlat = styled.div` 29 | display: flex; 30 | align-items: flex-end; 31 | ` 32 | 33 | export const AutoRow = styled(Row)<{ $gap?: string; justify?: string }>` 34 | flex-wrap: wrap; 35 | margin: ${({ $gap }) => $gap && `-${$gap}`}; 36 | justify-content: ${({ justify }) => justify && justify}; 37 | 38 | & > * { 39 | margin: ${({ $gap }) => $gap} !important; 40 | } 41 | ` 42 | 43 | export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` 44 | width: fit-content; 45 | margin: ${({ gap }) => gap && `-${gap}`}; 46 | ` 47 | 48 | export const ResponsiveRow = styled(RowBetween)` 49 | ${({ theme }) => theme.mediaWidth.upToSmall` 50 | flex-direction: column; 51 | row-gap: 1rem; 52 | `}; 53 | ` 54 | 55 | export default Row 56 | -------------------------------------------------------------------------------- /src/components/Text/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { TYPE } from 'theme' 3 | 4 | // responsive text 5 | export const Label = styled(TYPE.label)<{ end?: number }>` 6 | display: flex; 7 | font-size: 16px; 8 | font-weight: 400; 9 | justify-content: ${({ end }) => (end ? 'flex-end' : 'flex-start')}; 10 | align-items: center; 11 | font-variant-numeric: tabular-nums; 12 | @media screen and (max-width: 640px) { 13 | font-size: 14px; 14 | } 15 | ` 16 | 17 | export const ClickableText = styled(Label)` 18 | text-align: end; 19 | &:hover { 20 | cursor: pointer; 21 | opacity: 0.6; 22 | } 23 | user-select: none; 24 | @media screen and (max-width: 640px) { 25 | font-size: 12px; 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /src/components/Toggle/ListToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { TYPE } from '../../theme' 4 | 5 | const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 6 | border-radius: 20px; 7 | border: none; 8 | background: ${({ theme }) => theme.bg1}; 9 | display: flex; 10 | width: fit-content; 11 | cursor: pointer; 12 | outline: none; 13 | padding: 0.4rem 0.4rem; 14 | align-items: center; 15 | ` 16 | 17 | const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>` 18 | border-radius: 50%; 19 | height: 24px; 20 | width: 24px; 21 | background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)}; 22 | :hover { 23 | opacity: 0.8; 24 | } 25 | ` 26 | 27 | const StatusText = styled(TYPE.main)<{ isActive?: boolean }>` 28 | margin: 0 10px; 29 | width: 24px; 30 | color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)}; 31 | ` 32 | 33 | export interface ToggleProps { 34 | id?: string 35 | isActive: boolean 36 | bgColor: string 37 | toggle: () => void 38 | } 39 | 40 | export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) { 41 | return ( 42 | 43 | {isActive && ( 44 | 45 | ON 46 | 47 | )} 48 | 49 | {!isActive && ( 50 | 51 | OFF 52 | 53 | )} 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Toggle/MultiToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | export const ToggleWrapper = styled.button<{ width?: string }>` 5 | display: flex; 6 | align-items: center; 7 | width: ${({ width }) => width ?? '100%'} 8 | padding: 1px; 9 | background: ${({ theme }) => theme.bg0}; 10 | border-radius: 8px; 11 | border: ${({ theme }) => '2px solid ' + theme.bg2}; 12 | cursor: pointer; 13 | outline: none; 14 | ` 15 | 16 | export const ToggleElement = styled.span<{ isActive?: boolean; fontSize?: string }>` 17 | display: flex; 18 | align-items: center; 19 | width: 100%; 20 | padding: 4px 0.5rem; 21 | border-radius: 6px; 22 | justify-content: center; 23 | height: 100%; 24 | background: ${({ theme, isActive }) => (isActive ? theme.bg2 : 'none')}; 25 | color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)}; 26 | font-size: ${({ fontSize }) => fontSize ?? '1rem'}; 27 | font-weight: 500; 28 | :hover { 29 | user-select: initial; 30 | color: ${({ theme, isActive }) => (isActive ? theme.text2 : theme.text3)}; 31 | } 32 | ` 33 | 34 | export interface ToggleProps { 35 | options: string[] 36 | activeIndex: number 37 | toggle: (index: number) => void 38 | id?: string 39 | width?: string 40 | } 41 | 42 | export default function MultiToggle({ id, options, activeIndex, toggle, width }: ToggleProps) { 43 | return ( 44 | 45 | {options.map((option, index) => ( 46 | toggle(index)}> 47 | {option} 48 | 49 | ))} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /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: 1rem; 10 | font-weight: 400; 11 | 12 | padding: 0.35rem 0.6rem; 13 | border-radius: 12px; 14 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')}; 15 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text2)}; 16 | font-size: 1rem; 17 | font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')}; 18 | :hover { 19 | user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')}; 20 | background: ${({ theme, isActive, isOnSwitch }) => 21 | isActive ? (isOnSwitch ? theme.primary1 : theme.text3) : 'none'}; 22 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 23 | } 24 | ` 25 | 26 | const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 27 | border-radius: 12px; 28 | border: none; 29 | background: ${({ theme }) => theme.bg3}; 30 | display: flex; 31 | width: fit-content; 32 | cursor: pointer; 33 | outline: none; 34 | padding: 0; 35 | ` 36 | 37 | export interface ToggleProps { 38 | id?: string 39 | isActive: boolean 40 | toggle: () => void 41 | } 42 | 43 | export default function Toggle({ id, isActive, toggle }: ToggleProps) { 44 | return ( 45 | 46 | 47 | On 48 | 49 | 50 | Off 51 | 52 | 53 | ) 54 | } 55 | 56 | export const ToggleWrapper = styled.button<{ width?: string }>` 57 | display: flex; 58 | align-items: center; 59 | width: ${({ width }) => width ?? '100%'} 60 | padding: 1px; 61 | background: ${({ theme }) => theme.bg2}; 62 | border-radius: 12px; 63 | border: ${({ theme }) => '2px solid ' + theme.bg2}; 64 | cursor: pointer; 65 | outline: none; 66 | color: ${({ theme }) => theme.text2}; 67 | 68 | ` 69 | 70 | export const ToggleElementFree = styled.span<{ isActive?: boolean; fontSize?: string }>` 71 | display: flex; 72 | align-items: center; 73 | width: 100%; 74 | padding: 2px 10px; 75 | border-radius: 12px; 76 | justify-content: center; 77 | height: 100%; 78 | background: ${({ theme, isActive }) => (isActive ? theme.black : 'none')}; 79 | color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text2)}; 80 | font-size: ${({ fontSize }) => fontSize ?? '1rem'}; 81 | font-weight: 600; 82 | white-space: nowrap; 83 | :hover { 84 | user-select: initial; 85 | color: ${({ theme, isActive }) => (isActive ? theme.text2 : theme.text3)}; 86 | } 87 | margin-top: 0.5px; 88 | ` 89 | -------------------------------------------------------------------------------- /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 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/pools/TopPoolMovers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import styled from 'styled-components' 3 | import { ScrollableX, GreyCard, GreyBadge } from 'components/Card' 4 | import Loader from 'components/Loader' 5 | import { AutoColumn } from 'components/Column' 6 | import { RowFixed } from 'components/Row' 7 | import { TYPE, StyledInternalLink } from 'theme' 8 | import { formatDollarAmount } from 'utils/numbers' 9 | import Percent from 'components/Percent' 10 | import { useAllPoolData } from 'state/pools/hooks' 11 | import { PoolData } from 'state/pools/reducer' 12 | import DoubleCurrencyLogo from 'components/DoubleLogo' 13 | import HoverInlineText from 'components/HoverInlineText' 14 | import { feeTierPercent } from 'utils' 15 | 16 | const Container = styled(StyledInternalLink)` 17 | min-width: 210px; 18 | margin-right: 16px; 19 | 20 | :hover { 21 | cursor: pointer; 22 | opacity: 0.6; 23 | } 24 | ` 25 | 26 | const Wrapper = styled(GreyCard)` 27 | padding: 12px; 28 | ` 29 | 30 | const DataCard = ({ poolData }: { poolData: PoolData }) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {feeTierPercent(poolData.feeTier)} 42 | 43 | 44 | 45 | {formatDollarAmount(poolData.volumeUSD)} 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | 54 | export default function TopPoolMovers() { 55 | const allPools = useAllPoolData() 56 | 57 | const topVolume = useMemo(() => { 58 | return Object.values(allPools) 59 | .sort(({ data: a }, { data: b }) => { 60 | return a && b ? (a?.volumeUSDChange > b?.volumeUSDChange ? -1 : 1) : -1 61 | }) 62 | .slice(0, Math.min(20, Object.values(allPools).length)) 63 | }, [allPools]) 64 | 65 | if (Object.keys(allPools).length === 0) { 66 | return 67 | } 68 | 69 | return ( 70 | 71 | {topVolume.map((entry) => 72 | entry.data ? : null, 73 | )} 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/components/shared/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const PageButtons = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | margin-top: 0.2em; 9 | margin-bottom: 0.5em; 10 | ` 11 | 12 | export const Arrow = styled.div<{ $faded: boolean }>` 13 | color: ${({ theme }) => theme.primary1}; 14 | opacity: ${(props) => (props.$faded ? 0.3 : 1)}; 15 | padding: 0 20px; 16 | user-select: none; 17 | :hover { 18 | cursor: pointer; 19 | } 20 | ` 21 | 22 | export const Break = styled.div` 23 | height: 1px; 24 | background-color: ${({ theme }) => theme.bg1}; 25 | width: 100%; 26 | ` 27 | 28 | export const FixedSpan = styled.span<{ width?: string | null }>` 29 | width: ${({ width }) => width ?? ''}; 30 | ` 31 | 32 | export const MonoSpace = styled.span` 33 | font-variant-numeric: tabular-nums; 34 | ` 35 | -------------------------------------------------------------------------------- /src/components/tokens/TopTokenMovers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef, useState, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import { useAllTokenData } from 'state/tokens/hooks' 4 | import { GreyCard } from 'components/Card' 5 | import { TokenData } from 'state/tokens/reducer' 6 | import { AutoColumn } from 'components/Column' 7 | import { RowFixed, RowFlat } from 'components/Row' 8 | import CurrencyLogo from 'components/CurrencyLogo' 9 | import { TYPE, StyledInternalLink } from 'theme' 10 | import { formatDollarAmount } from 'utils/numbers' 11 | import Percent from 'components/Percent' 12 | import HoverInlineText from 'components/HoverInlineText' 13 | 14 | const CardWrapper = styled(StyledInternalLink)` 15 | min-width: 190px; 16 | margin-right: 16px; 17 | 18 | :hover { 19 | cursor: pointer; 20 | opacity: 0.6; 21 | } 22 | ` 23 | 24 | const FixedContainer = styled(AutoColumn)`` 25 | 26 | export const ScrollableRow = styled.div` 27 | display: flex; 28 | flex-direction: row; 29 | width: 100%; 30 | overflow-x: auto; 31 | white-space: nowrap; 32 | 33 | ::-webkit-scrollbar { 34 | display: none; 35 | } 36 | ` 37 | 38 | const DataCard = ({ tokenData }: { tokenData: TokenData }) => { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {formatDollarAmount(tokenData.priceUSD)} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default function TopTokenMovers() { 62 | const allTokens = useAllTokenData() 63 | 64 | const topPriceIncrease = useMemo(() => { 65 | return Object.values(allTokens) 66 | .sort(({ data: a }, { data: b }) => { 67 | return a && b ? (Math.abs(a?.priceUSDChange) > Math.abs(b?.priceUSDChange) ? -1 : 1) : -1 68 | }) 69 | .slice(0, Math.min(20, Object.values(allTokens).length)) 70 | }, [allTokens]) 71 | 72 | const increaseRef = useRef(null) 73 | const [increaseSet, setIncreaseSet] = useState(false) 74 | // const [pauseAnimation, setPauseAnimation] = useState(false) 75 | // const [resetInterval, setClearInterval] = useState<() => void | undefined>() 76 | 77 | useEffect(() => { 78 | if (!increaseSet && increaseRef && increaseRef.current) { 79 | setInterval(() => { 80 | if (increaseRef.current && increaseRef.current.scrollLeft !== increaseRef.current.scrollWidth) { 81 | increaseRef.current.scrollTo(increaseRef.current.scrollLeft + 1, 0) 82 | } 83 | }, 30) 84 | setIncreaseSet(true) 85 | } 86 | }, [increaseRef, increaseSet]) 87 | 88 | return ( 89 | 90 | 91 | {topPriceIncrease.map((entry) => 92 | entry.data ? : null, 93 | )} 94 | 95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /src/constants/abis/argent-wallet-detector.ts: -------------------------------------------------------------------------------- 1 | import ARGENT_WALLET_DETECTOR_ABI from './argent-wallet-detector.json' 2 | 3 | const ARGENT_WALLET_DETECTOR_MAINNET_ADDRESS = '0xeca4B0bDBf7c55E9b7925919d03CbF8Dc82537E8' 4 | 5 | export { ARGENT_WALLET_DETECTOR_ABI, ARGENT_WALLET_DETECTOR_MAINNET_ADDRESS } 6 | -------------------------------------------------------------------------------- /src/constants/abis/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [{ "name": "", "type": "string" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [ 14 | { "name": "_spender", "type": "address" }, 15 | { "name": "_value", "type": "uint256" } 16 | ], 17 | "name": "approve", 18 | "outputs": [{ "name": "", "type": "bool" }], 19 | "payable": false, 20 | "stateMutability": "nonpayable", 21 | "type": "function" 22 | }, 23 | { 24 | "constant": true, 25 | "inputs": [], 26 | "name": "totalSupply", 27 | "outputs": [{ "name": "", "type": "uint256" }], 28 | "payable": false, 29 | "stateMutability": "view", 30 | "type": "function" 31 | }, 32 | { 33 | "constant": false, 34 | "inputs": [ 35 | { "name": "_from", "type": "address" }, 36 | { "name": "_to", "type": "address" }, 37 | { "name": "_value", "type": "uint256" } 38 | ], 39 | "name": "transferFrom", 40 | "outputs": [{ "name": "", "type": "bool" }], 41 | "payable": false, 42 | "stateMutability": "nonpayable", 43 | "type": "function" 44 | }, 45 | { 46 | "constant": true, 47 | "inputs": [], 48 | "name": "decimals", 49 | "outputs": [{ "name": "", "type": "uint8" }], 50 | "payable": false, 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "constant": true, 56 | "inputs": [{ "name": "_owner", "type": "address" }], 57 | "name": "balanceOf", 58 | "outputs": [{ "name": "balance", "type": "uint256" }], 59 | "payable": false, 60 | "stateMutability": "view", 61 | "type": "function" 62 | }, 63 | { 64 | "constant": true, 65 | "inputs": [], 66 | "name": "symbol", 67 | "outputs": [{ "name": "", "type": "string" }], 68 | "payable": false, 69 | "stateMutability": "view", 70 | "type": "function" 71 | }, 72 | { 73 | "constant": false, 74 | "inputs": [ 75 | { "name": "_to", "type": "address" }, 76 | { "name": "_value", "type": "uint256" } 77 | ], 78 | "name": "transfer", 79 | "outputs": [{ "name": "", "type": "bool" }], 80 | "payable": false, 81 | "stateMutability": "nonpayable", 82 | "type": "function" 83 | }, 84 | { 85 | "constant": true, 86 | "inputs": [ 87 | { "name": "_owner", "type": "address" }, 88 | { "name": "_spender", "type": "address" } 89 | ], 90 | "name": "allowance", 91 | "outputs": [{ "name": "", "type": "uint256" }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { "payable": true, "stateMutability": "payable", "type": "fallback" }, 97 | { 98 | "anonymous": false, 99 | "inputs": [ 100 | { "indexed": true, "name": "owner", "type": "address" }, 101 | { "indexed": true, "name": "spender", "type": "address" }, 102 | { "indexed": false, "name": "value", "type": "uint256" } 103 | ], 104 | "name": "Approval", 105 | "type": "event" 106 | }, 107 | { 108 | "anonymous": false, 109 | "inputs": [ 110 | { "indexed": true, "name": "from", "type": "address" }, 111 | { "indexed": true, "name": "to", "type": "address" }, 112 | { "indexed": false, "name": "value", "type": "uint256" } 113 | ], 114 | "name": "Transfer", 115 | "type": "event" 116 | } 117 | ] 118 | -------------------------------------------------------------------------------- /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 | ] 56 | -------------------------------------------------------------------------------- /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/abis/staking-rewards.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/StakingRewards.json' 3 | import { abi as STAKING_REWARDS_FACTORY_ABI } from '@uniswap/liquidity-staker/build/StakingRewardsFactory.json' 4 | 5 | const STAKING_REWARDS_INTERFACE = new Interface(STAKING_REWARDS_ABI) 6 | 7 | const STAKING_REWARDS_FACTORY_INTERFACE = new Interface(STAKING_REWARDS_FACTORY_ABI) 8 | 9 | export { STAKING_REWARDS_FACTORY_INTERFACE, STAKING_REWARDS_INTERFACE } 10 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import { Connector } from '@web3-react/types' 3 | import ms from 'ms' 4 | 5 | import { SupportedNetwork } from './networks' 6 | 7 | export const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1) 8 | 9 | export const MATIC_ADDRESS = '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' 10 | export const CELO_ADDRESS = '0x471EcE3750Da237f93B8E339c536989b8978a438' 11 | 12 | const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' 13 | const ARBITRUM_WETH_ADDRESS = '0x82af49447d8a07e3bd95bd0d56f35241523fbab1' 14 | 15 | export const WETH_ADDRESSES = [WETH_ADDRESS, ARBITRUM_WETH_ADDRESS] 16 | 17 | export const TOKEN_HIDE: { [key: string]: string[] } = { 18 | [SupportedNetwork.ETHEREUM]: [ 19 | '0xd46ba6d942050d489dbd938a2c909a5d5039a161', 20 | '0x7dfb72a2aad08c937706f21421b15bfc34cba9ca', 21 | '0x12b32f10a499bf40db334efe04226cca00bf2d9b', 22 | '0x160de4468586b6b2f8a92feb0c260fc6cfc743b1', 23 | ], 24 | [SupportedNetwork.POLYGON]: ['0x8d52c2d70a7c28a9daac2ff12ad9bfbf041cd318'], 25 | [SupportedNetwork.ARBITRUM]: [], 26 | [SupportedNetwork.OPTIMISM]: [], 27 | [SupportedNetwork.CELO]: [], 28 | [SupportedNetwork.BNB]: [], 29 | [SupportedNetwork.AVALANCHE]: [], 30 | [SupportedNetwork.BASE]: [], 31 | } 32 | 33 | export const POOL_HIDE: { [key: string]: string[] } = { 34 | [SupportedNetwork.ETHEREUM]: [ 35 | '0x86d257cdb7bc9c0df10e84c8709697f92770b335', 36 | '0xf8dbd52488978a79dfe6ffbd81a01fc5948bf9ee', 37 | '0x8fe8d9bb8eeba3ed688069c3d6b556c9ca258248', 38 | '0xa850478adaace4c08fc61de44d8cf3b64f359bec', 39 | '0x277667eb3e34f134adf870be9550e9f323d0dc24', 40 | '0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51', 41 | '0x055284a4ca6532ecc219ac06b577d540c686669d', 42 | ], 43 | [SupportedNetwork.POLYGON]: ['0x5f616541c801e2b9556027076b730e0197974f6a'], 44 | [SupportedNetwork.ARBITRUM]: [], 45 | [SupportedNetwork.OPTIMISM]: [], 46 | [SupportedNetwork.CELO]: [], 47 | [SupportedNetwork.BNB]: [], 48 | [SupportedNetwork.AVALANCHE]: [], 49 | [SupportedNetwork.BASE]: [], 50 | } 51 | 52 | export const START_BLOCKS: { [key: string]: number } = { 53 | [SupportedNetwork.ETHEREUM]: 14292820, 54 | [SupportedNetwork.POLYGON]: 25459720, 55 | [SupportedNetwork.ARBITRUM]: 175, 56 | [SupportedNetwork.OPTIMISM]: 10028767, 57 | [SupportedNetwork.CELO]: 13916355, 58 | [SupportedNetwork.BNB]: 26324014, 59 | [SupportedNetwork.AVALANCHE]: 31422450, 60 | [SupportedNetwork.BASE]: 1371680, 61 | } 62 | 63 | export interface WalletInfo { 64 | connector?: Connector 65 | name: string 66 | iconName: string 67 | description: string 68 | href: string | null 69 | color: string 70 | primary?: true 71 | mobile?: true 72 | mobileOnly?: true 73 | } 74 | 75 | export const AVERAGE_L1_BLOCK_TIME = ms(`12s`) 76 | 77 | export const NetworkContextName = 'NETWORK' 78 | 79 | // SDN OFAC addresses 80 | export const BLOCKED_ADDRESSES: string[] = [ 81 | '0x7F367cC41522cE07553e823bf3be79A889DEbe1B', 82 | '0xd882cFc20F52f2599D84b8e8D58C7FB62cfE344b', 83 | '0x901bb9583b24D97e995513C6778dc6888AB6870e', 84 | '0xA7e5d5A720f06526557c513402f2e6B5fA20b008', 85 | ] 86 | -------------------------------------------------------------------------------- /src/constants/intervals.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constanst for historical data fetching. 3 | * 4 | */ 5 | 6 | import { OpUnitType } from 'dayjs' 7 | 8 | export const ONE_HOUR_SECONDS = 3600 9 | 10 | export const TimeWindow: { 11 | [key: string]: OpUnitType 12 | } = { 13 | DAY: 'day', 14 | WEEK: 'week', 15 | MONTH: 'month', 16 | } 17 | -------------------------------------------------------------------------------- /src/constants/lists.ts: -------------------------------------------------------------------------------- 1 | // used to mark unsupported tokens, these are hosted lists of unsupported tokens 2 | 3 | export const UNSUPPORTED_LIST_URLS: string[] = [] 4 | export const OPTIMISM_LIST = 'https://static.optimism.io/optimism.tokenlist.json' 5 | export const ARBITRUM_LIST = 'https://bridge.arbitrum.io/token-list-42161.json' 6 | export const POLYGON_LIST = 7 | 'https://unpkg.com/quickswap-default-token-list@1.2.2/build/quickswap-default.tokenlist.json' 8 | export const CELO_LIST = 'https://celo-org.github.io/celo-token-list/celo.tokenlist.json' 9 | export const BNB_LIST = 'https://raw.githubusercontent.com/plasmadlt/plasma-finance-token-list/master/bnb.json' 10 | 11 | // lower index == higher priority for token import 12 | export const DEFAULT_LIST_OF_LISTS: string[] = [ 13 | OPTIMISM_LIST, 14 | ARBITRUM_LIST, 15 | POLYGON_LIST, 16 | CELO_LIST, 17 | BNB_LIST, 18 | ...UNSUPPORTED_LIST_URLS, // need to load unsupported tokens as well 19 | ] 20 | 21 | // default lists to be 'active' aka searched across 22 | export const DEFAULT_ACTIVE_LIST_URLS: string[] = [OPTIMISM_LIST, ARBITRUM_LIST, POLYGON_LIST, CELO_LIST, BNB_LIST] 23 | -------------------------------------------------------------------------------- /src/constants/multicall/abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "getCurrentBlockTimestamp", 6 | "outputs": [ 7 | { 8 | "name": "timestamp", 9 | "type": "uint256" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": true, 18 | "inputs": [ 19 | { 20 | "components": [ 21 | { 22 | "name": "target", 23 | "type": "address" 24 | }, 25 | { 26 | "name": "callData", 27 | "type": "bytes" 28 | } 29 | ], 30 | "name": "calls", 31 | "type": "tuple[]" 32 | } 33 | ], 34 | "name": "aggregate", 35 | "outputs": [ 36 | { 37 | "name": "blockNumber", 38 | "type": "uint256" 39 | }, 40 | { 41 | "name": "returnData", 42 | "type": "bytes[]" 43 | } 44 | ], 45 | "payable": false, 46 | "stateMutability": "view", 47 | "type": "function" 48 | }, 49 | { 50 | "constant": true, 51 | "inputs": [], 52 | "name": "getLastBlockHash", 53 | "outputs": [ 54 | { 55 | "name": "blockHash", 56 | "type": "bytes32" 57 | } 58 | ], 59 | "payable": false, 60 | "stateMutability": "view", 61 | "type": "function" 62 | }, 63 | { 64 | "constant": true, 65 | "inputs": [ 66 | { 67 | "name": "addr", 68 | "type": "address" 69 | } 70 | ], 71 | "name": "getEthBalance", 72 | "outputs": [ 73 | { 74 | "name": "balance", 75 | "type": "uint256" 76 | } 77 | ], 78 | "payable": false, 79 | "stateMutability": "view", 80 | "type": "function" 81 | }, 82 | { 83 | "constant": true, 84 | "inputs": [], 85 | "name": "getCurrentBlockDifficulty", 86 | "outputs": [ 87 | { 88 | "name": "difficulty", 89 | "type": "uint256" 90 | } 91 | ], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": true, 98 | "inputs": [], 99 | "name": "getCurrentBlockGasLimit", 100 | "outputs": [ 101 | { 102 | "name": "gaslimit", 103 | "type": "uint256" 104 | } 105 | ], 106 | "payable": false, 107 | "stateMutability": "view", 108 | "type": "function" 109 | }, 110 | { 111 | "constant": true, 112 | "inputs": [], 113 | "name": "getCurrentBlockCoinbase", 114 | "outputs": [ 115 | { 116 | "name": "coinbase", 117 | "type": "address" 118 | } 119 | ], 120 | "payable": false, 121 | "stateMutability": "view", 122 | "type": "function" 123 | }, 124 | { 125 | "constant": true, 126 | "inputs": [ 127 | { 128 | "name": "blockNumber", 129 | "type": "uint256" 130 | } 131 | ], 132 | "name": "getBlockHash", 133 | "outputs": [ 134 | { 135 | "name": "blockHash", 136 | "type": "bytes32" 137 | } 138 | ], 139 | "payable": false, 140 | "stateMutability": "view", 141 | "type": "function" 142 | } 143 | ] 144 | -------------------------------------------------------------------------------- /src/constants/multicall/index.ts: -------------------------------------------------------------------------------- 1 | import MULTICALL_ABI from './abi.json' 2 | 3 | export { MULTICALL_ABI } 4 | -------------------------------------------------------------------------------- /src/constants/tokenLists/uniswap-v2-unsupported.tokenlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Uniswap V2 Unsupported List", 3 | "timestamp": "2021-01-05T20:47:02.923Z", 4 | "version": { 5 | "major": 1, 6 | "minor": 0, 7 | "patch": 0 8 | }, 9 | "tags": {}, 10 | "logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir", 11 | "keywords": ["uniswap", "unsupported"], 12 | "tokens": [ 13 | { 14 | "name": "Gold Tether", 15 | "address": "0x4922a015c4407F87432B179bb209e125432E4a2A", 16 | "symbol": "XAUt", 17 | "decimals": 6, 18 | "chainId": 1, 19 | "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4922a015c4407F87432B179bb209e125432E4a2A/logo.png" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/data/application/index.ts: -------------------------------------------------------------------------------- 1 | import { useActiveNetworkVersion } from 'state/application/hooks' 2 | import { healthClient } from './../../apollo/client' 3 | import { useQuery } from '@apollo/client' 4 | import gql from 'graphql-tag' 5 | import { ArbitrumNetworkInfo, EthereumNetworkInfo } from 'constants/networks' 6 | 7 | export const SUBGRAPH_HEALTH = gql` 8 | query health($name: Bytes) { 9 | indexingStatusForCurrentVersion(subgraphName: $name, subgraphError: allow) { 10 | synced 11 | health 12 | chains { 13 | chainHeadBlock { 14 | number 15 | } 16 | latestBlock { 17 | number 18 | } 19 | } 20 | } 21 | } 22 | ` 23 | 24 | interface HealthResponse { 25 | indexingStatusForCurrentVersion: { 26 | chains: { 27 | chainHeadBlock: { 28 | number: string 29 | } 30 | latestBlock: { 31 | number: string 32 | } 33 | }[] 34 | synced: boolean 35 | } 36 | } 37 | 38 | /** 39 | * Fetch top addresses by volume 40 | */ 41 | export function useFetchedSubgraphStatus(): { 42 | available: boolean | null 43 | syncedBlock: number | undefined 44 | headBlock: number | undefined 45 | } { 46 | const [activeNetwork] = useActiveNetworkVersion() 47 | 48 | const { loading, error, data } = useQuery(SUBGRAPH_HEALTH, { 49 | client: healthClient, 50 | fetchPolicy: 'network-only', 51 | variables: { 52 | name: 53 | activeNetwork === EthereumNetworkInfo 54 | ? 'uniswap/uniswap-v3' 55 | : activeNetwork === ArbitrumNetworkInfo 56 | ? 'ianlapham/uniswap-arbitrum-one' 57 | : 'ianlapham/uniswap-optimism', 58 | }, 59 | }) 60 | 61 | const parsed = data?.indexingStatusForCurrentVersion 62 | 63 | if (loading) { 64 | return { 65 | available: null, 66 | syncedBlock: undefined, 67 | headBlock: undefined, 68 | } 69 | } 70 | 71 | if ((!loading && !parsed) || error) { 72 | return { 73 | available: false, 74 | syncedBlock: undefined, 75 | headBlock: undefined, 76 | } 77 | } 78 | 79 | const syncedBlock = parsed?.chains[0].latestBlock.number 80 | const headBlock = parsed?.chains[0].chainHeadBlock.number 81 | 82 | return { 83 | available: true, 84 | syncedBlock: syncedBlock ? parseFloat(syncedBlock) : undefined, 85 | headBlock: headBlock ? parseFloat(headBlock) : undefined, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/data/pools/topPools.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useQuery } from '@apollo/client' 3 | import gql from 'graphql-tag' 4 | import { useActiveNetworkVersion, useClients } from 'state/application/hooks' 5 | import { notEmpty } from 'utils' 6 | import { POOL_HIDE } from '../../constants' 7 | 8 | export const TOP_POOLS = gql` 9 | query topPools { 10 | pools(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc, subgraphError: allow) { 11 | id 12 | } 13 | } 14 | ` 15 | 16 | interface TopPoolsResponse { 17 | pools: { 18 | id: string 19 | }[] 20 | } 21 | 22 | /** 23 | * Fetch top addresses by volume 24 | */ 25 | export function useTopPoolAddresses(): { 26 | loading: boolean 27 | error: boolean 28 | addresses: string[] | undefined 29 | } { 30 | const [currentNetwork] = useActiveNetworkVersion() 31 | const { dataClient } = useClients() 32 | const { loading, error, data } = useQuery(TOP_POOLS, { 33 | client: dataClient, 34 | fetchPolicy: 'cache-first', 35 | }) 36 | 37 | const formattedData = useMemo(() => { 38 | if (data) { 39 | return data.pools 40 | .map((p) => { 41 | if (POOL_HIDE[currentNetwork.id].includes(p.id.toLocaleLowerCase())) { 42 | return undefined 43 | } 44 | return p.id 45 | }) 46 | .filter(notEmpty) 47 | } else { 48 | return undefined 49 | } 50 | }, [currentNetwork.id, data]) 51 | 52 | return { 53 | loading: loading, 54 | error: Boolean(error), 55 | addresses: formattedData, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/data/tokens/poolsForToken.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, NormalizedCacheObject } from '@apollo/client' 2 | import gql from 'graphql-tag' 3 | 4 | export const POOLS_FOR_TOKEN = gql` 5 | query topPools($address: Bytes!) { 6 | asToken0: pools( 7 | first: 200 8 | orderBy: totalValueLockedUSD 9 | orderDirection: desc 10 | where: { token0: $address } 11 | subgraphError: allow 12 | ) { 13 | id 14 | } 15 | asToken1: pools( 16 | first: 200 17 | orderBy: totalValueLockedUSD 18 | orderDirection: desc 19 | where: { token1: $address } 20 | subgraphError: allow 21 | ) { 22 | id 23 | } 24 | } 25 | ` 26 | 27 | interface PoolsForTokenResponse { 28 | asToken0: { 29 | id: string 30 | }[] 31 | asToken1: { 32 | id: string 33 | }[] 34 | } 35 | 36 | /** 37 | * Fetch top addresses by volume 38 | */ 39 | export async function fetchPoolsForToken( 40 | address: string, 41 | client: ApolloClient, 42 | ): Promise<{ 43 | loading: boolean 44 | error: boolean 45 | addresses: string[] | undefined 46 | }> { 47 | try { 48 | const { loading, error, data } = await client.query({ 49 | query: POOLS_FOR_TOKEN, 50 | variables: { 51 | address: address, 52 | }, 53 | fetchPolicy: 'cache-first', 54 | }) 55 | 56 | if (loading || error || !data) { 57 | return { 58 | loading, 59 | error: Boolean(error), 60 | addresses: undefined, 61 | } 62 | } 63 | 64 | const formattedData = data.asToken0.concat(data.asToken1).map((p) => p.id) 65 | 66 | return { 67 | loading, 68 | error: Boolean(error), 69 | addresses: formattedData, 70 | } 71 | } catch { 72 | return { 73 | loading: false, 74 | error: true, 75 | addresses: undefined, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/data/tokens/topTokens.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useQuery } from '@apollo/client' 3 | import gql from 'graphql-tag' 4 | import { useClients } from 'state/application/hooks' 5 | 6 | export const TOP_TOKENS = gql` 7 | query topPools { 8 | tokens(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc, subgraphError: allow) { 9 | id 10 | } 11 | } 12 | ` 13 | 14 | interface TopTokensResponse { 15 | tokens: { 16 | id: string 17 | }[] 18 | } 19 | 20 | /** 21 | * Fetch top addresses by volume 22 | */ 23 | export function useTopTokenAddresses(): { 24 | loading: boolean 25 | error: boolean 26 | addresses: string[] | undefined 27 | } { 28 | const { dataClient } = useClients() 29 | 30 | const { loading, error, data } = useQuery(TOP_TOKENS, { client: dataClient }) 31 | 32 | const formattedData = useMemo(() => { 33 | if (data) { 34 | return data.tokens.map((t) => t.id) 35 | } else { 36 | return undefined 37 | } 38 | }, [data]) 39 | 40 | return { 41 | loading: loading, 42 | error: Boolean(error), 43 | addresses: formattedData, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/hooks/chart.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { PoolChartEntry } from 'state/pools/reducer' 3 | import { TokenChartEntry } from 'state/tokens/reducer' 4 | import { ChartDayData, GenericChartEntry } from 'types' 5 | import { unixToDate } from 'utils/date' 6 | import dayjs from 'dayjs' 7 | 8 | function unixToType(unix: number, type: 'month' | 'week') { 9 | const date = dayjs.unix(unix).utc() 10 | 11 | switch (type) { 12 | case 'month': 13 | return date.format('YYYY-MM') 14 | case 'week': 15 | let week = String(date.week()) 16 | if (week.length === 1) { 17 | week = `0${week}` 18 | } 19 | return `${date.year()}-${week}` 20 | } 21 | } 22 | 23 | export function useTransformedVolumeData( 24 | chartData: ChartDayData[] | PoolChartEntry[] | TokenChartEntry[] | undefined, 25 | type: 'month' | 'week', 26 | ) { 27 | return useMemo(() => { 28 | if (chartData) { 29 | const data: Record = {} 30 | 31 | chartData.forEach(({ date, volumeUSD }: { date: number; volumeUSD: number }) => { 32 | const group = unixToType(date, type) 33 | if (data[group]) { 34 | data[group].value += volumeUSD 35 | } else { 36 | data[group] = { 37 | time: unixToDate(date), 38 | value: volumeUSD, 39 | } 40 | } 41 | }) 42 | 43 | return Object.values(data) 44 | } else { 45 | return [] 46 | } 47 | }, [chartData, type]) 48 | } 49 | -------------------------------------------------------------------------------- /src/hooks/useAppDispatch.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | import { AppDispatch, AppState } from 'state' 3 | 4 | export const useAppDispatch = () => useDispatch() 5 | export const useAppSelector: TypedUseSelectorHook = useSelector 6 | -------------------------------------------------------------------------------- /src/hooks/useCMCLink.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | // endpoint to check asset exists 4 | const cmcEndpoint = 'https://3rdparty-apis.coinmarketcap.com/v1/cryptocurrency/contract?address=' 5 | 6 | /** 7 | * Check if asset exists on CMC, if exists 8 | * return url, if not return undefined 9 | * @param address token address 10 | */ 11 | export function useCMCLink(address: string): string | undefined { 12 | const [link, setLink] = useState(undefined) 13 | 14 | useEffect(() => { 15 | async function fetchLink() { 16 | const result = await fetch(cmcEndpoint + address) 17 | // if link exists, format the url 18 | if (result.status === 200) { 19 | result.json().then(({ data }) => { 20 | setLink(data.url) 21 | }) 22 | } 23 | } 24 | if (address) { 25 | fetchLink() 26 | } 27 | }, [address]) 28 | 29 | return link 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useColor.ts: -------------------------------------------------------------------------------- 1 | import { useState, useLayoutEffect, useMemo } from 'react' 2 | import { shade } from 'polished' 3 | import Vibrant from 'node-vibrant' 4 | import { hex } from 'wcag-contrast' 5 | import { Token } from '@uniswap/sdk-core' 6 | import uriToHttp from 'utils/uriToHttp' 7 | import { isAddress } from 'utils' 8 | 9 | async function getColorFromToken(token: Token): Promise { 10 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${token.address}/logo.png` 11 | 12 | return Vibrant.from(path) 13 | .getPalette() 14 | .then((palette) => { 15 | if (palette?.Vibrant) { 16 | let detectedHex = palette.Vibrant.hex 17 | let AAscore = hex(detectedHex, '#FFF') 18 | while (AAscore < 3) { 19 | detectedHex = shade(0.005, detectedHex) 20 | AAscore = hex(detectedHex, '#FFF') 21 | } 22 | return detectedHex 23 | } 24 | return null 25 | }) 26 | .catch(() => null) 27 | } 28 | 29 | async function getColorFromUriPath(uri: string): Promise { 30 | const formattedPath = uriToHttp(uri)[0] 31 | 32 | return Vibrant.from(formattedPath) 33 | .getPalette() 34 | .then((palette) => { 35 | if (palette?.Vibrant) { 36 | return palette.Vibrant.hex 37 | } 38 | return null 39 | }) 40 | .catch(() => null) 41 | } 42 | 43 | export function useColor(address?: string) { 44 | const [color, setColor] = useState('#2172E5') 45 | 46 | const formattedAddress = isAddress(address) 47 | 48 | const token = useMemo(() => { 49 | return formattedAddress ? new Token(1, formattedAddress, 0) : undefined 50 | }, [formattedAddress]) 51 | 52 | useLayoutEffect(() => { 53 | let stale = false 54 | 55 | if (token) { 56 | getColorFromToken(token).then((tokenColor) => { 57 | if (!stale && tokenColor !== null) { 58 | setColor(tokenColor) 59 | } 60 | }) 61 | } 62 | 63 | return () => { 64 | stale = true 65 | setColor('#2172E5') 66 | } 67 | }, [token]) 68 | 69 | return color 70 | } 71 | 72 | export function useListColor(listImageUri?: string) { 73 | const [color, setColor] = useState('#2172E5') 74 | 75 | useLayoutEffect(() => { 76 | let stale = false 77 | 78 | if (listImageUri) { 79 | getColorFromUriPath(listImageUri).then((color) => { 80 | if (!stale && color !== null) { 81 | setColor(color) 82 | } 83 | }) 84 | } 85 | 86 | return () => { 87 | stale = true 88 | setColor('#2172E5') 89 | } 90 | }, [listImageUri]) 91 | 92 | return color 93 | } 94 | -------------------------------------------------------------------------------- /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: string) => { 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/useFetchListCallback.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@reduxjs/toolkit' 2 | import { TokenList } from '@uniswap/token-lists' 3 | import { useCallback } from 'react' 4 | import { fetchTokenList } from '../state/lists/actions' 5 | import getTokenList from '../utils/getTokenList' 6 | import { useAppDispatch } from './useAppDispatch' 7 | 8 | export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean) => Promise { 9 | const dispatch = useAppDispatch() 10 | 11 | // note: prevent dispatch if using for list search or unsupported list 12 | return useCallback( 13 | async (listUrl: string, sendDispatch = true) => { 14 | const requestId = nanoid() 15 | sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl })) 16 | return getTokenList(listUrl) 17 | .then((tokenList) => { 18 | sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })) 19 | return tokenList 20 | }) 21 | .catch((error) => { 22 | console.debug(`Failed to get list at url ${listUrl}`, error) 23 | sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message })) 24 | throw error 25 | }) 26 | }, 27 | [dispatch], 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/useHttpLocations.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import uriToHttp from '../utils/uriToHttp' 3 | 4 | export default function useHttpLocations(uri: string | undefined): string[] { 5 | return useMemo(() => { 6 | return uri ? uriToHttp(uri) : [] 7 | }, [uri]) 8 | } 9 | -------------------------------------------------------------------------------- /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 | ignoredNodes: Array> = [], 7 | ) { 8 | const handlerRef = useRef void)>(handler) 9 | 10 | useEffect(() => { 11 | handlerRef.current = handler 12 | }, [handler]) 13 | 14 | useEffect(() => { 15 | const handleClickOutside = (e: MouseEvent) => { 16 | const nodeClicked = node.current?.contains(e.target as Node) 17 | const ignoredNodeClicked = ignoredNodes.reduce( 18 | (reducer, val) => reducer || !!val.current?.contains(e.target as Node), 19 | false, 20 | ) 21 | 22 | if ((nodeClicked || ignoredNodeClicked) ?? false) { 23 | return 24 | } 25 | 26 | if (handlerRef.current) handlerRef.current() 27 | } 28 | 29 | document.addEventListener('mousedown', handleClickOutside) 30 | 31 | return () => { 32 | document.removeEventListener('mousedown', handleClickOutside) 33 | } 34 | }, [node, ignoredNodes]) 35 | } 36 | -------------------------------------------------------------------------------- /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/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from 'styled-components' 2 | import { useContext } from 'react' 3 | 4 | export default function useTheme() { 5 | return useContext(ThemeContext) 6 | } 7 | -------------------------------------------------------------------------------- /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 | import useParsedQueryString from './useParsedQueryString' 2 | 3 | export enum Version { 4 | v1 = 'v1', 5 | v2 = 'v2', 6 | } 7 | 8 | export const DEFAULT_VERSION: Version = Version.v2 9 | 10 | export default function useToggledVersion(): Version { 11 | const { use } = useParsedQueryString() 12 | if (!use || typeof use !== 'string') return Version.v2 13 | if (use.toLowerCase() === 'v1') return Version.v1 14 | return DEFAULT_VERSION 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const isClient = typeof window === 'object' 4 | 5 | function getSize() { 6 | return { 7 | width: isClient ? window.innerWidth : undefined, 8 | height: isClient ? window.innerHeight : undefined, 9 | } 10 | } 11 | 12 | // https://usehooks.com/useWindowSize/ 13 | export function useWindowSize() { 14 | const [windowSize, setWindowSize] = useState(getSize) 15 | 16 | useEffect(() => { 17 | function handleResize() { 18 | setWindowSize(getSize()) 19 | } 20 | 21 | if (isClient) { 22 | window.addEventListener('resize', handleResize) 23 | return () => { 24 | window.removeEventListener('resize', handleResize) 25 | } 26 | } 27 | return undefined 28 | }, []) 29 | 30 | return windowSize 31 | } 32 | -------------------------------------------------------------------------------- /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: `./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.tsx: -------------------------------------------------------------------------------- 1 | import 'inter-ui' 2 | import React, { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import { Provider } from 'react-redux' 5 | import { HashRouter } from 'react-router-dom' 6 | import './i18n' 7 | import App from './pages/App' 8 | import store from './state' 9 | import UserUpdater from './state/user/updater' 10 | import ProtocolUpdater from './state/protocol/updater' 11 | import TokenUpdater from './state/tokens/updater' 12 | import PoolUpdater from './state/pools/updater' 13 | import { OriginApplication, initializeAnalytics } from '@uniswap/analytics' 14 | import ApplicationUpdater from './state/application/updater' 15 | import ListUpdater from './state/lists/updater' 16 | import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme' 17 | import { ApolloProvider } from '@apollo/client/react' 18 | import { client } from 'apollo/client' 19 | import { SharedEventName } from '@uniswap/analytics-events' 20 | 21 | // Actual key is set by proxy server 22 | const AMPLITUDE_DUMMY_KEY = '00000000000000000000000000000000' 23 | initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INFO, { 24 | proxyUrl: process.env.REACT_APP_AMPLITUDE_PROXY_URL, 25 | defaultEventName: SharedEventName.PAGE_VIEWED, 26 | debug: true, 27 | }) 28 | 29 | function Updaters() { 30 | return ( 31 | <> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const container = document.getElementById('root') 43 | const root = createRoot(container!) 44 | root.render( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | , 59 | ) 60 | -------------------------------------------------------------------------------- /src/pages/Pool/PoolsOverview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react' 2 | import { PageWrapper } from 'pages/styled' 3 | import { AutoColumn } from 'components/Column' 4 | import { TYPE } from 'theme' 5 | import PoolTable from 'components/pools/PoolTable' 6 | import { useAllPoolData, usePoolDatas } from 'state/pools/hooks' 7 | import { notEmpty } from 'utils' 8 | import { useSavedPools } from 'state/user/hooks' 9 | import { DarkGreyCard } from 'components/Card' 10 | import { Trace } from '@uniswap/analytics' 11 | // import TopPoolMovers from 'components/pools/TopPoolMovers' 12 | 13 | export default function PoolPage() { 14 | useEffect(() => { 15 | window.scrollTo(0, 0) 16 | }, []) 17 | 18 | // get all the pool datas that exist 19 | const allPoolData = useAllPoolData() 20 | const poolDatas = useMemo(() => { 21 | return Object.values(allPoolData) 22 | .map((p) => p.data) 23 | .filter(notEmpty) 24 | }, [allPoolData]) 25 | 26 | const [savedPools] = useSavedPools() 27 | const watchlistPools = usePoolDatas(savedPools) 28 | 29 | return ( 30 | 31 | 32 | 33 | Your Watchlist 34 | {watchlistPools.length > 0 ? ( 35 | 36 | ) : ( 37 | 38 | Saved pools will appear here 39 | 40 | )} 41 | All Pools 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/Protocol/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Protocol() { 4 | return
5 | } 6 | -------------------------------------------------------------------------------- /src/pages/Token/TokensOverview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useEffect } from 'react' 2 | import { PageWrapper } from 'pages/styled' 3 | import { AutoColumn } from 'components/Column' 4 | import { TYPE, HideSmall } from 'theme' 5 | import TokenTable from 'components/tokens/TokenTable' 6 | import { useAllTokenData, useTokenDatas } from 'state/tokens/hooks' 7 | import { notEmpty } from 'utils' 8 | import { useSavedTokens } from 'state/user/hooks' 9 | import { DarkGreyCard } from 'components/Card' 10 | import TopTokenMovers from 'components/tokens/TopTokenMovers' 11 | import { Trace } from '@uniswap/analytics' 12 | 13 | export default function TokensOverview() { 14 | useEffect(() => { 15 | window.scrollTo(0, 0) 16 | }, []) 17 | 18 | const allTokens = useAllTokenData() 19 | 20 | const formattedTokens = useMemo(() => { 21 | return Object.values(allTokens) 22 | .map((t) => t.data) 23 | .filter(notEmpty) 24 | }, [allTokens]) 25 | 26 | const [savedTokens] = useSavedTokens() 27 | const watchListTokens = useTokenDatas(savedTokens) 28 | 29 | return ( 30 | 31 | 32 | 33 | Your Watchlist 34 | {savedTokens.length > 0 ? ( 35 | 36 | ) : ( 37 | 38 | Saved tokens will appear here 39 | 40 | )} 41 | 42 | 43 | 44 | Top Movers 45 | 46 | 47 | 48 | 49 | All Tokens 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/Token/redirects.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TokenPage from './TokenPage' 3 | import { isAddress } from 'ethers' 4 | import { Navigate, useParams } from 'react-router-dom' 5 | 6 | export function RedirectInvalidToken() { 7 | const { address } = useParams<{ address?: string }>() 8 | 9 | if (!address || !isAddress(address)) { 10 | return 11 | } 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/Wallets/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Wallets() { 4 | return
5 | } 6 | -------------------------------------------------------------------------------- /src/pages/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const PageWrapper = styled.div` 4 | width: 90%; 5 | ` 6 | 7 | export const ThemedBackground = styled.div<{ $backgroundColor: string }>` 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | pointer-events: none; 13 | max-width: 100vw !important; 14 | height: 200vh; 15 | mix-blend-mode: color; 16 | background: ${({ $backgroundColor }) => 17 | `radial-gradient(50% 50% at 50% 50%, ${$backgroundColor} 0%, rgba(255, 255, 255, 0) 100%)`}; 18 | transform: translateY(-176vh); 19 | ` 20 | 21 | export const ThemedBackgroundGlobal = styled.div<{ $backgroundColor: string }>` 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | pointer-events: none; 27 | max-width: 100vw !important; 28 | height: 200vh; 29 | mix-blend-mode: color; 30 | background: ${({ $backgroundColor }) => 31 | `radial-gradient(50% 50% at 50% 50%, ${$backgroundColor} 0%, rgba(255, 255, 255, 0) 100%)`}; 32 | transform: translateY(-150vh); 33 | ` 34 | -------------------------------------------------------------------------------- /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 | // set by the Coinbase Wallet mobile dapp browser 12 | isCoinbaseWallet?: true 13 | // set by the Brave browser when using built-in wallet 14 | isBraveWallet?: true 15 | // set by the MetaMask browser extension (also set by Brave browser when using built-in wallet) 16 | isMetaMask?: true 17 | // set by the Rabby browser extension 18 | isRabby?: true 19 | // set by the Trust Wallet browser extension 20 | isTrust?: true 21 | // set by the Ledger Extension Web 3 browser extension 22 | isLedgerConnect?: true 23 | on?: (...args: any[]) => void 24 | removeListener?: (...args: any[]) => void 25 | autoRefreshOnNetworkChange?: boolean 26 | } 27 | web3?: any 28 | } 29 | 30 | declare module 'content-hash' { 31 | declare function decode(x: string): string 32 | declare function getCodec(x: string): string 33 | } 34 | 35 | declare module 'multihashes' { 36 | declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array } 37 | declare function toB58String(hash: Uint8Array): string 38 | } 39 | -------------------------------------------------------------------------------- /src/state/application/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | import { TokenList } from '@uniswap/token-lists' 3 | import { NetworkInfo } from 'constants/networks' 4 | 5 | export type PopupContent = { 6 | listUpdate: { 7 | listUrl: string 8 | oldList: TokenList 9 | newList: TokenList 10 | auto: boolean 11 | } 12 | } 13 | 14 | export enum ApplicationModal { 15 | WALLET, 16 | SETTINGS, 17 | MENU, 18 | } 19 | 20 | export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('application/updateBlockNumber') 21 | export const setOpenModal = createAction('application/setOpenModal') 22 | export const addPopup = createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>( 23 | 'application/addPopup', 24 | ) 25 | export const removePopup = createAction<{ key: string }>('application/removePopup') 26 | export const updateSubgraphStatus = createAction<{ 27 | available: boolean | null 28 | syncedBlock: number | undefined 29 | headBlock: number | undefined 30 | }>('application/updateSubgraphStatus') 31 | export const updateActiveNetworkVersion = createAction<{ activeNetworkVersion: NetworkInfo }>( 32 | 'application/updateActiveNetworkVersion', 33 | ) 34 | -------------------------------------------------------------------------------- /src/state/application/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, nanoid } from '@reduxjs/toolkit' 2 | import { NetworkInfo } from 'constants/networks' 3 | import { 4 | addPopup, 5 | PopupContent, 6 | removePopup, 7 | updateBlockNumber, 8 | updateSubgraphStatus, 9 | ApplicationModal, 10 | setOpenModal, 11 | updateActiveNetworkVersion, 12 | } from './actions' 13 | import { EthereumNetworkInfo } from '../../constants/networks' 14 | 15 | type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }> 16 | 17 | export interface ApplicationState { 18 | readonly blockNumber: { readonly [chainId: number]: number } 19 | readonly popupList: PopupList 20 | readonly openModal: ApplicationModal | null 21 | readonly subgraphStatus: { 22 | available: boolean | null 23 | syncedBlock: number | undefined 24 | headBlock: number | undefined 25 | } 26 | readonly activeNetworkVersion: NetworkInfo 27 | } 28 | 29 | const initialState: ApplicationState = { 30 | blockNumber: {}, 31 | popupList: [], 32 | openModal: null, 33 | subgraphStatus: { 34 | available: null, 35 | syncedBlock: undefined, 36 | headBlock: undefined, 37 | }, 38 | activeNetworkVersion: EthereumNetworkInfo, 39 | } 40 | 41 | export default createReducer(initialState, (builder) => 42 | builder 43 | .addCase(updateBlockNumber, (state, action) => { 44 | const { chainId, blockNumber } = action.payload 45 | if (typeof state.blockNumber[chainId] !== 'number') { 46 | state.blockNumber[chainId] = blockNumber 47 | } else { 48 | state.blockNumber[chainId] = Math.max(blockNumber, state.blockNumber[chainId]) 49 | } 50 | }) 51 | .addCase(setOpenModal, (state, action) => { 52 | state.openModal = action.payload 53 | }) 54 | .addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000 } }) => { 55 | state.popupList = (key ? state.popupList.filter((popup) => popup.key !== key) : state.popupList).concat([ 56 | { 57 | key: key || nanoid(), 58 | show: true, 59 | content, 60 | removeAfterMs, 61 | }, 62 | ]) 63 | }) 64 | .addCase(removePopup, (state, { payload: { key } }) => { 65 | state.popupList.forEach((p) => { 66 | if (p.key === key) { 67 | p.show = false 68 | } 69 | }) 70 | }) 71 | .addCase(updateSubgraphStatus, (state, { payload: { available, syncedBlock, headBlock } }) => { 72 | state.subgraphStatus = { 73 | available, 74 | syncedBlock, 75 | headBlock, 76 | } 77 | }) 78 | .addCase(updateActiveNetworkVersion, (state, { payload: { activeNetworkVersion } }) => { 79 | state.activeNetworkVersion = activeNetworkVersion 80 | }), 81 | ) 82 | -------------------------------------------------------------------------------- /src/state/application/updater.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useSubgraphStatus } from './hooks' 3 | import { useFetchedSubgraphStatus } from '../../data/application' 4 | 5 | export default function Updater(): null { 6 | // subgraph status 7 | const [status, updateStatus] = useSubgraphStatus() 8 | const { available, syncedBlock: newSyncedBlock, headBlock } = useFetchedSubgraphStatus() 9 | 10 | const syncedBlock = status.syncedBlock 11 | 12 | useEffect(() => { 13 | if (status.available === null && available !== null) { 14 | updateStatus(available, syncedBlock, headBlock) 15 | } 16 | if (!status.syncedBlock || (status.syncedBlock !== newSyncedBlock && syncedBlock)) { 17 | updateStatus(status.available, newSyncedBlock, headBlock) 18 | } 19 | }, [available, headBlock, newSyncedBlock, status.available, status.syncedBlock, syncedBlock, updateStatus]) 20 | 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /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 lists from './lists/reducer' 8 | import protocol from './protocol/reducer' 9 | import tokens from './tokens/reducer' 10 | import pools from './pools/reducer' 11 | 12 | const PERSISTED_KEYS: string[] = ['user', 'lists'] 13 | 14 | const store = configureStore({ 15 | reducer: { 16 | application, 17 | user, 18 | lists, 19 | protocol, 20 | tokens, 21 | pools, 22 | }, 23 | middleware: [...getDefaultMiddleware({ thunk: false, immutableCheck: false }), save({ states: PERSISTED_KEYS })], 24 | preloadedState: load({ states: PERSISTED_KEYS }), 25 | }) 26 | 27 | store.dispatch(updateVersion()) 28 | 29 | export default store 30 | 31 | export type AppState = ReturnType 32 | export type AppDispatch = typeof store.dispatch 33 | -------------------------------------------------------------------------------- /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 | // add and remove from list options 14 | export const addList = createAction('lists/addList') 15 | export const removeList = createAction('lists/removeList') 16 | 17 | // select which lists to search across from loaded lists 18 | export const enableList = createAction('lists/enableList') 19 | export const disableList = createAction('lists/disableList') 20 | 21 | // versioning 22 | export const acceptListUpdate = createAction('lists/acceptListUpdate') 23 | export const rejectVersionUpdate = createAction('lists/rejectVersionUpdate') 24 | -------------------------------------------------------------------------------- /src/state/lists/wrappedTokenInfo.ts: -------------------------------------------------------------------------------- 1 | import { Currency, Token } from '@uniswap/sdk-core' 2 | import { Tags, TokenInfo, TokenList } from '@uniswap/token-lists' 3 | 4 | import { isAddress } from '../../utils' 5 | 6 | type TagDetails = Tags[keyof Tags] 7 | interface TagInfo extends TagDetails { 8 | id: string 9 | } 10 | /** 11 | * Token instances created from token info on a token list. 12 | */ 13 | export class WrappedTokenInfo implements Token { 14 | public readonly isNative = false 15 | public readonly isToken = true 16 | public readonly list: TokenList 17 | 18 | public readonly tokenInfo: TokenInfo 19 | 20 | constructor(tokenInfo: TokenInfo, list: TokenList) { 21 | this.tokenInfo = tokenInfo 22 | this.list = list 23 | } 24 | 25 | private _checksummedAddress: string | null = null 26 | 27 | public get address(): string { 28 | if (this._checksummedAddress) return this._checksummedAddress 29 | const checksummedAddress = isAddress(this.tokenInfo.address) 30 | if (!checksummedAddress) throw new Error(`Invalid token address: ${this.tokenInfo.address}`) 31 | return (this._checksummedAddress = checksummedAddress) 32 | } 33 | 34 | public get chainId(): number { 35 | return this.tokenInfo.chainId 36 | } 37 | 38 | public get decimals(): number { 39 | return this.tokenInfo.decimals 40 | } 41 | 42 | public get name(): string { 43 | return this.tokenInfo.name 44 | } 45 | 46 | public get symbol(): string { 47 | return this.tokenInfo.symbol 48 | } 49 | 50 | public get logoURI(): string | undefined { 51 | return this.tokenInfo.logoURI 52 | } 53 | 54 | private _tags: TagInfo[] | null = null 55 | public get tags(): TagInfo[] { 56 | if (this._tags !== null) return this._tags 57 | if (!this.tokenInfo.tags) return (this._tags = []) 58 | const listTags = this.list.tags 59 | if (!listTags) return (this._tags = []) 60 | 61 | return (this._tags = this.tokenInfo.tags.map((tagId) => { 62 | return { 63 | ...listTags[tagId], 64 | id: tagId, 65 | } 66 | })) 67 | } 68 | 69 | equals(other: Currency): boolean { 70 | return other.chainId === this.chainId && other.isToken && other.address.toLowerCase() === this.address.toLowerCase() 71 | } 72 | 73 | sortsBefore(other: Token): boolean { 74 | if (this.equals(other)) throw new Error('Addresses should not be equal') 75 | return this.address.toLowerCase() < other.address.toLowerCase() 76 | } 77 | 78 | public get wrapped(): Token { 79 | return this 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/state/pools/actions.ts: -------------------------------------------------------------------------------- 1 | import { TickProcessed } from './../../data/pools/tickData' 2 | import { createAction } from '@reduxjs/toolkit' 3 | import { PoolData, PoolChartEntry } from './reducer' 4 | import { Transaction } from 'types' 5 | import { SupportedNetwork } from 'constants/networks' 6 | 7 | // protocol wide info 8 | export const updatePoolData = createAction<{ pools: PoolData[]; networkId: SupportedNetwork }>('pools/updatePoolData') 9 | 10 | // add pool address to byAddress 11 | export const addPoolKeys = createAction<{ poolAddresses: string[]; networkId: SupportedNetwork }>('pool/addPoolKeys') 12 | 13 | export const updatePoolChartData = createAction<{ 14 | poolAddress: string 15 | chartData: PoolChartEntry[] 16 | networkId: SupportedNetwork 17 | }>('pool/updatePoolChartData') 18 | 19 | export const updatePoolTransactions = createAction<{ 20 | poolAddress: string 21 | transactions: Transaction[] 22 | networkId: SupportedNetwork 23 | }>('pool/updatePoolTransactions') 24 | 25 | export const updateTickData = createAction<{ 26 | poolAddress: string 27 | tickData: 28 | | { 29 | ticksProcessed: TickProcessed[] 30 | feeTier: string 31 | tickSpacing: number 32 | activeTickIdx: number 33 | } 34 | | undefined 35 | networkId: SupportedNetwork 36 | }>('pool/updateTickData') 37 | -------------------------------------------------------------------------------- /src/state/pools/updater.ts: -------------------------------------------------------------------------------- 1 | import { useUpdatePoolData, useAllPoolData, useAddPoolKeys } from './hooks' 2 | import { useEffect, useMemo } from 'react' 3 | import { useTopPoolAddresses } from 'data/pools/topPools' 4 | import { usePoolDatas } from 'data/pools/poolData' 5 | import { POOL_HIDE } from '../../constants' 6 | import { useActiveNetworkVersion } from 'state/application/hooks' 7 | 8 | export default function Updater(): null { 9 | // updaters 10 | const [currentNetwork] = useActiveNetworkVersion() 11 | const updatePoolData = useUpdatePoolData() 12 | const addPoolKeys = useAddPoolKeys() 13 | 14 | // data 15 | const allPoolData = useAllPoolData() 16 | const { loading, error, addresses } = useTopPoolAddresses() 17 | 18 | // add top pools on first load 19 | useEffect(() => { 20 | if (addresses && !error && !loading) { 21 | addPoolKeys(addresses) 22 | } 23 | }, [addPoolKeys, addresses, error, loading]) 24 | 25 | // load data for pools we need to hide 26 | useEffect(() => { 27 | addPoolKeys(POOL_HIDE[currentNetwork.id]) 28 | }, [addPoolKeys, currentNetwork.id]) 29 | 30 | // detect for which addresses we havent loaded pool data yet 31 | const unfetchedPoolAddresses = useMemo(() => { 32 | return Object.keys(allPoolData).reduce((accum: string[], key) => { 33 | const poolData = allPoolData[key] 34 | if (!poolData.data || !poolData.lastUpdated) { 35 | accum.push(key) 36 | } 37 | return accum 38 | }, []) 39 | }, [allPoolData]) 40 | 41 | // update unloaded pool entries with fetched data 42 | const { error: poolDataError, loading: poolDataLoading, data: poolDatas } = usePoolDatas(unfetchedPoolAddresses) 43 | 44 | useEffect(() => { 45 | if (poolDatas && !poolDataError && !poolDataLoading) { 46 | updatePoolData(Object.values(poolDatas)) 47 | } 48 | }, [poolDataError, poolDataLoading, poolDatas, updatePoolData]) 49 | 50 | return null 51 | } 52 | -------------------------------------------------------------------------------- /src/state/protocol/actions.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolData } from './reducer' 2 | import { createAction } from '@reduxjs/toolkit' 3 | import { ChartDayData, Transaction } from 'types' 4 | import { SupportedNetwork } from 'constants/networks' 5 | 6 | // protocol wide info 7 | export const updateProtocolData = createAction<{ protocolData: ProtocolData; networkId: SupportedNetwork }>( 8 | 'protocol/updateProtocolData', 9 | ) 10 | export const updateChartData = createAction<{ chartData: ChartDayData[]; networkId: SupportedNetwork }>( 11 | 'protocol/updateChartData', 12 | ) 13 | export const updateTransactions = createAction<{ transactions: Transaction[]; networkId: SupportedNetwork }>( 14 | 'protocol/updateTransactions', 15 | ) 16 | -------------------------------------------------------------------------------- /src/state/protocol/hooks.ts: -------------------------------------------------------------------------------- 1 | import { updateProtocolData, updateChartData, updateTransactions } from './actions' 2 | import { AppState, AppDispatch } from './../index' 3 | import { ProtocolData } from './reducer' 4 | import { useCallback } from 'react' 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import { ChartDayData, Transaction } from 'types' 7 | import { useActiveNetworkVersion } from 'state/application/hooks' 8 | 9 | export function useProtocolData(): [ProtocolData | undefined, (protocolData: ProtocolData) => void] { 10 | const [activeNetwork] = useActiveNetworkVersion() 11 | const protocolData: ProtocolData | undefined = useSelector( 12 | (state: AppState) => state.protocol[activeNetwork.id]?.data, 13 | ) 14 | 15 | const dispatch = useDispatch() 16 | const setProtocolData: (protocolData: ProtocolData) => void = useCallback( 17 | (protocolData: ProtocolData) => dispatch(updateProtocolData({ protocolData, networkId: activeNetwork.id })), 18 | [activeNetwork.id, dispatch], 19 | ) 20 | return [protocolData, setProtocolData] 21 | } 22 | 23 | export function useProtocolChartData(): [ChartDayData[] | undefined, (chartData: ChartDayData[]) => void] { 24 | const [activeNetwork] = useActiveNetworkVersion() 25 | const chartData: ChartDayData[] | undefined = useSelector( 26 | (state: AppState) => state.protocol[activeNetwork.id]?.chartData, 27 | ) 28 | 29 | const dispatch = useDispatch() 30 | const setChartData: (chartData: ChartDayData[]) => void = useCallback( 31 | (chartData: ChartDayData[]) => dispatch(updateChartData({ chartData, networkId: activeNetwork.id })), 32 | [activeNetwork.id, dispatch], 33 | ) 34 | return [chartData, setChartData] 35 | } 36 | 37 | export function useProtocolTransactions(): [Transaction[] | undefined, (transactions: Transaction[]) => void] { 38 | const [activeNetwork] = useActiveNetworkVersion() 39 | const transactions: Transaction[] | undefined = useSelector( 40 | (state: AppState) => state.protocol[activeNetwork.id]?.transactions, 41 | ) 42 | const dispatch = useDispatch() 43 | const setTransactions: (transactions: Transaction[]) => void = useCallback( 44 | (transactions: Transaction[]) => dispatch(updateTransactions({ transactions, networkId: activeNetwork.id })), 45 | [activeNetwork.id, dispatch], 46 | ) 47 | return [transactions, setTransactions] 48 | } 49 | -------------------------------------------------------------------------------- /src/state/protocol/reducer.ts: -------------------------------------------------------------------------------- 1 | import { currentTimestamp } from './../../utils/index' 2 | import { updateProtocolData, updateChartData, updateTransactions } from './actions' 3 | import { createReducer } from '@reduxjs/toolkit' 4 | import { ChartDayData, Transaction } from 'types' 5 | import { SupportedNetwork } from 'constants/networks' 6 | 7 | export interface ProtocolData { 8 | // volume 9 | volumeUSD: number 10 | volumeUSDChange: number 11 | 12 | // in range liquidity 13 | tvlUSD: number 14 | tvlUSDChange: number 15 | 16 | // fees 17 | feesUSD: number 18 | feeChange: number 19 | 20 | // transactions 21 | txCount: number 22 | txCountChange: number 23 | } 24 | 25 | export interface ProtocolState { 26 | [networkId: string]: { 27 | // timestamp for last updated fetch 28 | readonly lastUpdated: number | undefined 29 | // overview data 30 | readonly data: ProtocolData | undefined 31 | readonly chartData: ChartDayData[] | undefined 32 | readonly transactions: Transaction[] | undefined 33 | } 34 | } 35 | 36 | const DEFAULT_INITIAL_STATE = { 37 | data: undefined, 38 | chartData: undefined, 39 | transactions: undefined, 40 | lastUpdated: undefined, 41 | } 42 | 43 | export const initialState: ProtocolState = { 44 | [SupportedNetwork.ETHEREUM]: DEFAULT_INITIAL_STATE, 45 | [SupportedNetwork.ARBITRUM]: DEFAULT_INITIAL_STATE, 46 | [SupportedNetwork.OPTIMISM]: DEFAULT_INITIAL_STATE, 47 | [SupportedNetwork.POLYGON]: DEFAULT_INITIAL_STATE, 48 | [SupportedNetwork.CELO]: DEFAULT_INITIAL_STATE, 49 | [SupportedNetwork.BNB]: DEFAULT_INITIAL_STATE, 50 | [SupportedNetwork.AVALANCHE]: DEFAULT_INITIAL_STATE, 51 | [SupportedNetwork.BASE]: DEFAULT_INITIAL_STATE, 52 | } 53 | 54 | export default createReducer(initialState, (builder) => 55 | builder 56 | .addCase(updateProtocolData, (state, { payload: { protocolData, networkId } }) => { 57 | state[networkId].data = protocolData 58 | // mark when last updated 59 | state[networkId].lastUpdated = currentTimestamp() 60 | }) 61 | .addCase(updateChartData, (state, { payload: { chartData, networkId } }) => { 62 | state[networkId].chartData = chartData 63 | }) 64 | .addCase(updateTransactions, (state, { payload: { transactions, networkId } }) => { 65 | state[networkId].transactions = transactions 66 | }), 67 | ) 68 | -------------------------------------------------------------------------------- /src/state/protocol/updater.ts: -------------------------------------------------------------------------------- 1 | import { useProtocolData, useProtocolChartData, useProtocolTransactions } from './hooks' 2 | import { useEffect } from 'react' 3 | import { useFetchProtocolData } from 'data/protocol/overview' 4 | import { useFetchGlobalChartData } from 'data/protocol/chart' 5 | import { fetchTopTransactions } from 'data/protocol/transactions' 6 | import { useClients } from 'state/application/hooks' 7 | 8 | export default function Updater(): null { 9 | // client for data fetching 10 | const { dataClient } = useClients() 11 | 12 | const [protocolData, updateProtocolData] = useProtocolData() 13 | const { data: fetchedProtocolData, error, loading } = useFetchProtocolData() 14 | 15 | const [chartData, updateChartData] = useProtocolChartData() 16 | const { data: fetchedChartData, error: chartError } = useFetchGlobalChartData() 17 | 18 | const [transactions, updateTransactions] = useProtocolTransactions() 19 | 20 | // update overview data if available and not set 21 | useEffect(() => { 22 | if (protocolData === undefined && fetchedProtocolData && !loading && !error) { 23 | updateProtocolData(fetchedProtocolData) 24 | } 25 | }, [error, fetchedProtocolData, loading, protocolData, updateProtocolData]) 26 | 27 | // update global chart data if available and not set 28 | useEffect(() => { 29 | if (chartData === undefined && fetchedChartData && !chartError) { 30 | updateChartData(fetchedChartData) 31 | } 32 | }, [chartData, chartError, fetchedChartData, updateChartData]) 33 | 34 | useEffect(() => { 35 | async function fetch() { 36 | const data = await fetchTopTransactions(dataClient) 37 | if (data) { 38 | updateTransactions(data) 39 | } 40 | } 41 | if (!transactions) { 42 | fetch() 43 | } 44 | }, [transactions, updateTransactions, dataClient]) 45 | 46 | return null 47 | } 48 | -------------------------------------------------------------------------------- /src/state/tokens/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | import { TokenData, TokenChartEntry } from './reducer' 3 | import { PriceChartEntry, Transaction } from 'types' 4 | import { SupportedNetwork } from 'constants/networks' 5 | 6 | // protocol wide info 7 | export const updateTokenData = createAction<{ tokens: TokenData[]; networkId: SupportedNetwork }>( 8 | 'tokens/updateTokenData', 9 | ) 10 | 11 | // add token address to byAddress 12 | export const addTokenKeys = createAction<{ tokenAddresses: string[]; networkId: SupportedNetwork }>( 13 | 'tokens/addTokenKeys', 14 | ) 15 | 16 | // add list of pools token is in 17 | export const addPoolAddresses = createAction<{ 18 | tokenAddress: string 19 | poolAddresses: string[] 20 | networkId: SupportedNetwork 21 | }>('tokens/addPoolAddresses') 22 | 23 | // tvl and volume data over time 24 | export const updateChartData = createAction<{ 25 | tokenAddress: string 26 | chartData: TokenChartEntry[] 27 | networkId: SupportedNetwork 28 | }>('tokens/updateChartData') 29 | 30 | // transactions 31 | export const updateTransactions = createAction<{ 32 | tokenAddress: string 33 | transactions: Transaction[] 34 | networkId: SupportedNetwork 35 | }>('tokens/updateTransactions') 36 | 37 | // price data at arbitrary intervals 38 | export const updatePriceData = createAction<{ 39 | tokenAddress: string 40 | secondsInterval: number 41 | priceData: PriceChartEntry[] | undefined 42 | oldestFetchedTimestamp: number 43 | networkId: SupportedNetwork 44 | }>('tokens/updatePriceData') 45 | -------------------------------------------------------------------------------- /src/state/tokens/updater.ts: -------------------------------------------------------------------------------- 1 | import { useAllTokenData, useUpdateTokenData, useAddTokenKeys } from './hooks' 2 | import { useEffect, useMemo } from 'react' 3 | import { useTopTokenAddresses } from '../../data/tokens/topTokens' 4 | import { useFetchedTokenDatas } from 'data/tokens/tokenData' 5 | 6 | export default function Updater(): null { 7 | // updaters 8 | const updateTokenDatas = useUpdateTokenData() 9 | const addTokenKeys = useAddTokenKeys() 10 | 11 | // intitial data 12 | const allTokenData = useAllTokenData() 13 | const { loading, error, addresses } = useTopTokenAddresses() 14 | 15 | // add top pools on first load 16 | useEffect(() => { 17 | if (addresses && !error && !loading) { 18 | addTokenKeys(addresses) 19 | } 20 | }, [addTokenKeys, addresses, error, loading]) 21 | 22 | // detect for which addresses we havent loaded token data yet 23 | const unfetchedTokenAddresses = useMemo(() => { 24 | return Object.keys(allTokenData).reduce((accum: string[], key) => { 25 | const tokenData = allTokenData[key] 26 | if (!tokenData || !tokenData.data || !tokenData.lastUpdated) { 27 | accum.push(key) 28 | } 29 | return accum 30 | }, []) 31 | }, [allTokenData]) 32 | 33 | // update unloaded pool entries with fetched data 34 | const { 35 | error: tokenDataError, 36 | loading: tokenDataLoading, 37 | data: tokenDatas, 38 | } = useFetchedTokenDatas(unfetchedTokenAddresses) 39 | 40 | useEffect(() => { 41 | if (tokenDatas && !tokenDataError && !tokenDataLoading) { 42 | updateTokenDatas(Object.values(tokenDatas)) 43 | } 44 | }, [tokenDataError, tokenDataLoading, tokenDatas, updateTokenDatas]) 45 | 46 | return null 47 | } 48 | -------------------------------------------------------------------------------- /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 addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken') 19 | export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken') 20 | export const addSavedToken = createAction<{ address: string }>('user/addSavedToken') 21 | export const addSavedPool = createAction<{ address: string }>('user/addSavedPool') 22 | export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair') 23 | export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>( 24 | 'user/removeSerializedPair', 25 | ) 26 | export const toggleURLWarning = createAction('app/toggleURLWarning') 27 | -------------------------------------------------------------------------------- /src/state/user/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { Token } from '@uniswap/sdk-core' 2 | import { useCallback } from 'react' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { AppDispatch, AppState } from '../index' 5 | import { 6 | addSerializedToken, 7 | removeSerializedToken, 8 | SerializedToken, 9 | updateUserDarkMode, 10 | toggleURLWarning, 11 | addSavedToken, 12 | addSavedPool, 13 | } from './actions' 14 | 15 | function serializeToken(token: Token): SerializedToken { 16 | return { 17 | chainId: token.chainId, 18 | address: token.address, 19 | decimals: token.decimals, 20 | symbol: token.symbol, 21 | name: token.name, 22 | } 23 | } 24 | 25 | export function useIsDarkMode(): boolean { 26 | return true 27 | } 28 | 29 | export function useDarkModeManager(): [boolean, () => void] { 30 | const dispatch = useDispatch() 31 | const darkMode = true 32 | 33 | const toggleSetDarkMode = useCallback(() => { 34 | dispatch(updateUserDarkMode({ userDarkMode: !darkMode })) 35 | }, [darkMode, dispatch]) 36 | 37 | return [darkMode, toggleSetDarkMode] 38 | } 39 | 40 | export function useAddUserToken(): (token: Token) => void { 41 | const dispatch = useDispatch() 42 | return useCallback( 43 | (token: Token) => { 44 | dispatch(addSerializedToken({ serializedToken: serializeToken(token) })) 45 | }, 46 | [dispatch], 47 | ) 48 | } 49 | 50 | export function useSavedTokens(): [string[], (address: string) => void] { 51 | const dispatch = useDispatch() 52 | const savedTokens = useSelector((state: AppState) => state.user.savedTokens) 53 | const updatedSavedTokens = useCallback( 54 | (address: string) => { 55 | dispatch(addSavedToken({ address })) 56 | }, 57 | [dispatch], 58 | ) 59 | return [savedTokens ?? [], updatedSavedTokens] 60 | } 61 | 62 | export function useSavedPools(): [string[], (address: string) => void] { 63 | const dispatch = useDispatch() 64 | const savedPools = useSelector((state: AppState) => state.user.savedPools) 65 | const updateSavedPools = useCallback( 66 | (address: string) => { 67 | dispatch(addSavedPool({ address })) 68 | }, 69 | [dispatch], 70 | ) 71 | return [savedPools ?? [], updateSavedPools] 72 | } 73 | 74 | export function useRemoveUserAddedToken(): (chainId: number, address: string) => void { 75 | const dispatch = useDispatch() 76 | return useCallback( 77 | (chainId: number, address: string) => { 78 | dispatch(removeSerializedToken({ chainId, address })) 79 | }, 80 | [dispatch], 81 | ) 82 | } 83 | 84 | export function useURLWarningVisible(): boolean { 85 | return useSelector((state: AppState) => state.user.URLWarningVisible) 86 | } 87 | 88 | export function useURLWarningToggle(): () => void { 89 | const dispatch = useDispatch() 90 | return useCallback(() => dispatch(toggleURLWarning()), [dispatch]) 91 | } 92 | -------------------------------------------------------------------------------- /src/state/user/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'redux' 2 | import { updateVersion } from '../global/actions' 3 | import reducer, { initialState, UserState } from './reducer' 4 | 5 | describe('swap reducer', () => { 6 | let store: Store 7 | 8 | beforeEach(() => { 9 | store = createStore(reducer, initialState) 10 | }) 11 | 12 | describe('updateVersion', () => { 13 | it('has no timestamp originally', () => { 14 | expect(store.getState().lastUpdateVersionTimestamp).toBeUndefined() 15 | }) 16 | it('sets the lastUpdateVersionTimestamp', () => { 17 | const time = new Date().getTime() 18 | store.dispatch(updateVersion()) 19 | expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /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 { useLocation } 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(): null { 9 | const dispatch = useDispatch() 10 | const { search } = useLocation() 11 | 12 | useEffect(() => { 13 | if (!search) return 14 | if (search.length < 2) return 15 | 16 | const parsed = parse(search, { 17 | parseArrays: false, 18 | ignoreQueryPrefix: true, 19 | }) 20 | 21 | const theme = parsed.theme 22 | 23 | if (typeof theme !== 'string') return 24 | 25 | if (theme.toLowerCase() === 'light') { 26 | dispatch(updateUserDarkMode({ userDarkMode: false })) 27 | } else if (theme.toLowerCase() === 'dark') { 28 | dispatch(updateUserDarkMode({ userDarkMode: true })) 29 | } 30 | }, [dispatch, search]) 31 | 32 | return null 33 | } 34 | -------------------------------------------------------------------------------- /src/theme/rebass.d.ts: -------------------------------------------------------------------------------- 1 | import { InterpolationWithTheme } from '@emotion/core' 2 | import { 3 | BoxProps as BoxP, 4 | ButtonProps as ButtonP, 5 | FlexProps as FlexP, 6 | LinkProps as LinkP, 7 | TextProps as TextP, 8 | } from 'rebass' 9 | 10 | declare module 'rebass' { 11 | interface BoxProps extends BoxP { 12 | css?: InterpolationWithTheme 13 | } 14 | interface ButtonProps extends ButtonP { 15 | css?: InterpolationWithTheme 16 | } 17 | interface FlexProps extends FlexP { 18 | css?: InterpolationWithTheme 19 | } 20 | interface LinkProps extends LinkP { 21 | css?: InterpolationWithTheme 22 | } 23 | interface TextProps extends TextP { 24 | css?: InterpolationWithTheme 25 | } 26 | } 27 | 28 | declare global { 29 | namespace JSX { 30 | interface IntrinsicAttributes { 31 | css?: InterpolationWithTheme 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | bg0: Color 18 | bg1: Color 19 | bg2: Color 20 | bg3: Color 21 | bg4: Color 22 | bg5: Color 23 | 24 | modalBG: Color 25 | advancedBG: Color 26 | 27 | //blues 28 | primary1: Color 29 | primary2: Color 30 | primary3: Color 31 | primary4: Color 32 | primary5: Color 33 | 34 | primaryText1: Color 35 | 36 | // pinks 37 | secondary1: Color 38 | secondary2: Color 39 | secondary3: Color 40 | 41 | // other 42 | pink1: Color 43 | red1: Color 44 | red2: Color 45 | red3: Color 46 | green1: Color 47 | yellow1: Color 48 | yellow2: Color 49 | yellow3: Color 50 | blue1: Color 51 | blue2: Color 52 | } 53 | 54 | export interface Grids { 55 | sm: number 56 | md: number 57 | lg: number 58 | } 59 | 60 | declare module 'styled-components' { 61 | export interface DefaultTheme extends Colors { 62 | grids: Grids 63 | 64 | // shadows 65 | shadow1: string 66 | 67 | // media queries 68 | mediaWidth: { 69 | upToExtraSmall: ThemedCssFunction 70 | upToSmall: ThemedCssFunction 71 | upToMedium: ThemedCssFunction 72 | upToLarge: ThemedCssFunction 73 | } 74 | 75 | // css snippets 76 | flexColumnNoWrap: FlattenSimpleInterpolation 77 | flexRowNoWrap: FlattenSimpleInterpolation 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Block { 2 | number: number 3 | timestamp: string 4 | } 5 | 6 | export enum VolumeWindow { 7 | daily, 8 | weekly, 9 | monthly, 10 | } 11 | 12 | export interface ChartDayData { 13 | date: number 14 | volumeUSD: number 15 | tvlUSD: number 16 | } 17 | 18 | export interface GenericChartEntry { 19 | time: string 20 | value: number 21 | } 22 | 23 | export enum TransactionType { 24 | SWAP, 25 | MINT, 26 | BURN, 27 | } 28 | 29 | export type Transaction = { 30 | type: TransactionType 31 | hash: string 32 | timestamp: string 33 | sender: string 34 | token0Symbol: string 35 | token1Symbol: string 36 | token0Address: string 37 | token1Address: string 38 | amountUSD: number 39 | amountToken0: number 40 | amountToken1: number 41 | } 42 | 43 | /** 44 | * Formatted type for Candlestick charts 45 | */ 46 | export type PriceChartEntry = { 47 | time: number // unix timestamp 48 | open: number 49 | close: number 50 | high: number 51 | low: number 52 | } 53 | -------------------------------------------------------------------------------- /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('uniswap.eth 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 } from '@uniswap/sdk-core' 2 | 3 | export function currencyId(currency: Currency): string { 4 | if (currency.isNative) return 'ETH' 5 | if (currency.isToken) return currency.address 6 | throw new Error('invalid currency') 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * gets the amoutn difference plus the % change in change itself (second order change) 3 | * @param {*} valueNow 4 | * @param {*} value24HoursAgo 5 | * @param {*} value48HoursAgo 6 | */ 7 | export const get2DayChange = (valueNow: string, value24HoursAgo: string, value48HoursAgo: string): [number, number] => { 8 | // get volume info for both 24 hour periods 9 | const currentChange = parseFloat(valueNow) - parseFloat(value24HoursAgo) 10 | const previousChange = parseFloat(value24HoursAgo) - parseFloat(value48HoursAgo) 11 | const adjustedPercentChange = ((currentChange - previousChange) / previousChange) * 100 12 | if (isNaN(adjustedPercentChange) || !isFinite(adjustedPercentChange)) { 13 | return [currentChange, 0] 14 | } 15 | return [currentChange, adjustedPercentChange] 16 | } 17 | 18 | /** 19 | * get standard percent change between two values 20 | * @param {*} valueNow 21 | * @param {*} value24HoursAgo 22 | */ 23 | export const getPercentChange = (valueNow: string | undefined, value24HoursAgo: string | undefined): number => { 24 | if (valueNow && value24HoursAgo) { 25 | const change = ((parseFloat(valueNow) - parseFloat(value24HoursAgo)) / parseFloat(value24HoursAgo)) * 100 26 | if (isFinite(change)) return change 27 | } 28 | return 0 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export function unixToDate(unix: number, format = 'YYYY-MM-DD'): string { 4 | return dayjs.unix(unix).utc().format(format) 5 | } 6 | 7 | export const formatTime = (unix: string, buffer?: number) => { 8 | const now = dayjs() 9 | const timestamp = dayjs.unix(parseInt(unix)).add(buffer ?? 0, 'minute') 10 | 11 | const inSeconds = now.diff(timestamp, 'second') 12 | const inMinutes = now.diff(timestamp, 'minute') 13 | const inHours = now.diff(timestamp, 'hour') 14 | const inDays = now.diff(timestamp, 'day') 15 | 16 | if (inMinutes < 1) { 17 | return 'recently' 18 | } 19 | 20 | if (inHours >= 24) { 21 | return `${inDays} ${inDays === 1 ? 'day' : 'days'} ago` 22 | } else if (inMinutes >= 60) { 23 | return `${inHours} ${inHours === 1 ? 'hour' : 'hours'} ago` 24 | } else if (inSeconds >= 60) { 25 | return `${inMinutes} ${inMinutes === 1 ? 'minute' : 'minutes'} ago` 26 | } else { 27 | return `${inSeconds} ${inSeconds === 1 ? 'second' : 'seconds'} ago` 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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, 'any') 5 | library.pollingInterval = 15000 6 | return library 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/getTokenList.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@uniswap/token-lists' 2 | import schema from '@uniswap/token-lists/src/tokenlist.schema.json' 3 | import Ajv from 'ajv' 4 | import uriToHttp from './uriToHttp' 5 | 6 | const tokenListValidator = new Ajv({ allErrors: true }).compile(schema) 7 | 8 | /** 9 | * Contains the logic for resolving a list URL to a validated token list 10 | * @param listUrl list url 11 | * @param resolveENSContentHash resolves an ens name to a contenthash 12 | */ 13 | export default async function getTokenList(listUrl: string): Promise { 14 | const urls = uriToHttp(listUrl) 15 | 16 | for (let i = 0; i < urls.length; i++) { 17 | const url = urls[i] 18 | const isLast = i === urls.length - 1 19 | let response 20 | try { 21 | response = await fetch(url) 22 | } catch (error) { 23 | console.debug('Failed to fetch list', listUrl, error) 24 | if (isLast) throw new Error(`Failed to download list ${listUrl}`) 25 | continue 26 | } 27 | 28 | if (!response.ok) { 29 | if (isLast) throw new Error(`Failed to download list ${listUrl}`) 30 | continue 31 | } 32 | 33 | const json = await response.json() 34 | if (!tokenListValidator(json)) { 35 | const validationErrors: string = 36 | tokenListValidator.errors?.reduce((memo, error) => { 37 | const add = `${error.dataPath} ${error.message ?? ''}` 38 | return memo.length > 0 ? `${memo}; ${add}` : `${add}` 39 | }, '') ?? 'unknown error' 40 | throw new Error(`Token list failed validation: ${validationErrors}`) 41 | } 42 | return json 43 | } 44 | throw new Error('Unrecognized list URL protocol.') 45 | } 46 | -------------------------------------------------------------------------------- /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/listSort.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LIST_OF_LISTS } from './../constants/lists' 2 | 3 | // use ordering of default list of lists to assign priority 4 | export default function sortByListPriority(urlA: string, urlB: string) { 5 | const first = DEFAULT_LIST_OF_LISTS.includes(urlA) ? DEFAULT_LIST_OF_LISTS.indexOf(urlA) : Number.MAX_SAFE_INTEGER 6 | const second = DEFAULT_LIST_OF_LISTS.includes(urlB) ? DEFAULT_LIST_OF_LISTS.indexOf(urlB) : Number.MAX_SAFE_INTEGER 7 | 8 | // need reverse order to make sure mapping includes top priority last 9 | if (first < second) return 1 10 | else if (first > second) return -1 11 | return 0 12 | } 13 | -------------------------------------------------------------------------------- /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/networkPrefix.ts: -------------------------------------------------------------------------------- 1 | import { EthereumNetworkInfo, NetworkInfo } from 'constants/networks' 2 | 3 | export function networkPrefix(activeNewtork: NetworkInfo) { 4 | const isEthereum = activeNewtork === EthereumNetworkInfo 5 | if (isEthereum) { 6 | return '/' 7 | } 8 | const prefix = '/' + activeNewtork.route.toLocaleLowerCase() + '/' 9 | return prefix 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | import numbro from 'numbro' 2 | 3 | // using a currency library here in case we want to add more in future 4 | export const formatDollarAmount = (num: number | undefined, digits = 2, round = true) => { 5 | if (num === 0) return '$0.00' 6 | if (!num) return '-' 7 | if (num < 0.001 && digits <= 3) { 8 | return '<$0.001' 9 | } 10 | 11 | return numbro(num).formatCurrency({ 12 | average: round, 13 | mantissa: num > 1000 ? 2 : digits, 14 | abbreviations: { 15 | million: 'M', 16 | billion: 'B', 17 | }, 18 | }) 19 | } 20 | 21 | // using a currency library here in case we want to add more in future 22 | export const formatAmount = (num: number | undefined, digits = 2) => { 23 | if (num === 0) return '0' 24 | if (!num) return '-' 25 | if (num < 0.001) { 26 | return '<0.001' 27 | } 28 | return numbro(num).format({ 29 | average: true, 30 | mantissa: num > 1000 ? 2 : digits, 31 | abbreviations: { 32 | million: 'M', 33 | billion: 'B', 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /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 | expect(parseENSAddress('hello-world.eth')).toEqual({ ensName: 'hello-world.eth', ensPath: undefined }) 14 | expect(parseENSAddress('-prefix-dash.eth')).toEqual(undefined) 15 | expect(parseENSAddress('suffix-dash-.eth')).toEqual(undefined) 16 | expect(parseENSAddress('it.eth')).toEqual({ ensName: 'it.eth', ensPath: undefined }) 17 | expect(parseENSAddress('only-single--dash.eth')).toEqual(undefined) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/utils/parseENSAddress.ts: -------------------------------------------------------------------------------- 1 | const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+(-[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[4] } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/queries.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, NormalizedCacheObject } from '@apollo/client' 2 | import dayjs from 'dayjs' 3 | 4 | /** 5 | * Used to get large amounts of data when 6 | * @param query 7 | * @param localClient 8 | * @param vars - any variables that are passed in every query 9 | * @param values - the keys that are used as the values to map over if 10 | * @param skipCount - amount of entities to skip per query 11 | */ 12 | export async function splitQuery( 13 | query: any, 14 | client: ApolloClient, 15 | vars: any[], 16 | values: any[], 17 | skipCount = 1000, 18 | ) { 19 | let fetchedData = {} as Type 20 | let allFound = false 21 | let skip = 0 22 | try { 23 | while (!allFound) { 24 | let end = values.length 25 | if (skip + skipCount < values.length) { 26 | end = skip + skipCount 27 | } 28 | const sliced = values.slice(skip, end) 29 | const result = await client.query({ 30 | query: query(...vars, sliced), 31 | fetchPolicy: 'network-only', 32 | }) 33 | fetchedData = { 34 | ...fetchedData, 35 | ...result.data, 36 | } 37 | if (Object.keys(result.data).length < skipCount || skip + skipCount > values.length) { 38 | allFound = true 39 | } else { 40 | skip += skipCount 41 | } 42 | } 43 | return fetchedData 44 | } catch (e) { 45 | console.log(e) 46 | return undefined 47 | } 48 | } 49 | 50 | export function useDeltaTimestamps(): [number, number, number] { 51 | const utcCurrentTime = dayjs() 52 | const t1 = utcCurrentTime.subtract(1, 'day').startOf('minute').unix() 53 | const t2 = utcCurrentTime.subtract(2, 'day').startOf('minute').unix() 54 | const tWeek = utcCurrentTime.subtract(1, 'week').startOf('minute').unix() 55 | return [t1, t2, tWeek] 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/resolveENSContentHash.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '@ethersproject/contracts' 2 | import { Provider } from '@ethersproject/abstract-provider' 3 | import { namehash } from 'ethers' 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 | constructor() { 14 | super('Cancelled') 15 | } 16 | } 17 | 18 | /** 19 | * Throw this error if the function should retry 20 | */ 21 | export class RetryableError extends Error {} 22 | 23 | /** 24 | * Retries the function that returns the promise until the promise successfully resolves up to n retries 25 | * @param fn function to retry 26 | * @param n how many times to retry 27 | * @param minWait min wait between retries in ms 28 | * @param maxWait max wait between retries in ms 29 | */ 30 | export function retry( 31 | fn: () => Promise, 32 | { n, minWait, maxWait }: { n: number; minWait: number; maxWait: number }, 33 | ): { promise: Promise; cancel: () => void } { 34 | let completed = false 35 | let rejectCancelled: (error: Error) => void 36 | const promise = new Promise(async (resolve, reject) => { 37 | rejectCancelled = reject 38 | while (true) { 39 | let result: T 40 | try { 41 | result = await fn() 42 | if (!completed) { 43 | resolve(result) 44 | completed = true 45 | } 46 | break 47 | } catch (error) { 48 | if (completed) { 49 | break 50 | } 51 | if (n <= 0 || !(error instanceof RetryableError)) { 52 | reject(error) 53 | completed = true 54 | break 55 | } 56 | n-- 57 | } 58 | await waitRandom(minWait, maxWait) 59 | } 60 | }) 61 | return { 62 | promise, 63 | cancel: () => { 64 | if (completed) return 65 | completed = true 66 | rejectCancelled(new CancelledError()) 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '@uniswap/sdk-core' 2 | import { CeloNetworkInfo, NetworkInfo, PolygonNetworkInfo } from 'constants/networks' 3 | import { CELO_ADDRESS, MATIC_ADDRESS, WETH_ADDRESSES } from '../constants' 4 | 5 | export interface SerializedToken { 6 | chainId: number 7 | address: string 8 | decimals: number 9 | symbol?: string 10 | name?: string 11 | } 12 | 13 | export function serializeToken(token: Token): SerializedToken { 14 | return { 15 | chainId: token.chainId, 16 | address: token.address, 17 | decimals: token.decimals, 18 | symbol: token.symbol, 19 | name: token.name, 20 | } 21 | } 22 | 23 | export function formatTokenSymbol(address: string, symbol: string, activeNetwork?: NetworkInfo) { 24 | // dumb catch for matic 25 | if (address === MATIC_ADDRESS && activeNetwork === PolygonNetworkInfo) { 26 | return 'MATIC' 27 | } 28 | 29 | // dumb catch for Celo 30 | if (address === CELO_ADDRESS && activeNetwork === CeloNetworkInfo) { 31 | return 'CELO' 32 | } 33 | 34 | if (WETH_ADDRESSES.includes(address)) { 35 | return 'ETH' 36 | } 37 | return symbol 38 | } 39 | 40 | export function formatTokenName(address: string, name: string, activeNetwork?: NetworkInfo) { 41 | // dumb catch for matic 42 | if (address === MATIC_ADDRESS && activeNetwork === PolygonNetworkInfo) { 43 | return 'MATIC' 44 | } 45 | 46 | // dumb catch for Celo 47 | if (address === CELO_ADDRESS && activeNetwork === CeloNetworkInfo) { 48 | return 'CELO' 49 | } 50 | 51 | if (WETH_ADDRESSES.includes(address)) { 52 | return 'Ether' 53 | } 54 | return name 55 | } 56 | -------------------------------------------------------------------------------- /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 ipns gateways for ipns:// urls', () => { 20 | expect(uriToHttp('ipns://app.uniswap.org')).toEqual([ 21 | 'https://cloudflare-ipfs.com/ipns/app.uniswap.org/', 22 | 'https://ipfs.io/ipns/app.uniswap.org/', 23 | ]) 24 | }) 25 | it('returns empty array for invalid scheme', () => { 26 | expect(uriToHttp('blah:test')).toEqual([]) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "noImplicitReturns": true, 23 | "moduleResolution": "node", 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "jsx": "react-jsx", 27 | "downlevelIteration": true, 28 | "allowSyntheticDefaultImports": true, 29 | "types": [ 30 | "react-spring", 31 | "jest" 32 | ], 33 | "baseUrl": "src" 34 | }, 35 | "exclude": [ 36 | "node_modules", 37 | "cypress" 38 | ], 39 | "include": [ 40 | "./src/**/*.ts", 41 | "./src/**/*.tsx", 42 | "src/components/Confetti/index.js" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------