From 2b89130411f1519f26d5628a322f84a07097cb75 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 8 Oct 2024 16:17:51 +0100 Subject: [PATCH] wip --- .../info/hooks/use-selected-token.test.ts | 67 ---------- .../confirm/info/hooks/use-selected-token.ts | 25 ---- .../info/hooks/use-token-values.test.ts | 120 ++++++++++++++++++ .../confirm/info/hooks/use-token-values.ts | 66 ++++++---- .../info/shared/send-heading/send-heading.tsx | 59 +++++---- ui/selectors/selectors.js | 19 +++ 6 files changed, 219 insertions(+), 137 deletions(-) delete mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.test.ts delete mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.ts create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.test.ts deleted file mode 100644 index 1f9b412c3583..000000000000 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TransactionMeta } from '@metamask/transaction-controller'; -import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; -import mockState from '../../../../../../../test/data/mock-state.json'; -import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; -import { useSelectedToken } from './use-selected-token'; - -describe('useSelectedToken', () => { - it('returns undefined for empty state', () => { - const transactionMeta = genUnapprovedTokenTransferConfirmation( - {}, - ) as TransactionMeta; - - const { result } = renderHookWithProvider( - () => useSelectedToken(transactionMeta), - mockState, - ); - - expect(result.current).toEqual({ selectedToken: undefined }); - }); - - it('returns token with address of transactionMeta.txParams.to', () => { - const transactionMeta = genUnapprovedTokenTransferConfirmation( - {}, - ) as TransactionMeta; - - const TEST_TOKEN = { - id: 'f5168b92-2d1d-442d-a0dc-bc02af987796', - address: '0x076146c765189d51be3160a2140cf80bfc73ad68', - options: {}, - methods: [ - 'personal_sign', - 'eth_sign', - 'eth_signTransaction', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - ], - type: 'eip155:eoa', - metadata: { - name: 'Account 1', - importTime: 1727786707545, - lastSelected: 1727786707545, - keyring: { - type: 'HD Key Tree', - }, - }, - balance: '0x42e29677a88e18', - }; - - const { result } = renderHookWithProvider( - () => useSelectedToken(transactionMeta), - { - ...mockState, - metamask: { - ...mockState.metamask, - allTokens: { - '0x5': { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [TEST_TOKEN], - }, - }, - }, - }, - ); - - expect(result.current).toEqual({ selectedToken: TEST_TOKEN }); - }); -}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.ts deleted file mode 100644 index 74ebd5dcc622..000000000000 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-selected-token.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TransactionMeta } from '@metamask/transaction-controller'; -import { useSelector } from 'react-redux'; -import { toChecksumHexAddress } from '../../../../../../../shared/modules/hexstring-utils'; -import { - getAllTokens, - getCurrentChainId, - getSelectedAccount, -} from '../../../../../../selectors'; - -export const useSelectedToken = (transactionMeta: TransactionMeta) => { - const selectedAccount = useSelector(getSelectedAccount); - - const detectedTokens = useSelector(getAllTokens); - const chainId = useSelector(getCurrentChainId); - - const selectedToken = detectedTokens?.[chainId]?.[ - selectedAccount.address - ].find( - (token: { address: string; decimals: number; symbol: string }) => - toChecksumHexAddress(token.address) === - toChecksumHexAddress(transactionMeta.txParams.to as string), - ); - - return { selectedToken }; -}; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts new file mode 100644 index 000000000000..d3bcf559e5aa --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts @@ -0,0 +1,120 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; +// import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; +import { useTokenValues } from './use-token-values'; + +jest.mock( + '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate', + () => jest.fn(), +); + +jest.mock('../../../../../../hooks/useTokenTracker', () => ({ + ...jest.requireActual('../../../../../../hooks/useTokenTracker'), + useTokenTracker: jest.fn(), +})); + +describe('useTokenValues', () => { + const useTokenExchangeRateMock = jest.mocked(useTokenExchangeRate); + const useTokenTrackerMock = jest.mocked(useTokenTracker); + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + iconUrl: 'iconUrl', + image: 'image', + }; + + it('returns native and fiat balances', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [ + { + address: '0x076146c765189d51be3160a2140cf80bfc73ad68', + balance: '1000000000000000000', + decimals: 18, + }, + ], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( + new Numeric(1, 10), + ); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: '$1.00', + tokenBalance: '1', + }); + }); + + it('returns undefined native and fiat balances if no token with balances is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( + new Numeric(1, 10), + ); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: undefined, + tokenBalance: undefined, + }); + }); + + it('returns undefined fiat balance if no token rate is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [ + { + address: '0x076146c765189d51be3160a2140cf80bfc73ad68', + balance: '1000000000000000000', + decimals: 18, + }, + ], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: undefined, + tokenBalance: '1', + }); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts index 21455ff0533e..6d7c16436e20 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -1,5 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { calcTokenAmount } from '../../../../../../../shared/lib/transactions-controller-utils'; import { toChecksumHexAddress } from '../../../../../../../shared/modules/hexstring-utils'; import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; @@ -11,44 +11,66 @@ export const useTokenValues = ( transactionMeta: TransactionMeta, selectedToken: SelectedToken, ) => { - const { - tokensWithBalances, - }: { - tokensWithBalances: { - balance: string; - address: string; - decimals: number; - string: string; - }[]; - } = useTokenTracker({ tokens: [selectedToken], address: undefined }); + const [tokensWithBalances, setTokensWithBalances] = useState< + { balance: string; address: string; decimals: number; string: string }[] + >([]); + + const fetchTokenBalances = async () => { + const result: { + tokensWithBalances: { + balance: string; + address: string; + decimals: number; + string: string; + }[]; + } = await useTokenTracker({ + tokens: [selectedToken], + address: undefined, + }); + + setTokensWithBalances(result.tokensWithBalances); + }; + + fetchTokenBalances(); + + const [exchangeRate, setExchangeRate] = useState(); + const fetchExchangeRate = async () => { + const result = await useTokenExchangeRate(transactionMeta?.txParams?.to); + + setExchangeRate(result); + }; + + fetchExchangeRate(); const tokenBalance = useMemo(() => { const tokenWithBalance = tokensWithBalances.find( - (token) => + (token: { + balance: string; + address: string; + decimals: number; + string: string; + }) => toChecksumHexAddress(token.address) === toChecksumHexAddress(transactionMeta?.txParams?.to as string), ); if (!tokenWithBalance) { - return ''; + return undefined; } return calcTokenAmount(tokenWithBalance.balance, tokenWithBalance.decimals); }, [tokensWithBalances]); - const exchangeRate = useTokenExchangeRate(transactionMeta?.txParams?.to); - - const fiatValue = useMemo(() => { - if (exchangeRate && tokenBalance !== '') { - return exchangeRate.times(tokenBalance).toNumber(); - } - return undefined; - }, [exchangeRate, tokenBalance]); + const fiatValue = + exchangeRate && tokenBalance && exchangeRate.times(tokenBalance).toNumber(); const fiatFormatter = useFiatFormatter(); const fiatDisplayValue = fiatValue && fiatFormatter(fiatValue, { shorten: true }); - return { fiatDisplayValue, tokenBalance }; + return { + fiatDisplayValue, + tokenBalance: tokenBalance && String(tokenBalance.toNumber()), + }; }; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx index 32a538ba3f3d..6ae3b9dc975a 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -1,5 +1,6 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; +import { useSelector } from 'react-redux'; import { AvatarToken, AvatarTokenSize, @@ -16,8 +17,8 @@ import { TextVariant, } from '../../../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; +import { getWatchedToken } from '../../../../../../../selectors'; import { useConfirmContext } from '../../../../../context/confirm'; -import { useSelectedToken } from '../../hooks/use-selected-token'; import { useTokenImage } from '../../hooks/use-token-image'; import { useTokenValues } from '../../hooks/use-token-values'; @@ -25,34 +26,33 @@ const SendHeading = () => { const t = useI18nContext(); const { currentConfirmation: transactionMeta } = useConfirmContext(); - const { selectedToken } = useSelectedToken(transactionMeta); + const selectedToken = useSelector((state: any) => + getWatchedToken(transactionMeta)(state), + ); const { tokenImage } = useTokenImage(transactionMeta, selectedToken); const { tokenBalance, fiatDisplayValue } = useTokenValues( transactionMeta, selectedToken, ); - return ( - - + const TokenImage = ( + + ); + + const TokenValue = ( + <> { {fiatDisplayValue} )} + + ); + + return ( + + {TokenImage} + {TokenValue} ); }; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 17e6ffc4500a..2b392107d6b2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -123,6 +123,7 @@ import { } from './permissions'; import { createDeepEqualSelector } from './util'; import { getMultichainBalances, getMultichainNetwork } from './multichain'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; /** * Returns true if the currently selected network is inaccessible or whether no @@ -537,6 +538,24 @@ export const getSelectedAccount = createDeepEqualSelector( }, ); +export const getWatchedToken = (transactionMeta) => + createSelector( + [getSelectedAccount, getAllTokens], + (selectedAccount, detectedTokens) => { + const chainId = transactionMeta.chainId; + + const selectedToken = detectedTokens?.[chainId]?.[ + selectedAccount.address + ]?.find( + (token) => + toChecksumHexAddress(token.address) === + toChecksumHexAddress(transactionMeta.txParams.to), + ); + + return selectedToken; + }, + ); + export function getTargetAccount(state, targetAddress) { const accounts = getMetaMaskAccounts(state); return accounts[targetAddress];