diff --git a/packages/arb-token-bridge-ui/src/components/App/App.tsx b/packages/arb-token-bridge-ui/src/components/App/App.tsx index d23f339fb8..bacdb3c9bc 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -1,12 +1,7 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' +import { useAccount, WagmiConfig } from 'wagmi' +import { darkTheme, RainbowKitProvider, Theme } from '@rainbow-me/rainbowkit' -import { useAccount, useNetwork, WagmiConfig, useDisconnect } from 'wagmi' -import { - darkTheme, - RainbowKitProvider, - Theme, - useConnectModal -} from '@rainbow-me/rainbowkit' import merge from 'lodash-es/merge' import axios from 'axios' import { createOvermind, Overmind } from 'overmind' @@ -24,23 +19,16 @@ import { ArbTokenBridgeStoreSync } from '../syncers/ArbTokenBridgeStoreSync' import { BalanceUpdater } from '../syncers/BalanceUpdater' import { TokenListSyncer } from '../syncers/TokenListSyncer' import { Header } from '../common/Header' -import { HeaderAccountPopover } from '../common/HeaderAccountPopover' -import { getNetworkName } from '../../util/networks' -import { - ArbQueryParamProvider, - useArbQueryParams -} from '../../hooks/useArbQueryParams' +import { ArbQueryParamProvider } from '../../hooks/useArbQueryParams' import { TOS_LOCALSTORAGE_KEY } from '../../constants' import { getProps } from '../../util/wagmi/setup' import { useAccountIsBlocked } from '../../hooks/useAccountIsBlocked' import { useCCTPIsBlocked } from '../../hooks/CCTP/useCCTPIsBlocked' import { useNativeCurrency } from '../../hooks/useNativeCurrency' -import { sanitizeQueryParams, useNetworks } from '../../hooks/useNetworks' +import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { HeaderConnectWalletButton } from '../common/HeaderConnectWalletButton' -import { onDisconnectHandler } from '../../util/walletConnectUtils' -import { addressIsSmartContract } from '../../util/AddressUtils' import { useSyncConnectedChainToAnalytics } from './useSyncConnectedChainToAnalytics' +import { useSyncConnectedChainToQueryParams } from './useSyncConnectedChainToQueryParams' import { isDepositMode } from '../../util/isDepositMode' declare global { @@ -71,6 +59,9 @@ const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { // We want to be sure this fetch is completed by the time we open the USDC modals useCCTPIsBlocked() + useSyncConnectedChainToAnalytics() + useSyncConnectedChainToQueryParams() + const [tokenBridgeParams, setTokenBridgeParams] = useState(null) @@ -159,18 +150,9 @@ const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { } function AppContent() { - const { address, isConnected } = useAccount() + const { address } = useAccount() const { isBlocked } = useAccountIsBlocked() const [tosAccepted] = useLocalStorage(TOS_LOCALSTORAGE_KEY, false) - const { openConnectModal } = useConnectModal() - - useEffect(() => { - if (tosAccepted && !isConnected) { - openConnectModal?.() - } - }, [isConnected, tosAccepted, openConnectModal]) - - useSyncConnectedChainToAnalytics() if (!tosAccepted) { return ( @@ -181,23 +163,6 @@ function AppContent() { ) } - if (!isConnected) { - return ( - <> -
- -
- -
-

No wallet connected

-

- Please connect your wallet to use the bridge. -

