diff --git a/.env.example b/.env.example index 0edb07504..f9a207acb 100644 --- a/.env.example +++ b/.env.example @@ -70,4 +70,13 @@ HARDHAT_INFURA_ID= REACT_APP_GENERAL_MAINTENANCE_MESSAGE="Message to display in top banner." # Publish to Storybook manually by adding the frontend-v2 chromatic project id -CHROMATIC_PROJECT_TOKEN = +CHROMATIC_PROJECT_TOKEN= + +# Disable bridge send transactions in UI and show an error when this is set to `true`. +REACT_APP_BRIDGE_DISABLED= + +# Disable and mock the serverless API. Note: for testing purposes +REACT_APP_MOCK_SERVERLESS= + +# JSON string with format: {[chainId]: numberInMinutes}, eg: { "1": 2, "10": 2, "137": 2, "288": 2, "42161": 2 } +REACT_APP_DEPOSIT_DELAY= diff --git a/.env.test b/.env.test new file mode 100644 index 000000000..a560452d5 --- /dev/null +++ b/.env.test @@ -0,0 +1,2 @@ +REACT_APP_V_ETH=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 +REACT_APP_UMA_ADDRESS=0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828 \ No newline at end of file diff --git a/.github/workflows/chromatic.yaml b/.github/workflows/chromatic.yaml index c27287483..5a0e83288 100644 --- a/.github/workflows/chromatic.yaml +++ b/.github/workflows/chromatic.yaml @@ -4,27 +4,44 @@ name: Chromatic on: # manual trigger workflow_dispatch: - push: - branches: - - master pull_request: - branches: - - master # List of jobs jobs: chromatic-deployment: + if: ${{ contains( github.event.pull_request.labels.*.name, 'storybook') }} # Operating System runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v1 - - name: Install dependencies - run: yarn - # 👇 Adds Chromatic as a step in the workflow + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: https://registry.npmjs.org + cache: yarn + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: node_modules/ + key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }} + + - if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --ignore-scripts + shell: bash - name: Publish to Chromatic uses: chromaui/action@v1 + env: + REACT_APP_PUBLIC_INFURA_ID: ${{ secrets.CYPRESS_PUBLIC_INFURA_ID }} + REACT_APP_PUBLIC_ONBOARD_API_KEY: ${{ secrets.CYPRESS_PUBLIC_ONBOARD_API_KEY }} + REACT_APP_REWARDS_API_URL: ${{ secrets.CYPRESS_REWARDS_API_URL }} + REACT_APP_CHAIN_137_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_137_PROVIDER_URL }} + REACT_APP_CHAIN_42161_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_42161_PROVIDER_URL }} # Chromatic GitHub Action options with: # 👇 Chromatic projectToken, refer to the manage page to obtain it. projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + exitZeroOnChanges: true diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index e4f791595..da8ee9c20 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -23,7 +23,15 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup - - run: yarn test + - name: Unit tests + env: + REACT_APP_PUBLIC_INFURA_ID: ${{ secrets.CYPRESS_PUBLIC_INFURA_ID }} + REACT_APP_PUBLIC_ONBOARD_API_KEY: ${{ secrets.CYPRESS_PUBLIC_ONBOARD_API_KEY }} + REACT_APP_REWARDS_API_URL: ${{ secrets.CYPRESS_REWARDS_API_URL }} + REACT_APP_CHAIN_137_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_137_PROVIDER_URL }} + REACT_APP_CHAIN_42161_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_42161_PROVIDER_URL }} + REACT_APP_MOCK_SERVERLESS: true + run: yarn test cypress-tests: runs-on: ubuntu-latest @@ -56,6 +64,7 @@ jobs: REACT_APP_REWARDS_API_URL: ${{ secrets.CYPRESS_REWARDS_API_URL }} REACT_APP_CHAIN_137_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_137_PROVIDER_URL }} REACT_APP_CHAIN_42161_PROVIDER_URL: ${{ secrets.CYPRESS_CHAIN_42161_PROVIDER_URL }} + REACT_APP_MOCK_SERVERLESS: true run: yarn build - name: Cypress run Chrome uses: cypress-io/github-action@v4 diff --git a/.storybook/main.js b/.storybook/main.js index da3f14f4c..276f7663a 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -5,9 +5,25 @@ module.exports = { "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app", + "storybook-addon-react-router-v6", ], framework: "@storybook/react", core: { builder: "webpack4", }, + // Note: by default, storybook only forwards environment variables that + // take the form /^STORYBOOK_/ . The code below creates a 1:1 mapping + // of the /^REACT_APP_/ environment variables so that this Storybook + // instance can run. + // + // This clears an error in which storybook cannot publish to Chromatic + // + // Relevant Storybook Docs: https://storybook.js.org/docs/react/configure/environment-variables + env: (config) => ({ + ...config, + ...Object.keys(process.env).reduce((accumulator, envKey) => { + if (/^REACT_APP_/.test(envKey)) accumulator[envKey] = process.env[envKey]; + return accumulator; + }, {}), + }), }; diff --git a/.storybook/preview.js b/.storybook/preview.js index 4183b8663..912a6d446 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,29 @@ import { Buffer } from "buffer"; +import { addDecorator } from "@storybook/react"; +import { default as GlobalStyles } from "components/GlobalStyles/GlobalStyles"; +import { OnboardContext, useOnboardManager } from "hooks/useOnboard"; + +import { MemoryRouter } from "react-router"; +addDecorator((story) => ( + {story()} +)); + window.Buffer = Buffer; +addDecorator((s) => ( + <> + + {s()} + +)); + +const OnboardDecorator = (storyFn) => { + const value = useOnboardManager(); + return ( + {storyFn()} + ); +}; +addDecorator(OnboardDecorator); export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { diff --git a/api/_utils.ts b/api/_utils.ts index bfb31299d..f0142e6f7 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -23,6 +23,7 @@ const { REACT_APP_GOOGLE_SERVICE_ACCOUNT, VERCEL_ENV, GAS_MARKUP, + DISABLE_DEBUG_LOGS, } = process.env; const GOOGLE_SERVICE_ACCOUNT = REACT_APP_GOOGLE_SERVICE_ACCOUNT @@ -44,6 +45,10 @@ export const log = ( severity: "DEBUG" | "INFO" | "WARN" | "ERROR", data: LogType ) => { + if (DISABLE_DEBUG_LOGS === "true" && severity === "DEBUG") { + console.log(data); + return; + } let message = JSON.stringify(data, null, 4); // Fire and forget. we don't wait for this to finish. gcpLogger @@ -127,13 +132,6 @@ export const getTokenDetails = async ( "0xc186fA914353c44b2E33eBE05f21846F1048bEda", provider ); - getLogger().debug({ - at: "getTokenDetails", - message: "Fetching token details", - l1Token, - l2Token, - chainId, - }); // 2 queries: treating the token as the l1Token or treating the token as the L2 token. const l2TokenFilter = hubPool.filters.SetPoolRebalanceRoute( @@ -158,11 +156,6 @@ export const getTokenDetails = async ( }); const event = events[0]; - getLogger().debug({ - at: "getTokenDetails", - message: "Fetched pool rebalance route event", - event, - }); return { hubPool, @@ -181,11 +174,6 @@ export class InputError extends Error {} */ export const infuraProvider = (name: string) => { const url = `https://${name}.infura.io/v3/${REACT_APP_PUBLIC_INFURA_ID}`; - getLogger().info({ - at: "infuraProvider", - message: "Using an Infura provider", - url, - }); return new ethers.providers.StaticJsonRpcProvider(url); }; @@ -196,6 +184,21 @@ export const infuraProvider = (name: string) => { export const bobaProvider = (): providers.StaticJsonRpcProvider => new ethers.providers.StaticJsonRpcProvider("https://mainnet.boba.network"); +/** + * Resolves a fixed Static RPC provider if an override url has been specified. + * @returns A provider or undefined if an override was not specified. + */ +export const overrideProvider = ( + chainId: string +): providers.StaticJsonRpcProvider | undefined => { + const url = process.env[`OVERRIDE_PROVIDER_${chainId}`]; + if (url) { + return new ethers.providers.StaticJsonRpcProvider(url); + } else { + return undefined; + } +}; + /** * Generates a fixed HubPoolClientConfig object * @returns A fixed constant @@ -313,11 +316,6 @@ export const getRelayerFeeCalculator = (destinationChainId: number) => { queries: queryFn(), capitalCostsConfig: relayerFeeCapitalCostConfig, }; - getLogger().info({ - at: "getRelayerFeeDetails", - message: "Relayer fee calculator config", - relayerFeeCalculatorConfig, - }); return new sdk.relayFeeCalculator.RelayFeeCalculator( relayerFeeCalculatorConfig, logger @@ -364,15 +362,14 @@ export const getRelayerFeeDetails = ( * @param l1Token The ERC20 token address of the coin to find the cached price of * @returns The price of the `l1Token` token. */ -export const getCachedTokenPrice = async (l1Token: string): Promise => { - getLogger().debug({ - at: "getCachedTokenPrice", - message: `Resolving price from ${resolveVercelEndpoint()}/api/coingecko`, - }); +export const getCachedTokenPrice = async ( + l1Token: string, + baseCurrency: string = "eth" +): Promise => { return Number( ( await axios(`${resolveVercelEndpoint()}/api/coingecko`, { - params: { l1Token }, + params: { l1Token, baseCurrency }, }) ).data.price ); @@ -388,24 +385,29 @@ export const providerCache: Record = {}; export const getProvider = (_chainId: number): providers.Provider => { const chainId = _chainId.toString(); if (!providerCache[chainId]) { - switch (chainId.toString()) { - case "1": - providerCache[chainId] = infuraProvider("mainnet"); - break; - case "10": - providerCache[chainId] = infuraProvider("optimism-mainnet"); - break; - case "137": - providerCache[chainId] = infuraProvider("polygon-mainnet"); - break; - case "288": - providerCache[chainId] = bobaProvider(); - break; - case "42161": - providerCache[chainId] = infuraProvider("arbitrum-mainnet"); - break; - default: - throw new Error(`Invalid chainId provided: ${chainId}`); + const override = overrideProvider(chainId); + if (override) { + providerCache[chainId] = override; + } else { + switch (chainId.toString()) { + case "1": + providerCache[chainId] = infuraProvider("mainnet"); + break; + case "10": + providerCache[chainId] = infuraProvider("optimism-mainnet"); + break; + case "137": + providerCache[chainId] = infuraProvider("polygon-mainnet"); + break; + case "288": + providerCache[chainId] = bobaProvider(); + break; + case "42161": + providerCache[chainId] = infuraProvider("arbitrum-mainnet"); + break; + default: + throw new Error(`Invalid chainId provided: ${chainId}`); + } } } return providerCache[chainId]; diff --git a/api/limits.ts b/api/limits.ts index 7d827ade6..0448e231a 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -52,12 +52,6 @@ const handler = async ( return ethers.utils.getAddress(relayer); } ); - logger.debug({ - at: "limits", - message: "Using relayers", - fullRelayers, - transferRestrictedRelayers, - }); if (!isString(token) || !isString(destinationChainId)) throw new InputError( "Must provide token and destinationChainId as query params" @@ -80,12 +74,6 @@ const handler = async ( getTokenDetails(provider, l1Token, undefined, destinationChainId), isRouteEnabled(computedOriginChainId, Number(destinationChainId), token), ]); - logger.debug({ - at: "limits", - message: "Checked enabled routes", - isRouteEnabled, - }); - // If any of the above fails or the route is not enabled, we assume that the if ( disabledL1Tokens.includes(l1Token.toLowerCase()) || @@ -119,17 +107,6 @@ const handler = async ( ]; let tokenPrice = await getCachedTokenPrice(l1Token); - logger.debug({ - at: "limits", - message: "Got token price from /coingecko", - tokenPrice, - }); - logger.debug({ - at: "limits", - message: - "Sending several requests to HubPool and fetching relayer balances", - multicallInput, - }); const [ relayerFeeDetails, @@ -173,13 +150,6 @@ const handler = async ( ) ), ]); - logger.debug({ - at: "limits", - message: "Fetched balances", - fullRelayerBalances, - transferRestrictedBalances, - fullRelayerMainnetBalances, - }); let { liquidReserves } = hubPool.interface.decodeFunctionResult( "pooledTokens", @@ -194,11 +164,6 @@ const handler = async ( liquidReserves = liquidReserves.sub( ethers.utils.parseEther(REACT_APP_WETH_LP_CUSHION || "0") ); - logger.debug({ - at: "limits", - message: "Adding WETH cushioning to LP liquidity", - liquidReserves, - }); } else if ( ethers.utils.getAddress(l1Token) === ethers.utils.getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") @@ -207,11 +172,6 @@ const handler = async ( liquidReserves = liquidReserves.sub( ethers.utils.parseUnits(REACT_APP_USDC_LP_CUSHION || "0", 6) ); - logger.debug({ - at: "limits", - message: "Adding USDC cushioning to LP liquidity", - liquidReserves, - }); } else if ( ethers.utils.getAddress(l1Token) === ethers.utils.getAddress("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") @@ -220,11 +180,6 @@ const handler = async ( liquidReserves = liquidReserves.sub( ethers.utils.parseUnits(REACT_APP_WBTC_LP_CUSHION || "0", 8) ); - logger.debug({ - at: "limits", - message: "Adding WBTC cushioning to LP liquidity", - liquidReserves, - }); } else if ( ethers.utils.getAddress(l1Token) === ethers.utils.getAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F") @@ -233,11 +188,6 @@ const handler = async ( liquidReserves = liquidReserves.sub( ethers.utils.parseUnits(REACT_APP_DAI_LP_CUSHION || "0", 18) ); - logger.debug({ - at: "limits", - message: "Adding DAI cushioning to LP liquidity", - liquidReserves, - }); } if (liquidReserves.lt(0)) liquidReserves = ethers.BigNumber.from(0); @@ -245,7 +195,6 @@ const handler = async ( const maxGasFee = ethers.utils .parseEther(maxRelayFeePct.toString()) .sub(relayerFeeDetails.capitalFeePercent); - logger.debug({ at: "limits", message: "Computed maxGasFee", maxGasFee }); const transferBalances = fullRelayerBalances.map((balance, i) => balance.add(fullRelayerMainnetBalances[i]) diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index 15bd0cdda..9942eeb22 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -72,13 +72,6 @@ const handler = async ( originChainId ); - logger.debug({ - at: "suggested-fees", - message: "Checking route", - computedOriginChainId, - destinationChainId, - token, - }); const blockFinder = new BlockFinder(provider.getBlock.bind(provider)); const [{ number: latestBlock }, routeEnabled] = await Promise.all([ blockFinder.getBlockForTimestamp(parsedTimestamp), @@ -91,8 +84,6 @@ const handler = async ( // to be "latest" const blockTag = isString(timestamp) ? latestBlock : BLOCK_TAG_LAG; - logger.debug({ at: "suggested-fees", message: `Using block ${blockTag}` }); - if (!routeEnabled || disabledL1Tokens.includes(l1Token.toLowerCase())) throw new InputError( `Route from chainId ${computedOriginChainId} to chainId ${destinationChainId} with origin token address ${token} is not enabled.` @@ -103,6 +94,8 @@ const handler = async ( provider ); + const baseCurrency = destinationChainId === "137" ? "matic" : "eth"; + const [currentUt, nextUt, rateModel, tokenPrice] = await Promise.all([ hubPool.callStatic.liquidityUtilizationCurrent(l1Token, { blockTag, @@ -113,43 +106,19 @@ const handler = async ( configStoreClient.getRateModel(l1Token, { blockTag, }), - getCachedTokenPrice(l1Token), + getCachedTokenPrice(l1Token, baseCurrency), ]); - logger.debug({ - at: "suggested-fees", - message: "Fetched utilization", - currentUt, - nextUt, - rateModel, - }); - const realizedLPFeePct = sdk.lpFeeCalculator.calculateRealizedLpFeePct( rateModel, currentUt, nextUt ); - logger.debug({ - at: "suggested-fees", - message: "Calculated realizedLPFeePct", - realizedLPFeePct, - }); - logger.debug({ - at: "suggested-fees", - message: "Got token price from /coingecko", - tokenPrice, - }); - const relayerFeeDetails = await getRelayerFeeDetails( l1Token, amount, Number(destinationChainId), tokenPrice ); - logger.debug({ - at: "suggested-fees", - message: "Calculated relayerFeeDetails", - relayerFeeDetails, - }); const skipAmountLimitEnabled = skipAmountLimit === "true"; @@ -158,8 +127,11 @@ const handler = async ( const responseJson = { capitalFeePct: relayerFeeDetails.capitalFeePercent, + capitalFeeTotal: relayerFeeDetails.capitalFeeTotal, relayGasFeePct: relayerFeeDetails.gasFeePercent, + relayGasFeeTotal: relayerFeeDetails.gasFeeTotal, relayFeePct: relayerFeeDetails.relayFeePercent, + relayFeeTotal: relayerFeeDetails.relayFeeTotal, lpFeePct: realizedLPFeePct.toString(), timestamp: parsedTimestamp.toString(), isAmountTooLow: relayerFeeDetails.isAmountTooLow, diff --git a/cypress/e2e/bridge.cy.ts b/cypress/e2e/bridge.cy.ts index 658ecd304..a4a1e7e94 100644 --- a/cypress/e2e/bridge.cy.ts +++ b/cypress/e2e/bridge.cy.ts @@ -11,7 +11,8 @@ describe("bridge", () => { }); it("render fees box on input", () => { - cy.wait(7000); + cy.dataCy("select-from-chain").click(); + cy.dataCy("from-chain-1").click(); cy.dataCy("bridge-amount-input").click().type("1"); cy.dataCy("send").should("be.disabled"); diff --git a/jest.config.js b/jest.config.js index 21a1e973a..d925b50e6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ + module.exports = { preset: "ts-jest", testEnvironment: "node", diff --git a/package.json b/package.json index d231055d9..82b0ec73c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "dependencies": { "@across-protocol/across-token": "^0.0.1", "@across-protocol/contracts-v2": "^0.0.34", - "@across-protocol/sdk-v2": "^0.1.24", + "@across-protocol/sdk-v2": "^0.1.26", "@datapunt/matomo-tracker-js": "^0.5.1", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", @@ -18,7 +18,7 @@ "@reach/dialog": "^0.16.2", "@reduxjs/toolkit": "^1.6.2", "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.1.0", + "@testing-library/react": "12.1.5", "@testing-library/user-event": "^12.1.10", "@types/jest": "^27.0.0", "@types/lodash-es": "^4.17.5", @@ -75,7 +75,7 @@ "cypress:run": "cypress run --e2e", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public", - "chromatic": "chromatic" + "chromatic": "chromatic --exit-zero-on-changes" }, "lint-staged": { "*.{jsx,tsx,js,ts}": "yarn lint:fix" @@ -95,16 +95,17 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.18.6", "@ethersproject/experimental": "^5.6.3", - "@storybook/addon-actions": "^6.5.10", - "@storybook/addon-essentials": "^6.5.10", - "@storybook/addon-interactions": "^6.5.10", - "@storybook/addon-links": "^6.5.10", - "@storybook/builder-webpack4": "^6.5.10", - "@storybook/manager-webpack4": "^6.5.10", - "@storybook/node-logger": "^6.5.10", + "@storybook/addon-actions": "^6.5.12", + "@storybook/addon-essentials": "^6.5.12", + "@storybook/addon-interactions": "^6.5.12", + "@storybook/addon-links": "^6.5.12", + "@storybook/builder-webpack4": "^6.5.12", + "@storybook/manager-webpack4": "^6.5.12", + "@storybook/node-logger": "^6.5.12", "@storybook/preset-create-react-app": "^3.2.0", - "@storybook/react": "^6.5.10", + "@storybook/react": "^6.5.12", "@storybook/testing-library": "^0.0.13", + "@testing-library/react-hooks": "^8.0.1", "@types/luxon": "^2.3.0", "@types/react-slider": "^1.3.1", "@vercel/node": "^2.5.13", @@ -117,6 +118,7 @@ "cypress": "^10.4.0", "eslint-plugin-prettier": "^4.2.1", "husky": "^8.0.0", + "jest-environment-hardhat": "^1.1.8", "lint-staged": "^12.4.1", "prettier": "^2.4.1", "serve": "^14.0.1", diff --git a/src/Routes.tsx b/src/Routes.tsx index 30e92c061..11c8ab0a2 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -1,19 +1,7 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, lazy, Suspense } from "react"; import { Switch, Route, useLocation, useHistory } from "react-router-dom"; - -import { - Send, - Pool, - About, - MyTransactions, - AllTransactions, - Rewards, - Staking, - Claim, - NotFound, -} from "views"; import { Header, SuperHeader, Banner, Sidebar } from "components"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useError } from "hooks"; import styled from "@emotion/styled"; import { @@ -27,6 +15,35 @@ import { } from "utils"; import { ReactComponent as InfoLogo } from "assets/icons/info-24.svg"; import Toast from "components/Toast"; +import BouncingDotsLoader from "components/BouncingDotsLoader"; +import NotFound from "./views/NotFound"; + +const Pool = lazy(() => import(/* webpackChunkName: "Pool" */ "./views/Pool")); +const Rewards = lazy( + () => import(/* webpackChunkName: "Rewards" */ "./views/Rewards") +); +const Send = lazy(() => import(/* webpackChunkName: "Send" */ "./views/Send")); +const About = lazy( + () => import(/* webpackChunkName: "About" */ "./views/About") +); +const Claim = lazy( + () => import(/* webpackChunkName: "Claim" */ "./views/Claim") +); +const Staking = lazy( + () => import(/* webpackChunkName: "Staking" */ "./views/Staking") +); +const MyTransactions = lazy( + () => + import( + /* webpackChunkName: "MyTransactions" */ "./views/Transactions/myTransactions" + ) +); +const AllTransactions = lazy( + () => + import( + /* webpackChunkName: "AllTransactions" */ "./views/Transactions/allTransactions" + ) +); const warningMessage = ` We noticed that you have connected from a contract address. @@ -100,36 +117,35 @@ const Routes: React.FC = () => { {isContractAddress && ( {warningMessage} )} - - - USDT currently disabled for Across contract upgrade. -
- - - - - - - { - const poolIdFound = stringValueInArray( - match.params.poolId.toLowerCase(), - config.getPoolSymbols() - ); + }> + + + + + + + { + const poolIdFound = stringValueInArray( + match.params.poolId.toLowerCase(), + config.getPoolSymbols() + ); + + if (poolIdFound) { + return ; + } else { + return ; + } + }} + /> + + - if (poolIdFound) { - return ; - } else { - return ; - } - }} - /> - diff --git a/src/assets/Across-About-Bullet-logox2.png b/src/assets/Across-About-Bullet-logox2.png deleted file mode 100644 index 6abab6c94..000000000 Binary files a/src/assets/Across-About-Bullet-logox2.png and /dev/null differ diff --git a/src/assets/Across-Discourse-white.svg b/src/assets/Across-Discourse-white.svg deleted file mode 100644 index 08ac4d8b6..000000000 --- a/src/assets/Across-Discourse-white.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/assets/Across-Medium-white.svg b/src/assets/Across-Medium-white.svg deleted file mode 100644 index ffc9912af..000000000 --- a/src/assets/Across-Medium-white.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/checkmark.svg b/src/assets/checkmark.svg deleted file mode 100644 index 366477019..000000000 --- a/src/assets/checkmark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/corner-down-right-tooltip.svg b/src/assets/corner-down-right-tooltip.svg deleted file mode 100644 index 1c148be00..000000000 --- a/src/assets/corner-down-right-tooltip.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/corner-down-right.svg b/src/assets/corner-down-right.svg deleted file mode 100644 index 1c148be00..000000000 --- a/src/assets/corner-down-right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/disc-logo.svg b/src/assets/disc-logo.svg deleted file mode 100644 index 54e76700e..000000000 --- a/src/assets/disc-logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/icon-twitter.svg b/src/assets/icon-twitter.svg deleted file mode 100644 index 2fc99675e..000000000 --- a/src/assets/icon-twitter.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/info.svg b/src/assets/info.svg deleted file mode 100644 index a58c046bc..000000000 --- a/src/assets/info.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/lg-across-logo.svg b/src/assets/lg-across-logo.svg deleted file mode 100644 index d7dd73db1..000000000 --- a/src/assets/lg-across-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/link.svg b/src/assets/link.svg deleted file mode 100644 index 71f6a5cbd..000000000 --- a/src/assets/link.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/logo-mobile.svg b/src/assets/logo-mobile.svg deleted file mode 100644 index 8100bc350..000000000 --- a/src/assets/logo-mobile.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg deleted file mode 100644 index 1f1f7608c..000000000 --- a/src/assets/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/optimism.svg b/src/assets/optimism.svg deleted file mode 100644 index c3118cec9..000000000 --- a/src/assets/optimism.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/assets/pagination-left-arrow.svg b/src/assets/pagination-left-arrow.svg deleted file mode 100644 index 281746f48..000000000 --- a/src/assets/pagination-left-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/pagination-right-arrow.svg b/src/assets/pagination-right-arrow.svg deleted file mode 100644 index 0ae312650..000000000 --- a/src/assets/pagination-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/powered_by_uma.png b/src/assets/powered_by_uma.png deleted file mode 100644 index 92b4b5cb7..000000000 Binary files a/src/assets/powered_by_uma.png and /dev/null differ diff --git a/src/assets/streamline-share.svg b/src/assets/streamline-share.svg deleted file mode 100644 index 4bd368672..000000000 --- a/src/assets/streamline-share.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/assets/user-tooltip.svg b/src/assets/user-tooltip.svg deleted file mode 100644 index 14dff8d0a..000000000 --- a/src/assets/user-tooltip.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/user.svg b/src/assets/user.svg deleted file mode 100644 index 14dff8d0a..000000000 --- a/src/assets/user.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/users-tooltip.svg b/src/assets/users-tooltip.svg deleted file mode 100644 index a477ea129..000000000 --- a/src/assets/users-tooltip.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/assets/users.svg b/src/assets/users.svg deleted file mode 100644 index 95db27add..000000000 --- a/src/assets/users.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/assets/white-up-right-arrow.svg b/src/assets/white-up-right-arrow.svg deleted file mode 100644 index 762698b81..000000000 --- a/src/assets/white-up-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/AddressSelection/useAddressSelection.ts b/src/components/AddressSelection/useAddressSelection.ts index 40ea384e4..9027b7d59 100644 --- a/src/components/AddressSelection/useAddressSelection.ts +++ b/src/components/AddressSelection/useAddressSelection.ts @@ -1,6 +1,6 @@ import { useSelect } from "downshift"; import { useState, useEffect } from "react"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useSendForm } from "hooks"; import { isValidAddress, diff --git a/src/components/BouncingDotsLoader/BouncingDotsLoader.tsx b/src/components/BouncingDotsLoader/BouncingDotsLoader.tsx index db068df19..d87e20b5b 100644 --- a/src/components/BouncingDotsLoader/BouncingDotsLoader.tsx +++ b/src/components/BouncingDotsLoader/BouncingDotsLoader.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import styled from "@emotion/styled"; -export type BounceType = "default | big"; +export type BounceType = "default" | "big"; interface Props { type?: BounceType; diff --git a/src/components/ChainSelection/useChainSelection.ts b/src/components/ChainSelection/useChainSelection.ts index 37bf140b0..0c29bacf2 100644 --- a/src/components/ChainSelection/useChainSelection.ts +++ b/src/components/ChainSelection/useChainSelection.ts @@ -1,5 +1,5 @@ import { useSelect } from "downshift"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useSendForm } from "hooks"; import { UnsupportedChainIdError, diff --git a/src/components/CoinSelection/useCoinSelection.tsx b/src/components/CoinSelection/useCoinSelection.tsx index 32c1e8324..3d9645be8 100644 --- a/src/components/CoinSelection/useCoinSelection.tsx +++ b/src/components/CoinSelection/useCoinSelection.tsx @@ -1,8 +1,8 @@ +import React, { useEffect, useCallback } from "react"; import { useSelect } from "downshift"; import { ethers, BigNumber } from "ethers"; import { formatUnits, parseUnits } from "ethers/lib/utils"; -import React, { useEffect, useCallback } from "react"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useBalancesBySymbols, useBridgeFees, useSendForm } from "hooks"; import { ParsingError, @@ -12,6 +12,7 @@ import { FEE_ESTIMATION, toWeiSafe, getConfig, + bridgeDisabled, } from "utils"; import { useQueryParams } from "hooks"; @@ -57,7 +58,12 @@ export default function useCoinSelection() { (token) => token.symbol === selectedItem?.symbol ); const balance = balances[selectedIndex]; - const { fees } = useBridgeFees(amount, toChain, selectedItem?.symbol); + const { fees } = useBridgeFees( + amount, + fromChain, + toChain, + selectedItem?.symbol + ); const [inputAmount, setInputAmount] = React.useState( selectedItem && amount.gt("0") @@ -74,7 +80,7 @@ export default function useCoinSelection() { try { // Check if Token exists and amount is convertable to Wei config.getTokenInfoBySymbol( - Number(params.from), + config.resolveChainIdFromNumericOrCanonical(params.from), params.asset.toUpperCase() ); toWeiSafe(params.amount); @@ -138,7 +144,9 @@ export default function useCoinSelection() { ) { error = new InsufficientBalanceError(); } - const errorMsg = error + const errorMsg = bridgeDisabled + ? "Bridge currently disabled" + : error ? error.message : fees?.isAmountTooLow ? "Bridge fee is high for this amount. Send a larger amount." @@ -147,6 +155,7 @@ export default function useCoinSelection() { : undefined; const showError = + bridgeDisabled || error || (fees?.isAmountTooLow && amount.gt(0)) || (fees?.isLiquidityInsufficient && amount.gt(0)); diff --git a/src/components/PoolForm/AddLiquidityForm.tsx b/src/components/PoolForm/AddLiquidityForm.tsx index f7096d0e9..7cd80e864 100644 --- a/src/components/PoolForm/AddLiquidityForm.tsx +++ b/src/components/PoolForm/AddLiquidityForm.tsx @@ -1,5 +1,5 @@ import { FC, useState, useCallback, useEffect } from "react"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { RoundBox, MaxButton, diff --git a/src/components/PoolForm/PoolForm.tsx b/src/components/PoolForm/PoolForm.tsx index 1068cb94c..ddc6f50e2 100644 --- a/src/components/PoolForm/PoolForm.tsx +++ b/src/components/PoolForm/PoolForm.tsx @@ -26,7 +26,7 @@ import { formatNumberMaxFracDigits, toWeiSafe, } from "utils"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import type { ShowSuccess } from "views/Pool"; import useSetLiquidityFormErrors from "./useSetLiquidityFormErrors"; import maxClickHandler from "./maxClickHandler"; diff --git a/src/components/PoolForm/RemoveLiquidityForm.tsx b/src/components/PoolForm/RemoveLiquidityForm.tsx index a1c36012c..8ddee9a29 100644 --- a/src/components/PoolForm/RemoveLiquidityForm.tsx +++ b/src/components/PoolForm/RemoveLiquidityForm.tsx @@ -1,6 +1,6 @@ import { FC, Dispatch, SetStateAction, useState, useEffect } from "react"; import PoolFormSlider from "./PoolFormSlider"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { RemoveAmount, RemovePercentButtonsWrapper, @@ -162,7 +162,10 @@ const RemoveLiqudityForm: FC = ({ feesEarned: max(feesEarned, 0), positionValue: totalPosition, }, - removeAmountSlider / 100 + // the sdk type specifies this needs to be a number but actually can take bignumberish. + // without setting tofixed is possible to crash the bignumber calculation with decimals + // longer than 18 digits by inputting very small amounts to withdraw. + (removeAmountSlider / 100).toFixed(18) as unknown as number ) : null; diff --git a/src/components/PoolSelection/PoolSelection.tsx b/src/components/PoolSelection/PoolSelection.tsx index a9944deb7..934bbfeb4 100644 --- a/src/components/PoolSelection/PoolSelection.tsx +++ b/src/components/PoolSelection/PoolSelection.tsx @@ -2,7 +2,8 @@ import { FC, Dispatch, SetStateAction } from "react"; import { AnimatePresence } from "framer-motion"; import { useSelect } from "downshift"; -import { useBalances, useConnection } from "state/hooks"; +import { useBalances } from "state/hooks"; +import { useConnection } from "hooks"; import { formatUnits, TokenList, ChainId, Token } from "utils"; import { SectionTitle } from "../Section"; diff --git a/src/components/SendAction/SendAction.tsx b/src/components/SendAction/SendAction.tsx index c36191d47..42974fcd4 100644 --- a/src/components/SendAction/SendAction.tsx +++ b/src/components/SendAction/SendAction.tsx @@ -51,8 +51,13 @@ const SendAction: React.FC = ({ onDeposit }) => { const tokenInfo = tokenSymbol ? getToken(tokenSymbol) : undefined; const isWETH = tokenInfo?.symbol === "WETH"; let timeToRelay = "loading"; - if (limits && toChain) { - timeToRelay = getConfirmationDepositTime(amount, limits, toChain); + if (limits && toChain && fromChain) { + timeToRelay = getConfirmationDepositTime( + amount, + limits, + toChain, + fromChain + ); } else if (limitsError) { timeToRelay = "estimation failed"; } diff --git a/src/components/SendAction/useSendAction.ts b/src/components/SendAction/useSendAction.ts index 480578676..d6e8055c1 100644 --- a/src/components/SendAction/useSendAction.ts +++ b/src/components/SendAction/useSendAction.ts @@ -1,8 +1,8 @@ import { useState } from "react"; import { useSendForm, useBridgeFees, useBridge, useBridgeLimits } from "hooks"; -import { confirmations } from "utils"; +import { confirmations, bridgeDisabled } from "utils"; import { Deposit } from "views/Confirmation"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; export default function useSendAction( onDepositConfirmed: (deposit: Deposit) => void @@ -12,7 +12,7 @@ export default function useSendAction( const toggleInfoModal = () => setOpenInfoModal((oldOpen) => !oldOpen); const { fromChain, toChain, amount, tokenSymbol, toAddress, selectedRoute } = useSendForm(); - const { fees } = useBridgeFees(amount, toChain, tokenSymbol); + const { fees } = useBridgeFees(amount, fromChain, toChain, tokenSymbol); const { limits, isError } = useBridgeLimits( selectedRoute?.fromTokenAddress, fromChain, @@ -23,7 +23,7 @@ export default function useSendAction( const [txHash, setTxHash] = useState(""); const handleActionClick = async () => { - if (status !== "ready" || !selectedRoute) { + if (status !== "ready" || !selectedRoute || bridgeDisabled) { return; } try { @@ -76,7 +76,8 @@ export default function useSendAction( } }; - const buttonDisabled = status !== "ready" || txPending || !selectedRoute; + const buttonDisabled = + status !== "ready" || txPending || !selectedRoute || bridgeDisabled; let buttonMsg: string = "Send"; if (txPending) { diff --git a/src/components/SendForm/useSendFormComponent.ts b/src/components/SendForm/useSendFormComponent.ts index 9a32f0db7..093fccc7f 100644 --- a/src/components/SendForm/useSendFormComponent.ts +++ b/src/components/SendForm/useSendFormComponent.ts @@ -1,5 +1,4 @@ -import { useSendForm } from "hooks"; -import { useConnection } from "state/hooks"; +import { useSendForm, useConnection } from "hooks"; export default function useSendFormComponent() { const { fromChain } = useSendForm(); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index d02d58f7c..d8ee04e4c 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -16,7 +16,7 @@ import { import { getChainInfo, isSupportedChainId } from "utils"; import useSidebar from "./useSidebar"; import closeIcon from "assets/across-close-button.svg"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; interface Props { openSidebar: boolean; @@ -24,8 +24,15 @@ interface Props { } const Sidebar: FC = ({ openSidebar, setOpenSidebar }) => { - const { account, ensName, isConnected, chainId, location, className } = - useSidebar(openSidebar); + const { + sidebarNavigationLinks, + account, + ensName, + isConnected, + chainId, + location, + className, + } = useSidebar(openSidebar); const { connect, disconnect, wallet } = useConnection(); const addrOrEns = ensName ?? account; @@ -33,9 +40,13 @@ const Sidebar: FC = ({ openSidebar, setOpenSidebar }) => { setOpenSidebar(false); }; + const onClickOverlay = () => { + setOpenSidebar(false); + }; + return ( <> - {openSidebar && } + {openSidebar && onClickOverlay()} />} @@ -66,106 +77,30 @@ const Sidebar: FC = ({ openSidebar, setOpenSidebar }) => { ) : null} - - onClickLink()} - to={{ pathname: "/", search: location.search }} - > - Bridge - - - - onClickLink()} - to={{ pathname: "/pool", search: location.search }} - > - Pool - - - - onClickLink()} - to={{ pathname: "/transactions", search: location.search }} - > - Transactions - - - - onClickLink()} - to={{ pathname: "/rewards", search: location.search }} - > - Rewards - - - - onClickLink()} - to={{ pathname: "/about", search: location.search }} - > - About - - - - onClickLink()} - > - Docs - - - - onClickLink()} - > - Support (Discord) - - - - onClickLink()} - > - Github - - - - onClickLink()} - > - Twitter - - - - onClickLink()} - > - Medium - - - - onClickLink()} + {sidebarNavigationLinks.map((item, idx) => ( + - Discourse - - + {item.isExternalLink ? ( + onClickLink()} + > + {item.title} + + ) : ( + onClickLink()} + to={{ pathname: item.pathName, search: location.search }} + > + {item.title} + + )} + + ))} diff --git a/src/components/Sidebar/useSidebar.ts b/src/components/Sidebar/useSidebar.ts index 55e185168..ccdd1a30a 100644 --- a/src/components/Sidebar/useSidebar.ts +++ b/src/components/Sidebar/useSidebar.ts @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; -import { useConnection } from "state/hooks"; import { useLocation } from "react-router-dom"; -import { usePrevious } from "hooks"; +import { usePrevious, useConnection } from "hooks"; type SidebarWrapperClasses = "open" | "closed" | "transition"; + export default function useSidebar(openSidebar: boolean) { const { account, ensName, isConnected, chainId } = useConnection(); const location = useLocation(); @@ -26,6 +26,59 @@ export default function useSidebar(openSidebar: boolean) { }, 250); } }, [openSidebar, prevOpenSidebar]); + + const sidebarNavigationLinks = [ + { + pathName: "/", + title: "Bridge", + }, + { + pathName: "/pool", + title: "Pool", + }, + { + pathName: "/transactions", + title: "Transactions", + }, + { + pathName: "/rewards", + title: "Rewards", + }, + { + pathName: "/about", + title: "About", + }, + { + title: "Docs", + link: "https://docs.across.to/bridge/", + isExternalLink: true, + }, + { + title: "Support (Discord)", + link: "https://discord.com/invite/across/", + isExternalLink: true, + }, + { + title: "Github", + link: "https://github.com/across-protocol", + isExternalLink: true, + }, + { + title: "Twitter", + link: "https://twitter.com/AcrossProtocol/", + isExternalLink: true, + }, + { + title: "Medium", + link: "https://medium.com/across-protocol", + isExternalLink: true, + }, + { + title: "Discourse", + link: "https://forum.across.to/", + isExternalLink: true, + }, + ]; return { account, ensName, @@ -34,5 +87,6 @@ export default function useSidebar(openSidebar: boolean) { location, className, setClassName, + sidebarNavigationLinks, }; } diff --git a/src/components/Wallet/Wallet.tsx b/src/components/Wallet/Wallet.tsx index 9f41bd91b..eae205cb0 100644 --- a/src/components/Wallet/Wallet.tsx +++ b/src/components/Wallet/Wallet.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { ConnectButton, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 1dd3023f8..e163e8a4f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -8,5 +8,7 @@ export * from "./useSendForm"; export * from "./useBridge"; export * from "./useQueryParams"; export * from "./useError"; -export * from "./useWindowsSize"; +export * from "./useWindowSize"; export * from "./useScrollPosition"; +export * from "./useNotify"; +export * from "./useConnection"; diff --git a/src/hooks/tests/usePrevious.test.ts b/src/hooks/tests/usePrevious.test.ts new file mode 100644 index 000000000..d7ec6ae90 --- /dev/null +++ b/src/hooks/tests/usePrevious.test.ts @@ -0,0 +1,23 @@ +import { renderHook } from "@testing-library/react-hooks"; +import { usePrevious } from "../usePrevious"; +const setUp = () => + renderHook(({ state }) => usePrevious(state), { initialProps: { state: 0 } }); + +it("should return 0 on initial render", () => { + const { result } = setUp(); + + expect(result.current).toEqual(0); +}); + +it("should always return previous state after each update", () => { + const { result, rerender } = setUp(); + + rerender({ state: 2 }); + expect(result.current).toBe(0); + + rerender({ state: 4 }); + expect(result.current).toBe(2); + + rerender({ state: 6 }); + expect(result.current).toBe(4); +}); diff --git a/src/hooks/tests/useScrollPosition.test.ts b/src/hooks/tests/useScrollPosition.test.ts new file mode 100644 index 000000000..cc7adee81 --- /dev/null +++ b/src/hooks/tests/useScrollPosition.test.ts @@ -0,0 +1,22 @@ +import { renderHook, act } from "@testing-library/react-hooks"; +import useScrollPosition from "../useScrollPosition"; + +test("should have a default scroll position", () => { + const { result } = renderHook(() => useScrollPosition()); + expect(result.current).toEqual(0); + act(() => { + global.pageYOffset = 100; + global.dispatchEvent(new Event("scroll")); + }); + expect(result.current).toEqual(100); +}); + +test("scrollPosition updates on change", () => { + const { result } = renderHook(() => useScrollPosition()); + + act(() => { + global.pageYOffset = 100; + global.dispatchEvent(new Event("scroll")); + }); + expect(result.current).toEqual(100); +}); diff --git a/src/hooks/tests/useWindowSize.test.ts b/src/hooks/tests/useWindowSize.test.ts new file mode 100644 index 000000000..744dcfd67 --- /dev/null +++ b/src/hooks/tests/useWindowSize.test.ts @@ -0,0 +1,21 @@ +import { renderHook, act } from "@testing-library/react-hooks"; +import useWindowSize from "../useWindowSize"; + +test("should have a default window size", () => { + const { result } = renderHook(() => useWindowSize()); + expect(result.current.width).toEqual(1024); + expect(result.current.height).toEqual(768); +}); + +test("Resize should change default width and height", () => { + const { result } = renderHook(() => useWindowSize()); + + act(() => { + global.innerWidth = 1000; + global.innerHeight = 500; + // Trigger the window resize event. + global.dispatchEvent(new Event("resize")); + }); + expect(result.current.width).toEqual(1000); + expect(result.current.height).toEqual(500); +}); diff --git a/src/hooks/useAllowance.ts b/src/hooks/useAllowance.ts index 41d40fc54..d5354bb5c 100644 --- a/src/hooks/useAllowance.ts +++ b/src/hooks/useAllowance.ts @@ -1,5 +1,5 @@ import { useQuery } from "react-query"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { allowanceQueryKey, getAllowance, ChainId } from "utils"; import { useBlock } from "./useBlock"; import { BigNumber } from "ethers"; diff --git a/src/hooks/useBalance.ts b/src/hooks/useBalance.ts index f6198460e..7f6e95af9 100644 --- a/src/hooks/useBalance.ts +++ b/src/hooks/useBalance.ts @@ -1,5 +1,5 @@ import { useQuery, useQueries } from "react-query"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useBlock } from "./useBlock"; import { usePrevious } from "hooks"; import { diff --git a/src/hooks/useBridge.ts b/src/hooks/useBridge.ts index b295406e9..da54a81dc 100644 --- a/src/hooks/useBridge.ts +++ b/src/hooks/useBridge.ts @@ -4,7 +4,7 @@ import { FormStatus, useSendForm } from "./useSendForm"; import { useBalanceBySymbol } from "./useBalance"; import { useBridgeFees } from "./useBridgeFees"; import { useAllowance } from "./useAllowance"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; import { useBlock } from "./useBlock"; import { FEE_ESTIMATION, @@ -78,7 +78,7 @@ export function useBridge() { fromChain && tokenSymbol ? config.getTokenInfoBySymbol(fromChain, tokenSymbol) : undefined; - const { fees } = useBridgeFees(amount, toChain, tokenSymbol); + const { fees } = useBridgeFees(amount, fromChain, toChain, tokenSymbol); const hasToSwitchChain = Boolean( fromChain && chainId && chainId !== fromChain ); diff --git a/src/hooks/useBridgeFees.ts b/src/hooks/useBridgeFees.ts index d04c8a18a..4fb798d56 100644 --- a/src/hooks/useBridgeFees.ts +++ b/src/hooks/useBridgeFees.ts @@ -6,19 +6,28 @@ import { useBlock } from "./useBlock"; /** * This hook calculates the bridge fees for a given token and amount. * @param amount - The amount to check bridge fees for. + * @param fromChainId The chain Id of the origin chain * @param toChainId The chain Id of the receiving chain, its timestamp will be used to calculate the fees. * @param tokenSymbol - The token symbol to check bridge fees for. * @returns The bridge fees for the given amount and token symbol and the UseQueryResult object. */ export function useBridgeFees( amount: ethers.BigNumber, + fromChainId?: ChainId, toChainId?: ChainId, tokenSymbol?: string ) { const { block } = useBlock(toChainId); - const enabledQuery = !!toChainId && !!block && !!tokenSymbol && amount.gt(0); + const enabledQuery = + !!toChainId && !!fromChainId && !!block && !!tokenSymbol && amount.gt(0); const queryKey = enabledQuery - ? bridgeFeesQueryKey(tokenSymbol, amount, toChainId, block.number) + ? bridgeFeesQueryKey( + tokenSymbol, + amount, + fromChainId, + toChainId, + block.number + ) : "DISABLED_BRIDGE_FEE_QUERY"; const { data: fees, ...delegated } = useQuery( queryKey, @@ -28,6 +37,7 @@ export function useBridgeFees( tokenSymbol: tokenSymbol!, blockTimestamp: block!.timestamp, toChainId: toChainId!, + fromChainId: fromChainId!, }); }, { diff --git a/src/hooks/useConnection.ts b/src/hooks/useConnection.ts new file mode 100644 index 000000000..19d68f851 --- /dev/null +++ b/src/hooks/useConnection.ts @@ -0,0 +1,53 @@ +import { useState, useEffect } from "react"; +import { useOnboard } from "hooks/useOnboard"; +import { ethers } from "ethers"; +import { getCode, noContractCode } from "utils"; + +export function useConnection() { + const [isContractAddress, setIsContractAddress] = useState(false); + const { + provider, + signer, + isConnected, + connect, + disconnect, + notify, + account, + chainId, + wallet, + error, + setChain, + setNotifyConfig, + } = useOnboard(); + + useEffect(() => { + setIsContractAddress(false); + if (account && chainId) { + const addr = ethers.utils.getAddress(account.address); + getCode(addr, chainId) + .then((res) => { + setIsContractAddress(res !== noContractCode); + }) + .catch((err) => { + console.log("err in getCode call", err); + }); + } + }, [account, chainId]); + + return { + account: account ? ethers.utils.getAddress(account.address) : undefined, + ensName: account?.ens, + chainId, + provider, + signer, + isConnected, + notify, + setNotifyConfig, + connect, + disconnect, + error, + wallet, + setChain, + isContractAddress, + }; +} diff --git a/src/hooks/useNotify.ts b/src/hooks/useNotify.ts new file mode 100644 index 000000000..88a83ed9b --- /dev/null +++ b/src/hooks/useNotify.ts @@ -0,0 +1,77 @@ +import { useEffect, useState, useCallback } from "react"; +import { ContractTransaction } from "ethers"; + +import { useConnection } from "hooks"; +import { supportedNotifyChainIds } from "utils/constants"; +import { getChainInfo } from "utils"; + +type TxStatus = "idle" | "pending" | "success" | "error"; + +export function useNotify() { + const [chainId, setChainId] = useState(); + const [txResponse, setTxResponse] = useState< + ContractTransaction | undefined + >(); + const [txStatus, setTxStatus] = useState("idle"); + const [txErrorMsg, setTxErrorMsg] = useState(); + + const { notify, setNotifyConfig } = useConnection(); + + const cleanup = useCallback(() => { + if (txResponse) { + notify.unsubscribe(txResponse.hash); + setChainId(undefined); + setTxResponse(undefined); + setNotifyConfig({ + networkId: 1, + }); + } + }, [txResponse, notify, setNotifyConfig]); + + useEffect(() => { + if (txResponse && chainId) { + setTxStatus("pending"); + + if (supportedNotifyChainIds.includes(chainId)) { + const { emitter } = notify.hash(txResponse.hash); + setNotifyConfig({ + networkId: chainId, + }); + + emitter.on("txSent", () => { + return { + link: getChainInfo(chainId).constructExplorerLink(txResponse.hash), + }; + }); + emitter.on("txConfirmed", () => { + setTxStatus("success"); + cleanup(); + }); + emitter.on("txFailed", () => { + setTxStatus("error"); + setTxErrorMsg("Tx failed"); + cleanup(); + }); + } else { + // TODO: We could still trigger notifications for chains that are not supported by Notify. + // See https://docs.blocknative.com/notify#notification + txResponse + .wait() + .then(() => { + setTxStatus("success"); + }) + .catch((error) => { + setTxStatus("error"); + setTxErrorMsg(error.message); + }); + } + } + }, [txResponse, cleanup, setNotifyConfig, chainId, notify]); + + return { + setChainId, + setTxResponse, + txStatus, + txErrorMsg, + }; +} diff --git a/src/hooks/useOnboard.tsx b/src/hooks/useOnboard.tsx index 4c7528d62..5516ede16 100644 --- a/src/hooks/useOnboard.tsx +++ b/src/hooks/useOnboard.tsx @@ -20,7 +20,7 @@ import { Account } from "@web3-onboard/core/dist/types"; import { useConnectWallet, useSetChain } from "@web3-onboard/react"; import { Chain } from "@web3-onboard/common"; import { ethers } from "ethers"; -import Notify, { API as NotifyAPI } from "bnc-notify"; +import Notify, { API as NotifyAPI, ConfigOptions } from "bnc-notify"; export type SetChainOptions = { chainId: string; @@ -40,18 +40,19 @@ type OnboardContextValue = { signer: ethers.providers.JsonRpcSigner | undefined; provider: ethers.providers.Web3Provider | null; notify: NotifyAPI; + setNotifyConfig: (opts: ConfigOptions) => void; account: Account | null; chainId: ChainId; error?: Error; }; const notify = Notify({ - dappId: process.env.REACT_APP_PUBLIC_ONBOARD_API_KEY, // [String] The API key created by step one above + dappId: process.env.REACT_APP_PUBLIC_ONBOARD_API_KEY, networkId: 1, desktopPosition: "topRight", }); -function useOnboardManager() { +export function useOnboardManager() { const [onboard, setOnboard] = useState(null); const [provider, setProvider] = useState(null); @@ -113,13 +114,14 @@ function useOnboardManager() { signer, provider, notify, + setNotifyConfig: (config: ConfigOptions) => notify.config(config), account, chainId: (Number(wallet?.chains[0].id) as ChainId) || 0, error, }; } -const OnboardContext = createContext( +export const OnboardContext = createContext( undefined ); OnboardContext.displayName = "OnboardContext"; @@ -130,6 +132,8 @@ export const OnboardProvider: React.FC = ({ children }) => { ); }; +OnboardProvider.displayName = "OnboardProvider"; + export function useOnboard() { const context = useContext(OnboardContext); if (!context) { diff --git a/src/hooks/useReferrer.ts b/src/hooks/useReferrer.ts index c17c37e32..206f80c89 100644 --- a/src/hooks/useReferrer.ts +++ b/src/hooks/useReferrer.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { ethers } from "ethers"; import { useQueryParams } from "./useQueryParams"; -import { useConnection } from "state/hooks"; +import { useConnection } from "hooks"; export default function useReferrer() { const { provider } = useConnection(); diff --git a/src/hooks/useSendForm.tsx b/src/hooks/useSendForm.tsx index 4fae59fcd..3af7332cb 100644 --- a/src/hooks/useSendForm.tsx +++ b/src/hooks/useSendForm.tsx @@ -21,8 +21,7 @@ import { isSupportedChainId, } from "utils"; -import { usePrevious } from "hooks"; -import { useConnection } from "state/hooks"; +import { usePrevious, useConnection } from "hooks"; import { useQueryParams } from "./useQueryParams"; import { useHistory } from "react-router-dom"; export enum FormStatus { @@ -375,8 +374,9 @@ function useSendFormManager(): SendFormManagerContext { Because we need the asset's decimal value, you need to define **both** asset and amount for the optional params. */ useEffect(() => { - const fromChain = Number(params.from); - const toChain = Number(params.to); + const [fromChain, toChain] = [params.from, params.to].map( + config.resolveChainIdFromNumericOrCanonical + ); const areSupportedChains = [fromChain, toChain].every( config.isSupportedChainId ); diff --git a/src/hooks/useWindowsSize.ts b/src/hooks/useWindowSize.ts similarity index 100% rename from src/hooks/useWindowsSize.ts rename to src/hooks/useWindowSize.ts diff --git a/src/state/hooks.ts b/src/state/hooks.ts index 475a1bb13..affc3e556 100644 --- a/src/state/hooks.ts +++ b/src/state/hooks.ts @@ -1,64 +1,18 @@ -import { useMemo, useState, useEffect } from "react"; +import { useMemo } from "react"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { ethers } from "ethers"; import { bindActionCreators } from "redux"; -import { ChainId, getConfig, Token, getCode, noContractCode } from "utils"; +import { ChainId, getConfig, Token } from "utils"; import type { RootState, AppDispatch } from "./"; import chainApi from "./chainApi"; import { add } from "./transactions"; -import { useOnboard } from "hooks/useOnboard"; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; -export function useConnection() { - const [isContractAddress, setIsContractAddress] = useState(false); - const { - provider, - signer, - isConnected, - connect, - disconnect, - notify, - account, - chainId, - wallet, - error, - } = useOnboard(); - - useEffect(() => { - setIsContractAddress(false); - if (account && chainId) { - const addr = ethers.utils.getAddress(account.address); - getCode(addr, chainId) - .then((res) => { - setIsContractAddress(res !== noContractCode); - }) - .catch((err) => { - console.log("err in getCode call", err); - }); - } - }, [account, chainId]); - - return { - account: account ? ethers.utils.getAddress(account.address) : undefined, - ensName: account?.ens, - chainId, - provider, - signer, - isConnected, - notify, - connect, - disconnect, - error, - wallet, - isContractAddress, - }; -} - export function useTransactions() { const { transactions } = useAppSelector((state) => state.transactions); const dispatch = useAppDispatch(); diff --git a/src/state/poolsApi.ts b/src/state/poolsApi.ts index 334d3f5d6..c1baae350 100644 --- a/src/state/poolsApi.ts +++ b/src/state/poolsApi.ts @@ -2,7 +2,6 @@ import * as acrossSdk from "@across-protocol/sdk-v2"; import { update } from "./pools"; import { store } from "../state"; import { - getProvider, getConfigStoreAddress, ChainId, hubPoolAddress, @@ -10,6 +9,7 @@ import { getConfig, } from "utils"; import { ethers } from "ethers"; +import { getProvider } from "utils/providers"; const provider = getProvider(); diff --git a/src/stories/Footer.stories.tsx b/src/stories/Footer.stories.tsx new file mode 100644 index 000000000..a7296c5fd --- /dev/null +++ b/src/stories/Footer.stories.tsx @@ -0,0 +1,17 @@ +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import Footer from "components/Footer"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "Footer", + component: Footer, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +} as ComponentMeta; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template: ComponentStory = () =>