From 4f448e01cf5ceef1590fa211449635d1f05fbe68 Mon Sep 17 00:00:00 2001 From: Nicholas Smith Date: Wed, 15 Jan 2025 10:47:36 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20ensure=20StakingBalance=20is=20shown=20o?= =?UTF-8?q?r=20hidden=20appropriately=20per=20asset=E2=80=A6=20(#12987)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. What is the reason for the change? There are 2 separate issues that are both related to the StakedBalance component. One is Asset Detail StakingBalance shows for ETH assets on unsupported chains with popular networks filter. The other is Asset Detail StakingBalance does not show for mainnet ETH asset when on unsupported chain with popular networks filter. 2. What is the improvement/solution? The user will not see staked ethereum info for assets that are not mainnet Ethereum asset when on mainnet The user will see staked ethereum info when on unsupported chain with popular networks filter and viewing the mainnet Ethereum asset detail. We show the details but hide any staking actions for the time being while we re-evaluate how to keep the actions and switch the network on the fly. Fixes: https://consensyssoftware.atlassian.net/browse/STAKE-921 **Asset Detail StakingBalance shows for ETH assets on unsupported chains with popular networks filter** - Switch to mainnet network - Switch to popular networks filter - Select the native asset of a popular network that is not mainnet, such as Base or Optimism - In the asset detail page you should not see staked ethereum info **Asset Detail StakingBalance does not show for mainnet ETH asset when on unsupported chain with popular networks filter** Switch to Base network Switch to popular networks filter Select the mainnet Ethereum asset In the asset detail page you should see staked ethereum info and earnings info but no action buttons or claim link banners https://github.com/user-attachments/assets/83132195-8bd8-452f-a25f-37a9b1d276d9 https://github.com/user-attachments/assets/6e0bde2f-1dd2-4cd8-ad2c-e78cbe962ff1 - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../TokenDetails/TokenDetails.tsx | 4 +- app/components/UI/Stake/__mocks__/mockData.ts | 25 +- .../StakingBalance/StakingBalance.test.tsx | 36 ++ .../StakingBalance/StakingBalance.tsx | 19 +- .../StakingBalance.test.tsx.snap | 476 +----------------- .../StakingEarnings/StakingEarnings.test.tsx | 10 +- .../components/StakingEarnings/index.tsx | 20 +- .../UI/Stake/hooks/useBalance.test.tsx | 32 +- app/components/UI/Stake/hooks/useBalance.ts | 10 +- 9 files changed, 131 insertions(+), 501 deletions(-) diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx index 54df1781873..a56469bfff6 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx @@ -147,7 +147,9 @@ const TokenDetails: React.FC = ({ asset }) => { return ( - {asset.isETH && isPooledStakingFeatureEnabled() && } + {asset.isETH && isPooledStakingFeatureEnabled() && ( + + )} {(asset.isETH || tokenMetadata) && ( )} diff --git a/app/components/UI/Stake/__mocks__/mockData.ts b/app/components/UI/Stake/__mocks__/mockData.ts index db3eb95bc26..ab3e2b9ff3e 100644 --- a/app/components/UI/Stake/__mocks__/mockData.ts +++ b/app/components/UI/Stake/__mocks__/mockData.ts @@ -10,10 +10,12 @@ import { Contract } from 'ethers'; import { Stake } from '../sdk/stakeSdkProvider'; export const MOCK_STAKED_ETH_ASSET = { + chainId: '0x1', balance: '4.9999 ETH', balanceFiat: '$13,292.20', name: 'Staked Ethereum', symbol: 'ETH', + isETH: true, } as TokenI; export const MOCK_GET_POOLED_STAKES_API_RESPONSE: PooledStakes = { @@ -67,17 +69,18 @@ export const MOCK_GET_POOLED_STAKES_API_RESPONSE: PooledStakes = { exchangeRate: '1.010906701603882254', }; -export const MOCK_GET_POOLED_STAKES_API_RESPONSE_HIGH_ASSETS_AMOUNT: PooledStakes = { - accounts: [ - { - account: '0x0111111111abcdef2222222222abcdef33333333', - lifetimeRewards: '0', - assets: '99999999990000000000000', - exitRequests: [], - }, - ], - exchangeRate: '1.010906701603882254', -}; +export const MOCK_GET_POOLED_STAKES_API_RESPONSE_HIGH_ASSETS_AMOUNT: PooledStakes = + { + accounts: [ + { + account: '0x0111111111abcdef2222222222abcdef33333333', + lifetimeRewards: '0', + assets: '99999999990000000000000', + exitRequests: [], + }, + ], + exchangeRate: '1.010906701603882254', + }; export const MOCK_GET_VAULT_RESPONSE: VaultData = { apy: '2.853065141088762750393474836309926', diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx index 39e0dd2084b..347fc5780c7 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx @@ -14,6 +14,7 @@ import { createMockAccountsControllerState } from '../../../../../util/test/acco import { backgroundState } from '../../../../../util/test/initial-root-state'; // eslint-disable-next-line import/no-namespace import * as networks from '../../../../../util/networks'; +import { mockNetworkState } from '../../../../../util/test/network'; const MOCK_ADDRESS_1 = '0x0'; @@ -168,4 +169,39 @@ describe('StakingBalance', () => { screen: Routes.STAKING.UNSTAKE, }); }); + + it('should not render if asset chainId is not a staking supporting chain', () => { + const { queryByText, queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + expect(queryByTestId('staking-balance-container')).toBeNull(); + expect(queryByText(strings('stake.stake_more'))).toBeNull(); + expect(queryByText(strings('stake.unstake'))).toBeNull(); + expect(queryByText(strings('stake.claim'))).toBeNull(); + }); + + it('should not render claim link or action buttons if asset.chainId is not selected chainId', () => { + const { queryByText, queryByTestId } = renderWithProvider( + , + { + state: { + ...mockInitialState, + engine: { + ...mockInitialState.engine, + backgroundState: { + ...mockInitialState.engine.backgroundState, + NetworkController: { + ...mockNetworkState({ chainId: '0x4268' }), + }, + }, + }, + }, + }, + ); + expect(queryByTestId('staking-balance-container')).toBeTruthy(); + expect(queryByText(strings('stake.stake_more'))).toBeNull(); + expect(queryByText(strings('stake.unstake'))).toBeNull(); + expect(queryByText(strings('stake.claim'))).toBeNull(); + }); }); diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 1d41646bc3c..6e5c3858160 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { Hex } from '@metamask/utils'; import Badge, { BadgeVariant, } from '../../../../../component-library/components/Badges/Badge'; @@ -35,7 +36,7 @@ import { import { multiplyValueByPowerOfTen } from '../../utils/bignumber'; import StakingCta from './StakingCta/StakingCta'; import useStakingEligibility from '../../hooks/useStakingEligibility'; -import useStakingChain from '../../hooks/useStakingChain'; +import { useStakingChainByChainId } from '../../hooks/useStakingChain'; import usePooledStakes from '../../hooks/usePooledStakes'; import useVaultData from '../../hooks/useVaultData'; import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; @@ -56,7 +57,9 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { isEligible: isEligibleForPooledStaking } = useStakingEligibility(); - const { isStakingSupportedChain } = useStakingChain(); + const { isStakingSupportedChain } = useStakingChainByChainId( + asset.chainId as Hex, + ); const { pooledStakesData, @@ -71,7 +74,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { formattedStakedBalanceETH: stakedBalanceETH, formattedStakedBalanceFiat: stakedBalanceFiat, - } = useBalance(); + } = useBalance(asset.chainId as Hex); const { unstakingRequests, claimableRequests } = useMemo(() => { const exitRequests = pooledStakesData?.exitRequests ?? []; @@ -97,6 +100,9 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { } const renderStakingContent = () => { + if (chainId !== asset.chainId) { + return <>; + } if (isLoadingPooledStakesData) { return ( @@ -165,7 +171,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { }; return ( - + {hasEthToUnstake && ( { badgeElement={ } diff --git a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap index 61fe94e4e3f..df84251881d 100644 --- a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap +++ b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StakingBalance render matches snapshot 1`] = ` - + - - - - - - - - - - - - - - - - - - - - Staked Ethereum - - - - - - - - - - - Unstaking 0.0010 ETH in progress. Come back in a few days to claim it. - - - - - - - - - - You can claim 0.00214 ETH. Once claimed, you'll get ETH back in your wallet. - - - - Claim - ETH - - - - - - - - Unstake - - - - - Stake more - - - - - -`; - -exports[`StakingBalance should match the snapshot when portfolio view is enabled 1`] = ` - + ({ isPooledStakingFeatureEnabled: jest.fn().mockReturnValue(true), @@ -70,9 +71,12 @@ jest.mock('../../../../../core/Engine', () => ({ describe('Staking Earnings', () => { it('should render correctly', () => { - const { toJSON, getByText } = renderWithProvider(, { - state: STATE_MOCK, - }); + const { toJSON, getByText } = renderWithProvider( + , + { + state: STATE_MOCK, + }, + ); expect(getByText(strings('stake.your_earnings'))).toBeDefined(); expect(getByText(strings('stake.annual_rate'))).toBeDefined(); diff --git a/app/components/UI/Stake/components/StakingEarnings/index.tsx b/app/components/UI/Stake/components/StakingEarnings/index.tsx index b5a7d035c02..134fda1c734 100644 --- a/app/components/UI/Stake/components/StakingEarnings/index.tsx +++ b/app/components/UI/Stake/components/StakingEarnings/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { View } from 'react-native'; +import { Hex } from '@metamask/utils'; import Text, { TextColor, TextVariant, @@ -16,15 +17,20 @@ import ButtonIcon, { import useTooltipModal from '../../../../../components/hooks/useTooltipModal'; import { strings } from '../../../../../../locales/i18n'; import { isPooledStakingFeatureEnabled } from '../../../Stake/constants'; -import useStakingChain from '../../hooks/useStakingChain'; +import { useStakingChainByChainId } from '../../hooks/useStakingChain'; import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; import useStakingEarnings from '../../hooks/useStakingEarnings'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { getTooltipMetricProperties } from '../../utils/metaMetrics/tooltipMetaMetricsUtils'; +import { TokenI } from '../../../Tokens/types'; -const StakingEarningsContent = () => { +export interface StakingEarningsProps { + asset: TokenI; +} + +const StakingEarningsContent = ({ asset }: StakingEarningsProps) => { const { styles } = useStyles(styleSheet, {}); const { openTooltipModal } = useTooltipModal(); @@ -39,14 +45,16 @@ const StakingEarningsContent = () => { hasStakedPositions, } = useStakingEarnings(); + const { isStakingSupportedChain } = useStakingChainByChainId( + asset.chainId as Hex, + ); + const onDisplayAnnualRateTooltip = () => openTooltipModal( strings('stake.annual_rate'), strings('tooltip_modal.reward_rate.tooltip'), ); - const { isStakingSupportedChain } = useStakingChain(); - if ( !isPooledStakingFeatureEnabled() || !isStakingSupportedChain || @@ -181,9 +189,9 @@ const StakingEarningsContent = () => { ); }; -export const StakingEarnings = () => ( +export const StakingEarnings = ({ asset }: StakingEarningsProps) => ( - + ); diff --git a/app/components/UI/Stake/hooks/useBalance.test.tsx b/app/components/UI/Stake/hooks/useBalance.test.tsx index e6bc9dc853d..5777c804fc7 100644 --- a/app/components/UI/Stake/hooks/useBalance.test.tsx +++ b/app/components/UI/Stake/hooks/useBalance.test.tsx @@ -22,7 +22,20 @@ const initialState = { AccountTrackerController: { accountsByChainId: { '0x1': { - [MOCK_ADDRESS_1]: { balance: toHex('12345678909876543210000000'), stakedBalance: toHex(MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].assets) }, + [MOCK_ADDRESS_1]: { + balance: toHex('12345678909876543210000000'), + stakedBalance: toHex( + MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].assets, + ), + }, + }, + '0x4268': { + [MOCK_ADDRESS_1]: { + balance: toHex('22345678909876543210000000'), + stakedBalance: toHex( + MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].assets, + ), + }, }, }, }, @@ -139,4 +152,21 @@ describe('useBalance', () => { expect(result.current.stakedBalanceFiatNumber).toBe(319999999.968); // Staked balance in fiat number expect(result.current.formattedStakedBalanceFiat).toBe('$319999999.96'); // should round to floor }); + + it('returns correct stake amounts and fiat values when chainId is overriden', async () => { + const { result } = renderHookWithProvider(() => useBalance('0x4268'), { + state: initialState, + }); + + expect(result.current.balanceETH).toBe('22345678.90988'); + expect(result.current.balanceWei.toString()).toBe( + '22345678909876543210000000', + ); + expect(result.current.balanceFiat).toBe('$71506172511.60'); // Fiat balance + expect(result.current.balanceFiatNumber).toBe(71506172511.6); // Fiat number balance + expect(result.current.stakedBalanceWei).toBe('5791332670714232000'); + expect(result.current.formattedStakedBalanceETH).toBe('5.79133 ETH'); // Formatted ETH balance + expect(result.current.stakedBalanceFiatNumber).toBe(18532.26454); // Staked balance in fiat number + expect(result.current.formattedStakedBalanceFiat).toBe('$18532.26'); // + }); }); diff --git a/app/components/UI/Stake/hooks/useBalance.ts b/app/components/UI/Stake/hooks/useBalance.ts index b6c83f4f7c2..9cd42b9638e 100644 --- a/app/components/UI/Stake/hooks/useBalance.ts +++ b/app/components/UI/Stake/hooks/useBalance.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; import { selectSelectedInternalAccountFormattedAddress } from '../../../../selectors/accountsController'; import { selectAccountsByChainId } from '../../../../selectors/accountTrackerController'; import { @@ -14,21 +15,22 @@ import { weiToFiatNumber, } from '../../../../util/number'; -const useBalance = () => { +const useBalance = (chainId?: Hex) => { const accountsByChainId = useSelector(selectAccountsByChainId); - const chainId = useSelector(selectChainId); + const selectedChainId = useSelector(selectChainId); const selectedAddress = useSelector( selectSelectedInternalAccountFormattedAddress, ); const currentCurrency = useSelector(selectCurrentCurrency); const currencyRates = useSelector(selectCurrencyRates); + const balanceChainId = chainId || selectedChainId; const conversionRate = currencyRates?.ETH?.conversionRate ?? 1; const rawAccountBalance = selectedAddress - ? accountsByChainId[chainId]?.[selectedAddress]?.balance + ? accountsByChainId[balanceChainId]?.[selectedAddress]?.balance : '0'; const stakedBalance = selectedAddress - ? accountsByChainId[chainId]?.[selectedAddress]?.stakedBalance || '0' + ? accountsByChainId[balanceChainId]?.[selectedAddress]?.stakedBalance || '0' : '0'; const balanceETH = useMemo(