diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab0e8995c2..3b54445a76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,7 @@ jobs: - name: Run e2e (Brave) uses: nick-fields/retry@v2 with: - timeout_minutes: 8 + timeout_minutes: 10 max_attempts: 3 command: | export BROWSER=chrome @@ -173,7 +173,7 @@ jobs: - name: Run e2e (Brave) uses: nick-fields/retry@v2 with: - timeout_minutes: 8 + timeout_minutes: 10 max_attempts: 3 command: | export BROWSER=brave diff --git a/e2e/numbers.ts b/e2e/numbers.ts new file mode 100644 index 0000000000..828dab28bf --- /dev/null +++ b/e2e/numbers.ts @@ -0,0 +1,14 @@ +import BigNumber from 'bignumber.js'; + +export type BigNumberish = number | string | BigNumber; + +export const convertRawAmountToDecimalFormat = ( + value: BigNumberish, + decimals = 18, +): string => + new BigNumber(value).dividedBy(new BigNumber(10).pow(decimals)).toFixed(); + +export const subtract = ( + numberOne: BigNumberish, + numberTwo: BigNumberish, +): string => new BigNumber(numberOne).minus(new BigNumber(numberTwo)).toFixed(); diff --git a/e2e/serial/swapFlow.test.ts b/e2e/serial/swapFlow.test.ts deleted file mode 100644 index 231c6a647f..0000000000 --- a/e2e/serial/swapFlow.test.ts +++ /dev/null @@ -1,679 +0,0 @@ -/* eslint-disable no-await-in-loop */ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable jest/expect-expect */ - -import 'chromedriver'; -import 'geckodriver'; -import { WebDriver } from 'selenium-webdriver'; -import { afterAll, beforeAll, expect, it } from 'vitest'; - -import { - delayTime, - doNotFindElementByTestId, - findElementAndClick, - findElementByTestId, - findElementByTestIdAndClick, - findElementByText, - getExtensionIdByName, - getTextFromText, - getTextFromTextInput, - goToPopup, - goToWelcome, - initDriverWithOptions, - querySelector, - typeOnTextInput, - waitAndClick, -} from '../helpers'; - -let rootURL = 'chrome-extension://'; -let driver: WebDriver; - -const browser = process.env.BROWSER || 'chrome'; -const os = process.env.OS || 'mac'; - -const DAI_MAINNET_ID = '0x6b175474e89094c44da98b954eedeac495271d0f_1'; -const ZEROX_MAINNET_ID = '0xe41d2489571d322189246dafa5ebde1f4699f498_1'; -const ETH_MAINNET_ID = 'eth_1'; -const OP_OPTIMISM_ID = '0x4200000000000000000000000000000000000042_10'; -const MATIC_POLYGON_ID = '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0_137'; -const GMX_ARBITRUM_ID = '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a_42161'; -const UNI_BNB_ID = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984_56'; - -beforeAll(async () => { - driver = await initDriverWithOptions({ - browser, - os, - }); - const extensionId = await getExtensionIdByName(driver, 'Rainbow'); - if (!extensionId) throw new Error('Extension not found'); - rootURL += extensionId; -}); - -afterAll(async () => driver.quit()); - -it('should be able import a wallet via seed', async () => { - // Start from welcome screen - await goToWelcome(driver, rootURL); - await findElementByTestIdAndClick({ - id: 'import-wallet-button', - driver, - }); - await findElementByTestIdAndClick({ - id: 'import-wallet-option', - driver, - }); - - await typeOnTextInput({ - id: 'secret-textarea', - driver, - text: 'test test test test test test test test test test test junk', - }); - - await findElementByTestIdAndClick({ - id: 'import-wallets-button', - driver, - }); - await findElementByTestIdAndClick({ - id: 'add-wallets-button', - driver, - }); - await typeOnTextInput({ id: 'password-input', driver, text: 'test1234' }); - await typeOnTextInput({ - id: 'confirm-password-input', - driver, - text: 'test1234', - }); - await findElementByTestIdAndClick({ id: 'set-password-button', driver }); - await delayTime('long'); - await findElementByText(driver, 'Your wallets ready'); -}); - -it('should be able to go to setings', async () => { - await goToPopup(driver, rootURL); - await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); - await findElementByTestIdAndClick({ id: 'settings-link', driver }); -}); - -it('should be able to connect to hardhat and turn swaps flag on', async () => { - const btn = await querySelector(driver, '[data-testid="connect-to-hardhat"]'); - await waitAndClick(btn, driver); - const button = await findElementByText(driver, 'Disconnect from Hardhat'); - expect(button).toBeTruthy(); - await findElementByTestIdAndClick({ id: 'feature-flag-swaps', driver }); - await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); -}); - -it('should be able to go to swap flow', async () => { - await findElementAndClick({ id: 'header-link-swap', driver }); -}); - -it('should be able to go to swap settings and check rows are visible', async () => { - await findElementByTestIdAndClick({ - id: 'swap-settings-navbar-button', - driver, - }); - const routeRow = await findElementByTestId({ - id: 'swap-settings-route-row', - driver, - }); - expect(routeRow).toBeTruthy(); - const slippageRow = await findElementByTestId({ - id: 'swap-settings-slippage-row', - driver, - }); - expect(slippageRow).toBeTruthy(); - await findElementByTestIdAndClick({ - id: 'swap-settings-done', - driver, - }); -}); - -it('should be able to go to settings and turn on flashbots', async () => { - await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); - await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); - await findElementByTestIdAndClick({ id: 'settings-link', driver }); - await findElementByTestIdAndClick({ id: 'settings-transactions', driver }); - await findElementByTestIdAndClick({ - id: 'flashbots-transactions-toggle', - driver, - }); - await findElementByTestIdAndClick({ - id: 'navbar-button-with-back', - driver, - }); - await findElementByTestIdAndClick({ - id: 'navbar-button-with-back', - driver, - }); - await findElementAndClick({ id: 'header-link-swap', driver }); -}); - -it('should be able to go to swap settings and check flashbots row is visible', async () => { - await findElementByTestIdAndClick({ - id: 'swap-settings-navbar-button', - driver, - }); - - const flashbotsRow = await findElementByTestId({ - id: 'swap-settings-flashbots-row', - driver, - }); - expect(flashbotsRow).toBeTruthy(); -}); - -it('should be able to interact with route settings', async () => { - await findElementByTestIdAndClick({ - id: 'swap-settings-route-label', - driver, - }); - await findElementByTestIdAndClick({ - id: 'explainer-action-button', - driver, - }); - await findElementByTestIdAndClick({ - id: 'settings-route-context-trigger-auto', - driver, - }); - await findElementByTestIdAndClick({ - id: 'settings-route-context-0x', - driver, - }); -}); - -it('should be able to interact with flashbots settings', async () => { - await findElementByTestIdAndClick({ - id: 'swap-settings-flashbots-label', - driver, - }); - await findElementByTestIdAndClick({ - id: 'explainer-action-button', - driver, - }); - await findElementByTestIdAndClick({ - id: 'swap-settings-flashbots-toggle', - driver, - }); - await findElementByTestIdAndClick({ - id: 'swap-settings-flashbots-toggle', - driver, - }); -}); - -it('should be able to interact with slippage settings', async () => { - await findElementByTestIdAndClick({ - id: 'swap-settings-slippage-label', - driver, - }); - await findElementByTestIdAndClick({ - id: 'explainer-action-button', - driver, - }); - await typeOnTextInput({ - id: 'slippage-input-mask', - driver, - text: '\b4', - }); - - const warning = findElementByTestId({ - id: 'swap-settings-slippage-warning', - driver, - }); - expect(warning).toBeTruthy(); -}); - -it('should be able to set default values for settings and go back to swap', async () => { - await findElementByTestIdAndClick({ - id: 'settings-use-defaults-button', - driver, - }); - const routeTriggerAuto = await findElementByTestId({ - id: 'settings-route-context-trigger-auto', - driver, - }); - expect(routeTriggerAuto).toBeTruthy(); - const text = await getTextFromTextInput({ - id: 'slippage-input-mask', - driver, - }); - expect(text).toBe('1'); - await findElementByTestIdAndClick({ id: 'swap-settings-done', driver }); -}); - -it('should be able to open token to sell input and select assets', async () => { - await findElementByTestIdAndClick({ - id: 'token-to-sell-search-token-input', - driver, - }); - await findElementByTestIdAndClick({ - id: 'token-to-sell-sort-trigger', - driver, - }); - - const sortByBalance = await findElementByTestId({ - id: 'token-to-sell-sort-balance', - driver, - }); - expect(sortByBalance).toBeTruthy(); - await findElementByTestIdAndClick({ - id: 'token-to-sell-sort-network', - driver, - }); - await findElementByTestIdAndClick({ - id: `${ETH_MAINNET_ID}-token-to-sell-row`, - driver, - }); - const toSellInputEthSelected = await findElementByTestId({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(toSellInputEthSelected).toBeTruthy(); - await findElementByTestIdAndClick({ - id: 'swap-flip-button', - driver, - }); - const toBuyInputEthSelected = await findElementByTestId({ - id: `${ETH_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - expect(toBuyInputEthSelected).toBeTruthy(); - await findElementByTestIdAndClick({ - id: 'swap-flip-button', - driver, - }); -}); - -it('should be able to open press max on token to sell input', async () => { - const fiatValueText = await getTextFromText({ - id: 'token-to-sell-info-fiat-value', - driver, - }); - expect(fiatValueText).toBe('$0.00'); - await findElementByTestIdAndClick({ - id: 'token-to-sell-info-max-button', - driver, - }); - const ethValueBeforeGas = await getTextFromTextInput({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(ethValueBeforeGas).toEqual('10000'); - const fiatValueTextAfterMax = await getTextFromText({ - id: 'token-to-sell-info-fiat-value', - driver, - }); - expect(fiatValueTextAfterMax).not.toEqual('$0.00'); -}); - -it('should be able to remove token to sell and select it again', async () => { - await findElementByTestIdAndClick({ - id: `${ETH_MAINNET_ID}-token-to-sell-token-input-remove`, - driver, - }); - await findElementByTestIdAndClick({ - id: `${ETH_MAINNET_ID}-token-to-sell-row`, - driver, - }); - const toSellInputEthSelected = await findElementByTestId({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(toSellInputEthSelected).toBeTruthy(); - // should clear input value - const ethValueAfterSelection = await getTextFromTextInput({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(ethValueAfterSelection).toEqual(''); -}); - -it('should be able to open token to buy input and select assets', async () => { - await findElementByTestIdAndClick({ - id: 'token-to-buy-search-token-input', - driver, - }); - // check sell asset is not present as buy option - const elementFound = await doNotFindElementByTestId({ - id: `${ETH_MAINNET_ID}-token-to-buy-row`, - driver, - }); - await findElementByTestIdAndClick({ - id: `${DAI_MAINNET_ID}-favorites-token-to-buy-row`, - driver, - }); - expect(elementFound).toBeFalsy(); - const toBuyInputDaiSelected = await findElementByTestId({ - id: `${DAI_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - expect(toBuyInputDaiSelected).toBeTruthy(); -}); - -it('should be able to open remove token to buy and check favorites and verified lists are visible', async () => { - await findElementByTestIdAndClick({ - id: `${DAI_MAINNET_ID}-token-to-buy-token-input-remove`, - driver, - }); - const favoritesSection = await findElementByTestId({ - id: 'favorites-token-to-buy-section', - driver, - }); - expect(favoritesSection).toBeTruthy(); - const verifiedSection = await findElementByTestId({ - id: 'verified-token-to-buy-section', - driver, - }); - expect(verifiedSection).toBeTruthy(); -}); - -it('should be able to favorite a token and check the info button is present', async () => { - await findElementByTestIdAndClick({ - id: `${ZEROX_MAINNET_ID}-verified-token-to-buy-row-favorite-button`, - driver, - }); - await delayTime('short'); - await findElementByTestIdAndClick({ - id: `${ZEROX_MAINNET_ID}-favorites-token-to-buy-row-info-button`, - driver, - }); - await findElementByTestIdAndClick({ - id: `${ZEROX_MAINNET_ID}-favorites-token-to-buy-row-info-button-copy`, - driver, - }); -}); - -it('should be able to check price and balance of token to buy', async () => { - const tokenToBuyInfoPrice = await getTextFromText({ - id: 'token-to-buy-info-price', - driver, - }); - expect(tokenToBuyInfoPrice).not.toBe(''); - const tokenToBuyInfoBalance = await getTextFromText({ - id: 'token-to-buy-info-balance', - driver, - }); - expect(tokenToBuyInfoBalance).not.toBe(''); - - it('should be able to flip correctly', async () => { - await findElementByTestIdAndClick({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - await typeOnTextInput({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - text: 1, - driver, - }); - const assetToSellInputText = await getTextFromTextInput({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(assetToSellInputText).toBe('1'); - - await delayTime('very-long'); - - const assetToBuyInputText = await getTextFromTextInput({ - id: `${DAI_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - expect(assetToBuyInputText).not.toBe(''); - - await findElementByTestIdAndClick({ - id: 'swap-flip-button', - driver, - }); - - await delayTime('very-long'); - - const assetToSellInputTextAfterMax = await getTextFromTextInput({ - id: `${DAI_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - }); - expect(assetToSellInputTextAfterMax).not.toEqual(''); - - const assetToBuyInputTextAfterMax = await getTextFromTextInput({ - id: `${ETH_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - expect(assetToBuyInputTextAfterMax).toEqual('1'); - }); - - it('should be able to check insufficient asset for swap', async () => { - const confirmButtonText = await getTextFromText({ - id: 'swap-confirmation-button', - driver, - }); - expect(confirmButtonText).toEqual('Insufficient DAI'); - }); - - it('should be able to check insufficient native asset for gas', async () => { - await findElementByTestIdAndClick({ - id: 'swap-flip-button', - driver, - }); - await delayTime('short'); - await typeOnTextInput({ - id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, - text: `\b10000`, - driver, - }); - await delayTime('very-long'); - const confirmButtonText = await getTextFromText({ - id: 'swap-confirmation-button', - driver, - }); - expect(confirmButtonText).toEqual('Insufficient ETH for gas'); - }); - - it('should be able to filter assets to buy by network', async () => { - // OP - await findElementByTestIdAndClick({ - id: `${DAI_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-2', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'op', - }); - await findElementByTestIdAndClick({ - id: `${OP_OPTIMISM_ID}-favorites-token-to-buy-row`, - driver, - }); - // POLYGON - await findElementByTestIdAndClick({ - id: `${OP_OPTIMISM_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-1', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'matic', - }); - await findElementByTestIdAndClick({ - id: `${MATIC_POLYGON_ID}-favorites-token-to-buy-row`, - driver, - }); - // ARBITRUM - await findElementByTestIdAndClick({ - id: `${MATIC_POLYGON_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-3', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'gmx', - }); - await findElementByTestIdAndClick({ - id: `${GMX_ARBITRUM_ID}-verified-token-to-buy-row`, - driver, - }); - // BNB - await findElementByTestIdAndClick({ - id: `${GMX_ARBITRUM_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-4', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'uni', - }); - await findElementByTestIdAndClick({ - id: `${UNI_BNB_ID}-verified-token-to-buy-row`, - driver, - }); - }); - - it('should be able to see no route explainer', async () => { - await findElementByTestIdAndClick({ - id: `${UNI_BNB_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-2', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'op', - }); - await findElementByTestIdAndClick({ - id: `${OP_OPTIMISM_ID}-favorites-token-to-buy-row`, - driver, - }); - - await findElementByTestIdAndClick({ - id: 'swap-flip-button', - driver, - }); - await delayTime('short'); - - await findElementByTestIdAndClick({ - id: `${ETH_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, - driver, - }); - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-3', - driver, - }); - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'gmx', - }); - await findElementByTestIdAndClick({ - id: `${GMX_ARBITRUM_ID}-verified-token-to-buy-row`, - driver, - }); - - await typeOnTextInput({ - id: `${OP_OPTIMISM_ID}-token-to-sell-swap-token-input-swap-input-mask`, - driver, - text: 1, - }); - - await delayTime('long'); - const confirmButtonText = await getTextFromText({ - id: 'swap-confirmation-button', - driver, - }); - expect(confirmButtonText).toEqual('No route found'); - - await findElementByTestIdAndClick({ - id: 'swap-confirmation-button', - driver, - }); - - const noRouteExplainer = await findElementByTestId({ - id: 'explainer-sheet-swap-no-route', - driver, - }); - expect(noRouteExplainer).toBeTruthy(); - await findElementByTestIdAndClick({ - id: 'explainer-action-button', - driver, - }); - }); - - it('should be able to find exact match on other networks', async () => { - await findElementByTestIdAndClick({ - id: `${OP_OPTIMISM_ID}-token-to-sell-token-input-remove`, - driver, - }); - await findElementByTestIdAndClick({ - id: `token-to-sell-search-token-input`, - driver, - }); - await findElementByTestIdAndClick({ - id: `${GMX_ARBITRUM_ID}-token-to-buy-token-input-remove`, - driver, - }); - - await findElementByTestIdAndClick({ - id: 'asset-to-buy-networks-trigger', - driver, - }); - await findElementByTestIdAndClick({ - id: 'switch-network-item-1', - driver, - }); - - await typeOnTextInput({ - id: 'token-to-buy-search-token-input', - driver, - text: 'optimism', - }); - - const onOtherNetworksSections = await findElementByTestId({ - id: 'other_networks-token-to-buy-section', - driver, - }); - - expect(onOtherNetworksSections).toBeTruthy(); - - await findElementByTestIdAndClick({ - id: `${OP_OPTIMISM_ID}-other_networks-token-to-buy-row`, - driver, - }); - }); -}); diff --git a/e2e/serial/swapFlow1.test.ts b/e2e/serial/swapFlow1.test.ts new file mode 100644 index 0000000000..17af50e36f --- /dev/null +++ b/e2e/serial/swapFlow1.test.ts @@ -0,0 +1,899 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable jest/expect-expect */ + +import 'chromedriver'; +import 'geckodriver'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { WebDriver } from 'selenium-webdriver'; +import { afterAll, beforeAll, expect, it } from 'vitest'; + +import { + delayTime, + doNotFindElementByTestId, + findElementAndClick, + findElementByTestId, + findElementByTestIdAndClick, + findElementByText, + getExtensionIdByName, + getTextFromText, + getTextFromTextInput, + goToPopup, + goToWelcome, + initDriverWithOptions, + querySelector, + typeOnTextInput, + waitAndClick, +} from '../helpers'; +import { convertRawAmountToDecimalFormat, subtract } from '../numbers'; + +let rootURL = 'chrome-extension://'; +let driver: WebDriver; + +const browser = process.env.BROWSER || 'chrome'; +const os = process.env.OS || 'mac'; + +const DAI_MAINNET_ID = '0x6b175474e89094c44da98b954eedeac495271d0f_1'; +const ZEROX_MAINNET_ID = '0xe41d2489571d322189246dafa5ebde1f4699f498_1'; +const ETH_MAINNET_ID = 'eth_1'; +const OP_OPTIMISM_ID = '0x4200000000000000000000000000000000000042_10'; +const MATIC_POLYGON_ID = '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0_137'; +const GMX_ARBITRUM_ID = '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a_42161'; +const UNI_BNB_ID = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984_56'; + +const TEST_ADDRESS_1 = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'; + +beforeAll(async () => { + driver = await initDriverWithOptions({ + browser, + os, + }); + const extensionId = await getExtensionIdByName(driver, 'Rainbow'); + if (!extensionId) throw new Error('Extension not found'); + rootURL += extensionId; +}); + +afterAll(async () => driver.quit()); + +it('should be able import a wallet via seed', async () => { + // Start from welcome screen + await goToWelcome(driver, rootURL); + await findElementByTestIdAndClick({ + id: 'import-wallet-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'import-wallet-option', + driver, + }); + + await typeOnTextInput({ + id: 'secret-textarea', + driver, + text: 'test test test test test test test test test test test junk', + }); + + await findElementByTestIdAndClick({ + id: 'import-wallets-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'add-wallets-button', + driver, + }); + await typeOnTextInput({ id: 'password-input', driver, text: 'test1234' }); + await typeOnTextInput({ + id: 'confirm-password-input', + driver, + text: 'test1234', + }); + await findElementByTestIdAndClick({ id: 'set-password-button', driver }); + await delayTime('long'); + await findElementByText(driver, 'Your wallets ready'); +}); + +it('should be able to go to setings', async () => { + await goToPopup(driver, rootURL); + await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); + await findElementByTestIdAndClick({ id: 'settings-link', driver }); +}); + +it('should be able to connect to hardhat and turn swaps flag on', async () => { + const btn = await querySelector(driver, '[data-testid="connect-to-hardhat"]'); + await waitAndClick(btn, driver); + const button = await findElementByText(driver, 'Disconnect from Hardhat'); + expect(button).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'feature-flag-swaps', driver }); + await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); +}); + +it('should be able to go to swap flow', async () => { + await findElementAndClick({ id: 'header-link-swap', driver }); +}); + +it('should be able to go to swap settings and check rows are visible', async () => { + await findElementByTestIdAndClick({ + id: 'swap-settings-navbar-button', + driver, + }); + const routeRow = await findElementByTestId({ + id: 'swap-settings-route-row', + driver, + }); + expect(routeRow).toBeTruthy(); + const slippageRow = await findElementByTestId({ + id: 'swap-settings-slippage-row', + driver, + }); + expect(slippageRow).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'swap-settings-done', + driver, + }); +}); + +it('should be able to go to settings and turn on flashbots', async () => { + await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); + await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); + await findElementByTestIdAndClick({ id: 'settings-link', driver }); + await findElementByTestIdAndClick({ id: 'settings-transactions', driver }); + await findElementByTestIdAndClick({ + id: 'flashbots-transactions-toggle', + driver, + }); + await findElementByTestIdAndClick({ + id: 'navbar-button-with-back', + driver, + }); + await findElementByTestIdAndClick({ + id: 'navbar-button-with-back', + driver, + }); + await findElementAndClick({ id: 'header-link-swap', driver }); +}); + +it('should be able to go to swap settings and check flashbots row is visible', async () => { + await findElementByTestIdAndClick({ + id: 'swap-settings-navbar-button', + driver, + }); + + const flashbotsRow = await findElementByTestId({ + id: 'swap-settings-flashbots-row', + driver, + }); + expect(flashbotsRow).toBeTruthy(); +}); + +it('should be able to interact with route settings', async () => { + await findElementByTestIdAndClick({ + id: 'swap-settings-route-label', + driver, + }); + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'settings-route-context-trigger-auto', + driver, + }); + await findElementByTestIdAndClick({ + id: 'settings-route-context-0x', + driver, + }); +}); + +it('should be able to interact with flashbots settings', async () => { + await findElementByTestIdAndClick({ + id: 'swap-settings-flashbots-label', + driver, + }); + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'swap-settings-flashbots-toggle', + driver, + }); + await findElementByTestIdAndClick({ + id: 'swap-settings-flashbots-toggle', + driver, + }); +}); + +it('should be able to interact with slippage settings', async () => { + await findElementByTestIdAndClick({ + id: 'swap-settings-slippage-label', + driver, + }); + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); + await typeOnTextInput({ + id: 'slippage-input-mask', + driver, + text: '\b4', + }); + + const warning = findElementByTestId({ + id: 'swap-settings-slippage-warning', + driver, + }); + expect(warning).toBeTruthy(); +}); + +it('should be able to set default values for settings and go back to swap', async () => { + await findElementByTestIdAndClick({ + id: 'settings-use-defaults-button', + driver, + }); + const routeTriggerAuto = await findElementByTestId({ + id: 'settings-route-context-trigger-auto', + driver, + }); + expect(routeTriggerAuto).toBeTruthy(); + const text = await getTextFromTextInput({ + id: 'slippage-input-mask', + driver, + }); + expect(text).toBe('1'); + await findElementByTestIdAndClick({ id: 'swap-settings-done', driver }); +}); + +it('should be able to open token to sell input and select assets', async () => { + await findElementByTestIdAndClick({ + id: 'token-to-sell-search-token-input', + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-sell-sort-trigger', + driver, + }); + + const sortByBalance = await findElementByTestId({ + id: 'token-to-sell-sort-balance', + driver, + }); + expect(sortByBalance).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'token-to-sell-sort-network', + driver, + }); + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-row`, + driver, + }); + const toSellInputEthSelected = await findElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(toSellInputEthSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'swap-flip-button', + driver, + }); + const toBuyInputEthSelected = await findElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(toBuyInputEthSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'swap-flip-button', + driver, + }); +}); + +it('should be able to open press max on token to sell input', async () => { + const fiatValueText = await getTextFromText({ + id: 'token-to-sell-info-fiat-value', + driver, + }); + expect(fiatValueText).toBe('$0.00'); + await findElementByTestIdAndClick({ + id: 'token-to-sell-info-max-button', + driver, + }); + const ethValueBeforeGas = await getTextFromTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(ethValueBeforeGas).toEqual('10000'); + const fiatValueTextAfterMax = await getTextFromText({ + id: 'token-to-sell-info-fiat-value', + driver, + }); + expect(fiatValueTextAfterMax).not.toEqual('$0.00'); +}); + +it('should be able to remove token to sell and select it again', async () => { + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-row`, + driver, + }); + const toSellInputEthSelected = await findElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(toSellInputEthSelected).toBeTruthy(); + // should clear input value + const ethValueAfterSelection = await getTextFromTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(ethValueAfterSelection).toEqual(''); +}); + +it('should be able to open token to buy input and select assets', async () => { + await findElementByTestIdAndClick({ + id: 'token-to-buy-search-token-input', + driver, + }); + // check sell asset is not present as buy option + const elementFound = await doNotFindElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-buy-row`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-favorites-token-to-buy-row`, + driver, + }); + expect(elementFound).toBeFalsy(); + const toBuyInputDaiSelected = await findElementByTestId({ + id: `${DAI_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(toBuyInputDaiSelected).toBeTruthy(); +}); + +it('should be able to open remove token to buy and check favorites and verified lists are visible', async () => { + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-token-to-buy-token-input-remove`, + driver, + }); + const favoritesSection = await findElementByTestId({ + id: 'favorites-token-to-buy-section', + driver, + }); + expect(favoritesSection).toBeTruthy(); + const verifiedSection = await findElementByTestId({ + id: 'verified-token-to-buy-section', + driver, + }); + expect(verifiedSection).toBeTruthy(); +}); + +it('should be able to favorite a token and check the info button is present', async () => { + await findElementByTestIdAndClick({ + id: `${ZEROX_MAINNET_ID}-verified-token-to-buy-row-favorite-button`, + driver, + }); + await delayTime('short'); + await findElementByTestIdAndClick({ + id: `${ZEROX_MAINNET_ID}-favorites-token-to-buy-row-info-button`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${ZEROX_MAINNET_ID}-favorites-token-to-buy-row-info-button-copy`, + driver, + }); +}); + +it('should be able to check price and balance of token to buy', async () => { + const tokenToBuyInfoPrice = await getTextFromText({ + id: 'token-to-buy-info-price', + driver, + }); + expect(tokenToBuyInfoPrice).not.toBe(''); + const tokenToBuyInfoBalance = await getTextFromText({ + id: 'token-to-buy-info-balance', + driver, + }); + expect(tokenToBuyInfoBalance).not.toBe(''); +}); + +it('should be able to flip correctly', async () => { + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + await typeOnTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + text: 1, + driver, + }); + const assetToSellInputText = await getTextFromTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(assetToSellInputText).toBe('1'); + + await delayTime('very-long'); + + const assetToBuyInputText = await getTextFromTextInput({ + id: `${ZEROX_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(assetToBuyInputText).not.toBe(''); + + await findElementByTestIdAndClick({ + id: 'swap-flip-button', + driver, + }); + + await delayTime('long'); + + const assetToSellInputTextAfterMax = await getTextFromTextInput({ + id: `${ZEROX_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + + expect(assetToSellInputTextAfterMax).not.toEqual(''); + + const assetToBuyInputTextAfterMax = await getTextFromTextInput({ + id: `${ETH_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(assetToBuyInputTextAfterMax).toEqual('1'); +}); + +it('should be able to check insufficient asset for swap', async () => { + const confirmButtonText = await getTextFromText({ + id: 'swap-confirmation-button', + driver, + }); + expect(confirmButtonText).toEqual('Insufficient ZRX'); +}); + +it('should be able to check insufficient native asset for gas', async () => { + await findElementByTestIdAndClick({ + id: 'swap-flip-button', + driver, + }); + await delayTime('short'); + await typeOnTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + text: `\b10000`, + driver, + }); + await delayTime('very-long'); + const confirmButtonText = await getTextFromText({ + id: 'swap-confirmation-button', + driver, + }); + expect(confirmButtonText).toEqual('Insufficient ETH for gas'); +}); + +it('should be able to see small market warning', async () => { + const swapWarning = await findElementByTestId({ + id: 'swap-warning-price-impact', + driver, + }); + expect(swapWarning).toBeTruthy(); +}); + +it('should be able to filter assets to buy by network', async () => { + // OP + await findElementByTestIdAndClick({ + id: `${ZEROX_MAINNET_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-2', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'op', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-favorites-token-to-buy-row`, + driver, + }); + // POLYGON + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-1', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'matic', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${MATIC_POLYGON_ID}-favorites-token-to-buy-row`, + driver, + }); + // ARBITRUM + await findElementByTestIdAndClick({ + id: `${MATIC_POLYGON_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-3', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'gmx', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${GMX_ARBITRUM_ID}-verified-token-to-buy-row`, + driver, + }); + // BNB + await findElementByTestIdAndClick({ + id: `${GMX_ARBITRUM_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-4', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'uni', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${UNI_BNB_ID}-verified-token-to-buy-row`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${UNI_BNB_ID}-token-to-buy-token-input-remove`, + driver, + }); +}); + +it('should be able to see no route explainer', async () => { + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-2', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'op', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-favorites-token-to-buy-row`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'swap-flip-button', + driver, + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-3', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'gmx', + }); + await delayTime('long'); + await findElementByTestIdAndClick({ + id: `${GMX_ARBITRUM_ID}-verified-token-to-buy-row`, + driver, + }); + await typeOnTextInput({ + id: `${OP_OPTIMISM_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + text: 1, + }); + await delayTime('long'); + const confirmButtonText = await getTextFromText({ + id: 'swap-confirmation-button', + driver, + }); + expect(confirmButtonText).toEqual('No route found'); + await findElementByTestIdAndClick({ + id: 'swap-confirmation-button', + driver, + }); + const noRouteExplainer = await findElementByTestId({ + id: 'explainer-sheet-swap-no-route', + driver, + }); + expect(noRouteExplainer).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); +}); + +it('should be able to find exact match on other networks', async () => { + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-token-to-sell-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: `token-to-sell-search-token-input`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${GMX_ARBITRUM_ID}-token-to-buy-token-input-remove`, + driver, + }); + + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-1', + driver, + }); + + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'optimism', + }); + await delayTime('long'); + + const onOtherNetworksSections = await findElementByTestId({ + id: 'other_networks-token-to-buy-section', + driver, + }); + + expect(onOtherNetworksSections).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-other_networks-token-to-buy-row`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${OP_OPTIMISM_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-search-token-input', + driver, + }); +}); + +it('should be able to go to review a swap', async () => { + await findElementByTestIdAndClick({ + id: 'token-to-sell-search-token-input', + driver, + }); + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-row`, + driver, + }); + const toSellInputEthSelected = await findElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(toSellInputEthSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'token-to-buy-search-token-input', + driver, + }); + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-favorites-token-to-buy-row`, + driver, + }); + const toBuyInputDaiSelected = await findElementByTestId({ + id: `${DAI_MAINNET_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(toBuyInputDaiSelected).toBeTruthy(); + await typeOnTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + text: 1, + driver, + }); + await delayTime('very-long'); + await findElementByTestIdAndClick({ id: 'swap-confirmation-button', driver }); +}); + +it('should be able to see swap information in review sheet', async () => { + const ethAssetToSellAssetCard = await findElementByTestId({ + id: `ETH-asset-to-sell-swap-asset-card`, + driver, + }); + expect(ethAssetToSellAssetCard).toBeTruthy(); + const daiAssetToBuyAssetCard = await findElementByTestId({ + id: `DAI-asset-to-buy-swap-asset-card`, + driver, + }); + expect(daiAssetToBuyAssetCard).toBeTruthy(); + const minimumReceivedDetailsRow = await findElementByTestId({ + id: `minimum-received-details-row`, + driver, + }); + expect(minimumReceivedDetailsRow).toBeTruthy(); + const swappingViaDetailsRow = await findElementByTestId({ + id: `swapping-via-details-row`, + driver, + }); + expect(swappingViaDetailsRow).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + + const includedFeeDetailsRow = await findElementByTestId({ + id: `included-fee-details-row`, + driver, + }); + expect(includedFeeDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'included-fee-carrousel-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'included-fee-carrousel-button', + driver, + }); + + await findElementByTestIdAndClick({ + id: 'swap-review-rnbw-fee-info-button', + driver, + }); + await findElementByTestIdAndClick({ id: 'explainer-action-button', driver }); + + const moreDetailsHiddendDetailsRow = await findElementByTestId({ + id: `more-details-hidden-details-row`, + driver, + }); + expect(moreDetailsHiddendDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'swap-review-more-details-button', + driver, + }); + + const moreDetailsdSection = await findElementByTestId({ + id: `more-details-section`, + driver, + }); + expect(moreDetailsdSection).toBeTruthy(); + + const exchangeRateDetailsRow = await findElementByTestId({ + id: `exchange-rate-details-row`, + driver, + }); + expect(exchangeRateDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'exchange-rate-carrousel-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'exchange-rate-carrousel-button', + driver, + }); + + // ETH is selected as input so there's no contract + await doNotFindElementByTestId({ + id: `asset-to-sell-contract-details-row`, + driver, + }); + + const assetToBuyContractDetailsRow = await findElementByTestId({ + id: `asset-to-buy-contract-details-row`, + driver, + }); + expect(assetToBuyContractDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'asset-to-buy-swap-view-contract-dropdown', + driver, + }); + const assetToSellContractDropdiwnView = await findElementByTestId({ + id: 'asset-to-buy-view-swap-view-contract-dropdown', + driver, + }); + expect(assetToSellContractDropdiwnView).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'asset-to-buy-copy-swap-view-contract-dropdown', + driver, + }); + + const swapReviewConfirmationText = await getTextFromText({ + id: 'swap-review-confirmation-text', + driver, + }); + expect(swapReviewConfirmationText).toBe('Swap ETH to DAI'); + + const swapReviewTitleText = await getTextFromText({ + id: 'swap-review-title-text', + driver, + }); + expect(swapReviewTitleText).toBe('Review & Swap'); +}); + +it('should be able to execute swap', async () => { + const provider = new StaticJsonRpcProvider('http://127.0.0.1:8545'); + await provider.ready; + await delayTime('short'); + + await findElementByTestIdAndClick({ + id: 'navbar-button-with-back-swap-review', + driver, + }); + await delayTime('short'); + + await findElementByTestIdAndClick({ + id: 'swap-settings-navbar-button', + driver, + }); + await delayTime('short'); + + await typeOnTextInput({ + id: 'slippage-input-mask', + driver, + text: '\b99', + }); + await delayTime('medium'); + + await findElementByTestIdAndClick({ id: 'swap-settings-done', driver }); + + const ethBalanceBeforeSwap = await provider.getBalance(TEST_ADDRESS_1); + await delayTime('very-long'); + await findElementByTestIdAndClick({ id: 'swap-confirmation-button', driver }); + await delayTime('medium'); + await findElementByTestIdAndClick({ id: 'swap-review-execute', driver }); + await delayTime('very-long'); + const ethBalanceAfterSwap = await provider.getBalance(TEST_ADDRESS_1); + const balanceDifference = subtract( + ethBalanceBeforeSwap.toString(), + ethBalanceAfterSwap.toString(), + ); + const ethDifferenceAmount = convertRawAmountToDecimalFormat( + balanceDifference, + 18, + ); + + expect(Number(ethDifferenceAmount)).toBeGreaterThan(1); +}); diff --git a/e2e/serial/swapFlow2.test.ts b/e2e/serial/swapFlow2.test.ts new file mode 100644 index 0000000000..3c67f93e70 --- /dev/null +++ b/e2e/serial/swapFlow2.test.ts @@ -0,0 +1,548 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable jest/expect-expect */ + +import 'chromedriver'; +import 'geckodriver'; +import { Contract } from '@ethersproject/contracts'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { WebDriver } from 'selenium-webdriver'; +import { afterAll, beforeAll, expect, it } from 'vitest'; +import { erc20ABI } from 'wagmi'; + +import { + delayTime, + doNotFindElementByTestId, + findElementAndClick, + findElementByTestId, + findElementByTestIdAndClick, + findElementByText, + getExtensionIdByName, + getTextFromText, + goToPopup, + goToWelcome, + initDriverWithOptions, + querySelector, + typeOnTextInput, + waitAndClick, +} from '../helpers'; +import { convertRawAmountToDecimalFormat, subtract } from '../numbers'; + +let rootURL = 'chrome-extension://'; +let driver: WebDriver; + +const browser = process.env.BROWSER || 'chrome'; +const os = process.env.OS || 'mac'; + +const DAI_MAINNET_ID = '0x6b175474e89094c44da98b954eedeac495271d0f_1'; +const DAI_ARBITRUM_ID = '0x6b175474e89094c44da98b954eedeac495271d0f_42161'; +const ETH_MAINNET_ID = 'eth_1'; +const ETH_OPTIMISM_ID = 'eth_10'; +const USDC_ARBITRUM_ID = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48_42161'; +const USDC_MAINNET_ID = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48_1'; +const DAI_MAINNET_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const TEST_ADDRESS_1 = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'; + +beforeAll(async () => { + driver = await initDriverWithOptions({ + browser, + os, + }); + const extensionId = await getExtensionIdByName(driver, 'Rainbow'); + if (!extensionId) throw new Error('Extension not found'); + rootURL += extensionId; +}); + +afterAll(async () => driver.quit()); + +it('should be able import a wallet via seed', async () => { + // Start from welcome screen + await goToWelcome(driver, rootURL); + await findElementByTestIdAndClick({ + id: 'import-wallet-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'import-wallet-option', + driver, + }); + + await typeOnTextInput({ + id: 'secret-textarea', + driver, + text: 'test test test test test test test test test test test junk', + }); + + await findElementByTestIdAndClick({ + id: 'import-wallets-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'add-wallets-button', + driver, + }); + await typeOnTextInput({ id: 'password-input', driver, text: 'test1234' }); + await typeOnTextInput({ + id: 'confirm-password-input', + driver, + text: 'test1234', + }); + await findElementByTestIdAndClick({ id: 'set-password-button', driver }); + await delayTime('long'); + await findElementByText(driver, 'Your wallets ready'); +}); + +it('should be able to go to setings', async () => { + await goToPopup(driver, rootURL); + await findElementByTestIdAndClick({ id: 'home-page-header-right', driver }); + await findElementByTestIdAndClick({ id: 'settings-link', driver }); +}); + +it('should be able to connect to hardhat and turn swaps flag on', async () => { + const btn = await querySelector(driver, '[data-testid="connect-to-hardhat"]'); + await waitAndClick(btn, driver); + const button = await findElementByText(driver, 'Disconnect from Hardhat'); + expect(button).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'feature-flag-swaps', driver }); + await findElementByTestIdAndClick({ id: 'navbar-button-with-back', driver }); +}); + +it('should be able to go to swap flow', async () => { + await findElementAndClick({ id: 'header-link-swap', driver }); + await delayTime('long'); +}); + +it('should be able to go to review a unlock and swap', async () => { + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-token-to-sell-row`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-search-token-input', + driver, + }); + await findElementByTestIdAndClick({ + id: `${USDC_MAINNET_ID}-favorites-token-to-buy-row`, + driver, + }); + await typeOnTextInput({ + id: `${DAI_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + text: `\b50`, + driver, + }); + await delayTime('long'); +}); + +it('should be able to execute unlock and swap', async () => { + const provider = new StaticJsonRpcProvider('http://127.0.0.1:8545'); + await provider.ready; + await delayTime('short'); + const tokenContract = new Contract(DAI_MAINNET_ADDRESS, erc20ABI, provider); + const daiBalanceBeforeSwap = await tokenContract.balanceOf(TEST_ADDRESS_1); + + await findElementByTestIdAndClick({ id: 'swap-confirmation-button', driver }); + await delayTime('long'); + await findElementByTestIdAndClick({ id: 'swap-review-execute', driver }); + await delayTime('long'); + const daiBalanceAfterSwap = await tokenContract.balanceOf(TEST_ADDRESS_1); + const balanceDifference = subtract( + daiBalanceBeforeSwap.toString(), + daiBalanceAfterSwap.toString(), + ); + const daiBalanceDifference = convertRawAmountToDecimalFormat( + balanceDifference.toString(), + 18, + ); + + expect(Number(daiBalanceDifference)).toBe(50); +}); + +it('should be able to go to swap flow', async () => { + await findElementAndClick({ id: 'header-link-swap', driver }); + await delayTime('long'); +}); + +it('should be able to go to review a crosschain swap', async () => { + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-token-to-sell-row`, + driver, + }); + await delayTime('medium'); + const toSellInputDaiSelected = await findElementByTestId({ + id: `${DAI_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(toSellInputDaiSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'token-to-buy-search-token-input', + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-3', + driver, + }); + const daiBridge = await findElementByTestId({ + id: `${DAI_ARBITRUM_ID}-bridge-token-to-buy-row`, + driver, + }); + expect(daiBridge).toBeTruthy(); + + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'USDC', + }); + await findElementByTestIdAndClick({ + id: `${USDC_ARBITRUM_ID}-favorites-token-to-buy-row`, + driver, + }); + const toBuyInputUsdcSelected = await findElementByTestId({ + id: `${USDC_ARBITRUM_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(toBuyInputUsdcSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'token-to-sell-info-max-button', + driver, + }); + await delayTime('very-long'); + + await findElementByTestIdAndClick({ + id: 'swap-confirmation-button', + driver, + }); + + await delayTime('medium'); + const longWaitExplainerFound = await doNotFindElementByTestId({ + id: 'explainer-sheet-swap-long-wait', + driver, + }); + + if (longWaitExplainerFound) { + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); + } + await delayTime('long'); +}); + +it('should be able to see crosschain swap information in review sheet', async () => { + const daiAssetToSellAssetCard = await findElementByTestId({ + id: `DAI-asset-to-sell-swap-asset-card`, + driver, + }); + expect(daiAssetToSellAssetCard).toBeTruthy(); + const usdcAssetToBuyAssetCard = await findElementByTestId({ + id: `USDC-asset-to-buy-swap-asset-card`, + driver, + }); + expect(usdcAssetToBuyAssetCard).toBeTruthy(); + const minimumReceivedDetailsRow = await findElementByTestId({ + id: `minimum-received-details-row`, + driver, + }); + expect(minimumReceivedDetailsRow).toBeTruthy(); + const swappingViaDetailsRow = await findElementByTestId({ + id: `swapping-via-details-row`, + driver, + }); + expect(swappingViaDetailsRow).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + + const includedFeeDetailsRow = await findElementByTestId({ + id: `included-fee-details-row`, + driver, + }); + expect(includedFeeDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'included-fee-carrousel-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'included-fee-carrousel-button', + driver, + }); + + await findElementByTestIdAndClick({ + id: 'swap-review-rnbw-fee-info-button', + driver, + }); + await findElementByTestIdAndClick({ id: 'explainer-action-button', driver }); + + const moreDetailsHiddendDetailsRow = await findElementByTestId({ + id: `more-details-hidden-details-row`, + driver, + }); + expect(moreDetailsHiddendDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'swap-review-more-details-button', + driver, + }); + + const moreDetailsdSection = await findElementByTestId({ + id: `more-details-section`, + driver, + }); + expect(moreDetailsdSection).toBeTruthy(); + + const exchangeRateDetailsRow = await findElementByTestId({ + id: `exchange-rate-details-row`, + driver, + }); + expect(exchangeRateDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'exchange-rate-carrousel-button', + driver, + }); + await findElementByTestIdAndClick({ + id: 'exchange-rate-carrousel-button', + driver, + }); + + const assetToSellContractDetailsRow = await findElementByTestId({ + id: `asset-to-sell-contract-details-row`, + driver, + }); + expect(assetToSellContractDetailsRow).toBeTruthy(); + + const assetToBuyContractDetailsRow = await findElementByTestId({ + id: `asset-to-buy-contract-details-row`, + driver, + }); + expect(assetToBuyContractDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'asset-to-sell-swap-view-contract-dropdown', + driver, + }); + const assetToSellContractDropdownView = await findElementByTestId({ + id: 'asset-to-sell-view-swap-view-contract-dropdown', + driver, + }); + expect(assetToSellContractDropdownView).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'asset-to-sell-copy-swap-view-contract-dropdown', + driver, + }); + + await findElementByTestIdAndClick({ + id: 'asset-to-buy-swap-view-contract-dropdown', + driver, + }); + const assetToBuyContractDropdownView = await findElementByTestId({ + id: 'asset-to-buy-view-swap-view-contract-dropdown', + driver, + }); + expect(assetToBuyContractDropdownView).toBeTruthy(); + await findElementByTestIdAndClick({ + id: 'asset-to-buy-copy-swap-view-contract-dropdown', + driver, + }); + + const swapReviewConfirmationText = await getTextFromText({ + id: 'swap-review-confirmation-text', + driver, + }); + expect(swapReviewConfirmationText).toBe('Swap DAI to USDC'); + + const swapReviewTitleText = await getTextFromText({ + id: 'swap-review-title-text', + driver, + }); + expect(swapReviewTitleText).toBe('Review & Swap'); + + await findElementByTestIdAndClick({ + id: 'navbar-button-with-back-swap-review', + driver, + }); + await delayTime('long'); +}); + +it('should be able to go to review a bridge', async () => { + await findElementByTestIdAndClick({ + id: `${DAI_MAINNET_ID}-token-to-sell-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: `${ETH_MAINNET_ID}-token-to-sell-row`, + driver, + }); + await delayTime('medium'); + const toSellInputEthSelected = await findElementByTestId({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + driver, + }); + expect(toSellInputEthSelected).toBeTruthy(); + await findElementByTestIdAndClick({ + id: `${USDC_ARBITRUM_ID}-token-to-buy-token-input-remove`, + driver, + }); + await findElementByTestIdAndClick({ + id: 'token-to-buy-networks-trigger', + driver, + }); + await findElementByTestIdAndClick({ + id: 'switch-network-item-2', + driver, + }); + await typeOnTextInput({ + id: 'token-to-buy-search-token-input', + driver, + text: 'eth', + }); + await findElementByTestIdAndClick({ + id: `${ETH_OPTIMISM_ID}-bridge-token-to-buy-row`, + driver, + }); + const toBuyInputEthSelected = await findElementByTestId({ + id: `${ETH_OPTIMISM_ID}-token-to-buy-swap-token-input-swap-input-mask`, + driver, + }); + expect(toBuyInputEthSelected).toBeTruthy(); + await typeOnTextInput({ + id: `${ETH_MAINNET_ID}-token-to-sell-swap-token-input-swap-input-mask`, + text: 1, + driver, + }); + + await delayTime('very-long'); + + await findElementByTestIdAndClick({ + id: 'swap-confirmation-button', + driver, + }); + + const longWaitExplainerFound = await doNotFindElementByTestId({ + id: 'explainer-sheet-swap-long-wait', + driver, + }); + + if (longWaitExplainerFound) { + await findElementByTestIdAndClick({ + id: 'explainer-action-button', + driver, + }); + } + + await delayTime('long'); +}); + +it('should be able to see bridge information in review sheet', async () => { + const ethAssetToSellAssetCard = await findElementByTestId({ + id: `ETH-asset-to-sell-swap-asset-card`, + driver, + }); + expect(ethAssetToSellAssetCard).toBeTruthy(); + const ethAssetToBuyAssetCard = await findElementByTestId({ + id: `ETH-asset-to-buy-swap-asset-card`, + driver, + }); + expect(ethAssetToBuyAssetCard).toBeTruthy(); + const minimumReceivedDetailsRow = await findElementByTestId({ + id: `minimum-received-details-row`, + driver, + }); + expect(minimumReceivedDetailsRow).toBeTruthy(); + const swappingViaDetailsRow = await findElementByTestId({ + id: `swapping-via-details-row`, + driver, + }); + expect(swappingViaDetailsRow).toBeTruthy(); + await findElementByTestIdAndClick({ id: 'swapping-via-swap-routes', driver }); + + const includedFeeDetailsRow = await findElementByTestId({ + id: `included-fee-details-row`, + driver, + }); + expect(includedFeeDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'included-fee-carrousel-button', + driver, + }); + + await findElementByTestIdAndClick({ + id: 'swap-review-rnbw-fee-info-button', + driver, + }); + await findElementByTestIdAndClick({ id: 'explainer-action-button', driver }); + + // const flashbotsEnabledDetailsRow = await findElementByTestId({ + // id: `flashbots-enabled-details-row`, + // driver, + // }); + // expect(flashbotsEnabledDetailsRow).toBeTruthy(); + // await findElementByTestIdAndClick({ + // id: 'swap-review-flashbots-info-button', + // driver, + // }); + // await findElementByTestIdAndClick({ id: 'explainer-action-button', driver }); + + const moreDetailsHiddendDetailsRow = await findElementByTestId({ + id: `more-details-hidden-details-row`, + driver, + }); + expect(moreDetailsHiddendDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'swap-review-more-details-button', + driver, + }); + + const moreDetailsdSection = await findElementByTestId({ + id: `more-details-section`, + driver, + }); + expect(moreDetailsdSection).toBeTruthy(); + + const exchangeRateDetailsRow = await findElementByTestId({ + id: `exchange-rate-details-row`, + driver, + }); + expect(exchangeRateDetailsRow).toBeTruthy(); + + await findElementByTestIdAndClick({ + id: 'exchange-rate-carrousel-button', + driver, + }); + + const assetToSellContractRow = await doNotFindElementByTestId({ + id: `asset-to-sell-contract-details-row`, + driver, + }); + expect(assetToSellContractRow).toBeFalsy(); + + const assetToBuyContractRow = await doNotFindElementByTestId({ + id: `asset-to-buy-contract-details-row`, + driver, + }); + expect(assetToBuyContractRow).toBeFalsy(); + + const swapReviewConfirmationText = await getTextFromText({ + id: 'swap-review-confirmation-text', + driver, + }); + expect(swapReviewConfirmationText).toBe('Bridge ETH'); + + const swapReviewTitleText = await getTextFromText({ + id: 'swap-review-title-text', + driver, + }); + expect(swapReviewTitleText).toBe('Review & Bridge'); + + await findElementByTestIdAndClick({ + id: 'navbar-button-with-back-swap-review', + driver, + }); + await delayTime('very-long'); +}); diff --git a/package.json b/package.json index a812499347..9241d2b51f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "// Build and zip": "", "bundle": "yarn build && yarn zip", "// Runs tests": "", - "anvil": "ALCHEMY_API_KEY=$(grep ALCHEMY_API_KEY .env | cut -d '=' -f2) && anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/$ALCHEMY_API_KEY --fork-block-number 16350936", + "anvil": "ALCHEMY_API_KEY=$(grep ALCHEMY_API_KEY .env | cut -d '=' -f2) && anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/$ALCHEMY_API_KEY", "anvil:kill": "lsof -i :8545|tail -n +2|awk '{print $2}'|xargs -r kill -s SIGINT", "test": "./scripts/unit-tests.sh", "test:watch": "yarn anvil & vitest --watch", diff --git a/src/core/languages/_english.json b/src/core/languages/_english.json index 82b4468e92..12e44dec75 100644 --- a/src/core/languages/_english.json +++ b/src/core/languages/_english.json @@ -320,6 +320,7 @@ "asset_contract": "%{symbol} contract", "more_details": "More details", "swap_confirmation": "Swap %{sellSymbol} to %{buySymbol}", + "bridge_confirmation": "Bridge %{sellSymbol}", "go_back": "Go back" }, "explainers": { diff --git a/src/core/resources/assets/userAssetsByChain.ts b/src/core/resources/assets/userAssetsByChain.ts index e0311341ea..9b8d4d0fa2 100644 --- a/src/core/resources/assets/userAssetsByChain.ts +++ b/src/core/resources/assets/userAssetsByChain.ts @@ -1,6 +1,6 @@ import { Contract } from '@ethersproject/contracts'; import { useQuery } from '@tanstack/react-query'; -import { getProvider } from '@wagmi/core'; +import { Provider, getProvider } from '@wagmi/core'; import { Address, erc20ABI } from 'wagmi'; import { refractionAddressWs } from '~/core/network'; @@ -24,6 +24,36 @@ import { import { greaterThan } from '~/core/utils/numbers'; import { ETH_MAINNET_ASSET } from '~/test/utils'; +const fetchAssetBalanceViaProvider = async ({ + parsedAsset, + currentAddress, + currency, + provider, +}: { + parsedAsset: ParsedAddressAsset; + currentAddress: Address; + currency: SupportedCurrencyKey; + provider: Provider; +}) => { + if (parsedAsset.isNativeAsset) { + const balance = await provider.getBalance(currentAddress); + const updatedAsset = parseParsedAddressAsset({ + parsedAsset, + currency, + quantity: balance.toString(), + }); + return updatedAsset; + } else { + const contract = new Contract(parsedAsset.address, erc20ABI, provider); + const balance = await contract.balanceOf(currentAddress); + const updatedAsset = parseParsedAddressAsset({ + parsedAsset, + currency, + quantity: balance.toString(), + }); + return updatedAsset; + } +}; const USER_ASSETS_TIMEOUT_DURATION = 10000; const USER_ASSETS_REFETCH_INTERVAL = 60000; @@ -113,42 +143,40 @@ export async function userAssetsByChainQueryFunction({ const resolver = async (message: AddressAssetsReceivedMessage) => { clearTimeout(timeout); - const parsedUserAssetsByChain = parseUserAssetsByChain(message, currency); + const parsedUserAssetsByUniqueId = parseUserAssetsByChain( + message, + currency, + ); if (connectedToHardhat && chain === ChainName.mainnet) { const provider = getProvider({ chainId: ChainId.hardhat }); // force checking for ETH if connected to hardhat - parsedUserAssetsByChain[ETH_MAINNET_ASSET.uniqueId] = ETH_MAINNET_ASSET; - Object.values(parsedUserAssetsByChain).forEach(async (parsedAsset) => { - if (parsedAsset.chainId !== ChainId.mainnet) return parsedAsset; - try { - if (parsedAsset.isNativeAsset) { - const balance = await provider.getBalance(currentAddress); - const updatedAsset = parseParsedAddressAsset({ + parsedUserAssetsByUniqueId[ETH_MAINNET_ASSET.uniqueId] = + ETH_MAINNET_ASSET; + const pasePromises = Object.values(parsedUserAssetsByUniqueId).map( + async (parsedAsset) => { + if (parsedAsset.chainId !== ChainId.mainnet) return parsedAsset; + try { + const pa = await fetchAssetBalanceViaProvider({ parsedAsset, + currentAddress, currency, - quantity: balance.toString(), - }); - parsedUserAssetsByChain[parsedAsset.uniqueId] = updatedAsset; - } else { - const contract = new Contract( - parsedAsset.address, - erc20ABI, provider, - ); - const balance = await contract.balanceOf(currentAddress); - const updatedAsset = parseParsedAddressAsset({ - parsedAsset, - currency, - quantity: balance.toString(), }); - parsedUserAssetsByChain[parsedAsset.uniqueId] = updatedAsset; + return pa; + } catch (e) { + return parsedAsset; } - } catch (e) { - return parsedAsset; - } - }); + }, + ); + const newParsedUserAssetsByUniqueId = await Promise.all(pasePromises); + const a: Record = {}; + newParsedUserAssetsByUniqueId.forEach( + (parseAsset) => (a[parseAsset.uniqueId] = parseAsset), + ); + resolve(a); + } else { + resolve(parsedUserAssetsByUniqueId); } - resolve(parsedUserAssetsByChain); }; refractionAddressWs.once(event, resolver); }); diff --git a/src/core/utils/assets.ts b/src/core/utils/assets.ts index c432918491..fca184d377 100644 --- a/src/core/utils/assets.ts +++ b/src/core/utils/assets.ts @@ -191,7 +191,7 @@ export const parseSearchAsset = ({ amount: '0', display: '0.00', }, - price: userAsset?.native?.price || assetWithPrice?.native.price, + price: assetWithPrice?.native.price || userAsset?.native?.price, }, price: assetWithPrice?.price || userAsset?.price, balance: userAsset?.balance || { amount: '0', display: '0.00' }, diff --git a/src/core/utils/gas.ts b/src/core/utils/gas.ts index 5eb5ee61f2..7ff7112a4b 100644 --- a/src/core/utils/gas.ts +++ b/src/core/utils/gas.ts @@ -638,3 +638,6 @@ export const getBaseFeeTrendParams = (trend: number) => { }; } }; + +export const chainShouldUseDefaultTxSpeed = (chainId: ChainId) => + chainId === ChainId.mainnet || chainId === ChainId.polygon; diff --git a/src/entries/popup/components/Navbar/Navbar.tsx b/src/entries/popup/components/Navbar/Navbar.tsx index 606cc1650a..c0b56c6145 100644 --- a/src/entries/popup/components/Navbar/Navbar.tsx +++ b/src/entries/popup/components/Navbar/Navbar.tsx @@ -12,6 +12,7 @@ type NavbarProps = { leftComponent?: React.ReactElement; rightComponent?: React.ReactElement; title?: string; + titleTestId?: string; titleComponent?: React.ReactElement; background?: BackgroundColor; }; @@ -20,6 +21,7 @@ export function Navbar({ leftComponent, rightComponent, title, + titleTestId, titleComponent, background, }: NavbarProps) { @@ -54,7 +56,7 @@ export function Navbar({ )} {title ? ( - + {title} @@ -147,12 +149,14 @@ function NavbarButtonWithBack({ onClick, symbol, symbolSize, + testId, }: { backTo?: To; height: ButtonSymbolProps['height']; onClick?: () => void; symbol: SymbolProps['symbol']; symbolSize?: SymbolProps['size']; + testId?: string; }) { const location = useLocation(); const navigate = useNavigate(); @@ -170,7 +174,10 @@ function NavbarButtonWithBack({ }, [backTo, location.pathname, navigate, onClick]); return ( - + void; + testId?: string; }) { return ( ); } diff --git a/src/entries/popup/components/TransactionFee/CustomGasSheet.tsx b/src/entries/popup/components/TransactionFee/CustomGasSheet.tsx index 11ee3afa67..f7aab0f135 100644 --- a/src/entries/popup/components/TransactionFee/CustomGasSheet.tsx +++ b/src/entries/popup/components/TransactionFee/CustomGasSheet.tsx @@ -34,6 +34,7 @@ import { SymbolStyles, TextStyles } from '~/design-system/styles/core.css'; import { SymbolName } from '~/design-system/styles/designTokens'; import usePrevious from '../../hooks/usePrevious'; +import { zIndexes } from '../../utils/zIndexes'; import { ExplainerSheet, useExplainerSheetParams, @@ -411,6 +412,7 @@ export const CustomGasSheet = ({ padding="16px" backdropFilter="blur(26px)" scrimBackground + zIndex={zIndexes.CUSTOM_GAS_SHEET} > diff --git a/src/entries/popup/components/TransactionFee/TransactionFee.tsx b/src/entries/popup/components/TransactionFee/TransactionFee.tsx index 1c60d88270..7bfbab301a 100644 --- a/src/entries/popup/components/TransactionFee/TransactionFee.tsx +++ b/src/entries/popup/components/TransactionFee/TransactionFee.tsx @@ -280,6 +280,7 @@ export function SwapFee({ assetToBuy, enabled, }); + return ( { }); }, [assetToSell, assetToSellWithPrice, userAssets]); - const setAssetToSell = useCallback((asset: ParsedSearchAsset | null) => { - setAssetToSellState(asset); - asset?.chainId && setOutputChainId(asset?.chainId); - }, []); - - // if user selects assetToBuy as assetToSell we need to flip assets - useEffect(() => { - if ( - assetToBuy?.address === assetToSell?.address && - assetToBuy?.chainId === assetToSell?.chainId - ) { - setAssetToBuy(prevAssetToSell === undefined ? null : prevAssetToSell); - } - }, [ - assetToBuy?.address, - assetToBuy?.chainId, - assetToSell?.address, - assetToSell?.chainId, - assetToSell?.uniqueId, - prevAssetToSell, - setAssetToBuy, - ]); + const setAssetToSell = useCallback( + (asset: ParsedSearchAsset | null) => { + if ( + assetToBuy && + asset && + assetToBuy?.address === asset?.address && + assetToBuy?.chainId === asset?.chainId + ) { + setAssetToBuy(prevAssetToSell === undefined ? null : prevAssetToSell); + } + setAssetToSellState(asset); + asset?.chainId && setOutputChainId(asset?.chainId); + }, + [assetToBuy, prevAssetToSell], + ); return { assetsToSell: filteredAssetsToSell, diff --git a/src/entries/popup/hooks/swap/useSwapInputs.ts b/src/entries/popup/hooks/swap/useSwapInputs.ts index 03b14aa89d..01147d3e70 100644 --- a/src/entries/popup/hooks/swap/useSwapInputs.ts +++ b/src/entries/popup/hooks/swap/useSwapInputs.ts @@ -103,6 +103,7 @@ export const useSwapInputs = ({ const setAssetToSellMaxValue = useCallback(() => { setAssetToSellValue(assetToSellMaxValue.amount); + setIndependentValue(assetToSellMaxValue.amount); focusOnInput(assetToSellInputRef); setIndependentField('sellField'); setTimeout(() => { diff --git a/src/entries/popup/hooks/swap/useSwapValidations.ts b/src/entries/popup/hooks/swap/useSwapValidations.ts index f3ecaca850..a88181df37 100644 --- a/src/entries/popup/hooks/swap/useSwapValidations.ts +++ b/src/entries/popup/hooks/swap/useSwapValidations.ts @@ -50,12 +50,7 @@ export const useSwapValidations = ({ } } return true; - }, [ - assetToSell?.balance?.amount, - assetToSell?.decimals, - assetToSell?.isNativeAsset, - assetToSellValue, - ]); + }, [assetToSell, assetToSellValue]); const enoughNativeAssetBalanceForGas = useMemo(() => { if (assetToSell?.isNativeAsset) { diff --git a/src/entries/popup/hooks/useSearchCurrencyLists.ts b/src/entries/popup/hooks/useSearchCurrencyLists.ts index 153fddc415..51ccce21b3 100644 --- a/src/entries/popup/hooks/useSearchCurrencyLists.ts +++ b/src/entries/popup/hooks/useSearchCurrencyLists.ts @@ -42,6 +42,17 @@ export interface AssetToBuySection { id: AssetToBuySectionId; } +const filterBridgeAsset = ({ + asset, + filter = '', +}: { + asset?: SearchAsset; + filter?: string; +}) => + asset?.address?.toLowerCase()?.startsWith(filter?.toLowerCase()) || + asset?.name?.toLowerCase()?.startsWith(filter?.toLowerCase()) || + asset?.symbol?.toLowerCase()?.startsWith(filter?.toLowerCase()); + export function useSearchCurrencyLists({ assetToSell, inputChainId, @@ -226,8 +237,14 @@ export function useSearchCurrencyLists({ ], ), ); - return outputChainId === assetToSell?.chainId ? null : bridgeAsset; - }, [assetToSell, getCuratedAssets, outputChainId]); + const filteredBridgeAsset = filterBridgeAsset({ + asset: bridgeAsset, + filter: query, + }) + ? bridgeAsset + : null; + return outputChainId === assetToSell?.chainId ? null : filteredBridgeAsset; + }, [assetToSell, getCuratedAssets, outputChainId, query]); const loading = useMemo(() => { return query === '' diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapAssetCard.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapAssetCard.tsx index eeccbf3933..84230bc28a 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapAssetCard.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapAssetCard.tsx @@ -14,9 +14,14 @@ import { CoinIcon } from '~/entries/popup/components/CoinIcon/CoinIcon'; export type SwapAssetCardProps = { asset: ParsedSearchAsset; assetAmount: string; + testId: string; }; -export const SwapAssetCard = ({ asset, assetAmount }: SwapAssetCardProps) => { +export const SwapAssetCard = ({ + asset, + assetAmount, + testId, +}: SwapAssetCardProps) => { const { currentCurrency } = useCurrentCurrencyStore(); const amount = useMemo( @@ -48,6 +53,7 @@ export const SwapAssetCard = ({ asset, assetAmount }: SwapAssetCardProps) => { height: '128px', backgroundColor: transparentAccentColorAsHsl, }} + testId={`${testId}-swap-asset-card`} > diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx index bf82dd1a0b..8a2d9d053f 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx @@ -6,6 +6,7 @@ import { Address } from 'wagmi'; import SendSound from 'static/assets/audio/woosh.mp3'; import { i18n } from '~/core/languages'; import { QuoteTypeMap } from '~/core/raps/references'; +import { useGasStore } from '~/core/state'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; import { ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; @@ -33,15 +34,16 @@ import { ExplainerSheet, useExplainerSheetParams, } from '~/entries/popup/components/ExplainerSheet/ExplainerSheet'; -import { - Navbar, - NavbarCloseButton, -} from '~/entries/popup/components/Navbar/Navbar'; +import { Navbar } from '~/entries/popup/components/Navbar/Navbar'; import { Spinner } from '~/entries/popup/components/Spinner/Spinner'; import { SwapFee } from '~/entries/popup/components/TransactionFee/TransactionFee'; -import { useSwapReviewDetails } from '~/entries/popup/hooks/swap'; +import { + useSwapReviewDetails, + useSwapValidations, +} from '~/entries/popup/hooks/swap'; import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; import { ROUTES } from '~/entries/popup/urls'; +import { zIndexes } from '~/entries/popup/utils/zIndexes'; import * as wallet from '../../../handlers/wallet'; @@ -49,9 +51,15 @@ import { SwapAssetCard } from './SwapAssetCard'; import { SwapRoutes } from './SwapRoutes'; import { SwapViewContractDropdown } from './SwapViewContractDropdown'; -const DetailsRow = ({ children }: { children: React.ReactNode }) => { +const DetailsRow = ({ + children, + testId, +}: { + children: React.ReactNode; + testId: string; +}) => { return ( - + {children} @@ -62,9 +70,11 @@ const DetailsRow = ({ children }: { children: React.ReactNode }) => { const CarrouselButton = ({ textArray, symbol, + testId, }: { textArray: string[]; symbol?: SymbolProps['symbol']; + testId: string; }) => { const [currentTextIndex, setCurrentTextIndex] = useState(0); @@ -76,7 +86,7 @@ const CarrouselButton = ({ return ( - + {textArray[currentTextIndex]} @@ -141,6 +151,7 @@ const Label = ({ export type SwapReviewSheetProps = { show: boolean; assetToSell?: ParsedSearchAsset | null; + assetToSellValue?: string; assetToBuy?: ParsedSearchAsset | null; quote?: Quote | CrosschainQuote | QuoteError; flashbotsEnabled: boolean; @@ -150,6 +161,7 @@ export type SwapReviewSheetProps = { export const SwapReviewSheet = ({ show, assetToSell, + assetToSellValue, assetToBuy, quote, flashbotsEnabled, @@ -161,6 +173,7 @@ export const SwapReviewSheet = ({ setShowDetails(false), []); const executeSwap = useCallback(async () => { - if (!assetToSell || !assetToBuy || !quote) return; + if (!assetToSell || !assetToBuy || !quote || sendingSwap) return; const type = assetToSell.chainId !== assetToBuy.chainId ? 'crosschainSwap' : 'swap'; const q = quote as QuoteTypeMap[typeof type]; @@ -238,12 +261,20 @@ const SwapReviewSheetWithQuote = ({ } else { setSendingSwap(false); } - }, [assetToBuy, assetToSell, connectedToHardhat, navigate, quote]); + }, [ + assetToBuy, + assetToSell, + connectedToHardhat, + navigate, + quote, + sendingSwap, + ]); const handleSwap = useCallback(() => { + if (!enoughNativeAssetBalanceForGas) return; executeSwap(); new Audio(SendSound).play(); - }, [executeSwap]); + }, [enoughNativeAssetBalanceForGas, executeSwap]); const goBack = useCallback(() => { hideSwapReview(); @@ -286,6 +317,31 @@ const SwapReviewSheetWithQuote = ({ }); }, [hideExplainerSheet, includedFee, showExplainerSheet]); + const buttonLabel = useMemo(() => { + if (!enoughNativeAssetBalanceForGas) { + return validationButtonLabel; + } + return isBridge + ? i18n.t('swap.review.bridge_confirmation', { + sellSymbol: assetToSell.symbol, + }) + : i18n.t('swap.review.swap_confirmation', { + sellSymbol: assetToSell.symbol, + buySymbol: assetToBuy.symbol, + }); + }, [ + assetToBuy.symbol, + assetToSell.symbol, + enoughNativeAssetBalanceForGas, + isBridge, + validationButtonLabel, + ]); + + const buttonColor = useMemo( + () => (enoughNativeAssetBalanceForGas ? 'accent' : 'fillSecondary'), + [enoughNativeAssetBalanceForGas], + ); + return ( <> - } - /> + + + } + /> + + @@ -332,7 +395,7 @@ const SwapReviewSheetWithQuote = ({ style={{ width: 32, height: 32, - zIndex: 10, + zIndex: zIndexes.CUSTOM_GAS_SHEET - 1, position: 'absolute', left: '0 auto', }} @@ -354,6 +417,7 @@ const SwapReviewSheetWithQuote = ({ @@ -361,7 +425,7 @@ const SwapReviewSheetWithQuote = ({ - + - + - + {flashbotsEnabled && ( - + + - @@ -506,6 +586,7 @@ const SwapReviewSheetWithQuote = ({ assetToSell={assetToSell} assetToBuy={assetToBuy} enabled={show} + defaultSpeed={selectedGas.option} /> @@ -513,8 +594,9 @@ const SwapReviewSheetWithQuote = ({ onClick={handleSwap} height="44px" variant="flat" - color={'accent'} + color={buttonColor} width="full" + testId="swap-review-execute" > {sendingSwap ? ( ) : ( - - {i18n.t('swap.review.swap_confirmation', { - sellSymbol: assetToSell.symbol, - buySymbol: assetToBuy.symbol, - })} + + {buttonLabel} )} diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapRoutes.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapRoutes.tsx index 7415be8e84..2afa502bc3 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapRoutes.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapRoutes.tsx @@ -13,6 +13,7 @@ export type SwapRoutesProps = { isBridge: boolean; part?: number; }[]; + testId?: string; }; const RoutePath = ({ protocols }: SwapRoutesProps) => { @@ -88,7 +89,7 @@ const RouteProtocol = ({ ); }; -export const SwapRoutes = ({ protocols }: SwapRoutesProps) => { +export const SwapRoutes = ({ protocols, testId }: SwapRoutesProps) => { const [currentComponentIndex, setCurrentComponentIndex] = useState(0); const routeWithBridge = useMemo( @@ -118,7 +119,9 @@ export const SwapRoutes = ({ protocols }: SwapRoutesProps) => { return ( - {components[currentComponentIndex]} + + {components[currentComponentIndex]} + ); }; diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapViewContractDropdown.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapViewContractDropdown.tsx index 85327e3554..7d95457211 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapViewContractDropdown.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapViewContractDropdown.tsx @@ -20,10 +20,12 @@ export const SwapViewContractDropdown = ({ address, chainId, children, + testId, }: { address?: Address; chainId?: ChainId; children: ReactNode; + testId: string; }) => { const viewOnEtherscan = useCallback(() => { const explorer = getBlockExplorerHostForChain(chainId || ChainId.mainnet); @@ -50,7 +52,10 @@ export const SwapViewContractDropdown = ({ return ( - + {children} @@ -58,7 +63,11 @@ export const SwapViewContractDropdown = ({ - + @@ -90,7 +99,11 @@ export const SwapViewContractDropdown = ({ - + diff --git a/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToBuyDropdown.tsx b/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToBuyDropdown.tsx index 2068e4eef9..552075aa78 100644 --- a/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToBuyDropdown.tsx +++ b/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToBuyDropdown.tsx @@ -64,7 +64,7 @@ export const TokenToBuyDropdown = ({ setOutputChainId(chainId); }} triggerComponent={ - + { - if (priceImpact?.type !== SwapPriceImpactType.none) { - return { - warningTitle: i18n.t('swap.warnings.price_impact.title'), - warningDescription: i18n.t('swap.warnings.price_impact.description', { - impactAmount: priceImpact?.impactDisplay, - }), - warningColor: (priceImpact?.type === SwapPriceImpactType.high - ? 'orange' - : 'red') as TextStyles['color'], - }; - } else if (timeEstimate?.isLongWait) { - return { - warningTitle: i18n.t('swap.warnings.long_wait.title'), - warningDescription: i18n.t('swap.warnings.long_wait.description', { - time: timeEstimate?.timeEstimateDisplay, - }), - warningColor: 'orange' as TextStyles['color'], - }; - } else { - return { - warningTitle: '', - warningDescription: '', - warningColor: 'orange' as TextStyles['color'], - }; - } - }, [ - priceImpact?.impactDisplay, - priceImpact?.type, - timeEstimate?.isLongWait, - timeEstimate?.timeEstimateDisplay, - ]); + const { warningTitle, warningDescription, warningColor, warningType } = + useMemo(() => { + if (priceImpact?.type !== SwapPriceImpactType.none) { + return { + warningType: 'price-impact', + warningTitle: i18n.t('swap.warnings.price_impact.title'), + warningDescription: i18n.t('swap.warnings.price_impact.description', { + impactAmount: priceImpact?.impactDisplay, + }), + warningColor: (priceImpact?.type === SwapPriceImpactType.high + ? 'orange' + : 'red') as TextStyles['color'], + }; + } else if (timeEstimate?.isLongWait) { + return { + warningType: 'long-wait', + warningTitle: i18n.t('swap.warnings.long_wait.title'), + warningDescription: i18n.t('swap.warnings.long_wait.description', { + time: timeEstimate?.timeEstimateDisplay, + }), + warningColor: 'orange' as TextStyles['color'], + }; + } else { + return { + warningType: '', + warningTitle: '', + warningDescription: '', + warningColor: 'orange' as TextStyles['color'], + }; + } + }, [ + priceImpact?.impactDisplay, + priceImpact?.type, + timeEstimate?.isLongWait, + timeEstimate?.timeEstimateDisplay, + ]); if (!showWarning) return null; return ( - + diff --git a/src/entries/popup/utils/zIndexes.ts b/src/entries/popup/utils/zIndexes.ts index f1371b9834..4817956ead 100644 --- a/src/entries/popup/utils/zIndexes.ts +++ b/src/entries/popup/utils/zIndexes.ts @@ -2,6 +2,7 @@ export const zIndexes = { NAVBAR: 1, PROMPT: 2, BOTTOM_SHEET: 4, + CUSTOM_GAS_SHEET: 5, EXPLAINER_BOTTOM_SHEET: 6, TOAST: 8, ALERT: 8,