├── .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 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/arrow-down-grey.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/bnb-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/src/assets/images/celo-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/src/assets/images/circle-grey.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/circle.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/dropdown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/dropup-blue.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/plus-blue.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/plus-grey.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/lightcircle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/optimism-plain.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
{
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 |
104 |
108 |
112 |
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 |
--------------------------------------------------------------------------------