├── .yarnrc ├── src ├── tron-config.js ├── connectors │ ├── injected-tron-connector │ │ ├── README.md │ │ ├── tronlink-provider.d.ts │ │ └── tronlink-abis.ts │ ├── index.ts │ └── NetworkConnector.ts ├── assets │ ├── images │ │ ├── noise.png │ │ ├── metamask.png │ │ ├── xl_kwik.png │ │ ├── back_logo.png │ │ ├── portisIcon.png │ │ ├── big_kwikcorn.png │ │ ├── ethereum-logo.png │ │ ├── fortmaticIcon.png │ │ ├── trustWallet.png │ │ ├── arrow-right-white.png │ │ ├── kswap_logo_white.png │ │ ├── token-list │ │ │ ├── lists-dark.png │ │ │ └── lists-light.png │ │ ├── dropdown.svg │ │ ├── dropup-blue.svg │ │ ├── dropdown-blue.svg │ │ ├── plus-blue.svg │ │ ├── plus-grey.svg │ │ ├── arrow-right.svg │ │ ├── x.svg │ │ ├── blue-loader.svg │ │ ├── link.svg │ │ ├── circle.svg │ │ ├── circle-grey.svg │ │ ├── arrow-down-blue.svg │ │ ├── arrow-down-grey.svg │ │ ├── menu.svg │ │ ├── arrrow-external.svg │ │ ├── spinner.svg │ │ ├── question.svg │ │ ├── question-mark.svg │ │ ├── arrow-down-yellow.svg │ │ └── tronlink.svg │ └── svg │ │ ├── QR.svg │ │ └── lightcircle.svg ├── pages │ ├── Vote │ │ ├── vote.tsx │ │ └── styled.tsx │ ├── RemoveLiquidity │ │ └── redirects.tsx │ ├── AddLiquidity │ │ ├── redirects.tsx │ │ └── PoolPriceBar.tsx │ ├── Swap │ │ └── redirects.tsx │ ├── AppBody.tsx │ └── Pool │ │ └── styleds.tsx ├── constants │ ├── abis │ │ ├── migrator.ts │ │ ├── argent-wallet-detector.ts │ │ ├── erc20.ts │ │ ├── erc20_bytes32.json │ │ └── migrator.json │ ├── multicall │ │ └── index.ts │ ├── v │ │ ├── index.ts │ │ └── v_factory.json │ └── lists.ts ├── utils │ ├── isZero.ts │ ├── listVersionLabel.ts │ ├── getLibrary.ts │ ├── currencyId.ts │ ├── parseENSAddress.ts │ ├── chunkArray.ts │ ├── maxAmountSpend.ts │ ├── uriToHttp.ts │ ├── parseENSAddress.test.ts │ ├── wrappedCurrency.ts │ ├── contenthashToUri.test.skip.ts │ ├── chunkArray.test.ts │ ├── uriToHttp.test.ts │ ├── useDebouncedChangeHandler.tsx │ ├── contenthashToUri.ts │ ├── computeKwikCirculation.test.ts │ ├── prices.test.ts │ ├── resolveENSContentHash.ts │ ├── retry.ts │ ├── retry.test.ts │ └── getTokenList.ts ├── state │ ├── global │ │ └── actions.ts │ ├── burn │ │ ├── actions.ts │ │ └── reducer.ts │ ├── mint │ │ ├── actions.ts │ │ ├── reducer.test.ts │ │ └── reducer.ts │ ├── swap │ │ ├── actions.ts │ │ └── reducer.test.ts │ ├── lists │ │ └── actions.ts │ ├── application │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── updater.ts │ ├── transactions │ │ ├── actions.ts │ │ ├── updater.test.ts │ │ └── reducer.ts │ ├── user │ │ ├── updater.tsx │ │ ├── reducer.test.ts │ │ └── actions.ts │ ├── index.ts │ └── multicall │ │ ├── actions.ts │ │ └── actions.test.ts ├── hooks │ ├── useToggle.ts │ ├── useParsedQueryString.ts │ ├── useCurrentBlockTimestamp.ts │ ├── useToggledVersion.ts │ ├── useIsArgentWallet.ts │ ├── usePrevious.ts │ ├── useTimestampFromBlock.ts │ ├── useInterval.ts │ ├── useCopyClipboard.ts │ ├── useHttpLocations.ts │ ├── useDebounce.ts │ ├── useTransactionDeadline.ts │ ├── useWindowSize.ts │ ├── useOnClickOutside.tsx │ ├── useSocksBalance.ts │ ├── useENS.ts │ ├── useIsWindowVisible.ts │ ├── useLast.ts │ ├── useENSContentHash.ts │ ├── useENSAddress.ts │ ├── useColor.ts │ ├── useENSName.ts │ └── useFetchListCallback.ts ├── components │ ├── analytics │ │ └── GoogleAnalyticsReporter.tsx │ ├── swap │ │ ├── FormattedPriceImpact.tsx │ │ ├── confirmPriceImpactWithoutFee.ts │ │ ├── AdvancedSwapDetailsDropdown.tsx │ │ ├── SwapRoute.tsx │ │ ├── TradePrice.tsx │ │ ├── AdvancedPriceDetails.tsx │ │ └── BetterTradeLink.tsx │ ├── ListLogo │ │ └── index.tsx │ ├── FormattedCurrencyAmount │ │ └── index.tsx │ ├── Column │ │ └── index.tsx │ ├── SearchModal │ │ ├── SortButton.tsx │ │ ├── filtering.ts │ │ ├── sorting.ts │ │ ├── styleds.tsx │ │ ├── CurrencySearchModal.tsx │ │ └── CommonBases.tsx │ ├── Identicon │ │ └── index.tsx │ ├── Logo │ │ └── index.tsx │ ├── Confetti │ │ └── index.tsx │ ├── Tooltip │ │ └── index.tsx │ ├── DoubleLogo │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── Popups │ │ ├── TransactionPopup.tsx │ │ └── index.tsx │ ├── AccountDetails │ │ ├── Copy.tsx │ │ └── Transaction.tsx │ ├── Row │ │ └── index.tsx │ ├── Card │ │ └── index.tsx │ ├── Header │ │ └── URLWarning.tsx │ ├── CurrencyLogo │ │ └── index.tsx │ ├── Toggle │ │ └── index.tsx │ ├── vote │ │ └── styled.ts │ ├── QuestionHelper │ │ └── index.tsx │ ├── ModalViews │ │ └── index.tsx │ ├── PositionCard │ │ └── V.tsx │ ├── Web3ReactManager │ │ └── index.tsx │ └── ProgressSteps │ │ └── index.tsx ├── i18n.ts ├── data │ ├── TotalSupply.ts │ └── Allowances.ts ├── theme │ └── DarkModeQueryParamReader.tsx ├── react-app-env.d.ts └── index.tsx ├── public ├── iswap.png ├── favicon.ico ├── images │ ├── favicon.ico │ └── iswap.png ├── 451.html ├── manifest.json ├── tokenlist.json ├── index.html └── locales │ ├── zh-CN.json │ └── zh-TW.json ├── .env ├── .env.production ├── .prettierrc ├── cypress.json ├── cypress ├── tsconfig.json ├── integration │ ├── migrate-v.test.ts │ ├── send.test.ts │ ├── pool.test.ts │ ├── token-warning.ts │ ├── landing.test.ts │ ├── lists.test.ts │ ├── remove-liquidity.test.ts │ ├── add-liquidity.test.ts │ └── swap.test.ts └── support │ ├── commands.d.ts │ └── index.js ├── .gitignore ├── tsconfig.json ├── .eslintrc.json └── README.md /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /src/tron-config.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FEE_LIMIT = 1000_000_000; 2 | -------------------------------------------------------------------------------- /src/connectors/injected-tron-connector/README.md: -------------------------------------------------------------------------------- 1 | TODO: move to web3-tron as independent pkg 2 | -------------------------------------------------------------------------------- /public/iswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/public/iswap.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/connectors/injected-tron-connector/tronlink-provider.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@loveswap7/tronlink-provider'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_CHAIN_ID="1" 2 | REACT_APP_NETWORK_URL='https://api.trongrid.io' 3 | REACT_APP_TRON_NETWORK="mainnet" -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/iswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/public/images/iswap.png -------------------------------------------------------------------------------- /src/assets/images/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/noise.png -------------------------------------------------------------------------------- /src/assets/images/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/metamask.png -------------------------------------------------------------------------------- /src/assets/images/xl_kwik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/xl_kwik.png -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_CHAIN_ID="11111" 2 | REACT_APP_NETWORK_URL='https://api.trongrid.io' 3 | REACT_APP_TRON_NETWORK="mainnet" 4 | -------------------------------------------------------------------------------- /src/assets/images/back_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/back_logo.png -------------------------------------------------------------------------------- /src/assets/images/portisIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/portisIcon.png -------------------------------------------------------------------------------- /src/assets/images/big_kwikcorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/big_kwikcorn.png -------------------------------------------------------------------------------- /src/assets/images/ethereum-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/ethereum-logo.png -------------------------------------------------------------------------------- /src/assets/images/fortmaticIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/fortmaticIcon.png -------------------------------------------------------------------------------- /src/assets/images/trustWallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/trustWallet.png -------------------------------------------------------------------------------- /src/assets/images/arrow-right-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/arrow-right-white.png -------------------------------------------------------------------------------- /src/assets/images/kswap_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/kswap_logo_white.png -------------------------------------------------------------------------------- /src/pages/Vote/vote.tsx: -------------------------------------------------------------------------------- 1 | export const VoteComingSoon = () => { 2 | return ( 3 |
4 |

Coming Soon

5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/token-list/lists-dark.png -------------------------------------------------------------------------------- /src/assets/images/token-list/lists-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyMicky0317/ISwap-Interface-Shasta/HEAD/src/assets/images/token-list/lists-light.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "bracketSpacing": true, 8 | "arrowParens": "always" 9 | } -------------------------------------------------------------------------------- /src/assets/images/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropup-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropdown-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/constants/abis/migrator.ts: -------------------------------------------------------------------------------- 1 | import MIGRATOR_ABI from './migrator.json'; 2 | 3 | const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'; 4 | 5 | export { MIGRATOR_ADDRESS, MIGRATOR_ABI }; 6 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "pluginsFile": false, 4 | "fixturesFolder": false, 5 | "supportFile": "cypress/support/index.js", 6 | "video": false, 7 | "defaultCommandTimeout": 10000 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/images/plus-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/plus-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/isZero.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the string value is zero in hex 3 | * @param hexNumberString 4 | */ 5 | export default function isZero(hexNumberString: string) { 6 | return /^0x0*$/.test(hexNumberString); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/listVersionLabel.ts: -------------------------------------------------------------------------------- 1 | import { Version } from '@loveswap7/token-lists'; 2 | 3 | export default function listVersionLabel(version: Version): string { 4 | return `v${version.major}.${version.minor}.${version.patch}`; 5 | } 6 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "lib": ["es5", "dom"], 7 | "types": ["cypress"] 8 | }, 9 | "include": ["**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /public/451.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unavailable For Legal Reasons 6 | 7 | 8 |

Unavailable For Legal Reasons

