diff --git a/features/wsteth/shared/wallet/wallet.tsx b/features/wsteth/shared/wallet/wallet.tsx index 2c5f4c5ab..9e0efca8b 100644 --- a/features/wsteth/shared/wallet/wallet.tsx +++ b/features/wsteth/shared/wallet/wallet.tsx @@ -1,5 +1,8 @@ +import { useConnectorInfo } from 'reef-knot/core-react'; import { Divider, Text } from '@lidofinance/lido-ui'; +import { useConfig } from 'config'; + import { FormatToken } from 'shared/formatters'; import { TokenToWallet } from 'shared/components'; import { @@ -8,15 +11,12 @@ import { useStethBalance, useWstethBalance, useWstethBySteth, - DAPP_CHAIN_TYPE, useStETHByWstETH, } from 'modules/web3'; +import { useIsLedgerLive } from 'shared/hooks/useIsLedgerLive'; import { CardBalance, CardRow, CardAccount, Fallback } from 'shared/wallet'; import { StyledCard } from './styles'; -import { useIsLedgerLive } from 'shared/hooks/useIsLedgerLive'; -import { useConfig } from 'config'; -import { useConnectorInfo } from 'reef-knot/core-react'; const WalletComponent = () => { const { isDappActiveOnL2 } = useDappStatus(); @@ -110,14 +110,12 @@ export const Wallet = ({ isUnwrapMode }: WrapWalletProps) => { const isLedgerLive = useIsLedgerLive(); const { isLedger: isLedgerHardware } = useConnectorInfo(); const { featureFlags } = useConfig().externalConfig; - const { chainType } = useDappStatus(); + const { isChainIdOnL2 } = useDappStatus(); const isLedgerLiveOptimism = - !featureFlags.ledgerLiveL2 && - isLedgerLive && - chainType === DAPP_CHAIN_TYPE.Optimism; - const isLedgerHardwareOptimism = - isLedgerHardware && chainType === DAPP_CHAIN_TYPE.Optimism; + !featureFlags.ledgerLiveL2 && isLedgerLive && isChainIdOnL2; + + const isLedgerHardwareOptimism = isLedgerHardware && isChainIdOnL2; if (isLedgerLiveOptimism || isLedgerHardwareOptimism) { const error = `Optimism is currently not supported in ${isLedgerLiveOptimism ? 'Ledger Live' : 'Ledger Hardware'}.`; diff --git a/features/wsteth/shared/wrap-faq/wrap-faq.tsx b/features/wsteth/shared/wrap-faq/wrap-faq.tsx index c2409724f..d9752e793 100644 --- a/features/wsteth/shared/wrap-faq/wrap-faq.tsx +++ b/features/wsteth/shared/wrap-faq/wrap-faq.tsx @@ -1,24 +1,28 @@ import React from 'react'; + +import { CHAINS } from 'consts/chains'; +import { useDappStatus } from 'modules/web3'; import { Section } from 'shared/components'; import { useMatomoEventHandle } from 'shared/hooks'; -import { useDappStatus, DAPP_CHAIN_TYPE } from 'modules/web3'; import { EthereumFAQ } from './ethereum-faq/faq'; import { OptimismFAQ } from './optimism-faq/faq'; export const faqComponentsMap = new Map([ - [DAPP_CHAIN_TYPE.Ethereum, EthereumFAQ], - [DAPP_CHAIN_TYPE.Optimism, OptimismFAQ], - // FAQ for other networks + [CHAINS.Mainnet, EthereumFAQ], + [CHAINS.Sepolia, EthereumFAQ], + [CHAINS.Holesky, EthereumFAQ], + [CHAINS.Optimism, OptimismFAQ], + [CHAINS.OptimismSepolia, OptimismFAQ], ]); export const WrapFaq = () => { - const { isWalletConnected, chainType } = useDappStatus(); + const { isWalletConnected, chainId } = useDappStatus(); const onClickHandler = useMatomoEventHandle(); const FAQ = !isWalletConnected ? EthereumFAQ - : faqComponentsMap.get(chainType) || EthereumFAQ; + : faqComponentsMap.get(chainId) || EthereumFAQ; return (
diff --git a/features/wsteth/unwrap/hooks/use-unwrap-tx-on-l2-approve.ts b/features/wsteth/unwrap/hooks/use-unwrap-tx-on-l2-approve.ts index 3341d2402..71a9b9fdd 100644 --- a/features/wsteth/unwrap/hooks/use-unwrap-tx-on-l2-approve.ts +++ b/features/wsteth/unwrap/hooks/use-unwrap-tx-on-l2-approve.ts @@ -13,7 +13,7 @@ type UseUnwrapTxApproveArgs = { }; export const useUnwrapTxOnL2Approve = ({ amount }: UseUnwrapTxApproveArgs) => { - const { isDappActiveOnL2, isChainTypeOnL2, address } = useDappStatus(); + const { isDappActiveOnL2, isChainIdOnL2, address } = useDappStatus(); const { l2, core } = useLidoSDKL2(); const { txModalStages } = useTxModalWrap(); @@ -77,8 +77,8 @@ export const useUnwrapTxOnL2Approve = ({ amount }: UseUnwrapTxApproveArgs) => { isAllowanceLoading, // There are 2 cases when we show the allowance on the unwrap page: // 1. wallet chain is any Optimism supported chain and chain switcher is Optimism (isDappActiveOnL2) - // 2. or wallet chain is any ETH supported chain, but chain switcher is Optimism (isChainTypeOnL2) - isShowAllowance: isDappActiveOnL2 || isChainTypeOnL2, + // 2. or wallet chain is any ETH supported chain, but chain switcher is Optimism (isChainIdOnL2) + isShowAllowance: isDappActiveOnL2 || isChainIdOnL2, }), [ processApproveTx, @@ -87,7 +87,7 @@ export const useUnwrapTxOnL2Approve = ({ amount }: UseUnwrapTxApproveArgs) => { isApprovalNeededBeforeUnwrap, isAllowanceLoading, isDappActiveOnL2, - isChainTypeOnL2, + isChainIdOnL2, ], ); }; diff --git a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx index 0ff8cf7fa..021e18f5e 100644 --- a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx +++ b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx @@ -17,25 +17,20 @@ import { useUnwrapFormData, UnwrapFormInputType } from '../unwrap-form-context'; import { TOKENS_TO_WRAP } from '../../shared/types'; export const UnwrapStats = () => { - const { isDappActiveOnL2, chainTypeChainId } = useDappStatus(); + const { isDappActiveOnL2, chainId } = useDappStatus(); const { allowance, isAllowanceLoading, isShowAllowance } = useUnwrapFormData(); const amount = useWatch({ name: 'amount' }); const unwrapGasLimit = useUnwrapGasLimit(); - // The 'unwrapGasLimit' difference between the networks is insignificant - // and can be neglected in the '!isChainTypeMatched' case - // - // Using the chainTypeChainId (chainId from the chain switcher) for TX calculation (and below for 'approveTxCostInUsd'), + // Using the chainId (chainId from the chain switcher) for TX calculation (and below for 'approveTxCostInUsd'), // because the statistics here are shown for the chain from the chain switcher const { txCostUsd: unwrapTxCostInUsd, isLoading: isUnwrapTxCostLoading } = - useTxCostInUsd(unwrapGasLimit, chainTypeChainId); + useTxCostInUsd(unwrapGasLimit, chainId); const approveGasLimit = useApproveGasLimit(); - // The 'approveGasLimit' difference between the networks is insignificant - // and can be neglected in the '!isChainTypeMatched' case const { txCostUsd: approveTxCostInUsd, isLoading: isApproveCostLoading } = - useTxCostInUsd(approveGasLimit, chainTypeChainId); + useTxCostInUsd(approveGasLimit, chainId); const { data: willReceiveStETH, isLoading: isWillReceiveStETHLoading } = useDebouncedStethByWsteth(amount); diff --git a/features/wsteth/wrap/hooks/use-wrap-tx-on-l1-approve.ts b/features/wsteth/wrap/hooks/use-wrap-tx-on-l1-approve.ts index c7ca37d12..098f92e57 100644 --- a/features/wsteth/wrap/hooks/use-wrap-tx-on-l1-approve.ts +++ b/features/wsteth/wrap/hooks/use-wrap-tx-on-l1-approve.ts @@ -22,7 +22,7 @@ export const useWrapTxOnL1Approve = ({ amount, token, }: UseWrapTxApproveArgs) => { - const { address, isWalletConnected, isDappActiveOnL1, isChainTypeOnL2 } = + const { address, isWalletConnected, isDappActiveOnL1, isChainIdOnL2 } = useDappStatus(); const { wrap } = useLidoSDK(); const { txModalStages } = useTxModalWrap(); @@ -90,9 +90,8 @@ export const useWrapTxOnL1Approve = ({ // There are 3 cases when we show the allowance on the wrap page: // 1. is wallet not connected (!isWalletConnected) // 2. or wallet chain is any ETH supported chain and chain switcher is ETH (isDappActiveOnL1) - // 3. or wallet chain is any Optimism supported chain, but chain switcher is ETH (!isChainTypeOnL2) - isShowAllowance: - !isWalletConnected || isDappActiveOnL1 || !isChainTypeOnL2, + // 3. or wallet chain is any Optimism supported chain, but chain switcher is ETH (!isChainIdOnL2) + isShowAllowance: !isWalletConnected || isDappActiveOnL1 || !isChainIdOnL2, }), [ processApproveTx, @@ -103,7 +102,7 @@ export const useWrapTxOnL1Approve = ({ refetchAllowance, isWalletConnected, isDappActiveOnL1, - isChainTypeOnL2, + isChainIdOnL2, ], ); }; diff --git a/features/wsteth/wrap/wrap-form/wrap-stats.tsx b/features/wsteth/wrap/wrap-form/wrap-stats.tsx index 5dd1ccdca..e395852c5 100644 --- a/features/wsteth/wrap/wrap-form/wrap-stats.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-stats.tsx @@ -16,7 +16,7 @@ import { useApproveGasLimit } from '../hooks/use-approve-gas-limit'; import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; export const WrapFormStats = () => { - const { isDappActive, chainTypeChainId } = useDappStatus(); + const { isDappActive, chainId } = useDappStatus(); const { allowance, isShowAllowance, wrapGasLimit, isAllowanceLoading } = useWrapFormData(); @@ -31,19 +31,14 @@ export const WrapFormStats = () => { const { data: oneWstethConverted, isLoading: oneWstethConvertedLoading } = useWstethBySteth(ONE_stETH); - // The 'approveGasLimit' difference between the networks is insignificant - // and can be neglected in the '!isChainTypeMatched' case - // - // Using the chainTypeChainId (chainId from the chain switcher) for TX calculation (and below for 'wrapTxCostInUsd'), + // Using the chainId (chainId from the chain switcher) for TX calculation (and below for 'wrapTxCostInUsd'), // because the statistics here are shown for the chain from the chain switcher const approveGasLimit = useApproveGasLimit(); const { txCostUsd: approveTxCostInUsd, isLoading: isApproveCostLoading } = - useTxCostInUsd(approveGasLimit, chainTypeChainId); + useTxCostInUsd(approveGasLimit, chainId); - // The 'wrapGasLimit' difference between the networks is insignificant - // and can be neglected in the '!isChainTypeMatched' case const { txCostUsd: wrapTxCostInUsd, isLoading: isWrapCostLoading } = - useTxCostInUsd(wrapGasLimit, chainTypeChainId); + useTxCostInUsd(wrapGasLimit, chainId); return ( diff --git a/modules/web3/consts/chains.ts b/modules/web3/consts/chains.ts new file mode 100644 index 000000000..9d2b2ac25 --- /dev/null +++ b/modules/web3/consts/chains.ts @@ -0,0 +1,43 @@ +import { ReactComponent as OptimismLogo } from 'assets/icons/chain-toggler/optimism.svg'; +import { ReactComponent as EthereumMainnetLogo } from 'assets/icons/chain-toggler/mainnet.svg'; + +import { CHAINS } from 'consts/chains'; + +export enum DAPP_CHAIN_TYPE { + Ethereum = 'Ethereum', + Optimism = 'Optimism', +} + +export type SupportedChainLabels = { + [key in DAPP_CHAIN_TYPE]: string; +}; + +export const ETHEREUM_CHAINS = new Set([ + CHAINS.Mainnet, + CHAINS.Holesky, + CHAINS.Sepolia, +]); + +export const OPTIMISM_CHAINS = new Set([ + CHAINS.Optimism, + CHAINS.OptimismSepolia, +]); + +export const getChainTypeByChainId = ( + chainId: number, +): DAPP_CHAIN_TYPE | null => { + if (ETHEREUM_CHAINS.has(chainId)) { + return DAPP_CHAIN_TYPE.Ethereum; + } else if (OPTIMISM_CHAINS.has(chainId)) { + return DAPP_CHAIN_TYPE.Optimism; + } + return null; +}; + +export const CHAIN_ICONS_MAP = new Map([ + [CHAINS.Mainnet, EthereumMainnetLogo], + [CHAINS.Holesky, EthereumMainnetLogo], + [CHAINS.Sepolia, EthereumMainnetLogo], + [CHAINS.Optimism, OptimismLogo], + [CHAINS.OptimismSepolia, OptimismLogo], +]); diff --git a/modules/web3/consts/index.ts b/modules/web3/consts/index.ts index a9009fc91..20063aa41 100644 --- a/modules/web3/consts/index.ts +++ b/modules/web3/consts/index.ts @@ -1,3 +1,4 @@ +export * from './chains'; export * from './symbols'; export * from './tx'; export * from './units'; diff --git a/modules/web3/hooks/use-dapp-status.ts b/modules/web3/hooks/use-dapp-status.ts index 742fbf03e..154ef12a1 100644 --- a/modules/web3/hooks/use-dapp-status.ts +++ b/modules/web3/hooks/use-dapp-status.ts @@ -1,5 +1,4 @@ import { useAccount } from 'wagmi'; -import { isSDKSupportedL2Chain } from 'consts/chains'; import { useDappChain } from 'modules/web3/web3-provider/dapp-chain'; @@ -13,19 +12,15 @@ export const useDappStatus = () => { // this can change between pages based on their dapp-chain context(or lack of) const dappChain = useDappChain(); - const { isSupportedChain, isChainTypeMatched } = dappChain; + const { isChainIdOnL2, isSupportedChain } = dappChain; const isAccountActive = walletChainId ? isWalletConnected && isSupportedChain : false; - const isL2 = isSDKSupportedL2Chain(walletChainId); - - const isDappActive = isAccountActive && isChainTypeMatched; - - const isDappActiveOnL1 = isDappActive && !isL2; - - const isDappActiveOnL2 = isDappActive && isL2; + const isDappActive = isAccountActive; + const isDappActiveOnL1 = isDappActive && !isChainIdOnL2; + const isDappActiveOnL2 = isDappActive && isChainIdOnL2; // no useMemo because memoisation is more expensive than boolean flags // hook is used in many places and every usage would create separate memoisation diff --git a/modules/web3/index.ts b/modules/web3/index.ts index 0d4f365b6..aeb589e30 100644 --- a/modules/web3/index.ts +++ b/modules/web3/index.ts @@ -7,5 +7,4 @@ export { useLidoSDK, useLidoSDKL2, SupportL2Chains, - DAPP_CHAIN_TYPE, } from './web3-provider'; diff --git a/modules/web3/web3-provider/dapp-chain.tsx b/modules/web3/web3-provider/dapp-chain.tsx index b4079e9a8..4073a2ccd 100644 --- a/modules/web3/web3-provider/dapp-chain.tsx +++ b/modules/web3/web3-provider/dapp-chain.tsx @@ -1,205 +1,50 @@ -import React, { - createContext, - useContext, - useState, - useMemo, - useEffect, -} from 'react'; +import React, { createContext, useContext } from 'react'; import invariant from 'tiny-invariant'; -import { CHAINS, isSDKSupportedL2Chain } from 'consts/chains'; -import { useAccount } from 'wagmi'; -import { config } from 'config'; import { ModalProvider } from 'providers/modal-provider'; -import { wagmiChainMap } from './web3-provider'; +import { useAppChainManager } from './use-app-chain-manager'; import { LidoSDKProvider } from './lido-sdk'; import { LidoSDKL2Provider } from './lido-sdk-l2'; -export enum DAPP_CHAIN_TYPE { - Ethereum = 'Ethereum', - Optimism = 'Optimism', -} +type DappChainContextValue = ReturnType; -type DappChainContextValue = { - chainType: DAPP_CHAIN_TYPE; - setChainType: React.Dispatch>; - supportedChainIds: number[]; - isChainTypeMatched: boolean; - isChainTypeOnL2: boolean; -}; - -export type SupportedChainLabels = { - [key in DAPP_CHAIN_TYPE]: string; -}; - -type UseDappChainValue = { - // Current DApp chain ID (may not match with chainType) - chainId: number; - // Chain ID by current chainType - chainTypeChainId: number; - - isSupportedChain: boolean; - supportedChainTypes: DAPP_CHAIN_TYPE[]; - supportedChainLabels: SupportedChainLabels; -} & DappChainContextValue; - -const DappChainContext = createContext(null); +export const DappChainContext = createContext( + null, +); DappChainContext.displayName = 'DappChainContext'; -const ETHEREUM_CHAINS = new Set([ - CHAINS.Mainnet, - CHAINS.Holesky, - CHAINS.Sepolia, -]); - -const OPTIMISM_CHAINS = new Set([CHAINS.Optimism, CHAINS.OptimismSepolia]); - -const getChainTypeByChainId = (chainId?: number): DAPP_CHAIN_TYPE | null => { - if (!chainId) return null; - if (ETHEREUM_CHAINS.has(chainId)) { - return DAPP_CHAIN_TYPE.Ethereum; - } else if (OPTIMISM_CHAINS.has(chainId)) { - return DAPP_CHAIN_TYPE.Optimism; - } - return null; -}; - -// At the current stage of the widget we don't care what ID is returned: -// - 'chainTypeChainId' is only used for statistics; -// - on the prod environment, the 'function map' of 'chainType' to 'chainId' will be 1 to 1 (bijective mapping). -const getChainIdByChainType = ( - chainType: DAPP_CHAIN_TYPE, - supportedChainIds: number[], -): number | undefined => - supportedChainIds.find((id) => getChainTypeByChainId(id) === chainType); - -export const useDappChain = (): UseDappChainValue => { - const context = useContext(DappChainContext); - invariant(context, 'useDappChain was used outside of DappChainProvider'); - const { chainId: walletChain } = useAccount(); - - return useMemo(() => { - const supportedChainTypes = context.supportedChainIds - .map(getChainTypeByChainId) - .filter( - (chainType, index, array) => - // duplicate/invalid pruning + stable order - chainType && array.indexOf(chainType) === index, - ) as DAPP_CHAIN_TYPE[]; - - const getChainLabelByType = (chainType: DAPP_CHAIN_TYPE) => { - // all testnets for chainType - const testnetsForType = context.supportedChainIds - .filter((id) => chainType == getChainTypeByChainId(id)) - .map((id) => wagmiChainMap[id]) - .filter((chain) => chain.testnet) - .map((chain) => chain.name); - - return ( - chainType + - (testnetsForType.length > 0 ? `(${testnetsForType.join(',')})` : '') - ); - }; - - const supportedChainLabels = supportedChainTypes.reduce( - (acc, chainType) => ({ - ...acc, - [chainType]: getChainLabelByType(chainType), - }), - {}, - ) as SupportedChainLabels; - - const chainTypeChainId = - getChainIdByChainType(context.chainType, context.supportedChainIds) ?? - config.defaultChain; +export const SupportL1Chains: React.FC = ({ + children, +}) => { + const appChainManager = useAppChainManager(false); - return { - ...context, - chainId: - walletChain && context.supportedChainIds.includes(walletChain) - ? walletChain - : config.defaultChain, - chainTypeChainId, - isSupportedChain: walletChain - ? context.supportedChainIds.includes(walletChain) - : true, - supportedChainTypes, - supportedChainLabels, - }; - }, [context, walletChain]); + return ( + + + {/* Stub LidoSDKL2Provider for hooks that gives isL2:false. Will be overriden in SupportL2Chains */} + {children} + + + ); }; export const SupportL2Chains: React.FC = ({ children, }) => { - const { chainId: walletChainId, isConnected } = useAccount(); - const [chainType, setChainType] = useState( - DAPP_CHAIN_TYPE.Ethereum, - ); - - useEffect(() => { - if (!walletChainId || !config.supportedChains.includes(walletChainId)) { - // This code resets 'chainType' to ETH when the wallet is disconnected. - // It also works on the first rendering, but we don't care, because the 'chainType' by default is ETH. - // Don't use it if you need to do something strictly, only when the wallet is disconnected. - setChainType(DAPP_CHAIN_TYPE.Ethereum); - return; - } - - if (isConnected) { - const newChainType = getChainTypeByChainId(walletChainId); - if (newChainType) setChainType(newChainType); - } - }, [walletChainId, isConnected, setChainType]); + const appChainManager = useAppChainManager(true); return ( - ({ - chainType, - setChainType, - supportedChainIds: config.supportedChains, - isChainTypeMatched: - chainType === getChainTypeByChainId(walletChainId), - // At the moment a simple check is enough for us, - // however in the future we will either rethink this flag - // or use an array or Set (for example with L2_DAPP_CHAINS_TYPE) - isChainTypeOnL2: chainType === DAPP_CHAIN_TYPE.Optimism, - }), - [chainType, walletChainId], - )} - > + - {/* Some modals depend on the LidoSDKL2Provider */} {children} ); }; -const onlyL1ChainsValue = { - chainType: DAPP_CHAIN_TYPE.Ethereum, - // only L1 chains - supportedChainIds: config.supportedChains.filter( - (chain) => !isSDKSupportedL2Chain(chain), - ), - isChainTypeMatched: true, - isChainTypeOnL2: false, - setChainType: () => {}, +export const useDappChain = () => { + const context = useContext(DappChainContext); + invariant(context, 'useDappChain was used outside of DappChainProvider'); + return context; }; - -// Value of this context only allows L1 chains and no chain switch -// this is actual for most pages and can be overriden by SupportL2Chains on per page basis -// for safety reasons this cannot be default context value -// in order to prevent accidental useDappChain/useDappStatus misusage in top-lvl components -export const SupportL1Chains: React.FC = ({ - children, -}) => ( - - - {/* Stub LidoSDKL2Provider for hooks that gives isL2:false. Will be overriden in SupportL2Chains */} - {children} - - -); diff --git a/modules/web3/web3-provider/index.ts b/modules/web3/web3-provider/index.ts index 649d9795c..56594c92c 100644 --- a/modules/web3/web3-provider/index.ts +++ b/modules/web3/web3-provider/index.ts @@ -1,4 +1,4 @@ export { Web3Provider, useMainnetOnlyWagmi } from './web3-provider'; export { useLidoSDK } from './lido-sdk'; export { useLidoSDKL2 } from './lido-sdk-l2'; -export { SupportL2Chains, DAPP_CHAIN_TYPE } from './dapp-chain'; +export { SupportL2Chains } from './dapp-chain'; diff --git a/modules/web3/web3-provider/lido-sdk.tsx b/modules/web3/web3-provider/lido-sdk.tsx index 6117b7669..f2f3b1341 100644 --- a/modules/web3/web3-provider/lido-sdk.tsx +++ b/modules/web3/web3-provider/lido-sdk.tsx @@ -1,12 +1,6 @@ -import { createContext, useContext, useEffect, useMemo } from 'react'; +import { createContext, useContext, useMemo } from 'react'; import invariant from 'tiny-invariant'; -import { - useAccount, - useConfig, - usePublicClient, - useSwitchChain, - useWalletClient, -} from 'wagmi'; +import { usePublicClient, useWalletClient } from 'wagmi'; import { LidoSDKStake } from '@lidofinance/lido-ethereum-sdk/stake'; import { CHAINS, LidoSDKCore } from '@lidofinance/lido-ethereum-sdk/core'; @@ -18,7 +12,6 @@ import { LidoSDKWrap } from '@lidofinance/lido-ethereum-sdk/wrap'; import { LidoSDKWithdraw } from '@lidofinance/lido-ethereum-sdk/withdraw'; import { LidoSDKStatistics } from '@lidofinance/lido-ethereum-sdk/statistics'; -import { config } from 'config'; import { useTokenTransferSubscription } from 'modules/web3/hooks/use-balance'; import { useDappChain } from './dapp-chain'; @@ -50,24 +43,6 @@ export const LidoSDKProvider = ({ children }: React.PropsWithChildren) => { const { chainId } = useDappChain(); const { data: walletClient } = useWalletClient({ chainId }); const publicClient = usePublicClient({ chainId }); - // reset internal wagmi state after disconnect - const { isConnected } = useAccount(); - - const wagmiConfig = useConfig(); - const { switchChain } = useSwitchChain(); - useEffect(() => { - if (isConnected) { - return () => { - // protecs from side effect double run - if (!wagmiConfig.state.current) { - switchChain({ - chainId: config.defaultChain, - }); - } - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isConnected]); const contextValue = useMemo(() => { // @ts-expect-error: typing (viem + LidoSDK) diff --git a/modules/web3/web3-provider/use-app-chain-manager.ts b/modules/web3/web3-provider/use-app-chain-manager.ts new file mode 100644 index 000000000..e40eb97f4 --- /dev/null +++ b/modules/web3/web3-provider/use-app-chain-manager.ts @@ -0,0 +1,126 @@ +import { useCallback, useMemo, useEffect } from 'react'; +import { useAccount, useConfig, useChainId, useSwitchChain } from 'wagmi'; + +import { config } from 'config'; +import { isSDKSupportedL2Chain } from 'consts/chains'; + +import { + getChainTypeByChainId, + DAPP_CHAIN_TYPE, + SupportedChainLabels, +} from '../consts'; +import { wagmiChainMap } from './web3-provider'; + +export const useAppChainManager = (supportedL2: boolean) => { + const chainId = useChainId(); + const { switchChain, switchChainAsync } = useSwitchChain(); + + // reset internal wagmi state after disconnect + const { isConnected, chainId: walletChain } = useAccount(); + const wagmiConfig = useConfig(); + useEffect(() => { + if (isConnected) { + return () => { + // protecs from side effect double run + if (!wagmiConfig.state.current) { + switchChain({ + chainId: config.defaultChain, + }); + } + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]); + + const supportedChainIds = useMemo( + () => + config.supportedChains.filter((chain) => + supportedL2 ? true : !isSDKSupportedL2Chain(chain), + ), + [supportedL2], + ); + + // Supported chain over the page + const dappChainId = useMemo(() => { + return supportedChainIds.includes(chainId) ? chainId : config.defaultChain; + }, [chainId, supportedChainIds]); + + const switchChainId = useCallback( + async (newChainId: number): Promise => { + if (walletChain === newChainId) return; + + if (!supportedChainIds.includes(newChainId)) { + throw new Error( + `Error switching chain (${newChainId} is unsupported chain)`, + ); + } + + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error('Error switching chain (timeout)')), + 5000, + ), + ); + + await Promise.race([ + switchChainAsync({ chainId: newChainId }), + timeoutPromise, + ]); + }, + [supportedChainIds, switchChainAsync, walletChain], + ); + + const [isChainIdOnL2, supportedChainLabels] = useMemo(() => { + // all L2 networks supported by the widget will be supported in SDK + const isChainIdOnL2 = isSDKSupportedL2Chain(dappChainId); + + const supportedChainTypes = supportedChainIds + .map(getChainTypeByChainId) + .filter( + (chainType, index, array) => + // duplicate/invalid pruning + stable order + chainType && array.indexOf(chainType) === index, + ) as DAPP_CHAIN_TYPE[]; + + const getChainLabelByType = (chainType: DAPP_CHAIN_TYPE) => { + // all testnets for chainType + const testnetsForType = supportedChainIds + .filter((id) => chainType == getChainTypeByChainId(id)) + .map((id) => wagmiChainMap[id]) + .filter((chain) => chain.testnet) + .map((chain) => chain.name); + + return ( + chainType + + (testnetsForType.length > 0 ? `(${testnetsForType.join(',')})` : '') + ); + }; + + const supportedChainLabels = supportedChainTypes.reduce( + (acc, chainType) => ({ + ...acc, + [chainType]: getChainLabelByType(chainType), + }), + {}, + ) as SupportedChainLabels; + + return [isChainIdOnL2, supportedChainLabels]; + }, [dappChainId, supportedChainIds]); + + return { + chainId: dappChainId, + switchChainId, + wagmiChain: wagmiChainMap[dappChainId], + + wagmiDefaultChain: wagmiChainMap[config.defaultChain], + + isChainIdOnL2, + supportedL2, + + isSupportedChain: walletChain + ? supportedChainIds.includes(walletChain) + : true, + supportedChainIds, + supportedChainLabels, + }; +}; diff --git a/package.json b/package.json index 1e61c1220..ee4db2f36 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "tiny-async-pool": "^1.2.0", "tiny-invariant": "^1.1.0", "uuid": "^8.3.2", - "viem": "2.21.54", - "wagmi": "2.13.4" + "viem": "2.22.16", + "wagmi": "2.14.9" }, "devDependencies": { "@commitlint/cli": "^17.4.4", diff --git a/shared/components/layout/header/components/chain-switcher/chain-switcher.tsx b/shared/components/layout/header/components/chain-switcher/chain-switcher.tsx index 459fd5643..8c0df7028 100644 --- a/shared/components/layout/header/components/chain-switcher/chain-switcher.tsx +++ b/shared/components/layout/header/components/chain-switcher/chain-switcher.tsx @@ -1,9 +1,20 @@ -import { FC, useState, useRef, ReactNode } from 'react'; -import { DAPP_CHAIN_TYPE } from 'modules/web3'; -import { useDappStatus } from 'modules/web3'; +import { + FC, + useState, + useRef, + useMemo, + createElement, + ComponentType, +} from 'react'; + +import { CHAIN_ICONS_MAP, useDappStatus } from 'modules/web3'; +import { wagmiChainMap } from 'modules/web3/web3-provider/web3-provider'; import { useClickOutside } from './hooks/use-click-outside'; -import { ChainSwitcherOptions } from './components/chain-switcher-options/chain-switcher-options'; +import { + ChainSwitcherOptions, + ChainOption, +} from './components/chain-switcher-options/chain-switcher-options'; import { SelectIconTooltip } from './components/select-icon-tooltip/select-icon-tooltip'; import { ChainSwitcherWrapperStyled, @@ -12,42 +23,64 @@ import { ArrowStyle, } from './styles'; -import { ReactComponent as OptimismLogo } from 'assets/icons/chain-toggler/optimism.svg'; -import { ReactComponent as EthereumMainnetLogo } from 'assets/icons/chain-toggler/mainnet.svg'; - -const iconsMap: Record = { - [DAPP_CHAIN_TYPE.Ethereum]: , - [DAPP_CHAIN_TYPE.Optimism]: , -}; +type IconsMapType = Record; export const ChainSwitcher: FC = () => { - const { isDappActive, chainType, supportedChainTypes, setChainType } = + const { isDappActive, chainId, switchChainId, supportedChainIds } = useDappStatus(); + const [opened, setOpened] = useState(false); + const [isLocked, setIsLocked] = useState(supportedChainIds.length < 2); const selectRef = useRef(null); - const isChainTypeUnlocked = supportedChainTypes.length > 1; - useClickOutside(selectRef, () => setOpened(false)); + const iconsMap = useMemo( + () => + supportedChainIds.reduce((acc: IconsMapType, chainId: number) => { + acc[chainId] = { + name: wagmiChainMap[chainId].name, + iconComponent: CHAIN_ICONS_MAP.has(Number(chainId)) + ? createElement( + CHAIN_ICONS_MAP.get(Number(chainId)) as ComponentType, + ) + : null, + }; + return acc; + }, {}), + [supportedChainIds], + ); + return ( setOpened((prev) => !prev)} + $disabled={isLocked} + $showArrow={!isLocked} + onClick={() => { + if (!isLocked) { + setOpened((prev) => !prev); + } + }} > - {iconsMap[chainType]} - {isChainTypeUnlocked && } + {iconsMap[chainId].iconComponent} + {!isLocked && } - {isChainTypeUnlocked && ( + {!isLocked && ( <> { - setChainType(chainType); + currentChainId={chainId} + onSelect={async (chainId) => { setOpened(false); + setIsLocked(true); + try { + await switchChainId(chainId); + } catch (err) { + console.warn(`[chain-switcher.tsx] ${err}`); + } finally { + setIsLocked(false); + } }} opened={opened} options={iconsMap} diff --git a/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/chain-switcher-options.tsx b/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/chain-switcher-options.tsx index b2a546bfc..9e93ef16f 100644 --- a/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/chain-switcher-options.tsx +++ b/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/chain-switcher-options.tsx @@ -1,17 +1,18 @@ import { FC, ReactNode } from 'react'; -import { DAPP_CHAIN_TYPE } from 'modules/web3'; import { PopoverWrapperStyled, PopupStyled, OptionStyled } from './styles'; +export type ChainOption = { name: string; iconComponent: ReactNode }; + interface ChainSwitcherOptionsProps { - currentChainType: DAPP_CHAIN_TYPE; - onSelect: (chainType: DAPP_CHAIN_TYPE) => void; + currentChainId: number; + onSelect: (chainId: number) => void; opened: boolean; - options: Record; + options: Record; } export const ChainSwitcherOptions: FC = ({ - currentChainType, + currentChainId, onSelect, opened, options, @@ -21,13 +22,13 @@ export const ChainSwitcherOptions: FC = ({ <> - {Object.entries(options).map(([chainType, icon]) => ( + {Object.entries(options).map(([chainId, chainOption]) => ( onSelect(chainType as DAPP_CHAIN_TYPE)} - $active={chainType === currentChainType} + key={chainId} + onClick={() => onSelect(Number(chainId))} + $active={Number(chainId) === currentChainId} > - {icon} {chainType} + {chainOption.iconComponent} {chainOption.name} ))} diff --git a/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/styles.tsx b/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/styles.tsx index 4246cc81a..5e637938d 100644 --- a/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/styles.tsx +++ b/shared/components/layout/header/components/chain-switcher/components/chain-switcher-options/styles.tsx @@ -42,7 +42,7 @@ const hiddenCSS = css` export const PopupStyled = styled.div` z-index: 201; - min-width: 115px; + min-width: 120px; position: absolute; top: 48px; diff --git a/shared/components/layout/header/components/chain-switcher/styles.tsx b/shared/components/layout/header/components/chain-switcher/styles.tsx index f2e15c1a8..ccca2965d 100644 --- a/shared/components/layout/header/components/chain-switcher/styles.tsx +++ b/shared/components/layout/header/components/chain-switcher/styles.tsx @@ -4,7 +4,10 @@ export const ChainSwitcherWrapperStyled = styled.div` position: relative; `; -export const ChainSwitcherStyled = styled.div<{ $disabled: boolean }>` +export const ChainSwitcherStyled = styled.div<{ + $disabled: boolean; + $showArrow: boolean; +}>` z-index: 202; display: inline-flex; @@ -16,7 +19,7 @@ export const ChainSwitcherStyled = styled.div<{ $disabled: boolean }>` overflow: ${({ $disabled }) => ($disabled ? 'hidden' : 'visible')}; box-sizing: border-box; - width: ${({ $disabled }) => ($disabled ? '44px' : '68px')}; + width: ${({ $showArrow }) => ($showArrow ? '68px' : '44px')}; height: 44px; margin-right: 12px; padding: 9px 8px; diff --git a/shared/components/layout/header/components/header-wallet.tsx b/shared/components/layout/header/components/header-wallet.tsx index 19cd65234..3279835ca 100644 --- a/shared/components/layout/header/components/header-wallet.tsx +++ b/shared/components/layout/header/components/header-wallet.tsx @@ -24,13 +24,10 @@ import { ThemeTogglerStyled } from './styles'; const HeaderWallet: FC = () => { const router = useRouter(); const { defaultChain: defaultChainId } = useUserConfig(); - const { isDappActive, address, walletChainId } = useDappStatus(); + const { isDappActive, address, walletChainId, wagmiChain } = useDappStatus(); const chainName = CHAINS[walletChainId || defaultChainId]; - const testNet = !( - walletChainId === CHAINS.Mainnet || walletChainId === CHAINS.Optimism - ); - const showNet = testNet && isDappActive; + const showNet = wagmiChain.testnet && isDappActive; const queryTheme = router?.query?.theme; const chainColor = useMemo( diff --git a/shared/wallet/button/button.tsx b/shared/wallet/button/button.tsx index a3d538e20..4807730ed 100644 --- a/shared/wallet/button/button.tsx +++ b/shared/wallet/button/button.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import type { Address } from 'viem'; -import { ButtonProps, useBreakpoint } from '@lidofinance/lido-ui'; +import { ButtonProps } from '@lidofinance/lido-ui'; import { FormatToken } from 'shared/formatters'; import { useDappStatus, useEthereumBalance } from 'modules/web3'; @@ -18,7 +18,6 @@ import { export const Button: FC = (props) => { const { onClick, ...rest } = props; - const isMobile = useBreakpoint('md'); const { isDappActive, address } = useDappStatus(); const { openModal } = useWalletModal(); @@ -30,23 +29,22 @@ export const Button: FC = (props) => { variant="text" color="secondary" onClick={() => openModal({})} - $isAddPaddingLeft={!isLoading && !isDappActive && !isMobile} {...rest} > - - {isLoading ? ( - - ) : ( - isDappActive && ( + {isLoading ? ( + + ) : ( + isDappActive && ( + - ) - )} - + + ) + )} diff --git a/shared/wallet/button/styles.tsx b/shared/wallet/button/styles.tsx index 7091eba93..fc9c3769c 100644 --- a/shared/wallet/button/styles.tsx +++ b/shared/wallet/button/styles.tsx @@ -6,8 +6,6 @@ export const WalledButtonStyle = styled((props) =>