diff --git a/.changeset/clean-ladybugs-kiss.md b/.changeset/clean-ladybugs-kiss.md new file mode 100644 index 0000000000..4844d8a07e --- /dev/null +++ b/.changeset/clean-ladybugs-kiss.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +update behavior to switch chains diff --git a/apps/evm/src/clients/api/queries/useGetPools/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/useGetPools/__tests__/index.spec.ts index b09b942e54..e6e1f19d13 100644 --- a/apps/evm/src/clients/api/queries/useGetPools/__tests__/index.spec.ts +++ b/apps/evm/src/clients/api/queries/useGetPools/__tests__/index.spec.ts @@ -13,7 +13,7 @@ import { useGetVaiControllerContractAddress, useGetVenusLensContractAddress, } from 'libs/contracts'; -import { useChainId, usePublicClient } from 'libs/wallet'; +import { usePublicClient } from 'libs/wallet'; import { renderHook } from 'testUtils/render'; import { restService } from 'utilities/restService'; import { useGetPools } from '..'; @@ -87,11 +87,9 @@ describe('useGetPools', () => { }); it('returns pools with time based reward rates in the correct format', async () => { - (useChainId as Mock).mockImplementation(() => ({ + const { result } = renderHook(() => useGetPools(), { chainId: ChainId.ARBITRUM_SEPOLIA, - })); - - const { result } = renderHook(() => useGetPools()); + }); await waitFor(() => expect(result.current.data).toBeDefined()); expect(result.current.data).toMatchSnapshot(); diff --git a/apps/evm/src/containers/ConnectWallet/__tests__/index.spec.tsx b/apps/evm/src/containers/ConnectWallet/__tests__/index.spec.tsx index 5bb1b03157..1363a2837d 100644 --- a/apps/evm/src/containers/ConnectWallet/__tests__/index.spec.tsx +++ b/apps/evm/src/containers/ConnectWallet/__tests__/index.spec.tsx @@ -3,15 +3,13 @@ import type { Mock } from 'vitest'; import fakeAccountAddress from '__mocks__/models/address'; import { en } from 'libs/translations'; -import { useAuthModal, useSwitchChain } from 'libs/wallet'; +import { useAuthModal } from 'libs/wallet'; import { renderComponent } from 'testUtils/render'; -import { ChainId } from 'types'; import { ConnectWallet } from '..'; describe('ConnectWallet', () => { beforeEach(() => { (useAuthModal as Mock).mockReturnValue({ openAuthModal: vi.fn() }); - (useSwitchChain as Mock).mockReturnValue({ switchChain: vi.fn() }); }); it('renders without crashing', () => { @@ -38,40 +36,9 @@ describe('ConnectWallet', () => { }); }); - it('displays chain switching button when current chain is different from chainId parameter', async () => { - const { getByText } = renderComponent(, { - accountAddress: fakeAccountAddress, - }); - - const switchChainButton = getByText( - en.connectWallet.switchChain.replace('{{chainName}}', 'opBNB testnet'), - ); - expect(switchChainButton).toBeInTheDocument(); - }); - - it('calls switchChain when switch chain button is clicked', async () => { - const mockSwitchChain = vi.fn(); - (useSwitchChain as Mock).mockReturnValue({ switchChain: mockSwitchChain }); - - const { getByText } = renderComponent(, { - accountAddress: fakeAccountAddress, - }); - - const switchChainButton = getByText( - en.connectWallet.switchChain.replace('{{chainName}}', 'opBNB testnet'), - ); - - fireEvent.click(switchChainButton); - - await waitFor(() => { - expect(mockSwitchChain).toHaveBeenCalledTimes(1); - expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: ChainId.OPBNB_TESTNET }); - }); - }); - - it('renders children when user is connected and on the correct chain', () => { + it('renders children when user is connected', () => { const { getByText } = renderComponent( - +
Child Component
, { diff --git a/apps/evm/src/containers/ConnectWallet/index.tsx b/apps/evm/src/containers/ConnectWallet/index.tsx index 62f94d25bd..5453a74d14 100644 --- a/apps/evm/src/containers/ConnectWallet/index.tsx +++ b/apps/evm/src/containers/ConnectWallet/index.tsx @@ -1,16 +1,10 @@ -import { chainMetadata } from '@venusprotocol/chains'; -import { useMemo } from 'react'; - -import { Button, type Variant } from 'components/Button'; +import { Button } from 'components/Button'; import { NoticeInfo } from 'components/Notice'; import { useTranslation } from 'libs/translations'; -import { useAccountAddress, useAuthModal, useChainId, useSwitchChain } from 'libs/wallet'; -import type { ChainId } from 'types'; -import { Container } from './Container'; +import { useAccountAddress, useAuthModal } from 'libs/wallet'; -export interface ConnectWalletProps extends React.HTMLAttributes { - chainId?: ChainId; - buttonVariant?: Variant; +export interface ConnectWalletProps + extends Omit, 'className'> { message?: string; className?: string; children?: React.ReactNode; @@ -18,52 +12,29 @@ export interface ConnectWalletProps extends React.HTMLAttributes export const ConnectWallet: React.FC = ({ children, - chainId, message, - buttonVariant, ...otherProps }) => { const { accountAddress } = useAccountAddress(); const isUserConnected = !!accountAddress; - const { chainId: currentChainId } = useChainId(); - - const isOnWrongChain = useMemo( - () => chainId && currentChainId !== chainId, - [currentChainId, chainId], - ); - - const chain = chainId && chainMetadata[chainId]; - const { openAuthModal } = useAuthModal(); - const { switchChain } = useSwitchChain(); - const handleSwitchChain = () => chainId && switchChain({ chainId }); const { t } = useTranslation(); - if (!isUserConnected) { - return ( - - {!!message && } - - - - ); - } - - if (isOnWrongChain) { - return ( - - - - ); - } - - return {children}; + return ( +
+ {isUserConnected ? ( + children + ) : ( + <> + {!!message && } + + + + )} +
+ ); }; diff --git a/apps/evm/src/containers/Form/RhfSubmitButton/index.tsx b/apps/evm/src/containers/Form/RhfSubmitButton/index.tsx index 73573fe217..29b4d78688 100644 --- a/apps/evm/src/containers/Form/RhfSubmitButton/index.tsx +++ b/apps/evm/src/containers/Form/RhfSubmitButton/index.tsx @@ -4,6 +4,8 @@ import { type ButtonProps, PrimaryButton } from 'components'; import { ConnectWallet } from 'containers/ConnectWallet'; import { cn } from 'utilities'; +import type { ChainId } from '@venusprotocol/chains'; +import { SwitchChain } from 'containers/SwitchChain'; import { ApproveTokenSteps, type ApproveTokenStepsProps } from './ApproveTokenSteps'; export interface RhfSubmitButtonProps extends ButtonProps { @@ -11,7 +13,11 @@ export interface RhfSubmitButtonProps extends ButtonProps { enabledLabel: string; disabledLabel: string; isDangerousSubmission?: boolean; - requiresConnectedWallet?: boolean; + requiresConnectedWallet?: + | boolean + | { + chainId: ChainId; + }; spendingApproval?: Omit; } @@ -41,7 +47,7 @@ export const RhfSubmitButton: React.FC = ({ ); - if (spendingApproval) { + if (formState.isValid && spendingApproval) { dom = ( {dom} @@ -49,8 +55,20 @@ export const RhfSubmitButton: React.FC = ({ ); } - if (requiresConnectedWallet || spendingApproval) { - dom = {dom}; + if (formState.isValid && (requiresConnectedWallet || spendingApproval)) { + dom = ( + + + {dom} + + + ); } return
{dom}
; diff --git a/apps/evm/src/containers/Layout/ClaimRewardButton/index.tsx b/apps/evm/src/containers/Layout/ClaimRewardButton/index.tsx index deefb843de..e9fa3de88d 100644 --- a/apps/evm/src/containers/Layout/ClaimRewardButton/index.tsx +++ b/apps/evm/src/containers/Layout/ClaimRewardButton/index.tsx @@ -9,6 +9,8 @@ import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import { cn, formatCentsToReadableValue } from 'utilities'; +import { ConnectWallet } from 'containers/ConnectWallet'; +import { SwitchChain } from 'containers/SwitchChain'; import TEST_IDS from '../testIds'; import { RewardGroup } from './RewardGroup'; import type { Group } from './types'; @@ -127,17 +129,21 @@ export const ClaimRewardButtonUi: React.FC = ({ ))} - - {isSubmitDisabled - ? t('claimReward.modal.claimButton.disabledLabel') - : t('claimReward.modal.claimButton.enabledLabel')} - + + + + {isSubmitDisabled + ? t('claimReward.modal.claimButton.disabledLabel') + : t('claimReward.modal.claimButton.enabledLabel')} + + + diff --git a/apps/evm/src/containers/SwitchChain/__tests__/index.spec.tsx b/apps/evm/src/containers/SwitchChain/__tests__/index.spec.tsx new file mode 100644 index 0000000000..0569fa40c8 --- /dev/null +++ b/apps/evm/src/containers/SwitchChain/__tests__/index.spec.tsx @@ -0,0 +1,70 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import type { Mock } from 'vitest'; + +import { ChainId } from '@venusprotocol/chains'; +import { chainMetadata } from '@venusprotocol/chains'; +import fakeAddress from '__mocks__/models/address'; +import { en } from 'libs/translations'; +import { useSwitchChain } from 'libs/wallet'; +import { renderComponent } from 'testUtils/render'; +import { SwitchChain } from '..'; + +const fakeContent = 'Fake content'; + +describe('SwitchChain', () => { + beforeEach(() => { + (useSwitchChain as Mock).mockReturnValue({ switchChain: vi.fn() }); + }); + + it('renders without SwitchChain', () => { + renderComponent(); + }); + + it('displays children when user is not connected', async () => { + renderComponent({fakeContent}); + + expect(screen.queryByText(fakeContent)).toBeInTheDocument(); + }); + + it('displays switch button when user is connected to the wrong chain', async () => { + renderComponent({fakeContent}, { + accountAddress: fakeAddress, + chainId: ChainId.ETHEREUM, + }); + + expect(screen.queryByText(fakeContent)).not.toBeInTheDocument(); + + expect( + screen.queryByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(); + }); + + it('calls switchChain when switch button is clicked', async () => { + const mockSwitchChain = vi.fn(); + (useSwitchChain as Mock).mockReturnValue({ switchChain: mockSwitchChain }); + + renderComponent({fakeContent}, { + accountAddress: fakeAddress, + chainId: ChainId.ETHEREUM, + }); + + fireEvent.click( + screen.getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ); + + await waitFor(() => expect(mockSwitchChain).toHaveBeenCalledTimes(1)); + expect(mockSwitchChain).toHaveBeenCalledWith({ + chainId: ChainId.BSC_TESTNET, + }); + }); +}); diff --git a/apps/evm/src/containers/SwitchChain/index.tsx b/apps/evm/src/containers/SwitchChain/index.tsx new file mode 100644 index 0000000000..02054fc23a --- /dev/null +++ b/apps/evm/src/containers/SwitchChain/index.tsx @@ -0,0 +1,41 @@ +import { chainMetadata } from '@venusprotocol/chains'; + +import { Button } from 'components/Button'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress, useAccountChainId, useChainId, useSwitchChain } from 'libs/wallet'; +import type { ChainId } from 'types'; + +export interface SwitchChainProps extends React.HTMLAttributes { + chainId?: ChainId; +} + +export const SwitchChain: React.FC = ({ children, chainId, ...otherProps }) => { + const { accountAddress } = useAccountAddress(); + const isUserConnected = !!accountAddress; + + const { chainId: currentChainId } = useChainId(); + const targetChainId = chainId || currentChainId; + + const { chainId: accountChainId } = useAccountChainId(); + const isOnWrongChain = accountChainId !== targetChainId; + const targetChain = chainMetadata[targetChainId]; + + const { switchChain } = useSwitchChain(); + const { t } = useTranslation(); + + const handleSwitchChain = () => switchChain({ chainId: targetChainId }); + + return ( +
+ {isUserConnected && isOnWrongChain ? ( + + ) : ( + children + )} +
+ ); +}; diff --git a/apps/evm/src/hooks/useUserChainSettings/__tests__/index.spec.tsx b/apps/evm/src/hooks/useUserChainSettings/__tests__/index.spec.tsx index f2cd4f9781..ce39f6b154 100644 --- a/apps/evm/src/hooks/useUserChainSettings/__tests__/index.spec.tsx +++ b/apps/evm/src/hooks/useUserChainSettings/__tests__/index.spec.tsx @@ -1,6 +1,5 @@ import type { Mock } from 'vitest'; -import { useChainId } from 'libs/wallet'; import { store } from 'store'; import { renderHook as renderHookWithContext } from 'testUtils/render'; import { ChainId } from 'types'; @@ -18,12 +17,6 @@ vi.mock('store', () => ({ })); describe('useUserChainSettings', () => { - beforeEach(() => { - (useChainId as Mock).mockReturnValue({ - chainId: ChainId.BSC_TESTNET, - }); - }); - it('returns correct settings from the store', () => { (store.use.userSettings as Mock).mockReturnValue({ [ChainId.BSC_TESTNET]: { diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index f746af3dd6..08b0a7ae80 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -106,9 +106,6 @@ "pointDistribution": { "learnMore": "Learn more" }, - "pointDistributions": { - "title": "Point distributions" - }, "primeDistribution": { "description": "Venus Prime rewards dedicated users with boosted rewards. The Venus protocol does not guarantee these rewards and accepts no liability.", "name": "Prime APY" @@ -197,9 +194,6 @@ "label": "Bridge gas fee", "tooltip": "Used to cover the gas cost for sending your transfer on the destination chain" }, - "connectWalletButton": { - "label": "Connect wallet" - }, "errors": { "dailyTransactionLimitExceeded": { "message": "You cannot bridge more than {{readableAmountTokens}} ({{readableAmountUsd}}) on the destination chain due to the 24-hour limit. This limit will be reset on {{ date, dd MMM yyyy h:mm a }}", @@ -263,8 +257,7 @@ "disconnect": "Disconnect" }, "connectWallet": { - "connectButton": "Connect wallet", - "switchChain": "Switch to {{chainName}}" + "connectButton": "Connect wallet" }, "convertVrt": { "connectWalletToWithdrawXvs": "Please connect your wallet to withdraw XVS", @@ -402,6 +395,7 @@ }, "price": "Price", "supply": "Supply", + "unichainBackgroundIllustrationAlt": "Background illustration", "utilizationRate": "Utilization rate" }, "menu": { @@ -534,7 +528,6 @@ "operationForm": { "borrowableAmount": "Borrowable Amount", "borrowTabTitle": "Borrow", - "connectWalletButtonLabel": "Connect wallet", "error": { "borrowCapReached": "The borrow cap of {{assetBorrowCap}} has been reached for this pool. You can not borrow from this market anymore until loans are repaid or its borrow cap is increased.", "higherThanAvailableLiquidity": "Insufficient asset liquidity", @@ -810,6 +803,9 @@ }, "walletBalance": "Wallet balance" }, + "switchChain": { + "switchButton": "Switch to {{chainName}}" + }, "table": { "cardsSelect": { "label": "Sort by" @@ -1108,8 +1104,8 @@ "for": "For", "goToXvsSnapshot": "Go to XVS Snapshot", "omnichain": { - "switchToBnb": "Switch to BNB chain", - "votingOnlyEnabledOnBnb": "Voting is only available on the BNB chain" + "switchToBnb": "Switch to BNB Chain", + "votingOnlyEnabledOnBnb": "Voting is only available on BNB Chain" }, "pages": { "actions": "3 of 3 Actions", @@ -1155,9 +1151,6 @@ "execute": "Execute", "queue": "Queue" }, - "cta": { - "execute": "Execute" - }, "dates": { "activeAt": "Pending until {{date, }}", "executableAt": "Executable at {{date, }}", diff --git a/apps/evm/src/libs/wallet/Web3Wrapper/ConnectKitWrapper/AuthHandler/index.tsx b/apps/evm/src/libs/wallet/Web3Wrapper/ConnectKitWrapper/AuthHandler/index.tsx index 17c00a64e5..86a0f7165f 100644 --- a/apps/evm/src/libs/wallet/Web3Wrapper/ConnectKitWrapper/AuthHandler/index.tsx +++ b/apps/evm/src/libs/wallet/Web3Wrapper/ConnectKitWrapper/AuthHandler/index.tsx @@ -1,20 +1,17 @@ -import { getAccount, watchAccount } from '@wagmi/core'; +import { getAccount } from '@wagmi/core'; import { useEffect, useRef } from 'react'; import { useLocation } from 'react-router'; import { useSearchParams } from 'react-router-dom'; -import { useDisconnect } from 'wagmi'; import { routes } from 'constants/routing'; import { useNavigate } from 'hooks/useNavigate'; import { getUnsafeChainIdFromSearchParams } from 'libs/wallet'; +import config from 'libs/wallet/Web3Wrapper/config'; import { chains, defaultChain } from 'libs/wallet/chains'; import { CHAIN_ID_SEARCH_PARAM } from 'libs/wallet/constants'; import { useUpdateUrlChainId } from 'libs/wallet/hooks/useUpdateUrlChainId'; -import { getChainId } from 'libs/wallet/utilities/getChainId'; -import config from '../../config'; export const AuthHandler: React.FC = () => { - const { disconnect } = useDisconnect(); const { updateUrlChainId } = useUpdateUrlChainId(); const { navigate } = useNavigate(); @@ -22,7 +19,6 @@ export const AuthHandler: React.FC = () => { const initialLocationRef = useRef(location); const navigateRef = useRef(navigate); const [searchParams] = useSearchParams(); - const isInitializedRef = useRef(false); // Initialize wallet connection on mount useEffect(() => { @@ -45,38 +41,6 @@ export const AuthHandler: React.FC = () => { } }, []); - // Detect change of chain ID triggered from wallet - useEffect(() => { - const stopWatchingNetwork = watchAccount(config, { - onChange: async ({ chain: walletChain }) => { - if (!walletChain) { - disconnect(); - return; - } - - const { chainId } = getChainId(); - - // Disconnect wallet if it is connected to a different chain than the one set in the URL. - // This check is only performed on initial mount, in order to support links without - // redirecting users - if (walletChain.id !== chainId && !isInitializedRef.current) { - disconnect(); - } - - // Update URL when wallet connects to a different chain - if (walletChain.id !== chainId && isInitializedRef.current) { - updateUrlChainId({ chainId: walletChain.id }); - } - - if (!isInitializedRef.current) { - isInitializedRef.current = true; - } - }, - }); - - return stopWatchingNetwork; - }, [updateUrlChainId, disconnect]); - // Detect change of chain ID triggered from URL useEffect(() => { const fn = async () => { @@ -84,26 +48,18 @@ export const AuthHandler: React.FC = () => { searchParams, }); - const { chain: walletChain } = getAccount(config); - - // Update URL again if it was updated with an unsupported chain ID or does not contain any - // chain ID search param + // Update URL if it was updated with an unsupported chain ID or does not contain any chain ID + // search param if ( unsafeSearchParamChainId === undefined || !chains.some(chain => chain.id === unsafeSearchParamChainId) ) { - updateUrlChainId({ chainId: walletChain?.id ?? defaultChain.id }); - return; - } - - // Disconnect wallet if chain ID changed in URL but wallet is connected to a different network - if (walletChain && walletChain.id !== unsafeSearchParamChainId) { - disconnect(); + updateUrlChainId({ chainId: defaultChain.id }); } }; fn(); - }, [searchParams, updateUrlChainId, disconnect]); + }, [searchParams, updateUrlChainId]); return null; }; diff --git a/apps/evm/src/libs/wallet/Web3Wrapper/config.ts b/apps/evm/src/libs/wallet/Web3Wrapper/config.ts index 0cc183b056..e001446d68 100644 --- a/apps/evm/src/libs/wallet/Web3Wrapper/config.ts +++ b/apps/evm/src/libs/wallet/Web3Wrapper/config.ts @@ -4,7 +4,7 @@ import { http, createConfig } from 'wagmi'; import localConfig from 'config'; import { MAIN_PRODUCTION_HOST } from 'constants/production'; import type { ChainId } from 'types'; -import type { Chain, Transport } from 'viem'; +import type { Chain } from 'viem'; import { chains } from '../chains'; import { WALLET_CONNECT_PROJECT_ID } from '../constants'; @@ -17,7 +17,7 @@ const connectKitConfig = getDefaultConfig({ ...acc, [chain.id]: http(url), }; - }, {}) as Record, + }, {}), walletConnectProjectId: WALLET_CONNECT_PROJECT_ID, appName: 'Venus', appUrl: `https://${MAIN_PRODUCTION_HOST}`, diff --git a/apps/evm/src/libs/wallet/__mocks__/index.ts b/apps/evm/src/libs/wallet/__mocks__/index.ts index 34cdc25ccb..d24548dd21 100644 --- a/apps/evm/src/libs/wallet/__mocks__/index.ts +++ b/apps/evm/src/libs/wallet/__mocks__/index.ts @@ -6,6 +6,7 @@ export * from 'libs/wallet/hooks/usePublicClient/__mocks__'; export * from 'libs/wallet/hooks/useProvider/__mocks__'; export * from 'libs/wallet/hooks/useSigner/__mocks__'; export * from 'libs/wallet/hooks/useAccountAddress/__mocks__'; +export * from 'libs/wallet/hooks/useAccountChainId/__mocks__'; export * from 'libs/wallet/hooks/useSwitchChain/__mocks__'; export * from 'libs/wallet/hooks/useChainId/__mocks__'; export * from 'libs/wallet/hooks/useAuthModal/__mocks__'; diff --git a/apps/evm/src/libs/wallet/hooks/useAccountChainId/__mocks__/index.ts b/apps/evm/src/libs/wallet/hooks/useAccountChainId/__mocks__/index.ts new file mode 100644 index 0000000000..83d508635c --- /dev/null +++ b/apps/evm/src/libs/wallet/hooks/useAccountChainId/__mocks__/index.ts @@ -0,0 +1,3 @@ +export const useAccountChainId = vi.fn(() => ({ + chainId: undefined, +})); diff --git a/apps/evm/src/libs/wallet/hooks/useAccountChainId/index.tsx b/apps/evm/src/libs/wallet/hooks/useAccountChainId/index.tsx new file mode 100644 index 0000000000..bbc2ca451f --- /dev/null +++ b/apps/evm/src/libs/wallet/hooks/useAccountChainId/index.tsx @@ -0,0 +1,6 @@ +import { useAccount } from 'wagmi'; + +export const useAccountChainId = () => { + const { chainId } = useAccount(); + return { chainId }; +}; diff --git a/apps/evm/src/libs/wallet/index.ts b/apps/evm/src/libs/wallet/index.ts index d67222a970..dc24aa1dcc 100644 --- a/apps/evm/src/libs/wallet/index.ts +++ b/apps/evm/src/libs/wallet/index.ts @@ -7,6 +7,7 @@ export * from './utilities/getUnsafeChainIdFromSearchParams'; export * from './hooks/useProvider'; export * from './hooks/useSigner'; export * from './hooks/useAccountAddress'; +export * from './hooks/useAccountChainId'; export * from './hooks/useSwitchChain'; export * from './hooks/useChainId'; export * from './hooks/useAuthModal'; diff --git a/apps/evm/src/pages/Bridge/__tests__/index.spec.tsx b/apps/evm/src/pages/Bridge/__tests__/index.spec.tsx index 4226c46d1f..5fc009f135 100644 --- a/apps/evm/src/pages/Bridge/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Bridge/__tests__/index.spec.tsx @@ -16,6 +16,7 @@ import { en } from 'libs/translations'; import { useAuthModal, useChainId, useSwitchChain } from 'libs/wallet'; import { ChainId } from 'types'; +import { chainMetadata } from '@venusprotocol/chains'; import { fromUnixTime } from 'date-fns'; import Bridge from '..'; import TEST_IDS from '../testIds'; @@ -88,14 +89,37 @@ describe('Bridge', () => { const { getByText } = renderComponent(); // Check connect button is present - await waitFor(() => getByText(en.bridgePage.connectWalletButton.label)); + await waitFor(() => getByText(en.connectButton.connect)); // Click on connect button - fireEvent.click(getByText(en.bridgePage.connectWalletButton.label).closest('button')!); + fireEvent.click(getByText(en.connectButton.connect).closest('button')!); await waitFor(() => expect(openAuthModalMock).toHaveBeenCalledTimes(1)); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { getByText, getByTestId } = renderComponent(, { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_ONE, + chainId: ChainId.BSC_TESTNET, + }); + + const tokenTextInput = await waitFor(() => getByTestId(TEST_IDS.tokenTextField)); + fireEvent.change(tokenTextInput, { target: { value: 1 } }); + + // Check "Switch chain" button is displayed + await waitFor(() => + expect( + getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('handles changing from chain ID correctly', async () => { const { getByTestId } = renderComponent(, { chainId: ChainId.SEPOLIA, diff --git a/apps/evm/src/pages/Bridge/index.tsx b/apps/evm/src/pages/Bridge/index.tsx index 731f12d85c..8a7e56a42c 100644 --- a/apps/evm/src/pages/Bridge/index.tsx +++ b/apps/evm/src/pages/Bridge/index.tsx @@ -18,7 +18,9 @@ import { TokenTextField, } from 'components'; import { NULL_ADDRESS } from 'constants/address'; +import { ConnectWallet } from 'containers/ConnectWallet'; import { Link } from 'containers/Link'; +import { SwitchChain } from 'containers/SwitchChain'; import { useGetChainMetadata } from 'hooks/useGetChainMetadata'; import useTokenApproval from 'hooks/useTokenApproval'; import { @@ -28,9 +30,9 @@ import { import { handleError } from 'libs/errors'; import { useGetToken } from 'libs/tokens'; import { useTranslation } from 'libs/translations'; -import { useAccountAddress, useAuthModal, useChainId, useSwitchChain } from 'libs/wallet'; +import { useAccountAddress, useChainId, useSwitchChain } from 'libs/wallet'; import { ChainId } from 'types'; -import { convertMantissaToTokens, formatTokensToReadableValue } from 'utilities'; +import { cn, convertMantissaToTokens, formatTokensToReadableValue } from 'utilities'; import { ChainSelect, getOptionsFromChainsList } from './ChainSelect'; import { bridgeChains } from './constants'; import { ReactComponent as LayerZeroLogo } from './layerZeroLogo.svg'; @@ -44,7 +46,6 @@ const BridgePage: React.FC = () => { const { chainId } = useChainId(); const { nativeToken } = useGetChainMetadata(); const { switchChain } = useSwitchChain(); - const { openAuthModal } = useAuthModal(); const { accountAddress } = useAccountAddress(); const isUserConnected = !!accountAddress; @@ -290,148 +291,140 @@ const BridgePage: React.FC = () => {
-
- ( -
- { - handleChainFieldChange({ - newFromChainId: newChainId as ChainId, - }); - }} - /> -
- )} - /> +
+
+ ( +
+ { + handleChainFieldChange({ + newFromChainId: newChainId as ChainId, + }); + }} + /> +
+ )} + /> - - - + + + + + ( +
+ { + handleChainFieldChange({ + newToChainId: newChainId as ChainId, + }); + }} + /> +
+ )} + /> +
( -
- { - handleChainFieldChange({ - newToChainId: newChainId as ChainId, + render={({ field, fieldState }) => ( + { + setValue('amountTokens', walletBalanceTokens.toFixed(), { + shouldValidate: true, + shouldDirty: true, }); - }} - /> -
+ }, + }} + {...field} + disabled={field.disabled || isApproveXvsLoading || !accountAddress} + /> )} />
- ( - +
+ {errorLabel && ( + + )} + + + {readableWalletBalance} + + + { - setValue('amountTokens', walletBalanceTokens.toFixed(), { - shouldValidate: true, - shouldDirty: true, - }); - }, - }} - {...field} - disabled={field.disabled || isApproveXvsLoading || !accountAddress} + walletBalanceTokens={walletBalanceTokens} + walletSpendingLimitTokens={xvsWalletSpendingLimitTokens} + onRevoke={revokeXvsWalletSpendingLimit} + isRevokeLoading={isRevokeXvsWalletSpendingLimitLoading} /> - )} - /> - - {errorLabel && ( - - )} - -
- - {readableWalletBalance} - - - -
- - {readableFee} - - - - {isUserConnected ? ( - + {readableFee} + +
+ + + - {submitButtonLabel} - - ) : ( - - {t('bridgePage.connectWalletButton.label')} - - )} - + + {submitButtonLabel} + + + + diff --git a/apps/evm/src/pages/Governance/ProposalList/CreateProposalModal/ProposalWizard/index.tsx b/apps/evm/src/pages/Governance/ProposalList/CreateProposalModal/ProposalWizard/index.tsx index b7bc5ff236..e4f3db7e7f 100644 --- a/apps/evm/src/pages/Governance/ProposalList/CreateProposalModal/ProposalWizard/index.tsx +++ b/apps/evm/src/pages/Governance/ProposalList/CreateProposalModal/ProposalWizard/index.tsx @@ -9,6 +9,8 @@ import { useNavigate } from 'hooks/useNavigate'; import { useTranslation } from 'libs/translations'; import { FormikSubmitButton } from 'containers/Form'; +import { SwitchChain } from 'containers/SwitchChain'; +import { governanceChain } from 'libs/wallet'; import ActionAccordion from '../ActionAccordion'; import ProposalInfo from '../ProposalInfo'; import ProposalPreview from '../ProposalPreview'; @@ -171,11 +173,13 @@ const ProposalWizard: React.FC = ({ )} {currentStep === 'proposal-preview' && ( - + + + )} ); diff --git a/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/SubmitSection/index.tsx b/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/SubmitSection/index.tsx new file mode 100644 index 0000000000..1bd7e4532d --- /dev/null +++ b/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/SubmitSection/index.tsx @@ -0,0 +1,37 @@ +import { ConnectWallet } from 'containers/ConnectWallet'; +import { FormikSubmitButton } from 'containers/Form'; +import { SwitchChain } from 'containers/SwitchChain'; +import { useFormikContext } from 'formik'; +import { useTranslation } from 'libs/translations'; +import { governanceChain } from 'libs/wallet'; + +export interface SubmitSectionProps { + previouslyDelegated: boolean; + isVoteDelegationLoading: boolean; +} + +export const SubmitSection = ({ + previouslyDelegated, + isVoteDelegationLoading, +}: SubmitSectionProps) => { + const { t } = useTranslation(); + const { isValid } = useFormikContext(); + + let dom = ( + + ); + + if (isValid) { + dom = ( + + {dom} + + ); + } + + return
{dom}
; +}; diff --git a/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/index.tsx b/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/index.tsx index 10d6436885..372bfb0d1a 100644 --- a/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/index.tsx +++ b/apps/evm/src/pages/Governance/VotingWallet/DelegateModal/index.tsx @@ -2,13 +2,14 @@ import { Typography } from '@mui/material'; import { Form, Formik } from 'formik'; -import { ButtonWrapper, Modal, NoticeInfo, PrimaryButton, TextButton } from 'components'; +import { ButtonWrapper, Modal, NoticeInfo, TextButton } from 'components'; import { routes } from 'constants/routing'; import { Link } from 'containers/Link'; import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; -import { FormikSubmitButton, FormikTextField } from 'containers/Form'; +import { FormikTextField } from 'containers/Form'; +import { SubmitSection } from './SubmitSection'; import addressValidationSchema from './addressValidationSchema'; import { useStyles } from './styles'; @@ -19,7 +20,6 @@ interface DelegateModalProps { setVoteDelegation: (input: { delegateAddress: string }) => unknown; previouslyDelegated: boolean; isVoteDelegationLoading: boolean; - openAuthModal: () => void; } const DelegateModal: React.FC = ({ @@ -29,7 +29,6 @@ const DelegateModal: React.FC = ({ setVoteDelegation, previouslyDelegated, isVoteDelegationLoading, - openAuthModal, }) => { const { t } = useTranslation(); const styles = useStyles(); @@ -81,19 +80,11 @@ const DelegateModal: React.FC = ({ maxLength={42} disabled={!currentUserAccountAddress} /> - {currentUserAccountAddress ? ( - - ) : ( - - {t('connectWallet.connectButton')} - - )} + + )} diff --git a/apps/evm/src/pages/Governance/VotingWallet/index.tsx b/apps/evm/src/pages/Governance/VotingWallet/index.tsx index 1cad899010..6f5ccdf2e6 100644 --- a/apps/evm/src/pages/Governance/VotingWallet/index.tsx +++ b/apps/evm/src/pages/Governance/VotingWallet/index.tsx @@ -243,7 +243,6 @@ const VotingWallet: React.FC = ({ className }) => { previouslyDelegated={previouslyDelegated} setVoteDelegation={setVoteDelegation} isVoteDelegationLoading={isVoteDelegationLoading} - openAuthModal={openAuthModal} /> )} diff --git a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx index f334d76cd0..0c62506a51 100644 --- a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import BigNumber from 'bignumber.js'; import _cloneDeep from 'lodash/cloneDeep'; import type { Mock } from 'vitest'; @@ -147,16 +147,6 @@ describe('Governance', () => { expect(createProposalButton).toBeDisabled(); }); - it('opens delegate modal when clicking text with connect wallet button when unauthenticated', async () => { - const { getByText, getAllByText, getByTestId } = renderComponent(); - const delegateVoteText = getByTestId(VOTING_WALLET_TEST_IDS.delegateYourVoting); - - fireEvent.click(delegateVoteText); - await waitFor(() => getByText(en.vote.delegateAddress)); - - expect(getAllByText(en.connectWallet.connectButton)).toHaveLength(2); - }); - it('opens delegate modal when clicking text with delegate button when authenticated', async () => { const { getByText, getByTestId } = renderComponent(, { accountAddress: fakeAccountAddress, @@ -180,6 +170,11 @@ describe('Governance', () => { }); it('prompts user to connect Wallet', async () => { + const { getByText } = renderComponent(); + getByText(en.connectWallet.connectButton); + }); + + it('prompts user to switch chain if they are connected to the wrong one', async () => { (getCurrentVotes as Mock).mockImplementationOnce(() => ({ votesMantissa: new BigNumber(0), })); @@ -240,8 +235,10 @@ describe('Governance', () => { const addressInput = getByPlaceholderText(en.vote.enterContactAddress); - fireEvent.change(addressInput, { - target: { value: altAddress }, + await act(async () => { + fireEvent.change(addressInput, { + target: { value: altAddress }, + }); }); const delegateVotesButton = getByText(en.vote.delegateVotes); @@ -270,14 +267,16 @@ describe('Governance', () => { await waitFor(() => getByText(en.vote.delegateAddress)); - fireEvent.click(getByText(en.vote.pasteYourAddress)); + await act(async () => { + fireEvent.click(getByText(en.vote.pasteYourAddress)); + }); const delegateVotesButton = getByText(en.vote.delegateVotes); await waitFor(() => expect(delegateVotesButton).toBeEnabled()); fireEvent.click(delegateVotesButton); await waitFor(() => - expect(setVoteDelegate).toBeCalledWith({ + expect(setVoteDelegate).toHaveBeenCalledWith({ delegateAddress: fakeAccountAddress, }), ); diff --git a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/SubmitSection/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/SubmitSection/index.tsx index ab47c26417..542bb38488 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/SubmitSection/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/SubmitSection/index.tsx @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { useMemo } from 'react'; import { PrimaryButton } from 'components'; +import { SwitchChain } from 'containers/SwitchChain'; import { useTranslation } from 'libs/translations'; import { cn } from 'utilities'; import { ApproveDelegateSteps, type ApproveDelegateStepsProps } from '../../ApproveDelegateSteps'; @@ -42,25 +43,34 @@ export const SubmitSection: React.FC = ({ return t('operationForm.submitButtonLabel.borrow'); }, [isFormValid, t]); - return ( - - - {submitButtonLabel} - - + {submitButtonLabel} + ); + + if (isFormValid) { + dom = ( + + + {dom} + + + ); + } + + return dom; }; export default SubmitSection; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/index.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/index.spec.tsx index 78477e3146..3c66e6e7e8 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/index.spec.tsx @@ -10,8 +10,9 @@ import { renderComponent } from 'testUtils/render'; import { borrow } from 'clients/api'; import { SAFE_BORROW_LIMIT_PERCENTAGE } from 'constants/safeBorrowLimitPercentage'; import { en } from 'libs/translations'; -import type { Asset, Pool } from 'types'; +import { type Asset, ChainId, type Pool } from 'types'; +import { chainMetadata } from '@venusprotocol/chains'; import BorrowForm from '..'; import { fakeAsset, fakePool } from '../__testUtils__/fakeData'; import TEST_IDS from '../testIds'; @@ -35,7 +36,7 @@ describe('BorrowForm', () => { ); // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); + expect(getByText(en.connectWallet.connectButton)).toBeInTheDocument(); // Check input is disabled expect(getByTestId(TEST_IDS.tokenTextField).closest('input')).toBeDisabled(); @@ -245,6 +246,34 @@ describe('BorrowForm', () => { await checkSubmitButtonIsDisabled(); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { getByText, getByTestId } = renderComponent( + , + { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_ONE, + chainId: ChainId.BSC_TESTNET, + }, + ); + + const correctAmountTokens = 1; + + const tokenTextInput = await waitFor(() => getByTestId(TEST_IDS.tokenTextField)); + fireEvent.change(tokenTextInput, { target: { value: correctAmountTokens } }); + + // Check "Switch chain" button is displayed + await waitFor(() => + expect( + getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('displays warning notice if amount to borrow requested would bring user borrow balance at safe borrow limit', async () => { const { getByText, getByTestId } = renderComponent( , diff --git a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/indexWrapUnwrapNative.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/indexWrapUnwrapNative.spec.tsx index 2534994209..c5ba5bdcf5 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/indexWrapUnwrapNative.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/__tests__/indexWrapUnwrapNative.spec.tsx @@ -30,18 +30,6 @@ describe('BorrowForm - Feature flag enabled: wrapUnwrapNativeToken', () => { }); }); - it('prompts user to connect their wallet if they are not connected', async () => { - const { getByText, getByTestId } = renderComponent( - , - ); - - // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); - - // Check input is disabled - expect(getByTestId(TEST_IDS.tokenTextField)).toBeDisabled(); - }); - it('does not display the receive native token toggle if the underlying token does not wrap the chain native token', async () => { const { queryByTestId } = renderComponent( , diff --git a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/index.tsx index c7128e6e0a..bf599842b1 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/BorrowForm/index.tsx @@ -12,7 +12,7 @@ import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import { useGetNativeTokenGatewayContractAddress } from 'libs/contracts'; import { useTranslation } from 'libs/translations'; import type { Asset, Pool } from 'types'; -import { convertTokensToMantissa } from 'utilities'; +import { cn, convertTokensToMantissa } from 'utilities'; import { NULL_ADDRESS } from 'constants/address'; import { ConnectWallet } from 'containers/ConnectWallet'; @@ -136,40 +136,44 @@ export const BorrowFormUi: React.FC = ({ }, [safeLimitTokens, setFormValues]); return ( -
- - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - })) - } - disabled={ - !isUserConnected || - isSubmitting || - formError?.code === 'BORROW_CAP_ALREADY_REACHED' || - formError?.code === 'NO_COLLATERALS' - } - rightMaxButton={{ - label: t('operationForm.limitButtonLabel', { - limitPercentage: SAFE_BORROW_LIMIT_PERCENTAGE, - }), - onClick: handleRightMaxButtonClick, - }} - hasError={isUserConnected && !!formError && Number(formValues.amountTokens) > 0} - description={ - isUserConnected && !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - - {isUserConnected ? ( - <> + +
+ + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + })) + } + disabled={ + !isUserConnected || + isSubmitting || + formError?.code === 'BORROW_CAP_ALREADY_REACHED' || + formError?.code === 'NO_COLLATERALS' + } + rightMaxButton={{ + label: t('operationForm.limitButtonLabel', { + limitPercentage: SAFE_BORROW_LIMIT_PERCENTAGE, + }), + onClick: handleRightMaxButtonClick, + }} + hasError={isUserConnected && !!formError && Number(formValues.amountTokens) > 0} + description={ + isUserConnected && !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } + /> + + {!isUserConnected && } +
+ + +
{!isSubmitting && !formError && ( = ({ )} -
-
- + - - - -
+ - -
- - ) : ( -
- - - - {t('operationForm.connectWalletButtonLabel')} - +
- )} + + + ); }; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/SubmitSection/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/SubmitSection/index.tsx index efcf0d9738..835ae77a6a 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/SubmitSection/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/SubmitSection/index.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'libs/translations'; import type { Swap, Token } from 'types'; import { cn } from 'utilities'; +import { SwitchChain } from 'containers/SwitchChain'; import SwapSummary from '../../SwapSummary'; export interface SubmitSectionProps { @@ -53,17 +54,8 @@ export const SubmitSection: React.FC = ({ return t('operationForm.submitButtonLabel.repay'); }, [isFormValid, t]); - return ( - + let dom = ( + <> = ({ {isFormValid && !isSwapLoading && !isFromTokenWalletSpendingLimitLoading && ( )} - + ); + + if (isFormValid) { + dom = ( + + + {dom} + + + ); + } + + return dom; }; export default SubmitSection; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/index.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/index.spec.tsx index f8f3d6b65d..5603d7d084 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/index.spec.tsx @@ -14,6 +14,8 @@ import { repay } from 'clients/api'; import useTokenApproval from 'hooks/useTokenApproval'; import { en } from 'libs/translations'; +import { chainMetadata } from '@venusprotocol/chains'; +import { ChainId } from 'types'; import Repay, { PRESET_PERCENTAGES } from '..'; import { fakeAsset, fakePool } from '../__testUtils__/fakeData'; import TEST_IDS from '../testIds'; @@ -47,7 +49,7 @@ describe('RepayForm', () => { ); // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); + expect(getByText(en.connectWallet.connectButton)).toBeInTheDocument(); // Check input is disabled expect(getByTestId(TEST_IDS.tokenTextField).closest('input')).toBeDisabled(); @@ -175,6 +177,34 @@ describe('RepayForm', () => { await checkSubmitButtonIsDisabled(); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { getByText, getByTestId } = renderComponent( + , + { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_ONE, + chainId: ChainId.BSC_TESTNET, + }, + ); + + const correctAmountTokens = 1; + + const tokenTextInput = await waitFor(() => getByTestId(TEST_IDS.tokenTextField)); + fireEvent.change(tokenTextInput, { target: { value: correctAmountTokens } }); + + // Check "Switch chain" button is displayed + await waitFor(() => + expect( + getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('displays the wallet spending limit correctly and lets user revoke it', async () => { const originalTokenApprovalOutput = useTokenApproval({ token: xvs, diff --git a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/indexIntegratedSwap.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/indexIntegratedSwap.spec.tsx index 9fa910a6a0..e16f435aef 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/indexIntegratedSwap.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/__tests__/indexIntegratedSwap.spec.tsx @@ -106,24 +106,6 @@ describe('RepayForm - Feature flag enabled: integratedSwap', () => { renderComponent(); }); - it('prompts user to connect their wallet if they are not connected', async () => { - const { getByText, getByTestId } = renderComponent( - , - ); - - // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); - - // Check input is disabled - expect( - getByTestId( - getTokenTextFieldTestId({ - parentTestId: TEST_IDS.selectTokenTextField, - }), - ), - ).toBeDisabled(); - }); - it('displays correct wallet balance', async () => { const { getByText, container } = renderComponent( , diff --git a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/index.tsx index 1e498fd8a3..a7e4524e16 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/RepayForm/index.tsx @@ -26,6 +26,7 @@ import { useAccountAddress } from 'libs/wallet'; import type { Asset, Pool, Swap, SwapError, TokenBalance } from 'types'; import { areTokensEqual, + cn, convertMantissaToTokens, convertTokensToMantissa, formatPercentageToReadableValue, @@ -215,90 +216,94 @@ export const RepayFormUi: React.FC = ({ ]); return ( -
- {isIntegratedSwapFeatureEnabled || canWrapNativeToken ? ( - 0} - disabled={!isUserConnected || isSubmitting} - onChange={amountTokens => - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - // Reset selected fixed percentage - fixedRepayPercentage: undefined, - })) - } - onChangeSelectedToken={fromToken => - setFormValues(currentFormValues => ({ - ...currentFormValues, - fromToken, - })) - } - rightMaxButton={{ - label: t('operationForm.rightMaxButtonLabel'), - onClick: handleRightMaxButtonClick, - }} - tokenBalances={tokenBalances} - description={ - !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - ) : ( - - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - // Reset selected fixed percentage - fixedRepayPercentage: undefined, - })) - } - disabled={!isUserConnected || isSubmitting} - rightMaxButton={{ - label: t('operationForm.rightMaxButtonLabel'), - onClick: handleRightMaxButtonClick, - }} - data-testid={TEST_IDS.tokenTextField} - hasError={ - isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 - } - description={ - isUserConnected && !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - )} - -
- {PRESET_PERCENTAGES.map(percentage => ( - + +
+ {isIntegratedSwapFeatureEnabled || canWrapNativeToken ? ( + 0} + disabled={!isUserConnected || isSubmitting} + onChange={amountTokens => + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + // Reset selected fixed percentage + fixedRepayPercentage: undefined, + })) + } + onChangeSelectedToken={fromToken => setFormValues(currentFormValues => ({ ...currentFormValues, - fixedRepayPercentage: percentage, + fromToken, })) } - > - {formatPercentageToReadableValue(percentage)} - - ))} + rightMaxButton={{ + label: t('operationForm.rightMaxButtonLabel'), + onClick: handleRightMaxButtonClick, + }} + tokenBalances={tokenBalances} + description={ + !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } + /> + ) : ( + + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + // Reset selected fixed percentage + fixedRepayPercentage: undefined, + })) + } + disabled={!isUserConnected || isSubmitting} + rightMaxButton={{ + label: t('operationForm.rightMaxButtonLabel'), + onClick: handleRightMaxButtonClick, + }} + data-testid={TEST_IDS.tokenTextField} + hasError={ + isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 + } + description={ + isUserConnected && !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } + /> + )} + +
+ {PRESET_PERCENTAGES.map(percentage => ( + + setFormValues(currentFormValues => ({ + ...currentFormValues, + fixedRepayPercentage: percentage, + })) + } + > + {formatPercentageToReadableValue(percentage)} + + ))} +
+ + {!isUserConnected && }
- {isUserConnected ? ( - <> + +
{!isSubmitting && !isSwapLoading && !formError && ( )} @@ -334,55 +339,41 @@ export const RepayFormUi: React.FC = ({ )} -
-
- + - + - -
- - -
- - ) : ( -
- - - - {t('operationForm.connectWalletButtonLabel')} - +
- )} + + + ); }; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/SubmitSection/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/SubmitSection/index.tsx index 4bf9bac66d..a10155a497 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/SubmitSection/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/SubmitSection/index.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'libs/translations'; import type { Swap, Token } from 'types'; import { cn } from 'utilities'; +import { SwitchChain } from 'containers/SwitchChain'; import SwapSummary from '../../SwapSummary'; import type { FormError } from '../../types'; import type { FormErrorCode } from '../useForm/types'; @@ -57,17 +58,8 @@ export const SubmitSection: React.FC = ({ return t('operationForm.submitButtonLabel.supply'); }, [isFormValid, t]); - return ( - + let dom = ( + <> = ({ {isFormValid && !isSwapLoading && !isFromTokenWalletSpendingLimitLoading && ( )} - + ); + + if (isFormValid) { + dom = ( + + + {dom} + + + ); + } + + return dom; }; export default SubmitSection; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/index.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/index.spec.tsx index 32fd330055..4c009048d6 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/index.spec.tsx @@ -13,8 +13,9 @@ import { supply } from 'clients/api'; import useCollateral from 'hooks/useCollateral'; import useTokenApproval from 'hooks/useTokenApproval'; import { en } from 'libs/translations'; -import type { Asset } from 'types'; +import { type Asset, ChainId } from 'types'; +import { chainMetadata } from '@venusprotocol/chains'; import MAX_UINT256 from 'constants/maxUint256'; import SupplyForm from '..'; import { fakeAsset, fakePool } from '../__testUtils__/fakeData'; @@ -46,7 +47,7 @@ describe('SupplyForm', () => { ); // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); + expect(getByText(en.connectWallet.connectButton)).toBeInTheDocument(); // Check collateral switch is disabled expect(getByRole('checkbox')).toBeDisabled(); @@ -225,6 +226,34 @@ describe('SupplyForm', () => { await checkSubmitButtonIsDisabled(); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { getByText, getByTestId } = renderComponent( + , + { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_ONE, + chainId: ChainId.BSC_TESTNET, + }, + ); + + const correctAmountTokens = 1; + + const tokenTextInput = await waitFor(() => getByTestId(TEST_IDS.tokenTextField)); + fireEvent.change(tokenTextInput, { target: { value: correctAmountTokens } }); + + // Check "Switch chain" button is displayed + await waitFor(() => + expect( + getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('displays the wallet spending limit correctly and lets user revoke it', async () => { const originalTokenApprovalOutput = useTokenApproval({ token: xvs, diff --git a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/indexIntegratedSwap.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/indexIntegratedSwap.spec.tsx index c55633b171..75e2505821 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/indexIntegratedSwap.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/__tests__/indexIntegratedSwap.spec.tsx @@ -94,27 +94,6 @@ describe('SupplyForm - Feature flag enabled: integratedSwap', () => { renderComponent(); }); - it('prompts user to connect their wallet if they are not connected', async () => { - const { getByText, getByTestId, getByRole } = renderComponent( - , - ); - - // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); - - // Check collateral switch is disabled - expect(getByRole('checkbox')).toBeDisabled(); - - // Check input is disabled - expect( - getByTestId( - getTokenTextFieldTestId({ - parentTestId: TEST_IDS.selectTokenTextField, - }), - ), - ).toBeDisabled(); - }); - it('disables swap feature when swapAndSupply action of asset is disabled', async () => { const customFakeAsset: Asset = { ...fakeAsset, diff --git a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/index.tsx index ca2cdeeee9..948acad087 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/SupplyForm/index.tsx @@ -29,6 +29,7 @@ import { useAccountAddress } from 'libs/wallet'; import type { Asset, Pool, Swap, SwapError, TokenBalance } from 'types'; import { areTokensEqual, + cn, convertMantissaToTokens, convertTokensToMantissa, getUniqueTokenBalances, @@ -225,104 +226,108 @@ export const SupplyFormUi: React.FC = ({ ]); return ( -
- {!isWrapUnwrapNativeTokenEnabled && !!asset.vToken.underlyingToken.tokenWrapped && ( - , - }} - values={{ - nativeTokenSymbol: asset.vToken.underlyingToken.tokenWrapped.symbol, - wrappedNativeTokenSymbol: asset.vToken.underlyingToken.symbol, - }} + +
+ {!isWrapUnwrapNativeTokenEnabled && !!asset.vToken.underlyingToken.tokenWrapped && ( + , + }} + values={{ + nativeTokenSymbol: asset.vToken.underlyingToken.tokenWrapped.symbol, + wrappedNativeTokenSymbol: asset.vToken.underlyingToken.symbol, + }} + /> + } + /> + )} + + {(asset.collateralFactor || asset.isCollateralOfUser) && ( + + - } - /> - )} - - {(asset.collateralFactor || asset.isCollateralOfUser) && ( - - + )} + + {isIntegratedSwapFeatureEnabled || canWrapNativeToken ? ( + 0} + disabled={ + !isUserConnected || + isSubmitting || + isApproveFromTokenLoading || + formError?.code === 'SUPPLY_CAP_ALREADY_REACHED' + } + onChange={amountTokens => + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + })) + } + onChangeSelectedToken={fromToken => + setFormValues(currentFormValues => ({ + ...currentFormValues, + fromToken, + })) + } + rightMaxButton={{ + label: t('operationForm.rightMaxButtonLabel'), + onClick: handleRightMaxButtonClick, + }} + tokenBalances={tokenBalances} + description={ + !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } /> -
- )} - - {isIntegratedSwapFeatureEnabled || canWrapNativeToken ? ( - 0} - disabled={ - !isUserConnected || - isSubmitting || - isApproveFromTokenLoading || - formError?.code === 'SUPPLY_CAP_ALREADY_REACHED' - } - onChange={amountTokens => - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - })) - } - onChangeSelectedToken={fromToken => - setFormValues(currentFormValues => ({ - ...currentFormValues, - fromToken, - })) - } - rightMaxButton={{ - label: t('operationForm.rightMaxButtonLabel'), - onClick: handleRightMaxButtonClick, - }} - tokenBalances={tokenBalances} - description={ - !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - ) : ( - - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - })) - } - disabled={ - !isUserConnected || - isSubmitting || - isApproveFromTokenLoading || - formError?.code === 'SUPPLY_CAP_ALREADY_REACHED' - } - rightMaxButton={{ - label: t('operationForm.rightMaxButtonLabel'), - onClick: handleRightMaxButtonClick, - }} - hasError={ - isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 - } - description={ - isUserConnected && !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - )} + ) : ( + + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + })) + } + disabled={ + !isUserConnected || + isSubmitting || + isApproveFromTokenLoading || + formError?.code === 'SUPPLY_CAP_ALREADY_REACHED' + } + rightMaxButton={{ + label: t('operationForm.rightMaxButtonLabel'), + onClick: handleRightMaxButtonClick, + }} + hasError={ + isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 + } + description={ + isUserConnected && !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } + /> + )} + + {!isUserConnected && } +
- {isUserConnected ? ( - <> + +
{!isSubmitting && !isSwapLoading && !formError && }
@@ -356,57 +361,43 @@ export const SupplyFormUi: React.FC = ({ )} -
-
- + - + - -
- - -
- - ) : ( -
- - - - {t('operationForm.connectWalletButtonLabel')} - +
- )} + + + ); }; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/SubmitSection/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/SubmitSection/index.tsx index 89d2320f48..59e00000ec 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/SubmitSection/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/SubmitSection/index.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { PrimaryButton } from 'components'; +import { SwitchChain } from 'containers/SwitchChain'; import { useTranslation } from 'libs/translations'; import { ApproveDelegateSteps, type ApproveDelegateStepsProps } from '../../ApproveDelegateSteps'; @@ -31,25 +32,34 @@ export const SubmitSection: React.FC = ({ return t('operationForm.submitButtonLabel.withdraw'); }, [isFormValid, t]); - return ( - - - {submitButtonLabel} - - + {submitButtonLabel} + ); + + if (isFormValid) { + dom = ( + + + {dom} + + + ); + } + + return dom; }; export default SubmitSection; diff --git a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/index.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/index.spec.tsx index f470e79cbb..d90f53a97a 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/index.spec.tsx @@ -9,8 +9,9 @@ import { renderComponent } from 'testUtils/render'; import { getVTokenBalanceOf, withdraw } from 'clients/api'; import { en } from 'libs/translations'; -import type { Asset, Pool } from 'types'; +import { type Asset, ChainId, type Pool } from 'types'; +import { chainMetadata } from '@venusprotocol/chains'; import Withdraw from '..'; import { fakeAsset, fakePool, fakeVTokenBalanceMantissa } from '../__testUtils__/fakeData'; import TEST_IDS from '../testIds'; @@ -22,7 +23,7 @@ describe('WithdrawForm', () => { ); // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); + expect(getByText(en.connectWallet.connectButton)).toBeInTheDocument(); // Check input is disabled expect(getByTestId(TEST_IDS.valueInput).closest('input')).toBeDisabled(); @@ -120,6 +121,34 @@ describe('WithdrawForm', () => { expect(submitButton).toBeDisabled(); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { getByText, getByTestId } = renderComponent( + , + { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_ONE, + chainId: ChainId.BSC_TESTNET, + }, + ); + + const correctAmountTokens = 1; + + const tokenTextInput = await waitFor(() => getByTestId(TEST_IDS.valueInput)); + fireEvent.change(tokenTextInput, { target: { value: correctAmountTokens } }); + + // Check "Switch chain" button is displayed + await waitFor(() => + expect( + getByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('displays correct token withdrawable amount', async () => { const { getByText } = renderComponent( , diff --git a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/indexWrapUnwrapNative.spec.tsx b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/indexWrapUnwrapNative.spec.tsx index 8e9a0e0c2a..fead4e95f7 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/indexWrapUnwrapNative.spec.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/__tests__/indexWrapUnwrapNative.spec.tsx @@ -31,18 +31,6 @@ describe('WithdrawForm - Feature flag enabled: wrapUnwrapNativeToken', () => { }); }); - it('prompts user to connect their wallet if they are not connected', async () => { - const { getByText, getByTestId } = renderComponent( - , - ); - - // Check "Connect wallet" button is displayed - expect(getByText(en.operationForm.connectWalletButtonLabel)).toBeInTheDocument(); - - // Check input is disabled - expect(getByTestId(TEST_IDS.valueInput).closest('input')).toBeDisabled(); - }); - it('does not display the receive native token toggle if the underlying token does not wrap the chain native token', async () => { const { queryByTestId } = renderComponent( , diff --git a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/index.tsx b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/index.tsx index 5fa39614bb..ac568188bc 100644 --- a/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/index.tsx +++ b/apps/evm/src/pages/Market/Page/OperationForm/WithdrawForm/index.tsx @@ -13,7 +13,7 @@ import { VError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import type { Asset, Pool } from 'types'; -import { convertTokensToMantissa } from 'utilities'; +import { cn, convertTokensToMantissa } from 'utilities'; import { NULL_ADDRESS } from 'constants/address'; import { ConnectWallet } from 'containers/ConnectWallet'; @@ -147,35 +147,39 @@ export const WithdrawFormUi: React.FC = ({ } return ( -
- - setFormValues(currentFormValues => ({ - ...currentFormValues, - amountTokens, - })) - } - disabled={!isUserConnected || isSubmitting} - rightMaxButton={{ - label: t('operationForm.rightMaxButtonLabel'), - onClick: handleRightMaxButtonClick, - }} - hasError={ - isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 - } - description={ - isUserConnected && !isSubmitting && !!formError?.message ? ( -

{formError.message}

- ) : undefined - } - /> - - {isUserConnected ? ( - <> + +
+ + setFormValues(currentFormValues => ({ + ...currentFormValues, + amountTokens, + })) + } + disabled={!isUserConnected || isSubmitting} + rightMaxButton={{ + label: t('operationForm.rightMaxButtonLabel'), + onClick: handleRightMaxButtonClick, + }} + hasError={ + isUserConnected && !isSubmitting && !!formError && Number(formValues.amountTokens) > 0 + } + description={ + isUserConnected && !isSubmitting && !!formError?.message ? ( +

{formError.message}

+ ) : undefined + } + /> + + {!isUserConnected && } +
+ + +
{readableWithdrawableAmountTokens} @@ -204,44 +208,32 @@ export const WithdrawFormUi: React.FC = ({ )} -
-
- + - + - -
- - -
- - ) : ( -
- - - - {t('operationForm.connectWalletButtonLabel')} - +
- )} + + + ); }; diff --git a/apps/evm/src/pages/Proposal/Commands/BscCommand/ActionButton/index.tsx b/apps/evm/src/pages/Proposal/Commands/BscCommand/ActionButton/index.tsx index 597f1f0e6d..72bf9c0511 100644 --- a/apps/evm/src/pages/Proposal/Commands/BscCommand/ActionButton/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/BscCommand/ActionButton/index.tsx @@ -2,13 +2,13 @@ import { useCancelProposal, useExecuteProposal, useQueueProposal } from 'clients import { Button } from 'components'; import type { ConnectWalletProps } from 'containers/ConnectWallet'; import { ConnectWallet } from 'containers/ConnectWallet'; +import { SwitchChain } from 'containers/SwitchChain'; import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable'; import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { governanceChain, useAccountAddress } from 'libs/wallet'; import { useMemo } from 'react'; import { ProposalState } from 'types'; -import { cn } from 'utilities'; import type { Address } from 'viem'; import { useIsProposalCancelableByUser } from '../../useIsProposalCancelableByUser'; @@ -24,7 +24,6 @@ export const ActionButton: React.FC = ({ proposalId, proposerAddress, executionEtaDate, - className, ...otherProps }) => { const { t } = useTranslation(); @@ -116,8 +115,8 @@ export const ActionButton: React.FC = ({ ]); return ( - - {buttonDom} + + {buttonDom} ); }; diff --git a/apps/evm/src/pages/Proposal/Commands/NonBscCommand/ExecuteButton/index.tsx b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/ExecuteButton/index.tsx new file mode 100644 index 0000000000..cb5fe2ca4a --- /dev/null +++ b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/ExecuteButton/index.tsx @@ -0,0 +1,42 @@ +import type { ChainId } from '@venusprotocol/chains'; +import { useExecuteProposal } from 'clients/api'; +import { Button } from 'components'; +import type { ConnectWalletProps } from 'containers/ConnectWallet'; +import { ConnectWallet } from 'containers/ConnectWallet'; +import { SwitchChain } from 'containers/SwitchChain'; +import { handleError } from 'libs/errors'; +import { useTranslation } from 'libs/translations'; + +export interface ExecuteButtonProps extends Omit { + remoteProposalChainId: ChainId; + remoteProposalId: number; +} + +export const ExecuteButton: React.FC = ({ + remoteProposalChainId, + remoteProposalId, + ...otherProps +}) => { + const { t } = useTranslation(); + + const { mutateAsync: executeProposal, isPending: isExecuteProposalLoading } = + useExecuteProposal(); + + const execute = async () => { + try { + await executeProposal({ proposalId: remoteProposalId, chainId: remoteProposalChainId }); + } catch (error) { + handleError({ error }); + } + }; + + return ( + + + + + + ); +}; diff --git a/apps/evm/src/pages/Proposal/Commands/NonBscCommand/index.tsx b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/index.tsx index f8a04b46ac..5eb732200c 100644 --- a/apps/evm/src/pages/Proposal/Commands/NonBscCommand/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/index.tsx @@ -1,9 +1,5 @@ import { chainMetadata } from '@venusprotocol/chains'; -import { useExecuteProposal } from 'clients/api'; -import { Button } from 'components'; -import { ConnectWallet } from 'containers/ConnectWallet'; import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable'; -import { VError, handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { governanceChain, useChainId } from 'libs/wallet'; import { useMemo } from 'react'; @@ -11,6 +7,7 @@ import { type RemoteProposal, RemoteProposalState } from 'types'; import { Command } from '../Command'; import { Description } from '../Description'; import { CurrentStep } from './CurrentStep'; +import { ExecuteButton } from './ExecuteButton'; const governanceChainMetadata = chainMetadata[governanceChain.id]; @@ -35,24 +32,6 @@ export const NonBscCommand: React.FC = ({ executionEtaDate: remoteProposal.executionEtaDate, }); - const { mutateAsync: executeProposal, isPending: isExecuteProposalLoading } = - useExecuteProposal(); - - const execute = async () => { - if (!remoteProposal.remoteProposalId) { - throw new VError({ type: 'unexpected', code: 'somethingWentWrong' }); - } - - try { - await executeProposal({ - proposalId: remoteProposal.remoteProposalId, - chainId: remoteProposal.chainId, - }); - } catch (error) { - handleError({ error }); - } - }; - const description = useMemo(() => { switch (remoteProposal.state) { case RemoteProposalState.Pending: @@ -91,12 +70,12 @@ export const NonBscCommand: React.FC = ({ } proposalActions={remoteProposal.proposalActions} contentRightItem={ - isExecutable ? ( - - - + isExecutable && remoteProposal.remoteProposalId ? ( + ) : ( = ({ ) } contentBottomItem={ - isExecutable ? ( - - - + isExecutable && remoteProposal.remoteProposalId ? ( + ) : undefined } {...otherProps} diff --git a/apps/evm/src/pages/Proposal/__tests__/index.spec.tsx b/apps/evm/src/pages/Proposal/__tests__/index.spec.tsx index a46512b3aa..354e847362 100644 --- a/apps/evm/src/pages/Proposal/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Proposal/__tests__/index.spec.tsx @@ -74,6 +74,7 @@ const checkVoteButtonsAreHidden = async ( waitFor(() => expect(queryByText(en.vote.abstain, { selector: 'button' })).toBeNull()); }; +// TODO: rename to "Proposal" describe('ProposalComp page', () => { beforeEach(() => { vi.useFakeTimers().setSystemTime(fakeNow); @@ -220,6 +221,17 @@ describe('ProposalComp page', () => { await waitFor(() => expect(screen.getByTestId(TEST_IDS.votingDisabledWarning)).toBeVisible()); }); + it('renders warning about voting being disabled when the feature flag is on, proposal is active and user is connected to another chain than the governance one', async () => { + (useIsFeatureEnabled as Mock).mockImplementation(() => true); + + renderComponent(, { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.ARBITRUM_SEPOLIA, + }); + + await waitFor(() => expect(screen.getByTestId(TEST_IDS.votingDisabledWarning)).toBeVisible()); + }); + it('allows user to vote for', async () => { const vote = vi.fn(); (useVote as Mock).mockImplementation(() => ({ @@ -701,7 +713,7 @@ describe('ProposalComp page', () => { }); const executeButton = screen - .getAllByText(en.voteProposalUi.command.cta.execute)[0] + .getAllByText(en.voteProposalUi.command.actionButton.execute)[0] .closest('button'); expect(executeButton).toBeInTheDocument(); diff --git a/apps/evm/src/pages/Proposal/index.tsx b/apps/evm/src/pages/Proposal/index.tsx index 8bd23d96f1..74a2d97ad1 100644 --- a/apps/evm/src/pages/Proposal/index.tsx +++ b/apps/evm/src/pages/Proposal/index.tsx @@ -10,7 +10,7 @@ import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import useVote, { type UseVoteParams } from 'hooks/useVote'; import { useGetToken } from 'libs/tokens'; import { useTranslation } from 'libs/translations'; -import { governanceChain, useAccountAddress, useSwitchChain } from 'libs/wallet'; +import { governanceChain, useAccountAddress, useAccountChainId, useSwitchChain } from 'libs/wallet'; import { ProposalState, type Proposal as ProposalType } from 'types'; import { convertMantissaToTokens } from 'utilities'; @@ -27,7 +27,8 @@ import TEST_IDS from './testIds'; interface ProposalUiProps { proposal: ProposalType | undefined; vote: (params: UseVoteParams) => Promise; - votingEnabled: boolean; + canUserVoteOnProposal: boolean; + isUserConnectedToGovernanceChain: boolean; readableVoteWeight: string; isVoteLoading: boolean; } @@ -35,17 +36,23 @@ interface ProposalUiProps { export const ProposalUi: React.FC = ({ proposal, vote, - votingEnabled, + canUserVoteOnProposal, + isUserConnectedToGovernanceChain, readableVoteWeight, isVoteLoading, }) => { - const { switchChain } = useSwitchChain(); - const isVoteProposalFeatureEnabled = useIsFeatureEnabled({ name: 'voteProposal' }); const styles = useStyles(); const { t } = useTranslation(); + const { switchChain } = useSwitchChain(); const [voteModalType, setVoteModalType] = useState<0 | 1 | 2 | undefined>(undefined); + const isVoteProposalFeatureEnabled = useIsFeatureEnabled({ name: 'voteProposal' }); + + const shouldEnableVoteButtons = + isVoteProposalFeatureEnabled && canUserVoteOnProposal && isUserConnectedToGovernanceChain; + const shouldShowWarning = !isVoteProposalFeatureEnabled || !isUserConnectedToGovernanceChain; + if (!proposal) { return (
@@ -58,7 +65,7 @@ export const ProposalUi: React.FC = ({
- {!isVoteProposalFeatureEnabled && proposal.state === ProposalState.Active && ( + {shouldShowWarning && ( = ({ voters={proposal.forVotes} openVoteModal={() => setVoteModalType(1)} progressBarColor={styles.successColor} - votingEnabled={votingEnabled && isVoteProposalFeatureEnabled} + votingEnabled={shouldEnableVoteButtons} data-testid={TEST_IDS.voteSummary.for} /> @@ -94,7 +101,7 @@ export const ProposalUi: React.FC = ({ voters={proposal.againstVotes} openVoteModal={() => setVoteModalType(0)} progressBarColor={styles.againstColor} - votingEnabled={votingEnabled && isVoteProposalFeatureEnabled} + votingEnabled={shouldEnableVoteButtons} data-testid={TEST_IDS.voteSummary.against} /> @@ -105,7 +112,7 @@ export const ProposalUi: React.FC = ({ voters={proposal.abstainVotes} openVoteModal={() => setVoteModalType(2)} progressBarColor={styles.abstainColor} - votingEnabled={votingEnabled && isVoteProposalFeatureEnabled} + votingEnabled={shouldEnableVoteButtons} data-testid={TEST_IDS.voteSummary.abstain} />
@@ -131,6 +138,7 @@ export const ProposalUi: React.FC = ({ const Proposal = () => { const { accountAddress } = useAccountAddress(); + const { chainId: accountChainId } = useAccountChainId(); const { proposalId = '' } = useParams<{ proposalId: string }>(); const { data: proposalData, error: getProposalError } = useGetProposal( { proposalId: Number(proposalId), accountAddress }, @@ -169,7 +177,7 @@ const Proposal = () => { ); // voting should be enabled if: - const votingEnabled = + const canUserVoteOnProposal = // user wallet is connected !!accountAddress && // proposal is still active @@ -179,6 +187,8 @@ const Proposal = () => { // user has some voting weight votingWeightData.votesMantissa.isGreaterThan(0); + const isUserConnectedToGovernanceChain = accountChainId === governanceChain.id; + if (getProposalError) { return ; } @@ -188,7 +198,8 @@ const Proposal = () => { diff --git a/apps/evm/src/pages/Swap/SubmitSection/index.tsx b/apps/evm/src/pages/Swap/SubmitSection/index.tsx index 4f271b4594..250cfd3916 100644 --- a/apps/evm/src/pages/Swap/SubmitSection/index.tsx +++ b/apps/evm/src/pages/Swap/SubmitSection/index.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'libs/translations'; import type { Swap, SwapError } from 'types'; import { cn } from 'utilities'; +import { SwitchChain } from 'containers/SwitchChain'; import type { FormError, FormValues } from '../types'; import { useStyles } from './styles'; @@ -93,35 +94,44 @@ const SubmitSection: React.FC = ({ return t('swapPage.submitButton.disabledLabels.processing'); }, [swap, swapError, isSwappingWithHighPriceImpact, formErrors, fromToken.symbol, t]); - return ( - - - {submitButtonLabel} - - + {submitButtonLabel} + ); + + if (isFormValid) { + dom = ( + + + {dom} + + + ); + } + + return dom; }; export default SubmitSection; diff --git a/apps/evm/src/pages/Vai/Repay/index.tsx b/apps/evm/src/pages/Vai/Repay/index.tsx index a2d34b9cc4..fb9da391fc 100644 --- a/apps/evm/src/pages/Vai/Repay/index.tsx +++ b/apps/evm/src/pages/Vai/Repay/index.tsx @@ -254,7 +254,6 @@ export const Repay: React.FC = () => { vaiControllerContractAddress && { token: vai, spenderAddress: vaiControllerContractAddress, - hideSpendingApprovalStep: !formState.isValid, } } control={control} diff --git a/apps/evm/src/pages/Vault/TransactionForm/SubmitSection/index.tsx b/apps/evm/src/pages/Vault/TransactionForm/SubmitSection/index.tsx new file mode 100644 index 0000000000..43105ae064 --- /dev/null +++ b/apps/evm/src/pages/Vault/TransactionForm/SubmitSection/index.tsx @@ -0,0 +1,72 @@ +import { ApproveTokenSteps } from 'components'; +import { FormikSubmitButton } from 'containers/Form'; +import { SwitchChain } from 'containers/SwitchChain'; +import type { Token } from 'types'; +import { cn } from 'utilities'; + +export interface SubmitSectionProps { + token: Token; + approveToken: () => Promise; + tokenNeedsToBeApproved: boolean; + isFormValid: boolean; + isSubmitting: boolean; + isTokenApproved: boolean; + isApproveTokenLoading: boolean; + isWalletSpendingLimitLoading: boolean; + isRevokeWalletSpendingLimitLoading: boolean; + submitButtonEnabledLabel: string; + submitButtonDisabledLabel: string; + isDangerousAction: boolean; +} + +export const SubmitSection: React.FC = ({ + token, + tokenNeedsToBeApproved, + approveToken, + isFormValid, + isSubmitting, + isTokenApproved, + isApproveTokenLoading, + isWalletSpendingLimitLoading, + isRevokeWalletSpendingLimitLoading, + submitButtonEnabledLabel, + submitButtonDisabledLabel, + isDangerousAction, +}) => { + let dom = ( + + ); + + if (isFormValid && tokenNeedsToBeApproved) { + dom = ( + + {dom} + + ); + } + + if (isFormValid) { + dom = {dom}; + } + + return dom; +}; diff --git a/apps/evm/src/pages/Vault/TransactionForm/index.spec.tsx b/apps/evm/src/pages/Vault/TransactionForm/index.spec.tsx index ba559a3c0b..8e504e3bda 100644 --- a/apps/evm/src/pages/Vault/TransactionForm/index.spec.tsx +++ b/apps/evm/src/pages/Vault/TransactionForm/index.spec.tsx @@ -10,7 +10,9 @@ import { renderComponent } from 'testUtils/render'; import useTokenApproval from 'hooks/useTokenApproval'; +import { ChainId, chainMetadata } from '@venusprotocol/chains'; import { NULL_ADDRESS } from 'constants/address'; +import { en } from 'libs/translations'; import TransactionForm, { type TransactionFormProps } from '.'; import TEST_IDS from './testIds'; @@ -39,6 +41,30 @@ describe('TransactionForm', () => { expect(getByTestId(TEST_IDS.lockingPeriod).textContent).toMatchSnapshot(); }); + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { queryByText, getByTestId } = renderComponent(, { + accountAddress: fakeAccountAddress, + accountChainId: ChainId.SEPOLIA, + chainId: ChainId.BSC_TESTNET, + }); + + fireEvent.change(getByTestId(TEST_IDS.tokenTextField), { + target: { value: 1 }, + }); + + // Check switch button is present + await waitFor(() => + expect( + queryByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('displays the wallet spending limit correctly and lets user revoke it', async () => { const originalTokenApprovalOutput = useTokenApproval({ token: vai, diff --git a/apps/evm/src/pages/Vault/TransactionForm/index.tsx b/apps/evm/src/pages/Vault/TransactionForm/index.tsx index 047c00f936..e66297b2bf 100644 --- a/apps/evm/src/pages/Vault/TransactionForm/index.tsx +++ b/apps/evm/src/pages/Vault/TransactionForm/index.tsx @@ -2,7 +2,6 @@ import BigNumber from 'bignumber.js'; import { useCallback, useMemo } from 'react'; import { - ApproveTokenSteps, type ApproveTokenStepsProps, LabeledInlineContent, NoticeWarning, @@ -15,10 +14,11 @@ import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import type { Token } from 'types'; -import { cn, convertMantissaToTokens, convertTokensToMantissa } from 'utilities'; +import { convertMantissaToTokens, convertTokensToMantissa } from 'utilities'; -import { FormikSubmitButton, FormikTokenTextField } from 'containers/Form'; +import { FormikTokenTextField } from 'containers/Form'; import type { Address } from 'viem'; +import { SubmitSection } from './SubmitSection'; import TEST_IDS from './testIds'; export interface TransactionFormUiProps { @@ -101,7 +101,7 @@ export const TransactionFormUi: React.FC = ({ const shouldDisplayWarning = useCallback( (amountTokens: string) => - !!amountTokens && warning?.amountTokens.isLessThanOrEqualTo(amountTokens), + amountTokens && warning ? warning.amountTokens.isLessThanOrEqualTo(amountTokens) : false, [warning], ); @@ -174,44 +174,24 @@ export const TransactionFormUi: React.FC = ({ )}
- {tokenNeedsToBeApproved ? ( - - - - ) : ( - - )} +
)} diff --git a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/RequestWithdrawal/__tests__/index.spec.tsx b/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/RequestWithdrawal/__tests__/index.spec.tsx index 62865d6437..b8a31b9edc 100644 --- a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/RequestWithdrawal/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/RequestWithdrawal/__tests__/index.spec.tsx @@ -54,6 +54,20 @@ describe('RequestWithdrawal', () => { await waitFor(() => getByTestId(TEST_IDS.availableTokens)); }); + it('prompts user to connect their wallet if they are not connected', async () => { + const { queryByText } = renderComponent( + , + ); + + // Check connect button is present + await waitFor(() => expect(queryByText(en.connectWallet.connectButton)).toBeInTheDocument()); + }); + it('fetches staked tokens and locking period and displays them correctly', async () => { const { getByTestId } = renderComponent( { await waitFor(() => getByText(en.withdrawFromVestingVaultModalModal.withdrawTab.submitButton)); }); + it('prompts user to connect their wallet if they are not connected', async () => { + const { queryByText } = renderComponent( + , + ); + + // Check connect button is present + await waitFor(() => expect(queryByText(en.connectWallet.connectButton)).toBeInTheDocument()); + }); + + it('prompts user to switch chain if they are connected to the wrong one', async () => { + const { queryByText } = renderComponent( + , + { + accountAddress: fakeAddress, + accountChainId: ChainId.SEPOLIA, + chainId: ChainId.BSC_TESTNET, + }, + ); + + // Check switch button is present + await waitFor(() => + expect( + queryByText( + en.switchChain.switchButton.replace( + '{{chainName}}', + chainMetadata[ChainId.BSC_TESTNET].name, + ), + ), + ).toBeInTheDocument(), + ); + }); + it('fetches available tokens amount and displays it correctly', async () => { const { getByTestId } = renderComponent( , diff --git a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/index.tsx b/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/index.tsx index aab07bcef7..ba813aa757 100644 --- a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/index.tsx +++ b/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/index.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import type { Token } from 'types'; -import { useStyles } from './styles'; +import { SwitchChain } from 'containers/SwitchChain'; import TEST_IDS from './testIds'; export interface WithdrawUiProps { @@ -33,7 +33,6 @@ const WithdrawUi: React.FC = ({ withdrawableMantissa, }) => { const { t } = useTranslation(); - const styles = useStyles(); const handleSubmit = async () => { await onSubmit(); @@ -46,14 +45,31 @@ const WithdrawUi: React.FC = ({ token: stakedToken, }); + const hasWithdrawableTokens = !!withdrawableMantissa && withdrawableMantissa.isGreaterThan(0); + + let submitDom = ( + + {t('withdrawFromVestingVaultModalModal.withdrawTab.submitButton')} + + ); + + if (hasWithdrawableTokens) { + submitDom = {submitDom}; + } + return ( <> {isInitialLoading || !withdrawableMantissa ? ( ) : ( - <> +
= ({ {readableWithdrawableTokens} - - {t('withdrawFromVestingVaultModalModal.withdrawTab.submitButton')} - - + {submitDom} +
)} ); diff --git a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/styles.ts b/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/styles.ts deleted file mode 100644 index dc5f0063cc..0000000000 --- a/apps/evm/src/pages/Vault/modals/WithdrawFromVestingVaultModal/Withdraw/styles.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { css } from '@emotion/react'; -import { useTheme } from '@mui/material'; - -export const useStyles = () => { - const theme = useTheme(); - - return { - content: css` - margin-bottom: ${theme.spacing(12)}; - `, - }; -}; diff --git a/apps/evm/src/testUtils/render.tsx b/apps/evm/src/testUtils/render.tsx index cce1db66c4..5271ba059c 100644 --- a/apps/evm/src/testUtils/render.tsx +++ b/apps/evm/src/testUtils/render.tsx @@ -7,9 +7,15 @@ import type { Mock } from 'vitest'; import fakeSigner from '__mocks__/models/signer'; -import { Web3Wrapper, useAccountAddress, useChainId, useSigner } from 'libs/wallet'; +import { + Web3Wrapper, + useAccountAddress, + useAccountChainId, + useChainId, + useSigner, +} from 'libs/wallet'; import { MuiThemeProvider } from 'theme/MuiThemeProvider'; -import type { ChainId } from 'types'; +import { ChainId } from 'types'; const createQueryClient = () => new QueryClient({ @@ -23,6 +29,7 @@ const createQueryClient = () => interface Options { accountAddress?: string; + accountChainId?: ChainId; chainId?: ChainId; routerInitialEntries?: string[]; routePath?: string; @@ -35,6 +42,8 @@ interface WrapperProps { } const Wrapper: React.FC = ({ children, options }) => { + const chainId = options?.chainId || ChainId.BSC_TESTNET; + if (options?.accountAddress) { const accountAddress = options?.accountAddress; @@ -42,6 +51,10 @@ const Wrapper: React.FC = ({ children, options }) => { accountAddress, })); + (useAccountChainId as Mock).mockImplementation(() => ({ + chainId: options?.accountChainId || chainId, + })); + (useSigner as Mock).mockImplementation(() => ({ signer: { ...fakeSigner, @@ -50,11 +63,9 @@ const Wrapper: React.FC = ({ children, options }) => { })); } - if (options?.chainId) { - (useChainId as Mock).mockImplementation(() => ({ - chainId: options?.chainId, - })); - } + (useChainId as Mock).mockImplementation(() => ({ + chainId, + })); return (