9 | 10 | 11 | -------------------------------------------------------------------------------- /src/utils/getLibrary.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from '@ethersproject/providers'; 2 | export default function getLibrary(provider: any): Web3Provider { 3 | const library = new Web3Provider(provider); 4 | library.pollingInterval = 15000; 5 | return library; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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/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/utils/currencyId.ts: -------------------------------------------------------------------------------- 1 | import { Currency, ETHER, Token } from '@intercroneswap/swap-sdk'; 2 | 3 | export function currencyId(currency: Currency): string { 4 | if (currency === ETHER) return 'TRX'; 5 | if (currency instanceof Token) return currency.address; 6 | throw new Error('invalid currency'); 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/images/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/blue-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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/assets/images/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/integration/migrate-v.test.ts: -------------------------------------------------------------------------------- 1 | describe('Migrate V Liquidity', () => { 2 | describe('Remove V liquidity', () => { 3 | it('renders the correct page', () => { 4 | cy.visit('/remove/v1/0x93bB63aFe1E0180d0eF100D774B473034fd60C36') 5 | cy.get('#remove-v-exchange').should('contain', 'MKR/ETH') 6 | }) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/support/commands.d.ts: -------------------------------------------------------------------------------- 1 | export const TEST_ADDRESS_NEVER_USE: string 2 | 3 | export const TEST_ADDRESS_NEVER_USE_SHORTENED: string 4 | 5 | // declare namespace Cypress { 6 | // // eslint-disable-next-line @typescript-eslint/class-name-casing 7 | // interface cy { 8 | // additionalCommands(): void 9 | // } 10 | // } 11 | -------------------------------------------------------------------------------- /src/assets/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-down-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/arrow-down-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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/utils/parseENSAddress.ts: -------------------------------------------------------------------------------- 1 | const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/; 2 | 3 | export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined { 4 | const match = ensAddress.match(ENS_NAME_REGEX); 5 | if (!match) return undefined; 6 | return { ensName: `${match[1].toLowerCase()}trx`, ensPath: match[3] }; 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This file is processed and loaded automatically before your test files. 3 | // 4 | // You can read more here: 5 | // https://on.cypress.io/configuration 6 | // *********************************************************** 7 | 8 | // Import commands.ts using ES2015 syntax: 9 | import './commands' 10 | -------------------------------------------------------------------------------- /cypress/integration/send.test.ts: -------------------------------------------------------------------------------- 1 | describe('Send', () => { 2 | it('should redirect', () => { 3 | cy.visit('/send') 4 | cy.url().should('include', '/swap') 5 | }) 6 | 7 | it('should redirect with url params', () => { 8 | cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz') 9 | cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/integration/pool.test.ts: -------------------------------------------------------------------------------- 1 | describe('Pool', () => { 2 | beforeEach(() => cy.visit('/pool')) 3 | it('add liquidity links to /add/ETH', () => { 4 | cy.get('#join-pool-button').click() 5 | cy.url().should('contain', '/add/ETH') 6 | }) 7 | 8 | it('import pool links to /import', () => { 9 | cy.get('#import-pool-link').click() 10 | cy.url().should('contain', '/find') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /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/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/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/useToggledVersion.ts: -------------------------------------------------------------------------------- 1 | import useParsedQueryString from './useParsedQueryString'; 2 | 3 | export enum Version { 4 | v = 'v', 5 | v1 = 'v1', 6 | } 7 | 8 | export const DEFAULT_VERSION: Version = Version.v1; 9 | 10 | export default function useToggledVersion(): Version { 11 | const { use } = useParsedQueryString(); 12 | if (!use || typeof use !== 'string') return Version.v1; 13 | if (use.toLowerCase() === 'v') return Version.v; 14 | return DEFAULT_VERSION; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/analytics/GoogleAnalyticsReporter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import ReactGA from 'react-ga'; 3 | import { RouteComponentProps } from 'react-router-dom'; 4 | 5 | // fires a GA pageview every time the route changes 6 | export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null { 7 | useEffect(() => { 8 | ReactGA.pageview(`${pathname}${search}`); 9 | }, [pathname, search]); 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ISwap", 3 | "name": "ISwap", 4 | "icons": [ 5 | { 6 | "src": "./images/iswap.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "./images/iswap.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | } 17 | ], 18 | "orientation": "portrait", 19 | "display": "standalone", 20 | "theme_color": "#ff007a", 21 | "background_color": "#fff" 22 | } 23 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | /.netlify 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | notes.txt 26 | .idea/ 27 | 28 | .vscode/ 29 | 30 | package-lock.json 31 | 32 | cypress/videos 33 | cypress/screenshots 34 | cypress/fixtures/example.json 35 | .vercel 36 | -------------------------------------------------------------------------------- /src/pages/RemoveLiquidity/redirects.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps, Redirect } from 'react-router-dom'; 2 | 3 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/; 4 | 5 | export function RedirectOldRemoveLiquidityPathStructure({ 6 | match: { 7 | params: { tokens }, 8 | }, 9 | }: RouteComponentProps<{ tokens: string }>) { 10 | if (!OLD_PATH_STRUCTURE.test(tokens)) { 11 | return ; 12 | } 13 | const [currency0, currency1] = tokens.split('-'); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /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/constants/multicall/index.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@intercroneswap/swap-sdk'; 2 | import MULTICALL_ABI from './abi.json'; 3 | 4 | const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = { 5 | // TODO: TRON: mainnet multicall contract address 6 | [ChainId.MAINNET]: '0x9037ae53c89147e009d26f7547143544be00f984', 7 | // [ChainId.MAINNET]: '0xD3573a8728A49512A1485D63180Ed5b095e11D5C', 8 | [ChainId.NILE]: '0x04A6730FC23a5f2C3d94F7C7aCb4F92Eab8282c2', 9 | [ChainId.SHASTA]: '0x9037ae53c89147e009d26f7547143544be00f984', 10 | }; 11 | 12 | export { MULTICALL_ABI, MULTICALL_NETWORKS }; 13 | -------------------------------------------------------------------------------- /src/assets/svg/QR.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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/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/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/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/components/swap/FormattedPriceImpact.tsx: -------------------------------------------------------------------------------- 1 | import { Percent } from '@intercroneswap/swap-sdk'; 2 | import { ONE_BIPS } from '../../constants'; 3 | import { warningSeverity } from '../../utils/prices'; 4 | import { ErrorText } from './styleds'; 5 | 6 | /** 7 | * Formatted version of price impact text with warning colors 8 | */ 9 | export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { 10 | return ( 11 | 12 | {priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /cypress/integration/token-warning.ts: -------------------------------------------------------------------------------- 1 | describe('Warning', () => { 2 | beforeEach(() => { 3 | cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a') 4 | }) 5 | 6 | it('Check that warning is displayed', () => { 7 | cy.get('.token-warning-container').should('be.visible') 8 | }) 9 | 10 | it('Check that warning hides after button dismissal', () => { 11 | cy.get('.token-dismiss-button').should('be.disabled') 12 | cy.get('.understand-checkbox').click() 13 | cy.get('.token-dismiss-button').should('not.be.disabled') 14 | cy.get('.token-dismiss-button').click() 15 | cy.get('.token-warning-container').should('not.be.visible') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/ListLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import useHttpLocations from '../../hooks/useHttpLocations'; 3 | 4 | import Logo from '../Logo'; 5 | 6 | const StyledListLogo = styled(Logo)<{ size: string }>` 7 | width: ${({ size }) => size}; 8 | height: ${({ size }) => size}; 9 | `; 10 | 11 | export default function ListLogo({ 12 | logoURI, 13 | style, 14 | size = '24px', 15 | alt, 16 | }: { 17 | logoURI: string; 18 | size?: string; 19 | style?: React.CSSProperties; 20 | alt?: string; 21 | }) { 22 | const srcs: string[] = useHttpLocations(logoURI); 23 | 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cypress/integration/landing.test.ts: -------------------------------------------------------------------------------- 1 | import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands' 2 | 3 | describe('Landing Page', () => { 4 | beforeEach(() => cy.visit('/')) 5 | it('loads swap page', () => { 6 | cy.get('#swap-page') 7 | }) 8 | 9 | it('redirects to url /swap', () => { 10 | cy.url().should('include', '/swap') 11 | }) 12 | 13 | it('allows navigation to pool', () => { 14 | cy.get('#pool-nav-link').click() 15 | cy.url().should('include', '/pool') 16 | }) 17 | 18 | it('is connected', () => { 19 | cy.get('#web3-status-connected').click() 20 | cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/FormattedCurrencyAmount/index.tsx: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount, Fraction, JSBI } from '@intercroneswap/swap-sdk'; 2 | 3 | const CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000)); 4 | 5 | export default function FormattedCurrencyAmount({ 6 | currencyAmount, 7 | significantDigits = 4, 8 | }: { 9 | currencyAmount: CurrencyAmount; 10 | significantDigits?: number; 11 | }) { 12 | return ( 13 | <> 14 | {currencyAmount.equalTo(JSBI.BigInt(0)) 15 | ? '0' 16 | : currencyAmount.greaterThan(CURRENCY_AMOUNT_MIN) 17 | ? currencyAmount.toSignificant(significantDigits) 18 | : `<${CURRENCY_AMOUNT_MIN.toSignificant(1)}`} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/maxAmountSpend.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount, ETHER, JSBI } from '@intercroneswap/swap-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/assets/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/data/TotalSupply.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import { Token, TokenAmount } from '@intercroneswap/swap-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/data/Allowances.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenAmount } from '@intercroneswap/swap-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/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/assets/images/arrrow-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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/assets/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/assets/images/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/constants/v/index.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi'; 2 | import { ChainId } from '@intercroneswap/swap-sdk'; 3 | import V_EXCHANGE_ABI from './v_exchange.json'; 4 | import V_FACTORY_ABI from './v_factory.json'; 5 | 6 | const V_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = { 7 | // TODO: TRON: mainnet factory address 8 | [ChainId.MAINNET]: '0x0bdCBA8Ca6bAfcEc522F20eEF0CcE9BA603F3e43', 9 | [ChainId.NILE]: '0x64d5aF91C3A4aE5dB503dA8be25b5E47ad2D944e', 10 | [ChainId.SHASTA]: '0x735d2b61c97839d4dca5f39cdd2f49b92c3298ae', 11 | }; 12 | 13 | const V_FACTORY_INTERFACE = new Interface(V_FACTORY_ABI); 14 | const V_EXCHANGE_INTERFACE = new Interface(V_EXCHANGE_ABI); 15 | 16 | export { V_FACTORY_ADDRESSES, V_FACTORY_INTERFACE, V_FACTORY_ABI, V_EXCHANGE_INTERFACE, V_EXCHANGE_ABI }; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cypress/integration/lists.test.ts: -------------------------------------------------------------------------------- 1 | describe('Lists', () => { 2 | beforeEach(() => { 3 | cy.visit('/swap'); 4 | }); 5 | 6 | it('defaults to ISwap list', () => { 7 | cy.get('#swap-currency-output .open-currency-select-button').click(); 8 | cy.get('#currency-search-selected-list-name').should('contain', 'ISwap'); 9 | }); 10 | 11 | it('change list', () => { 12 | cy.get('#swap-currency-output .open-currency-select-button').click(); 13 | cy.get('#currency-search-change-list-button').click(); 14 | cy.get('#list-row-tokens-1inch-eth .select-button').click(); 15 | cy.get('#currency-search-selected-list-name').should('contain', '1inch'); 16 | cy.get('#currency-search-change-list-button').click(); 17 | cy.get('#currency-search-selected-list-name').should('contain', 'ISwap'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /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/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 | console.log('blockTimestamp', blockTimestamp); 12 | return useMemo(() => { 13 | if (blockTimestamp && ttl) return blockTimestamp.add(ttl); 14 | return undefined; 15 | }, [blockTimestamp, ttl]); 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/images/question-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/tokenlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Iswap Default List", 3 | "tokens": [ 4 | { 5 | "symbol": "JFI", 6 | "address": "0x854abd86ab4b76ec440eb7f4eeeba79e6d499417", 7 | "chainId": 11111, 8 | "decimals": 18, 9 | "name": "JFI TOKEN", 10 | "logoURI": "https://coin.top/production/logo/SUNLogo.178d4636.png" 11 | }, 12 | { 13 | "symbol": "TAI", 14 | "address": "0xaf2c205a7e44f79f680d149d339b733f6d34b6d5", 15 | "chainId": 11111, 16 | "decimals": 8, 17 | "name": "TAI", 18 | "logoURI": "https://coin.top/production/logo/TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9.png" 19 | } 20 | ], 21 | "logoURI": "https://intercroneswap.finance/iswap.png", 22 | "version": { 23 | "patch": 1, 24 | "major": 1, 25 | "minor": 0 26 | }, 27 | "timestamp": "2021-11-01T00:12:46.685Z" 28 | } 29 | -------------------------------------------------------------------------------- /src/constants/lists.ts: -------------------------------------------------------------------------------- 1 | // the ISwap Default token list lives here 2 | export const DEFAULT_TOKEN_LIST_URL = 'https://intercroneswap.finance/tokenlist.json'; 3 | 4 | export const DEFAULT_LIST_OF_LISTS: string[] = [ 5 | DEFAULT_TOKEN_LIST_URL, 6 | // 't2crtokens.eth', // kleros 7 | // 'tokens.1inch.eth', // 1inch 8 | // 'synths.snx.eth', 9 | // 'tokenlist.dharma.eth', 10 | // 'defi.cmc.eth', 11 | // 'erc20.cmc.eth', 12 | // 'stablecoin.cmc.eth', 13 | // 'tokenlist.zerion.eth', 14 | // 'tokenlist.aave.eth', 15 | // 'https://tokens.coingecko.com/uniswap/all.json', 16 | // 'https://app.tryroll.com/tokens.json', 17 | // 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json', 18 | // 'https://defiprime.com/defiprime.tokenlist.json', 19 | // 'https://umaproject.org/uma.tokenlist.json' 20 | ]; 21 | -------------------------------------------------------------------------------- /src/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/components/SearchModal/SortButton.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'rebass'; 2 | import styled from 'styled-components'; 3 | import { RowFixed } from '../Row'; 4 | 5 | export const FilterWrapper = styled(RowFixed)` 6 | padding: 8px; 7 | background-color: ${({ theme }) => theme.bg2}; 8 | color: ${({ theme }) => theme.text1}; 9 | border-radius: 8px; 10 | user-select: none; 11 | & > * { 12 | user-select: none; 13 | } 14 | :hover { 15 | cursor: pointer; 16 | } 17 | `; 18 | 19 | export default function SortButton({ 20 | toggleSortOrder, 21 | ascending, 22 | }: { 23 | toggleSortOrder: () => void; 24 | ascending: boolean; 25 | }) { 26 | return ( 27 | 28 | 29 | {ascending ? '↑' : '↓'} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /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/hooks/useSocksBalance.ts: -------------------------------------------------------------------------------- 1 | import { JSBI } from '@intercroneswap/swap-sdk'; 2 | import { useMemo } from 'react'; 3 | import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'; 4 | import { useActiveWeb3React } from './index'; 5 | import { useSocksController } from './useContract'; 6 | 7 | export default function useSocksBalance(): JSBI | undefined { 8 | const { account } = useActiveWeb3React(); 9 | const socksContract = useSocksController(); 10 | 11 | const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD); 12 | const data = result?.[0]; 13 | return data ? JSBI.BigInt(data.toString()) : undefined; 14 | } 15 | 16 | export function useHasSocks(): boolean | undefined { 17 | const balance = useSocksBalance(); 18 | return useMemo(() => balance && JSBI.greaterThan(balance, JSBI.BigInt(0)), [balance]); 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/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/utils/parseENSAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { parseENSAddress } from './parseENSAddress'; 2 | 3 | describe('parseENSAddress', () => { 4 | it('test cases', () => { 5 | expect(parseENSAddress('hello.eth')).toEqual({ ensName: 'hello.eth', ensPath: undefined }); 6 | expect(parseENSAddress('hello.eth/')).toEqual({ ensName: 'hello.eth', ensPath: '/' }); 7 | expect(parseENSAddress('hello.world.eth/')).toEqual({ ensName: 'hello.world.eth', ensPath: '/' }); 8 | expect(parseENSAddress('hello.world.eth/abcdef')).toEqual({ ensName: 'hello.world.eth', ensPath: '/abcdef' }); 9 | expect(parseENSAddress('abso.lutely')).toEqual(undefined); 10 | expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined }); 11 | expect(parseENSAddress('eth')).toEqual(undefined); 12 | expect(parseENSAddress('eth/hello-world')).toEqual(undefined); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/wrappedCurrency.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Currency, CurrencyAmount, ETHER, Token, TokenAmount, WETH } from '@intercroneswap/swap-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 | -------------------------------------------------------------------------------- /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('ISwap.eth contenthash', () => { 13 | expect(contenthashToUri('0xe5010170000f6170702e756e69737761702e6f7267')).toEqual('ipns://tron.ISwap.io'); 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/components/Identicon/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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/connectors/index.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from '@ethersproject/providers'; 2 | import { NetworkConnector } from './NetworkConnector'; 3 | import { InjectedTronConnector } from './injected-tron-connector'; 4 | 5 | const NETWORK_URL = process.env.REACT_APP_NETWORK_URL; 6 | 7 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1'); 8 | 9 | if (typeof NETWORK_URL === 'undefined') { 10 | throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`); 11 | } 12 | 13 | export const network = new NetworkConnector({ 14 | urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }, 15 | }); 16 | 17 | let networkLibrary: Web3Provider | undefined; 18 | export function getNetworkLibrary(): Web3Provider { 19 | return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any)); 20 | } 21 | 22 | export const injected = new InjectedTronConnector({ 23 | supportedChainIds: [11111, 1], 24 | }); 25 | -------------------------------------------------------------------------------- /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/assets/svg/lightcircle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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": "react-jsx", 23 | "downlevelIteration": true, 24 | "allowSyntheticDefaultImports": true, 25 | "types": ["react-spring", "jest"] 26 | }, 27 | "exclude": ["node_modules", "cypress"], 28 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "src/components/Confetti/index.js"] 29 | } 30 | -------------------------------------------------------------------------------- /src/connectors/NetworkConnector.ts: -------------------------------------------------------------------------------- 1 | import createJavaTronProvider from '@loveswap7/java-tron-provider'; 2 | 3 | import { InjectedTronConnector } from './injected-tron-connector'; 4 | 5 | export class NetworkConnector extends InjectedTronConnector { 6 | constructor(kwargs: any) { 7 | super(kwargs); 8 | this.provider = createJavaTronProvider({ 9 | network: process.env.REACT_APP_TRON_NETWORK, 10 | tronApiUrl: 'https://api.shasta.trongrid.io', 11 | }); 12 | } 13 | 14 | async requestProvider(...args: any[]) { 15 | const res = await this.provider.request(...args); 16 | // TODO: wrap error with throw new NoEthereumProviderError()? 17 | return res; 18 | } 19 | 20 | public async activate(): Promise { 21 | return { provider: this.provider }; 22 | } 23 | 24 | public async getProvider(): Promise { 25 | return this.provider; 26 | } 27 | 28 | public async getAccount(): Promise { 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | // Allows for the parsing of JSX 8 | "jsx": true 9 | } 10 | }, 11 | "ignorePatterns": ["node_modules/**/*"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "extends": [ 18 | "plugin:react/recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react-hooks/recommended", 21 | "plugin:prettier/recommended" 22 | ], 23 | "rules": { 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "prettier/prettier": "error", 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "react/prop-types": "off", 28 | "@typescript-eslint/ban-ts-comment": "off", 29 | "react/react-in-jsx-scope": "off", 30 | "@typescript-eslint/explicit-module-boundary-types": "off", 31 | "@typescript-eslint/no-var-requires": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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/components/Confetti/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactConfetti from 'react-confetti'; 2 | import { useWindowSize } from '../../hooks/useWindowSize'; 3 | 4 | // eslint-disable-next-line react/prop-types 5 | export default function Confetti({ start, variant }: { start: boolean; variant?: string }) { 6 | const { width, height } = useWindowSize(); 7 | 8 | const _variant = variant ? variant : height && width && height > 1.5 * width ? 'bottom' : variant; 9 | 10 | return start && width && height ? ( 11 | 30 | ) : null; 31 | } 32 | -------------------------------------------------------------------------------- /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/lists/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorWithPayload, createAction } from '@reduxjs/toolkit'; 2 | import { TokenList, Version } from '@loveswap7/token-lists'; 3 | 4 | export const fetchTokenList: Readonly<{ 5 | pending: ActionCreatorWithPayload<{ url: string; requestId: string }>; 6 | fulfilled: ActionCreatorWithPayload<{ url: string; tokenList: TokenList; requestId: string }>; 7 | rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }>; 8 | }> = { 9 | pending: createAction('lists/fetchTokenList/pending'), 10 | fulfilled: createAction('lists/fetchTokenList/fulfilled'), 11 | rejected: createAction('lists/fetchTokenList/rejected'), 12 | }; 13 | 14 | export const acceptListUpdate = createAction('lists/acceptListUpdate'); 15 | export const addList = createAction('lists/addList'); 16 | export const removeList = createAction('lists/removeList'); 17 | export const selectList = createAction('lists/selectList'); 18 | export const rejectVersionUpdate = createAction('lists/rejectVersionUpdate'); 19 | -------------------------------------------------------------------------------- /src/pages/Vote/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const handleColorType = (status?: any, theme?: any) => { 4 | switch (status) { 5 | case 'pending': 6 | return theme.blue1; 7 | case 'active': 8 | return theme.blue1; 9 | case 'succeeded': 10 | return theme.green1; 11 | case 'defeated': 12 | return theme.red1; 13 | case 'queued': 14 | return theme.text3; 15 | case 'executed': 16 | return theme.green1; 17 | case 'canceled': 18 | return theme.text3; 19 | case 'expired': 20 | return theme.text3; 21 | default: 22 | return theme.text3; 23 | } 24 | }; 25 | 26 | export const ProposalStatus = styled.span<{ status: string }>` 27 | font-size: 0.825rem; 28 | font-weight: 600; 29 | padding: 0.5rem; 30 | border-radius: 8px; 31 | color: ${({ status, theme }) => handleColorType(status, theme)}; 32 | border: 1px solid ${({ status, theme }) => handleColorType(status, theme)}; 33 | width: fit-content; 34 | justify-self: flex-end; 35 | text-transform: uppercase; 36 | `; 37 | -------------------------------------------------------------------------------- /src/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/components/SearchModal/filtering.ts: -------------------------------------------------------------------------------- 1 | import { isAddress } from '../../utils'; 2 | import { Token } from '@intercroneswap/swap-sdk'; 3 | 4 | export function filterTokens(tokens: Token[], search: string): Token[] { 5 | if (search.length === 0) return tokens; 6 | 7 | const searchingAddress = isAddress(search); 8 | 9 | if (searchingAddress) { 10 | return tokens.filter((token) => token.address === searchingAddress); 11 | } 12 | 13 | const lowerSearchParts = search 14 | .toLowerCase() 15 | .split(/\s+/) 16 | .filter((s) => s.length > 0); 17 | 18 | if (lowerSearchParts.length === 0) { 19 | return tokens; 20 | } 21 | 22 | const matchesSearch = (s: string): boolean => { 23 | const sParts = s 24 | .toLowerCase() 25 | .split(/\s+/) 26 | .filter((s) => s.length > 0); 27 | 28 | return lowerSearchParts.every((p) => p.length === 0 || sParts.some((sp) => sp.startsWith(p) || sp.endsWith(p))); 29 | }; 30 | 31 | return tokens.filter((token) => { 32 | const { symbol, name } = token; 33 | 34 | return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name)); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/state/application/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { TokenList } from '@loveswap7/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/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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 | background-color: ${({ theme }) => theme.settingCardbg}; 11 | border-radius: 8px; 12 | `; 13 | 14 | interface TooltipProps extends Omit { 15 | text: string; 16 | } 17 | 18 | export default function Tooltip({ text, ...rest }: TooltipProps) { 19 | return {text}} {...rest} />; 20 | } 21 | 22 | export function MouseoverTooltip({ children, ...rest }: Omit) { 23 | const [show, setShow] = useState(false); 24 | const open = useCallback(() => setShow(true), [setShow]); 25 | const close = useCallback(() => setShow(false), [setShow]); 26 | return ( 27 | 28 |
29 | {children} 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/AddLiquidity/redirects.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, RouteComponentProps } from 'react-router-dom'; 2 | import AddLiquidity from './index'; 3 | 4 | export function RedirectToAddLiquidity() { 5 | return ; 6 | } 7 | 8 | const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/; 9 | export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) { 10 | const { 11 | match: { 12 | params: { currencyIdA }, 13 | }, 14 | } = props; 15 | const match = currencyIdA.match(OLD_PATH_STRUCTURE); 16 | if (match?.length) { 17 | return ; 18 | } 19 | 20 | return ; 21 | } 22 | 23 | export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) { 24 | const { 25 | match: { 26 | params: { currencyIdA, currencyIdB }, 27 | }, 28 | } = props; 29 | if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) { 30 | return ; 31 | } 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/swap/confirmPriceImpactWithoutFee.ts: -------------------------------------------------------------------------------- 1 | import { Percent } from '@intercroneswap/swap-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/state/transactions/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { ChainId } from '@intercroneswap/swap-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/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/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/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/state/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; 2 | import { save, load } from 'redux-localstorage-simple'; 3 | 4 | import application from './application/reducer'; 5 | import { updateVersion } from './global/actions'; 6 | import user from './user/reducer'; 7 | import transactions from './transactions/reducer'; 8 | import swap from './swap/reducer'; 9 | import mint from './mint/reducer'; 10 | import lists from './lists/reducer'; 11 | import burn from './burn/reducer'; 12 | import multicall from './multicall/reducer'; 13 | 14 | const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists']; 15 | 16 | const store = configureStore({ 17 | reducer: { 18 | application, 19 | user, 20 | transactions, 21 | swap, 22 | mint, 23 | burn, 24 | multicall, 25 | lists, 26 | }, 27 | middleware: [...getDefaultMiddleware({ thunk: false }), save({ states: PERSISTED_KEYS })], 28 | preloadedState: load({ states: PERSISTED_KEYS }), 29 | }); 30 | 31 | store.dispatch(updateVersion()); 32 | 33 | export default store; 34 | 35 | export type AppState = ReturnType; 36 | export type AppDispatch = typeof store.dispatch; 37 | -------------------------------------------------------------------------------- /src/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://tron.ISwap.io')).toEqual([ 21 | 'https://cloudflare-ipfs.com/ipns/tron.ISwap.io/', 22 | 'https://ipfs.io/ipns/tron.ISwap.io/', 23 | ]); 24 | }); 25 | it('returns empty array for invalid scheme', () => { 26 | expect(uriToHttp('blah:test')).toEqual([]); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/DoubleLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Currency } from '@intercroneswap/swap-sdk'; 2 | import styled from 'styled-components'; 3 | import CurrencyLogo from '../CurrencyLogo'; 4 | 5 | const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>` 6 | position: relative; 7 | display: flex; 8 | flex-direction: row; 9 | margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'}; 10 | `; 11 | 12 | interface DoubleCurrencyLogoProps { 13 | margin?: boolean; 14 | size?: number; 15 | currency0?: Currency; 16 | currency1?: Currency; 17 | } 18 | 19 | const HigherLogo = styled(CurrencyLogo)` 20 | z-index: 2; 21 | `; 22 | const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>` 23 | position: absolute; 24 | left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important; 25 | `; 26 | 27 | export default function DoubleCurrencyLogo({ 28 | currency0, 29 | currency1, 30 | size = 16, 31 | margin = false, 32 | }: DoubleCurrencyLogoProps) { 33 | return ( 34 | 35 | {currency0 && } 36 | {currency1 && } 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const rotate = keyframes` 4 | from { 5 | transform: rotate(0deg); 6 | } 7 | to { 8 | transform: rotate(360deg); 9 | } 10 | `; 11 | 12 | const StyledSVG = styled.svg<{ size: string; stroke?: string }>` 13 | animation: 2s ${rotate} linear infinite; 14 | height: ${({ size }) => size}; 15 | width: ${({ size }) => size}; 16 | path { 17 | stroke: ${({ stroke, theme }) => stroke ?? theme.primary3}; 18 | } 19 | `; 20 | 21 | /** 22 | * Takes in custom size and stroke for circle color, default to primary color as fill, 23 | * need ...rest for layered styles on top 24 | */ 25 | export default function Loader({ 26 | size = '16px', 27 | stroke, 28 | ...rest 29 | }: { 30 | size?: string; 31 | stroke?: string; 32 | [k: string]: any; 33 | }) { 34 | return ( 35 | 36 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/constants/abis/migrator.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_factoryV", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_router", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "inputs": [ 20 | { 21 | "internalType": "address", 22 | "name": "token", 23 | "type": "address" 24 | }, 25 | { 26 | "internalType": "uint256", 27 | "name": "amountTokenMin", 28 | "type": "uint256" 29 | }, 30 | { 31 | "internalType": "uint256", 32 | "name": "amountETHMin", 33 | "type": "uint256" 34 | }, 35 | { 36 | "internalType": "address", 37 | "name": "to", 38 | "type": "address" 39 | }, 40 | { 41 | "internalType": "uint256", 42 | "name": "deadline", 43 | "type": "uint256" 44 | } 45 | ], 46 | "name": "migrate", 47 | "outputs": [], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "stateMutability": "payable", 53 | "type": "receive" 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /src/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/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/components/swap/AdvancedSwapDetailsDropdown.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useLastTruthy } from '../../hooks/useLast'; 3 | import AdvancedPriceDetails from './AdvancedPriceDetails'; 4 | import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'; 5 | 6 | const AdvancedDetailsFooter = styled.div<{ show: boolean }>` 7 | // padding-top: calc(16px + 2rem); 8 | // padding-top: 20px; 9 | padding-bottom: 20px; 10 | // margin-top: -2rem; 11 | width: 100%; 12 | max-width: 400px; 13 | // border-bottom-left-radius: 20px; 14 | // border-bottom-right-radius: 20px; 15 | border-radius: 16px; 16 | color: ${({ theme }) => theme.text2}; 17 | // background-color: ${({ theme }) => theme.bg1}; 18 | z-index: -1; 19 | width: 100%; 20 | max-width: 560px; 21 | transform: ${({ show }) => (show ? 'translateY(0px)' : 'translateY(1000px)')}; 22 | transition: transform 300ms ease-in-out; 23 | `; 24 | 25 | export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) { 26 | const lastTrade = useLastTruthy(trade); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Popups/TransactionPopup.tsx: -------------------------------------------------------------------------------- 1 | import { 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 && View on Tronscan} 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /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/components/AccountDetails/Copy.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import useCopyClipboard from '../../hooks/useCopyClipboard'; 3 | 4 | import { LinkStyledButton } from '../../theme'; 5 | import { CheckCircle, Copy } from 'react-feather'; 6 | 7 | const CopyIcon = styled(LinkStyledButton)` 8 | color: ${({ theme }) => theme.text3}; 9 | flex-shrink: 0; 10 | display: flex; 11 | text-decoration: none; 12 | font-size: 0.825rem; 13 | :hover, 14 | :active, 15 | :focus { 16 | text-decoration: none; 17 | color: ${({ theme }) => theme.text2}; 18 | } 19 | `; 20 | const TransactionStatusText = styled.span` 21 | margin-left: 0.25rem; 22 | font-size: 0.825rem; 23 | ${({ theme }) => theme.flexRowNoWrap}; 24 | align-items: center; 25 | `; 26 | 27 | export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { 28 | const [isCopied, setCopied] = useCopyClipboard(); 29 | 30 | return ( 31 | setCopied(props.toCopy)}> 32 | {isCopied ? ( 33 | 34 | 35 | Copied 36 | 37 | ) : ( 38 | 39 | 40 | 41 | )} 42 | {isCopied ? '' : props.children} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/swap/SwapRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Trade } from '@intercroneswap/swap-sdk'; 2 | import { Fragment, memo, useContext } from 'react'; 3 | import { ChevronRight } from 'react-feather'; 4 | import { Flex } from 'rebass'; 5 | import { ThemeContext } from 'styled-components'; 6 | import { TYPE } from '../../theme'; 7 | import CurrencyLogo from '../CurrencyLogo'; 8 | 9 | export default memo(function SwapRoute({ trade }: { trade: Trade }) { 10 | const theme = useContext(ThemeContext); 11 | return ( 12 | 22 | {trade.route.path.map((token, i, path) => { 23 | const isLastItem: boolean = i === path.length - 1; 24 | return ( 25 | 26 | 27 | 28 | 29 | {token.symbol} 30 | 31 | 32 | {isLastItem ? null : } 33 | 34 | ); 35 | })} 36 | 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /src/pages/Swap/redirects.tsx: -------------------------------------------------------------------------------- 1 | import { 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/state/user/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | 3 | export interface SerializedToken { 4 | chainId: number; 5 | address: string; 6 | decimals: number; 7 | symbol?: string; 8 | name?: string; 9 | } 10 | 11 | export interface SerializedPair { 12 | token0: SerializedToken; 13 | token1: SerializedToken; 14 | } 15 | 16 | export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode'); 17 | export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode'); 18 | export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode'); 19 | export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>( 20 | 'user/updateUserSlippageTolerance', 21 | ); 22 | export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline'); 23 | export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken'); 24 | export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken'); 25 | export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair'); 26 | export const removeSerializedPair = 27 | createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>('user/removeSerializedPair'); 28 | export const toggleURLWarning = createAction('app/toggleURLWarning'); 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/components/swap/TradePrice.tsx: -------------------------------------------------------------------------------- 1 | import { Price } from '@intercroneswap/swap-sdk'; 2 | import { useContext } from 'react'; 3 | import { Repeat } from 'react-feather'; 4 | import { Text } from 'rebass'; 5 | import { ThemeContext } from 'styled-components'; 6 | import { StyledBalanceMaxMini } from './styleds'; 7 | 8 | interface TradePriceProps { 9 | price?: Price; 10 | showInverted: boolean; 11 | setShowInverted: (showInverted: boolean) => void; 12 | } 13 | 14 | export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) { 15 | const theme = useContext(ThemeContext); 16 | 17 | const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6); 18 | 19 | const show = Boolean(price?.baseCurrency && price?.quoteCurrency); 20 | const label = showInverted 21 | ? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}` 22 | : `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`; 23 | 24 | return ( 25 | 31 | {show ? ( 32 | <> 33 | {formattedPrice ?? '-'} {label} 34 | setShowInverted(!showInverted)}> 35 | 36 | 37 | 38 | ) : ( 39 | '-' 40 | )} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /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/pages/AppBody.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const BodyWrapper = styled.div` 4 | // position: relative; 5 | max-width: 560px; 6 | width: 100%; 7 | margin-bottom: 20px; 8 | // opacity: 0.6; 9 | // background green; 10 | background: ${({ theme }) => theme.bg1}; 11 | // box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 12 | // 0px 24px 32px rgba(0, 0, 0, 0.01); 13 | box-shadow: 0px 2px 26px rgba(0, 0, 0, 0.15); 14 | border-radius: 16px; 15 | padding: 30px; 16 | // padding: 1rem; 17 | // margin: 64px auto; 18 | overflow: hidden; 19 | // position: relative; 20 | // max-width: 420px; 21 | // width: 100%; 22 | // background: ${({ theme }) => theme.bg1}; 23 | // box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 24 | // 0px 24px 32px rgba(0, 0, 0, 0.01); 25 | // border-radius: 30px; 26 | // padding: 1rem; 27 | `; 28 | 29 | /** 30 | * The styled container element that wraps the content of most pages and the tabs. 31 | */ 32 | export default function AppBody({ children }: { children: React.ReactNode }) { 33 | return {children}; 34 | } 35 | export const Container = styled.div` 36 | width: 100%; 37 | max-width: 1200px; 38 | margin: 0 auto; 39 | display: grid; 40 | grid-template-columns: 1fr 1fr; 41 | column-gap: 20px; 42 | @media (max-width: 1140px) { 43 | max-width: 560px; 44 | column-gap: 0px; 45 | grid-template-columns: 1fr; 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /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/computeKwikCirculation.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, JSBI, Token, TokenAmount } from '@intercroneswap/swap-sdk'; 2 | import { BigNumber } from 'ethers'; 3 | import { ZERO_ADDRESS } from '../constants'; 4 | import { computeKwikCirculation } from './computeKwikCirculation'; 5 | 6 | describe('computeKwikCirculation', () => { 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(computeKwikCirculation(token, BigNumber.from(0), undefined)).toEqual(tokenAmount(150_000_000)); 19 | expect(computeKwikCirculation(token, BigNumber.from(1600387200), undefined)).toEqual(tokenAmount(150_000_000)); 20 | }); 21 | it('mid staking', () => { 22 | expect(computeKwikCirculation(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(computeKwikCirculation(token, BigNumber.from(1600387200 + 60 * 24 * 60 * 60), undefined)).toEqual( 28 | tokenAmount(224_575_341), 29 | ); 30 | }); 31 | it('subtracts unclaimed kwik', () => { 32 | expect(computeKwikCirculation(token, BigNumber.from(1600387200 + 15 * 24 * 60 * 60), tokenAmount(1000))).toEqual( 33 | tokenAmount(154_999_000), 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /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 '@intercroneswap/swap-sdk'; 6 | 7 | async function getColorFromToken(token: Token): Promise { 8 | if (token.chainId === ChainId.SHASTA && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') { 9 | return Promise.resolve('#FAAB14'); 10 | } 11 | 12 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${token.address}/logo.png`; 13 | 14 | return Vibrant.from(path) 15 | .getPalette() 16 | .then((palette) => { 17 | if (palette?.Vibrant) { 18 | let detectedHex = palette.Vibrant.hex; 19 | let AAscore = hex(detectedHex, '#FFF'); 20 | while (AAscore < 3) { 21 | detectedHex = shade(0.005, detectedHex); 22 | AAscore = hex(detectedHex, '#FFF'); 23 | } 24 | return detectedHex; 25 | } 26 | return null; 27 | }) 28 | .catch(() => null); 29 | } 30 | 31 | export function useColor(token?: Token) { 32 | const [color, setColor] = useState('#2172E5'); 33 | 34 | useLayoutEffect(() => { 35 | let stale = false; 36 | 37 | if (token) { 38 | getColorFromToken(token).then((tokenColor) => { 39 | if (!stale && tokenColor !== null) { 40 | setColor(tokenColor); 41 | } 42 | }); 43 | } 44 | 45 | return () => { 46 | stale = true; 47 | setColor('#2172E5'); 48 | }; 49 | }, [token]); 50 | 51 | return color; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Row/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Box } from 'rebass/styled-components'; 3 | 4 | const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>` 5 | width: 100%; 6 | display: flex; 7 | padding: 0; 8 | align-items: ${({ align }) => (align ? align : 'center')}; 9 | padding: ${({ padding }) => padding}; 10 | border: ${({ border }) => border}; 11 | border-radius: ${({ borderRadius }) => borderRadius}; 12 | `; 13 | export const PercentageDiv = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>` 14 | width: 100%; 15 | display: flex; 16 | padding: 0; 17 | align-items: ${({ align }) => (align ? align : 'center')}; 18 | padding: ${({ padding }) => padding}; 19 | border: ${({ border }) => border}; 20 | border-radius: ${({ borderRadius }) => borderRadius}; 21 | `; 22 | 23 | export const RowBetween = styled(Row)` 24 | justify-content: space-between; 25 | `; 26 | 27 | export const RowFlat = styled.div` 28 | display: flex; 29 | align-items: flex-end; 30 | `; 31 | 32 | export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` 33 | flex-wrap: wrap; 34 | margin: ${({ gap }) => gap && `-${gap}`}; 35 | justify-content: ${({ justify }) => justify && justify}; 36 | 37 | & > * { 38 | margin: ${({ gap }) => gap} !important; 39 | } 40 | `; 41 | 42 | export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` 43 | width: fit-content; 44 | // background-color: red; 45 | justify-content: space-between; 46 | margin: ${({ gap }) => gap && `-${gap}`}; 47 | `; 48 | 49 | export default Row; 50 | -------------------------------------------------------------------------------- /src/pages/Pool/styleds.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'rebass'; 2 | import styled from 'styled-components'; 3 | 4 | export const Wrapper = styled.div` 5 | position: relative; 6 | `; 7 | 8 | export const ClickableText = styled(Text)` 9 | :hover { 10 | cursor: pointer; 11 | } 12 | color: ${({ theme }) => theme.primary3}; 13 | `; 14 | export const MaxButton = styled.button<{ width: string; active?: boolean }>` 15 | padding: 0.5rem 1rem; 16 | background-color: ${({ theme }) => theme.primary5}; 17 | border: 1px solid ${({ theme }) => theme.primary5}; 18 | border-radius: 0.5rem; 19 | font-size: 1rem; 20 | ${({ theme }) => theme.mediaWidth.upToSmall` 21 | padding: 0.25rem 0.5rem; 22 | `}; 23 | font-weight: 500; 24 | cursor: pointer; 25 | margin: 0.25rem; 26 | overflow: hidden; 27 | color: ${({ theme }) => theme.text1}; 28 | // color: ${({ theme }) => theme.primary3}; 29 | background: ${({ theme }) => theme.bg3}; 30 | ${({ active, theme }) => 31 | active && 32 | ` 33 | color:#000; 34 | background:${theme.primary1}; 35 | `} 36 | :hover { 37 | border: 1px solid ${({ theme }) => theme.primary3}; 38 | } 39 | :focus { 40 | border: 1px solid ${({ theme }) => theme.primary3}; 41 | outline: none; 42 | } 43 | `; 44 | 45 | export const Dots = styled.span` 46 | &::after { 47 | display: inline-block; 48 | animation: ellipsis 1.25s infinite; 49 | content: '.'; 50 | width: 1em; 51 | text-align: left; 52 | } 53 | @keyframes ellipsis { 54 | 0% { 55 | content: '.'; 56 | } 57 | 33% { 58 | content: '..'; 59 | } 60 | 66% { 61 | content: '...'; 62 | } 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /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 | interface Ethereum { 9 | isMetaMask?: true; 10 | send: unknown; 11 | enable: () => Promise; 12 | on?: (method: string, listener: (...args: any[]) => void) => void; 13 | removeListener?: (method: string, listener: (...args: any[]) => void) => void; 14 | } 15 | interface tronWeb { 16 | send: unknown; 17 | enable: () => Promise; 18 | on?: (method: string, listener: (...args: any[]) => void) => void; 19 | removeListener?: (method: string, listener: (...args: any[]) => void) => void; 20 | trx?: { getBlock?: (m: string) => Promise }; 21 | defaultAddress?: { 22 | base58?: ''; 23 | }; 24 | fullNode?: { 25 | chainType?: ''; 26 | host?: ''; 27 | }; 28 | } 29 | interface Window { 30 | // ethereum?: { 31 | // isMetaMask?: true 32 | // on?: (...args: any[]) => void 33 | // removeListener?: (...args: any[]) => void 34 | // }, 35 | ethereum?: Ethereum; 36 | tronWeb?: tronWeb; 37 | web3?: {}; 38 | } 39 | declare const __DEV__: boolean; 40 | declare module 'tronweb'; 41 | declare module '@loveswap7/java-tron-provider'; 42 | declare module 'trongrid'; 43 | declare module 'content-hash' { 44 | declare function decode(x: string): string; 45 | declare function getCodec(x: string): string; 46 | } 47 | 48 | declare module 'multihashes' { 49 | declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }; 50 | declare function toB58String(hash: Uint8Array): string; 51 | } 52 | -------------------------------------------------------------------------------- /src/connectors/injected-tron-connector/tronlink-abis.ts: -------------------------------------------------------------------------------- 1 | // all abis... 2 | import { V_FACTORY_ABI, V_EXCHANGE_ABI } from '../../constants/v'; 3 | import ENS_ABI from '../../constants/abis/ens-registrar.json'; 4 | import IloveswapV1Router02ABI from '../../constants/abis/router02.json'; 5 | 6 | import ENS_PUBLIC_RESOLVER_ABI from '../../constants/abis/ens-public-resolver.json'; 7 | // import UNISOCKS_ABI from '../../constants/abis/unisocks.json' 8 | import WETH_ABI from '../../constants/abis/weth.json'; 9 | import { MIGRATOR_ABI } from '../../constants/abis/migrator'; 10 | import ERC20_ABI from '../../constants/abis/erc20.json'; 11 | import { MULTICALL_ABI } from '../../constants/multicall'; 12 | import { abi as IloveswapV1PairABI } from '@loveswap7/v1-core/build/ILoveswapV1Pair.json'; 13 | 14 | export const abis = [ 15 | ...ERC20_ABI, 16 | ...V_FACTORY_ABI, 17 | ...V_EXCHANGE_ABI, 18 | ...IloveswapV1Router02ABI, 19 | // ...IUniswapV2PairABI, 20 | ...ENS_ABI, 21 | ...ENS_PUBLIC_RESOLVER_ABI, 22 | // ...UNISOCKS_ABI, 23 | ...WETH_ABI, 24 | ...MIGRATOR_ABI, 25 | ...MULTICALL_ABI, 26 | ...IloveswapV1PairABI, 27 | { 28 | constant: true, 29 | inputs: [ 30 | { 31 | internalType: 'address', 32 | name: 'tokenA', 33 | type: 'address', 34 | }, 35 | { 36 | internalType: 'address', 37 | name: 'tokenB', 38 | type: 'address', 39 | }, 40 | ], 41 | name: 'getPair', 42 | outputs: [ 43 | { 44 | internalType: 'address', 45 | name: 'pair', 46 | type: 'address', 47 | }, 48 | ], 49 | payable: false, 50 | stateMutability: 'view', 51 | type: 'function', 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { CardProps, Text } from 'rebass'; 3 | import { Box } from 'rebass/styled-components'; 4 | 5 | const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>` 6 | width: 100%; 7 | border-radius: 16px; 8 | padding: 2rem; 9 | padding: ${({ padding }) => padding}; 10 | border: ${({ border }) => border}; 11 | border-radius: ${({ borderRadius }) => borderRadius}; 12 | `; 13 | export default Card; 14 | 15 | export const LightCard = styled(Card)` 16 | border: 1px solid ${({ theme }) => theme.primary6}; 17 | background-color: ${({ theme }) => theme.bg1}; 18 | `; 19 | 20 | export const GreyCard = styled(Card)` 21 | background-color: ${({ theme }) => theme.bg3}; 22 | `; 23 | 24 | export const OutlineCard = styled(Card)` 25 | border: 1px solid ${({ theme }) => theme.bg3}; 26 | `; 27 | 28 | export const YellowCard = styled(Card)` 29 | background-color: rgba(243, 132, 30, 0.05); 30 | color: ${({ theme }) => theme.yellow2}; 31 | font-weight: 500; 32 | `; 33 | 34 | export const PinkCard = styled(Card)` 35 | background-color: rgba(255, 0, 122, 0.03); 36 | color: ${({ theme }) => theme.primary3}; 37 | font-weight: 500; 38 | `; 39 | 40 | const BlueCardStyled = styled(Card)` 41 | background-color: ${({ theme }) => theme.primary5}; 42 | color: ${({ theme }) => theme.primary3}; 43 | border-radius: 12px; 44 | width: fit-content; 45 | `; 46 | 47 | export const BlueCard = ({ children, ...rest }: CardProps) => { 48 | return ( 49 | 50 | 51 | {children} 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /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/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/components/Header/URLWarning.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { AlertTriangle, X } from 'react-feather'; 4 | import { useURLWarningToggle, useURLWarningVisible } from '../../state/user/hooks'; 5 | import { isMobile } from 'react-device-detect'; 6 | 7 | const PhishAlert = styled.div<{ isActive: any }>` 8 | width: 100%; 9 | padding: 6px 6px; 10 | background-color: ${({ theme }) => theme.blue1}; 11 | color: white; 12 | font-size: 11px; 13 | justify-content: space-between; 14 | align-items: center; 15 | display: ${({ isActive }) => (isActive ? 'flex' : 'none')}; 16 | `; 17 | 18 | export const StyledClose = styled(X)` 19 | :hover { 20 | cursor: pointer; 21 | } 22 | `; 23 | 24 | export default function URLWarning() { 25 | const toggleURLWarning = useURLWarningToggle(); 26 | const showURLWarning = useURLWarningVisible(); 27 | 28 | return isMobile ? ( 29 | 30 |
31 | Make sure the URL is 32 | tron.ISwap.io 33 |
34 | 35 |
36 | ) : window.location.hostname === 'tron.ISwap.io' ? ( 37 | 38 |
39 | Always make sure the URL is 40 | tron.ISwap.io - bookmark it to 41 | be safe. 42 |
43 | 44 |
45 | ) : null; 46 | } 47 | -------------------------------------------------------------------------------- /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/assets/images/arrow-down-yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/prices.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, JSBI, Pair, Route, Token, TokenAmount, Trade, TradeType } from '@intercroneswap/swap-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/components/SearchModal/sorting.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenAmount } from '@intercroneswap/swap-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/constants/v/v_factory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "NewExchange", 4 | "inputs": [ 5 | { "type": "address", "name": "token", "indexed": true }, 6 | { "type": "address", "name": "exchange", "indexed": true } 7 | ], 8 | "anonymous": false, 9 | "type": "event" 10 | }, 11 | { 12 | "name": "initializeFactory", 13 | "outputs": [], 14 | "inputs": [{ "type": "address", "name": "template" }], 15 | "constant": false, 16 | "payable": false, 17 | "type": "function" 18 | }, 19 | { 20 | "name": "createExchange", 21 | "outputs": [{ "type": "address", "name": "out" }], 22 | "inputs": [{ "type": "address", "name": "token" }], 23 | "constant": false, 24 | "payable": false, 25 | "type": "function" 26 | }, 27 | { 28 | "name": "getExchange", 29 | "outputs": [{ "type": "address", "name": "out" }], 30 | "inputs": [{ "type": "address", "name": "token" }], 31 | "constant": true, 32 | "payable": false, 33 | "type": "function" 34 | }, 35 | { 36 | "name": "getToken", 37 | "outputs": [{ "type": "address", "name": "out" }], 38 | "inputs": [{ "type": "address", "name": "exchange" }], 39 | "constant": true, 40 | "payable": false, 41 | "type": "function" 42 | }, 43 | { 44 | "name": "getTokenWithId", 45 | "outputs": [{ "type": "address", "name": "out" }], 46 | "inputs": [{ "type": "uint256", "name": "token_id" }], 47 | "constant": true, 48 | "payable": false, 49 | "type": "function" 50 | }, 51 | { 52 | "name": "exchangeTemplate", 53 | "outputs": [{ "type": "address", "name": "out" }], 54 | "inputs": [], 55 | "constant": true, 56 | "payable": false, 57 | "type": "function" 58 | }, 59 | { 60 | "name": "tokenCount", 61 | "outputs": [{ "type": "uint256", "name": "out" }], 62 | "inputs": [], 63 | "constant": true, 64 | "payable": false, 65 | "type": "function" 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /cypress/integration/remove-liquidity.test.ts: -------------------------------------------------------------------------------- 1 | describe('Remove Liquidity', () => { 2 | it('redirects', () => { 3 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 4 | cy.url().should( 5 | 'contain', 6 | '/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85' 7 | ) 8 | }) 9 | 10 | it('eth remove', () => { 11 | cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 12 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH') 13 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR') 14 | }) 15 | 16 | it('eth remove swap order', () => { 17 | cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH') 18 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR') 19 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH') 20 | }) 21 | 22 | it('loads the two correct tokens', () => { 23 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 24 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH') 25 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR') 26 | }) 27 | 28 | it('does not crash if ETH is duplicated', () => { 29 | cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') 30 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH') 31 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH') 32 | }) 33 | 34 | it('token not in storage is loaded', () => { 35 | cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 36 | cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL') 37 | cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /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/pages/AddLiquidity/PoolPriceBar.tsx: -------------------------------------------------------------------------------- 1 | import { Currency, Percent, Price } from '@intercroneswap/swap-sdk'; 2 | import { 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/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ISwap Interface 2 | 3 | 4 | An open source interface for ISwap -- a protocol for decentralized exchange of Ethereum tokens. 5 | 6 | - Website: [ISwap.io](https://ISwap.io/) 7 | - Interface: [tron.ISwap.io](https://tron.ISwap.io) 8 | - Docs: [ISwap.io/docs/](https://ISwap.io/docs/) 9 | - Reddit: [/r/ISwap](https://www.reddit.com/r/ISwap/) 10 | - Email: [admin@ISwap.io](mailto:admin@ISwap.io) 11 | 12 | ## Accessing the ISwap Interface 13 | 14 | To access the ISwap Interface, use an IPFS gateway link from the 15 | [latest release](https://github.com/ISwap/ISwap-interface/releases/latest), 16 | or visit [tron.ISwap.io](https://tron.ISwap.io). 17 | 18 | ## Listing a token 19 | 20 | Please see the 21 | [@ISwap/default-token-list](https://github.com/ISwap/default-token-list) 22 | repository. 23 | 24 | ## Development 25 | 26 | ### Install Dependencies 27 | 28 | ```bash 29 | yarn 30 | ``` 31 | 32 | ### Run 33 | 34 | ```bash 35 | yarn start 36 | ``` 37 | 38 | ### Configuring the environment (optional) 39 | 40 | To have the interface default to a different network when a wallet is not connected: 41 | 42 | 1. Make a copy of `.env` named `.env.local` 43 | 2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"` 44 | 3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"` 45 | 46 | Note that the interface only works on testnets where both 47 | [ISwap V1](https://ISwap.io/docs/v1/smart-contracts/factory/) and 48 | [multicall](https://github.com/makerdao/multicall) are deployed. 49 | The interface will not work on other networks. 50 | 51 | ## Contributions 52 | 53 | **Please open all pull requests against the `master` branch.** 54 | CI checks will run against all PRs. 55 | 56 | ## Accessing ISwap Interface 57 | 58 | The ISwap Interface supports swapping against, and migrating or removing liquidity from ISwap . However, 59 | if you would like to use ISwap, the ISwap interface for mainnet and testnets is accessible via IPFS gateways 60 | linked from the [v1.0.0 release](https://github.com/ISwap/ISwap-interface/releases/tag/v1.0.0). 61 | -------------------------------------------------------------------------------- /src/hooks/useFetchListCallback.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@reduxjs/toolkit'; 2 | import { ChainId } from '@intercroneswap/swap-sdk'; 3 | import { TokenList } from '@loveswap7/token-lists'; 4 | import { useCallback } from 'react'; 5 | import { useDispatch } from 'react-redux'; 6 | import { getNetworkLibrary, NETWORK_CHAIN_ID } from '../connectors'; 7 | import { AppDispatch } from '../state'; 8 | import { fetchTokenList } from '../state/lists/actions'; 9 | import getTokenList from '../utils/getTokenList'; 10 | import resolveENSContentHash from '../utils/resolveENSContentHash'; 11 | import { useActiveWeb3React } from './index'; 12 | 13 | export function useFetchListCallback(): (listUrl: string) => Promise { 14 | const { chainId, library } = useActiveWeb3React(); 15 | const dispatch = useDispatch(); 16 | 17 | const ensResolver = useCallback( 18 | (ensName: string) => { 19 | if (!library || chainId !== ChainId.MAINNET) { 20 | if (NETWORK_CHAIN_ID === ChainId.MAINNET) { 21 | const networkLibrary = getNetworkLibrary(); 22 | if (networkLibrary) { 23 | return resolveENSContentHash(ensName, networkLibrary); 24 | } 25 | } 26 | throw new Error('Could not construct mainnet ENS resolver'); 27 | } 28 | return resolveENSContentHash(ensName, library); 29 | }, 30 | [chainId, library], 31 | ); 32 | 33 | return useCallback( 34 | async (listUrl: string) => { 35 | const requestId = nanoid(); 36 | dispatch(fetchTokenList.pending({ requestId, url: listUrl })); 37 | return getTokenList(listUrl, ensResolver) 38 | .then((tokenList) => { 39 | dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })); 40 | return tokenList; 41 | }) 42 | .catch((error) => { 43 | console.debug(`Failed to get list at url ${listUrl}`, error); 44 | dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message })); 45 | throw error; 46 | }); 47 | }, 48 | [dispatch, ensResolver], 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/images/tronlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 7 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/CurrencyLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Currency, ETHER, Token } from '@intercroneswap/swap-sdk'; 2 | import { 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 | import { ethAddress } from '@loveswap7/java-tron-provider'; 10 | 11 | const getTokenLogoURL = (address: string) => { 12 | const tronAddress = ethAddress.toTron(address); 13 | return `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/tron/assets/${tronAddress}/logo.png`; 14 | }; 15 | 16 | const StyledEthereumLogo = styled.img<{ size: string }>` 17 | width: ${({ size }) => size}; 18 | height: ${({ size }) => size}; 19 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); 20 | border-radius: 24px; 21 | `; 22 | 23 | const StyledLogo = styled(Logo)<{ size: string }>` 24 | width: ${({ size }) => size}; 25 | height: ${({ size }) => size}; 26 | border-radius: ${({ size }) => size}; 27 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); 28 | `; 29 | 30 | export default function CurrencyLogo({ 31 | currency, 32 | size = '24px', 33 | style, 34 | }: { 35 | currency?: Currency; 36 | size?: string; 37 | style?: React.CSSProperties; 38 | }) { 39 | const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined); 40 | 41 | const srcs: string[] = useMemo(() => { 42 | if (currency === ETHER) return []; 43 | 44 | if (currency instanceof Token) { 45 | if (currency instanceof WrappedTokenInfo) { 46 | return [...uriLocations, getTokenLogoURL(currency.address)]; 47 | } 48 | 49 | return [getTokenLogoURL(currency.address)]; 50 | } 51 | return []; 52 | }, [currency, uriLocations]); 53 | 54 | if (currency === ETHER) { 55 | return ; 56 | } 57 | 58 | return ; 59 | } 60 | -------------------------------------------------------------------------------- /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/components/SearchModal/styleds.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { AutoColumn } from '../Column'; 3 | import { RowBetween, RowFixed } from '../Row'; 4 | 5 | export const ModalInfo = styled.div` 6 | ${({ theme }) => theme.flexRowNoWrap} 7 | align-items: center; 8 | padding: 1rem 1rem; 9 | margin: 0.25rem 0.5rem; 10 | justify-content: center; 11 | flex: 1; 12 | user-select: none; 13 | `; 14 | 15 | export const FadedSpan = styled(RowFixed)` 16 | color: ${({ theme }) => theme.primary3}; 17 | font-size: 14px; 18 | `; 19 | 20 | export const PaddedColumn = styled(AutoColumn)` 21 | padding: 20px; 22 | padding-bottom: 12px; 23 | `; 24 | 25 | export const MenuItem = styled(RowBetween)` 26 | padding: 4px 20px; 27 | height: 56px; 28 | display: grid; 29 | grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px); 30 | grid-gap: 16px; 31 | cursor: ${({ disabled }) => !disabled && 'pointer'}; 32 | pointer-events: ${({ disabled }) => disabled && 'none'}; 33 | :hover { 34 | background-color: ${({ theme, disabled }) => !disabled && theme.bg2}; 35 | } 36 | opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)}; 37 | `; 38 | 39 | export const SearchInput = styled.input` 40 | position: relative; 41 | display: flex; 42 | padding: 16px; 43 | align-items: center; 44 | width: 100%; 45 | white-space: nowrap; 46 | background: none; 47 | border: none; 48 | outline: none; 49 | border-radius: 8px; 50 | color: ${({ theme }) => theme.text1}; 51 | background: ${({ theme }) => theme.bg3}; 52 | // border-style: solid; 53 | // border: 1px solid ${({ theme }) => theme.bg3}; 54 | -webkit-appearance: none; 55 | 56 | font-size: 18px; 57 | 58 | ::placeholder { 59 | color: ${({ theme }) => theme.text3}; 60 | } 61 | transition: border 100ms; 62 | :focus { 63 | border: 1px solid ${({ theme }) => theme.primary3}; 64 | outline: none; 65 | } 66 | `; 67 | export const Separator = styled.div` 68 | width: 100%; 69 | height: 1px; 70 | background-color: ${({ theme }) => theme.bg2}; 71 | `; 72 | 73 | export const SeparatorDark = styled.div` 74 | width: 100%; 75 | height: 1px; 76 | background-color: ${({ theme }) => theme.bg3}; 77 | `; 78 | -------------------------------------------------------------------------------- /src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>` 4 | padding: 0.25rem 0.5rem; 5 | border-radius: 14px; 6 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary3 : theme.text4) : 'none')}; 7 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 8 | font-size: 1rem; 9 | font-weight: 400; 10 | 11 | padding: 0.35rem 0.6rem; 12 | border-radius: 12px; 13 | background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary3 : theme.text4) : 'none')}; 14 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text2)}; 15 | font-size: 1rem; 16 | font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')}; 17 | :hover { 18 | user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')}; 19 | background: ${({ theme, isActive, isOnSwitch }) => 20 | isActive ? (isOnSwitch ? theme.primary3 : theme.text3) : 'none'}; 21 | color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)}; 22 | } 23 | `; 24 | 25 | const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>` 26 | border-radius: 12px; 27 | border: none; 28 | /* border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; */ 29 | background: ${({ theme }) => theme.bg3}; 30 | display: flex; 31 | width: fit-content; 32 | cursor: pointer; 33 | outline: none; 34 | padding: 0; 35 | /* background-color: transparent; */ 36 | `; 37 | 38 | export interface ToggleProps { 39 | id?: string; 40 | isActive: boolean; 41 | toggle: () => void; 42 | } 43 | 44 | export default function Toggle({ id, isActive, toggle }: ToggleProps) { 45 | return ( 46 | 47 | 48 | On 49 | 50 | 51 | Off 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /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/components/vote/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { AutoColumn } from '../Column'; 3 | 4 | import uImage from '../../assets/images/big_kwikcorn.png'; 5 | import xlKwikcorn from '../../assets/images/xl_kwik.png'; 6 | import noise from '../../assets/images/noise.png'; 7 | 8 | export const TextBox = styled.div` 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 4px 12px; 13 | border: 1px solid rgba(255, 255, 255, 0.4); 14 | border-radius: 20px; 15 | width: fit-content; 16 | justify-self: flex-end; 17 | `; 18 | 19 | export const DataCard = styled(AutoColumn)<{ disabled?: boolean }>` 20 | background: radial-gradient(76.02% 75.41% at 1.84% 0%, #ff007a 0%, #2172e5 100%); 21 | border-radius: 12px; 22 | width: 100%; 23 | position: relative; 24 | overflow: hidden; 25 | `; 26 | 27 | export const CardBGImage = styled.span<{ desaturate?: boolean }>` 28 | background: url(${uImage}); 29 | width: 1000px; 30 | height: 600px; 31 | position: absolute; 32 | border-radius: 12px; 33 | opacity: 0.4; 34 | top: -100px; 35 | left: -100px; 36 | transform: rotate(-15deg); 37 | user-select: none; 38 | 39 | ${({ desaturate }) => desaturate && `filter: saturate(0)`} 40 | `; 41 | 42 | export const CardBGImageSmaller = styled.span<{ desaturate?: boolean }>` 43 | background: url(${xlKwikcorn}); 44 | width: 1200px; 45 | height: 1200px; 46 | position: absolute; 47 | border-radius: 12px; 48 | top: -300px; 49 | left: -300px; 50 | opacity: 0.4; 51 | user-select: none; 52 | 53 | ${({ desaturate }) => desaturate && `filter: saturate(0)`} 54 | `; 55 | 56 | export const CardNoise = styled.span` 57 | background: url(${noise}); 58 | background-size: cover; 59 | mix-blend-mode: overlay; 60 | border-radius: 12px; 61 | width: 100%; 62 | height: 100%; 63 | opacity: 0.15; 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | user-select: none; 68 | `; 69 | 70 | export const CardSection = styled(AutoColumn)<{ disabled?: boolean }>` 71 | padding: 1rem; 72 | z-index: 1; 73 | opacity: ${({ disabled }) => disabled && '0.4'}; 74 | `; 75 | 76 | export const Break = styled.div` 77 | width: 100%; 78 | background-color: rgba(255, 255, 255, 0.2); 79 | height: 1px; 80 | `; 81 | -------------------------------------------------------------------------------- /src/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/components/AccountDetails/Transaction.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { CheckCircle, Triangle } from 'react-feather'; 3 | 4 | import { useActiveWeb3React } from '../../hooks'; 5 | import { getEtherscanLink } from '../../utils'; 6 | import { ExternalLink } from '../../theme'; 7 | import { useAllTransactions } from '../../state/transactions/hooks'; 8 | import { RowFixed } from '../Row'; 9 | import Loader from '../Loader'; 10 | 11 | const TransactionWrapper = styled.div``; 12 | 13 | const TransactionStatusText = styled.div` 14 | margin-right: 0.5rem; 15 | display: flex; 16 | align-items: center; 17 | :hover { 18 | text-decoration: underline; 19 | } 20 | `; 21 | 22 | const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | text-decoration: none !important; 27 | border-radius: 0.5rem; 28 | padding: 0.25rem 0rem; 29 | font-weight: 500; 30 | font-size: 0.825rem; 31 | color: ${({ theme }) => theme.primary3}; 32 | `; 33 | 34 | const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` 35 | color: ${({ pending, success, theme }) => (pending ? theme.primary3 : success ? theme.green1 : theme.red1)}; 36 | `; 37 | 38 | export default function Transaction({ hash }: { hash: string }): JSX.Element { 39 | const { chainId } = useActiveWeb3React(); 40 | const allTransactions = useAllTransactions(); 41 | 42 | const tx = allTransactions?.[hash]; 43 | const summary = tx?.summary; 44 | const pending = !tx?.receipt; 45 | const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined'); 46 | 47 | if (!chainId) return <> ; 48 | 49 | return ( 50 | 51 | 52 | 53 | {summary ?? hash} ↗ 54 | 55 | 56 | {pending ? : success ? : } 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Popups/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useActivePopups } from '../../state/application/hooks'; 3 | import { AutoColumn } from '../Column'; 4 | import PopupItem from './PopupItem'; 5 | import { useURLWarningVisible } from '../../state/user/hooks'; 6 | 7 | const MobilePopupWrapper = styled.div<{ height: string | number }>` 8 | position: relative; 9 | max-width: 100%; 10 | height: ${({ height }) => height}; 11 | margin: ${({ height }) => (height ? '0 auto;' : 0)}; 12 | margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; 13 | 14 | display: none; 15 | ${({ theme }) => theme.mediaWidth.upToSmall` 16 | display: block; 17 | `}; 18 | `; 19 | 20 | const MobilePopupInner = styled.div` 21 | height: 99%; 22 | overflow-x: auto; 23 | overflow-y: hidden; 24 | display: flex; 25 | flex-direction: row; 26 | -webkit-overflow-scrolling: touch; 27 | ::-webkit-scrollbar { 28 | display: none; 29 | } 30 | `; 31 | 32 | const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean }>` 33 | position: fixed; 34 | top: ${({ extraPadding }) => (extraPadding ? '108px' : '88px')}; 35 | right: 1rem; 36 | max-width: 355px !important; 37 | width: 100%; 38 | z-index: 3; 39 | 40 | ${({ theme }) => theme.mediaWidth.upToSmall` 41 | display: none; 42 | `}; 43 | `; 44 | 45 | export default function Popups() { 46 | // get all popups 47 | const activePopups = useActivePopups(); 48 | 49 | const urlWarningActive = useURLWarningVisible(); 50 | 51 | return ( 52 | <> 53 | 54 | {activePopups.map((item) => ( 55 | 56 | ))} 57 | 58 | 0 ? 'fit-content' : 0}> 59 | 60 | {activePopups // reverse so new items up front 61 | .slice(0) 62 | .reverse() 63 | .map((item) => ( 64 | 65 | ))} 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/SearchModal/CurrencySearchModal.tsx: -------------------------------------------------------------------------------- 1 | import { Currency } from '@intercroneswap/swap-sdk'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import ReactGA from 'react-ga'; 4 | import useLast from '../../hooks/useLast'; 5 | import Modal from '../Modal'; 6 | import { CurrencySearch } from './CurrencySearch'; 7 | import { ListSelect } from './ListSelect'; 8 | 9 | interface CurrencySearchModalProps { 10 | isOpen: boolean; 11 | onDismiss: () => void; 12 | selectedCurrency?: Currency | null; 13 | onCurrencySelect: (currency: Currency) => void; 14 | otherSelectedCurrency?: Currency | null; 15 | showCommonBases?: boolean; 16 | } 17 | 18 | export default function CurrencySearchModal({ 19 | isOpen, 20 | onDismiss, 21 | onCurrencySelect, 22 | selectedCurrency, 23 | otherSelectedCurrency, 24 | showCommonBases = false, 25 | }: CurrencySearchModalProps) { 26 | const [listView, setListView] = useState(false); 27 | const lastOpen = useLast(isOpen); 28 | 29 | useEffect(() => { 30 | if (isOpen && !lastOpen) { 31 | setListView(false); 32 | } 33 | }, [isOpen, lastOpen]); 34 | 35 | const handleCurrencySelect = useCallback( 36 | (currency: Currency) => { 37 | onCurrencySelect(currency); 38 | onDismiss(); 39 | }, 40 | [onDismiss, onCurrencySelect], 41 | ); 42 | 43 | const handleClickChangeList = useCallback(() => { 44 | ReactGA.event({ 45 | category: 'Lists', 46 | action: 'Change Lists', 47 | }); 48 | setListView(true); 49 | }, []); 50 | const handleClickBack = useCallback(() => { 51 | ReactGA.event({ 52 | category: 'Lists', 53 | action: 'Back', 54 | }); 55 | setListView(false); 56 | }, []); 57 | 58 | return ( 59 | 60 | {listView ? ( 61 | 62 | ) : ( 63 | 72 | )} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/QuestionHelper/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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/ModalViews/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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 Tronscan 67 | 68 | )} 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/swap/AdvancedPriceDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Trade } from '@intercroneswap/swap-sdk'; 2 | import { useContext } from 'react'; 3 | import { ThemeContext } from 'styled-components'; 4 | import { Divider, TYPE } from '../../theme'; 5 | import { AutoColumn } from '../Column'; 6 | import Row, { RowBetween } from '../Row'; 7 | 8 | export default function AdvancedPriceDetails({ trade }: { trade?: Trade }) { 9 | const theme = useContext(ThemeContext); 10 | const price = trade?.executionPrice; 11 | const formattedPrice = price?.toSignificant(6); 12 | const invertedFormattedPrice = price?.invert()?.toSignificant(6); 13 | return ( 14 | 18 | {Boolean(trade) && ( 19 | <> 20 | 21 | 22 | Price 23 | 24 | 25 | 26 | 27 | 28 | {formattedPrice} 29 | 30 |   31 | 32 | {price?.quoteCurrency?.symbol} 33 | 34 |   35 | 36 | per 37 | 38 |   39 | 40 | {price?.baseCurrency?.symbol} 41 | 42 | 43 | 44 | 45 | {invertedFormattedPrice} 46 | 47 |   48 | 49 | {price?.baseCurrency?.symbol} 50 | 51 |   52 | 53 | per 54 | 55 |   56 | 57 | {price?.quoteCurrency?.symbol} 58 | 59 | 60 | 61 | )} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /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/components/PositionCard/V.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; 3 | import { Token, TokenAmount, WETH } from '@intercroneswap/swap-sdk'; 4 | 5 | import { Text } from 'rebass'; 6 | import { AutoColumn } from '../Column'; 7 | import { ButtonSecondary } from '../Button'; 8 | import { RowBetween, RowFixed } from '../Row'; 9 | import { FixedHeightRow, HoverCard } from './index'; 10 | import DoubleCurrencyLogo from '../DoubleLogo'; 11 | import { useActiveWeb3React } from '../../hooks'; 12 | import { ThemeContext } from 'styled-components'; 13 | 14 | interface PositionCardProps extends RouteComponentProps<{}> { 15 | token: Token; 16 | VLiquidityBalance: TokenAmount; 17 | } 18 | 19 | function VPositionCard({ token, VLiquidityBalance }: PositionCardProps) { 20 | const theme = useContext(ThemeContext); 21 | 22 | const { chainId } = useActiveWeb3React(); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | {`${chainId && token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/TRX`} 32 | 33 | 43 | V 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Migrate 52 | 53 | 54 | 60 | Remove 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | 69 | export default withRouter(VPositionCard); 70 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 28 | 29 | 30 | 32 | 35 | ISwap Interface 36 | 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/swap/BetterTradeLink.tsx: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs'; 2 | import { useContext, useMemo } from 'react'; 3 | import { useLocation } from 'react-router'; 4 | import { Text } from 'rebass'; 5 | import { ThemeContext } from 'styled-components'; 6 | import useParsedQueryString from '../../hooks/useParsedQueryString'; 7 | import useToggledVersion, { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'; 8 | 9 | import { StyledInternalLink } from '../../theme'; 10 | import { YellowCard } from '../Card'; 11 | import { AutoColumn } from '../Column'; 12 | 13 | function VersionLinkContainer({ children }: { children: React.ReactNode }) { 14 | const theme = useContext(ThemeContext); 15 | 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default function BetterTradeLink({}: // version 28 | { 29 | version: Version; 30 | }) { 31 | // const location = useLocation() 32 | // const search = useParsedQueryString() 33 | 34 | // const linkDestination = useMemo(() => { 35 | // return { 36 | // ...location, 37 | // search: `?${stringify({ 38 | // ...search, 39 | // use: version !== DEFAULT_VERSION ? version : undefined 40 | // })}` 41 | // } 42 | // }, [location, search, version]) 43 | 44 | return null; 45 | // 46 | // There is a better price for this trade on{' '} 47 | // 48 | // ISwap {version.toUpperCase()} ↗ 49 | // 50 | // 51 | } 52 | 53 | export function DefaultVersionLink() { 54 | const location = useLocation(); 55 | const search = useParsedQueryString(); 56 | const version = useToggledVersion(); 57 | 58 | const linkDestination = useMemo(() => { 59 | return { 60 | ...location, 61 | search: `?${stringify({ 62 | ...search, 63 | use: DEFAULT_VERSION, 64 | })}`, 65 | }; 66 | }, [location, search]); 67 | 68 | return ( 69 | 70 | Showing {version.toUpperCase()} price.{' '} 71 | 72 | Switch to ISwap {DEFAULT_VERSION.toUpperCase()} ↗ 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createWeb3ReactRoot, Web3ReactProvider } from '@web3-react/core'; 2 | import 'inter-ui'; 3 | import { StrictMode } from 'react'; 4 | import { isMobile } from 'react-device-detect'; 5 | import ReactDOM from 'react-dom'; 6 | import ReactGA from 'react-ga'; 7 | import { Provider } from 'react-redux'; 8 | import { HashRouter } from 'react-router-dom'; 9 | import { NetworkContextName } from './constants'; 10 | import './i18n'; 11 | import App from './pages/App'; 12 | import store from './state'; 13 | import ApplicationUpdater from './state/application/updater'; 14 | import ListsUpdater from './state/lists/updater'; 15 | import MulticallUpdater from './state/multicall/updater'; 16 | import TransactionUpdater from './state/transactions/updater'; 17 | import UserUpdater from './state/user/updater'; 18 | import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'; 19 | import getLibrary from './utils/getLibrary'; 20 | 21 | const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName); 22 | 23 | if ('ethereum' in window) { 24 | (window.ethereum as any).autoRefreshOnNetworkChange = false; 25 | } 26 | 27 | const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID; 28 | if (typeof GOOGLE_ANALYTICS_ID === 'string') { 29 | ReactGA.initialize(GOOGLE_ANALYTICS_ID); 30 | ReactGA.set({ 31 | customBrowserType: !isMobile 32 | ? 'desktop' 33 | : 'web3' in window || 'ethereum' in window 34 | ? 'mobileWeb3' 35 | : 'mobileRegular', 36 | }); 37 | } else { 38 | ReactGA.initialize('test', { testMode: true, debug: true }); 39 | } 40 | 41 | window.addEventListener('error', (error) => { 42 | ReactGA.exception({ 43 | description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`, 44 | fatal: true, 45 | }); 46 | }); 47 | 48 | function Updaters() { 49 | return ( 50 | <> 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | ReactDOM.render( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | , 77 | document.getElementById('root'), 78 | ); 79 | -------------------------------------------------------------------------------- /cypress/integration/add-liquidity.test.ts: -------------------------------------------------------------------------------- 1 | describe('Add Liquidity', () => { 2 | it('loads the two correct tokens', () => { 3 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab') 4 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR') 5 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH') 6 | }) 7 | 8 | it('does not crash if ETH is duplicated', () => { 9 | cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') 10 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH') 11 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH') 12 | }) 13 | 14 | it('token not in storage is loaded', () => { 15 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 16 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL') 17 | cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR') 18 | }) 19 | 20 | it('single token can be selected', () => { 21 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d') 22 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL') 23 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 24 | cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR') 25 | }) 26 | 27 | it('redirects /add/token-token to add/token/token', () => { 28 | cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 29 | cy.url().should( 30 | 'contain', 31 | '/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85' 32 | ) 33 | }) 34 | 35 | it('redirects /add/WETH-token to /add/WETH-address/token', () => { 36 | cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') 37 | cy.url().should( 38 | 'contain', 39 | '/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85' 40 | ) 41 | }) 42 | 43 | it('redirects /add/token-WETH to /add/token/WETH-address', () => { 44 | cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab') 45 | cy.url().should( 46 | 'contain', 47 | '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab' 48 | ) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/components/SearchModal/CommonBases.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'rebass'; 2 | import { ChainId, Currency, currencyEquals, ETHER, Token } from '@intercroneswap/swap-sdk'; 3 | import styled from 'styled-components'; 4 | 5 | import { SUGGESTED_BASES } from '../../constants'; 6 | import { AutoColumn } from '../Column'; 7 | import QuestionHelper from '../QuestionHelper'; 8 | import { AutoRow } from '../Row'; 9 | import CurrencyLogo from '../CurrencyLogo'; 10 | 11 | const BaseWrapper = styled.div<{ disable?: boolean }>` 12 | border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)}; 13 | border-radius: 10px; 14 | display: flex; 15 | padding: 6px; 16 | 17 | align-items: center; 18 | :hover { 19 | cursor: ${({ disable }) => !disable && 'pointer'}; 20 | background-color: ${({ theme, disable }) => !disable && theme.bg2}; 21 | } 22 | 23 | background-color: ${({ theme, disable }) => disable && theme.bg3}; 24 | opacity: ${({ disable }) => disable && '0.4'}; 25 | `; 26 | 27 | export default function CommonBases({ 28 | chainId, 29 | onSelect, 30 | selectedCurrency, 31 | }: { 32 | chainId?: ChainId; 33 | selectedCurrency?: Currency | null; 34 | onSelect: (currency: Currency) => void; 35 | }) { 36 | return ( 37 | 38 | 39 | 40 | Common bases 41 | 42 | 43 | 44 | 45 | { 47 | if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) { 48 | onSelect(ETHER); 49 | } 50 | }} 51 | disable={selectedCurrency === ETHER} 52 | > 53 | 54 | 55 | TRX 56 | 57 | 58 | {(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => { 59 | const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address; 60 | return ( 61 | !selected && onSelect(token)} disable={selected} key={token.address}> 62 | 63 | 64 | {token.symbol} 65 | 66 | 67 | ); 68 | })} 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Web3ReactManager/index.tsx: -------------------------------------------------------------------------------- 1 | import { 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 | console.error(networkActive, networkError, 'network error'); 57 | 58 | // if the account context isn't active, and there's an error on the network context, it's an irrecoverable error 59 | if (!active && networkError) { 60 | return ( 61 | 62 | {t('unknownError')} 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/ProgressSteps/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { RowBetween } from '../Row'; 3 | import { AutoColumn } from '../Column'; 4 | import { transparentize } from 'polished'; 5 | 6 | const Wrapper = styled(AutoColumn)``; 7 | 8 | const Grouping = styled(RowBetween)` 9 | width: 50%; 10 | `; 11 | 12 | const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>` 13 | min-width: 20px; 14 | min-height: 20px; 15 | background-color: ${({ theme, confirmed, disabled }) => 16 | disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary3}; 17 | border-radius: 50%; 18 | color: ${({ theme }) => theme.white}; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | line-height: 8px; 23 | font-size: 12px; 24 | `; 25 | 26 | const CircleRow = styled.div` 27 | width: calc(100% - 20px); 28 | display: flex; 29 | align-items: center; 30 | `; 31 | 32 | const Connector = styled.div<{ prevConfirmed?: boolean; disabled?: boolean }>` 33 | width: 100%; 34 | height: 2px; 35 | background-color: ; 36 | background: linear-gradient( 37 | 90deg, 38 | ${({ theme, prevConfirmed, disabled }) => 39 | disabled ? theme.bg4 : transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary3)} 40 | 0%, 41 | ${({ theme, prevConfirmed, disabled }) => (disabled ? theme.bg4 : prevConfirmed ? theme.primary3 : theme.bg4)} 80% 42 | ); 43 | opacity: 0.6; 44 | `; 45 | 46 | interface ProgressCirclesProps { 47 | steps: boolean[]; 48 | disabled?: boolean; 49 | } 50 | 51 | /** 52 | * Based on array of steps, create a step counter of circles. 53 | * A circle can be enabled, disabled, or confirmed. States are derived 54 | * from previous step. 55 | * 56 | * An extra circle is added to represent the ability to swap, add, or remove. 57 | * This step will never be marked as complete (because no 'txn done' state in body ui). 58 | * 59 | * @param steps array of booleans where true means step is complete 60 | */ 61 | export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) { 62 | return ( 63 | 64 | 65 | {steps.map((step, i) => { 66 | return ( 67 | 68 | 69 | {step ? '✓' : i + 1} 70 | 71 | 72 | 73 | ); 74 | })} 75 | {steps.length + 1} 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/getTokenList.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@loveswap7/token-lists'; 2 | import schema from '@loveswap7/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: any) => { 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 as any; 67 | } 68 | throw new Error('Unrecognized list URL protocol.'); 69 | } 70 | -------------------------------------------------------------------------------- /cypress/integration/swap.test.ts: -------------------------------------------------------------------------------- 1 | describe('Swap', () => { 2 | beforeEach(() => { 3 | cy.visit('/swap') 4 | }) 5 | it('can enter an amount into input', () => { 6 | cy.get('#swap-currency-input .token-amount-input') 7 | .type('0.001', { delay: 200 }) 8 | .should('have.value', '0.001') 9 | }) 10 | 11 | it('zero swap amount', () => { 12 | cy.get('#swap-currency-input .token-amount-input') 13 | .type('0.0', { delay: 200 }) 14 | .should('have.value', '0.0') 15 | }) 16 | 17 | it('invalid swap amount', () => { 18 | cy.get('#swap-currency-input .token-amount-input') 19 | .type('\\', { delay: 200 }) 20 | .should('have.value', '') 21 | }) 22 | 23 | it('can enter an amount into output', () => { 24 | cy.get('#swap-currency-output .token-amount-input') 25 | .type('0.001', { delay: 200 }) 26 | .should('have.value', '0.001') 27 | }) 28 | 29 | it('zero output amount', () => { 30 | cy.get('#swap-currency-output .token-amount-input') 31 | .type('0.0', { delay: 200 }) 32 | .should('have.value', '0.0') 33 | }) 34 | 35 | it('can swap ETH for DAI', () => { 36 | cy.get('#swap-currency-output .open-currency-select-button').click() 37 | cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible') 38 | cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true }) 39 | cy.get('#swap-currency-input .token-amount-input').should('be.visible') 40 | cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 }) 41 | cy.get('#swap-currency-output .token-amount-input').should('not.equal', '') 42 | cy.get('#swap-button').click() 43 | cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap') 44 | }) 45 | 46 | it('add a recipient does not exist unless in expert mode', () => { 47 | cy.get('#add-recipient-button').should('not.exist') 48 | }) 49 | 50 | describe('expert mode', () => { 51 | beforeEach(() => { 52 | cy.window().then(win => { 53 | cy.stub(win, 'prompt').returns('confirm') 54 | }) 55 | cy.get('#open-settings-dialog-button').click() 56 | cy.get('#toggle-expert-mode-button').click() 57 | cy.get('#confirm-expert-mode').click() 58 | }) 59 | 60 | it('add a recipient is visible', () => { 61 | cy.get('#add-recipient-button').should('be.visible') 62 | }) 63 | 64 | it('add a recipient', () => { 65 | cy.get('#add-recipient-button').click() 66 | cy.get('#recipient').should('exist') 67 | }) 68 | 69 | it('remove recipient', () => { 70 | cy.get('#add-recipient-button').click() 71 | cy.get('#remove-recipient-button').click() 72 | cy.get('#recipient').should('not.exist') 73 | }) 74 | }) 75 | }) 76 | --------------------------------------------------------------------------------