├── .env
├── .env.production
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── .yarnrc
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── integration
│ ├── add-liquidity.test.ts
│ ├── landing.test.ts
│ ├── lists.test.ts
│ ├── migrate-v1.test.ts
│ ├── pool.test.ts
│ ├── remove-liquidity.test.ts
│ ├── send.test.ts
│ ├── swap.test.ts
│ └── token-warning.ts
├── support
│ ├── commands.d.ts
│ ├── commands.js
│ └── index.js
└── tsconfig.json
├── package.json
├── public
├── 451.html
├── favicon.ico
├── 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
├── src
├── assets
│ ├── images
│ │ ├── arrow-down-blue.svg
│ │ ├── arrow-down-grey.svg
│ │ ├── arrow-right-white.png
│ │ ├── arrow-right.svg
│ │ ├── big_unicorn.png
│ │ ├── blue-loader.svg
│ │ ├── circle-grey.svg
│ │ ├── circle.svg
│ │ ├── coinbaseWalletIcon.svg
│ │ ├── dropdown-blue.svg
│ │ ├── dropdown.svg
│ │ ├── dropup-blue.svg
│ │ ├── ethereum-logo.png
│ │ ├── fortmaticIcon.png
│ │ ├── link.svg
│ │ ├── magnifying-glass.svg
│ │ ├── menu.svg
│ │ ├── metamask.png
│ │ ├── noise.png
│ │ ├── plus-blue.svg
│ │ ├── plus-grey.svg
│ │ ├── portisIcon.png
│ │ ├── question-mark.svg
│ │ ├── question.svg
│ │ ├── spinner.svg
│ │ ├── token-list
│ │ │ ├── lists-dark.png
│ │ │ └── lists-light.png
│ │ ├── token-logo.png
│ │ ├── trustWallet.png
│ │ ├── walletConnectIcon.svg
│ │ ├── x.svg
│ │ └── xl_uni.png
│ └── svg
│ │ ├── QR.svg
│ │ ├── lightcircle.svg
│ │ ├── logo.png
│ │ ├── wordmark.svg
│ │ ├── wordmark_pink.svg
│ │ └── wordmark_white.svg
├── components
│ ├── AccountDetails
│ │ ├── Copy.tsx
│ │ ├── Transaction.tsx
│ │ └── index.tsx
│ ├── AddressInputPanel
│ │ └── index.tsx
│ ├── Button
│ │ └── index.tsx
│ ├── Card
│ │ └── index.tsx
│ ├── Column
│ │ └── index.tsx
│ ├── Confetti
│ │ └── index.tsx
│ ├── CurrencyInputPanel
│ │ └── index.tsx
│ ├── CurrencyLogo
│ │ └── index.tsx
│ ├── DoubleLogo
│ │ └── index.tsx
│ ├── FormattedCurrencyAmount
│ │ └── index.tsx
│ ├── Header
│ │ ├── Polling.tsx
│ │ ├── URLWarning.tsx
│ │ ├── UniBalanceContent.tsx
│ │ └── index.tsx
│ ├── Identicon
│ │ └── index.tsx
│ ├── ListLogo
│ │ └── index.tsx
│ ├── Loader
│ │ └── index.tsx
│ ├── Logo
│ │ └── index.tsx
│ ├── Menu
│ │ └── index.tsx
│ ├── Modal
│ │ └── index.tsx
│ ├── ModalViews
│ │ └── index.tsx
│ ├── NavigationTabs
│ │ └── index.tsx
│ ├── NumericalInput
│ │ └── index.tsx
│ ├── Popover
│ │ └── index.tsx
│ ├── Popups
│ │ ├── ClaimPopup.tsx
│ │ ├── ListUpdatePopup.tsx
│ │ ├── PopupItem.tsx
│ │ ├── TransactionPopup.tsx
│ │ └── index.tsx
│ ├── PositionCard
│ │ ├── V1.tsx
│ │ └── index.tsx
│ ├── ProgressSteps
│ │ └── index.tsx
│ ├── QuestionHelper
│ │ └── index.tsx
│ ├── Row
│ │ └── index.tsx
│ ├── SearchModal
│ │ ├── CommonBases.tsx
│ │ ├── CurrencyList.tsx
│ │ ├── CurrencySearch.tsx
│ │ ├── CurrencySearchModal.tsx
│ │ ├── ListSelect.tsx
│ │ ├── SortButton.tsx
│ │ ├── filtering.ts
│ │ ├── sorting.ts
│ │ └── styleds.tsx
│ ├── Settings
│ │ └── index.tsx
│ ├── Slider
│ │ └── index.tsx
│ ├── Toggle
│ │ └── index.tsx
│ ├── TokenWarningModal
│ │ └── index.tsx
│ ├── Tooltip
│ │ └── index.tsx
│ ├── TransactionConfirmationModal
│ │ └── index.tsx
│ ├── TransactionSettings
│ │ └── index.tsx
│ ├── WalletModal
│ │ ├── Option.tsx
│ │ ├── PendingView.tsx
│ │ └── index.tsx
│ ├── Web3ReactManager
│ │ └── index.tsx
│ ├── Web3Status
│ │ └── index.tsx
│ ├── analytics
│ │ └── GoogleAnalyticsReporter.tsx
│ ├── claim
│ │ ├── AddressClaimModal.tsx
│ │ └── ClaimModal.tsx
│ ├── earn
│ │ ├── ClaimRewardModal.tsx
│ │ ├── PoolCard.tsx
│ │ ├── StakingModal.tsx
│ │ ├── UnstakingModal.tsx
│ │ └── styled.ts
│ ├── swap
│ │ ├── AdvancedSwapDetails.tsx
│ │ ├── AdvancedSwapDetailsDropdown.tsx
│ │ ├── BetterTradeLink.tsx
│ │ ├── ConfirmSwapModal.tsx
│ │ ├── FormattedPriceImpact.tsx
│ │ ├── SwapModalFooter.tsx
│ │ ├── SwapModalHeader.tsx
│ │ ├── SwapRoute.tsx
│ │ ├── TradePrice.tsx
│ │ ├── confirmPriceImpactWithoutFee.ts
│ │ └── styleds.tsx
│ └── vote
│ │ ├── DelegateModal.tsx
│ │ └── VoteModal.tsx
├── connectors
│ ├── Fortmatic.ts
│ ├── NetworkConnector.ts
│ ├── fortmatic.d.ts
│ └── index.ts
├── 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
│ ├── index.ts
│ ├── lists.ts
│ ├── multicall
│ │ ├── abi.json
│ │ └── index.ts
│ └── v1
│ │ ├── index.ts
│ │ ├── v1_exchange.json
│ │ └── v1_factory.json
├── data
│ ├── Allowances.ts
│ ├── Reserves.ts
│ ├── TotalSupply.ts
│ └── V1.ts
├── hooks
│ ├── StakingRewards.json
│ ├── Tokens.ts
│ ├── Trades.ts
│ ├── index.ts
│ ├── useApproveCallback.ts
│ ├── useColor.ts
│ ├── useContract.ts
│ ├── useCopyClipboard.ts
│ ├── useCurrentBlockTimestamp.ts
│ ├── useDebounce.ts
│ ├── useENS.ts
│ ├── useENSAddress.ts
│ ├── useENSContentHash.ts
│ ├── useENSName.ts
│ ├── useFetchListCallback.ts
│ ├── useHttpLocations.ts
│ ├── useInterval.ts
│ ├── useIsArgentWallet.ts
│ ├── useIsWindowVisible.ts
│ ├── useLast.ts
│ ├── useOnClickOutside.tsx
│ ├── useParsedQueryString.ts
│ ├── usePrevious.ts
│ ├── useSocksBalance.ts
│ ├── useSwapCallback.ts
│ ├── useTimestampFromBlock.ts
│ ├── useToggle.ts
│ ├── useToggledVersion.ts
│ ├── useTransactionDeadline.ts
│ ├── useWindowSize.ts
│ └── useWrapCallback.ts
├── i18n.ts
├── index.tsx
├── pages
│ ├── AddLiquidity
│ │ ├── ConfirmAddModalBottom.tsx
│ │ ├── PoolPriceBar.tsx
│ │ ├── index.tsx
│ │ └── redirects.tsx
│ ├── App.tsx
│ ├── AppBody.tsx
│ ├── Earn
│ │ ├── Countdown.tsx
│ │ ├── Manage.tsx
│ │ └── index.tsx
│ ├── MigrateV1
│ │ ├── EmptyState.tsx
│ │ ├── MigrateV1Exchange.tsx
│ │ ├── RemoveV1Exchange.tsx
│ │ └── index.tsx
│ ├── Pool
│ │ ├── index.tsx
│ │ └── styleds.tsx
│ ├── PoolFinder
│ │ └── index.tsx
│ ├── RemoveLiquidity
│ │ ├── index.tsx
│ │ └── redirects.tsx
│ ├── Swap
│ │ ├── index.tsx
│ │ └── redirects.tsx
│ └── Vote
│ │ ├── VotePage.tsx
│ │ ├── index.tsx
│ │ └── styled.tsx
├── react-app-env.d.ts
├── state
│ ├── application
│ │ ├── actions.ts
│ │ ├── hooks.ts
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ └── updater.ts
│ ├── burn
│ │ ├── actions.ts
│ │ ├── hooks.ts
│ │ └── reducer.ts
│ ├── claim
│ │ └── hooks.ts
│ ├── global
│ │ └── actions.ts
│ ├── governance
│ │ └── hooks.ts
│ ├── index.ts
│ ├── lists
│ │ ├── actions.ts
│ │ ├── hooks.ts
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ └── updater.ts
│ ├── mint
│ │ ├── actions.ts
│ │ ├── hooks.ts
│ │ ├── reducer.test.ts
│ │ └── reducer.ts
│ ├── multicall
│ │ ├── actions.test.ts
│ │ ├── actions.ts
│ │ ├── hooks.ts
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ ├── updater.test.ts
│ │ └── updater.tsx
│ ├── stake
│ │ └── hooks.ts
│ ├── swap
│ │ ├── actions.ts
│ │ ├── hooks.test.ts
│ │ ├── hooks.ts
│ │ ├── reducer.test.ts
│ │ └── reducer.ts
│ ├── transactions
│ │ ├── actions.ts
│ │ ├── hooks.tsx
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ ├── updater.test.ts
│ │ └── updater.tsx
│ ├── user
│ │ ├── actions.ts
│ │ ├── hooks.tsx
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ └── updater.tsx
│ └── wallet
│ │ └── hooks.ts
├── theme
│ ├── DarkModeQueryParamReader.tsx
│ ├── components.tsx
│ ├── index.tsx
│ └── styled.d.ts
└── utils
│ ├── chunkArray.test.ts
│ ├── chunkArray.ts
│ ├── computeUniCirculation.test.ts
│ ├── computeUniCirculation.ts
│ ├── contenthashToUri.test.skip.ts
│ ├── contenthashToUri.ts
│ ├── currencyId.ts
│ ├── getLibrary.ts
│ ├── getTokenList.ts
│ ├── index.test.ts
│ ├── index.ts
│ ├── isZero.ts
│ ├── listVersionLabel.ts
│ ├── maxAmountSpend.ts
│ ├── parseENSAddress.test.ts
│ ├── parseENSAddress.ts
│ ├── prices.test.ts
│ ├── prices.ts
│ ├── resolveENSContentHash.ts
│ ├── retry.test.ts
│ ├── retry.ts
│ ├── uriToHttp.test.ts
│ ├── uriToHttp.ts
│ ├── useDebouncedChangeHandler.tsx
│ ├── useUSDCPrice.ts
│ ├── v1SwapArgument.test.ts
│ ├── v1SwapArguments.ts
│ └── wrappedCurrency.ts
├── tsconfig.json
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_CHAIN_ID="1"
2 | REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_CHAIN_ID="1"
2 | REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
3 | REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
4 | REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
5 | REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
6 |
--------------------------------------------------------------------------------
/.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": [
12 | "node_modules/**/*"
13 | ],
14 | "settings": {
15 | "react": {
16 | "version": "detect"
17 | }
18 | },
19 | "extends": [
20 | "plugin:react/recommended",
21 | "plugin:@typescript-eslint/recommended",
22 | "plugin:react-hooks/recommended",
23 | "prettier/@typescript-eslint",
24 | "plugin:prettier/recommended"
25 | ],
26 | "rules": {
27 | "@typescript-eslint/explicit-function-return-type": "off",
28 | "prettier/prettier": "error",
29 | "@typescript-eslint/no-explicit-any": "off"
30 | }
31 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | /.netlify
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | notes.txt
26 | .idea/
27 |
28 | .vscode/
29 |
30 | package-lock.json
31 |
32 | cypress/videos
33 | cypress/screenshots
34 | cypress/fixtures/example.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 120
5 | }
6 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | ignore-scripts true
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # orn-liquidity-mining-ui
2 |
3 | Based on https://github.com/Uniswap/uniswap-interface
4 |
5 | ## Development
6 |
7 | ### Install Dependencies
8 |
9 | ```bash
10 | yarn
11 | ```
12 |
13 | ### Run
14 |
15 | ```bash
16 | yarn start
17 | ```
18 |
19 | ### Configuring the environment (optional)
20 |
21 | To have the interface default to a different network when a wallet is not connected:
22 |
23 | 1. Make a copy of `.env` named `.env.local`
24 | 2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"`
25 | 3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"`
26 |
27 | Note that the interface only works on testnets where both
28 | [Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
29 | [multicall](https://github.com/makerdao/multicall) are deployed.
30 | The interface will not work on other networks.
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000",
3 | "pluginsFile": false,
4 | "fixturesFolder": false,
5 | "supportFile": "cypress/support/index.js",
6 | "video": false,
7 | "defaultCommandTimeout": 10000
8 | }
9 |
--------------------------------------------------------------------------------
/cypress/integration/add-liquidity.test.ts:
--------------------------------------------------------------------------------
1 | describe('Add Liquidity', () => {
2 | it('loads the two correct tokens', () => {
3 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
4 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
5 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
6 | })
7 |
8 | it('does not crash if ETH is duplicated', () => {
9 | cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
10 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
11 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
12 | })
13 |
14 | it('token not in storage is loaded', () => {
15 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
16 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
17 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
18 | })
19 |
20 | it('single token can be selected', () => {
21 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
22 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
23 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
24 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
25 | })
26 |
27 | it('redirects /add/token-token to add/token/token', () => {
28 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
29 | cy.url().should(
30 | 'contain',
31 | '/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
32 | )
33 | })
34 |
35 | it('redirects /add/WETH-token to /add/WETH-address/token', () => {
36 | cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
37 | cy.url().should(
38 | 'contain',
39 | '/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
40 | )
41 | })
42 |
43 | it('redirects /add/token-WETH to /add/token/WETH-address', () => {
44 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
45 | cy.url().should(
46 | 'contain',
47 | '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab'
48 | )
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/cypress/integration/landing.test.ts:
--------------------------------------------------------------------------------
1 | import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
2 |
3 | describe('Landing Page', () => {
4 | beforeEach(() => cy.visit('/'))
5 | it('loads swap page', () => {
6 | cy.get('#swap-page')
7 | })
8 |
9 | it('redirects to url /swap', () => {
10 | cy.url().should('include', '/swap')
11 | })
12 |
13 | it('allows navigation to pool', () => {
14 | cy.get('#pool-nav-link').click()
15 | cy.url().should('include', '/pool')
16 | })
17 |
18 | it('is connected', () => {
19 | cy.get('#web3-status-connected').click()
20 | cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/cypress/integration/lists.test.ts:
--------------------------------------------------------------------------------
1 | describe('Lists', () => {
2 | beforeEach(() => {
3 | cy.visit('/swap')
4 | })
5 |
6 | it('defaults to uniswap list', () => {
7 | cy.get('#swap-currency-output .open-currency-select-button').click()
8 | cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
9 | })
10 |
11 | it('change list', () => {
12 | cy.get('#swap-currency-output .open-currency-select-button').click()
13 | cy.get('#currency-search-change-list-button').click()
14 | cy.get('#list-row-tokens-1inch-eth .select-button').click()
15 | cy.get('#currency-search-selected-list-name').should('contain', '1inch')
16 | cy.get('#currency-search-change-list-button').click()
17 | cy.get('#list-row-tokens-uniswap-eth .select-button').click()
18 | cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/cypress/integration/migrate-v1.test.ts:
--------------------------------------------------------------------------------
1 | describe('Migrate V1 Liquidity', () => {
2 | describe('Remove V1 liquidity', () => {
3 | it('renders the correct page', () => {
4 | cy.visit('/remove/v1/0x93bB63aFe1E0180d0eF100D774B473034fd60C36')
5 | cy.get('#remove-v1-exchange').should('contain', 'MKR/ETH')
6 | })
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/cypress/integration/pool.test.ts:
--------------------------------------------------------------------------------
1 | describe('Pool', () => {
2 | beforeEach(() => cy.visit('/pool'))
3 | it('add liquidity links to /add/ETH', () => {
4 | cy.get('#join-pool-button').click()
5 | cy.url().should('contain', '/add/ETH')
6 | })
7 |
8 | it('import pool links to /import', () => {
9 | cy.get('#import-pool-link').click()
10 | cy.url().should('contain', '/find')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/cypress/integration/remove-liquidity.test.ts:
--------------------------------------------------------------------------------
1 | describe('Remove Liquidity', () => {
2 | it('redirects', () => {
3 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
4 | cy.url().should(
5 | 'contain',
6 | '/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
7 | )
8 | })
9 |
10 | it('eth remove', () => {
11 | cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
12 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
13 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
14 | })
15 |
16 | it('eth remove swap order', () => {
17 | cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
18 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR')
19 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
20 | })
21 |
22 | it('loads the two correct tokens', () => {
23 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
24 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
25 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
26 | })
27 |
28 | it('does not crash if ETH is duplicated', () => {
29 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
30 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
31 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
32 | })
33 |
34 | it('token not in storage is loaded', () => {
35 | cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
36 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
37 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/cypress/integration/send.test.ts:
--------------------------------------------------------------------------------
1 | describe('Send', () => {
2 | it('should redirect', () => {
3 | cy.visit('/send')
4 | cy.url().should('include', '/swap')
5 | })
6 |
7 | it('should redirect with url params', () => {
8 | cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz')
9 | cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/cypress/integration/swap.test.ts:
--------------------------------------------------------------------------------
1 | describe('Swap', () => {
2 | beforeEach(() => {
3 | cy.visit('/swap')
4 | })
5 | it('can enter an amount into input', () => {
6 | cy.get('#swap-currency-input .token-amount-input')
7 | .type('0.001', { delay: 200 })
8 | .should('have.value', '0.001')
9 | })
10 |
11 | it('zero swap amount', () => {
12 | cy.get('#swap-currency-input .token-amount-input')
13 | .type('0.0', { delay: 200 })
14 | .should('have.value', '0.0')
15 | })
16 |
17 | it('invalid swap amount', () => {
18 | cy.get('#swap-currency-input .token-amount-input')
19 | .type('\\', { delay: 200 })
20 | .should('have.value', '')
21 | })
22 |
23 | it('can enter an amount into output', () => {
24 | cy.get('#swap-currency-output .token-amount-input')
25 | .type('0.001', { delay: 200 })
26 | .should('have.value', '0.001')
27 | })
28 |
29 | it('zero output amount', () => {
30 | cy.get('#swap-currency-output .token-amount-input')
31 | .type('0.0', { delay: 200 })
32 | .should('have.value', '0.0')
33 | })
34 |
35 | it('can swap ETH for DAI', () => {
36 | cy.get('#swap-currency-output .open-currency-select-button').click()
37 | cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
38 | cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
39 | cy.get('#swap-currency-input .token-amount-input').should('be.visible')
40 | cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
41 | cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
42 | cy.get('#swap-button').click()
43 | cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
44 | })
45 |
46 | it('add a recipient does not exist unless in expert mode', () => {
47 | cy.get('#add-recipient-button').should('not.exist')
48 | })
49 |
50 | describe('expert mode', () => {
51 | beforeEach(() => {
52 | cy.window().then(win => {
53 | cy.stub(win, 'prompt').returns('confirm')
54 | })
55 | cy.get('#open-settings-dialog-button').click()
56 | cy.get('#toggle-expert-mode-button').click()
57 | cy.get('#confirm-expert-mode').click()
58 | })
59 |
60 | it('add a recipient is visible', () => {
61 | cy.get('#add-recipient-button').should('be.visible')
62 | })
63 |
64 | it('add a recipient', () => {
65 | cy.get('#add-recipient-button').click()
66 | cy.get('#recipient').should('exist')
67 | })
68 |
69 | it('remove recipient', () => {
70 | cy.get('#add-recipient-button').click()
71 | cy.get('#remove-recipient-button').click()
72 | cy.get('#recipient').should('not.exist')
73 | })
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/cypress/integration/token-warning.ts:
--------------------------------------------------------------------------------
1 | describe('Warning', () => {
2 | beforeEach(() => {
3 | cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
4 | })
5 |
6 | it('Check that warning is displayed', () => {
7 | cy.get('.token-warning-container').should('be.visible')
8 | })
9 |
10 | it('Check that warning hides after button dismissal', () => {
11 | cy.get('.token-dismiss-button').should('be.disabled')
12 | cy.get('.understand-checkbox').click()
13 | cy.get('.token-dismiss-button').should('not.be.disabled')
14 | cy.get('.token-dismiss-button').click()
15 | cy.get('.token-warning-container').should('not.be.visible')
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/cypress/support/commands.d.ts:
--------------------------------------------------------------------------------
1 | export const TEST_ADDRESS_NEVER_USE: string
2 |
3 | export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
4 |
5 | // declare namespace Cypress {
6 | // // eslint-disable-next-line @typescript-eslint/class-name-casing
7 | // interface cy {
8 | // additionalCommands(): void
9 | // }
10 | // }
11 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This file is processed and loaded automatically before your test files.
3 | //
4 | // You can read more here:
5 | // https://on.cypress.io/configuration
6 | // ***********************************************************
7 |
8 | // Import commands.ts using ES2015 syntax:
9 | import './commands'
10 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "baseUrl": "../node_modules",
5 | "target": "es5",
6 | "lib": ["es5", "dom"],
7 | "types": ["cypress"]
8 | },
9 | "include": ["**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/public/451.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Unavailable For Legal Reasons
6 |
7 |
8 | Unavailable For Legal Reasons
9 |
10 |
11 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/public/favicon.png
--------------------------------------------------------------------------------
/public/images/192x192_App_Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/public/images/192x192_App_Icon.png
--------------------------------------------------------------------------------
/public/images/512x512_App_Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/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 | Orion Staking
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/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": "Orion",
3 | "name": "Orion",
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/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/arrow-right-white.png
--------------------------------------------------------------------------------
/src/assets/images/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/big_unicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/big_unicorn.png
--------------------------------------------------------------------------------
/src/assets/images/blue-loader.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/circle-grey.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/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/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/ethereum-logo.png
--------------------------------------------------------------------------------
/src/assets/images/fortmaticIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/fortmaticIcon.png
--------------------------------------------------------------------------------
/src/assets/images/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/menu.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/metamask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/metamask.png
--------------------------------------------------------------------------------
/src/assets/images/noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/noise.png
--------------------------------------------------------------------------------
/src/assets/images/plus-blue.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/plus-grey.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/portisIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/portisIcon.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/token-list/lists-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/token-list/lists-dark.png
--------------------------------------------------------------------------------
/src/assets/images/token-list/lists-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/token-list/lists-light.png
--------------------------------------------------------------------------------
/src/assets/images/token-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/token-logo.png
--------------------------------------------------------------------------------
/src/assets/images/trustWallet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/trustWallet.png
--------------------------------------------------------------------------------
/src/assets/images/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/xl_uni.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/xl_uni.png
--------------------------------------------------------------------------------
/src/assets/svg/QR.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/lightcircle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/svg/logo.png
--------------------------------------------------------------------------------
/src/components/AccountDetails/Copy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import useCopyClipboard from '../../hooks/useCopyClipboard'
4 |
5 | import { LinkStyledButton } from '../../theme'
6 | import { CheckCircle, Copy } from 'react-feather'
7 |
8 | const CopyIcon = styled(LinkStyledButton)`
9 | color: ${({ theme }) => theme.text3};
10 | flex-shrink: 0;
11 | display: flex;
12 | text-decoration: none;
13 | font-size: 0.825rem;
14 | :hover,
15 | :active,
16 | :focus {
17 | text-decoration: none;
18 | color: ${({ theme }) => theme.text2};
19 | }
20 | `
21 | const TransactionStatusText = styled.span`
22 | margin-left: 0.25rem;
23 | font-size: 0.825rem;
24 | ${({ theme }) => theme.flexRowNoWrap};
25 | align-items: center;
26 | `
27 |
28 | export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) {
29 | const [isCopied, setCopied] = useCopyClipboard()
30 |
31 | return (
32 | setCopied(props.toCopy)}>
33 | {isCopied ? (
34 |
35 |
36 | Copied
37 |
38 | ) : (
39 |
40 |
41 |
42 | )}
43 | {isCopied ? '' : props.children}
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/AccountDetails/Transaction.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { CheckCircle, Triangle } from 'react-feather'
4 |
5 | import { useActiveWeb3React } from '../../hooks'
6 | import { getEtherscanLink } from '../../utils'
7 | import { ExternalLink } from '../../theme'
8 | import { useAllTransactions } from '../../state/transactions/hooks'
9 | import { RowFixed } from '../Row'
10 | import Loader from '../Loader'
11 |
12 | const TransactionWrapper = styled.div``
13 |
14 | const TransactionStatusText = styled.div`
15 | margin-right: 0.5rem;
16 | display: flex;
17 | align-items: center;
18 | :hover {
19 | text-decoration: underline;
20 | }
21 | `
22 |
23 | const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
24 | display: flex;
25 | justify-content: space-between;
26 | align-items: center;
27 | text-decoration: none !important;
28 | border-radius: 0.5rem;
29 | padding: 0.25rem 0rem;
30 | font-weight: 500;
31 | font-size: 0.825rem;
32 | color: ${({ theme }) => theme.primary1};
33 | `
34 |
35 | const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
36 | color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
37 | `
38 |
39 | export default function Transaction({ hash }: { hash: string }) {
40 | const { chainId } = useActiveWeb3React()
41 | const allTransactions = useAllTransactions()
42 |
43 | const tx = allTransactions?.[hash]
44 | const summary = tx?.summary
45 | const pending = !tx?.receipt
46 | const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined')
47 |
48 | if (!chainId) return null
49 |
50 | return (
51 |
52 |
53 |
54 | {summary ?? hash} ↗
55 |
56 |
57 | {pending ? : success ? : }
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { CardProps, Text } from 'rebass'
4 | import { Box } from 'rebass/styled-components'
5 |
6 | const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>`
7 | width: 100%;
8 | border-radius: 16px;
9 | padding: 1.25rem;
10 | padding: ${({ padding }) => padding};
11 | border: ${({ border }) => border};
12 | border-radius: ${({ borderRadius }) => borderRadius};
13 | `
14 | export default Card
15 |
16 | export const LightCard = styled(Card)`
17 | border: 1px solid ${({ theme }) => theme.bg2};
18 | background-color: ${({ theme }) => theme.bg1};
19 | `
20 |
21 | export const GreyCard = styled(Card)`
22 | background-color: ${({ theme }) => theme.bg3};
23 | `
24 |
25 | export const OutlineCard = styled(Card)`
26 | border: 1px solid ${({ theme }) => theme.bg3};
27 | `
28 |
29 | export const YellowCard = styled(Card)`
30 | background-color: rgba(243, 132, 30, 0.05);
31 | color: ${({ theme }) => theme.yellow2};
32 | font-weight: 500;
33 | `
34 |
35 | export const PinkCard = styled(Card)`
36 | background-color: rgba(255, 0, 122, 0.03);
37 | color: ${({ theme }) => theme.primary1};
38 | font-weight: 500;
39 | `
40 |
41 | const BlueCardStyled = styled(Card)`
42 | background-color: ${({ theme }) => theme.primary5};
43 | color: ${({ theme }) => theme.primary1};
44 | border-radius: 12px;
45 | width: fit-content;
46 | `
47 |
48 | export const BlueCard = ({ children, ...rest }: CardProps) => {
49 | return (
50 |
51 |
52 | {children}
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/Column/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Column = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: flex-start;
7 | `
8 | export const ColumnCenter = styled(Column)`
9 | width: 100%;
10 | align-items: center;
11 | `
12 |
13 | export const AutoColumn = styled.div<{
14 | gap?: 'sm' | 'md' | 'lg' | string
15 | justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between'
16 | }>`
17 | display: grid;
18 | grid-auto-rows: auto;
19 | grid-row-gap: ${({ gap }) => (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap};
20 | justify-items: ${({ justify }) => justify && justify};
21 | `
22 |
23 | export default Column
24 |
--------------------------------------------------------------------------------
/src/components/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/CurrencyLogo/index.tsx:
--------------------------------------------------------------------------------
1 | import { Currency, ETHER, Token } from '@uniswap/sdk'
2 | import React, { useMemo } from 'react'
3 | import styled from 'styled-components'
4 |
5 | import EthereumLogo from '../../assets/images/ethereum-logo.png'
6 | import useHttpLocations from '../../hooks/useHttpLocations'
7 | import { WrappedTokenInfo } from '../../state/lists/hooks'
8 | import Logo from '../Logo'
9 |
10 | const getTokenLogoURL = (address: string) =>
11 | `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
12 |
13 | const StyledEthereumLogo = styled.img<{ size: string }>`
14 | width: ${({ size }) => size};
15 | height: ${({ size }) => size};
16 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
17 | border-radius: 24px;
18 | `
19 |
20 | const StyledLogo = styled(Logo)<{ size: string }>`
21 | width: ${({ size }) => size};
22 | height: ${({ size }) => size};
23 | border-radius: ${({ size }) => size};
24 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
25 | `
26 |
27 | export default function CurrencyLogo({
28 | currency,
29 | size = '24px',
30 | style
31 | }: {
32 | currency?: Currency
33 | size?: string
34 | style?: React.CSSProperties
35 | }) {
36 | const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
37 |
38 | const srcs: string[] = useMemo(() => {
39 | if (currency === ETHER) return []
40 |
41 | if (currency instanceof Token) {
42 | if (currency instanceof WrappedTokenInfo) {
43 | return [...uriLocations, getTokenLogoURL(currency.address)]
44 | }
45 |
46 | return [getTokenLogoURL(currency.address)]
47 | }
48 | return []
49 | }, [currency, uriLocations])
50 |
51 | if (currency === ETHER) {
52 | return
53 | }
54 |
55 | return
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/DoubleLogo/index.tsx:
--------------------------------------------------------------------------------
1 | import { Currency } from '@uniswap/sdk'
2 | import React from 'react'
3 | import styled from 'styled-components'
4 | import CurrencyLogo from '../CurrencyLogo'
5 |
6 | const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
7 | position: relative;
8 | display: flex;
9 | flex-direction: row;
10 | margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
11 | `
12 |
13 | interface DoubleCurrencyLogoProps {
14 | margin?: boolean
15 | size?: number
16 | currency0?: Currency
17 | currency1?: Currency
18 | }
19 |
20 | const HigherLogo = styled(CurrencyLogo)`
21 | z-index: 2;
22 | `
23 | const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
24 | position: absolute;
25 | left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
26 | `
27 |
28 | export default function DoubleCurrencyLogo({
29 | currency0,
30 | currency1,
31 | size = 16,
32 | margin = false
33 | }: DoubleCurrencyLogoProps) {
34 | return (
35 |
36 | {currency0 && }
37 | {currency1 && }
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/FormattedCurrencyAmount/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CurrencyAmount, Fraction, JSBI } from '@uniswap/sdk'
3 |
4 | const CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
5 |
6 | export default function FormattedCurrencyAmount({
7 | currencyAmount,
8 | significantDigits = 4
9 | }: {
10 | currencyAmount: CurrencyAmount
11 | significantDigits?: number
12 | }) {
13 | return (
14 | <>
15 | {currencyAmount.equalTo(JSBI.BigInt(0))
16 | ? '0'
17 | : currencyAmount.greaterThan(CURRENCY_AMOUNT_MIN)
18 | ? currencyAmount.toSignificant(significantDigits)
19 | : `<${CURRENCY_AMOUNT_MIN.toSignificant(1)}`}
20 | >
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/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/Identicon/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 |
3 | import styled from 'styled-components'
4 |
5 | import { useActiveWeb3React } from '../../hooks'
6 | import Jazzicon from 'jazzicon'
7 |
8 | const StyledIdenticonContainer = styled.div`
9 | height: 1rem;
10 | width: 1rem;
11 | border-radius: 1.125rem;
12 | background-color: ${({ theme }) => theme.bg4};
13 | `
14 |
15 | export default function Identicon() {
16 | const ref = useRef()
17 |
18 | const { account } = useActiveWeb3React()
19 |
20 | useEffect(() => {
21 | if (account && ref.current) {
22 | ref.current.innerHTML = ''
23 | ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
24 | }
25 | }, [account])
26 |
27 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ListLogo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import useHttpLocations from '../../hooks/useHttpLocations'
4 |
5 | import Logo from '../Logo'
6 |
7 | const StyledListLogo = styled(Logo)<{ size: string }>`
8 | width: ${({ size }) => size};
9 | height: ${({ size }) => size};
10 | `
11 |
12 | export default function ListLogo({
13 | logoURI,
14 | style,
15 | size = '24px',
16 | alt
17 | }: {
18 | logoURI: string
19 | size?: string
20 | style?: React.CSSProperties
21 | alt?: string
22 | }) {
23 | const srcs: string[] = useHttpLocations(logoURI)
24 |
25 | return
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Loader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import styled, { keyframes } from 'styled-components'
4 |
5 | const rotate = keyframes`
6 | from {
7 | transform: rotate(0deg);
8 | }
9 | to {
10 | transform: rotate(360deg);
11 | }
12 | `
13 |
14 | const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
15 | animation: 2s ${rotate} linear infinite;
16 | height: ${({ size }) => size};
17 | width: ${({ size }) => size};
18 | path {
19 | stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
20 | }
21 | `
22 |
23 | /**
24 | * Takes in custom size and stroke for circle color, default to primary color as fill,
25 | * need ...rest for layered styles on top
26 | */
27 | export default function Loader({
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 |
--------------------------------------------------------------------------------
/src/components/Logo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { HelpCircle } from 'react-feather'
3 | import { ImageProps } from 'rebass'
4 |
5 | const BAD_SRCS: { [tokenAddress: string]: true } = {}
6 |
7 | export interface LogoProps extends Pick {
8 | srcs: string[]
9 | }
10 |
11 | /**
12 | * Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
13 | */
14 | export default function Logo({ srcs, alt, ...rest }: LogoProps) {
15 | const [, refresh] = useState(0)
16 |
17 | const src: string | undefined = srcs.find(src => !BAD_SRCS[src])
18 |
19 | if (src) {
20 | return (
21 |
{
26 | if (src) BAD_SRCS[src] = true
27 | refresh(i => i + 1)
28 | }}
29 | />
30 | )
31 | }
32 |
33 | return
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ModalViews/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { useActiveWeb3React } from '../../hooks'
3 |
4 | import { AutoColumn, ColumnCenter } from '../Column'
5 | import styled, { ThemeContext } from 'styled-components'
6 | import { RowBetween } from '../Row'
7 | import { TYPE, CloseIcon, CustomLightSpinner } from '../../theme'
8 | import { ArrowUpCircle } from 'react-feather'
9 |
10 | import Circle from '../../assets/images/blue-loader.svg'
11 | import { getEtherscanLink } from '../../utils'
12 | import { ExternalLink } from '../../theme/components'
13 |
14 | const ConfirmOrLoadingWrapper = styled.div`
15 | width: 100%;
16 | padding: 24px;
17 | `
18 |
19 | const ConfirmedIcon = styled(ColumnCenter)`
20 | padding: 60px 0;
21 | `
22 |
23 | export function LoadingView({ children, onDismiss }: { children: any; onDismiss: () => void }) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {children}
35 | Confirm this transaction in your wallet
36 |
37 |
38 | )
39 | }
40 |
41 | export function SubmittedView({
42 | children,
43 | onDismiss,
44 | hash
45 | }: {
46 | children: any
47 | onDismiss: () => void
48 | hash: string | undefined
49 | }) {
50 | const theme = useContext(ThemeContext)
51 | const { chainId } = useActiveWeb3React()
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {children}
64 | {chainId && hash && (
65 |
66 | View transaction on Etherscan
67 |
68 | )}
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/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 | ...rest
47 | }: {
48 | value: string | number
49 | onUserInput: (input: string) => void
50 | error?: boolean
51 | fontSize?: string
52 | align?: 'right' | 'left'
53 | } & Omit, 'ref' | 'onChange' | 'as'>) {
54 | const enforcer = (nextUserInput: string) => {
55 | if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
56 | onUserInput(nextUserInput)
57 | }
58 | }
59 |
60 | return (
61 | {
65 | // replace commas with periods, because uniswap exclusively uses period as the decimal separator
66 | enforcer(event.target.value.replace(/,/g, '.'))
67 | }}
68 | // universal input options
69 | inputMode="decimal"
70 | title="Token Amount"
71 | autoComplete="off"
72 | autoCorrect="off"
73 | // text-specific options
74 | type="text"
75 | pattern="^[0-9]*[.,]?[0-9]*$"
76 | placeholder={placeholder || '0.0'}
77 | minLength={1}
78 | maxLength={79}
79 | spellCheck="false"
80 | />
81 | )
82 | })
83 |
84 | export default Input
85 |
86 | // const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
87 |
--------------------------------------------------------------------------------
/src/components/Popups/TransactionPopup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { AlertCircle, CheckCircle } from 'react-feather'
3 | import styled, { ThemeContext } from 'styled-components'
4 | import { useActiveWeb3React } from '../../hooks'
5 | import { TYPE } from '../../theme'
6 | import { ExternalLink } from '../../theme/components'
7 | import { getEtherscanLink } from '../../utils'
8 | import { AutoColumn } from '../Column'
9 | import { AutoRow } from '../Row'
10 |
11 | const RowNoFlex = styled(AutoRow)`
12 | flex-wrap: nowrap;
13 | `
14 |
15 | export default function TransactionPopup({
16 | hash,
17 | success,
18 | summary
19 | }: {
20 | hash: string
21 | success?: boolean
22 | summary?: string
23 | }) {
24 | const { chainId } = useActiveWeb3React()
25 |
26 | const theme = useContext(ThemeContext)
27 |
28 | return (
29 |
30 |
33 |
34 | {summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
35 | {chainId && (
36 | View on Etherscan
37 | )}
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Popups/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { useActivePopups } from '../../state/application/hooks'
4 | import { AutoColumn } from '../Column'
5 | import PopupItem from './PopupItem'
6 | import ClaimPopup from './ClaimPopup'
7 | import { useURLWarningVisible } from '../../state/user/hooks'
8 |
9 | const MobilePopupWrapper = styled.div<{ height: string | number }>`
10 | position: relative;
11 | max-width: 100%;
12 | height: ${({ height }) => height};
13 | margin: ${({ height }) => (height ? '0 auto;' : 0)};
14 | margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
15 |
16 | display: none;
17 | ${({ theme }) => theme.mediaWidth.upToSmall`
18 | display: block;
19 | `};
20 | `
21 |
22 | const MobilePopupInner = styled.div`
23 | height: 99%;
24 | overflow-x: auto;
25 | overflow-y: hidden;
26 | display: flex;
27 | flex-direction: row;
28 | -webkit-overflow-scrolling: touch;
29 | ::-webkit-scrollbar {
30 | display: none;
31 | }
32 | `
33 |
34 | const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean }>`
35 | position: fixed;
36 | top: ${({ extraPadding }) => (extraPadding ? '108px' : '88px')};
37 | right: 1rem;
38 | max-width: 355px !important;
39 | width: 100%;
40 | z-index: 3;
41 |
42 | ${({ theme }) => theme.mediaWidth.upToSmall`
43 | display: none;
44 | `};
45 | `
46 |
47 | export default function Popups() {
48 | // get all popups
49 | const activePopups = useActivePopups()
50 |
51 | const urlWarningActive = useURLWarningVisible()
52 |
53 | return (
54 | <>
55 |
56 |
57 | {activePopups.map(item => (
58 |
59 | ))}
60 |
61 | 0 ? 'fit-content' : 0}>
62 |
63 | {activePopups // reverse so new items up front
64 | .slice(0)
65 | .reverse()
66 | .map(item => (
67 |
68 | ))}
69 |
70 |
71 | >
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/PositionCard/V1.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
3 | import { Token, TokenAmount, WETH } from '@uniswap/sdk'
4 |
5 | import { Text } from 'rebass'
6 | import { AutoColumn } from '../Column'
7 | import { ButtonSecondary } from '../Button'
8 | import { RowBetween, RowFixed } from '../Row'
9 | import { FixedHeightRow, HoverCard } from './index'
10 | import DoubleCurrencyLogo from '../DoubleLogo'
11 | import { useActiveWeb3React } from '../../hooks'
12 | import { ThemeContext } from 'styled-components'
13 |
14 | interface PositionCardProps extends RouteComponentProps<{}> {
15 | token: Token
16 | V1LiquidityBalance: TokenAmount
17 | }
18 |
19 | function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
20 | const theme = useContext(ThemeContext)
21 |
22 | const { chainId } = useActiveWeb3React()
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | {`${chainId && token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
32 |
33 |
43 | V1
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Migrate
52 |
53 |
54 |
60 | Remove
61 |
62 |
63 |
64 |
65 |
66 | )
67 | }
68 |
69 | export default withRouter(V1PositionCard)
70 |
--------------------------------------------------------------------------------
/src/components/ProgressSteps/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { RowBetween } from '../Row'
4 | import { AutoColumn } from '../Column'
5 | import { transparentize } from 'polished'
6 |
7 | const Wrapper = styled(AutoColumn)``
8 |
9 | const Grouping = styled(RowBetween)`
10 | width: 50%;
11 | `
12 |
13 | const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
14 | min-width: 20px;
15 | min-height: 20px;
16 | background-color: ${({ theme, confirmed, disabled }) =>
17 | disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary1};
18 | border-radius: 50%;
19 | color: ${({ theme }) => theme.white};
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | line-height: 8px;
24 | font-size: 12px;
25 | `
26 |
27 | const CircleRow = styled.div`
28 | width: calc(100% - 20px);
29 | display: flex;
30 | align-items: center;
31 | `
32 |
33 | const Connector = styled.div<{ prevConfirmed?: boolean; disabled?: boolean }>`
34 | width: 100%;
35 | height: 2px;
36 | background-color: ;
37 | background: linear-gradient(
38 | 90deg,
39 | ${({ theme, prevConfirmed, disabled }) =>
40 | disabled ? theme.bg4 : transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary1)}
41 | 0%,
42 | ${({ theme, prevConfirmed, disabled }) => (disabled ? theme.bg4 : prevConfirmed ? theme.primary1 : theme.bg4)} 80%
43 | );
44 | opacity: 0.6;
45 | `
46 |
47 | interface ProgressCirclesProps {
48 | steps: boolean[]
49 | disabled?: boolean
50 | }
51 |
52 | /**
53 | * Based on array of steps, create a step counter of circles.
54 | * A circle can be enabled, disabled, or confirmed. States are derived
55 | * from previous step.
56 | *
57 | * An extra circle is added to represent the ability to swap, add, or remove.
58 | * This step will never be marked as complete (because no 'txn done' state in body ui).
59 | *
60 | * @param steps array of booleans where true means step is complete
61 | */
62 | export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) {
63 | return (
64 |
65 |
66 | {steps.map((step, i) => {
67 | return (
68 |
69 |
70 | {step ? '✓' : i + 1}
71 |
72 |
73 |
74 | )
75 | })}
76 | {steps.length + 1}
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/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)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
5 | width: 100%;
6 | display: flex;
7 | padding: 0;
8 | align-items: ${({ align }) => (align ? align : 'center')};
9 | padding: ${({ padding }) => padding};
10 | border: ${({ border }) => border};
11 | border-radius: ${({ borderRadius }) => borderRadius};
12 | `
13 |
14 | export const RowBetween = styled(Row)`
15 | justify-content: space-between;
16 | `
17 |
18 | export const RowFlat = styled.div`
19 | display: flex;
20 | align-items: flex-end;
21 | `
22 |
23 | export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>`
24 | flex-wrap: wrap;
25 | margin: ${({ gap }) => gap && `-${gap}`};
26 | justify-content: ${({ justify }) => justify && justify};
27 |
28 | & > * {
29 | margin: ${({ gap }) => gap} !important;
30 | }
31 | `
32 |
33 | export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>`
34 | width: fit-content;
35 | margin: ${({ gap }) => gap && `-${gap}`};
36 | `
37 |
38 | export default Row
39 |
--------------------------------------------------------------------------------
/src/components/SearchModal/CommonBases.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text } from 'rebass'
3 | import { ChainId, Currency, currencyEquals, ETHER, Token } from '@uniswap/sdk'
4 | import styled from 'styled-components'
5 |
6 | import { SUGGESTED_BASES } from '../../constants'
7 | import { AutoColumn } from '../Column'
8 | import QuestionHelper from '../QuestionHelper'
9 | import { AutoRow } from '../Row'
10 | import CurrencyLogo from '../CurrencyLogo'
11 |
12 | const BaseWrapper = styled.div<{ disable?: boolean }>`
13 | border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
14 | border-radius: 10px;
15 | display: flex;
16 | padding: 6px;
17 |
18 | align-items: center;
19 | :hover {
20 | cursor: ${({ disable }) => !disable && 'pointer'};
21 | background-color: ${({ theme, disable }) => !disable && theme.bg2};
22 | }
23 |
24 | background-color: ${({ theme, disable }) => disable && theme.bg3};
25 | opacity: ${({ disable }) => disable && '0.4'};
26 | `
27 |
28 | export default function CommonBases({
29 | chainId,
30 | onSelect,
31 | selectedCurrency
32 | }: {
33 | chainId?: ChainId
34 | selectedCurrency?: Currency | null
35 | onSelect: (currency: Currency) => void
36 | }) {
37 | return (
38 |
39 |
40 |
41 | Common bases
42 |
43 |
44 |
45 |
46 | {
48 | if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) {
49 | onSelect(ETHER)
50 | }
51 | }}
52 | disable={selectedCurrency === ETHER}
53 | >
54 |
55 |
56 | ETH
57 |
58 |
59 | {(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
60 | const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
61 | return (
62 | !selected && onSelect(token)} disable={selected} key={token.address}>
63 |
64 |
65 | {token.symbol}
66 |
67 |
68 | )
69 | })}
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/SearchModal/CurrencySearchModal.tsx:
--------------------------------------------------------------------------------
1 | import { Currency } from '@uniswap/sdk'
2 | import React, { useCallback, useEffect, useState } from 'react'
3 | import ReactGA from 'react-ga'
4 | import useLast from '../../hooks/useLast'
5 | import Modal from '../Modal'
6 | import { CurrencySearch } from './CurrencySearch'
7 | import { ListSelect } from './ListSelect'
8 |
9 | interface CurrencySearchModalProps {
10 | isOpen: boolean
11 | onDismiss: () => void
12 | selectedCurrency?: Currency | null
13 | onCurrencySelect: (currency: Currency) => void
14 | otherSelectedCurrency?: Currency | null
15 | showCommonBases?: boolean
16 | }
17 |
18 | export default function CurrencySearchModal({
19 | isOpen,
20 | onDismiss,
21 | onCurrencySelect,
22 | selectedCurrency,
23 | otherSelectedCurrency,
24 | showCommonBases = false
25 | }: CurrencySearchModalProps) {
26 | const [listView, setListView] = useState(false)
27 | const lastOpen = useLast(isOpen)
28 |
29 | useEffect(() => {
30 | if (isOpen && !lastOpen) {
31 | setListView(false)
32 | }
33 | }, [isOpen, lastOpen])
34 |
35 | const handleCurrencySelect = useCallback(
36 | (currency: Currency) => {
37 | onCurrencySelect(currency)
38 | onDismiss()
39 | },
40 | [onDismiss, onCurrencySelect]
41 | )
42 |
43 | const handleClickChangeList = useCallback(() => {
44 | ReactGA.event({
45 | category: 'Lists',
46 | action: 'Change Lists'
47 | })
48 | setListView(true)
49 | }, [])
50 | const handleClickBack = useCallback(() => {
51 | ReactGA.event({
52 | category: 'Lists',
53 | action: 'Back'
54 | })
55 | setListView(false)
56 | }, [])
57 |
58 | return (
59 |
60 | {listView ? (
61 |
62 | ) : (
63 |
72 | )}
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/SearchModal/SortButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text } from 'rebass'
3 | import styled from 'styled-components'
4 | import { RowFixed } from '../Row'
5 |
6 | export const FilterWrapper = styled(RowFixed)`
7 | padding: 8px;
8 | background-color: ${({ theme }) => theme.bg2};
9 | color: ${({ theme }) => theme.text1};
10 | border-radius: 8px;
11 | user-select: none;
12 | & > * {
13 | user-select: none;
14 | }
15 | :hover {
16 | cursor: pointer;
17 | }
18 | `
19 |
20 | export default function SortButton({
21 | toggleSortOrder,
22 | ascending
23 | }: {
24 | toggleSortOrder: () => void
25 | ascending: boolean
26 | }) {
27 | return (
28 |
29 |
30 | {ascending ? '↑' : '↓'}
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/SearchModal/filtering.ts:
--------------------------------------------------------------------------------
1 | import { isAddress } from '../../utils'
2 | import { Token } from '@uniswap/sdk'
3 |
4 | export function filterTokens(tokens: Token[], search: string): Token[] {
5 | if (search.length === 0) return tokens
6 |
7 | const searchingAddress = isAddress(search)
8 |
9 | if (searchingAddress) {
10 | return tokens.filter(token => token.address === searchingAddress)
11 | }
12 |
13 | const lowerSearchParts = search
14 | .toLowerCase()
15 | .split(/\s+/)
16 | .filter(s => s.length > 0)
17 |
18 | if (lowerSearchParts.length === 0) {
19 | return tokens
20 | }
21 |
22 | const matchesSearch = (s: string): boolean => {
23 | const sParts = s
24 | .toLowerCase()
25 | .split(/\s+/)
26 | .filter(s => s.length > 0)
27 |
28 | return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
29 | }
30 |
31 | return tokens.filter(token => {
32 | const { symbol, name } = token
33 |
34 | return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/SearchModal/sorting.ts:
--------------------------------------------------------------------------------
1 | import { Token, TokenAmount } from '@uniswap/sdk'
2 | import { useMemo } from 'react'
3 | import { useAllTokenBalances } from '../../state/wallet/hooks'
4 |
5 | // compare two token amounts with highest one coming first
6 | function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
7 | if (balanceA && balanceB) {
8 | return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
9 | } else if (balanceA && balanceA.greaterThan('0')) {
10 | return -1
11 | } else if (balanceB && balanceB.greaterThan('0')) {
12 | return 1
13 | }
14 | return 0
15 | }
16 |
17 | function getTokenComparator(balances: {
18 | [tokenAddress: string]: TokenAmount | undefined
19 | }): (tokenA: Token, tokenB: Token) => number {
20 | return function sortTokens(tokenA: Token, tokenB: Token): number {
21 | // -1 = a is first
22 | // 1 = b is first
23 |
24 | // sort by balances
25 | const balanceA = balances[tokenA.address]
26 | const balanceB = balances[tokenB.address]
27 |
28 | const balanceComp = balanceComparator(balanceA, balanceB)
29 | if (balanceComp !== 0) return balanceComp
30 |
31 | if (tokenA.symbol && tokenB.symbol) {
32 | // sort by symbol
33 | return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
34 | } else {
35 | return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
36 | }
37 | }
38 | }
39 |
40 | export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
41 | const balances = useAllTokenBalances()
42 | const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
43 | return useMemo(() => {
44 | if (inverted) {
45 | return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
46 | } else {
47 | return comparator
48 | }
49 | }, [inverted, comparator])
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/SearchModal/styleds.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { AutoColumn } from '../Column'
3 | import { RowBetween, RowFixed } from '../Row'
4 |
5 | export const ModalInfo = styled.div`
6 | ${({ theme }) => theme.flexRowNoWrap}
7 | align-items: center;
8 | padding: 1rem 1rem;
9 | margin: 0.25rem 0.5rem;
10 | justify-content: center;
11 | flex: 1;
12 | user-select: none;
13 | `
14 |
15 | export const FadedSpan = styled(RowFixed)`
16 | color: ${({ theme }) => theme.primary1};
17 | font-size: 14px;
18 | `
19 |
20 | export const PaddedColumn = styled(AutoColumn)`
21 | padding: 20px;
22 | padding-bottom: 12px;
23 | `
24 |
25 | export const MenuItem = styled(RowBetween)`
26 | padding: 4px 20px;
27 | height: 56px;
28 | display: grid;
29 | grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px);
30 | grid-gap: 16px;
31 | cursor: ${({ disabled }) => !disabled && 'pointer'};
32 | pointer-events: ${({ disabled }) => disabled && 'none'};
33 | :hover {
34 | background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
35 | }
36 | opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
37 | `
38 |
39 | export const SearchInput = styled.input`
40 | position: relative;
41 | display: flex;
42 | padding: 16px;
43 | align-items: center;
44 | width: 100%;
45 | white-space: nowrap;
46 | background: none;
47 | border: none;
48 | outline: none;
49 | border-radius: 20px;
50 | color: ${({ theme }) => theme.text1};
51 | border-style: solid;
52 | border: 1px solid ${({ theme }) => theme.bg3};
53 | -webkit-appearance: none;
54 |
55 | font-size: 18px;
56 |
57 | ::placeholder {
58 | color: ${({ theme }) => theme.text3};
59 | }
60 | transition: border 100ms;
61 | :focus {
62 | border: 1px solid ${({ theme }) => theme.primary1};
63 | outline: none;
64 | }
65 | `
66 | export const Separator = styled.div`
67 | width: 100%;
68 | height: 1px;
69 | background-color: ${({ theme }) => theme.bg2};
70 | `
71 |
72 | export const SeparatorDark = styled.div`
73 | width: 100%;
74 | height: 1px;
75 | background-color: ${({ theme }) => theme.bg3};
76 | `
77 |
--------------------------------------------------------------------------------
/src/components/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 | /* border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; */
30 | background: ${({ theme }) => theme.bg3};
31 | display: flex;
32 | width: fit-content;
33 | cursor: pointer;
34 | outline: none;
35 | padding: 0;
36 | /* background-color: transparent; */
37 | `
38 |
39 | export interface ToggleProps {
40 | id?: string
41 | isActive: boolean
42 | toggle: () => void
43 | }
44 |
45 | export default function Toggle({ id, isActive, toggle }: ToggleProps) {
46 | return (
47 |
48 |
49 | On
50 |
51 |
52 | Off
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/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/Web3ReactManager/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useWeb3React } from '@web3-react/core'
3 | import styled from 'styled-components'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | import { network } from '../../connectors'
7 | import { useEagerConnect, useInactiveListener } from '../../hooks'
8 | import { NetworkContextName } from '../../constants'
9 | import Loader from '../Loader'
10 |
11 | const MessageWrapper = styled.div`
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | height: 20rem;
16 | `
17 |
18 | const Message = styled.h2`
19 | color: ${({ theme }) => theme.secondary1};
20 | `
21 |
22 | export default function Web3ReactManager({ children }: { children: JSX.Element }) {
23 | const { t } = useTranslation()
24 | const { active } = useWeb3React()
25 | const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
26 |
27 | // try to eagerly connect to an injected provider, if it exists and has granted access already
28 | const triedEager = useEagerConnect()
29 |
30 | // after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd
31 | useEffect(() => {
32 | if (triedEager && !networkActive && !networkError && !active) {
33 | activateNetwork(network)
34 | }
35 | }, [triedEager, networkActive, networkError, activateNetwork, active])
36 |
37 | // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
38 | useInactiveListener(!triedEager)
39 |
40 | // handle delayed loader state
41 | const [showLoader, setShowLoader] = useState(false)
42 | useEffect(() => {
43 | const timeout = setTimeout(() => {
44 | setShowLoader(true)
45 | }, 600)
46 |
47 | return () => {
48 | clearTimeout(timeout)
49 | }
50 | }, [])
51 |
52 | // on page load, do nothing until we've tried to connect to the injected connector
53 | if (!triedEager) {
54 | return null
55 | }
56 |
57 | // if the account context isn't active, and there's an error on the network context, it's an irrecoverable error
58 | if (!active && networkError) {
59 | return (
60 |
61 | {t('unknownError')}
62 |
63 | )
64 | }
65 |
66 | // if neither context is active, spin
67 | if (!active && !networkActive) {
68 | return showLoader ? (
69 |
70 |
71 |
72 | ) : null
73 | }
74 |
75 | return children
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/analytics/GoogleAnalyticsReporter.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import ReactGA from 'react-ga'
3 | import { RouteComponentProps } from 'react-router-dom'
4 |
5 | // fires a GA pageview every time the route changes
6 | export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null {
7 | useEffect(() => {
8 | ReactGA.pageview(`${pathname}${search}`)
9 | }, [pathname, search])
10 | return null
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/earn/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { AutoColumn } from '../Column'
3 |
4 | import uImage from '../../assets/images/big_unicorn.png'
5 | import xlUnicorn from '../../assets/images/xl_uni.png'
6 | import noise from '../../assets/images/noise.png'
7 |
8 | export const TextBox = styled.div`
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | padding: 4px 12px;
13 | border: 1px solid rgba(255, 255, 255, 0.4);
14 | border-radius: 20px;
15 | width: fit-content;
16 | justify-self: flex-end;
17 | `
18 |
19 | export const DataCard = styled(AutoColumn)<{ disabled?: boolean }>`
20 | background: linear-gradient(135deg,rgba(0,255,170,1.0) 0%,rgba(0,187,255,1.0) 53%,rgba(69,121,245,1.0) 100%);
21 | border-radius: 12px;
22 | width: 100%;
23 | position: relative;
24 | overflow: hidden;
25 | `
26 |
27 | export const CardBGImage = styled.span<{ desaturate?: boolean }>`
28 | background: url(${uImage});
29 | width: 1000px;
30 | height: 600px;
31 | position: absolute;
32 | border-radius: 12px;
33 | opacity: 0.4;
34 | top: -100px;
35 | left: -100px;
36 | transform: rotate(-15deg);
37 | user-select: none;
38 |
39 | ${({ desaturate }) => desaturate && `filter: saturate(0)`}
40 | `
41 |
42 | export const CardBGImageSmaller = styled.span<{ desaturate?: boolean }>`
43 | background: url(${xlUnicorn});
44 | width: 1200px;
45 | height: 1200px;
46 | position: absolute;
47 | border-radius: 12px;
48 | top: -300px;
49 | left: -300px;
50 | opacity: 0.4;
51 | user-select: none;
52 |
53 | ${({ desaturate }) => desaturate && `filter: saturate(0)`}
54 | `
55 |
56 | export const CardNoise = styled.span`
57 | background: url(${noise});
58 | background-size: cover;
59 | mix-blend-mode: overlay;
60 | border-radius: 12px;
61 | width: 100%;
62 | height: 100%;
63 | opacity: 0.15;
64 | position: absolute;
65 | top: 0;
66 | left: 0;
67 | user-select: none;
68 | `
69 |
70 | export const CardSection = styled(AutoColumn)<{ disabled?: boolean }>`
71 | padding: 1rem;
72 | z-index: 1;
73 | opacity: ${({ disabled }) => disabled && '0.4'};
74 | `
75 |
76 | export const Break = styled.div`
77 | width: 100%;
78 | background-color: rgba(255, 255, 255, 0.2);
79 | height: 1px;
80 | `
81 |
--------------------------------------------------------------------------------
/src/components/swap/AdvancedSwapDetailsDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { useLastTruthy } from '../../hooks/useLast'
4 | import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
5 |
6 | const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
7 | padding-top: calc(16px + 2rem);
8 | padding-bottom: 20px;
9 | margin-top: -2rem;
10 | width: 100%;
11 | max-width: 400px;
12 | border-bottom-left-radius: 20px;
13 | border-bottom-right-radius: 20px;
14 | color: ${({ theme }) => theme.text2};
15 | background-color: ${({ theme }) => theme.advancedBG};
16 | z-index: -1;
17 |
18 | transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
19 | transition: transform 300ms ease-in-out;
20 | `
21 |
22 | export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
23 | const lastTrade = useLastTruthy(trade)
24 |
25 | return (
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/swap/BetterTradeLink.tsx:
--------------------------------------------------------------------------------
1 | import { stringify } from 'qs'
2 | import React, { useContext, useMemo } from 'react'
3 | import { useLocation } from 'react-router'
4 | import { Text } from 'rebass'
5 | import { ThemeContext } from 'styled-components'
6 | import useParsedQueryString from '../../hooks/useParsedQueryString'
7 | import useToggledVersion, { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
8 |
9 | import { StyledInternalLink } from '../../theme'
10 | import { YellowCard } from '../Card'
11 | import { AutoColumn } from '../Column'
12 |
13 | function VersionLinkContainer({ children }: { children: React.ReactNode }) {
14 | const theme = useContext(ThemeContext)
15 |
16 | return (
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default function BetterTradeLink({ version }: { version: Version }) {
28 | const location = useLocation()
29 | const search = useParsedQueryString()
30 |
31 | const linkDestination = useMemo(() => {
32 | return {
33 | ...location,
34 | search: `?${stringify({
35 | ...search,
36 | use: version !== DEFAULT_VERSION ? version : undefined
37 | })}`
38 | }
39 | }, [location, search, version])
40 |
41 | return (
42 |
43 | There is a better price for this trade on{' '}
44 |
45 | Orion {version.toUpperCase()} ↗
46 |
47 |
48 | )
49 | }
50 |
51 | export function DefaultVersionLink() {
52 | const location = useLocation()
53 | const search = useParsedQueryString()
54 | const version = useToggledVersion()
55 |
56 | const linkDestination = useMemo(() => {
57 | return {
58 | ...location,
59 | search: `?${stringify({
60 | ...search,
61 | use: DEFAULT_VERSION
62 | })}`
63 | }
64 | }, [location, search])
65 |
66 | return (
67 |
68 | Showing {version.toUpperCase()} price.{' '}
69 |
70 | Switch to Orion {DEFAULT_VERSION.toUpperCase()} ↗
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/swap/FormattedPriceImpact.tsx:
--------------------------------------------------------------------------------
1 | import { Percent } from '@uniswap/sdk'
2 | import React from 'react'
3 | import { ONE_BIPS } from '../../constants'
4 | import { warningSeverity } from '../../utils/prices'
5 | import { ErrorText } from './styleds'
6 |
7 | /**
8 | * Formatted version of price impact text with warning colors
9 | */
10 | export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
11 | return (
12 |
13 | {priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/swap/SwapRoute.tsx:
--------------------------------------------------------------------------------
1 | import { Trade } from '@uniswap/sdk'
2 | import React, { Fragment, memo, useContext } from 'react'
3 | import { ChevronRight } from 'react-feather'
4 | import { Flex } from 'rebass'
5 | import { ThemeContext } from 'styled-components'
6 | import { TYPE } from '../../theme'
7 | import CurrencyLogo from '../CurrencyLogo'
8 |
9 | export default memo(function SwapRoute({ trade }: { trade: Trade }) {
10 | const theme = useContext(ThemeContext)
11 | return (
12 |
22 | {trade.route.path.map((token, i, path) => {
23 | const isLastItem: boolean = i === path.length - 1
24 | return (
25 |
26 |
27 |
28 |
29 | {token.symbol}
30 |
31 |
32 | {isLastItem ? null : }
33 |
34 | )
35 | })}
36 |
37 | )
38 | })
39 |
--------------------------------------------------------------------------------
/src/components/swap/TradePrice.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Price } from '@uniswap/sdk'
3 | import { useContext } from 'react'
4 | import { Repeat } from 'react-feather'
5 | import { Text } from 'rebass'
6 | import { ThemeContext } from 'styled-components'
7 | import { StyledBalanceMaxMini } from './styleds'
8 |
9 | interface TradePriceProps {
10 | price?: Price
11 | showInverted: boolean
12 | setShowInverted: (showInverted: boolean) => void
13 | }
14 |
15 | export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
16 | const theme = useContext(ThemeContext)
17 |
18 | const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
19 |
20 | const show = Boolean(price?.baseCurrency && price?.quoteCurrency)
21 | const label = showInverted
22 | ? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
23 | : `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
24 |
25 | return (
26 |
32 | {show ? (
33 | <>
34 | {formattedPrice ?? '-'} {label}
35 | setShowInverted(!showInverted)}>
36 |
37 |
38 | >
39 | ) : (
40 | '-'
41 | )}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/swap/confirmPriceImpactWithoutFee.ts:
--------------------------------------------------------------------------------
1 | import { Percent } from '@uniswap/sdk'
2 | import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants'
3 |
4 | /**
5 | * Given the price impact, get user confirmation.
6 | *
7 | * @param priceImpactWithoutFee price impact of the trade without the fee.
8 | */
9 | export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean {
10 | if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) {
11 | return (
12 | window.prompt(
13 | `This swap has a price impact of at least ${PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN.toFixed(
14 | 0
15 | )}%. Please type the word "confirm" to continue with this swap.`
16 | ) === 'confirm'
17 | )
18 | } else if (!priceImpactWithoutFee.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) {
19 | return window.confirm(
20 | `This swap has a price impact of at least ${ALLOWED_PRICE_IMPACT_HIGH.toFixed(
21 | 0
22 | )}%. Please confirm that you would like to continue with this swap.`
23 | )
24 | }
25 | return true
26 | }
27 |
--------------------------------------------------------------------------------
/src/connectors/Fortmatic.ts:
--------------------------------------------------------------------------------
1 | import { ChainId } from '@uniswap/sdk'
2 | import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
3 |
4 | export const OVERLAY_READY = 'OVERLAY_READY'
5 |
6 | type FormaticSupportedChains = Extract
7 |
8 | const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = {
9 | [ChainId.MAINNET]: undefined,
10 | [ChainId.ROPSTEN]: 'ropsten',
11 | [ChainId.RINKEBY]: 'rinkeby',
12 | [ChainId.KOVAN]: 'kovan'
13 | }
14 |
15 | export class FortmaticConnector extends FortmaticConnectorCore {
16 | async activate() {
17 | if (!this.fortmatic) {
18 | const { default: Fortmatic } = await import('fortmatic')
19 |
20 | const { apiKey, chainId } = this as any
21 | if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
22 | this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
23 | } else {
24 | throw new Error(`Unsupported network ID: ${chainId}`)
25 | }
26 | }
27 |
28 | const provider = this.fortmatic.getProvider()
29 |
30 | const pollForOverlayReady = new Promise(resolve => {
31 | const interval = setInterval(() => {
32 | if (provider.overlayReady) {
33 | clearInterval(interval)
34 | this.emit(OVERLAY_READY)
35 | resolve()
36 | }
37 | }, 200)
38 | })
39 |
40 | const [account] = await Promise.all([
41 | provider.enable().then((accounts: string[]) => accounts[0]),
42 | pollForOverlayReady
43 | ])
44 |
45 | return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/connectors/fortmatic.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'formatic'
2 |
--------------------------------------------------------------------------------
/src/connectors/index.ts:
--------------------------------------------------------------------------------
1 | import { Web3Provider } from '@ethersproject/providers'
2 | import { InjectedConnector } from '@web3-react/injected-connector'
3 | import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
4 | import { WalletLinkConnector } from '@web3-react/walletlink-connector'
5 | import { PortisConnector } from '@web3-react/portis-connector'
6 |
7 | import { FortmaticConnector } from './Fortmatic'
8 | import { NetworkConnector } from './NetworkConnector'
9 |
10 | const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
11 | const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
12 | const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
13 |
14 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1')
15 |
16 | if (typeof NETWORK_URL === 'undefined') {
17 | throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
18 | }
19 |
20 | export const network = new NetworkConnector({
21 | urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }
22 | })
23 |
24 | let networkLibrary: Web3Provider | undefined
25 | export function getNetworkLibrary(): Web3Provider {
26 | return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any))
27 | }
28 |
29 | export const injected = new InjectedConnector({
30 | supportedChainIds: [1, 3, 4, 5, 42]
31 | })
32 |
33 | // mainnet only
34 | export const walletconnect = new WalletConnectConnector({
35 | rpc: { 1: NETWORK_URL },
36 | bridge: 'https://bridge.walletconnect.org',
37 | qrcode: true,
38 | pollingInterval: 15000
39 | })
40 |
41 | // mainnet only
42 | export const fortmatic = new FortmaticConnector({
43 | apiKey: FORMATIC_KEY ?? '',
44 | chainId: 1
45 | })
46 |
47 | // mainnet only
48 | export const portis = new PortisConnector({
49 | dAppId: PORTIS_ID ?? '',
50 | networks: [1]
51 | })
52 |
53 | // mainnet only
54 | export const walletlink = new WalletLinkConnector({
55 | url: NETWORK_URL,
56 | appName: 'Orion',
57 | appLogoUrl:
58 | 'https://mpng.pngfly.com/20181202/bex/kisspng-emoji-domain-unicorn-pin-badges-sticker-unicorn-tumblr-emoji-unicorn-iphoneemoji-5c046729264a77.5671679315437924251569.jpg'
59 | })
60 |
--------------------------------------------------------------------------------
/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.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/lists.ts:
--------------------------------------------------------------------------------
1 | // the Uniswap Default token list lives here
2 | export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
3 |
4 | export const DEFAULT_LIST_OF_LISTS: string[] = [
5 | DEFAULT_TOKEN_LIST_URL,
6 | 't2crtokens.eth', // kleros
7 | 'tokens.1inch.eth', // 1inch
8 | 'synths.snx.eth',
9 | 'tokenlist.dharma.eth',
10 | 'defi.cmc.eth',
11 | 'erc20.cmc.eth',
12 | 'stablecoin.cmc.eth',
13 | 'tokenlist.zerion.eth',
14 | 'tokenlist.aave.eth',
15 | 'https://tokens.coingecko.com/uniswap/all.json',
16 | 'https://app.tryroll.com/tokens.json',
17 | 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json',
18 | 'https://defiprime.com/defiprime.tokenlist.json',
19 | 'https://umaproject.org/uma.tokenlist.json'
20 | ]
21 |
--------------------------------------------------------------------------------
/src/constants/multicall/index.ts:
--------------------------------------------------------------------------------
1 | import { ChainId } from '@uniswap/sdk'
2 | import MULTICALL_ABI from './abi.json'
3 |
4 | const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {
5 | [ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
6 | [ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',
7 | [ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
8 | [ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',
9 | [ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e'
10 | }
11 |
12 | export { MULTICALL_ABI, MULTICALL_NETWORKS }
13 |
--------------------------------------------------------------------------------
/src/constants/v1/index.ts:
--------------------------------------------------------------------------------
1 | import { Interface } from '@ethersproject/abi'
2 | import { ChainId } from '@uniswap/sdk'
3 | import V1_EXCHANGE_ABI from './v1_exchange.json'
4 | import V1_FACTORY_ABI from './v1_factory.json'
5 |
6 | const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
7 | [ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
8 | [ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
9 | [ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
10 | [ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
11 | [ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
12 | }
13 |
14 | const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
15 | const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
16 |
17 | export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
18 |
--------------------------------------------------------------------------------
/src/constants/v1/v1_factory.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "NewExchange",
4 | "inputs": [
5 | { "type": "address", "name": "token", "indexed": true },
6 | { "type": "address", "name": "exchange", "indexed": true }
7 | ],
8 | "anonymous": false,
9 | "type": "event"
10 | },
11 | {
12 | "name": "initializeFactory",
13 | "outputs": [],
14 | "inputs": [{ "type": "address", "name": "template" }],
15 | "constant": false,
16 | "payable": false,
17 | "type": "function"
18 | },
19 | {
20 | "name": "createExchange",
21 | "outputs": [{ "type": "address", "name": "out" }],
22 | "inputs": [{ "type": "address", "name": "token" }],
23 | "constant": false,
24 | "payable": false,
25 | "type": "function"
26 | },
27 | {
28 | "name": "getExchange",
29 | "outputs": [{ "type": "address", "name": "out" }],
30 | "inputs": [{ "type": "address", "name": "token" }],
31 | "constant": true,
32 | "payable": false,
33 | "type": "function"
34 | },
35 | {
36 | "name": "getToken",
37 | "outputs": [{ "type": "address", "name": "out" }],
38 | "inputs": [{ "type": "address", "name": "exchange" }],
39 | "constant": true,
40 | "payable": false,
41 | "type": "function"
42 | },
43 | {
44 | "name": "getTokenWithId",
45 | "outputs": [{ "type": "address", "name": "out" }],
46 | "inputs": [{ "type": "uint256", "name": "token_id" }],
47 | "constant": true,
48 | "payable": false,
49 | "type": "function"
50 | },
51 | {
52 | "name": "exchangeTemplate",
53 | "outputs": [{ "type": "address", "name": "out" }],
54 | "inputs": [],
55 | "constant": true,
56 | "payable": false,
57 | "type": "function"
58 | },
59 | {
60 | "name": "tokenCount",
61 | "outputs": [{ "type": "uint256", "name": "out" }],
62 | "inputs": [],
63 | "constant": true,
64 | "payable": false,
65 | "type": "function"
66 | }
67 | ]
68 |
--------------------------------------------------------------------------------
/src/data/Allowances.ts:
--------------------------------------------------------------------------------
1 | import { Token, TokenAmount } from '@uniswap/sdk'
2 | import { useMemo } from 'react'
3 |
4 | import { useTokenContract } from '../hooks/useContract'
5 | import { useSingleCallResult } from '../state/multicall/hooks'
6 |
7 | export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount | undefined {
8 | const contract = useTokenContract(token?.address, false)
9 |
10 | const inputs = useMemo(() => [owner, spender], [owner, spender])
11 | const allowance = useSingleCallResult(contract, 'allowance', inputs).result
12 |
13 | return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
14 | token,
15 | allowance
16 | ])
17 | }
18 |
--------------------------------------------------------------------------------
/src/data/Reserves.ts:
--------------------------------------------------------------------------------
1 | import { TokenAmount, Pair, Currency } from '@uniswap/sdk'
2 | import { useMemo } from 'react'
3 | import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
4 | import { Interface } from '@ethersproject/abi'
5 | import { useActiveWeb3React } from '../hooks'
6 |
7 | import { useMultipleContractSingleData } from '../state/multicall/hooks'
8 | import { wrappedCurrency } from '../utils/wrappedCurrency'
9 |
10 | const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
11 |
12 | export enum PairState {
13 | LOADING,
14 | NOT_EXISTS,
15 | EXISTS,
16 | INVALID
17 | }
18 |
19 | export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] {
20 | const { chainId } = useActiveWeb3React()
21 |
22 | const tokens = useMemo(
23 | () =>
24 | currencies.map(([currencyA, currencyB]) => [
25 | wrappedCurrency(currencyA, chainId),
26 | wrappedCurrency(currencyB, chainId)
27 | ]),
28 | [chainId, currencies]
29 | )
30 |
31 | const pairAddresses = useMemo(
32 | () =>
33 | tokens.map(([tokenA, tokenB]) => {
34 | return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
35 | }),
36 | [tokens]
37 | )
38 |
39 | const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
40 |
41 | return useMemo(() => {
42 | return results.map((result, i) => {
43 | const { result: reserves, loading } = result
44 | const tokenA = tokens[i][0]
45 | const tokenB = tokens[i][1]
46 |
47 | if (loading) return [PairState.LOADING, null]
48 | if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]
49 | if (!reserves) return [PairState.NOT_EXISTS, null]
50 | const { reserve0, reserve1 } = reserves
51 | const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
52 | return [
53 | PairState.EXISTS,
54 | new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
55 | ]
56 | })
57 | }, [results, tokens])
58 | }
59 |
60 | export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] {
61 | return usePairs([[tokenA, tokenB]])[0]
62 | }
63 |
--------------------------------------------------------------------------------
/src/data/TotalSupply.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from '@ethersproject/bignumber'
2 | import { Token, TokenAmount } from '@uniswap/sdk'
3 | import { useTokenContract } from '../hooks/useContract'
4 | import { useSingleCallResult } from '../state/multicall/hooks'
5 |
6 | // returns undefined if input token is undefined, or fails to get token contract,
7 | // or contract total supply cannot be fetched
8 | export function useTotalSupply(token?: Token): TokenAmount | undefined {
9 | const contract = useTokenContract(token?.address, false)
10 |
11 | const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
12 |
13 | return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/useColor.ts:
--------------------------------------------------------------------------------
1 | import { useState, useLayoutEffect } from 'react'
2 | import { shade } from 'polished'
3 | import Vibrant from 'node-vibrant'
4 | import { hex } from 'wcag-contrast'
5 | import { Token, ChainId } from '@uniswap/sdk'
6 |
7 | async function getColorFromToken(token: Token): Promise {
8 | if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
9 | return Promise.resolve('#FAAB14')
10 | }
11 |
12 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${token.address}/logo.png`
13 |
14 | return Vibrant.from(path)
15 | .getPalette()
16 | .then(palette => {
17 | if (palette?.Vibrant) {
18 | let detectedHex = palette.Vibrant.hex
19 | let AAscore = hex(detectedHex, '#FFF')
20 | while (AAscore < 3) {
21 | detectedHex = shade(0.005, detectedHex)
22 | AAscore = hex(detectedHex, '#FFF')
23 | }
24 | return detectedHex
25 | }
26 | return null
27 | })
28 | .catch(() => null)
29 | }
30 |
31 | export function useColor(token?: Token) {
32 | const [color, setColor] = useState('#2172E5')
33 |
34 | useLayoutEffect(() => {
35 | let stale = false
36 |
37 | if (token) {
38 | getColorFromToken(token).then(tokenColor => {
39 | if (!stale && tokenColor !== null) {
40 | setColor(tokenColor)
41 | }
42 | })
43 | }
44 |
45 | return () => {
46 | stale = true
47 | setColor('#2172E5')
48 | }
49 | }, [token])
50 |
51 | return color
52 | }
53 |
--------------------------------------------------------------------------------
/src/hooks/useCopyClipboard.ts:
--------------------------------------------------------------------------------
1 | import copy from 'copy-to-clipboard'
2 | import { useCallback, useEffect, useState } from 'react'
3 |
4 | export default function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] {
5 | const [isCopied, setIsCopied] = useState(false)
6 |
7 | const staticCopy = useCallback(text => {
8 | const didCopy = copy(text)
9 | setIsCopied(didCopy)
10 | }, [])
11 |
12 | useEffect(() => {
13 | if (isCopied) {
14 | const hide = setTimeout(() => {
15 | setIsCopied(false)
16 | }, timeout)
17 |
18 | return () => {
19 | clearTimeout(hide)
20 | }
21 | }
22 | return undefined
23 | }, [isCopied, setIsCopied, timeout])
24 |
25 | return [isCopied, staticCopy]
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentBlockTimestamp.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { useSingleCallResult } from '../state/multicall/hooks'
3 | import { useMulticallContract } from './useContract'
4 |
5 | // gets the current timestamp from the blockchain
6 | export default function useCurrentBlockTimestamp(): BigNumber | undefined {
7 | const multicall = useMulticallContract()
8 | return useSingleCallResult(multicall, 'getCurrentBlockTimestamp')?.result?.[0]
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | // modified from https://usehooks.com/useDebounce/
4 | export default function useDebounce(value: T, delay: number): T {
5 | const [debouncedValue, setDebouncedValue] = useState(value)
6 |
7 | useEffect(() => {
8 | // Update debounced value after delay
9 | const handler = setTimeout(() => {
10 | setDebouncedValue(value)
11 | }, delay)
12 |
13 | // Cancel the timeout if value changes (also on delay change or unmount)
14 | // This is how we prevent debounced value from updating if value is changed ...
15 | // .. within the delay period. Timeout gets cleared and restarted.
16 | return () => {
17 | clearTimeout(handler)
18 | }
19 | }, [value, delay])
20 |
21 | return debouncedValue
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useENS.ts:
--------------------------------------------------------------------------------
1 | import { isAddress } from '../utils'
2 | import useENSAddress from './useENSAddress'
3 | import useENSName from './useENSName'
4 |
5 | /**
6 | * Given a name or address, does a lookup to resolve to an address and name
7 | * @param nameOrAddress ENS name or address
8 | */
9 | export default function useENS(
10 | nameOrAddress?: string | null
11 | ): { loading: boolean; address: string | null; name: string | null } {
12 | const validated = isAddress(nameOrAddress)
13 | const reverseLookup = useENSName(validated ? validated : undefined)
14 | const lookup = useENSAddress(nameOrAddress)
15 |
16 | return {
17 | loading: reverseLookup.loading || lookup.loading,
18 | address: validated ? validated : lookup.address,
19 | name: reverseLookup.ENSName ? reverseLookup.ENSName : !validated && lookup.address ? nameOrAddress || null : null
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useENSAddress.ts:
--------------------------------------------------------------------------------
1 | import { namehash } from 'ethers/lib/utils'
2 | import { useMemo } from 'react'
3 | import { useSingleCallResult } from '../state/multicall/hooks'
4 | import isZero from '../utils/isZero'
5 | import { useENSRegistrarContract, useENSResolverContract } from './useContract'
6 | import useDebounce from './useDebounce'
7 |
8 | /**
9 | * Does a lookup for an ENS name to find its address.
10 | */
11 | export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } {
12 | const debouncedName = useDebounce(ensName, 200)
13 | const ensNodeArgument = useMemo(() => {
14 | if (!debouncedName) return [undefined]
15 | try {
16 | return debouncedName ? [namehash(debouncedName)] : [undefined]
17 | } catch (error) {
18 | return [undefined]
19 | }
20 | }, [debouncedName])
21 | const registrarContract = useENSRegistrarContract(false)
22 | const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
23 | const resolverAddressResult = resolverAddress.result?.[0]
24 | const resolverContract = useENSResolverContract(
25 | resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
26 | false
27 | )
28 | const addr = useSingleCallResult(resolverContract, 'addr', ensNodeArgument)
29 |
30 | const changed = debouncedName !== ensName
31 | return {
32 | address: changed ? null : addr.result?.[0] ?? null,
33 | loading: changed || resolverAddress.loading || addr.loading
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/hooks/useENSContentHash.ts:
--------------------------------------------------------------------------------
1 | import { namehash } from 'ethers/lib/utils'
2 | import { useMemo } from 'react'
3 | import { useSingleCallResult } from '../state/multicall/hooks'
4 | import isZero from '../utils/isZero'
5 | import { useENSRegistrarContract, useENSResolverContract } from './useContract'
6 |
7 | /**
8 | * Does a lookup for an ENS name to find its contenthash.
9 | */
10 | export default function useENSContentHash(ensName?: string | null): { loading: boolean; contenthash: string | null } {
11 | const ensNodeArgument = useMemo(() => {
12 | if (!ensName) return [undefined]
13 | try {
14 | return ensName ? [namehash(ensName)] : [undefined]
15 | } catch (error) {
16 | return [undefined]
17 | }
18 | }, [ensName])
19 | const registrarContract = useENSRegistrarContract(false)
20 | const resolverAddressResult = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
21 | const resolverAddress = resolverAddressResult.result?.[0]
22 | const resolverContract = useENSResolverContract(
23 | resolverAddress && isZero(resolverAddress) ? undefined : resolverAddress,
24 | false
25 | )
26 | const contenthash = useSingleCallResult(resolverContract, 'contenthash', ensNodeArgument)
27 |
28 | return {
29 | contenthash: contenthash.result?.[0] ?? null,
30 | loading: resolverAddressResult.loading || contenthash.loading
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/hooks/useENSName.ts:
--------------------------------------------------------------------------------
1 | import { namehash } from 'ethers/lib/utils'
2 | import { useMemo } from 'react'
3 | import { useSingleCallResult } from '../state/multicall/hooks'
4 | import { isAddress } from '../utils'
5 | import isZero from '../utils/isZero'
6 | import { useENSRegistrarContract, useENSResolverContract } from './useContract'
7 | import useDebounce from './useDebounce'
8 |
9 | /**
10 | * Does a reverse lookup for an address to find its ENS name.
11 | * Note this is not the same as looking up an ENS name to find an address.
12 | */
13 | export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
14 | const debouncedAddress = useDebounce(address, 200)
15 | const ensNodeArgument = useMemo(() => {
16 | if (!debouncedAddress || !isAddress(debouncedAddress)) return [undefined]
17 | try {
18 | return debouncedAddress ? [namehash(`${debouncedAddress.toLowerCase().substr(2)}.addr.reverse`)] : [undefined]
19 | } catch (error) {
20 | return [undefined]
21 | }
22 | }, [debouncedAddress])
23 | const registrarContract = useENSRegistrarContract(false)
24 | const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
25 | const resolverAddressResult = resolverAddress.result?.[0]
26 | const resolverContract = useENSResolverContract(
27 | resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
28 | false
29 | )
30 | const name = useSingleCallResult(resolverContract, 'name', ensNodeArgument)
31 |
32 | const changed = debouncedAddress !== address
33 | return {
34 | ENSName: changed ? null : name.result?.[0] ?? null,
35 | loading: changed || resolverAddress.loading || name.loading
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/hooks/useFetchListCallback.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from '@reduxjs/toolkit'
2 | import { ChainId } from '@uniswap/sdk'
3 | import { TokenList } from '@uniswap/token-lists'
4 | import { useCallback } from 'react'
5 | import { useDispatch } from 'react-redux'
6 | import { getNetworkLibrary, NETWORK_CHAIN_ID } from '../connectors'
7 | import { AppDispatch } from '../state'
8 | import { fetchTokenList } from '../state/lists/actions'
9 | import getTokenList from '../utils/getTokenList'
10 | import resolveENSContentHash from '../utils/resolveENSContentHash'
11 | import { useActiveWeb3React } from './index'
12 |
13 | export function useFetchListCallback(): (listUrl: string) => Promise {
14 | const { chainId, library } = useActiveWeb3React()
15 | const dispatch = useDispatch()
16 |
17 | const ensResolver = useCallback(
18 | (ensName: string) => {
19 | if (!library || chainId !== ChainId.MAINNET) {
20 | if (NETWORK_CHAIN_ID === ChainId.MAINNET) {
21 | const networkLibrary = getNetworkLibrary()
22 | if (networkLibrary) {
23 | return resolveENSContentHash(ensName, networkLibrary)
24 | }
25 | }
26 | throw new Error('Could not construct mainnet ENS resolver')
27 | }
28 | return resolveENSContentHash(ensName, library)
29 | },
30 | [chainId, library]
31 | )
32 |
33 | return useCallback(
34 | async (listUrl: string) => {
35 | const requestId = nanoid()
36 | dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
37 | return getTokenList(listUrl, ensResolver)
38 | .then(tokenList => {
39 | dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
40 | return tokenList
41 | })
42 | .catch(error => {
43 | console.debug(`Failed to get list at url ${listUrl}`, error)
44 | dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
45 | throw error
46 | })
47 | },
48 | [dispatch, ensResolver]
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/hooks/useHttpLocations.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import contenthashToUri from '../utils/contenthashToUri'
3 | import { parseENSAddress } from '../utils/parseENSAddress'
4 | import uriToHttp from '../utils/uriToHttp'
5 | import useENSContentHash from './useENSContentHash'
6 |
7 | export default function useHttpLocations(uri: string | undefined): string[] {
8 | const ens = useMemo(() => (uri ? parseENSAddress(uri) : undefined), [uri])
9 | const resolvedContentHash = useENSContentHash(ens?.ensName)
10 | return useMemo(() => {
11 | if (ens) {
12 | return resolvedContentHash.contenthash ? uriToHttp(contenthashToUri(resolvedContentHash.contenthash)) : []
13 | } else {
14 | return uri ? uriToHttp(uri) : []
15 | }
16 | }, [ens, resolvedContentHash.contenthash, uri])
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/useInterval.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | export default function useInterval(callback: () => void, delay: null | number, leading = true) {
4 | const savedCallback = useRef<() => void>()
5 |
6 | // Remember the latest callback.
7 | useEffect(() => {
8 | savedCallback.current = callback
9 | }, [callback])
10 |
11 | // Set up the interval.
12 | useEffect(() => {
13 | function tick() {
14 | const current = savedCallback.current
15 | current && current()
16 | }
17 |
18 | if (delay !== null) {
19 | if (leading) tick()
20 | const id = setInterval(tick, delay)
21 | return () => clearInterval(id)
22 | }
23 | return undefined
24 | }, [delay, leading])
25 | }
26 |
--------------------------------------------------------------------------------
/src/hooks/useIsArgentWallet.ts:
--------------------------------------------------------------------------------
1 | import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
2 | import { useActiveWeb3React } from './index'
3 | import { useArgentWalletDetectorContract } from './useContract'
4 |
5 | export default function useIsArgentWallet(): boolean {
6 | const { account } = useActiveWeb3React()
7 | const argentWalletDetector = useArgentWalletDetectorContract()
8 | const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', [account ?? undefined], NEVER_RELOAD)
9 | return call?.result?.[0] ?? false
10 | }
11 |
--------------------------------------------------------------------------------
/src/hooks/useIsWindowVisible.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react'
2 |
3 | const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
4 |
5 | function isWindowVisible() {
6 | return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
7 | }
8 |
9 | /**
10 | * Returns whether the window is currently visible to the user.
11 | */
12 | export default function useIsWindowVisible(): boolean {
13 | const [focused, setFocused] = useState(isWindowVisible())
14 | const listener = useCallback(() => {
15 | setFocused(isWindowVisible())
16 | }, [setFocused])
17 |
18 | useEffect(() => {
19 | if (!VISIBILITY_STATE_SUPPORTED) return undefined
20 |
21 | document.addEventListener('visibilitychange', listener)
22 | return () => {
23 | document.removeEventListener('visibilitychange', listener)
24 | }
25 | }, [listener])
26 |
27 | return focused
28 | }
29 |
--------------------------------------------------------------------------------
/src/hooks/useLast.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | /**
4 | * Returns the last value of type T that passes a filter function
5 | * @param value changing value
6 | * @param filterFn function that determines whether a given value should be considered for the last value
7 | */
8 | export default function useLast(
9 | value: T | undefined | null,
10 | filterFn?: (value: T | null | undefined) => boolean
11 | ): T | null | undefined {
12 | const [last, setLast] = useState(filterFn && filterFn(value) ? value : undefined)
13 | useEffect(() => {
14 | setLast(last => {
15 | const shouldUse: boolean = filterFn ? filterFn(value) : true
16 | if (shouldUse) return value
17 | return last
18 | })
19 | }, [filterFn, value])
20 | return last
21 | }
22 |
23 | function isDefined(x: T | null | undefined): x is T {
24 | return x !== null && x !== undefined
25 | }
26 |
27 | /**
28 | * Returns the last truthy value of type T
29 | * @param value changing value
30 | */
31 | export function useLastTruthy(value: T | undefined | null): T | null | undefined {
32 | return useLast(value, isDefined)
33 | }
34 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useRef } from 'react'
2 |
3 | export function useOnClickOutside(
4 | node: RefObject,
5 | handler: undefined | (() => void)
6 | ) {
7 | const handlerRef = useRef void)>(handler)
8 | useEffect(() => {
9 | handlerRef.current = handler
10 | }, [handler])
11 |
12 | useEffect(() => {
13 | const handleClickOutside = (e: MouseEvent) => {
14 | if (node.current?.contains(e.target as Node) ?? false) {
15 | return
16 | }
17 | if (handlerRef.current) handlerRef.current()
18 | }
19 |
20 | document.addEventListener('mousedown', handleClickOutside)
21 |
22 | return () => {
23 | document.removeEventListener('mousedown', handleClickOutside)
24 | }
25 | }, [node])
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useParsedQueryString.ts:
--------------------------------------------------------------------------------
1 | import { parse, ParsedQs } from 'qs'
2 | import { useMemo } from 'react'
3 | import { useLocation } from 'react-router-dom'
4 |
5 | export default function useParsedQueryString(): ParsedQs {
6 | const { search } = useLocation()
7 | return useMemo(
8 | () => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
9 | [search]
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | // modified from https://usehooks.com/usePrevious/
4 | export default function usePrevious(value: T) {
5 | // The ref object is a generic container whose current property is mutable ...
6 | // ... and can hold any value, similar to an instance property on a class
7 | const ref = useRef()
8 |
9 | // Store current value in ref
10 | useEffect(() => {
11 | ref.current = value
12 | }, [value]) // Only re-run if value changes
13 |
14 | // Return previous value (happens before update in useEffect above)
15 | return ref.current
16 | }
17 |
--------------------------------------------------------------------------------
/src/hooks/useSocksBalance.ts:
--------------------------------------------------------------------------------
1 | import { JSBI } from '@uniswap/sdk'
2 | import { useMemo } from 'react'
3 | import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
4 | import { useActiveWeb3React } from './index'
5 | import { useSocksController } from './useContract'
6 |
7 | export default function useSocksBalance(): JSBI | undefined {
8 | const { account } = useActiveWeb3React()
9 | const socksContract = useSocksController()
10 |
11 | const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD)
12 | const data = result?.[0]
13 | return data ? JSBI.BigInt(data.toString()) : undefined
14 | }
15 |
16 | export function useHasSocks(): boolean | undefined {
17 | const balance = useSocksBalance()
18 | return useMemo(() => balance && JSBI.greaterThan(balance, JSBI.BigInt(0)), [balance])
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useTimestampFromBlock.ts:
--------------------------------------------------------------------------------
1 | import { useActiveWeb3React } from '.'
2 | import { useState, useEffect } from 'react'
3 |
4 | export function useTimestampFromBlock(block: number | undefined): number | undefined {
5 | const { library } = useActiveWeb3React()
6 | const [timestamp, setTimestamp] = useState()
7 | useEffect(() => {
8 | async function fetchTimestamp() {
9 | if (block) {
10 | const blockData = await library?.getBlock(block)
11 | blockData && setTimestamp(blockData.timestamp)
12 | }
13 | }
14 | if (!timestamp) {
15 | fetchTimestamp()
16 | }
17 | }, [block, library, timestamp])
18 | return timestamp
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | export default function useToggle(initialState = false): [boolean, () => void] {
4 | const [state, setState] = useState(initialState)
5 | const toggle = useCallback(() => setState(state => !state), [])
6 | return [state, toggle]
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/useToggledVersion.ts:
--------------------------------------------------------------------------------
1 | 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/useTransactionDeadline.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { useMemo } from 'react'
3 | import { useSelector } from 'react-redux'
4 | import { AppState } from '../state'
5 | import useCurrentBlockTimestamp from './useCurrentBlockTimestamp'
6 |
7 | // combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction
8 | export default function useTransactionDeadline(): BigNumber | undefined {
9 | const ttl = useSelector(state => state.user.userDeadline)
10 | const blockTimestamp = useCurrentBlockTimestamp()
11 | return useMemo(() => {
12 | if (blockTimestamp && ttl) return blockTimestamp.add(ttl)
13 | return undefined
14 | }, [blockTimestamp, ttl])
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 { createWeb3ReactRoot, Web3ReactProvider } from '@web3-react/core'
2 | import 'inter-ui'
3 | import React, { StrictMode } from 'react'
4 | import { isMobile } from 'react-device-detect'
5 | import ReactDOM from 'react-dom'
6 | import ReactGA from 'react-ga'
7 | import { Provider } from 'react-redux'
8 | import { HashRouter } from 'react-router-dom'
9 | import { NetworkContextName } from './constants'
10 | import './i18n'
11 | import App from './pages/App'
12 | import store from './state'
13 | import ApplicationUpdater from './state/application/updater'
14 | import ListsUpdater from './state/lists/updater'
15 | import MulticallUpdater from './state/multicall/updater'
16 | import TransactionUpdater from './state/transactions/updater'
17 | import UserUpdater from './state/user/updater'
18 | import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
19 | import getLibrary from './utils/getLibrary'
20 |
21 | const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
22 |
23 | if ('ethereum' in window) {
24 | ;(window.ethereum as any).autoRefreshOnNetworkChange = false
25 | }
26 |
27 | const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
28 | if (typeof GOOGLE_ANALYTICS_ID === 'string') {
29 | ReactGA.initialize(GOOGLE_ANALYTICS_ID)
30 | ReactGA.set({
31 | customBrowserType: !isMobile ? 'desktop' : 'web3' in window || 'ethereum' in window ? 'mobileWeb3' : 'mobileRegular'
32 | })
33 | } else {
34 | ReactGA.initialize('test', { testMode: true, debug: true })
35 | }
36 |
37 | window.addEventListener('error', error => {
38 | ReactGA.exception({
39 | description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
40 | fatal: true
41 | })
42 | })
43 |
44 | function Updaters() {
45 | return (
46 | <>
47 |
48 |
49 |
50 |
51 |
52 | >
53 | )
54 | }
55 |
56 | ReactDOM.render(
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ,
73 | document.getElementById('root')
74 | )
75 |
--------------------------------------------------------------------------------
/src/pages/AddLiquidity/ConfirmAddModalBottom.tsx:
--------------------------------------------------------------------------------
1 | import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk'
2 | import React from 'react'
3 | import { Text } from 'rebass'
4 | import { ButtonPrimary } from '../../components/Button'
5 | import { RowBetween, RowFixed } from '../../components/Row'
6 | import CurrencyLogo from '../../components/CurrencyLogo'
7 | import { Field } from '../../state/mint/actions'
8 | import { TYPE } from '../../theme'
9 |
10 | export function ConfirmAddModalBottom({
11 | noLiquidity,
12 | price,
13 | currencies,
14 | parsedAmounts,
15 | poolTokenPercentage,
16 | onAdd
17 | }: {
18 | noLiquidity?: boolean
19 | price?: Fraction
20 | currencies: { [field in Field]?: Currency }
21 | parsedAmounts: { [field in Field]?: CurrencyAmount }
22 | poolTokenPercentage?: Percent
23 | onAdd: () => void
24 | }) {
25 | return (
26 | <>
27 |
28 | {currencies[Field.CURRENCY_A]?.symbol} Deposited
29 |
30 |
31 | {parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
32 |
33 |
34 |
35 | {currencies[Field.CURRENCY_B]?.symbol} Deposited
36 |
37 |
38 | {parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
39 |
40 |
41 |
42 | Rates
43 |
44 | {`1 ${currencies[Field.CURRENCY_A]?.symbol} = ${price?.toSignificant(4)} ${
45 | currencies[Field.CURRENCY_B]?.symbol
46 | }`}
47 |
48 |
49 |
50 |
51 | {`1 ${currencies[Field.CURRENCY_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
52 | currencies[Field.CURRENCY_A]?.symbol
53 | }`}
54 |
55 |
56 |
57 | Share of Pool:
58 | {noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%
59 |
60 |
61 |
62 | {noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
63 |
64 |
65 | >
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/AddLiquidity/PoolPriceBar.tsx:
--------------------------------------------------------------------------------
1 | import { Currency, Percent, Price } from '@uniswap/sdk'
2 | import React, { useContext } from 'react'
3 | import { Text } from 'rebass'
4 | import { ThemeContext } from 'styled-components'
5 | import { AutoColumn } from '../../components/Column'
6 | import { AutoRow } from '../../components/Row'
7 | import { ONE_BIPS } from '../../constants'
8 | import { Field } from '../../state/mint/actions'
9 | import { TYPE } from '../../theme'
10 |
11 | export function PoolPriceBar({
12 | currencies,
13 | noLiquidity,
14 | poolTokenPercentage,
15 | price
16 | }: {
17 | currencies: { [field in Field]?: Currency }
18 | noLiquidity?: boolean
19 | poolTokenPercentage?: Percent
20 | price?: Price
21 | }) {
22 | const theme = useContext(ThemeContext)
23 | return (
24 |
25 |
26 |
27 | {price?.toSignificant(6) ?? '-'}
28 |
29 | {currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
30 |
31 |
32 |
33 | {price?.invert()?.toSignificant(6) ?? '-'}
34 |
35 | {currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
36 |
37 |
38 |
39 |
40 | {noLiquidity && price
41 | ? '100'
42 | : (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
43 | %
44 |
45 |
46 | Share of Pool
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/AddLiquidity/redirects.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect, RouteComponentProps } from 'react-router-dom'
3 | import AddLiquidity from './index'
4 |
5 | export function RedirectToAddLiquidity() {
6 | return
7 | }
8 |
9 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
10 | export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) {
11 | const {
12 | match: {
13 | params: { currencyIdA }
14 | }
15 | } = props
16 | const match = currencyIdA.match(OLD_PATH_STRUCTURE)
17 | if (match?.length) {
18 | return
19 | }
20 |
21 | return
22 | }
23 |
24 | export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
25 | const {
26 | match: {
27 | params: { currencyIdA, currencyIdB }
28 | }
29 | } = props
30 | if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) {
31 | return
32 | }
33 | return
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/AppBody.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export const BodyWrapper = styled.div`
5 | position: relative;
6 | max-width: 420px;
7 | width: 100%;
8 | background: ${({ theme }) => theme.bg1};
9 | box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
10 | 0px 24px 32px rgba(0, 0, 0, 0.01);
11 | border-radius: 30px;
12 | padding: 1rem;
13 | `
14 |
15 | /**
16 | * The styled container element that wraps the content of most pages and the tabs.
17 | */
18 | export default function AppBody({ children }: { children: React.ReactNode }) {
19 | return {children}
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/Earn/Countdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react'
2 | import { STAKING_GENESIS, REWARDS_DURATION_DAYS } from '../../state/stake/hooks'
3 | import { TYPE } from '../../theme'
4 |
5 | const MINUTE = 60
6 | const HOUR = MINUTE * 60
7 | const DAY = HOUR * 24
8 | const REWARDS_DURATION = DAY * REWARDS_DURATION_DAYS
9 |
10 | export function Countdown({ exactEnd }: { exactEnd?: Date }) {
11 | // get end/beginning times
12 | const end = useMemo(() => (exactEnd ? Math.floor(exactEnd.getTime() / 1000) : STAKING_GENESIS + REWARDS_DURATION), [
13 | exactEnd
14 | ])
15 | const begin = useMemo(() => end - REWARDS_DURATION, [end])
16 |
17 | // get current time
18 | const [time, setTime] = useState(() => Math.floor(Date.now() / 1000))
19 | useEffect((): (() => void) | void => {
20 | // we only need to tick if rewards haven't ended yet
21 | if (time <= end) {
22 | const timeout = setTimeout(() => setTime(Math.floor(Date.now() / 1000)), 1000)
23 | return () => {
24 | clearTimeout(timeout)
25 | }
26 | }
27 | }, [time, end])
28 |
29 | const timeUntilGenesis = begin - time
30 | const timeUntilEnd = end - time
31 |
32 | let timeRemaining: number
33 | let message: string
34 | if (timeUntilGenesis >= 0) {
35 | message = 'Rewards begin in'
36 | timeRemaining = timeUntilGenesis
37 | } else {
38 | const ongoing = timeUntilEnd >= 0
39 | if (ongoing) {
40 | message = 'Rewards end in'
41 | timeRemaining = timeUntilEnd
42 | } else {
43 | message = 'Rewards have ended!'
44 | timeRemaining = Infinity
45 | }
46 | }
47 |
48 | message = 'Rewards begin in'
49 | timeRemaining = timeUntilEnd
50 |
51 | const days = (timeRemaining - (timeRemaining % DAY)) / DAY
52 | timeRemaining -= days * DAY
53 | const hours = (timeRemaining - (timeRemaining % HOUR)) / HOUR
54 | timeRemaining -= hours * HOUR
55 | const minutes = (timeRemaining - (timeRemaining % MINUTE)) / MINUTE
56 | timeRemaining -= minutes * MINUTE
57 | const seconds = timeRemaining
58 |
59 | return (
60 |
61 | {message}{' '}
62 | {Number.isFinite(timeRemaining) && (
63 |
64 | {`${days}:${hours.toString().padStart(2, '0')}:${minutes
65 | .toString()
66 | .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`}
67 |
68 | )}
69 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/src/pages/MigrateV1/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AutoColumn } from '../../components/Column'
3 | import { TYPE } from '../../theme'
4 |
5 | export function EmptyState({ message }: { message: string }) {
6 | return (
7 |
8 | {message}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/Pool/styleds.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from 'rebass'
2 | import styled from 'styled-components'
3 |
4 | export const Wrapper = styled.div`
5 | position: relative;
6 | `
7 |
8 | export const ClickableText = styled(Text)`
9 | :hover {
10 | cursor: pointer;
11 | }
12 | color: ${({ theme }) => theme.primary1};
13 | `
14 | export const MaxButton = styled.button<{ width: string }>`
15 | padding: 0.5rem 1rem;
16 | background-color: ${({ theme }) => theme.primary5};
17 | border: 1px solid ${({ theme }) => theme.primary5};
18 | border-radius: 0.5rem;
19 | font-size: 1rem;
20 | ${({ theme }) => theme.mediaWidth.upToSmall`
21 | padding: 0.25rem 0.5rem;
22 | `};
23 | font-weight: 500;
24 | cursor: pointer;
25 | margin: 0.25rem;
26 | overflow: hidden;
27 | color: ${({ theme }) => theme.primary1};
28 | :hover {
29 | border: 1px solid ${({ theme }) => theme.primary1};
30 | }
31 | :focus {
32 | border: 1px solid ${({ theme }) => theme.primary1};
33 | outline: none;
34 | }
35 | `
36 |
37 | export const Dots = styled.span`
38 | &::after {
39 | display: inline-block;
40 | animation: ellipsis 1.25s infinite;
41 | content: '.';
42 | width: 1em;
43 | text-align: left;
44 | }
45 | @keyframes ellipsis {
46 | 0% {
47 | content: '.';
48 | }
49 | 33% {
50 | content: '..';
51 | }
52 | 66% {
53 | content: '...';
54 | }
55 | }
56 | `
57 |
--------------------------------------------------------------------------------
/src/pages/RemoveLiquidity/redirects.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { RouteComponentProps, Redirect } from 'react-router-dom'
3 |
4 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
5 |
6 | export function RedirectOldRemoveLiquidityPathStructure({
7 | match: {
8 | params: { tokens }
9 | }
10 | }: RouteComponentProps<{ tokens: string }>) {
11 | if (!OLD_PATH_STRUCTURE.test(tokens)) {
12 | return
13 | }
14 | const [currency0, currency1] = tokens.split('-')
15 |
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/Swap/redirects.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { Redirect, RouteComponentProps } from 'react-router-dom'
4 | import { AppDispatch } from '../../state'
5 | import { ApplicationModal, setOpenModal } from '../../state/application/actions'
6 |
7 | // Redirects to swap but only replace the pathname
8 | export function RedirectPathToSwapOnly({ location }: RouteComponentProps) {
9 | return
10 | }
11 |
12 | // Redirects from the /swap/:outputCurrency path to the /swap?outputCurrency=:outputCurrency format
13 | export function RedirectToSwap(props: RouteComponentProps<{ outputCurrency: string }>) {
14 | const {
15 | location: { search },
16 | match: {
17 | params: { outputCurrency }
18 | }
19 | } = props
20 |
21 | return (
22 | 1
28 | // ? `${search}&outputCurrency=${outputCurrency}`
29 | // : `?outputCurrency=${outputCurrency}`
30 | }}
31 | />
32 | )
33 | }
34 |
35 | export function OpenClaimAddressModalAndRedirectToSwap(props: RouteComponentProps) {
36 | const dispatch = useDispatch()
37 | useEffect(() => {
38 | dispatch(setOpenModal(ApplicationModal.ADDRESS_CLAIM))
39 | }, [dispatch])
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/Vote/styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const handleColorType = (status?: any, theme?: any) => {
4 | switch (status) {
5 | case 'pending':
6 | return theme.blue1
7 | case 'active':
8 | return theme.blue1
9 | case 'succeeded':
10 | return theme.green1
11 | case 'defeated':
12 | return theme.red1
13 | case 'queued':
14 | return theme.text3
15 | case 'executed':
16 | return theme.green1
17 | case 'canceled':
18 | return theme.text3
19 | case 'expired':
20 | return theme.text3
21 | default:
22 | return theme.text3
23 | }
24 | }
25 |
26 | export const ProposalStatus = styled.span<{ status: string }>`
27 | font-size: 0.825rem;
28 | font-weight: 600;
29 | padding: 0.5rem;
30 | border-radius: 8px;
31 | color: ${({ status, theme }) => handleColorType(status, theme)};
32 | border: 1px solid ${({ status, theme }) => handleColorType(status, theme)};
33 | width: fit-content;
34 | justify-self: flex-end;
35 | text-transform: uppercase;
36 | `
37 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'jazzicon' {
4 | export default function(diameter: number, seed: number): HTMLElement
5 | }
6 |
7 | declare module 'fortmatic'
8 |
9 | interface Window {
10 | ethereum?: {
11 | isMetaMask?: true
12 | on?: (...args: any[]) => void
13 | removeListener?: (...args: any[]) => void
14 | }
15 | web3?: {}
16 | }
17 |
18 | declare module 'content-hash' {
19 | declare function decode(x: string): string
20 | declare function getCodec(x: string): string
21 | }
22 |
23 | declare module 'multihashes' {
24 | declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }
25 | declare function toB58String(hash: Uint8Array): string
26 | }
27 |
--------------------------------------------------------------------------------
/src/state/application/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 | import { TokenList } from '@uniswap/token-lists'
3 |
4 | export type PopupContent =
5 | | {
6 | txn: {
7 | hash: string
8 | success: boolean
9 | summary?: string
10 | }
11 | }
12 | | {
13 | listUpdate: {
14 | listUrl: string
15 | oldList: TokenList
16 | newList: TokenList
17 | auto: boolean
18 | }
19 | }
20 |
21 | export enum ApplicationModal {
22 | WALLET,
23 | SETTINGS,
24 | SELF_CLAIM,
25 | ADDRESS_CLAIM,
26 | CLAIM_POPUP,
27 | MENU
28 | }
29 |
30 | export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('application/updateBlockNumber')
31 | export const setOpenModal = createAction('application/setOpenModal')
32 | export const addPopup = createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>(
33 | 'application/addPopup'
34 | )
35 | export const removePopup = createAction<{ key: string }>('application/removePopup')
36 |
--------------------------------------------------------------------------------
/src/state/application/reducer.ts:
--------------------------------------------------------------------------------
1 | import { createReducer, nanoid } from '@reduxjs/toolkit'
2 | import { addPopup, PopupContent, removePopup, updateBlockNumber, ApplicationModal, setOpenModal } from './actions'
3 |
4 | type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
5 |
6 | export interface ApplicationState {
7 | readonly blockNumber: { readonly [chainId: number]: number }
8 | readonly popupList: PopupList
9 | readonly openModal: ApplicationModal | null
10 | }
11 |
12 | const initialState: ApplicationState = {
13 | blockNumber: {},
14 | popupList: [],
15 | openModal: null
16 | }
17 |
18 | export default createReducer(initialState, builder =>
19 | builder
20 | .addCase(updateBlockNumber, (state, action) => {
21 | const { chainId, blockNumber } = action.payload
22 | if (typeof state.blockNumber[chainId] !== 'number') {
23 | state.blockNumber[chainId] = blockNumber
24 | } else {
25 | state.blockNumber[chainId] = Math.max(blockNumber, state.blockNumber[chainId])
26 | }
27 | })
28 | .addCase(setOpenModal, (state, action) => {
29 | state.openModal = action.payload
30 | })
31 | .addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000 } }) => {
32 | state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([
33 | {
34 | key: key || nanoid(),
35 | show: true,
36 | content,
37 | removeAfterMs
38 | }
39 | ])
40 | })
41 | .addCase(removePopup, (state, { payload: { key } }) => {
42 | state.popupList.forEach(p => {
43 | if (p.key === key) {
44 | p.show = false
45 | }
46 | })
47 | })
48 | )
49 |
--------------------------------------------------------------------------------
/src/state/application/updater.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react'
2 | import { useActiveWeb3React } from '../../hooks'
3 | import useDebounce from '../../hooks/useDebounce'
4 | import useIsWindowVisible from '../../hooks/useIsWindowVisible'
5 | import { updateBlockNumber } from './actions'
6 | import { useDispatch } from 'react-redux'
7 |
8 | export default function Updater(): null {
9 | const { library, chainId } = useActiveWeb3React()
10 | const dispatch = useDispatch()
11 |
12 | const windowVisible = useIsWindowVisible()
13 |
14 | const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({
15 | chainId,
16 | blockNumber: null
17 | })
18 |
19 | const blockNumberCallback = useCallback(
20 | (blockNumber: number) => {
21 | setState(state => {
22 | if (chainId === state.chainId) {
23 | if (typeof state.blockNumber !== 'number') return { chainId, blockNumber }
24 | return { chainId, blockNumber: Math.max(blockNumber, state.blockNumber) }
25 | }
26 | return state
27 | })
28 | },
29 | [chainId, setState]
30 | )
31 |
32 | // attach/detach listeners
33 | useEffect(() => {
34 | if (!library || !chainId || !windowVisible) return undefined
35 |
36 | setState({ chainId, blockNumber: null })
37 |
38 | library
39 | .getBlockNumber()
40 | .then(blockNumberCallback)
41 | .catch(error => console.error(`Failed to get block number for chainId: ${chainId}`, error))
42 |
43 | library.on('block', blockNumberCallback)
44 | return () => {
45 | library.removeListener('block', blockNumberCallback)
46 | }
47 | }, [dispatch, chainId, library, blockNumberCallback, windowVisible])
48 |
49 | const debouncedState = useDebounce(state, 100)
50 |
51 | useEffect(() => {
52 | if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
53 | dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
54 | }, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])
55 |
56 | return null
57 | }
58 |
--------------------------------------------------------------------------------
/src/state/burn/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export enum Field {
4 | LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
5 | LIQUIDITY = 'LIQUIDITY',
6 | CURRENCY_A = 'CURRENCY_A',
7 | CURRENCY_B = 'CURRENCY_B'
8 | }
9 |
10 | export const typeInput = createAction<{ field: Field; typedValue: string }>('burn/typeInputBurn')
11 |
--------------------------------------------------------------------------------
/src/state/burn/reducer.ts:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 | import { Field, typeInput } from './actions'
3 |
4 | export interface BurnState {
5 | readonly independentField: Field
6 | readonly typedValue: string
7 | }
8 |
9 | const initialState: BurnState = {
10 | independentField: Field.LIQUIDITY_PERCENT,
11 | typedValue: '0'
12 | }
13 |
14 | export default createReducer(initialState, builder =>
15 | builder.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
16 | return {
17 | ...state,
18 | independentField: field,
19 | typedValue
20 | }
21 | })
22 | )
23 |
--------------------------------------------------------------------------------
/src/state/global/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | // fired once when the app reloads but before the app renders
4 | // allows any updates to be applied to store data loaded from localStorage
5 | export const updateVersion = createAction('global/updateVersion')
6 |
--------------------------------------------------------------------------------
/src/state/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
2 | import { save, load } from 'redux-localstorage-simple'
3 |
4 | import application from './application/reducer'
5 | import { updateVersion } from './global/actions'
6 | import user from './user/reducer'
7 | import transactions from './transactions/reducer'
8 | import swap from './swap/reducer'
9 | import mint from './mint/reducer'
10 | import lists from './lists/reducer'
11 | import burn from './burn/reducer'
12 | import multicall from './multicall/reducer'
13 |
14 | const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists']
15 |
16 | const store = configureStore({
17 | reducer: {
18 | application,
19 | user,
20 | transactions,
21 | swap,
22 | mint,
23 | burn,
24 | multicall,
25 | lists
26 | },
27 | middleware: [...getDefaultMiddleware({ thunk: false }), save({ states: PERSISTED_KEYS })],
28 | preloadedState: load({ states: PERSISTED_KEYS })
29 | })
30 |
31 | store.dispatch(updateVersion())
32 |
33 | export default store
34 |
35 | export type AppState = ReturnType
36 | export type AppDispatch = typeof store.dispatch
37 |
--------------------------------------------------------------------------------
/src/state/lists/actions.ts:
--------------------------------------------------------------------------------
1 | import { ActionCreatorWithPayload, createAction } from '@reduxjs/toolkit'
2 | import { TokenList, Version } from '@uniswap/token-lists'
3 |
4 | export const fetchTokenList: Readonly<{
5 | pending: ActionCreatorWithPayload<{ url: string; requestId: string }>
6 | fulfilled: ActionCreatorWithPayload<{ url: string; tokenList: TokenList; requestId: string }>
7 | rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }>
8 | }> = {
9 | pending: createAction('lists/fetchTokenList/pending'),
10 | fulfilled: createAction('lists/fetchTokenList/fulfilled'),
11 | rejected: createAction('lists/fetchTokenList/rejected')
12 | }
13 |
14 | export const acceptListUpdate = createAction('lists/acceptListUpdate')
15 | export const addList = createAction('lists/addList')
16 | export const removeList = createAction('lists/removeList')
17 | export const selectList = createAction('lists/selectList')
18 | export const rejectVersionUpdate = createAction('lists/rejectVersionUpdate')
19 |
--------------------------------------------------------------------------------
/src/state/mint/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export enum Field {
4 | CURRENCY_A = 'CURRENCY_A',
5 | CURRENCY_B = 'CURRENCY_B'
6 | }
7 |
8 | export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('mint/typeInputMint')
9 | export const resetMintState = createAction('mint/resetMintState')
10 |
--------------------------------------------------------------------------------
/src/state/mint/reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, Store } from 'redux'
2 |
3 | import { Field, typeInput } from './actions'
4 | import reducer, { MintState } from './reducer'
5 |
6 | describe('mint reducer', () => {
7 | let store: Store
8 |
9 | beforeEach(() => {
10 | store = createStore(reducer, {
11 | independentField: Field.CURRENCY_A,
12 | typedValue: '',
13 | otherTypedValue: ''
14 | })
15 | })
16 |
17 | describe('typeInput', () => {
18 | it('sets typed value', () => {
19 | store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false }))
20 | expect(store.getState()).toEqual({ independentField: Field.CURRENCY_A, typedValue: '1.0', otherTypedValue: '' })
21 | })
22 | it('clears other value', () => {
23 | store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false }))
24 | store.dispatch(typeInput({ field: Field.CURRENCY_B, typedValue: '1.0', noLiquidity: false }))
25 | expect(store.getState()).toEqual({ independentField: Field.CURRENCY_B, typedValue: '1.0', otherTypedValue: '' })
26 | })
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/state/mint/reducer.ts:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 | import { Field, resetMintState, typeInput } from './actions'
3 |
4 | export interface MintState {
5 | readonly independentField: Field
6 | readonly typedValue: string
7 | readonly otherTypedValue: string // for the case when there's no liquidity
8 | }
9 |
10 | const initialState: MintState = {
11 | independentField: Field.CURRENCY_A,
12 | typedValue: '',
13 | otherTypedValue: ''
14 | }
15 |
16 | export default createReducer(initialState, builder =>
17 | builder
18 | .addCase(resetMintState, () => initialState)
19 | .addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => {
20 | if (noLiquidity) {
21 | // they're typing into the field they've last typed in
22 | if (field === state.independentField) {
23 | return {
24 | ...state,
25 | independentField: field,
26 | typedValue
27 | }
28 | }
29 | // they're typing into a new field, store the other value
30 | else {
31 | return {
32 | ...state,
33 | independentField: field,
34 | typedValue,
35 | otherTypedValue: state.typedValue
36 | }
37 | }
38 | } else {
39 | return {
40 | ...state,
41 | independentField: field,
42 | typedValue,
43 | otherTypedValue: ''
44 | }
45 | }
46 | })
47 | )
48 |
--------------------------------------------------------------------------------
/src/state/multicall/actions.test.ts:
--------------------------------------------------------------------------------
1 | import { parseCallKey, toCallKey } from './actions'
2 |
3 | describe('actions', () => {
4 | describe('#parseCallKey', () => {
5 | it('does not throw for invalid address', () => {
6 | expect(parseCallKey('0x-0x')).toEqual({ address: '0x', callData: '0x' })
7 | })
8 | it('does not throw for invalid calldata', () => {
9 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toEqual({
10 | address: '0x6b175474e89094c44da98b954eedeac495271d0f',
11 | callData: 'abc'
12 | })
13 | })
14 | it('throws for invalid format', () => {
15 | expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
16 | })
17 | it('throws for uppercase calldata', () => {
18 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
19 | address: '0x6b175474e89094c44da98b954eedeac495271d0f',
20 | callData: '0xabcD'
21 | })
22 | })
23 | it('parses pieces into address', () => {
24 | expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({
25 | address: '0x6b175474e89094c44da98b954eedeac495271d0f',
26 | callData: '0xabcd'
27 | })
28 | })
29 | })
30 |
31 | describe('#toCallKey', () => {
32 | it('throws for invalid address', () => {
33 | expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x')
34 | })
35 | it('throws for invalid calldata', () => {
36 | expect(() =>
37 | toCallKey({
38 | address: '0x6b175474e89094c44da98b954eedeac495271d0f',
39 | callData: 'abc'
40 | })
41 | ).toThrow('Invalid hex: abc')
42 | })
43 | it('throws for uppercase hex', () => {
44 | expect(() =>
45 | toCallKey({
46 | address: '0x6b175474e89094c44da98b954eedeac495271d0f',
47 | callData: '0xabcD'
48 | })
49 | ).toThrow('Invalid hex: 0xabcD')
50 | })
51 | it('concatenates address to data', () => {
52 | expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
53 | '0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'
54 | )
55 | })
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/src/state/multicall/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export interface Call {
4 | address: string
5 | callData: string
6 | }
7 |
8 | const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
9 | const LOWER_HEX_REGEX = /^0x[a-f0-9]*$/
10 | export function toCallKey(call: Call): string {
11 | if (!ADDRESS_REGEX.test(call.address)) {
12 | throw new Error(`Invalid address: ${call.address}`)
13 | }
14 | if (!LOWER_HEX_REGEX.test(call.callData)) {
15 | throw new Error(`Invalid hex: ${call.callData}`)
16 | }
17 | return `${call.address}-${call.callData}`
18 | }
19 |
20 | export function parseCallKey(callKey: string): Call {
21 | const pcs = callKey.split('-')
22 | if (pcs.length !== 2) {
23 | throw new Error(`Invalid call key: ${callKey}`)
24 | }
25 | return {
26 | address: pcs[0],
27 | callData: pcs[1]
28 | }
29 | }
30 |
31 | export interface ListenerOptions {
32 | // how often this data should be fetched, by default 1
33 | readonly blocksPerFetch?: number
34 | }
35 |
36 | export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
37 | 'multicall/addMulticallListeners'
38 | )
39 | export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
40 | 'multicall/removeMulticallListeners'
41 | )
42 | export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
43 | 'multicall/fetchingMulticallResults'
44 | )
45 | export const errorFetchingMulticallResults = createAction<{
46 | chainId: number
47 | calls: Call[]
48 | fetchingBlockNumber: number
49 | }>('multicall/errorFetchingMulticallResults')
50 | export const updateMulticallResults = createAction<{
51 | chainId: number
52 | blockNumber: number
53 | results: {
54 | [callKey: string]: string | null
55 | }
56 | }>('multicall/updateMulticallResults')
57 |
--------------------------------------------------------------------------------
/src/state/swap/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export enum Field {
4 | INPUT = 'INPUT',
5 | OUTPUT = 'OUTPUT'
6 | }
7 |
8 | export const selectCurrency = createAction<{ field: Field; currencyId: string }>('swap/selectCurrency')
9 | export const switchCurrencies = createAction('swap/switchCurrencies')
10 | export const typeInput = createAction<{ field: Field; typedValue: string }>('swap/typeInput')
11 | export const replaceSwapState = createAction<{
12 | field: Field
13 | typedValue: string
14 | inputCurrencyId?: string
15 | outputCurrencyId?: string
16 | recipient: string | null
17 | }>('swap/replaceSwapState')
18 | export const setRecipient = createAction<{ recipient: string | null }>('swap/setRecipient')
19 |
--------------------------------------------------------------------------------
/src/state/swap/reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, Store } from 'redux'
2 | import { Field, selectCurrency } from './actions'
3 | import reducer, { SwapState } from './reducer'
4 |
5 | describe('swap reducer', () => {
6 | let store: Store
7 |
8 | beforeEach(() => {
9 | store = createStore(reducer, {
10 | [Field.OUTPUT]: { currencyId: '' },
11 | [Field.INPUT]: { currencyId: '' },
12 | typedValue: '',
13 | independentField: Field.INPUT,
14 | recipient: null
15 | })
16 | })
17 |
18 | describe('selectToken', () => {
19 | it('changes token', () => {
20 | store.dispatch(
21 | selectCurrency({
22 | field: Field.OUTPUT,
23 | currencyId: '0x0000'
24 | })
25 | )
26 |
27 | expect(store.getState()).toEqual({
28 | [Field.OUTPUT]: { currencyId: '0x0000' },
29 | [Field.INPUT]: { currencyId: '' },
30 | typedValue: '',
31 | independentField: Field.INPUT,
32 | recipient: null
33 | })
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/state/transactions/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 | import { ChainId } from '@uniswap/sdk'
3 |
4 | export interface SerializableTransactionReceipt {
5 | to: string
6 | from: string
7 | contractAddress: string
8 | transactionIndex: number
9 | blockHash: string
10 | transactionHash: string
11 | blockNumber: number
12 | status?: number
13 | }
14 |
15 | export const addTransaction = createAction<{
16 | chainId: ChainId
17 | hash: string
18 | from: string
19 | approval?: { tokenAddress: string; spender: string }
20 | claim?: { recipient: string }
21 | summary?: string
22 | }>('transactions/addTransaction')
23 | export const clearAllTransactions = createAction<{ chainId: ChainId }>('transactions/clearAllTransactions')
24 | export const finalizeTransaction = createAction<{
25 | chainId: ChainId
26 | hash: string
27 | receipt: SerializableTransactionReceipt
28 | }>('transactions/finalizeTransaction')
29 | export const checkedTransaction = createAction<{
30 | chainId: ChainId
31 | hash: string
32 | blockNumber: number
33 | }>('transactions/checkedTransaction')
34 |
--------------------------------------------------------------------------------
/src/state/transactions/reducer.ts:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 | import {
3 | addTransaction,
4 | checkedTransaction,
5 | clearAllTransactions,
6 | finalizeTransaction,
7 | SerializableTransactionReceipt
8 | } from './actions'
9 |
10 | const now = () => new Date().getTime()
11 |
12 | export interface TransactionDetails {
13 | hash: string
14 | approval?: { tokenAddress: string; spender: string }
15 | summary?: string
16 | claim?: { recipient: string }
17 | receipt?: SerializableTransactionReceipt
18 | lastCheckedBlockNumber?: number
19 | addedTime: number
20 | confirmedTime?: number
21 | from: string
22 | }
23 |
24 | export interface TransactionState {
25 | [chainId: number]: {
26 | [txHash: string]: TransactionDetails
27 | }
28 | }
29 |
30 | export const initialState: TransactionState = {}
31 |
32 | export default createReducer(initialState, builder =>
33 | builder
34 | .addCase(addTransaction, (transactions, { payload: { chainId, from, hash, approval, summary, claim } }) => {
35 | if (transactions[chainId]?.[hash]) {
36 | throw Error('Attempted to add existing transaction.')
37 | }
38 | const txs = transactions[chainId] ?? {}
39 | txs[hash] = { hash, approval, summary, claim, from, addedTime: now() }
40 | transactions[chainId] = txs
41 | })
42 | .addCase(clearAllTransactions, (transactions, { payload: { chainId } }) => {
43 | if (!transactions[chainId]) return
44 | transactions[chainId] = {}
45 | })
46 | .addCase(checkedTransaction, (transactions, { payload: { chainId, hash, blockNumber } }) => {
47 | const tx = transactions[chainId]?.[hash]
48 | if (!tx) {
49 | return
50 | }
51 | if (!tx.lastCheckedBlockNumber) {
52 | tx.lastCheckedBlockNumber = blockNumber
53 | } else {
54 | tx.lastCheckedBlockNumber = Math.max(blockNumber, tx.lastCheckedBlockNumber)
55 | }
56 | })
57 | .addCase(finalizeTransaction, (transactions, { payload: { hash, chainId, receipt } }) => {
58 | const tx = transactions[chainId]?.[hash]
59 | if (!tx) {
60 | return
61 | }
62 | tx.receipt = receipt
63 | tx.confirmedTime = now()
64 | })
65 | )
66 |
--------------------------------------------------------------------------------
/src/state/transactions/updater.test.ts:
--------------------------------------------------------------------------------
1 | import { shouldCheck } from './updater'
2 |
3 | describe('transactions updater', () => {
4 | describe('shouldCheck', () => {
5 | it('returns true if no receipt and never checked', () => {
6 | expect(shouldCheck(10, { addedTime: 100 })).toEqual(true)
7 | })
8 | it('returns false if has receipt and never checked', () => {
9 | expect(shouldCheck(10, { addedTime: 100, receipt: {} })).toEqual(false)
10 | })
11 | it('returns true if has not been checked in 1 blocks', () => {
12 | expect(shouldCheck(10, { addedTime: new Date().getTime(), lastCheckedBlockNumber: 9 })).toEqual(true)
13 | })
14 | it('returns false if checked in last 3 blocks and greater than 20 minutes old', () => {
15 | expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 8 })).toEqual(
16 | false
17 | )
18 | })
19 | it('returns true if not checked in last 5 blocks and greater than 20 minutes old', () => {
20 | expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 5 })).toEqual(
21 | true
22 | )
23 | })
24 | it('returns false if checked in last 10 blocks and greater than 60 minutes old', () => {
25 | expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 11 })).toEqual(
26 | false
27 | )
28 | })
29 | it('returns true if checked in last 3 blocks and greater than 20 minutes old', () => {
30 | expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 10 })).toEqual(
31 | true
32 | )
33 | })
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/state/user/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export interface SerializedToken {
4 | chainId: number
5 | address: string
6 | decimals: number
7 | symbol?: string
8 | name?: string
9 | }
10 |
11 | export interface SerializedPair {
12 | token0: SerializedToken
13 | token1: SerializedToken
14 | }
15 |
16 | export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode')
17 | export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
18 | export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
19 | export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
20 | 'user/updateUserSlippageTolerance'
21 | )
22 | export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline')
23 | export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken')
24 | export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken')
25 | export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair')
26 | export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>(
27 | 'user/removeSerializedPair'
28 | )
29 | export const toggleURLWarning = createAction('app/toggleURLWarning')
30 |
--------------------------------------------------------------------------------
/src/state/user/reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, Store } from 'redux'
2 | import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
3 | import { updateVersion } from '../global/actions'
4 | import reducer, { initialState, UserState } from './reducer'
5 |
6 | describe('swap reducer', () => {
7 | let store: Store
8 |
9 | beforeEach(() => {
10 | store = createStore(reducer, initialState)
11 | })
12 |
13 | describe('updateVersion', () => {
14 | it('has no timestamp originally', () => {
15 | expect(store.getState().lastUpdateVersionTimestamp).toBeUndefined()
16 | })
17 | it('sets the lastUpdateVersionTimestamp', () => {
18 | const time = new Date().getTime()
19 | store.dispatch(updateVersion())
20 | expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time)
21 | })
22 | it('sets allowed slippage and deadline', () => {
23 | store = createStore(reducer, {
24 | ...initialState,
25 | userDeadline: undefined,
26 | userSlippageTolerance: undefined
27 | } as any)
28 | store.dispatch(updateVersion())
29 | expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
30 | expect(store.getState().userSlippageTolerance).toEqual(INITIAL_ALLOWED_SLIPPAGE)
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/state/user/updater.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { AppDispatch } from '../index'
4 | import { updateMatchesDarkMode } from './actions'
5 |
6 | export default function Updater(): null {
7 | const dispatch = useDispatch()
8 |
9 | // keep dark mode in sync with the system
10 | useEffect(() => {
11 | const darkHandler = (match: MediaQueryListEvent) => {
12 | dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches }))
13 | }
14 |
15 | const match = window?.matchMedia('(prefers-color-scheme: dark)')
16 | dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches }))
17 |
18 | if (match?.addListener) {
19 | match?.addListener(darkHandler)
20 | } else if (match?.addEventListener) {
21 | match?.addEventListener('change', darkHandler)
22 | }
23 |
24 | return () => {
25 | if (match?.removeListener) {
26 | match?.removeListener(darkHandler)
27 | } else if (match?.removeEventListener) {
28 | match?.removeEventListener('change', darkHandler)
29 | }
30 | }
31 | }, [dispatch])
32 |
33 | return null
34 | }
35 |
--------------------------------------------------------------------------------
/src/theme/DarkModeQueryParamReader.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { RouteComponentProps } from 'react-router-dom'
4 | import { parse } from 'qs'
5 | import { AppDispatch } from '../state'
6 | import { updateUserDarkMode } from '../state/user/actions'
7 |
8 | export default function DarkModeQueryParamReader({ location: { search } }: RouteComponentProps): null {
9 | const dispatch = useDispatch()
10 |
11 | useEffect(() => {
12 | if (!search) return
13 | if (search.length < 2) return
14 |
15 | const parsed = parse(search, {
16 | parseArrays: false,
17 | ignoreQueryPrefix: true
18 | })
19 |
20 | const theme = parsed.theme
21 |
22 | if (typeof theme !== 'string') return
23 |
24 | if (theme.toLowerCase() === 'light') {
25 | dispatch(updateUserDarkMode({ userDarkMode: false }))
26 | } else if (theme.toLowerCase() === 'dark') {
27 | dispatch(updateUserDarkMode({ userDarkMode: true }))
28 | }
29 | }, [dispatch, search])
30 |
31 | return null
32 | }
33 |
--------------------------------------------------------------------------------
/src/theme/styled.d.ts:
--------------------------------------------------------------------------------
1 | import { FlattenSimpleInterpolation, ThemedCssFunction } from 'styled-components'
2 |
3 | export type Color = string
4 | export interface Colors {
5 | // base
6 | white: Color
7 | black: Color
8 |
9 | // text
10 | text1: Color
11 | text2: Color
12 | text3: Color
13 | text4: Color
14 | text5: Color
15 |
16 | // backgrounds / greys
17 | bg1: Color
18 | bg2: Color
19 | bg3: Color
20 | bg4: Color
21 | bg5: Color
22 |
23 | modalBG: Color
24 | advancedBG: Color
25 |
26 | //blues
27 | primary1: Color
28 | primary2: Color
29 | primary3: Color
30 | primary4: Color
31 | primary5: Color
32 |
33 | primaryText1: Color
34 |
35 | // pinks
36 | secondary1: Color
37 | secondary2: Color
38 | secondary3: Color
39 |
40 | // other
41 | red1: Color
42 | red2: Color
43 | green1: Color
44 | yellow1: Color
45 | yellow2: Color
46 | blue1: Color
47 | }
48 |
49 | export interface Grids {
50 | sm: number
51 | md: number
52 | lg: number
53 | }
54 |
55 | declare module 'styled-components' {
56 | export interface DefaultTheme extends Colors {
57 | grids: Grids
58 |
59 | // shadows
60 | shadow1: string
61 |
62 | // media queries
63 | mediaWidth: {
64 | upToExtraSmall: ThemedCssFunction
65 | upToSmall: ThemedCssFunction
66 | upToMedium: ThemedCssFunction
67 | upToLarge: ThemedCssFunction
68 | }
69 |
70 | // css snippets
71 | flexColumnNoWrap: FlattenSimpleInterpolation
72 | flexRowNoWrap: FlattenSimpleInterpolation
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/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/computeUniCirculation.test.ts:
--------------------------------------------------------------------------------
1 | import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
2 | import { BigNumber } from 'ethers'
3 | import { ZERO_ADDRESS } from '../constants'
4 | import { computeUniCirculation } from './computeUniCirculation'
5 |
6 | describe('computeUniCirculation', () => {
7 | const token = new Token(ChainId.RINKEBY, ZERO_ADDRESS, 18)
8 |
9 | function expandTo18Decimals(num: JSBI | string | number) {
10 | return JSBI.multiply(JSBI.BigInt(num), JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18)))
11 | }
12 |
13 | function tokenAmount(num: JSBI | string | number) {
14 | return new TokenAmount(token, expandTo18Decimals(num))
15 | }
16 |
17 | it('before staking', () => {
18 | expect(computeUniCirculation(token, BigNumber.from(0), undefined)).toEqual(tokenAmount(150_000_000))
19 | expect(computeUniCirculation(token, BigNumber.from(1600387200), undefined)).toEqual(tokenAmount(150_000_000))
20 | })
21 | it('mid staking', () => {
22 | expect(computeUniCirculation(token, BigNumber.from(1600387200 + 15 * 24 * 60 * 60), undefined)).toEqual(
23 | tokenAmount(155_000_000)
24 | )
25 | })
26 | it('after staking and treasury vesting cliff', () => {
27 | expect(computeUniCirculation(token, BigNumber.from(1600387200 + 60 * 24 * 60 * 60), undefined)).toEqual(
28 | tokenAmount(224_575_341)
29 | )
30 | })
31 | it('subtracts unclaimed uni', () => {
32 | expect(computeUniCirculation(token, BigNumber.from(1600387200 + 15 * 24 * 60 * 60), tokenAmount(1000))).toEqual(
33 | tokenAmount(154_999_000)
34 | )
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/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, ETHER, Token } from '@uniswap/sdk'
2 |
3 | export function currencyId(currency: Currency): string {
4 | if (currency === ETHER) return 'ETH'
5 | if (currency instanceof Token) return currency.address
6 | throw new Error('invalid currency')
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/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 contenthashToUri from './contenthashToUri'
5 | import { parseENSAddress } from './parseENSAddress'
6 | import uriToHttp from './uriToHttp'
7 |
8 | const tokenListValidator = new Ajv({ allErrors: true }).compile(schema)
9 |
10 | /**
11 | * Contains the logic for resolving a list URL to a validated token list
12 | * @param listUrl list url
13 | * @param resolveENSContentHash resolves an ens name to a contenthash
14 | */
15 | export default async function getTokenList(
16 | listUrl: string,
17 | resolveENSContentHash: (ensName: string) => Promise
18 | ): Promise {
19 | const parsedENS = parseENSAddress(listUrl)
20 | let urls: string[]
21 | if (parsedENS) {
22 | let contentHashUri
23 | try {
24 | contentHashUri = await resolveENSContentHash(parsedENS.ensName)
25 | } catch (error) {
26 | console.debug(`Failed to resolve ENS name: ${parsedENS.ensName}`, error)
27 | throw new Error(`Failed to resolve ENS name: ${parsedENS.ensName}`)
28 | }
29 | let translatedUri
30 | try {
31 | translatedUri = contenthashToUri(contentHashUri)
32 | } catch (error) {
33 | console.debug('Failed to translate contenthash to URI', contentHashUri)
34 | throw new Error(`Failed to translate contenthash to URI: ${contentHashUri}`)
35 | }
36 | urls = uriToHttp(`${translatedUri}${parsedENS.ensPath ?? ''}`)
37 | } else {
38 | urls = uriToHttp(listUrl)
39 | }
40 | for (let i = 0; i < urls.length; i++) {
41 | const url = urls[i]
42 | const isLast = i === urls.length - 1
43 | let response
44 | try {
45 | response = await fetch(url)
46 | } catch (error) {
47 | console.debug('Failed to fetch list', listUrl, error)
48 | if (isLast) throw new Error(`Failed to download list ${listUrl}`)
49 | continue
50 | }
51 |
52 | if (!response.ok) {
53 | if (isLast) throw new Error(`Failed to download list ${listUrl}`)
54 | continue
55 | }
56 |
57 | const json = await response.json()
58 | if (!tokenListValidator(json)) {
59 | const validationErrors: string =
60 | tokenListValidator.errors?.reduce((memo, error) => {
61 | const add = `${error.dataPath} ${error.message ?? ''}`
62 | return memo.length > 0 ? `${memo}; ${add}` : `${add}`
63 | }, '') ?? 'unknown error'
64 | throw new Error(`Token list failed validation: ${validationErrors}`)
65 | }
66 | return json
67 | }
68 | throw new Error('Unrecognized list URL protocol.')
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/isZero.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns true if the string value is zero in hex
3 | * @param hexNumberString
4 | */
5 | export default function isZero(hexNumberString: string) {
6 | return /^0x0*$/.test(hexNumberString)
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/listVersionLabel.ts:
--------------------------------------------------------------------------------
1 | import { Version } from '@uniswap/token-lists'
2 |
3 | export default function listVersionLabel(version: Version): string {
4 | return `v${version.major}.${version.minor}.${version.patch}`
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/maxAmountSpend.ts:
--------------------------------------------------------------------------------
1 | import { CurrencyAmount, ETHER, JSBI } from '@uniswap/sdk'
2 | import { MIN_ETH } from '../constants'
3 |
4 | /**
5 | * Given some token amount, return the max that can be spent of it
6 | * @param currencyAmount to return max of
7 | */
8 | export function maxAmountSpend(currencyAmount?: CurrencyAmount): CurrencyAmount | undefined {
9 | if (!currencyAmount) return undefined
10 | if (currencyAmount.currency === ETHER) {
11 | if (JSBI.greaterThan(currencyAmount.raw, MIN_ETH)) {
12 | return CurrencyAmount.ether(JSBI.subtract(currencyAmount.raw, MIN_ETH))
13 | } else {
14 | return CurrencyAmount.ether(JSBI.BigInt(0))
15 | }
16 | }
17 | return currencyAmount
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/parseENSAddress.test.ts:
--------------------------------------------------------------------------------
1 | import { parseENSAddress } from './parseENSAddress'
2 |
3 | describe('parseENSAddress', () => {
4 | it('test cases', () => {
5 | expect(parseENSAddress('hello.eth')).toEqual({ ensName: 'hello.eth', ensPath: undefined })
6 | expect(parseENSAddress('hello.eth/')).toEqual({ ensName: 'hello.eth', ensPath: '/' })
7 | expect(parseENSAddress('hello.world.eth/')).toEqual({ ensName: 'hello.world.eth', ensPath: '/' })
8 | expect(parseENSAddress('hello.world.eth/abcdef')).toEqual({ ensName: 'hello.world.eth', ensPath: '/abcdef' })
9 | expect(parseENSAddress('abso.lutely')).toEqual(undefined)
10 | expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined })
11 | expect(parseENSAddress('eth')).toEqual(undefined)
12 | expect(parseENSAddress('eth/hello-world')).toEqual(undefined)
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/utils/parseENSAddress.ts:
--------------------------------------------------------------------------------
1 | const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/
2 |
3 | export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined {
4 | const match = ensAddress.match(ENS_NAME_REGEX)
5 | if (!match) return undefined
6 | return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[3] }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/prices.test.ts:
--------------------------------------------------------------------------------
1 | import { ChainId, JSBI, Pair, Route, Token, TokenAmount, Trade, TradeType } from '@uniswap/sdk'
2 | import { computeTradePriceBreakdown } from './prices'
3 |
4 | describe('prices', () => {
5 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18)
6 | const token2 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18)
7 | const token3 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000003', 18)
8 |
9 | const pair12 = new Pair(new TokenAmount(token1, JSBI.BigInt(10000)), new TokenAmount(token2, JSBI.BigInt(20000)))
10 | const pair23 = new Pair(new TokenAmount(token2, JSBI.BigInt(20000)), new TokenAmount(token3, JSBI.BigInt(30000)))
11 |
12 | describe('computeTradePriceBreakdown', () => {
13 | it('returns undefined for undefined', () => {
14 | expect(computeTradePriceBreakdown(undefined)).toEqual({
15 | priceImpactWithoutFee: undefined,
16 | realizedLPFee: undefined
17 | })
18 | })
19 |
20 | it('correct realized lp fee for single hop', () => {
21 | expect(
22 | computeTradePriceBreakdown(
23 | new Trade(new Route([pair12], token1), new TokenAmount(token1, JSBI.BigInt(1000)), TradeType.EXACT_INPUT)
24 | ).realizedLPFee
25 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(3)))
26 | })
27 |
28 | it('correct realized lp fee for double hop', () => {
29 | expect(
30 | computeTradePriceBreakdown(
31 | new Trade(
32 | new Route([pair12, pair23], token1),
33 | new TokenAmount(token1, JSBI.BigInt(1000)),
34 | TradeType.EXACT_INPUT
35 | )
36 | ).realizedLPFee
37 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(5)))
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/utils/resolveENSContentHash.ts:
--------------------------------------------------------------------------------
1 | import { Contract } from '@ethersproject/contracts'
2 | import { Provider } from '@ethersproject/abstract-provider'
3 | import { namehash } from 'ethers/lib/utils'
4 |
5 | const REGISTRAR_ABI = [
6 | {
7 | constant: true,
8 | inputs: [
9 | {
10 | name: 'node',
11 | type: 'bytes32'
12 | }
13 | ],
14 | name: 'resolver',
15 | outputs: [
16 | {
17 | name: 'resolverAddress',
18 | type: 'address'
19 | }
20 | ],
21 | payable: false,
22 | stateMutability: 'view',
23 | type: 'function'
24 | }
25 | ]
26 | const REGISTRAR_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
27 |
28 | const RESOLVER_ABI = [
29 | {
30 | constant: true,
31 | inputs: [
32 | {
33 | internalType: 'bytes32',
34 | name: 'node',
35 | type: 'bytes32'
36 | }
37 | ],
38 | name: 'contenthash',
39 | outputs: [
40 | {
41 | internalType: 'bytes',
42 | name: '',
43 | type: 'bytes'
44 | }
45 | ],
46 | payable: false,
47 | stateMutability: 'view',
48 | type: 'function'
49 | }
50 | ]
51 |
52 | // cache the resolver contracts since most of them are the public resolver
53 | function resolverContract(resolverAddress: string, provider: Provider): Contract {
54 | return new Contract(resolverAddress, RESOLVER_ABI, provider)
55 | }
56 |
57 | /**
58 | * Fetches and decodes the result of an ENS contenthash lookup on mainnet to a URI
59 | * @param ensName to resolve
60 | * @param provider provider to use to fetch the data
61 | */
62 | export default async function resolveENSContentHash(ensName: string, provider: Provider): Promise {
63 | const ensRegistrarContract = new Contract(REGISTRAR_ADDRESS, REGISTRAR_ABI, provider)
64 | const hash = namehash(ensName)
65 | const resolverAddress = await ensRegistrarContract.resolver(hash)
66 | return resolverContract(resolverAddress, provider).contenthash(hash)
67 | }
68 |
--------------------------------------------------------------------------------
/src/utils/retry.test.ts:
--------------------------------------------------------------------------------
1 | import { retry, RetryableError } from './retry'
2 |
3 | describe('retry', () => {
4 | function makeFn(fails: number, result: T, retryable = true): () => Promise {
5 | return async () => {
6 | if (fails > 0) {
7 | fails--
8 | throw retryable ? new RetryableError('failure') : new Error('bad failure')
9 | }
10 | return result
11 | }
12 | }
13 |
14 | it('fails for non-retryable error', async () => {
15 | await expect(retry(makeFn(1, 'abc', false), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow(
16 | 'bad failure'
17 | )
18 | })
19 |
20 | it('works after one fail', async () => {
21 | await expect(retry(makeFn(1, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc')
22 | })
23 |
24 | it('works after two fails', async () => {
25 | await expect(retry(makeFn(2, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc')
26 | })
27 |
28 | it('throws if too many fails', async () => {
29 | await expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow('failure')
30 | })
31 |
32 | it('cancel causes promise to reject', async () => {
33 | const { promise, cancel } = retry(makeFn(2, 'abc'), { n: 3, minWait: 100, maxWait: 100 })
34 | cancel()
35 | await expect(promise).rejects.toThrow('Cancelled')
36 | })
37 |
38 | it('cancel no-op after complete', async () => {
39 | const { promise, cancel } = retry(makeFn(0, 'abc'), { n: 3, minWait: 100, maxWait: 100 })
40 | // defer
41 | setTimeout(cancel, 0)
42 | await expect(promise).resolves.toEqual('abc')
43 | })
44 |
45 | async function checkTime(fn: () => Promise, min: number, max: number) {
46 | const time = new Date().getTime()
47 | await fn()
48 | const diff = new Date().getTime() - time
49 | expect(diff).toBeGreaterThanOrEqual(min)
50 | expect(diff).toBeLessThanOrEqual(max)
51 | }
52 |
53 | it('waits random amount of time between min and max', async () => {
54 | const promises = []
55 | for (let i = 0; i < 10; i++) {
56 | promises.push(
57 | checkTime(
58 | () => expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 100, minWait: 50 }).promise).rejects.toThrow('failure'),
59 | 150,
60 | 400
61 | )
62 | )
63 | }
64 | await Promise.all(promises)
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/src/utils/retry.ts:
--------------------------------------------------------------------------------
1 | function wait(ms: number): Promise {
2 | return new Promise(resolve => setTimeout(resolve, ms))
3 | }
4 |
5 | function waitRandom(min: number, max: number): Promise {
6 | return wait(min + Math.round(Math.random() * Math.max(0, max - min)))
7 | }
8 |
9 | /**
10 | * This error is thrown if the function is cancelled before completing
11 | */
12 | export class CancelledError extends Error {
13 | 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/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 |
--------------------------------------------------------------------------------
/src/utils/wrappedCurrency.ts:
--------------------------------------------------------------------------------
1 | import { ChainId, Currency, CurrencyAmount, ETHER, Token, TokenAmount, WETH } from '@uniswap/sdk'
2 |
3 | export function wrappedCurrency(currency: Currency | undefined, chainId: ChainId | undefined): Token | undefined {
4 | return chainId && currency === ETHER ? WETH[chainId] : currency instanceof Token ? currency : undefined
5 | }
6 |
7 | export function wrappedCurrencyAmount(
8 | currencyAmount: CurrencyAmount | undefined,
9 | chainId: ChainId | undefined
10 | ): TokenAmount | undefined {
11 | const token = currencyAmount && chainId ? wrappedCurrency(currencyAmount.currency, chainId) : undefined
12 | return token && currencyAmount ? new TokenAmount(token, currencyAmount.raw) : undefined
13 | }
14 |
15 | export function unwrappedToken(token: Token): Currency {
16 | if (token.equals(WETH[token.chainId])) return ETHER
17 | return token
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "strict": true,
12 | "alwaysStrict": true,
13 | "strictNullChecks": true,
14 | "noUnusedLocals": false,
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitAny": true,
17 | "noImplicitThis": true,
18 | "noImplicitReturns": true,
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "jsx": "preserve",
23 | "downlevelIteration": true,
24 | "allowSyntheticDefaultImports": true,
25 | "types": ["react-spring", "jest"]
26 | },
27 | "exclude": ["node_modules", "cypress"],
28 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "src/components/Confetti/index.js"]
29 | }
30 |
--------------------------------------------------------------------------------