├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── forks └── @uniswap │ └── sdk │ ├── LICENSE │ ├── README.md │ ├── dist │ ├── constants.d.ts │ ├── entities │ │ ├── currency.d.ts │ │ ├── fractions │ │ │ ├── currencyAmount.d.ts │ │ │ ├── fraction.d.ts │ │ │ ├── index.d.ts │ │ │ ├── percent.d.ts │ │ │ ├── price.d.ts │ │ │ └── tokenAmount.d.ts │ │ ├── index.d.ts │ │ ├── pair.d.ts │ │ ├── route.d.ts │ │ ├── token.d.ts │ │ └── trade.d.ts │ ├── errors.d.ts │ ├── fetcher.d.ts │ ├── index.d.ts │ ├── index.js │ ├── router.d.ts │ ├── sdk.cjs.development.js │ ├── sdk.cjs.development.js.map │ ├── sdk.cjs.production.min.js │ ├── sdk.cjs.production.min.js.map │ ├── sdk.esm.js │ ├── sdk.esm.js.map │ ├── test │ │ ├── constants.test.d.ts │ │ ├── data.test.d.ts │ │ ├── entities.test.d.ts │ │ ├── fraction.test.d.ts │ │ ├── miscellaneous.test.d.ts │ │ ├── pair.test.d.ts │ │ ├── route.test.d.ts │ │ ├── router.test.d.ts │ │ ├── token.test.d.ts │ │ └── trade.test.d.ts │ └── utils.d.ts │ └── package.json ├── package.json ├── public ├── 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 │ │ ├── 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 │ │ ├── plus-blue.svg │ │ ├── plus-grey.svg │ │ ├── portisIcon.png │ │ ├── question-mark.svg │ │ ├── question.svg │ │ ├── spinner.svg │ │ ├── token-list-logo.png │ │ ├── token-list │ │ │ ├── lists-dark.png │ │ │ └── lists-light.png │ │ ├── trustWallet.png │ │ ├── walletConnectIcon.svg │ │ └── x.svg │ └── svg │ │ ├── QR.svg │ │ ├── lightcircle.svg │ │ ├── logo.svg │ │ └── logo_white.svg ├── components │ ├── AccountDetails │ │ ├── Copy.tsx │ │ ├── Transaction.tsx │ │ └── index.tsx │ ├── AddressInputPanel │ │ └── index.tsx │ ├── Button │ │ └── index.tsx │ ├── Card │ │ └── index.tsx │ ├── Column │ │ └── index.tsx │ ├── CurrencyInputPanel │ │ └── index.tsx │ ├── CurrencyLogo │ │ └── index.tsx │ ├── DoubleLogo │ │ └── index.tsx │ ├── FormattedCurrencyAmount │ │ └── index.tsx │ ├── Header │ │ ├── Polling.tsx │ │ └── index.tsx │ ├── Identicon │ │ └── index.tsx │ ├── ListLogo │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── Logo │ │ └── index.tsx │ ├── Modal │ │ └── index.tsx │ ├── ModalViews │ │ └── index.tsx │ ├── NavigationTabs │ │ └── index.tsx │ ├── NumericalInput │ │ └── index.tsx │ ├── Popover │ │ └── index.tsx │ ├── Popups │ │ ├── ListUpdatePopup.tsx │ │ ├── PopupItem.tsx │ │ ├── TransactionPopup.tsx │ │ └── index.tsx │ ├── PositionCard │ │ └── index.tsx │ ├── ProgressSteps │ │ └── index.tsx │ ├── QuestionHelper │ │ └── index.tsx │ ├── Row │ │ └── index.tsx │ ├── SearchModal │ │ ├── CommonBases.tsx │ │ ├── CurrencyList.tsx │ │ ├── CurrencySearch.tsx │ │ ├── CurrencySearchModal.tsx │ │ ├── ImportList.tsx │ │ ├── ImportRow.tsx │ │ ├── ImportToken.tsx │ │ ├── Manage.tsx │ │ ├── ManageLists.tsx │ │ ├── ManageTokens.tsx │ │ ├── SortButton.tsx │ │ ├── filtering.ts │ │ ├── sorting.ts │ │ └── styleds.tsx │ ├── Settings │ │ └── index.tsx │ ├── Slider │ │ └── index.tsx │ ├── Toggle │ │ ├── ListToggle.tsx │ │ └── 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 │ └── swap │ │ ├── AdvancedSwapDetails.tsx │ │ ├── AdvancedSwapDetailsDropdown.tsx │ │ ├── ConfirmSwapModal.tsx │ │ ├── FormattedPriceImpact.tsx │ │ ├── SwapHeader.tsx │ │ ├── SwapModalFooter.tsx │ │ ├── SwapModalHeader.tsx │ │ ├── SwapRoute.tsx │ │ ├── TradePrice.tsx │ │ ├── confirmPriceImpactWithoutFee.ts │ │ └── styleds.tsx ├── connectors │ ├── Fortmatic.ts │ ├── NetworkConnector.ts │ ├── fortmatic.d.ts │ └── index.ts ├── constants │ ├── abis │ │ ├── argent-wallet-detector.json │ │ ├── argent-wallet-detector.ts │ │ ├── ens-public-resolver.json │ │ ├── ens-registrar.json │ │ ├── erc20.json │ │ ├── erc20.ts │ │ ├── erc20_bytes32.json │ │ └── weth.json │ ├── index.ts │ ├── lists.ts │ └── multicall │ │ ├── abi.json │ │ └── index.ts ├── data │ ├── Allowances.ts │ ├── Reserves.ts │ └── TotalSupply.ts ├── hooks │ ├── 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 │ ├── useSwapCallback.ts │ ├── useTheme.ts │ ├── useTimestampFromBlock.ts │ ├── useToggle.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 │ ├── Pool │ │ ├── index.tsx │ │ └── styleds.tsx │ ├── PoolFinder │ │ └── index.tsx │ ├── RemoveLiquidity │ │ ├── index.tsx │ │ └── redirects.tsx │ └── Swap │ │ ├── index.tsx │ │ └── redirects.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 │ ├── global │ │ └── actions.ts │ ├── index.ts │ ├── lists │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── mint │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ └── reducer.ts │ ├── multicall │ │ ├── actions.test.ts │ │ ├── actions.ts │ │ ├── hooks.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ ├── updater.test.ts │ │ └── updater.tsx │ ├── stake │ │ └── 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 │ ├── listSort.ts │ ├── listVersionLabel.ts │ ├── maxAmountSpend.ts │ ├── parseENSAddress.test.ts │ ├── parseENSAddress.ts │ ├── prices.test.ts │ ├── prices.ts │ ├── resolveENSContentHash.ts │ ├── retry.test.ts │ ├── retry.ts │ ├── trades.ts │ ├── uriToHttp.test.ts │ ├── uriToHttp.ts │ ├── useDebouncedChangeHandler.tsx │ ├── useUSDCPrice.ts │ └── wrappedCurrency.ts ├── test └── .gitkeep └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_NETWORK_URL="https://rinkeby.infura.io/v3/3cb031735f9a46a69f2babab4fae3e0d" 2 | REACT_APP_CHAIN_ID="4" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | // Allows for the parsing of JSX 8 | "jsx": true 9 | } 10 | }, 11 | "ignorePatterns": ["node_modules/**/*"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "extends": [ 18 | "plugin:react/recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react-hooks/recommended", 21 | "prettier/@typescript-eslint", 22 | "plugin:prettier/recommended" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "prettier/prettier": "error", 27 | "@typescript-eslint/no-explicit-any": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | 7 | # misc 8 | .secret 9 | .env.production 10 | .env.production.local 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | yarn.lock 16 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoolSwap template 2 | 3 | ### Rinkeby 4 | 5 | - Factory: 0x6Bd5A1A63ffF10De3c6B7C667040E9AE1B47fDf2 6 | - Router: 0xA4E1f3fD10E2397f58926E215Ed331D7cDA14056 7 | - Pair hash: 0xaf88dd15a55596feb9d67243c727bfd6144af12453963809bc91f0cfcf8241bc 8 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Noah Zinsmeister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /forks/@uniswap/sdk/README.md: -------------------------------------------------------------------------------- 1 | # Uniswap SDK 2 | 3 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 4 | [![Actions Status](https://github.com/Uniswap/uniswap-sdk/workflows/CI/badge.svg)](https://github.com/Uniswap/uniswap-sdk) 5 | [![npm version](https://img.shields.io/npm/v/@uniswap/sdk/latest.svg)](https://www.npmjs.com/package/@uniswap/sdk/v/latest) 6 | [![npm bundle size (scoped version)](https://img.shields.io/bundlephobia/minzip/@uniswap/sdk/latest.svg)](https://bundlephobia.com/result?p=@uniswap/sdk@latest) 7 | 8 | In-depth documentation on this SDK is available at [uniswap.org](https://uniswap.org/docs/v2/SDK/getting-started/). 9 | 10 | ## Running tests 11 | 12 | To run the tests, follow these steps. You must have at least node v10 and [yarn](https://yarnpkg.com/) installed. 13 | 14 | First clone the repository: 15 | 16 | ```sh 17 | git clone https://github.com/Uniswap/uniswap-sdk.git 18 | ``` 19 | 20 | Move into the uniswap-sdk working directory 21 | 22 | ```sh 23 | cd uniswap-sdk/ 24 | ``` 25 | 26 | Install dependencies 27 | 28 | ```sh 29 | yarn install 30 | ``` 31 | 32 | Run tests 33 | 34 | ```sh 35 | yarn test 36 | ``` 37 | 38 | You should see output like the following: 39 | 40 | ```sh 41 | yarn run v1.22.4 42 | $ tsdx test 43 | PASS test/constants.test.ts 44 | PASS test/pair.test.ts 45 | PASS test/fraction.test.ts 46 | PASS test/miscellaneous.test.ts 47 | PASS test/entities.test.ts 48 | PASS test/trade.test.ts 49 | 50 | Test Suites: 1 skipped, 6 passed, 6 of 7 total 51 | Tests: 3 skipped, 82 passed, 85 total 52 | Snapshots: 0 total 53 | Time: 5.091s 54 | Ran all test suites. 55 | ✨ Done in 6.61s. 56 | ``` 57 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/constants.d.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi'; 2 | export declare type BigintIsh = JSBI | bigint | string; 3 | export declare enum ChainId { 4 | MAINNET = 1, 5 | ROPSTEN = 3, 6 | RINKEBY = 4, 7 | GÖRLI = 5, 8 | KOVAN = 42 9 | } 10 | export declare enum TradeType { 11 | EXACT_INPUT = 0, 12 | EXACT_OUTPUT = 1 13 | } 14 | export declare enum Rounding { 15 | ROUND_DOWN = 0, 16 | ROUND_HALF_UP = 1, 17 | ROUND_UP = 2 18 | } 19 | export declare const FACTORY_ADDRESS = "0x6Bd5A1A63ffF10De3c6B7C667040E9AE1B47fDf2"; 20 | export declare const INIT_CODE_HASH = "0xaf88dd15a55596feb9d67243c727bfd6144af12453963809bc91f0cfcf8241bc"; 21 | export declare const MINIMUM_LIQUIDITY: JSBI; 22 | export declare const ZERO: JSBI; 23 | export declare const ONE: JSBI; 24 | export declare const TWO: JSBI; 25 | export declare const THREE: JSBI; 26 | export declare const FIVE: JSBI; 27 | export declare const TEN: JSBI; 28 | export declare const _100: JSBI; 29 | export declare const _997: JSBI; 30 | export declare const _1000: JSBI; 31 | export declare enum SolidityType { 32 | uint8 = "uint8", 33 | uint256 = "uint256" 34 | } 35 | export declare const SOLIDITY_TYPE_MAXIMA: { 36 | uint8: JSBI; 37 | uint256: JSBI; 38 | }; 39 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/currency.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A currency is any fungible financial instrument on Ethereum, including Ether and all ERC20 tokens. 3 | * 4 | * The only instance of the base class `Currency` is Ether. 5 | */ 6 | export declare class Currency { 7 | readonly decimals: number; 8 | readonly symbol?: string; 9 | readonly name?: string; 10 | /** 11 | * The only instance of the base class `Currency`. 12 | */ 13 | static readonly ETHER: Currency; 14 | /** 15 | * Constructs an instance of the base class `Currency`. The only instance of the base class `Currency` is `Currency.ETHER`. 16 | * @param decimals decimals of the currency 17 | * @param symbol symbol of the currency 18 | * @param name of the currency 19 | */ 20 | protected constructor(decimals: number, symbol?: string, name?: string); 21 | } 22 | declare const ETHER: Currency; 23 | export { ETHER }; 24 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/currencyAmount.d.ts: -------------------------------------------------------------------------------- 1 | import { Currency } from '../currency'; 2 | import JSBI from 'jsbi'; 3 | import { BigintIsh, Rounding } from '../../constants'; 4 | import { Fraction } from './fraction'; 5 | export declare class CurrencyAmount extends Fraction { 6 | readonly currency: Currency; 7 | /** 8 | * Helper that calls the constructor with the ETHER currency 9 | * @param amount ether amount in wei 10 | */ 11 | static ether(amount: BigintIsh): CurrencyAmount; 12 | protected constructor(currency: Currency, amount: BigintIsh); 13 | get raw(): JSBI; 14 | add(other: CurrencyAmount): CurrencyAmount; 15 | subtract(other: CurrencyAmount): CurrencyAmount; 16 | toSignificant(significantDigits?: number, format?: object, rounding?: Rounding): string; 17 | toFixed(decimalPlaces?: number, format?: object, rounding?: Rounding): string; 18 | toExact(format?: object): string; 19 | } 20 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/fraction.d.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi'; 2 | import { BigintIsh, Rounding } from '../../constants'; 3 | export declare class Fraction { 4 | readonly numerator: JSBI; 5 | readonly denominator: JSBI; 6 | constructor(numerator: BigintIsh, denominator?: BigintIsh); 7 | get quotient(): JSBI; 8 | get remainder(): Fraction; 9 | invert(): Fraction; 10 | add(other: Fraction | BigintIsh): Fraction; 11 | subtract(other: Fraction | BigintIsh): Fraction; 12 | lessThan(other: Fraction | BigintIsh): boolean; 13 | equalTo(other: Fraction | BigintIsh): boolean; 14 | greaterThan(other: Fraction | BigintIsh): boolean; 15 | multiply(other: Fraction | BigintIsh): Fraction; 16 | divide(other: Fraction | BigintIsh): Fraction; 17 | toSignificant(significantDigits: number, format?: object, rounding?: Rounding): string; 18 | toFixed(decimalPlaces: number, format?: object, rounding?: Rounding): string; 19 | } 20 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './fraction'; 2 | export * from './percent'; 3 | export * from './tokenAmount'; 4 | export * from './currencyAmount'; 5 | export * from './price'; 6 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/percent.d.ts: -------------------------------------------------------------------------------- 1 | import { Rounding } from '../../constants'; 2 | import { Fraction } from './fraction'; 3 | export declare class Percent extends Fraction { 4 | toSignificant(significantDigits?: number, format?: object, rounding?: Rounding): string; 5 | toFixed(decimalPlaces?: number, format?: object, rounding?: Rounding): string; 6 | } 7 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/price.d.ts: -------------------------------------------------------------------------------- 1 | import { BigintIsh, Rounding } from '../../constants'; 2 | import { Currency } from '../currency'; 3 | import { Route } from '../route'; 4 | import { Fraction } from './fraction'; 5 | import { CurrencyAmount } from './currencyAmount'; 6 | export declare class Price extends Fraction { 7 | readonly baseCurrency: Currency; 8 | readonly quoteCurrency: Currency; 9 | readonly scalar: Fraction; 10 | static fromRoute(route: Route): Price; 11 | constructor(baseCurrency: Currency, quoteCurrency: Currency, denominator: BigintIsh, numerator: BigintIsh); 12 | get raw(): Fraction; 13 | get adjusted(): Fraction; 14 | invert(): Price; 15 | multiply(other: Price): Price; 16 | quote(currencyAmount: CurrencyAmount): CurrencyAmount; 17 | toSignificant(significantDigits?: number, format?: object, rounding?: Rounding): string; 18 | toFixed(decimalPlaces?: number, format?: object, rounding?: Rounding): string; 19 | } 20 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/fractions/tokenAmount.d.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount } from './currencyAmount'; 2 | import { Token } from '../token'; 3 | import { BigintIsh } from '../../constants'; 4 | export declare class TokenAmount extends CurrencyAmount { 5 | readonly token: Token; 6 | constructor(token: Token, amount: BigintIsh); 7 | add(other: TokenAmount): TokenAmount; 8 | subtract(other: TokenAmount): TokenAmount; 9 | } 10 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './token'; 2 | export * from './pair'; 3 | export * from './route'; 4 | export * from './trade'; 5 | export * from './currency'; 6 | export * from './fractions'; 7 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/pair.d.ts: -------------------------------------------------------------------------------- 1 | import { Price } from './fractions/price'; 2 | import { TokenAmount } from './fractions/tokenAmount'; 3 | import { BigintIsh, ChainId } from '../constants'; 4 | import { Token } from './token'; 5 | export declare class Pair { 6 | readonly liquidityToken: Token; 7 | private readonly tokenAmounts; 8 | static getAddress(tokenA: Token, tokenB: Token): string; 9 | constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount); 10 | /** 11 | * Returns true if the token is either token0 or token1 12 | * @param token to check 13 | */ 14 | involvesToken(token: Token): boolean; 15 | /** 16 | * Returns the current mid price of the pair in terms of token0, i.e. the ratio of reserve1 to reserve0 17 | */ 18 | get token0Price(): Price; 19 | /** 20 | * Returns the current mid price of the pair in terms of token1, i.e. the ratio of reserve0 to reserve1 21 | */ 22 | get token1Price(): Price; 23 | /** 24 | * Return the price of the given token in terms of the other token in the pair. 25 | * @param token token to return price of 26 | */ 27 | priceOf(token: Token): Price; 28 | /** 29 | * Returns the chain ID of the tokens in the pair. 30 | */ 31 | get chainId(): ChainId; 32 | get token0(): Token; 33 | get token1(): Token; 34 | get reserve0(): TokenAmount; 35 | get reserve1(): TokenAmount; 36 | reserveOf(token: Token): TokenAmount; 37 | getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair]; 38 | getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair]; 39 | getLiquidityMinted(totalSupply: TokenAmount, tokenAmountA: TokenAmount, tokenAmountB: TokenAmount): TokenAmount; 40 | getLiquidityValue(token: Token, totalSupply: TokenAmount, liquidity: TokenAmount, feeOn?: boolean, kLast?: BigintIsh): TokenAmount; 41 | } 42 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/route.d.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../constants'; 2 | import { Currency } from './currency'; 3 | import { Token } from './token'; 4 | import { Pair } from './pair'; 5 | import { Price } from './fractions/price'; 6 | export declare class Route { 7 | readonly pairs: Pair[]; 8 | readonly path: Token[]; 9 | readonly input: Currency; 10 | readonly output: Currency; 11 | readonly midPrice: Price; 12 | constructor(pairs: Pair[], input: Currency, output?: Currency); 13 | get chainId(): ChainId; 14 | } 15 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/entities/token.d.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../constants'; 2 | import { Currency } from './currency'; 3 | /** 4 | * Represents an ERC20 token with a unique address and some metadata. 5 | */ 6 | export declare class Token extends Currency { 7 | readonly chainId: ChainId; 8 | readonly address: string; 9 | constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string); 10 | /** 11 | * Returns true if the two tokens are equivalent, i.e. have the same chainId and address. 12 | * @param other other token to compare 13 | */ 14 | equals(other: Token): boolean; 15 | /** 16 | * Returns true if the address of this token sorts before the address of the other token 17 | * @param other other token to compare 18 | * @throws if the tokens have the same address 19 | * @throws if the tokens are on different chains 20 | */ 21 | sortsBefore(other: Token): boolean; 22 | } 23 | /** 24 | * Compares two currencies for equality 25 | */ 26 | export declare function currencyEquals(currencyA: Currency, currencyB: Currency): boolean; 27 | export declare const WETH: { 28 | 1: Token; 29 | 3: Token; 30 | 4: Token; 31 | 5: Token; 32 | 42: Token; 33 | }; 34 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/errors.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be 3 | * obtained by sending any amount of input. 4 | */ 5 | export declare class InsufficientReservesError extends Error { 6 | readonly isInsufficientReservesError: true; 7 | constructor(); 8 | } 9 | /** 10 | * Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less 11 | * than the price of a single unit of output after fees. 12 | */ 13 | export declare class InsufficientInputAmountError extends Error { 14 | readonly isInsufficientInputAmountError: true; 15 | constructor(); 16 | } 17 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/fetcher.d.ts: -------------------------------------------------------------------------------- 1 | import { Pair } from './entities/pair'; 2 | import { ChainId } from './constants'; 3 | import { Token } from './entities/token'; 4 | /** 5 | * Contains methods for constructing instances of pairs and tokens from on-chain data. 6 | */ 7 | export declare abstract class Fetcher { 8 | /** 9 | * Cannot be constructed. 10 | */ 11 | private constructor(); 12 | /** 13 | * Fetch information for a given token on the given chain, using the given ethers provider. 14 | * @param chainId chain of the token 15 | * @param address address of the token on the chain 16 | * @param provider provider used to fetch the token 17 | * @param symbol optional symbol of the token 18 | * @param name optional name of the token 19 | */ 20 | static fetchTokenData(chainId: ChainId, address: string, provider?: import("@ethersproject/providers").BaseProvider, symbol?: string, name?: string): Promise; 21 | /** 22 | * Fetches information about a pair and constructs a pair from the given two tokens. 23 | * @param tokenA first token 24 | * @param tokenB second token 25 | * @param provider the provider to use to fetch the data 26 | */ 27 | static fetchPairData(tokenA: Token, tokenB: Token, provider?: import("@ethersproject/providers").BaseProvider): Promise; 28 | } 29 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi'; 2 | export { JSBI }; 3 | export { BigintIsh, ChainId, TradeType, Rounding, FACTORY_ADDRESS, INIT_CODE_HASH, MINIMUM_LIQUIDITY } from './constants'; 4 | export * from './errors'; 5 | export * from './entities'; 6 | export * from './router'; 7 | export * from './fetcher'; 8 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | if (process.env.NODE_ENV === 'production') { 5 | module.exports = require('./sdk.cjs.production.min.js') 6 | } else { 7 | module.exports = require('./sdk.cjs.development.js') 8 | } 9 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/router.d.ts: -------------------------------------------------------------------------------- 1 | import { Percent, Trade } from './entities'; 2 | /** 3 | * Options for producing the arguments to send call to the router. 4 | */ 5 | export interface TradeOptions { 6 | /** 7 | * How much the execution price is allowed to move unfavorably from the trade execution price. 8 | */ 9 | allowedSlippage: Percent; 10 | /** 11 | * How long the swap is valid until it expires, in seconds. 12 | * This will be used to produce a `deadline` parameter which is computed from when the swap call parameters 13 | * are generated. 14 | */ 15 | ttl: number; 16 | /** 17 | * The account that should receive the output of the swap. 18 | */ 19 | recipient: string; 20 | /** 21 | * Whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods 22 | */ 23 | feeOnTransfer?: boolean; 24 | } 25 | export interface TradeOptionsDeadline extends Omit { 26 | /** 27 | * When the transaction expires. 28 | * This is an atlernate to specifying the ttl, for when you do not want to use local time. 29 | */ 30 | deadline: number; 31 | } 32 | /** 33 | * The parameters to use in the call to the Uniswap V2 Router to execute a trade. 34 | */ 35 | export interface SwapParameters { 36 | /** 37 | * The method to call on the Uniswap V2 Router. 38 | */ 39 | methodName: string; 40 | /** 41 | * The arguments to pass to the method, all hex encoded. 42 | */ 43 | args: (string | string[])[]; 44 | /** 45 | * The amount of wei to send in hex. 46 | */ 47 | value: string; 48 | } 49 | /** 50 | * Represents the Uniswap V2 Router, and has static methods for helping execute trades. 51 | */ 52 | export declare abstract class Router { 53 | /** 54 | * Cannot be constructed. 55 | */ 56 | private constructor(); 57 | /** 58 | * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade. 59 | * @param trade to produce call parameters for 60 | * @param options options for the call parameters 61 | */ 62 | static swapCallParameters(trade: Trade, options: TradeOptions | TradeOptionsDeadline): SwapParameters; 63 | } 64 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/constants.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/data.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/entities.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/fraction.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/miscellaneous.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/pair.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/route.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/router.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/token.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/test/trade.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi'; 2 | import { BigintIsh, SolidityType } from './constants'; 3 | export declare function validateSolidityTypeInstance(value: JSBI, solidityType: SolidityType): void; 4 | export declare function validateAndParseAddress(address: string): string; 5 | export declare function parseBigintIsh(bigintIsh: BigintIsh): JSBI; 6 | export declare function sqrt(y: JSBI): JSBI; 7 | export declare function sortedInsert(items: T[], add: T, maxSize: number, comparator: (a: T, b: T) => number): T | null; 8 | -------------------------------------------------------------------------------- /forks/@uniswap/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/sdk", 3 | "license": "MIT", 4 | "version": "3.0.3", 5 | "description": "🛠 An SDK for building applications on top of Uniswap.", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": "https://github.com/Uniswap/uniswap-sdk.git", 12 | "keywords": [ 13 | "uniswap", 14 | "ethereum" 15 | ], 16 | "module": "dist/sdk.esm.js", 17 | "scripts": { 18 | "lint": "tsdx lint src test", 19 | "build": "tsdx build", 20 | "start": "tsdx watch", 21 | "test": "tsdx test", 22 | "prepublishOnly": "tsdx build" 23 | }, 24 | "dependencies": { 25 | "@uniswap/v2-core": "^1.0.0", 26 | "big.js": "^5.2.2", 27 | "decimal.js-light": "^2.5.0", 28 | "jsbi": "^3.1.1", 29 | "tiny-invariant": "^1.1.0", 30 | "tiny-warning": "^1.0.3", 31 | "toformat": "^2.0.0" 32 | }, 33 | "peerDependencies": { 34 | "@ethersproject/address": "^5.0.0-beta", 35 | "@ethersproject/contracts": "^5.0.0-beta", 36 | "@ethersproject/networks": "^5.0.0-beta", 37 | "@ethersproject/providers": "^5.0.0-beta", 38 | "@ethersproject/solidity": "^5.0.0-beta" 39 | }, 40 | "devDependencies": { 41 | "@ethersproject/address": "^5.0.2", 42 | "@ethersproject/contracts": "^5.0.2", 43 | "@ethersproject/networks": "^5.0.2", 44 | "@ethersproject/providers": "^5.0.5", 45 | "@ethersproject/solidity": "^5.0.2", 46 | "@types/big.js": "^4.0.5", 47 | "@types/jest": "^24.0.25", 48 | "babel-plugin-transform-jsbi-to-bigint": "^1.3.1", 49 | "tsdx": "^0.12.3" 50 | }, 51 | "engines": { 52 | "node": ">=10" 53 | }, 54 | "prettier": { 55 | "printWidth": 120, 56 | "semi": false, 57 | "singleQuote": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/public/favicon.png -------------------------------------------------------------------------------- /public/images/192x192_App_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/public/images/192x192_App_Icon.png -------------------------------------------------------------------------------- /public/images/512x512_App_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/public/images/512x512_App_Icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Swap 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /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": "swap", 3 | "name": "swap", 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": "#000", 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/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/arrow-right-white.png -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/blue-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/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/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/ethereum-logo.png -------------------------------------------------------------------------------- /src/assets/images/fortmaticIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/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/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/metamask.png -------------------------------------------------------------------------------- /src/assets/images/plus-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/plus-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/portisIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/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-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/token-list-logo.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/token-list/lists-dark.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/token-list/lists-light.png -------------------------------------------------------------------------------- /src/assets/images/trustWallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/src/assets/images/trustWallet.png -------------------------------------------------------------------------------- /src/assets/images/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/logo_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AccountDetails/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import useCopyClipboard from '../../hooks/useCopyClipboard'; 4 | 5 | import { LinkStyledButton } from '../../theme'; 6 | import { CheckCircle, Copy } from 'react-feather'; 7 | 8 | const CopyIcon = styled(LinkStyledButton)` 9 | color: ${({ theme }) => theme.text3}; 10 | flex-shrink: 0; 11 | display: flex; 12 | text-decoration: none; 13 | font-size: 0.825rem; 14 | :hover, 15 | :active, 16 | :focus { 17 | text-decoration: none; 18 | color: ${({ theme }) => theme.text2}; 19 | } 20 | `; 21 | const TransactionStatusText = styled.span` 22 | margin-left: 0.25rem; 23 | font-size: 0.825rem; 24 | ${({ theme }) => theme.flexRowNoWrap}; 25 | align-items: center; 26 | `; 27 | 28 | export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { 29 | const [isCopied, setCopied] = useCopyClipboard(); 30 | 31 | return ( 32 | setCopied(props.toCopy)}> 33 | {isCopied ? ( 34 | 35 | 36 | Copied 37 | 38 | ) : ( 39 | 40 | 41 | 42 | )} 43 | {isCopied ? '' : props.children} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/AccountDetails/Transaction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { CheckCircle, Triangle } from 'react-feather'; 4 | 5 | import { useActiveWeb3React } from '../../hooks'; 6 | import { getEtherscanLink } from '../../utils'; 7 | import { ExternalLink } from '../../theme'; 8 | import { useAllTransactions } from '../../state/transactions/hooks'; 9 | import { RowFixed } from '../Row'; 10 | import Loader from '../Loader'; 11 | 12 | const TransactionWrapper = styled.div``; 13 | 14 | const TransactionStatusText = styled.div` 15 | margin-right: 0.5rem; 16 | display: flex; 17 | align-items: center; 18 | :hover { 19 | text-decoration: underline; 20 | } 21 | `; 22 | 23 | const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | text-decoration: none !important; 28 | border-radius: 0.5rem; 29 | padding: 0.25rem 0rem; 30 | font-weight: 500; 31 | font-size: 0.825rem; 32 | color: ${({ theme }) => theme.primary1}; 33 | `; 34 | 35 | const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` 36 | color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)}; 37 | `; 38 | 39 | export default function Transaction({ hash }: { hash: string }) { 40 | const { chainId } = useActiveWeb3React(); 41 | const allTransactions = useAllTransactions(); 42 | 43 | const tx = allTransactions?.[hash]; 44 | const summary = tx?.summary; 45 | const pending = !tx?.receipt; 46 | const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined'); 47 | 48 | if (!chainId) return null; 49 | 50 | return ( 51 | 52 | 53 | 54 | {summary ?? hash} ↗ 55 | 56 | 57 | {pending ? : success ? : } 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { CardProps, Text } from 'rebass'; 4 | import { Box } from 'rebass/styled-components'; 5 | 6 | const Card = styled(Box)<{ width?: string; padding?: string; border?: string; borderRadius?: string }>` 7 | width: ${({ width }) => 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.bg3}; 18 | background-color: ${({ theme }) => theme.bg2}; 19 | `; 20 | 21 | export const GreyCard = styled(Card)` 22 | background-color: ${({ theme }) => theme.bg3}; 23 | `; 24 | 25 | const BlueCardStyled = styled(Card)` 26 | background-color: ${({ theme }) => theme.primary5}; 27 | color: ${({ theme }) => theme.primary1}; 28 | border-radius: 12px; 29 | width: fit-content; 30 | `; 31 | 32 | export const BlueCard = ({ children, ...rest }: CardProps) => { 33 | return ( 34 | 35 | 36 | {children} 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Column/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Column = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | `; 8 | export const ColumnCenter = styled(Column)` 9 | width: 100%; 10 | align-items: center; 11 | `; 12 | 13 | export const AutoColumn = styled.div<{ 14 | gap?: 'sm' | 'md' | 'lg' | string; 15 | justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between'; 16 | }>` 17 | display: grid; 18 | grid-auto-rows: auto; 19 | grid-row-gap: ${({ gap }) => (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap}; 20 | justify-items: ${({ justify }) => justify && justify}; 21 | `; 22 | 23 | export default Column; 24 | -------------------------------------------------------------------------------- /src/components/CurrencyLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Currency, ETHER, Token } from '@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 | background-color: ${({ theme }) => theme.white}; 26 | `; 27 | 28 | export default function CurrencyLogo({ 29 | currency, 30 | size = '24px', 31 | style, 32 | }: { 33 | currency?: Currency; 34 | size?: string; 35 | style?: React.CSSProperties; 36 | }) { 37 | const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined); 38 | 39 | const srcs: string[] = useMemo(() => { 40 | if (currency === ETHER) return []; 41 | 42 | if (currency instanceof Token) { 43 | if (currency instanceof WrappedTokenInfo) { 44 | return [...uriLocations, getTokenLogoURL(currency.address)]; 45 | } 46 | 47 | return [getTokenLogoURL(currency.address)]; 48 | } 49 | return []; 50 | }, [currency, uriLocations]); 51 | 52 | if (currency === ETHER) { 53 | return ; 54 | } 55 | 56 | return ; 57 | } 58 | -------------------------------------------------------------------------------- /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/Polling.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | import { TYPE, ExternalLink } from '../../theme'; 4 | 5 | import { useBlockNumber } from '../../state/application/hooks'; 6 | import { getEtherscanLink } from '../../utils'; 7 | import { useActiveWeb3React } from '../../hooks'; 8 | 9 | const StyledPolling = styled.div` 10 | position: fixed; 11 | display: flex; 12 | align-items: center; 13 | right: 0; 14 | bottom: 0; 15 | padding: 1rem; 16 | transition: opacity 0.3s ease; 17 | color: ${({ theme }) => theme.text1}; 18 | 19 | :hover { 20 | opacity: 1; 21 | } 22 | 23 | ${({ theme }) => theme.mediaWidth.upToMedium` 24 | display: none; 25 | `} 26 | `; 27 | 28 | const StyledPollingDot = styled.div` 29 | width: 11px; 30 | height: 11px; 31 | min-height: 11px; 32 | min-width: 11px; 33 | margin-left: 0.6rem; 34 | border-radius: 50%; 35 | position: relative; 36 | background-color: ${({ theme }) => theme.text1}; 37 | `; 38 | 39 | const rotate360 = keyframes` 40 | from { 41 | transform: rotate(0deg); 42 | } 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | `; 47 | 48 | const Spinner = styled.div` 49 | animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite; 50 | transform: translateZ(0); 51 | 52 | border-top: 1px solid transparent; 53 | border-right: 1px solid transparent; 54 | border-bottom: 1px solid transparent; 55 | border-left: 2px solid ${({ theme }) => theme.text1}; 56 | background: transparent; 57 | width: 22px; 58 | height: 22px; 59 | border-radius: 50%; 60 | position: relative; 61 | 62 | left: -6px; 63 | top: -6px; 64 | `; 65 | 66 | export default function Polling() { 67 | const { chainId } = useActiveWeb3React(); 68 | const blockNumber = useBlockNumber(); 69 | const [isMounted, setIsMounted] = useState(true); 70 | 71 | useEffect(() => { 72 | const timer1 = setTimeout(() => setIsMounted(true), 1000); 73 | 74 | return () => { 75 | setIsMounted(false); 76 | clearTimeout(timer1); 77 | }; 78 | }, [blockNumber]); 79 | 80 | return ( 81 | 82 | 83 | {blockNumber} 84 | {!isMounted && } 85 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /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 | enforcer(event.target.value.replace(/,/g, '.'))} 65 | // universal input options 66 | inputMode="decimal" 67 | title="Token Amount" 68 | autoComplete="off" 69 | autoCorrect="off" 70 | // text-specific options 71 | type="text" 72 | pattern="^[0-9]*[.,]?[0-9]*$" 73 | placeholder={placeholder || '0.0'} 74 | minLength={1} 75 | maxLength={79} 76 | spellCheck="false" 77 | /> 78 | ); 79 | }); 80 | 81 | export default Input; 82 | -------------------------------------------------------------------------------- /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 { useURLWarningVisible } from '../../state/user/hooks'; 7 | 8 | const MobilePopupWrapper = styled.div<{ height: string | number }>` 9 | position: relative; 10 | max-width: 100%; 11 | height: ${({ height }) => height}; 12 | margin: ${({ height }) => (height ? '0 auto;' : 0)}; 13 | margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; 14 | 15 | display: none; 16 | ${({ theme }) => theme.mediaWidth.upToSmall` 17 | display: block; 18 | `}; 19 | `; 20 | 21 | const MobilePopupInner = styled.div` 22 | height: 99%; 23 | overflow-x: auto; 24 | overflow-y: hidden; 25 | display: flex; 26 | flex-direction: row; 27 | -webkit-overflow-scrolling: touch; 28 | ::-webkit-scrollbar { 29 | display: none; 30 | } 31 | `; 32 | 33 | const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean }>` 34 | position: fixed; 35 | top: ${({ extraPadding }) => (extraPadding ? '108px' : '88px')}; 36 | right: 1rem; 37 | max-width: 355px !important; 38 | width: 100%; 39 | z-index: 3; 40 | 41 | ${({ theme }) => theme.mediaWidth.upToSmall` 42 | display: none; 43 | `}; 44 | `; 45 | 46 | export default function Popups() { 47 | // get all popups 48 | const activePopups = useActivePopups(); 49 | 50 | const urlWarningActive = useURLWarningVisible(); 51 | 52 | return ( 53 | <> 54 | 55 | {activePopups.map((item) => ( 56 | 57 | ))} 58 | 59 | 0 ? 'fit-content' : 0}> 60 | 61 | {activePopups // reverse so new items up front 62 | .slice(0) 63 | .reverse() 64 | .map((item) => ( 65 | 66 | ))} 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/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)<{ 5 | width?: string; 6 | align?: string; 7 | justify?: string; 8 | padding?: string; 9 | border?: string; 10 | borderRadius?: string; 11 | }>` 12 | width: ${({ width }) => width ?? '100%'}; 13 | display: flex; 14 | padding: 0; 15 | align-items: ${({ align }) => align ?? 'center'}; 16 | justify-content: ${({ justify }) => justify ?? 'flex-start'}; 17 | padding: ${({ padding }) => padding}; 18 | border: ${({ border }) => border}; 19 | border-radius: ${({ borderRadius }) => borderRadius}; 20 | `; 21 | 22 | export const RowBetween = styled(Row)` 23 | justify-content: space-between; 24 | `; 25 | 26 | export const RowFlat = styled.div` 27 | display: flex; 28 | align-items: flex-end; 29 | `; 30 | 31 | export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` 32 | flex-wrap: wrap; 33 | margin: ${({ gap }) => gap && `-${gap}`}; 34 | justify-content: ${({ justify }) => justify && justify}; 35 | 36 | & > * { 37 | margin: ${({ gap }) => gap} !important; 38 | } 39 | `; 40 | 41 | export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` 42 | width: fit-content; 43 | margin: ${({ gap }) => gap && `-${gap}`}; 44 | `; 45 | 46 | export default Row; 47 | -------------------------------------------------------------------------------- /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/Manage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { PaddedColumn, Separator } from './styleds'; 3 | import { RowBetween } from 'components/Row'; 4 | import { ArrowLeft } from 'react-feather'; 5 | import { Text } from 'rebass'; 6 | import { CloseIcon } from 'theme'; 7 | import styled from 'styled-components'; 8 | import { Token } from '@uniswap/sdk'; 9 | import { ManageLists } from './ManageLists'; 10 | import ManageTokens from './ManageTokens'; 11 | import { TokenList } from '@uniswap/token-lists'; 12 | import { CurrencyModalView } from './CurrencySearchModal'; 13 | 14 | const Wrapper = styled.div` 15 | width: 100%; 16 | position: relative; 17 | padding-bottom: 80px; 18 | `; 19 | 20 | const ToggleWrapper = styled(RowBetween)` 21 | background-color: ${({ theme }) => theme.bg3}; 22 | border-radius: 12px; 23 | padding: 6px; 24 | `; 25 | 26 | const ToggleOption = styled.div<{ active?: boolean }>` 27 | width: 48%; 28 | padding: 10px; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | border-radius: 12px; 33 | font-weight: 600; 34 | background-color: ${({ theme, active }) => (active ? theme.bg1 : theme.bg3)}; 35 | color: ${({ theme, active }) => (active ? theme.text1 : theme.text2)}; 36 | user-select: none; 37 | 38 | :hover { 39 | cursor: pointer; 40 | opacity: 0.7; 41 | } 42 | `; 43 | 44 | export default function Manage({ 45 | onDismiss, 46 | setModalView, 47 | setImportList, 48 | setImportToken, 49 | setListUrl, 50 | }: { 51 | onDismiss: () => void; 52 | setModalView: (view: CurrencyModalView) => void; 53 | setImportToken: (token: Token) => void; 54 | setImportList: (list: TokenList) => void; 55 | setListUrl: (url: string) => void; 56 | }) { 57 | // toggle between tokens and lists 58 | const [showLists, setShowLists] = useState(true); 59 | 60 | return ( 61 | 62 | 63 | 64 | setModalView(CurrencyModalView.search)} /> 65 | 66 | Manage 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | setShowLists(!showLists)} active={showLists}> 75 | Lists 76 | 77 | setShowLists(!showLists)} active={!showLists}> 78 | Tokens 79 | 80 | 81 | 82 | {showLists ? ( 83 | 84 | ) : ( 85 | 86 | )} 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /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 | return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name)); 34 | }); 35 | // .sort((t0: Token, t1: Token) => { 36 | // if (t0.symbol && matchesSearch(t0.symbol) && t1.symbol && !matchesSearch(t1.symbol)) { 37 | // return -1 38 | // } 39 | // if (t0.symbol && !matchesSearch(t0.symbol) && t1.symbol && matchesSearch(t1.symbol)) { 40 | // return 1 41 | // } 42 | // return 0 43 | // }) 44 | } 45 | -------------------------------------------------------------------------------- /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/Toggle/ListToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { TYPE } from '../../theme'; 4 | 5 | const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 6 | border-radius: 20px; 7 | border: none; 8 | background: ${({ theme }) => theme.bg1}; 9 | display: flex; 10 | width: fit-content; 11 | cursor: pointer; 12 | outline: none; 13 | padding: 0.4rem 0.4rem; 14 | align-items: center; 15 | `; 16 | 17 | const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>` 18 | border-radius: 50%; 19 | height: 24px; 20 | width: 24px; 21 | background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)}; 22 | :hover { 23 | opacity: 0.8; 24 | } 25 | `; 26 | 27 | const StatusText = styled(TYPE.main)<{ isActive?: boolean }>` 28 | margin: 0 10px; 29 | width: 24px; 30 | color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)}; 31 | `; 32 | 33 | export interface ToggleProps { 34 | id?: string; 35 | isActive: boolean; 36 | bgColor: string; 37 | toggle: () => void; 38 | } 39 | 40 | export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) { 41 | return ( 42 | 43 | {isActive && ( 44 | 45 | ON 46 | 47 | )} 48 | 49 | {!isActive && ( 50 | 51 | OFF 52 | 53 | )} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>` 5 | padding: 0.25rem 0.5rem; 6 | border-radius: 14px; 7 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')}; 8 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 9 | font-size: 1rem; 10 | font-weight: 400; 11 | 12 | padding: 0.35rem 0.6rem; 13 | border-radius: 12px; 14 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')}; 15 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text2)}; 16 | font-size: 1rem; 17 | font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')}; 18 | :hover { 19 | user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')}; 20 | background: ${({ theme, isActive, isOnSwitch }) => 21 | isActive ? (isOnSwitch ? theme.primary1 : theme.text3) : 'none'}; 22 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 23 | } 24 | `; 25 | 26 | const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 27 | border-radius: 12px; 28 | border: none; 29 | background: ${({ theme }) => theme.bg3}; 30 | display: flex; 31 | width: fit-content; 32 | cursor: pointer; 33 | outline: none; 34 | padding: 0; 35 | `; 36 | 37 | export interface ToggleProps { 38 | id?: string; 39 | isActive: boolean; 40 | toggle: () => void; 41 | } 42 | 43 | export default function Toggle({ id, isActive, toggle }: ToggleProps) { 44 | return ( 45 | 46 | 47 | On 48 | 49 | 50 | Off 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/TokenWarningModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Token } from '@uniswap/sdk'; 2 | import React, { useCallback } from 'react'; 3 | import Modal from '../Modal'; 4 | import { ImportToken } from 'components/SearchModal/ImportToken'; 5 | 6 | export default function TokenWarningModal({ 7 | isOpen, 8 | tokens, 9 | onConfirm, 10 | }: { 11 | isOpen: boolean; 12 | tokens: Token[]; 13 | onConfirm: () => void; 14 | }) { 15 | const handleDismiss = useCallback(() => null, []); 16 | 17 | return ( 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /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/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(8px + 2rem); 8 | padding-bottom: 16px; 9 | margin-top: -2rem; 10 | width: 100%; 11 | max-width: 500px; 12 | border-radius: 0 0 1.2rem 1.2rem; 13 | color: ${({ theme }) => theme.text2}; 14 | background-color: ${({ theme }) => theme.advancedBG}; 15 | z-index: -1; 16 | 17 | transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')}; 18 | transition: transform 300ms ease-in-out; 19 | `; 20 | 21 | export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) { 22 | const lastTrade = useLastTruthy(trade); 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /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/SwapHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Settings from '../Settings'; 4 | import { RowBetween } from '../Row'; 5 | import { TYPE } from '../../theme'; 6 | 7 | const StyledSwapHeader = styled.div` 8 | padding: 12px 1rem 0px 1.5rem; 9 | margin-bottom: 0.4rem; 10 | width: 100%; 11 | color: ${({ theme }) => theme.text2}; 12 | `; 13 | 14 | export default function SwapHeader() { 15 | return ( 16 | 17 | 18 | Swap 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /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 { unwrappedToken } from 'utils/wrappedCurrency'; 8 | 9 | export default memo(function SwapRoute({ trade }: { trade: Trade }) { 10 | const theme = useContext(ThemeContext); 11 | return ( 12 | 13 | {trade.route.path.map((token, i, path) => { 14 | const isLastItem: boolean = i === path.length - 1; 15 | const currency = unwrappedToken(token); 16 | return ( 17 | 18 | 19 | 20 | {currency.symbol} 21 | 22 | 23 | {isLastItem ? null : } 24 | 25 | ); 26 | })} 27 | 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /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 | // import { FortmaticConnector } from './Fortmatic'; 7 | import { NetworkConnector } from './NetworkConnector'; 8 | 9 | const REACT_APP_NETWORK_URL = process.env.REACT_APP_NETWORK_URL; 10 | // const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY 11 | // const PORTIS_ID = process.env.REACT_APP_PORTIS_ID 12 | 13 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1'); 14 | 15 | if (typeof REACT_APP_NETWORK_URL === 'undefined') { 16 | throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`); 17 | } 18 | 19 | export const network = new NetworkConnector({ 20 | urls: { [NETWORK_CHAIN_ID]: REACT_APP_NETWORK_URL }, 21 | }); 22 | 23 | let networkLibrary: Web3Provider | undefined; 24 | export function getNetworkLibrary(): Web3Provider { 25 | return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any)); 26 | } 27 | 28 | export const injected = new InjectedConnector({ 29 | supportedChainIds: [1, 4], 30 | }); 31 | 32 | // mainnet only 33 | export const walletconnect = new WalletConnectConnector({ 34 | rpc: { 1: REACT_APP_NETWORK_URL }, 35 | bridge: 'https://bridge.walletconnect.org', 36 | qrcode: true, 37 | pollingInterval: 15000, 38 | }); 39 | 40 | // mainnet only 41 | // export const fortmatic = new FortmaticConnector({ 42 | // apiKey: FORMATIC_KEY ?? '', 43 | // chainId: 1 44 | // }) 45 | 46 | // mainnet only 47 | // export const portis = new PortisConnector({ 48 | // dAppId: PORTIS_ID ?? '', 49 | // networks: [1] 50 | // }) 51 | 52 | // mainnet only 53 | export const walletlink = new WalletLinkConnector({ 54 | url: REACT_APP_NETWORK_URL, 55 | appName: 'Swap', 56 | // appLogoUrl: '', 57 | }); 58 | -------------------------------------------------------------------------------- /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/lists.ts: -------------------------------------------------------------------------------- 1 | const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'; 2 | 3 | // lower index == higher priority for token import 4 | export const DEFAULT_LIST_OF_LISTS: string[] = [COMPOUND_LIST]; 5 | 6 | // default lists to be 'active' aka searched across 7 | export const DEFAULT_ACTIVE_LIST_URLS: string[] = [COMPOUND_LIST]; 8 | -------------------------------------------------------------------------------- /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/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( 14 | () => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), 15 | [token, 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 | // computePairAddress 39 | const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves'); 40 | 41 | console.groupEnd(); 42 | 43 | return useMemo(() => { 44 | return results.map((result, i) => { 45 | const { result: reserves, loading } = result; 46 | const tokenA = tokens[i][0]; 47 | const tokenB = tokens[i][1]; 48 | 49 | if (loading) return [PairState.LOADING, null]; 50 | if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]; 51 | if (!reserves) return [PairState.NOT_EXISTS, null]; 52 | const { reserve0, reserve1 } = reserves; 53 | const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]; 54 | return [ 55 | PairState.EXISTS, 56 | new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString())), 57 | ]; 58 | }); 59 | }, [results, tokens]); 60 | } 61 | 62 | export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] { 63 | return usePairs([[tokenA, tokenB]])[0]; 64 | } 65 | -------------------------------------------------------------------------------- /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 | import uriToHttp from 'utils/uriToHttp'; 7 | 8 | async function getColorFromToken(token: Token): Promise { 9 | if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') { 10 | return Promise.resolve('#FAAB14'); 11 | } 12 | 13 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${token.address}/logo.png`; 14 | 15 | return Vibrant.from(path) 16 | .getPalette() 17 | .then((palette) => { 18 | if (palette?.Vibrant) { 19 | let detectedHex = palette.Vibrant.hex; 20 | let AAscore = hex(detectedHex, '#FFF'); 21 | while (AAscore < 3) { 22 | detectedHex = shade(0.005, detectedHex); 23 | AAscore = hex(detectedHex, '#FFF'); 24 | } 25 | return detectedHex; 26 | } 27 | return null; 28 | }) 29 | .catch(() => null); 30 | } 31 | 32 | async function getColorFromUriPath(uri: string): Promise { 33 | const formattedPath = uriToHttp(uri)[0]; 34 | 35 | return Vibrant.from(formattedPath) 36 | .getPalette() 37 | .then((palette) => { 38 | if (palette?.Vibrant) { 39 | return palette.Vibrant.hex; 40 | } 41 | return null; 42 | }) 43 | .catch(() => null); 44 | } 45 | 46 | export function useColor(token?: Token) { 47 | const [color, setColor] = useState('#2172E5'); 48 | 49 | useLayoutEffect(() => { 50 | let stale = false; 51 | 52 | if (token) { 53 | getColorFromToken(token).then((tokenColor) => { 54 | if (!stale && tokenColor !== null) { 55 | setColor(tokenColor); 56 | } 57 | }); 58 | } 59 | 60 | return () => { 61 | stale = true; 62 | setColor('#2172E5'); 63 | }; 64 | }, [token]); 65 | 66 | return color; 67 | } 68 | 69 | export function useListColor(listImageUri?: string) { 70 | const [color, setColor] = useState('#2172E5'); 71 | 72 | useLayoutEffect(() => { 73 | let stale = false; 74 | 75 | if (listImageUri) { 76 | getColorFromUriPath(listImageUri).then((color) => { 77 | if (!stale && color !== null) { 78 | setColor(color); 79 | } 80 | }); 81 | } 82 | 83 | return () => { 84 | stale = true; 85 | setColor('#2172E5'); 86 | }; 87 | }, [listImageUri]); 88 | 89 | return color; 90 | } 91 | -------------------------------------------------------------------------------- /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(nameOrAddress?: string | null): { 10 | loading: boolean; 11 | address: string | null; 12 | name: string | null; 13 | } { 14 | const validated = isAddress(nameOrAddress); 15 | const reverseLookup = useENSName(validated ? validated : undefined); 16 | const lookup = useENSAddress(nameOrAddress); 17 | 18 | return { 19 | loading: reverseLookup.loading || lookup.loading, 20 | address: validated ? validated : lookup.address, 21 | name: reverseLookup.ENSName ? reverseLookup.ENSName : !validated && lookup.address ? nameOrAddress || null : null, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /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, sendDispatch?: boolean) => 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 | // note: prevent dispatch if using for list search or unsupported list 34 | return useCallback( 35 | async (listUrl: string, sendDispatch = true) => { 36 | const requestId = nanoid(); 37 | sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl })); 38 | return getTokenList(listUrl, ensResolver) 39 | .then((tokenList) => { 40 | sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })); 41 | return tokenList; 42 | }) 43 | .catch((error) => { 44 | console.debug(`Failed to get list at url ${listUrl}`, error); 45 | sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message })); 46 | throw error; 47 | }); 48 | }, 49 | [dispatch, ensResolver] 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from 'styled-components'; 2 | import { useContext } from 'react'; 3 | 4 | export default function useTheme() { 5 | return useContext(ThemeContext); 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/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/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 ReactDOM from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { HashRouter } from 'react-router-dom'; 7 | import { NetworkContextName } from './constants'; 8 | import './i18n'; 9 | import App from './pages/App'; 10 | import store from './state'; 11 | import ApplicationUpdater from './state/application/updater'; 12 | import ListsUpdater from './state/lists/updater'; 13 | import MulticallUpdater from './state/multicall/updater'; 14 | import TransactionUpdater from './state/transactions/updater'; 15 | import UserUpdater from './state/user/updater'; 16 | import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'; 17 | import getLibrary from './utils/getLibrary'; 18 | 19 | const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName); 20 | 21 | if ('ethereum' in window) { 22 | (window.ethereum as any).autoRefreshOnNetworkChange = false; 23 | } 24 | 25 | function Updaters() { 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | ReactDOM.render( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | , 54 | document.getElementById('root') 55 | ); 56 | -------------------------------------------------------------------------------- /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: 500px; 7 | width: 100%; 8 | padding: 0.2rem; 9 | border-radius: 1.6rem; 10 | box-shadow: rgba(0, 0, 0, 0.01) 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 4px 8px, rgba(0, 0, 0, 0.04) 0px 16px 24px, 11 | rgba(0, 0, 0, 0.01) 0px 24px 32px; 12 | background: ${({ theme }) => theme.bg1}; 13 | 14 | ${({ theme }) => theme.mediaWidth.upToExtraSmall` 15 | width: 90%; 16 | `} 17 | `; 18 | 19 | export default function AppBody({ children }: { children: React.ReactNode }) { 20 | return {children}; 21 | } 22 | -------------------------------------------------------------------------------- /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 | padding: 1rem; 7 | `; 8 | 9 | export const ClickableText = styled(Text)` 10 | :hover { 11 | cursor: pointer; 12 | } 13 | color: ${({ theme }) => theme.primary1}; 14 | `; 15 | export const MaxButton = styled.button<{ width: string }>` 16 | padding: 0.5rem 1rem; 17 | background-color: ${({ theme }) => theme.primary5}; 18 | border: 1px solid ${({ theme }) => theme.primary5}; 19 | border-radius: 0.5rem; 20 | font-size: 1rem; 21 | font-weight: 500; 22 | cursor: pointer; 23 | margin: 0.25rem; 24 | overflow: hidden; 25 | transition: 0.2s; 26 | 27 | :hover { 28 | opacity: 0.6; 29 | } 30 | 31 | :focus { 32 | border: 1px solid ${({ theme }) => theme.primary1}; 33 | outline: none; 34 | } 35 | 36 | ${({ theme }) => theme.mediaWidth.upToSmall` 37 | padding: 0.25rem 0.5rem; 38 | `}; 39 | `; 40 | 41 | export const Dots = styled.span` 42 | &::after { 43 | display: inline-block; 44 | animation: ellipsis 1.25s infinite; 45 | content: '.'; 46 | width: 1em; 47 | text-align: left; 48 | } 49 | @keyframes ellipsis { 50 | 0% { 51 | content: '.'; 52 | } 53 | 33% { 54 | content: '..'; 55 | } 56 | 66% { 57 | content: '...'; 58 | } 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /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/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 | DELEGATE, 29 | VOTE, 30 | } 31 | 32 | export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>( 33 | 'application/updateBlockNumber' 34 | ); 35 | export const setOpenModal = createAction('application/setOpenModal'); 36 | export const addPopup = 37 | createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>('application/addPopup'); 38 | export const removePopup = createAction<{ key: string }>('application/removePopup'); 39 | -------------------------------------------------------------------------------- /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: [ 28 | ...getDefaultMiddleware({ immutableCheck: false, thunk: false, serializableCheck: false }), 29 | save({ states: PERSISTED_KEYS }), 30 | ], 31 | preloadedState: load({ states: PERSISTED_KEYS }), 32 | }); 33 | 34 | store.dispatch(updateVersion()); 35 | 36 | export default store; 37 | export type AppState = ReturnType; 38 | export type AppDispatch = typeof store.dispatch; 39 | -------------------------------------------------------------------------------- /src/state/lists/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorWithPayload, createAction } from '@reduxjs/toolkit'; 2 | import { TokenList, Version } from '@uniswap/token-lists'; 3 | 4 | export const fetchTokenList: Readonly<{ 5 | pending: ActionCreatorWithPayload<{ url: string; requestId: string }>; 6 | fulfilled: ActionCreatorWithPayload<{ url: string; tokenList: TokenList; requestId: string }>; 7 | rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }>; 8 | }> = { 9 | pending: createAction('lists/fetchTokenList/pending'), 10 | fulfilled: createAction('lists/fetchTokenList/fulfilled'), 11 | rejected: createAction('lists/fetchTokenList/rejected'), 12 | }; 13 | // add and remove from list options 14 | export const addList = createAction('lists/addList'); 15 | export const removeList = createAction('lists/removeList'); 16 | 17 | // select which lists to search across from loaded lists 18 | export const enableList = createAction('lists/enableList'); 19 | export const disableList = createAction('lists/disableList'); 20 | 21 | // versioning 22 | export const acceptListUpdate = createAction('lists/acceptListUpdate'); 23 | export const rejectVersionUpdate = createAction('lists/rejectVersionUpdate'); 24 | -------------------------------------------------------------------------------- /src/state/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/swap/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'; 3 | 4 | export interface SwapState { 5 | readonly independentField: Field; 6 | readonly typedValue: string; 7 | readonly [Field.INPUT]: { 8 | readonly currencyId: string | undefined; 9 | }; 10 | readonly [Field.OUTPUT]: { 11 | readonly currencyId: string | undefined; 12 | }; 13 | // the typed recipient address or ENS name, or null if swap should go to sender 14 | readonly recipient: string | null; 15 | } 16 | 17 | const initialState: SwapState = { 18 | independentField: Field.INPUT, 19 | typedValue: '', 20 | [Field.INPUT]: { 21 | currencyId: '', 22 | }, 23 | [Field.OUTPUT]: { 24 | currencyId: '', 25 | }, 26 | recipient: null, 27 | }; 28 | 29 | export default createReducer(initialState, (builder) => 30 | builder 31 | .addCase( 32 | replaceSwapState, 33 | (state, { payload: { typedValue, recipient, field, inputCurrencyId, outputCurrencyId } }) => { 34 | return { 35 | [Field.INPUT]: { 36 | currencyId: inputCurrencyId, 37 | }, 38 | [Field.OUTPUT]: { 39 | currencyId: outputCurrencyId, 40 | }, 41 | independentField: field, 42 | typedValue: typedValue, 43 | recipient, 44 | }; 45 | } 46 | ) 47 | .addCase(selectCurrency, (state, { payload: { currencyId, field } }) => { 48 | const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT; 49 | if (currencyId === state[otherField].currencyId) { 50 | // the case where we have to swap the order 51 | return { 52 | ...state, 53 | independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT, 54 | [field]: { currencyId: currencyId }, 55 | [otherField]: { currencyId: state[field].currencyId }, 56 | }; 57 | } else { 58 | // the normal case 59 | return { 60 | ...state, 61 | [field]: { currencyId: currencyId }, 62 | }; 63 | } 64 | }) 65 | .addCase(switchCurrencies, (state) => { 66 | return { 67 | ...state, 68 | independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT, 69 | [Field.INPUT]: { currencyId: state[Field.OUTPUT].currencyId }, 70 | [Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId }, 71 | }; 72 | }) 73 | .addCase(typeInput, (state, { payload: { field, typedValue } }) => { 74 | return { 75 | ...state, 76 | independentField: field, 77 | typedValue, 78 | }; 79 | }) 80 | .addCase(setRecipient, (state, { payload: { recipient } }) => { 81 | state.recipient = recipient; 82 | }) 83 | ); 84 | -------------------------------------------------------------------------------- /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 updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly'); 20 | export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>( 21 | 'user/updateUserSlippageTolerance' 22 | ); 23 | export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline'); 24 | export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken'); 25 | export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken'); 26 | export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair'); 27 | export const removeSerializedPair = 28 | createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>('user/removeSerializedPair'); 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 | red3: Color; 44 | green1: Color; 45 | yellow1: Color; 46 | yellow2: Color; 47 | blue1: Color; 48 | } 49 | 50 | export interface Grids { 51 | sm: number; 52 | md: number; 53 | lg: number; 54 | } 55 | 56 | declare module 'styled-components' { 57 | export interface DefaultTheme extends Colors { 58 | grids: Grids; 59 | 60 | // shadows 61 | shadow1: string; 62 | 63 | // media queries 64 | mediaWidth: { 65 | upToExtraSmall: ThemedCssFunction; 66 | upToSmall: ThemedCssFunction; 67 | upToMedium: ThemedCssFunction; 68 | upToLarge: ThemedCssFunction; 69 | }; 70 | 71 | // css snippets 72 | flexColumnNoWrap: FlattenSimpleInterpolation; 73 | flexRowNoWrap: FlattenSimpleInterpolation; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /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/listSort.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LIST_OF_LISTS } from './../constants/lists'; 2 | 3 | // use ordering of default list of lists to assign priority 4 | export default function sortByListPriority(urlA: string, urlB: string) { 5 | const first = DEFAULT_LIST_OF_LISTS.includes(urlA) ? DEFAULT_LIST_OF_LISTS.indexOf(urlA) : Number.MAX_SAFE_INTEGER; 6 | const second = DEFAULT_LIST_OF_LISTS.includes(urlB) ? DEFAULT_LIST_OF_LISTS.indexOf(urlB) : Number.MAX_SAFE_INTEGER; 7 | 8 | // need reverse order to make sure mapping includes top priority last 9 | if (first < second) return 1; 10 | else if (first > second) return -1; 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/listVersionLabel.ts: -------------------------------------------------------------------------------- 1 | import { Version } from '@uniswap/token-lists'; 2 | 3 | export default function listVersionLabel(version: Version): string { 4 | return `v${version.major}.${version.minor}.${version.patch}`; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/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 | expect(parseENSAddress('hello-world.eth')).toEqual({ ensName: 'hello-world.eth', ensPath: undefined }); 14 | expect(parseENSAddress('-prefix-dash.eth')).toEqual(undefined); 15 | expect(parseENSAddress('suffix-dash-.eth')).toEqual(undefined); 16 | expect(parseENSAddress('it.eth')).toEqual({ ensName: 'it.eth', ensPath: undefined }); 17 | expect(parseENSAddress('only-single--dash.eth')).toEqual(undefined); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/utils/parseENSAddress.ts: -------------------------------------------------------------------------------- 1 | const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+)eth(\/.*)?$/; 2 | 3 | export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined { 4 | const match = ensAddress.match(ENS_NAME_REGEX); 5 | if (!match) return undefined; 6 | return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[4] }; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/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/trades.ts: -------------------------------------------------------------------------------- 1 | import { ZERO_PERCENT, ONE_HUNDRED_PERCENT } from './../constants/index'; 2 | import { Trade, Percent, currencyEquals } from '@uniswap/sdk'; 3 | 4 | // returns whether tradeB is better than tradeA by at least a threshold percentage amount 5 | export function isTradeBetter( 6 | tradeA: Trade | undefined | null, 7 | tradeB: Trade | undefined | null, 8 | minimumDelta: Percent = ZERO_PERCENT 9 | ): boolean | undefined { 10 | if (tradeA && !tradeB) return false; 11 | if (tradeB && !tradeA) return true; 12 | if (!tradeA || !tradeB) return undefined; 13 | 14 | if ( 15 | tradeA.tradeType !== tradeB.tradeType || 16 | !currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) || 17 | !currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency) 18 | ) { 19 | throw new Error('Trades are not comparable'); 20 | } 21 | 22 | if (minimumDelta.equalTo(ZERO_PERCENT)) { 23 | return tradeA.executionPrice.lessThan(tradeB.executionPrice); 24 | } else { 25 | return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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/useUSDCPrice.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Currency, currencyEquals, JSBI, Price, WETH } from '@uniswap/sdk'; 2 | import { useMemo } from 'react'; 3 | import { USDC } from '../constants'; 4 | import { PairState, usePairs } from '../data/Reserves'; 5 | import { useActiveWeb3React } from '../hooks'; 6 | import { wrappedCurrency } from './wrappedCurrency'; 7 | 8 | /** 9 | * Returns the price in USDC of the input currency 10 | * @param currency currency to compute the USDC price of 11 | */ 12 | export default function useUSDCPrice(currency?: Currency): Price | undefined { 13 | const { chainId } = useActiveWeb3React(); 14 | const wrapped = wrappedCurrency(currency, chainId); 15 | const tokenPairs: [Currency | undefined, Currency | undefined][] = useMemo( 16 | () => [ 17 | [ 18 | chainId && wrapped && currencyEquals(WETH[chainId], wrapped) ? undefined : currency, 19 | chainId ? WETH[chainId] : undefined, 20 | ], 21 | [wrapped?.equals(USDC) ? undefined : wrapped, chainId === ChainId.MAINNET ? USDC : undefined], 22 | [chainId ? WETH[chainId] : undefined, chainId === ChainId.MAINNET ? USDC : undefined], 23 | ], 24 | [chainId, currency, wrapped] 25 | ); 26 | const [[ethPairState, ethPair], [usdcPairState, usdcPair], [usdcEthPairState, usdcEthPair]] = usePairs(tokenPairs); 27 | 28 | return useMemo(() => { 29 | if (!currency || !wrapped || !chainId) { 30 | return undefined; 31 | } 32 | // handle weth/eth 33 | if (wrapped.equals(WETH[chainId])) { 34 | if (usdcPair) { 35 | const price = usdcPair.priceOf(WETH[chainId]); 36 | return new Price(currency, USDC, price.denominator, price.numerator); 37 | } else { 38 | return undefined; 39 | } 40 | } 41 | // handle usdc 42 | if (wrapped.equals(USDC)) { 43 | return new Price(USDC, USDC, '1', '1'); 44 | } 45 | 46 | const ethPairETHAmount = ethPair?.reserveOf(WETH[chainId]); 47 | const ethPairETHUSDCValue: JSBI = 48 | ethPairETHAmount && usdcEthPair ? usdcEthPair.priceOf(WETH[chainId]).quote(ethPairETHAmount).raw : JSBI.BigInt(0); 49 | 50 | // all other tokens 51 | // first try the usdc pair 52 | if (usdcPairState === PairState.EXISTS && usdcPair && usdcPair.reserveOf(USDC).greaterThan(ethPairETHUSDCValue)) { 53 | const price = usdcPair.priceOf(wrapped); 54 | return new Price(currency, USDC, price.denominator, price.numerator); 55 | } 56 | if (ethPairState === PairState.EXISTS && ethPair && usdcEthPairState === PairState.EXISTS && usdcEthPair) { 57 | if (usdcEthPair.reserveOf(USDC).greaterThan('0') && ethPair.reserveOf(WETH[chainId]).greaterThan('0')) { 58 | const ethUsdcPrice = usdcEthPair.priceOf(USDC); 59 | const currencyEthPrice = ethPair.priceOf(WETH[chainId]); 60 | const usdcPrice = ethUsdcPrice.multiply(currencyEthPrice).invert(); 61 | return new Price(currency, USDC, usdcPrice.denominator, usdcPrice.numerator); 62 | } 63 | } 64 | return undefined; 65 | }, [chainId, currency, ethPair, ethPairState, usdcEthPair, usdcEthPairState, usdcPair, usdcPairState, wrapped]); 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1azy-guy/CoolSwap-interface/fab8686405f950db4c381453684a2242b7e376ef/test/.gitkeep -------------------------------------------------------------------------------- /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": true, 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 | "baseUrl": "src" 27 | }, 28 | "exclude": ["node_modules"], 29 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "src/components/Confetti/index.js"] 30 | } 31 | --------------------------------------------------------------------------------