├── .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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-down-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-right-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orionprotocol/orn-liquidity-mining-ui/9a6689c5caf980c5514a15ca7464d0783222fb47/src/assets/images/arrow-right-white.png -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropdown-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropup-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ethereum-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/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 | 2 | 3 | 4 | 5 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/plus-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/images/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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/lightcircle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/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 | {alt} { 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 |
31 | {success ? : } 32 |
33 | 34 | {summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)} 35 | {chainId && ( 36 | View on Etherscan 37 | )} 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Popups/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useActivePopups } from '../../state/application/hooks' 4 | import { AutoColumn } from '../Column' 5 | import PopupItem from './PopupItem' 6 | 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 | --------------------------------------------------------------------------------