diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx index 8b7203262f1..9256b3a43d4 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx @@ -146,7 +146,7 @@ const TokenDetails: React.FC = ({ asset }) => { return ( - {asset.isETH && } + {asset.isETH && } {(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/StakeButton/index.tsx b/app/components/UI/Stake/components/StakeButton/index.tsx index 9b13d2fe4bf..9e18af410f0 100644 --- a/app/components/UI/Stake/components/StakeButton/index.tsx +++ b/app/components/UI/Stake/components/StakeButton/index.tsx @@ -25,6 +25,8 @@ import { RootState } from '../../../../../reducers'; import useStakingEligibility from '../../hooks/useStakingEligibility'; import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; import { EVENT_LOCATIONS } from '../../constants/events'; +import useStakingChain from '../../hooks/useStakingChain'; +import Engine from '../../../../../core/Engine'; interface StakeButtonProps { asset: TokenI; @@ -37,11 +39,14 @@ const StakeButtonContent = ({ asset }: StakeButtonProps) => { const browserTabs = useSelector((state: RootState) => state.browser.tabs); const chainId = useSelector(selectChainId); - - const { refreshPooledStakingEligibility } = useStakingEligibility(); + const { isEligible } = useStakingEligibility(); + const { isStakingSupportedChain } = useStakingChain(); const onStakeButtonPress = async () => { - const { isEligible } = await refreshPooledStakingEligibility(); + if (!isStakingSupportedChain) { + const { NetworkController } = Engine.context; + await NetworkController.setActiveNetwork('mainnet'); + } if (isEligible) { navigation.navigate('StakeScreens', { screen: Routes.STAKING.STAKE }); } else { 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 4dadbb0672c..b5e307b970b 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, { useEffect, useMemo, useState } 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'; @@ -64,7 +65,9 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { isEligible: isEligibleForPooledStaking } = useStakingEligibility(); - const { isStakingSupportedChain } = useStakingChain(); + const { isStakingSupportedChain } = useStakingChainByChainId( + asset.chainId as Hex, + ); const { trackEvent, createEventBuilder } = useMetrics(); @@ -81,7 +84,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { formattedStakedBalanceETH: stakedBalanceETH, formattedStakedBalanceFiat: stakedBalanceFiat, - } = useBalance(); + } = useBalance(asset.chainId as Hex); const { unstakingRequests, claimableRequests } = useMemo(() => { const exitRequests = pooledStakesData?.exitRequests ?? []; @@ -129,6 +132,9 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { } const renderStakingContent = () => { + if (chainId !== asset.chainId) { + return <>; + } if (isLoadingPooledStakesData) { return ( @@ -197,7 +203,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { }; return ( - + {hasEthToUnstake && !isLoadingPooledStakesData && ( { 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 12d5815c0fb..e93f64940ba 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`] = ` - + + ({ 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 c16fe9bfee4..e7970ffc63d 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, @@ -15,15 +16,20 @@ import ButtonIcon, { } from '../../../../../component-library/components/Buttons/ButtonIcon'; import useTooltipModal from '../../../../../components/hooks/useTooltipModal'; import { strings } from '../../../../../../locales/i18n'; -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(); @@ -38,14 +44,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 (!isStakingSupportedChain || !hasStakedPositions) return <>; return ( @@ -175,9 +183,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( diff --git a/app/components/UI/Tokens/index.test.tsx b/app/components/UI/Tokens/index.test.tsx index e8a0df5103e..838143c7b21 100644 --- a/app/components/UI/Tokens/index.test.tsx +++ b/app/components/UI/Tokens/index.test.tsx @@ -248,10 +248,14 @@ jest.mock('../../UI/Stake/hooks/useStakingEligibility', () => ({ })), })); -jest.mock('../Stake/hooks/useStakingChain', () => ({ - useStakingChainByChainId: () => ({ +jest.mock('../../UI/Stake/hooks/useStakingChain', () => ({ + __esModule: true, + default: jest.fn(() => ({ isStakingSupportedChain: true, - }), + })), + useStakingChainByChainId: jest.fn(() => ({ + isStakingSupportedChain: true, + })), })); const Stack = createStackNavigator(); diff --git a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.styles.ts b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.styles.ts index f48bc62161d..d06bd8b4410 100644 --- a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.styles.ts +++ b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.styles.ts @@ -29,9 +29,12 @@ const styleSheet = (params: { theme: Theme }) => { }, messageContainer: { backgroundColor: theme.colors.background.default, - padding: 16, borderRadius: 8, minHeight: 200, + maxHeight: 300, + }, + scrollableSection: { + padding: 16, }, messageExpanded: { color: theme.colors.text.default, diff --git a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx index 23542c6992e..dcde1d6a851 100644 --- a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx +++ b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from 'react'; -import { Text, View } from 'react-native'; +import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { strings } from '../../../../../../../locales/i18n'; import { useStyles } from '../../../../../../component-library/hooks'; @@ -42,7 +42,11 @@ const SignatureMessageSection = ({ - {messageExpanded} + + + {messageExpanded} + + } expandedContentTitle={strings('confirm.message')} diff --git a/app/util/smart-transactions/smart-publish-hook.test.ts b/app/util/smart-transactions/smart-publish-hook.test.ts index 3953ad17ee5..a247bece910 100644 --- a/app/util/smart-transactions/smart-publish-hook.test.ts +++ b/app/util/smart-transactions/smart-publish-hook.test.ts @@ -11,7 +11,10 @@ import { WalletDevice, } from '@metamask/transaction-controller'; import SmartTransactionsController from '@metamask/smart-transactions-controller'; -import { type SmartTransaction, ClientId } from '@metamask/smart-transactions-controller/dist/types'; +import { + type SmartTransaction, + ClientId, +} from '@metamask/smart-transactions-controller/dist/types'; import { AllowedActions, @@ -113,11 +116,15 @@ type WithRequestOptions = { type WithRequestCallback = ({ request, controllerMessenger, + getFeesSpy, + submitSignedTransactionsSpy, + smartTransactionsController, }: { request: SubmitSmartTransactionRequestMocked; controllerMessenger: SubmitSmartTransactionRequestMocked['controllerMessenger']; getFeesSpy: jest.SpyInstance; submitSignedTransactionsSpy: jest.SpyInstance; + smartTransactionsController: SmartTransactionsController; }) => ReturnValue; type WithRequestArgs = @@ -194,6 +201,7 @@ function withRequest( expectedDeadline: 45, maxDeadline: 150, mobileReturnTxHashAsap: false, + batchStatusPollingInterval: 1000, }, mobile_active: true, extension_active: true, @@ -208,6 +216,7 @@ function withRequest( request, getFeesSpy, submitSignedTransactionsSpy, + smartTransactionsController, }); } @@ -392,7 +401,7 @@ describe('submitSmartTransactionHook', () => { const result = await submitSmartTransactionHook(request); expect(result).toEqual({ transactionHash }); - const { txParams, chainId } = request.transactionMeta; + const { txParams, chainId } = request.transactionMeta; expect( request.transactionController.approveTransactionsWithSameNonce, @@ -657,4 +666,33 @@ describe('submitSmartTransactionHook', () => { ); }); }); + it('sets the status refresh interval if provided in feature flags', async () => { + withRequest(async ({ request, smartTransactionsController }) => { + const setStatusRefreshIntervalSpy = jest.spyOn( + smartTransactionsController, + 'setStatusRefreshInterval', + ); + + request.featureFlags.smartTransactions.batchStatusPollingInterval = 2000; + + await submitSmartTransactionHook(request); + + expect(setStatusRefreshIntervalSpy).toHaveBeenCalledWith(2000); + }); + }); + + it('does not set the status refresh interval if not provided in feature flags', async () => { + withRequest(async ({ request, smartTransactionsController }) => { + const setStatusRefreshIntervalSpy = jest.spyOn( + smartTransactionsController, + 'setStatusRefreshInterval', + ); + + request.featureFlags.smartTransactions.batchStatusPollingInterval = 0; + + await submitSmartTransactionHook(request); + + expect(setStatusRefreshIntervalSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/app/util/smart-transactions/smart-publish-hook.ts b/app/util/smart-transactions/smart-publish-hook.ts index 16ecfa2bdf9..df837618412 100644 --- a/app/util/smart-transactions/smart-publish-hook.ts +++ b/app/util/smart-transactions/smart-publish-hook.ts @@ -53,6 +53,7 @@ export interface SubmitSmartTransactionRequest { expectedDeadline: number; maxDeadline: number; mobileReturnTxHashAsap: boolean; + batchStatusPollingInterval: number; } | Record; }; @@ -75,6 +76,7 @@ class SmartTransactionHook { expectedDeadline?: number; maxDeadline?: number; mobileReturnTxHashAsap?: boolean; + batchStatusPollingInterval?: number; }; }; #shouldUseSmartTransaction: boolean; @@ -186,6 +188,11 @@ class SmartTransactionHook { return useRegularTransactionSubmit; } + const batchStatusPollingInterval = this.#featureFlags?.smartTransactions?.batchStatusPollingInterval; + if (batchStatusPollingInterval) { + this.#smartTransactionsController.setStatusRefreshInterval(batchStatusPollingInterval); + } + const submitTransactionResponse = await this.#signAndSubmitTransactions({ getFeesResponse, });