diff --git a/cypress/e2e/2-settings/wallet-connect.cy.ts b/cypress/e2e/2-settings/wallet-connect.cy.ts index b2dd40b1a8..7666f324b7 100644 --- a/cypress/e2e/2-settings/wallet-connect.cy.ts +++ b/cypress/e2e/2-settings/wallet-connect.cy.ts @@ -17,8 +17,9 @@ describe('Manipulation on the wallet connect', () => { const walletButton = '#wallet-button'; it('step1:Disconnect wallet', () => { + cy.wait(1000); cy.get(walletButton).click(); - cy.contains('Disconnect Wallet').click(); + cy.contains('Disconnect').click(); cy.contains('Please, connect your wallet').should('be.visible'); }); diff --git a/cypress/support/steps/configuration.steps.ts b/cypress/support/steps/configuration.steps.ts index 93c7908182..10cc7b1134 100644 --- a/cypress/support/steps/configuration.steps.ts +++ b/cypress/support/steps/configuration.steps.ts @@ -59,7 +59,6 @@ export const configEnvWithTenderly = ({ win.localStorage.setItem('selectedAccount', walletAddress.toLowerCase()); win.localStorage.setItem('selectedMarket', market); win.localStorage.setItem('testnetsEnabled', enableTestnet.toString()); - win.localStorage.setItem('mockWalletAddress', walletAddress.toLowerCase()); }, }); }); diff --git a/src/components/WalletConnection/WalletSelector.tsx b/src/components/WalletConnection/WalletSelector.tsx index 06fe4446cc..d7f8def080 100644 --- a/src/components/WalletConnection/WalletSelector.tsx +++ b/src/components/WalletConnection/WalletSelector.tsx @@ -1,10 +1,23 @@ import { Trans } from '@lingui/macro'; -import { Alert, Box, Button, Link, Typography } from '@mui/material'; +import { + Alert, + Box, + Button, + InputBase, + Link, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; import { UnsupportedChainIdError } from '@web3-react/core'; import { NoEthereumProviderError } from '@web3-react/injected-connector'; import { UserRejectedRequestError } from '@web3-react/walletconnect-connector'; +import { utils } from 'ethers'; +import { useState } from 'react'; +import { WatchOnlyModeTooltip } from 'src/components/infoTooltips/WatchOnlyModeTooltip'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { WalletType } from 'src/libs/web3-data-provider/WalletOptions'; +import { getENSProvider } from 'src/utils/marketsAndNetworksConfig'; import { TxModalTitle } from '../transactions/FlowCommons/TxModalTitle'; @@ -95,7 +108,12 @@ export enum ErrorType { } export const WalletSelector = () => { - const { error } = useWeb3Context(); + const { error, updateWatchModeOnlyAddress } = useWeb3Context(); + const [inputMockWalletAddress, setInputMockWalletAddress] = useState(''); + const [validAddressError, setValidAddressError] = useState(false); + const { breakpoints } = useTheme(); + const sm = useMediaQuery(breakpoints.down('sm')); + const mainnetProvider = getENSProvider(); let blockingError: ErrorType | undefined = undefined; if (error) { @@ -125,6 +143,31 @@ export const WalletSelector = () => { } }; + const handleWatchAddress = async (inputMockWalletAddress: string): Promise => { + if (validAddressError) setValidAddressError(false); + if (utils.isAddress(inputMockWalletAddress)) { + updateWatchModeOnlyAddress(inputMockWalletAddress); + } else { + // Check if address could be valid ENS before trying to resolve + if (inputMockWalletAddress.slice(-4) !== '.eth') { + setValidAddressError(true); + } else { + // Attempt to resolve ENS name and use resolved address if valid + const resolvedAddress = await mainnetProvider.resolveName(inputMockWalletAddress); + if (resolvedAddress && utils.isAddress(resolvedAddress)) { + updateWatchModeOnlyAddress(resolvedAddress); + } else { + setValidAddressError(true); + } + } + } + }; + + const handleSubmit = (event: React.FormEvent): void => { + event.preventDefault(); + handleWatchAddress(inputMockWalletAddress); + }; + return ( @@ -150,6 +193,56 @@ export const WalletSelector = () => { /> + + + Enter an address to track in watch-only mode + + + +
+ ({ + py: 1, + px: 3, + border: `1px solid ${theme.palette.divider}`, + borderRadius: '6px', + mb: 1, + overflow: 'show', + fontSize: sm ? '16px' : '14px', + })} + placeholder="Enter ethereum address or ENS name" + fullWidth + autoFocus + value={inputMockWalletAddress} + onChange={(e) => setInputMockWalletAddress(e.target.value)} + inputProps={{ + 'aria-label': 'watch mode only address', + }} + /> + + + {validAddressError && ( + + Please enter a valid wallet address. + + )} Need help connecting a wallet?{' '} diff --git a/src/components/infoTooltips/WatchOnlyModeTooltip.tsx b/src/components/infoTooltips/WatchOnlyModeTooltip.tsx new file mode 100644 index 0000000000..20c6726755 --- /dev/null +++ b/src/components/infoTooltips/WatchOnlyModeTooltip.tsx @@ -0,0 +1,14 @@ +import { Trans } from '@lingui/macro'; + +import { TextWithTooltip, TextWithTooltipProps } from '../TextWithTooltip'; + +export const WatchOnlyModeTooltip = ({ ...rest }: TextWithTooltipProps) => { + return ( + + + Watch-only mode allows to see address positions in Aave, but you won't be able to + perform transactions. + + + ); +}; diff --git a/src/components/transactions/ClaimRewards/ClaimRewardsModalContent.tsx b/src/components/transactions/ClaimRewards/ClaimRewardsModalContent.tsx index 16a8898f9f..28d9cf1667 100644 --- a/src/components/transactions/ClaimRewards/ClaimRewardsModalContent.tsx +++ b/src/components/transactions/ClaimRewards/ClaimRewardsModalContent.tsx @@ -33,7 +33,7 @@ export const ClaimRewardsModalContent = () => { const { gasLimit, mainTxState: claimRewardsTxState, txError } = useModalContext(); const { user, reserves } = useAppDataContext(); const { currentChainId, currentMarketData, currentMarket } = useProtocolDataContext(); - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const [claimableUsd, setClaimableUsd] = useState('0'); const [selectedRewardSymbol, setSelectedRewardSymbol] = useState('all'); const [rewards, setRewards] = useState([]); @@ -140,7 +140,7 @@ export const ClaimRewardsModalContent = () => { return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} diff --git a/src/components/transactions/Emode/EmodeModalContent.tsx b/src/components/transactions/Emode/EmodeModalContent.tsx index 766ae0af65..85062276ac 100644 --- a/src/components/transactions/Emode/EmodeModalContent.tsx +++ b/src/components/transactions/Emode/EmodeModalContent.tsx @@ -72,7 +72,7 @@ export const EmodeModalContent = ({ mode }: EmodeModalContentProps) => { userReserves, } = useAppDataContext(); const { currentChainId } = useProtocolDataContext(); - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const currentTimestamp = useCurrentTimestamp(1); const { gasLimit, mainTxState: emodeTxState, txError } = useModalContext(); @@ -178,7 +178,7 @@ export const EmodeModalContent = ({ mode }: EmodeModalContentProps) => { return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} diff --git a/src/components/transactions/FlowCommons/ModalWrapper.tsx b/src/components/transactions/FlowCommons/ModalWrapper.tsx index 60ffc6e3ba..c7d225bce7 100644 --- a/src/components/transactions/FlowCommons/ModalWrapper.tsx +++ b/src/components/transactions/FlowCommons/ModalWrapper.tsx @@ -46,7 +46,7 @@ export const ModalWrapper: React.FC<{ requiredPermission, keepWrappedSymbol, }) => { - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { walletBalances } = useWalletBalances(); const { currentChainId: marketChainId, @@ -95,7 +95,7 @@ export const ModalWrapper: React.FC<{ {!mainTxState.success && ( )} - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( { - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { daveTokens: { aave, stkAave }, } = useAaveTokensProviderContext(); @@ -101,7 +101,7 @@ export const GovDelegationModalContent = () => { return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} diff --git a/src/components/transactions/GovVote/GovVoteModalContent.tsx b/src/components/transactions/GovVote/GovVoteModalContent.tsx index 2d53313919..46f0ad477f 100644 --- a/src/components/transactions/GovVote/GovVoteModalContent.tsx +++ b/src/components/transactions/GovVote/GovVoteModalContent.tsx @@ -36,7 +36,7 @@ export const GovVoteModalContent = ({ support, power: votingPower, }: GovVoteModalContentProps) => { - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { gasLimit, mainTxState: txState, txError } = useModalContext(); const { currentNetworkConfig, currentChainId } = useProtocolDataContext(); @@ -78,7 +78,7 @@ export const GovVoteModalContent = ({ return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} {blockingError !== undefined && ( diff --git a/src/components/transactions/Stake/StakeModalContent.tsx b/src/components/transactions/Stake/StakeModalContent.tsx index dd2bdf0986..ed9cbc4a95 100644 --- a/src/components/transactions/Stake/StakeModalContent.tsx +++ b/src/components/transactions/Stake/StakeModalContent.tsx @@ -33,7 +33,7 @@ type StakingType = 'aave' | 'bpt'; export const StakeModalContent = ({ stakeAssetName, icon }: StakeProps) => { const data = useStakeData(); const stakeData = data.stakeGeneralResult?.stakeGeneralUIData[stakeAssetName as StakingType]; - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { gasLimit, mainTxState: txState, txError } = useModalContext(); const { currentNetworkConfig, currentChainId } = useProtocolDataContext(); @@ -97,7 +97,7 @@ export const StakeModalContent = ({ stakeAssetName, icon }: StakeProps) => { return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} diff --git a/src/components/transactions/StakeCooldown/StakeCooldownModalContent.tsx b/src/components/transactions/StakeCooldown/StakeCooldownModalContent.tsx index 50bda2a4d2..bbe87247ec 100644 --- a/src/components/transactions/StakeCooldown/StakeCooldownModalContent.tsx +++ b/src/components/transactions/StakeCooldown/StakeCooldownModalContent.tsx @@ -34,7 +34,7 @@ type StakingType = 'aave' | 'bpt'; export const StakeCooldownModalContent = ({ stakeAssetName }: StakeCooldownProps) => { const { stakeUserResult, stakeGeneralResult } = useStakeData(); - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { gasLimit, mainTxState: txState, txError } = useModalContext(); const { currentNetworkConfig, currentChainId } = useProtocolDataContext(); @@ -108,7 +108,7 @@ export const StakeCooldownModalContent = ({ stakeAssetName }: StakeCooldownProps return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} diff --git a/src/components/transactions/StakeRewardClaim/StakeRewardClaimModalContent.tsx b/src/components/transactions/StakeRewardClaim/StakeRewardClaimModalContent.tsx index ea3ff1175f..326b63a688 100644 --- a/src/components/transactions/StakeRewardClaim/StakeRewardClaimModalContent.tsx +++ b/src/components/transactions/StakeRewardClaim/StakeRewardClaimModalContent.tsx @@ -30,7 +30,7 @@ type StakingType = 'aave' | 'bpt'; export const StakeRewardClaimModalContent = ({ stakeAssetName }: StakeRewardClaimProps) => { const data = useStakeData(); const stakeData = data.stakeGeneralResult?.stakeGeneralUIData[stakeAssetName as StakingType]; - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { gasLimit, mainTxState: txState, txError } = useModalContext(); const { currentNetworkConfig, currentChainId } = useProtocolDataContext(); @@ -89,7 +89,7 @@ export const StakeRewardClaimModalContent = ({ stakeAssetName }: StakeRewardClai return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} {blockingError !== undefined && ( diff --git a/src/components/transactions/TxActionsWrapper.tsx b/src/components/transactions/TxActionsWrapper.tsx index d413206447..26c3f0b846 100644 --- a/src/components/transactions/TxActionsWrapper.tsx +++ b/src/components/transactions/TxActionsWrapper.tsx @@ -1,8 +1,9 @@ import { Trans } from '@lingui/macro'; -import { Box, BoxProps, Button, CircularProgress } from '@mui/material'; +import { Box, BoxProps, Button, CircularProgress, Typography } from '@mui/material'; import isEmpty from 'lodash/isEmpty'; import { ReactNode } from 'react'; import { TxStateType, useModalContext } from 'src/hooks/useModal'; +import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { TxAction } from 'src/ui-config/errorMapping'; import { LeftHelperText } from './FlowCommons/LeftHelperText'; @@ -42,6 +43,7 @@ export const TxActionsWrapper = ({ ...rest }: TxActionsWrapperProps) => { const { txError, retryWithApproval } = useModalContext(); + const { watchModeOnlyAddress } = useWeb3Context(); const hasApprovalError = requiresApproval && txError && txError.txAction === TxAction.APPROVAL && txError.actionBlocked; @@ -87,14 +89,14 @@ export const TxActionsWrapper = ({ return ( - {requiresApproval && ( + {requiresApproval && !watchModeOnlyAddress && ( )} - {approvalParams && ( + {approvalParams && !watchModeOnlyAddress && ( + {watchModeOnlyAddress && ( + + Watch-only mode. Connect to a wallet to perform transactions. + + )} ); }; diff --git a/src/components/transactions/UnStake/UnStakeModalContent.tsx b/src/components/transactions/UnStake/UnStakeModalContent.tsx index 3bbc8925db..49e653fac0 100644 --- a/src/components/transactions/UnStake/UnStakeModalContent.tsx +++ b/src/components/transactions/UnStake/UnStakeModalContent.tsx @@ -33,7 +33,7 @@ type StakingType = 'aave' | 'bpt'; export const UnStakeModalContent = ({ stakeAssetName, icon }: UnStakeProps) => { const data = useStakeData(); const stakeData = data.stakeGeneralResult?.stakeGeneralUIData[stakeAssetName as StakingType]; - const { chainId: connectedChainId } = useWeb3Context(); + const { chainId: connectedChainId, watchModeOnlyAddress } = useWeb3Context(); const { gasLimit, mainTxState: txState, txError } = useModalContext(); const { currentNetworkConfig, currentChainId } = useProtocolDataContext(); @@ -97,7 +97,7 @@ export const UnStakeModalContent = ({ stakeAssetName, icon }: UnStakeProps) => { return ( <> - {isWrongNetwork && ( + {isWrongNetwork && !watchModeOnlyAddress && ( )} ( ); export const WalletModalContextProvider: React.FC = ({ children }) => { - const { connected } = useWeb3Context(); + const { connected, watchModeOnlyAddress } = useWeb3Context(); const [isWalletModalOpen, setWalletModalOpen] = useState(false); useEffect(() => { - if (connected) { + if (connected || watchModeOnlyAddress) { setWalletModalOpen(false); } - }, [connected]); + }, [connected, watchModeOnlyAddress]); return ( ) => { - if (!connected) { + if (!connected && !watchModeOnlyAddress) { setWalletModalOpen(true); } else { setOpen(true); @@ -86,10 +93,10 @@ export default function WalletWidget({ open, setOpen, headerHeight }: WalletWidg }; const handleDisconnect = () => { - if (connected) { + if (connected || watchModeOnlyAddress) { disconnectWallet(); handleClose(); - localStorage.removeItem('mockWalletAddress'); + localStorage.removeItem('watchModeOnlyAddress'); } }; @@ -98,7 +105,12 @@ export default function WalletWidget({ open, setOpen, headerHeight }: WalletWidg handleClose(); }; - const hideWalletAccountText = xsm && (ENABLE_TESTNET || STAGING_ENV); + const handleSwitchWallet = (): void => { + setWalletModalOpen(true); + handleClose(); + }; + + const hideWalletAccountText = xsm && (ENABLE_TESTNET || STAGING_ENV || watchModeOnlyAddress); const accountAvatar = ( setUseBlockie(true)} /> + {watchModeOnlyAddress && ( + + + + )} ); @@ -146,47 +174,99 @@ export default function WalletWidget({ open, setOpen, headerHeight }: WalletWidg - - - setUseBlockie(true)} - /> - - - {ensNameAbbreviated && ( - - {ensNameAbbreviated} - - )} - - + + - {textCenterEllipsis(currentAccount, ensNameAbbreviated ? 12 : 7, 4)} - + {watchModeOnlyAddress && ( + + + + )} + setUseBlockie(true)} + /> + + + {ensNameAbbreviated && ( + + {ensNameAbbreviated} + + )} + + + {textCenterEllipsis(currentAccount, ensNameAbbreviated ? 12 : 7, 4)} + + + {watchModeOnlyAddress && ( + + Watch-only mode. + + )}
+ {!md && ( + + + + + )} @@ -273,37 +353,50 @@ export default function WalletWidget({ open, setOpen, headerHeight }: WalletWidg )} - - - - - - - - - Disconnect Wallet - - + {md && ( + <> + + + + + + + )} ); return ( <> - {md && connected && open ? ( + {md && (connected || watchModeOnlyAddress) && open ? ( ) : loading ? ( ) : (