diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08f79a9bec..5ec2ddb146 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -298,6 +298,39 @@ jobs: - name: Fail if any tests failed if: steps.ChromeE2ESend.outcome == 'failure' run: exit 1 + chrome-optimism-e2e-send: + runs-on: send-runner-bx + timeout-minutes: 25 + needs: [build] + env: + DISPLAY: :0 + VITEST_SEGFAULT_RETRY: 3 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/chromeTestsSetup + with: + gh-access-token: ${{ secrets.DOTENV_GITHUB_ACCESS_TOKEN }} + - name: Run Optimism e2e send (Chrome) + id: ChromeOpE2ESend + continue-on-error: true + uses: nick-fields/retry@v2 + with: + timeout_minutes: 25 + max_attempts: 2 + command: | + export BROWSER=chrome + export OS=linux + export CHROMIUM_BIN=$(find chrome -type f -name 'chrome') + yarn vitest:send:optimism --retry=5 + - name: Upload deps artifacts + if: steps.ChromeOpE2ESend.outcome == 'failure' + uses: actions/upload-artifact@v3 + with: + name: screenshots + path: screenshots/ + - name: Fail if any tests failed + if: steps.ChromeOpE2ESend.outcome == 'failure' + run: exit 1 chrome-e2e-dappInteractions: runs-on: dapp-interactions-runner-bx timeout-minutes: 25 @@ -457,7 +490,7 @@ jobs: run: yarn typecheck cleanup: runs-on: ubuntu-latest - needs: [firefox-e2e-parallel, firefox-e2e-send, firefox-e2e-dappInteractions, chrome-e2e-parallel, chrome-e2e-swap, chrome-e2e-send, chrome-e2e-dappInteractions, unit-tests, ci-checks] + needs: [firefox-e2e-parallel, firefox-e2e-send, firefox-e2e-dappInteractions, chrome-e2e-parallel, chrome-e2e-swap, chrome-e2e-send, chrome-e2e-dappInteractions, chrome-optimism-e2e-send, unit-tests, ci-checks] steps: - uses: geekyeggo/delete-artifact@v2 with: diff --git a/e2e/helpers.ts b/e2e/helpers.ts index f42456c882..c6bc28d012 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -687,13 +687,10 @@ export async function getOnchainBalance(addy: string, contract: string) { export async function transactionStatus() { const provider = getDefaultProvider('http://127.0.0.1:8545'); const blockData = await provider.getBlock('latest'); - const txn = await provider.getTransaction(blockData.transactions[0]); - const txnData = txn.wait(); - - // transactionResponse.wait.status returns '1' if txn is successful - // it returns '0' if the txn is a failure - const txnStatus = (await txnData).status === 1 ? 'success' : 'failure'; - + const txnReceipt = await provider.getTransactionReceipt( + blockData.transactions[0], + ); + const txnStatus = txnReceipt.status === 1 ? 'success' : 'failure'; return txnStatus; } diff --git a/e2e/serial/optimismTransactions/1_sendFlow.test.ts b/e2e/serial/optimismTransactions/1_sendFlow.test.ts new file mode 100644 index 0000000000..7247825474 --- /dev/null +++ b/e2e/serial/optimismTransactions/1_sendFlow.test.ts @@ -0,0 +1,144 @@ +import 'chromedriver'; +import 'geckodriver'; +import { WebDriver } from 'selenium-webdriver'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from 'vitest'; + +import { + checkExtensionURL, + checkWalletName, + delay, + delayTime, + executePerformShortcut, + findElementByTestId, + findElementByText, + getExtensionIdByName, + getRootUrl, + goToPopup, + importWalletFlowUsingKeyboardNavigation, + initDriverWithOptions, + navigateToElementWithTestId, + takeScreenshotOnFailure, + transactionStatus, +} from '../../helpers'; +import { TEST_VARIABLES } from '../../walletVariables'; + +let rootURL = getRootUrl(); +let driver: WebDriver; + +const browser = process.env.BROWSER || 'chrome'; +const os = process.env.OS || 'mac'; + +describe('Complete Hardhat Optimism send flow', () => { + beforeAll(async () => { + driver = await initDriverWithOptions({ + browser, + os, + }); + const extensionId = await getExtensionIdByName(driver, 'Rainbow'); + if (!extensionId) throw new Error('Extension not found'); + rootURL += extensionId; + }); + + beforeEach<{ driver: WebDriver }>(async (context) => { + context.driver = driver; + }); + + afterEach<{ driver: WebDriver }>(async (context) => { + await takeScreenshotOnFailure(context); + }); + + afterAll(() => driver.quit()); + + it('should be able import a wallet via pk', async () => { + await importWalletFlowUsingKeyboardNavigation( + driver, + rootURL, + TEST_VARIABLES.SEED_WALLET.PK, + ); + }); + + it('should display account name', async () => { + await checkWalletName(driver, rootURL, TEST_VARIABLES.SEED_WALLET.ADDRESS); + }); + + it('should be able to go to setings', async () => { + await goToPopup(driver, rootURL); + await executePerformShortcut({ driver, key: 'DECIMAL' }); + await executePerformShortcut({ driver, key: 'ARROW_DOWN' }); + await executePerformShortcut({ driver, key: 'ENTER' }); + await checkExtensionURL(driver, 'settings'); + }); + + it('should be able to connect to hardhat Optimism', async () => { + await navigateToElementWithTestId({ + driver, + testId: 'connect-to-hardhat-op', + }); + const button = await findElementByText( + driver, + 'Disconnect from Hardhat Optimism', + ); + expect(button).toBeTruthy(); + await executePerformShortcut({ driver, key: 'ESCAPE' }); + }); + + it('should be able to navigate to send', async () => { + await executePerformShortcut({ driver, key: 's' }); + await checkExtensionURL(driver, 'send'); + }); + + it('should be able to nav to send field and type in address', async () => { + await executePerformShortcut({ driver, key: 'TAB', timesToPress: 2 }); + await driver + .actions() + .sendKeys('0x9126914f62314402cC3f098becfaa7c2Bc23a55C') + .perform(); + const shortenedAddress = await findElementByText(driver, '0x9126…a55C'); + expect(shortenedAddress).toBeTruthy(); + }); + + it('should be able to select asset to send with keyboard', async () => { + await navigateToElementWithTestId({ + driver, + testId: 'asset-name-eth_10', + }); + await delayTime('long'); + const tokenInput = await findElementByTestId({ + id: 'input-wrapper-dropdown-token-input', + driver, + }); + expect(await tokenInput.getText()).toContain('Ethereum'); + const value = await findElementByTestId({ id: 'send-input-mask', driver }); + const valueNum = await value.getAttribute('value'); + expect(Number(valueNum)).toBe(0); + }); + + it('should be able to initiate Optimisim ETH transaction', async () => { + await driver.actions().sendKeys('1').perform(); + const value = await findElementByTestId({ id: 'send-input-mask', driver }); + const valueNum = await value.getAttribute('value'); + expect(Number(valueNum)).toBe(1); + await navigateToElementWithTestId({ driver, testId: 'send-review-button' }); + const reviewText = await findElementByText(driver, 'Review & Send'); + expect(reviewText).toBeTruthy(); + await delayTime('medium'); + await navigateToElementWithTestId({ driver, testId: 'L2-check-1' }); + await navigateToElementWithTestId({ driver, testId: 'L2-check-2' }); + await navigateToElementWithTestId({ + driver, + testId: 'review-confirm-button', + }); + await delayTime('very-long'); + const sendTransaction = await transactionStatus(); + expect(sendTransaction).toBe('success'); + await delay(10000); + }); +}); diff --git a/e2e/serial/send/2_shortcuts-sendFlow.test.ts b/e2e/serial/send/2_shortcuts-sendFlow.test.ts index 90e60ec0f4..aa42c5a7a6 100644 --- a/e2e/serial/send/2_shortcuts-sendFlow.test.ts +++ b/e2e/serial/send/2_shortcuts-sendFlow.test.ts @@ -174,7 +174,6 @@ describe('Complete send flow via shortcuts and keyboard navigation', () => { }); const placeholderBeforeContent = await placeholderBefore.getAttribute('placeholder'); - console.log(placeholderBeforeContent); expect(placeholderBeforeContent).toContain('ETH'); await executePerformShortcut({ driver, key: 'TAB', timesToPress: 2 }); await executePerformShortcut({ driver, key: 'ENTER' }); @@ -184,7 +183,6 @@ describe('Complete send flow via shortcuts and keyboard navigation', () => { driver, }); const placeholderContent = await placeholder.getAttribute('placeholder'); - console.log(placeholderContent); expect(placeholderContent).toContain('USD'); }); diff --git a/package.json b/package.json index ba7b9f0bea..a9410b7439 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "// 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", + "anvil": "ETH_MAINNET_RPC=$(grep ETH_MAINNET_RPC .env | cut -d '=' -f2) && anvil --fork-url $ETH_MAINNET_RPC", + "anvil:optimism": "OPTIMISM_MAINNET_RPC=$(grep OPTIMISM_MAINNET_RPC .env | cut -d '=' -f2) && anvil --fork-url $OPTIMISM_MAINNET_RPC", "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", @@ -60,6 +61,7 @@ "vitest:serial": "./scripts/e2e-serial-tests.sh", "vitest:swap": "./scripts/e2e-serial-tests.sh 'swap'", "vitest:send": "./scripts/e2e-serial-tests.sh 'send'", + "vitest:send:optimism": "./scripts/e2e-op-serial-tests.sh 'optimismTransactions'", "vitest:dappInteractions": "./scripts/e2e-serial-tests.sh 'dappInteractions'", "e2e:mac:chrome": "BROWSER=chrome OS=mac yarn vitest:parallel && yarn vitest:serial", "e2e:mac:brave": "BROWSER=brave OS=mac yarn vitest:parallel && yarn vitest:serial", diff --git a/scripts/e2e-op-serial-tests.sh b/scripts/e2e-op-serial-tests.sh new file mode 100755 index 0000000000..15244a2c78 --- /dev/null +++ b/scripts/e2e-op-serial-tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash +ANVIL_PORT=8545 + +# Launch anvil in the bg +yarn anvil:kill +yarn anvil:optimism --chain-id 1338 & +echo "Launching Anvil..." + +# Give it some time to boot +interval=5 +until nc -z localhost $ANVIL_PORT; do + sleep $interval + interval=$((interval * 2)) +done +echo "Anvil Launched..." + +# Run the tests and store the result +echo "Running Tests..." +yarn vitest e2e/serial/$1 --config ./e2e/serial/vitest.config.ts --reporter=verbose + +# Store exit code +TEST_RESULT=$? + +# kill anvil +echo "Cleaning Up..." +yarn anvil:kill + +# return the result of the tests +exit "$TEST_RESULT" diff --git a/src/core/keychain/index.ts b/src/core/keychain/index.ts index e6a5dcbd25..a9e2b3f3b2 100644 --- a/src/core/keychain/index.ts +++ b/src/core/keychain/index.ts @@ -233,6 +233,7 @@ export const sendTransaction = async ( if (typeof txPayload.from === 'undefined') { throw new Error('Missing from address'); } + const signer = await keychainManager.getSigner(txPayload.from as Address); const wallet = signer.connect(provider); let response = await wallet.sendTransaction(txPayload); diff --git a/src/core/references/index.ts b/src/core/references/index.ts index bc459b0546..789499daf4 100644 --- a/src/core/references/index.ts +++ b/src/core/references/index.ts @@ -105,6 +105,7 @@ export const NATIVE_ASSETS_PER_CHAIN: Record = { [ChainId.bsc]: BSC_BNB_ADDRESS as Address, [ChainId.bscTestnet]: BSC_BNB_ADDRESS as Address, [ChainId.optimism]: ETH_OPTIMISM_ADDRESS as Address, + [ChainId.hardhatOptimism]: ETH_OPTIMISM_ADDRESS as Address, [ChainId.optimismGoerli]: ETH_OPTIMISM_ADDRESS as Address, [ChainId.base]: ETH_BASE_ADDRESS as Address, [ChainId.baseGoerli]: ETH_BASE_ADDRESS as Address, diff --git a/src/core/resources/assets/userAssets.ts b/src/core/resources/assets/userAssets.ts index 0eab1660e6..231cda6da8 100644 --- a/src/core/resources/assets/userAssets.ts +++ b/src/core/resources/assets/userAssets.ts @@ -11,7 +11,7 @@ import { queryClient, } from '~/core/react-query'; import { SupportedCurrencyKey } from '~/core/references'; -import { connectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; +import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; import { ParsedAssetsDictByChain, ParsedUserAsset, @@ -30,6 +30,7 @@ import { RainbowError, logger } from '~/logger'; import { DAI_MAINNET_ASSET, ETH_MAINNET_ASSET, + OPTIMISM_MAINNET_ASSET, USDC_MAINNET_ASSET, } from '~/test/utils'; @@ -219,7 +220,6 @@ export async function parseUserAssets({ chainIds: ChainId[]; currency: SupportedCurrencyKey; }) { - const { connectedToHardhat } = connectedToHardhatStore.getState(); const parsedAssetsDict = chainIds.reduce( (dict, currentChainId) => ({ ...dict, [currentChainId]: {} }), {}, @@ -236,41 +236,57 @@ export async function parseUserAssets({ parsedAsset; } } - if (connectedToHardhat) { - const provider = getProvider({ chainId: ChainId.hardhat }); - // force checking for ETH if connected to hardhat - const mainnetAssets = parsedAssetsDict[ChainId.mainnet]; - mainnetAssets[ETH_MAINNET_ASSET.uniqueId] = ETH_MAINNET_ASSET; + const { connectedToHardhat, connectedToHardhatOp } = + useConnectedToHardhatStore.getState(); + if (connectedToHardhat || connectedToHardhatOp) { + // separating out these ternaries for readability + const selectedHardhatChainId = connectedToHardhat + ? ChainId.hardhat + : ChainId.hardhatOptimism; + + const mainnetOrOptimismChainId = connectedToHardhat + ? ChainId.mainnet + : ChainId.optimism; + + const ethereumOrOptimismAsset = connectedToHardhat + ? ETH_MAINNET_ASSET + : OPTIMISM_MAINNET_ASSET; + + const provider = getProvider({ chainId: selectedHardhatChainId }); + + // Ensure assets are checked if connected to hardhat + const assets = parsedAssetsDict[mainnetOrOptimismChainId]; + assets[ethereumOrOptimismAsset.uniqueId] = ethereumOrOptimismAsset; if (process.env.IS_TESTING === 'true') { - mainnetAssets[USDC_MAINNET_ASSET.uniqueId] = USDC_MAINNET_ASSET; - mainnetAssets[DAI_MAINNET_ASSET.uniqueId] = DAI_MAINNET_ASSET; + assets[USDC_MAINNET_ASSET.uniqueId] = USDC_MAINNET_ASSET; + assets[DAI_MAINNET_ASSET.uniqueId] = DAI_MAINNET_ASSET; } - const mainnetBalanceRequests = Object.values(mainnetAssets).map( - async (asset) => { - if (asset.chainId !== ChainId.mainnet) return asset; - try { - const parsedAsset = await fetchAssetBalanceViaProvider({ - parsedAsset: asset, - currentAddress: address, - currency, - provider, - }); - return parsedAsset; - } catch (e) { - return asset; - } - }, - ); - const newParsedMainnetAssetsByUniqueId = await Promise.all( - mainnetBalanceRequests, - ); - const newMainnetAssets = newParsedMainnetAssetsByUniqueId.reduce< + + const balanceRequests = Object.values(assets).map(async (asset) => { + if (asset.chainId !== mainnetOrOptimismChainId) return asset; + + try { + const parsedAsset = await fetchAssetBalanceViaProvider({ + parsedAsset: asset, + currentAddress: address, + currency, + provider, + }); + return parsedAsset; + } catch (e) { + return asset; + } + }); + + const newParsedAssetsByUniqueId = await Promise.all(balanceRequests); + const newAssets = newParsedAssetsByUniqueId.reduce< Record >((acc, parsedAsset) => { acc[parsedAsset.uniqueId] = parsedAsset; return acc; }, {}); - parsedAssetsDict[ChainId.mainnet] = newMainnetAssets; + + parsedAssetsDict[mainnetOrOptimismChainId] = newAssets; } return parsedAssetsDict; } diff --git a/src/core/resources/gas/gasData.ts b/src/core/resources/gas/gasData.ts index 5b5311b295..20cf1bd87f 100644 --- a/src/core/resources/gas/gasData.ts +++ b/src/core/resources/gas/gasData.ts @@ -15,6 +15,7 @@ const getRefetchTime = (chainId: ChainId) => { case ChainId.optimism: case ChainId.polygon: case ChainId.zora: + case ChainId.hardhatOptimism: default: return 2000; } diff --git a/src/core/state/currentSettings/connectedToHardhat.ts b/src/core/state/currentSettings/connectedToHardhat.ts index 49bb815eb1..6cbd079eb4 100644 --- a/src/core/state/currentSettings/connectedToHardhat.ts +++ b/src/core/state/currentSettings/connectedToHardhat.ts @@ -5,6 +5,9 @@ import { createStore } from '~/core/state/internal/createStore'; export interface ConnectedToHardhatState { connectedToHardhat: boolean; setConnectedToHardhat: (connectedToHardhat: boolean) => void; + + connectedToHardhatOp: boolean; + setConnectedToHardhatOp: (connectedToHardhatOp: boolean) => void; } export const connectedToHardhatStore = createStore( @@ -13,6 +16,11 @@ export const connectedToHardhatStore = createStore( setConnectedToHardhat: (connectedToHardhat) => { set({ connectedToHardhat }); }, + + connectedToHardhatOp: false, + setConnectedToHardhatOp: (connectedToHardhatOp) => { + set({ connectedToHardhatOp }); + }, }), { persist: { diff --git a/src/core/types/chains.ts b/src/core/types/chains.ts index 1b606d55fe..82c5ea3dd6 100644 --- a/src/core/types/chains.ts +++ b/src/core/types/chains.ts @@ -2,6 +2,7 @@ import * as chain from '@wagmi/chains'; import type { Chain } from 'wagmi'; const HARDHAT_CHAIN_ID = 1337; +const HARDHAT_OP_CHAIN_ID = 1338; export const hardhat: Chain = { id: HARDHAT_CHAIN_ID, @@ -19,6 +20,22 @@ export const hardhat: Chain = { testnet: true, }; +export const hardhatOptimism: Chain = { + id: HARDHAT_OP_CHAIN_ID, + name: 'Hardhat OP', + network: 'hardhatOptimism', + nativeCurrency: { + decimals: 18, + name: 'Hardhat Op', + symbol: 'op', + }, + rpcUrls: { + public: { http: ['http://127.0.0.1:8545'] }, + default: { http: ['http://127.0.0.1:8545'] }, + }, + testnet: true, +}; + export enum ChainName { arbitrum = 'arbitrum', base = 'base', @@ -28,6 +45,7 @@ export enum ChainName { zora = 'zora', mainnet = 'mainnet', hardhat = 'hardhat', + hardhatOptimism = 'hardhatOptimism', goerli = 'goerli', sepolia = 'sepolia', optimismGoerli = 'optimismGoerli', @@ -47,6 +65,7 @@ export enum ChainId { polygon = chain.polygon.id, zora = chain.zora.id, hardhat = HARDHAT_CHAIN_ID, + hardhatOptimism = HARDHAT_OP_CHAIN_ID, goerli = chain.goerli.id, sepolia = chain.sepolia.id, optimismGoerli = chain.optimismGoerli.id, @@ -66,6 +85,7 @@ export const ChainNameDisplay = { [ChainId.zora]: 'Zora', [ChainId.mainnet]: 'Ethereum', [ChainId.hardhat]: 'Hardhat', + [ChainId.hardhatOptimism]: 'Hardhat Optimism', [ChainId.goerli]: chain.goerli.name, [ChainId.sepolia]: chain.sepolia.name, [ChainId.optimismGoerli]: chain.optimismGoerli.name, diff --git a/src/core/utils/chains.ts b/src/core/utils/chains.ts index dc29c2ff88..3e493f75f4 100644 --- a/src/core/utils/chains.ts +++ b/src/core/utils/chains.ts @@ -48,7 +48,8 @@ export const getSupportedChainsWithHardhat = () => { return chains.filter( (chain) => !chain.testnet || - (process.env.IS_TESTING === 'true' && chain.id === ChainId.hardhat), + (process.env.IS_TESTING === 'true' && + (chain.id === ChainId.hardhat || chain.id === ChainId.hardhatOptimism)), ); }; @@ -116,3 +117,21 @@ export function getChain({ chainId }: { chainId?: ChainId }) { export function isSupportedChainId(chainId: number) { return SUPPORTED_CHAINS.map((chain) => chain.id).includes(chainId); } + +export const chainIdToUse = ( + connectedToHardhat: boolean, + connectedToHardhatOp: boolean, + activeSessionChainId?: number | null, +) => { + if (connectedToHardhat) { + return ChainId.hardhat; + } + if (connectedToHardhatOp) { + return ChainId.hardhatOptimism; + } + if (activeSessionChainId !== null && activeSessionChainId !== undefined) { + return activeSessionChainId; + } else { + return ChainId.mainnet; + } +}; diff --git a/src/core/utils/swaps.ts b/src/core/utils/swaps.ts index 10b1a8bcc8..7f4fd24a24 100644 --- a/src/core/utils/swaps.ts +++ b/src/core/utils/swaps.ts @@ -6,7 +6,7 @@ import { } from '@rainbow-me/swaps'; import { i18n } from '../languages'; -import { connectedToHardhatStore } from '../state/currentSettings/connectedToHardhat'; +import { useConnectedToHardhatStore } from '../state/currentSettings/connectedToHardhat'; import { ChainId } from '../types/chains'; import { isLowerCaseMatch } from './strings'; @@ -63,7 +63,7 @@ export const isUnwrapEth = ({ sellTokenAddress: string; buyTokenAddress: string; }) => { - const { connectedToHardhat } = connectedToHardhatStore.getState(); + const { connectedToHardhat } = useConnectedToHardhatStore.getState(); return ( isLowerCaseMatch( sellTokenAddress, @@ -81,7 +81,7 @@ export const isWrapEth = ({ sellTokenAddress: string; buyTokenAddress: string; }) => { - const { connectedToHardhat } = connectedToHardhatStore.getState(); + const { connectedToHardhat } = useConnectedToHardhatStore.getState(); return ( isLowerCaseMatch(sellTokenAddress, ETH_ADDRESS) && isLowerCaseMatch( diff --git a/src/core/wagmi/createWagmiClient.ts b/src/core/wagmi/createWagmiClient.ts index 6b40ce6de3..ee113a29a7 100644 --- a/src/core/wagmi/createWagmiClient.ts +++ b/src/core/wagmi/createWagmiClient.ts @@ -11,7 +11,7 @@ import { jsonRpcProvider } from 'wagmi/providers/jsonRpc'; import { proxyRpcEndpoint } from '../providers'; import { queryClient } from '../react-query'; import { Storage } from '../storage'; -import { ChainId, hardhat } from '../types/chains'; +import { ChainId, hardhat, hardhatOptimism } from '../types/chains'; import { SUPPORTED_CHAINS } from '../utils/chains'; const IS_TESTING = process.env.IS_TESTING === 'true'; @@ -26,6 +26,8 @@ const getOriginalRpcEndpoint = (chain: Chain) => { switch (chain.id) { case ChainId.hardhat: return { http: chain.rpcUrls.default.http[0] }; + case ChainId.hardhatOptimism: + return { http: chain.rpcUrls.default.http[0] }; case ChainId.mainnet: return { http: process.env.ETH_MAINNET_RPC as string }; case ChainId.optimism: @@ -62,7 +64,9 @@ const getOriginalRpcEndpoint = (chain: Chain) => { }; const { chains, provider, webSocketProvider } = configureChains( - IS_TESTING ? SUPPORTED_CHAINS.concat(hardhat) : SUPPORTED_CHAINS, + IS_TESTING + ? SUPPORTED_CHAINS.concat(hardhat, hardhatOptimism) + : SUPPORTED_CHAINS, [ jsonRpcProvider({ rpc: (chain) => { diff --git a/src/entries/iframe/notification.tsx b/src/entries/iframe/notification.tsx index 88734cb6a0..1212630d40 100644 --- a/src/entries/iframe/notification.tsx +++ b/src/entries/iframe/notification.tsx @@ -28,6 +28,7 @@ const ASSET_SOURCE = { [ChainId.zora]: 'assets/badges/zoraBadge.png', [ChainId.bsc]: 'assets/badges/bscBadge.png', [ChainId.hardhat]: 'assets/badges/hardhatBadge.png', + [ChainId.hardhatOptimism]: 'assets/badges/hardhatBadge.png', [ChainId.goerli]: 'assets/badges/ethereumBadge.png', [ChainId.sepolia]: 'assets/badges/ethereumBadge.png', [ChainId.optimismGoerli]: 'assets/badges/optimismBadge.png', diff --git a/src/entries/popup/components/ChainBadge/ChainBadge.tsx b/src/entries/popup/components/ChainBadge/ChainBadge.tsx index 3ff8e9353b..79bc5ca087 100644 --- a/src/entries/popup/components/ChainBadge/ChainBadge.tsx +++ b/src/entries/popup/components/ChainBadge/ChainBadge.tsx @@ -34,6 +34,7 @@ const networkBadges = { [ChainId.zora]: ZoraBadge, [ChainId.bsc]: BscBadge, [ChainId.hardhat]: HardhatBadge, + [ChainId.hardhatOptimism]: HardhatBadge, [ChainId.goerli]: EthereumBadge, [ChainId.sepolia]: EthereumBadge, [ChainId.optimismGoerli]: OptimismBadge, diff --git a/src/entries/popup/components/SideChainExplainer.tsx b/src/entries/popup/components/SideChainExplainer.tsx index ca0beb1da3..5b29bb8733 100644 --- a/src/entries/popup/components/SideChainExplainer.tsx +++ b/src/entries/popup/components/SideChainExplainer.tsx @@ -8,7 +8,10 @@ import { ExplainerSheetProps, } from './ExplainerSheet/ExplainerSheet'; -type SideChain = Exclude; +type SideChain = Exclude< + ChainId, + ChainId.mainnet | ChainId.goerli | ChainId.hardhat | ChainId.hardhatOptimism +>; export const isSideChain = (chainId: ChainId): chainId is SideChain => [ChainId.arbitrum, ChainId.polygon, ChainId.optimism, ChainId.bsc].includes( chainId, diff --git a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index 343b2fd7ee..8ee6e953cb 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -5,7 +5,7 @@ import { i18n } from '~/core/languages'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; import { ChainId } from '~/core/types/chains'; import { GasFeeLegacyParams, GasFeeParams } from '~/core/types/gas'; -import { getChain } from '~/core/utils/chains'; +import { chainIdToUse, getChain } from '~/core/utils/chains'; import { toWei } from '~/core/utils/ethereum'; import { lessThan } from '~/core/utils/numbers'; @@ -20,8 +20,14 @@ export const useApproveAppRequestValidations = ({ selectedGas?: GasFeeParams | GasFeeLegacyParams; dappStatus?: DAppStatus; }) => { - const { connectedToHardhat } = useConnectedToHardhatStore(); - const chainIdToUse = connectedToHardhat ? ChainId.mainnet : chainId; + const { connectedToHardhat, connectedToHardhatOp } = + useConnectedToHardhatStore.getState(); + + const activeChainId = chainIdToUse( + connectedToHardhat, + connectedToHardhatOp, + chainId, + ); const { nativeAsset } = useNativeAsset({ chainId }); @@ -38,11 +44,11 @@ export const useApproveAppRequestValidations = ({ if (!enoughNativeAssetForGas) return i18n.t('approve_request.insufficient_native_asset_for_gas', { - symbol: getChain({ chainId: chainIdToUse }).nativeCurrency.name, + symbol: getChain({ chainId: activeChainId }).nativeCurrency.name, }); return i18n.t('approve_request.send_transaction'); - }, [chainIdToUse, enoughNativeAssetForGas, dappStatus]); + }, [activeChainId, enoughNativeAssetForGas, dappStatus]); return { enoughNativeAssetForGas: diff --git a/src/entries/popup/hooks/useFavoriteAssets.ts b/src/entries/popup/hooks/useFavoriteAssets.ts index 1d6a5308e7..01fe6ca809 100644 --- a/src/entries/popup/hooks/useFavoriteAssets.ts +++ b/src/entries/popup/hooks/useFavoriteAssets.ts @@ -17,6 +17,7 @@ const FAVORITES_EMPTY_STATE = { [ChainId.base]: [], [ChainId.zora]: [], [ChainId.hardhat]: [], + [ChainId.hardhatOptimism]: [], }; // expensive hook, only use in top level parent components diff --git a/src/entries/popup/pages/messages/SendTransaction/index.tsx b/src/entries/popup/pages/messages/SendTransaction/index.tsx index 7174f019a4..ba5b5b685b 100644 --- a/src/entries/popup/pages/messages/SendTransaction/index.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/index.tsx @@ -16,6 +16,7 @@ import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags' import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { ChainId } from '~/core/types/chains'; import { NewTransaction, TxHash } from '~/core/types/transactions'; +import { chainIdToUse } from '~/core/utils/chains'; import { addNewTransaction } from '~/core/utils/transactions'; import { Row, Rows } from '~/design-system'; import { triggerAlert } from '~/design-system/components/Alert/Alert'; @@ -54,7 +55,8 @@ export function SendTransaction({ const { activeSession } = useAppSession({ host: dappMetadata?.appHost }); const { selectedGas } = useGasStore(); const selectedWallet = activeSession?.address || ''; - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToHardhat, connectedToHardhatOp } = + useConnectedToHardhatStore(); const { asset, selectAssetAddressAndChain } = useSendAsset(); const { watchedWallets } = useWallets(); const { featureFlags } = useFeatureFlagsStore(); @@ -71,12 +73,18 @@ export function SendTransaction({ if (type === 'HardwareWalletKeychain') { setWaitingForDevice(true); } + + const activeChainId = chainIdToUse( + connectedToHardhat, + connectedToHardhatOp, + activeSession?.chainId, + ); const txData = { from: selectedWallet, to: txRequest?.to ? getAddress(txRequest?.to) : undefined, value: txRequest.value || '0x0', data: txRequest.data ?? '0x', - chainId: connectedToHardhat ? ChainId.hardhat : activeSession?.chainId, + chainId: activeChainId, }; const result = await wallet.sendTransaction(txData); if (result) { @@ -120,6 +128,7 @@ export function SendTransaction({ activeSession, request?.params, connectedToHardhat, + connectedToHardhatOp, asset, selectedGas.transactionGasParams, approveRequest, @@ -157,19 +166,23 @@ export function SendTransaction({ } }, [featureFlags.full_watching_wallets, isWatchingWallet, rejectRequest]); + const activeChainId = chainIdToUse( + connectedToHardhat, + connectedToHardhatOp, + activeSession?.chainId, + ); + useEffect(() => { if (activeSession) { selectAssetAddressAndChain( - NATIVE_ASSETS_PER_CHAIN[ - connectedToHardhat ? ChainId.hardhat : activeSession?.chainId - ] as Address, - connectedToHardhat ? ChainId.hardhat : activeSession?.chainId, + NATIVE_ASSETS_PER_CHAIN[activeChainId] as Address, + activeChainId, ); } }, [ activeSession, - activeSession?.chainId, connectedToHardhat, + activeChainId, selectAssetAddressAndChain, ]); diff --git a/src/entries/popup/pages/send/ReviewSheet.tsx b/src/entries/popup/pages/send/ReviewSheet.tsx index 491b73ae9e..303a2a48b7 100644 --- a/src/entries/popup/pages/send/ReviewSheet.tsx +++ b/src/entries/popup/pages/send/ReviewSheet.tsx @@ -30,6 +30,7 @@ import { Text, } from '~/design-system'; import { BottomSheet } from '~/design-system/components/BottomSheet/BottomSheet'; +import { Lens } from '~/design-system/components/Lens/Lens'; import { TextOverflow } from '~/design-system/components/TextOverflow/TextOverflow'; import { transformScales, @@ -539,7 +540,8 @@ export const ReviewSheet = ({ /> - setSendingOnL2Checks([ !sendingOnL2Checks[0], @@ -555,7 +557,7 @@ export const ReviewSheet = ({ > {i18n.t('send.review.sending_on_l2_check_1')} - + @@ -577,7 +579,8 @@ export const ReviewSheet = ({ /> - setSendingOnL2Checks([ sendingOnL2Checks[0], @@ -594,7 +597,7 @@ export const ReviewSheet = ({ chainName, })} - + diff --git a/src/entries/popup/pages/send/index.tsx b/src/entries/popup/pages/send/index.tsx index f871779977..4843510247 100644 --- a/src/entries/popup/pages/send/index.tsx +++ b/src/entries/popup/pages/send/index.tsx @@ -27,6 +27,7 @@ import { TransactionLegacyGasParams, } from '~/core/types/gas'; import { NewTransaction, TxHash } from '~/core/types/transactions'; +import { chainIdToUse } from '~/core/utils/chains'; import { addNewTransaction } from '~/core/utils/transactions'; import { Box, Button, Inline, Row, Rows, Symbol, Text } from '~/design-system'; import { AccentColorProvider } from '~/design-system/components/Box/ColorContext'; @@ -82,7 +83,8 @@ export function Send() { const isMyWallet = (address: Address) => allWallets?.some((w) => w.address === address); - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToHardhat, connectedToHardhatOp } = + useConnectedToHardhatStore(); const { asset, @@ -189,6 +191,12 @@ export function Send() { saveSendTokenAddressAndChain, } = usePopupInstanceStore(); + const activeChainId = chainIdToUse( + connectedToHardhat, + connectedToHardhatOp, + chainId, + ); + const handleSend = useCallback( async (callback?: () => void) => { if (!config.send_enabled) return; @@ -204,7 +212,7 @@ export function Send() { from: fromAddress, to: txToAddress, value, - chainId: connectedToHardhat ? ChainId.hardhat : chainId, + chainId: activeChainId, data, }); if (result && asset) { @@ -271,11 +279,11 @@ export function Send() { resetSendValues, txToAddress, value, - connectedToHardhat, - chainId, + activeChainId, data, - assetAmount, asset, + assetAmount, + chainId, selectedGas.transactionGasParams, navigate, ], diff --git a/src/entries/popup/pages/settings/settings.tsx b/src/entries/popup/pages/settings/settings.tsx index 045c3857a8..36bfde6c93 100644 --- a/src/entries/popup/pages/settings/settings.tsx +++ b/src/entries/popup/pages/settings/settings.tsx @@ -52,8 +52,12 @@ export function Settings() { const { currentUserSelectedTheme, currentTheme, setCurrentTheme } = useCurrentThemeStore(); - const { connectedToHardhat, setConnectedToHardhat } = - useConnectedToHardhatStore(); + const { + connectedToHardhat, + setConnectedToHardhat, + connectedToHardhatOp, + setConnectedToHardhatOp, + } = useConnectedToHardhatStore(); const { clearNonces } = useNonceStore(); const [themeDropdownOpen, setThemeDropdownOpen] = useState(false); @@ -110,6 +114,10 @@ export function Settings() { setConnectedToHardhat(!connectedToHardhat); }, [setConnectedToHardhat, connectedToHardhat]); + const connectToHardhatOp = useCallback(() => { + setConnectedToHardhatOp(!connectedToHardhatOp); + }, [setConnectedToHardhatOp, connectedToHardhatOp]); + const setRainbowAsDefaultWallet = useCallback( async (rainbowAsDefault: boolean) => { analytics.track( @@ -430,6 +438,20 @@ export function Settings() { onClick={connectToHardhat} testId="connect-to-hardhat" /> + + } + onClick={connectToHardhatOp} + testId="connect-to-hardhat-op" + /> } diff --git a/src/test/utils.ts b/src/test/utils.ts index c32ad755e0..a249e2cf4d 100644 --- a/src/test/utils.ts +++ b/src/test/utils.ts @@ -89,6 +89,30 @@ export const USDC_MAINNET_ASSET = { decimals: 6, } satisfies ParsedUserAsset; +export const OPTIMISM_MAINNET_ASSET = { + address: '0x0000000000000000000000000000000000000000', + balance: { amount: '10000', display: '10,000.00 ETH' }, + chainId: 10, + chainName: 'optimism' as ChainName, + colors: { primary: '#808088', fallback: '#E8EAF5' }, + decimals: 18, + icon_url: + 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/assets/ethereum/eth.png', + isNativeAsset: true, + mainnetAddress: 'eth', + name: 'Ethereum', + native: { + balance: { amount: '16341800', display: '$16,341,800.00' }, + price: { change: '0.15%', amount: 1634.18, display: '$1,634.18' }, + }, + price: { + value: 1634.18, + relative_change_24h: 0.14646492502099484, + }, + symbol: 'ETH', + uniqueId: 'eth_10', +} satisfies ParsedUserAsset; + export const ENS_MAINNET_ASSET = { address: '0xc18360217d8f7ab5e7c516566761ea12ce7f9d72', chainId: 1,