-
- - ) - } - if (address && isBlocked) { return ( -
- -
+
@@ -248,97 +211,6 @@ Object.keys(localStorage).forEach(key => { } }) -function ConnectedChainSyncer() { - const { address } = useAccount() - const [shouldSync, setShouldSync] = useState(false) - const [didSync, setDidSync] = useState(false) - const { disconnect } = useDisconnect({ - onSettled: onDisconnectHandler - }) - - const [{ sourceChain, destinationChain }, setQueryParams] = - useArbQueryParams() - const { chain } = useNetwork() - - const setSourceChainToConnectedChain = useCallback(() => { - if (typeof chain === 'undefined') { - return - } - - const { sourceChainId: sourceChain, destinationChainId: destinationChain } = - sanitizeQueryParams({ - sourceChainId: chain.id, - destinationChainId: undefined - }) - - setQueryParams({ sourceChain, destinationChain }) - }, [chain, setQueryParams]) - - useEffect(() => { - async function checkCorrectChainForSmartContractWallet() { - if (typeof chain === 'undefined') { - return - } - if (!address) { - return - } - const isSmartContractWallet = await addressIsSmartContract( - address, - chain.id - ) - if (isSmartContractWallet && sourceChain !== chain.id) { - const chainName = getNetworkName(chain.id) - - setSourceChainToConnectedChain() - - window.alert( - `You're connected to the app with a smart contract wallet on ${chainName}. In order to properly enable transfers, the app will now reload.\n\nPlease reconnect after the reload.` - ) - disconnect() - } - } - - checkCorrectChainForSmartContractWallet() - }, [ - address, - chain, - disconnect, - setQueryParams, - setSourceChainToConnectedChain, - sourceChain - ]) - - useEffect(() => { - if (shouldSync) { - return - } - - // Only sync connected chain to query params if the query params were not initially provided - if ( - typeof sourceChain === 'undefined' && - typeof destinationChain === 'undefined' - ) { - setShouldSync(true) - } - }, [shouldSync, sourceChain, destinationChain]) - - useEffect(() => { - // When the chain is connected and we should sync, and we haven't synced yet, sync the connected chain to the query params - if (chain && shouldSync && !didSync) { - setSourceChainToConnectedChain() - setDidSync(true) - } - }, [ - chain, - shouldSync, - didSync, - setQueryParams, - setSourceChainToConnectedChain - ]) - - return null -} - export default function App() { const [overmind] = useState>(createOvermind(config)) @@ -350,7 +222,6 @@ export default function App() { theme={rainbowkitTheme} {...rainbowKitProviderProps} > - diff --git a/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx b/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx index 61a3c65f85..32085080b2 100644 --- a/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx @@ -1,12 +1,9 @@ -import { useConnectModal } from '@rainbow-me/rainbowkit' import { useCallback } from 'react' import { useLocalStorage } from '@uidotdev/usehooks' import { ExternalLink } from '../common/ExternalLink' -import { errorToast } from '../common/atoms/Toast' import { TOS_LOCALSTORAGE_KEY } from '../../constants' import { Button } from '../common/Button' -import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' export function WelcomeDialog() { const [, setTosAccepted] = useLocalStorage( @@ -14,21 +11,9 @@ export function WelcomeDialog() { false ) - const { openConnectModal } = useConnectModal() - const closeHandler = useCallback(() => { setTosAccepted(true) - - try { - openConnectModal?.() - } catch (error) { - errorToast('Failed to open up RainbowKit Connect Modal') - captureSentryErrorWithExtraData({ - error, - originFunction: 'WelcomeDialog closeHandler' - }) - } - }, [openConnectModal, setTosAccepted]) + }, [setTosAccepted]) return (
diff --git a/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToAnalytics.ts b/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToAnalytics.ts index a8d5aeba73..b245e8ad40 100644 --- a/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToAnalytics.ts +++ b/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToAnalytics.ts @@ -1,5 +1,5 @@ -import { useAccount } from 'wagmi' import { useEffect } from 'react' +import { useAccount } from 'wagmi' import * as Sentry from '@sentry/react' import { useNetworks } from '../../hooks/useNetworks' @@ -16,11 +16,8 @@ function getWalletName(connectorName: string): ProviderName { case 'Safe': case 'Injected': case 'Ledger': - return connectorName - - case 'WalletConnectLegacy': case 'WalletConnect': - return 'WalletConnect' + return connectorName default: return 'Other' @@ -28,41 +25,48 @@ function getWalletName(connectorName: string): ProviderName { } /** given our RPC url, sanitize it before logging to Sentry, to only pass the url and not the keys */ -function getBaseUrl(url: string) { +function getBaseUrl(url: string | undefined): string | null { + if (typeof url === 'undefined') { + return null + } + try { const urlObject = new URL(url) return `${urlObject.protocol}//${urlObject.hostname}` } catch { // if invalid url passed - return '' + return null } } export function useSyncConnectedChainToAnalytics() { + const { isConnected, connector } = useAccount() const [networks] = useNetworks() const { parentChain, childChain } = useNetworksRelationship(networks) - const { isConnected, connector } = useAccount() useEffect(() => { if (isConnected && connector) { const walletName = getWalletName(connector.name) trackEvent('Connect Wallet Click', { walletName }) - } - // set a custom tag in sentry to filter issues by connected wallet.name - Sentry.setTag('wallet.name', connector?.name ?? '') + // set a custom tag in sentry to filter issues by connected wallet.name + Sentry.setTag('wallet.name', walletName) + } }, [isConnected, connector]) useEffect(() => { Sentry.setTag('network.parent_chain_id', parentChain.id) - Sentry.setTag( - 'network.parent_chain_rpc_url', - getBaseUrl(rpcURLs[parentChain.id] ?? '') - ) Sentry.setTag('network.child_chain_id', childChain.id) - Sentry.setTag( - 'network.child_chain_rpc_url', - getBaseUrl(rpcURLs[childChain.id] ?? '') - ) + + const parentChainRpcUrl = getBaseUrl(rpcURLs[parentChain.id]) + const childChainRpcUrl = getBaseUrl(rpcURLs[childChain.id]) + + if (parentChainRpcUrl) { + Sentry.setTag('network.parent_chain_rpc_url', parentChainRpcUrl) + } + + if (childChainRpcUrl) { + Sentry.setTag('network.child_chain_rpc_url', childChainRpcUrl) + } }, [childChain.id, parentChain.id]) } diff --git a/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToQueryParams.ts b/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToQueryParams.ts new file mode 100644 index 0000000000..cc3377f7e2 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/App/useSyncConnectedChainToQueryParams.ts @@ -0,0 +1,97 @@ +import { useState, useEffect, useCallback } from 'react' +import { useAccount, useDisconnect, useNetwork } from 'wagmi' + +import { useArbQueryParams } from '../../hooks/useArbQueryParams' +import { sanitizeQueryParams } from '../../hooks/useNetworks' +import { onDisconnectHandler } from '../../util/walletConnectUtils' +import { addressIsSmartContract } from '../../util/AddressUtils' +import { getNetworkName } from '../../util/networks' + +export function useSyncConnectedChainToQueryParams() { + const { address } = useAccount() + const [shouldSync, setShouldSync] = useState(false) + const [didSync, setDidSync] = useState(false) + const { disconnect } = useDisconnect({ + onSettled: onDisconnectHandler + }) + + const [{ sourceChain, destinationChain }, setQueryParams] = + useArbQueryParams() + const { chain } = useNetwork() + + const setSourceChainToConnectedChain = useCallback(() => { + if (typeof chain === 'undefined') { + return + } + + const { sourceChainId: sourceChain, destinationChainId: destinationChain } = + sanitizeQueryParams({ + sourceChainId: chain.id, + destinationChainId: undefined + }) + + setQueryParams({ sourceChain, destinationChain }) + }, [chain, setQueryParams]) + + useEffect(() => { + async function checkCorrectChainForSmartContractWallet() { + if (typeof chain === 'undefined') { + return + } + if (!address) { + return + } + const isSmartContractWallet = await addressIsSmartContract( + address, + chain.id + ) + if (isSmartContractWallet && sourceChain !== chain.id) { + const chainName = getNetworkName(chain.id) + + setSourceChainToConnectedChain() + + window.alert( + `You're connected to the app with a smart contract wallet on ${chainName}. In order to properly enable transfers, the app will now reload.\n\nPlease reconnect after the reload.` + ) + disconnect() + } + } + + checkCorrectChainForSmartContractWallet() + }, [ + address, + chain, + disconnect, + setQueryParams, + setSourceChainToConnectedChain, + sourceChain + ]) + + useEffect(() => { + if (shouldSync) { + return + } + + // Only sync connected chain to query params if the query params were not initially provided + if ( + typeof sourceChain === 'undefined' && + typeof destinationChain === 'undefined' + ) { + setShouldSync(true) + } + }, [shouldSync, sourceChain, destinationChain]) + + useEffect(() => { + // When the chain is connected and we should sync, and we haven't synced yet, sync the connected chain to the query params + if (chain && shouldSync && !didSync) { + setSourceChainToConnectedChain() + setDidSync(true) + } + }, [ + chain, + shouldSync, + didSync, + setQueryParams, + setSourceChainToConnectedChain + ]) +} diff --git a/packages/arb-token-bridge-ui/src/components/MainContent/MainContent.tsx b/packages/arb-token-bridge-ui/src/components/MainContent/MainContent.tsx index 158308db54..0cb04710a6 100644 --- a/packages/arb-token-bridge-ui/src/components/MainContent/MainContent.tsx +++ b/packages/arb-token-bridge-ui/src/components/MainContent/MainContent.tsx @@ -23,7 +23,8 @@ function TransactionHistorySidePanel() { runFetcher: true }) - const { transactions, updatePendingTransaction } = transactionHistoryProps + const { transactions, loading, updatePendingTransaction } = + transactionHistoryProps const pendingTransactions = useMemo(() => { return transactions.filter(isTxPending) @@ -31,11 +32,15 @@ function TransactionHistorySidePanel() { useEffect(() => { const interval = setInterval(() => { - pendingTransactions.forEach(updatePendingTransaction) + // only update pending transactions when tx history is not loading + // otherwise it would cause a race condition in updating the swr state and fetching would get stuck + if (!loading) { + pendingTransactions.forEach(updatePendingTransaction) + } }, 10_000) return () => clearInterval(interval) - }, [pendingTransactions, updatePendingTransaction]) + }, [pendingTransactions, updatePendingTransaction, loading]) return ( { return 'bg-gray-1 text-white/70' }, [numClaimableTransactions, numPendingTransactions, numRetryablesToRedeem]) + if (typeof address === 'undefined') { + return null + } + return ( + )} +
{typeof tokenFromSearchParams !== 'undefined' && ( {children} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useNativeCurrencyBalances.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useNativeCurrencyBalances.ts index 799148769a..8c37d9ad6f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useNativeCurrencyBalances.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useNativeCurrencyBalances.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' -import { BigNumber } from 'ethers' +import { BigNumber, constants } from 'ethers' +import { useAccount } from 'wagmi' import { useNativeCurrency } from '../../../hooks/useNativeCurrency' import { useNetworks } from '../../../hooks/useNetworks' @@ -13,12 +14,20 @@ export function useNativeCurrencyBalances(): { const [networks] = useNetworks() const { childChainProvider, isDepositMode } = useNetworksRelationship(networks) + const { isConnected } = useAccount() const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const { ethParentBalance, erc20ParentBalances, ethChildBalance } = useBalances() return useMemo(() => { + if (!isConnected) { + return { + sourceBalance: constants.Zero, + destinationBalance: constants.Zero + } + } + if (!nativeCurrency.isCustom) { return { sourceBalance: isDepositMode ? ethParentBalance : ethChildBalance, @@ -39,6 +48,7 @@ export function useNativeCurrencyBalances(): { : customFeeTokenParentBalance } }, [ + isConnected, nativeCurrency, erc20ParentBalances, ethChildBalance, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts index df4f371fc1..b28333ee9c 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts @@ -222,6 +222,11 @@ export function useTransferReadiness(): UseTransferReadinessResult { } ) + if (typeof walletAddress === 'undefined') { + // show gas estimation without wallet connection + return ready() + } + const ethBalanceFloat = isDepositMode ? ethL1BalanceFloat : ethL2BalanceFloat diff --git a/packages/arb-token-bridge-ui/src/components/common/Header.tsx b/packages/arb-token-bridge-ui/src/components/common/Header.tsx index 67f6d0b0d2..066b00369c 100644 --- a/packages/arb-token-bridge-ui/src/components/common/Header.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/Header.tsx @@ -2,14 +2,26 @@ import React from 'react' import Image from 'next/image' import { twMerge } from 'tailwind-merge' import ArbitrumLogoSmall from '@/images/ArbitrumLogo.svg' +import { useAccount } from 'wagmi' import { isNetwork } from '../../util/networks' import { useNetworks } from '../../hooks/useNetworks' +import { HeaderAccountPopover } from './HeaderAccountPopover' +import { HeaderConnectWalletButton } from './HeaderConnectWalletButton' import { useDestinationChainStyle } from '../../hooks/useDestinationChainStyle' import { AppMobileSidebar } from '../Sidebar/AppMobileSidebar' import { isExperimentalModeEnabled } from '../../util' -export function Header({ children }: { children?: React.ReactNode }) { +function HeaderAccountOrConnectWalletButton() { + const { isConnected } = useAccount() + + if (isConnected) { + return + } + return +} + +export function Header() { const [{ sourceChain }] = useNetworks() const { isTestnet } = isNetwork(sourceChain.id) @@ -47,7 +59,9 @@ export function Header({ children }: { children?: React.ReactNode }) { EXPERIMENTAL MODE: features may be incomplete or not work properly )} -
{children}
+
+ +
diff --git a/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx b/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx index d2bdb11462..51e42bb622 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'react' -import { useAccount } from 'wagmi' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' @@ -15,7 +14,6 @@ const TokenListSyncer = (): JSX.Element => { const { app: { arbTokenBridge, arbTokenBridgeLoaded } } = useAppState() - const { address: walletAddress } = useAccount() const [networks] = useNetworks() const { childChain } = useNetworksRelationship(networks) @@ -23,27 +21,24 @@ const TokenListSyncer = (): JSX.Element => { if (!arbTokenBridgeLoaded) { return } - - if (!walletAddress) { - return - } - const tokenListsToSet = BRIDGE_TOKEN_LISTS.filter(bridgeTokenList => { // Always load the Arbitrum Token token list if (bridgeTokenList.isArbitrumTokenTokenList) { return true } - return ( bridgeTokenList.originChainID === childChain.id && bridgeTokenList.isDefault ) }) - tokenListsToSet.forEach(bridgeTokenList => { addBridgeTokenListToBridge(bridgeTokenList, arbTokenBridge) }) - }, [walletAddress, childChain.id, arbTokenBridgeLoaded]) + }, [ + // arbTokenBridge.token is not a memoized object, adding it here would cause infinite loop + childChain.id, + arbTokenBridgeLoaded + ]) return <> } diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts index 991617fe1d..d0728a635e 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts @@ -1,6 +1,6 @@ -import { BigNumber, Signer, utils } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import useSWR from 'swr' -import { useAccount, useSigner } from 'wagmi' +import { useAccount } from 'wagmi' import { DepositGasEstimates, GasEstimates } from '../arbTokenBridge.types' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' @@ -11,7 +11,7 @@ import { useNetworks } from '../useNetworks' import { useDestinationAddressStore } from '../../components/TransferPanel/AdvancedSettings' async function fetcher([ - signer, + walletAddress, sourceChainId, destinationChainId, sourceChainErc20Address, @@ -19,7 +19,7 @@ async function fetcher([ destinationAddress, amount ]: [ - signer: Signer, + walletAddress: string | undefined, sourceChainId: number, destinationChainId: number, sourceChainErc20Address: string | undefined, @@ -27,6 +27,9 @@ async function fetcher([ destinationAddress: string | undefined, amount: BigNumber ]): Promise { + const _walletAddress = walletAddress ?? constants.AddressZero + const sourceProvider = getProviderForChainId(sourceChainId) + const signer = sourceProvider.getSigner(_walletAddress) // use chainIds to initialize the bridgeTransferStarter to save RPC calls const bridgeTransferStarter = BridgeTransferStarterFactory.create({ sourceChainId, @@ -61,7 +64,6 @@ export function useGasEstimates({ } = useAppState() const { address: walletAddress } = useAccount() const balance = useBalanceOnSourceChain(token) - const { data: signer } = useSigner() const amountToTransfer = balance !== null && amount.gte(balance) ? balance : amount @@ -73,18 +75,16 @@ export function useGasEstimates({ : undefined const { data: gasEstimates, error } = useSWR( - signer - ? ([ - sourceChain.id, - destinationChain.id, - sourceChainErc20Address, - destinationChainErc20Address, - amountToTransfer.toString(), // BigNumber is not serializable - sanitizedDestinationAddress, - walletAddress, - 'gasEstimates' - ] as const) - : null, + [ + sourceChain.id, + destinationChain.id, + sourceChainErc20Address, + destinationChainErc20Address, + amountToTransfer.toString(), // BigNumber is not serializable + sanitizedDestinationAddress, + walletAddress, + 'gasEstimates' + ], ([ _sourceChainId, _destinationChainId, @@ -93,20 +93,16 @@ export function useGasEstimates({ _amount, _destinationAddress, _walletAddress - ]) => { - const sourceProvider = getProviderForChainId(_sourceChainId) - const _signer = sourceProvider.getSigner(_walletAddress) - - return fetcher([ - _signer, + ]) => + fetcher([ + _walletAddress, _sourceChainId, _destinationChainId, _sourceChainErc20Address, _destinationChainErc20Address, _destinationAddress, BigNumber.from(_amount) - ]) - }, + ]), { refreshInterval: 30_000, shouldRetryOnError: true, @@ -115,5 +111,9 @@ export function useGasEstimates({ } ) + if (typeof walletAddress === 'undefined') { + return { gasEstimates, error: 'walletNotConnected' } + } + return { gasEstimates, error } } diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts index 7dfdb5fa9c..27f784c476 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts @@ -137,7 +137,7 @@ export function useGasSummary(): UseGasSummaryResult { } } - if (amountBigNumber.gt(balance)) { + if (balance && amountBigNumber.gt(balance)) { return { status: 'insufficientBalance', estimatedParentChainGasFees, @@ -145,7 +145,7 @@ export function useGasSummary(): UseGasSummaryResult { } } - if (gasEstimatesError) { + if (gasEstimatesError && gasEstimatesError !== 'walletNotConnected') { return { status: 'error', estimatedParentChainGasFees, diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts index 2083f5868e..38b83df56b 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts @@ -1,5 +1,7 @@ import { BigNumber, constants } from 'ethers' import { useMemo } from 'react' +import { useAccount } from 'wagmi' + import { useAppState } from '../../state' import { useNetworks } from '../useNetworks' import { @@ -19,6 +21,7 @@ export function useSelectedTokenBalances(): Balances { const { app } = useAppState() const { selectedToken } = app const [networks] = useNetworks() + const { isConnected } = useAccount() const { isArbitrumOne: isSourceChainArbitrumOne, @@ -49,6 +52,13 @@ export function useSelectedTokenBalances(): Balances { childBalance: null } + if (!isConnected) { + return { + parentBalance: constants.Zero, + childBalance: constants.Zero + } + } + if (!selectedToken) { return result } @@ -103,10 +113,11 @@ export function useSelectedTokenBalances(): Balances { return result }, [ + isConnected, + selectedToken, erc20ParentBalances, erc20ChildBalances, isEthereumArbitrumOnePair, - isSepoliaArbSepoliaPair, - selectedToken + isSepoliaArbSepoliaPair ]) } diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useBalance.test.tsx b/packages/arb-token-bridge-ui/src/hooks/__tests__/useBalance.test.tsx index 033fa1da6c..5a22c11b78 100644 --- a/packages/arb-token-bridge-ui/src/hooks/__tests__/useBalance.test.tsx +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useBalance.test.tsx @@ -54,7 +54,7 @@ describe('useBalance', () => { jest.restoreAllMocks() }) - it('getter return null for undefined walletAddress', async () => { + it('getter return 0 for undefined walletAddress', async () => { // This should not be called. It's here to avoid false positive const getBalanceSpy = jest.spyOn(provider, 'getBalance') getBalanceSpy.mockImplementationOnce(() => @@ -83,7 +83,7 @@ describe('useBalance', () => { expect(getBalanceSpy).not.toHaveBeenCalled() expect(getTokenDataSpy).not.toHaveBeenCalled() - expect(ethBalance).toBeNull() + expect(ethBalance?.toNumber()).toEqual(0) expect(erc20Balances).toBeNull() }) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 955e15a097..5e93b2feab 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -298,10 +298,6 @@ export const useArbTokenBridge = ( let l1Address: string let l2Address: string | undefined - if (!walletAddress) { - return - } - const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() const maybeL1Address = await getL1ERC20Address({ erc20L2Address: lowercasedErc20L1orL2Address, @@ -414,7 +410,8 @@ export const useArbTokenBridge = ( updateErc20L1Balance, updateErc20L2Balance, updateErc20L1CustomDestinationBalance, - updateErc20CustomDestinationL2Balance + updateErc20CustomDestinationL2Balance, + destinationAddress ] ) diff --git a/packages/arb-token-bridge-ui/src/hooks/useBalance.ts b/packages/arb-token-bridge-ui/src/hooks/useBalance.ts index 6090aa9630..6be997a8fd 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useBalance.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useBalance.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react' -import { BigNumber, utils } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import useSWR, { useSWRConfig, unstable_serialize, @@ -50,7 +50,8 @@ const useBalance = ({ chainId, walletAddress }: UseBalanceProps) => { const queryKey = useCallback( (type: 'eth' | 'erc20') => { - if (typeof walletAddressLowercased === 'undefined') { + // we return 0 for ETH when wallet is not connected, but do not fetch for ERC20 + if (typeof walletAddressLowercased === 'undefined' && type === 'erc20') { // Don't fetch return null } @@ -66,9 +67,13 @@ const useBalance = ({ chainId, walletAddress }: UseBalanceProps) => { chainId }: { addresses: string[] | undefined - walletAddress: string + walletAddress: string | undefined chainId: number }) => { + if (typeof walletAddress === 'undefined') { + return + } + if (!addresses?.length) { return {} } @@ -107,6 +112,9 @@ const useBalance = ({ chainId, walletAddress }: UseBalanceProps) => { const { data: dataEth = null, mutate: updateEthBalance } = useSWR( queryKey('eth'), ([_walletAddress, _chainId]) => { + if (typeof _walletAddress === 'undefined') { + return constants.Zero + } const _provider = getProviderForChainId(_chainId) return _provider.getBalance(_walletAddress) }, diff --git a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts index 2fd1588848..26e6ffdc9d 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts @@ -7,8 +7,8 @@ import UniswapLogo from '@/images/lists/uniswap.png' import CMCLogo from '@/images/lists/cmc.png' import CoinGeckoLogo from '@/images/lists/coinGecko.svg' import ArbitrumLogo from '@/images/lists/ArbitrumLogo.png' -import { ArbTokenBridge } from '../hooks/arbTokenBridge.types' import { ChainId } from './networks' +import { ArbTokenBridge } from '../hooks/arbTokenBridge.types' export const SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID = 0 diff --git a/packages/arb-token-bridge-ui/tailwind.config.js b/packages/arb-token-bridge-ui/tailwind.config.js index 687d267831..442a3126c9 100644 --- a/packages/arb-token-bridge-ui/tailwind.config.js +++ b/packages/arb-token-bridge-ui/tailwind.config.js @@ -36,8 +36,8 @@ module.exports = { 'gray-1': '#191919', 'gray-2': '#E5E5E5', 'gray-3': '#DADADA', - 'gray-5': '#AEAEAE', 'gray-4': '#CCCCCC', + 'gray-5': '#AEAEAE', 'gray-6': '#999999', 'gray-7': '#BDBDBD', 'gray-dark': '#6D6D6D', diff --git a/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts b/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts index 5b44d31925..39e01e63f0 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts @@ -27,7 +27,6 @@ import { closeTransactionHistoryPanel, claimCctp } from '../support/commands' -import { NetworkType, NetworkName } from '../support/common' declare global { namespace Cypress { @@ -36,15 +35,10 @@ declare global { * Custom command to connect MetaMask to the UI. * @example cy.login() */ - connectToApp(): typeof connectToApp + connectToApp: typeof connectToApp // eslint-disable-next-line no-unused-vars - login(options: { - networkType: NetworkType - networkName?: NetworkName - url?: string - query?: { [s: string]: string } - }): typeof login - logout(): typeof logout + login: typeof login + logout: typeof logout selectTransactionsPanelTab: typeof selectTransactionsPanelTab openTransactionsPanel: typeof openTransactionsPanel searchAndSelectToken({ diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts index 86c712c350..ad93e5decd 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts @@ -27,7 +27,7 @@ describe('Import token', () => { }) context('User uses L1 address', () => { it('should import token through its L1 address', () => { - cy.login({ networkType: 'parentChain' }) + cy.login({ networkType: 'parentChain', connectMetamask: false }) importTokenThroughUI(ERC20TokenAddressL1) // Select the ERC-20 token @@ -46,7 +46,7 @@ describe('Import token', () => { context('User uses L2 address', () => { it('should import token through its L2 address', () => { - cy.login({ networkType: 'parentChain' }) + cy.login({ networkType: 'parentChain', connectMetamask: false }) importTokenThroughUI(ERC20TokenAddressL2) // Select the ERC-20 token @@ -61,7 +61,7 @@ describe('Import token', () => { context('User uses invalid address', () => { it('should display an error message after invalid input', () => { - cy.login({ networkType: 'parentChain' }) + cy.login({ networkType: 'parentChain', connectMetamask: false }) importTokenThroughUI(invalidTokenAddress) // Error message is displayed @@ -74,7 +74,8 @@ describe('Import token', () => { // we don't have the token list locally so we test on mainnet cy.login({ networkType: 'parentChain', - networkName: 'mainnet' + networkName: 'mainnet', + connectMetamask: false }) cy.findSelectTokenButton('ETH').click() @@ -98,7 +99,8 @@ describe('Import token', () => { // we don't have the token list locally so we test on mainnet cy.login({ networkType: 'parentChain', - networkName: 'mainnet' + networkName: 'mainnet', + connectMetamask: false }) cy.findSelectTokenButton('ETH').click() @@ -136,7 +138,7 @@ describe('Import token', () => { it('should disable Add button if address is too long/short', () => { const addressWithoutLastChar = ERC20TokenAddressL1.slice(0, -1) // Remove the last character - cy.login({ networkType: 'parentChain' }) + cy.login({ networkType: 'parentChain', connectMetamask: false }) cy.findSelectTokenButton(nativeTokenSymbol).click() // open the Select Token popup @@ -173,10 +175,11 @@ describe('Import token', () => { url: '/', query: { token: ERC20TokenAddressL1 - } + }, + connectMetamask: false }) - // waiting for metamask notification to disappear + // waiting for URL to resolve correctly // eslint-disable-next-line cy.wait(3000) @@ -190,9 +193,7 @@ describe('Import token', () => { // Import token cy.findByRole('button', { name: 'Import token' }) .should('be.visible') - .trigger('click', { - force: true - }) + .trigger('click') cy.findSelectTokenButton(ERC20TokenSymbol) // Modal is closed @@ -207,10 +208,11 @@ describe('Import token', () => { url: '/', query: { token: ERC20TokenAddressL2 - } + }, + connectMetamask: false }) - // waiting for metamask notification to disappear + // waiting for URL to resolve correctly // eslint-disable-next-line cy.wait(3000) @@ -242,7 +244,8 @@ describe('Import token', () => { url: '/', query: { token: invalidTokenAddress - } + }, + connectMetamask: false }) visitAfterSomeDelay('/', { @@ -261,9 +264,7 @@ describe('Import token', () => { // Close modal cy.findByRole('button', { name: 'Dialog Cancel' }) .should('be.visible') - .trigger('click', { - force: true - }) + .trigger('click') cy.findSelectTokenButton(nativeTokenSymbol) // Modal is closed diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts index ce8413b0cc..b48b8c3263 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts @@ -44,7 +44,7 @@ describe('Login Account', () => { cy.findByText(/Agree to Terms and Continue/i) .should('be.visible') .click() - cy.findByText('Connect a Wallet').should('be.visible') + cy.findAllByText('Connect Wallet').first().should('be.visible').click() cy.findByText('MetaMask').should('be.visible') }) diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index fef1e5d1f0..c5068e65d5 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -19,27 +19,18 @@ import { import { shortenAddress } from '../../src/util/CommonUtils' import { formatAmount } from 'packages/arb-token-bridge-ui/src/util/NumberUtils' -function shouldChangeNetwork(networkName: NetworkName) { - // synpress throws if trying to connect to a network we are already connected to - // issue has been raised with synpress and this is just a workaround - // TODO: remove this whenever fixed - return cy - .task('getCurrentNetworkName') - .then((currentNetworkName: NetworkName) => { - return currentNetworkName !== networkName - }) -} - export function login({ networkType, networkName, url, - query + query, + connectMetamask = true }: { networkType: NetworkType networkName?: NetworkName url?: string query?: { [s: string]: string } + connectMetamask?: boolean }) { // if networkName is not specified we connect to default network from config const network = @@ -56,22 +47,13 @@ export function login({ ? 'l3-localhost' : '' startWebApp(url, { - ...query, - sourceChain, - destinationChain + query: { ...query, sourceChain, destinationChain }, + connectMetamask }) } - shouldChangeNetwork(networkNameWithDefault).then(changeNetwork => { - if (changeNetwork) { - cy.changeMetamaskNetwork(networkNameWithDefault).then(() => { - _startWebApp() - }) - } else { - _startWebApp() - } - - cy.task('setCurrentNetworkName', networkNameWithDefault) + cy.changeMetamaskNetwork(networkNameWithDefault).then(() => { + _startWebApp() }) } @@ -85,13 +67,15 @@ export const logout = () => { cy.changeMetamaskNetwork('sepolia') } -export const connectToApp = () => { +export const connectToApp = (connectMetamask: boolean) => { // initial modal prompts which come in the web-app cy.findByText(/Agree to Terms and Continue/i) .should('be.visible') .click() - cy.findByText('Connect a Wallet').should('be.visible') - cy.findByText('MetaMask').should('be.visible').click() + if (connectMetamask) { + cy.findAllByText('Connect Wallet').first().should('be.visible').click() + cy.findByText('MetaMask').should('be.visible').click() + } } export const selectTransactionsPanelTab = (tab: 'pending' | 'settled') => { diff --git a/packages/arb-token-bridge-ui/tests/support/common.ts b/packages/arb-token-bridge-ui/tests/support/common.ts index 83aa73c988..90d3bc1436 100644 --- a/packages/arb-token-bridge-ui/tests/support/common.ts +++ b/packages/arb-token-bridge-ui/tests/support/common.ts @@ -163,24 +163,32 @@ export const acceptMetamaskAccess = () => { }) } -export const startWebApp = (url = '/', qs: { [s: string]: string } = {}) => { +export const startWebApp = ( + url = '/', + options: { + query: { [s: string]: string } + connectMetamask: boolean + } +) => { // once all the metamask setup is done, we can start the actual web-app for testing // clear local storage for terms to always have it pop up cy.clearLocalStorage('arbitrum:bridge:tos-v2') cy.visit(url, { - qs + qs: options.query }) if (Cypress.currentRetry > 0) { // ensures we don't test with the same state that could have caused the test to fail cy.reload(true) } - cy.connectToApp() - cy.task('getWalletConnectedToDapp').then(connected => { - if (!connected) { - acceptMetamaskAccess() - cy.task('setWalletConnectedToDapp') - } - }) + cy.connectToApp(options.connectMetamask) + if (options.connectMetamask) { + cy.task('getWalletConnectedToDapp').then(connected => { + if (!connected) { + acceptMetamaskAccess() + cy.task('setWalletConnectedToDapp') + } + }) + } } export const visitAfterSomeDelay = (