diff --git a/app/components/UI/Transactions/TransactionsFooter.tsx b/app/components/UI/Transactions/TransactionsFooter.tsx index e2fd4bb9713..18caad4182c 100644 --- a/app/components/UI/Transactions/TransactionsFooter.tsx +++ b/app/components/UI/Transactions/TransactionsFooter.tsx @@ -93,7 +93,7 @@ const TransactionsFooter = ({ return null; } - if (isMainnetByChainId(chainId) || providerType !== RPC) { + if (isMainnetByChainId(chainId) || (providerType && providerType !== RPC)) { return strings('transactions.view_full_history_on_etherscan'); } diff --git a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.test.tsx b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.test.tsx index 141f185ea5e..0391156d894 100644 --- a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.test.tsx +++ b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.test.tsx @@ -98,6 +98,7 @@ jest.mock('../../../selectors/networkController', () => ({ selectNetworkConfigurations: jest.fn(), selectProviderType: jest.fn(), selectRpcUrl: jest.fn(), + selectProviderConfig: jest.fn(), })); jest.mock('../../../selectors/networkEnablementController', () => ({ selectEVMEnabledNetworks: jest.fn(), @@ -145,6 +146,16 @@ jest.mock('../../UI/Bridge/hooks/useBridgeHistoryItemBySrcTxHash', () => ({ }), })); +const mockGetBlockExplorerUrl = jest.fn(() => undefined); +const mockGetBlockExplorerName = jest.fn(() => 'Explorer'); +jest.mock('../../hooks/useBlockExplorer', () => ({ + __esModule: true, + default: () => ({ + getBlockExplorerUrl: mockGetBlockExplorerUrl, + getBlockExplorerName: mockGetBlockExplorerName, + }), +})); + const mockTransactionsFooter = jest.fn((props: unknown) => { const ReactActual = jest.requireActual('react'); const { Text } = jest.requireActual('react-native'); @@ -183,6 +194,7 @@ jest.mock('../../../core/Multichain/utils', () => ({ __esModule: true, getAddressUrl: (address: string, chainId: string) => mockGetAddressUrl(address, chainId), + isNonEvmChainId: jest.fn((chainId: string) => chainId.includes(':')), })); // Mock refresh util @@ -292,6 +304,7 @@ const { selectNetworkConfigurations, selectProviderType, selectRpcUrl, + selectProviderConfig, } = jest.requireMock('../../../selectors/networkController'); const { selectEVMEnabledNetworks, selectNonEVMEnabledNetworks } = jest.requireMock('../../../selectors/networkEnablementController'); @@ -312,6 +325,10 @@ describe('UnifiedTransactionsView', () => { mockTransactionsFooter.mockClear(); mockMultichainTransactionsFooter.mockClear(); mockGetAddressUrl.mockClear(); + mockGetBlockExplorerUrl.mockClear(); + mockGetBlockExplorerUrl.mockReturnValue(undefined); + mockGetBlockExplorerName.mockClear(); + mockGetBlockExplorerName.mockReturnValue('Explorer'); mockGetAddressUrl.mockImplementation( (address?: string) => `https://solscan.io/account/${address}`, ); @@ -350,6 +367,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectNetworkConfigurations) return {}; if (selector === selectProviderType) return 'rpc'; if (selector === selectRpcUrl) return 'https://rpc.example'; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return ['0x1']; if (selector === selectNonEVMEnabledNetworks) return ['solana:mainnet']; if (selector === selectCurrentCurrency) return 'USD'; @@ -410,8 +429,10 @@ describe('UnifiedTransactionsView', () => { it('pull-to-refresh calls updateIncomingTransactions', async () => { const { UNSAFE_getAllByType } = render(); + const [rc] = UNSAFE_getAllByType(RefreshControl); - rc.props.onRefresh(); + await rc.props.onRefresh(); + expect(updateIncomingTransactions).toHaveBeenCalled(); }); @@ -583,6 +604,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectNetworkConfigurations) return {}; if (selector === selectProviderType) return 'rpc'; if (selector === selectRpcUrl) return 'https://rpc.example'; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return ['0x1', '0x5']; if (selector === selectNonEVMEnabledNetworks) return []; if (selector === selectCurrentCurrency) return 'USD'; @@ -599,12 +622,9 @@ describe('UnifiedTransactionsView', () => { expect(footerProps.rpcBlockExplorer).toBeUndefined(); footerProps.onViewBlockExplorer?.(); - expect(networksMock.getBlockExplorerAddressUrl).toHaveBeenCalledWith( - 'rpc', - '0xabc', - undefined, - ); - expect(networksMock.getBlockExplorerAddressUrl).toHaveBeenCalledTimes(1); + // When configBlockExplorerUrl is undefined (multiple chains case), + // the component uses blockExplorerUrl directly without calling getBlockExplorerAddressUrl + expect(networksMock.getBlockExplorerAddressUrl).not.toHaveBeenCalled(); isRemoveGlobalNetworkSelectorEnabled.mockReturnValue(false); }); }); @@ -635,6 +655,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectNetworkConfigurations) return {}; if (selector === selectProviderType) return 'rpc'; if (selector === selectRpcUrl) return 'https://rpc.example'; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return []; if (selector === selectNonEVMEnabledNetworks) return ['solana:mainnet']; if (selector === selectCurrentCurrency) return 'USD'; @@ -682,6 +704,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectNetworkConfigurations) return {}; if (selector === selectProviderType) return 'rpc'; if (selector === selectRpcUrl) return 'https://rpc.example'; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return []; if (selector === selectNonEVMEnabledNetworks) return ['bip122:000000000019d6689c085ae165831e93']; @@ -720,6 +744,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectTokens) return []; if (selector === selectChainId) return '0x1'; if (selector === selectIsPopularNetwork) return false; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return []; if (selector === selectNonEVMEnabledNetworks) return []; if (selector === selectCurrentCurrency) return 'USD'; @@ -754,6 +780,8 @@ describe('UnifiedTransactionsView', () => { if (selector === selectTokens) return []; if (selector === selectChainId) return '0x1'; if (selector === selectIsPopularNetwork) return false; + if (selector === selectProviderConfig) + return { type: 'rpc', rpcUrl: 'https://rpc.example' }; if (selector === selectEVMEnabledNetworks) return []; if (selector === selectNonEVMEnabledNetworks) return []; if (selector === selectCurrentCurrency) return 'USD'; diff --git a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx index 5a75b545e3f..3713ae78c0a 100644 --- a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx +++ b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx @@ -61,6 +61,7 @@ import { getAddressUrl } from '../../../core/Multichain/utils'; import UpdateEIP1559Tx from '../confirmations/legacy/components/UpdateEIP1559Tx'; import styleSheet from './UnifiedTransactionsView.styles'; import { useUnifiedTxActions } from './useUnifiedTxActions'; +import useBlockExplorer from '../../hooks/useBlockExplorer'; type SmartTransactionWithId = SmartTransaction & { id: string }; type EvmTransaction = TransactionMeta | SmartTransactionWithId; @@ -357,7 +358,12 @@ const UnifiedTransactionsView = ({ currentEvmChainId, ]); - const blockExplorerUrl = useMemo(() => { + const hasEvmChainsEnabled = enabledEVMChainIds.length > 0; + const popularListBlockExplorer = useBlockExplorer( + hasEvmChainsEnabled ? enabledEVMChainIds[0] : undefined, + ); + + const configBlockExplorerUrl = useMemo(() => { // When using the per-dapp/multiselect network selector, only return a block // explorer if exactly one EVM chain is selected. Otherwise, undefined. if (isRemoveGlobalNetworkSelectorEnabled()) { @@ -377,7 +383,24 @@ const UnifiedTransactionsView = ({ return config.blockExplorerUrls?.[index]; }, [enabledEVMChainIds, evmNetworkConfigurationsByChainId]); - const hasEvmChainsEnabled = enabledEVMChainIds.length > 0; + const blockExplorerUrl = useMemo(() => { + // configBlockExplorerUrl contains block explorer urls only for networks added by default after fresh install + // other networks should use PopularList, which is used by useBlockExplorer hook + if (configBlockExplorerUrl) { + return configBlockExplorerUrl; + } + return hasEvmChainsEnabled + ? popularListBlockExplorer.getBlockExplorerUrl( + selectedAccountGroupEvmAddress, + ) || undefined + : undefined; + }, [ + configBlockExplorerUrl, + popularListBlockExplorer, + selectedAccountGroupEvmAddress, + hasEvmChainsEnabled, + ]); + const hasNonEvmChainsEnabled = enabledNonEVMChainIds.length > 0; const showEvmFooter = hasEvmChainsEnabled && !hasNonEvmChainsEnabled; @@ -388,14 +411,25 @@ const UnifiedTransactionsView = ({ return; } - const { url, title } = getBlockExplorerAddressUrl( - providerType, - selectedAccountGroupEvmAddress, - blockExplorerUrl, - ); + let url; + let title; + if (configBlockExplorerUrl) { + const result = getBlockExplorerAddressUrl( + providerType, + selectedAccountGroupEvmAddress, + blockExplorerUrl, + ); + url = result.url; + title = result.title; - if (!url) { - return; + if (!url) { + return; + } + } else { + url = blockExplorerUrl; + title = hasEvmChainsEnabled + ? popularListBlockExplorer.getBlockExplorerName(enabledEVMChainIds[0]) + : undefined; } navigation.navigate('Webview', { @@ -410,6 +444,10 @@ const UnifiedTransactionsView = ({ providerType, blockExplorerUrl, selectedAccountGroupEvmAddress, + popularListBlockExplorer, + enabledEVMChainIds, + configBlockExplorerUrl, + hasEvmChainsEnabled, ]); const allNonEvmChainsAreSolana = useMemo( @@ -461,7 +499,7 @@ const UnifiedTransactionsView = ({ return ( @@ -494,6 +532,7 @@ const UnifiedTransactionsView = ({ showEvmFooter, showNonEvmExplorerLink, showNonEvmFooter, + configBlockExplorerUrl, ]); const [refreshing, setRefreshing] = useState(false); diff --git a/app/components/Views/confirmations/hooks/send/useNameValidation.test.ts b/app/components/Views/confirmations/hooks/send/useNameValidation.test.ts index ac8b27e606c..e7a3c69cc7b 100644 --- a/app/components/Views/confirmations/hooks/send/useNameValidation.test.ts +++ b/app/components/Views/confirmations/hooks/send/useNameValidation.test.ts @@ -7,16 +7,41 @@ import * as SnapNameResolution from '../../../../Snaps/hooks/useSnapNameResoluti import * as SendValidationUtils from '../../utils/send-address-validations'; import { evmSendStateMock } from '../../__mocks__/send.mock'; import { useNameValidation } from './useNameValidation'; +import { useSendType } from './useSendType'; +import { useSendFlowEnsResolutions } from './useSendFlowEnsResolutions'; jest.mock('@metamask/bridge-controller', () => ({ formatChainIdToCaip: jest.fn(), })); +jest.mock('./useSendType', () => ({ + useSendType: jest.fn(), +})); + +jest.mock('./useSendFlowEnsResolutions', () => ({ + useSendFlowEnsResolutions: jest.fn(() => ({ + setResolvedAddress: jest.fn(), + })), +})); + const mockState = { state: evmSendStateMock, }; describe('useNameValidation', () => { + const mockUseSendType = jest.mocked(useSendType); + const mockUseSendFlowEnsResolutions = jest.mocked(useSendFlowEnsResolutions); + const mockSetResolvedAddress = jest.fn(); + + beforeEach(() => { + mockUseSendType.mockReturnValue({ + isEvmSendType: false, + } as ReturnType); + mockUseSendFlowEnsResolutions.mockReturnValue({ + setResolvedAddress: mockSetResolvedAddress, + } as unknown as ReturnType); + }); + it('return function to validate name', () => { const { result } = renderHookWithProvider( () => useNameValidation(), @@ -44,6 +69,36 @@ describe('useNameValidation', () => { ).toStrictEqual({ resolvedAddress: 'dummy_address', }); + expect(mockSetResolvedAddress).not.toHaveBeenCalled(); + }); + + it('calls setResolvedAddress when name is resolved and isEvmSendType is true', async () => { + mockUseSendType.mockReturnValue({ + isEvmSendType: true, + } as ReturnType); + jest.spyOn(SnapNameResolution, 'useSnapNameResolution').mockReturnValue({ + fetchResolutions: () => + Promise.resolve([ + { resolvedAddress: 'dummy_address' } as unknown as AddressResolution, + ]), + }); + const { result } = renderHookWithProvider( + () => useNameValidation(), + mockState, + ); + expect( + await result.current.validateName( + '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + 'test.eth', + ), + ).toStrictEqual({ + resolvedAddress: 'dummy_address', + }); + expect(mockSetResolvedAddress).toHaveBeenCalledWith( + '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + 'test.eth', + 'dummy_address', + ); }); it('return confusable error and warning as name is resolved', async () => { diff --git a/app/components/Views/confirmations/hooks/send/useNameValidation.ts b/app/components/Views/confirmations/hooks/send/useNameValidation.ts index 2585de5c384..f4ae7a25bc7 100644 --- a/app/components/Views/confirmations/hooks/send/useNameValidation.ts +++ b/app/components/Views/confirmations/hooks/send/useNameValidation.ts @@ -5,9 +5,13 @@ import { strings } from '../../../../../../locales/i18n'; import { isENS } from '../../../../../util/address'; import { useSnapNameResolution } from '../../../../Snaps/hooks/useSnapNameResolution'; import { getConfusableCharacterInfo } from '../../utils/send-address-validations'; +import { useSendFlowEnsResolutions } from './useSendFlowEnsResolutions'; +import { useSendType } from './useSendType'; export const useNameValidation = () => { const { fetchResolutions } = useSnapNameResolution(); + const { setResolvedAddress } = useSendFlowEnsResolutions(); + const { isEvmSendType } = useSendType(); const validateName = useCallback( async (chainId: string, to: string) => { @@ -24,6 +28,11 @@ export const useNameValidation = () => { } const resolvedAddress = resolutions[0]?.resolvedAddress; + if (resolvedAddress && isEvmSendType) { + // Set short living cache of ENS resolution for the given chain and address for confirmation screen + setResolvedAddress(chainId, to, resolvedAddress); + } + return { resolvedAddress, ...getConfusableCharacterInfo(to), @@ -34,7 +43,7 @@ export const useNameValidation = () => { error: strings('send.could_not_resolve_name'), }; }, - [fetchResolutions], + [fetchResolutions, isEvmSendType, setResolvedAddress], ); return { diff --git a/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.test.ts b/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.test.ts new file mode 100644 index 00000000000..0f6305d5d79 --- /dev/null +++ b/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.test.ts @@ -0,0 +1,93 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSendFlowEnsResolutions } from './useSendFlowEnsResolutions'; + +describe('useSendFlowEnsResolutions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('stores and retrieves ENS name for an address', () => { + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId = '0x1'; + const ensName = 'vitalik.eth'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + + result.current.setResolvedAddress(chainId, ensName, address); + const retrieved = result.current.getResolvedENSName(chainId, address); + + expect(retrieved).toBe(ensName); + }); + + it('returns undefined for non-existent address', () => { + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId = '0x1'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96046'; + + const retrieved = result.current.getResolvedENSName(chainId, address); + + expect(retrieved).toBeUndefined(); + }); + + it('differentiates between different chain IDs', () => { + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId1 = '0x1'; + const chainId2 = '0x89'; + const ensName = 'vitalik.eth'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + + result.current.setResolvedAddress(chainId1, ensName, address); + const retrieved1 = result.current.getResolvedENSName(chainId1, address); + const retrieved2 = result.current.getResolvedENSName(chainId2, address); + + expect(retrieved1).toBe(ensName); + expect(retrieved2).toBeUndefined(); + }); + + it('returns undefined for expired cache entries', () => { + jest.useFakeTimers(); + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId = '0x1'; + const ensName = 'vitalik.eth'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + + result.current.setResolvedAddress(chainId, ensName, address); + + jest.advanceTimersByTime(5 * 60 * 1000 + 1); + + const retrieved = result.current.getResolvedENSName(chainId, address); + + expect(retrieved).toBeUndefined(); + jest.useRealTimers(); + }); + + it('returns cached entry before expiration', () => { + jest.useFakeTimers(); + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId = '0x1'; + const ensName = 'vitalik.eth'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + + result.current.setResolvedAddress(chainId, ensName, address); + + jest.advanceTimersByTime(5 * 60 * 1000 - 1000); + + const retrieved = result.current.getResolvedENSName(chainId, address); + + expect(retrieved).toBe(ensName); + jest.useRealTimers(); + }); + + it('overwrites existing cache entry for same chain and address', () => { + const { result } = renderHook(() => useSendFlowEnsResolutions()); + const chainId = '0x1'; + const ensName1 = 'vitalik.eth'; + const ensName2 = 'newname.eth'; + const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + + result.current.setResolvedAddress(chainId, ensName1, address); + result.current.setResolvedAddress(chainId, ensName2, address); + const retrieved = result.current.getResolvedENSName(chainId, address); + + expect(retrieved).toBe(ensName2); + }); +}); diff --git a/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.ts b/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.ts new file mode 100644 index 00000000000..05ea8fd19df --- /dev/null +++ b/app/components/Views/confirmations/hooks/send/useSendFlowEnsResolutions.ts @@ -0,0 +1,80 @@ +import { useCallback } from 'react'; + +interface CacheEntry { + ensName: string; + timestamp: number; +} + +const CACHE_TTL = 5 * 60 * 1000; + +const ensResolutionCache = new Map(); + +const createCacheKey = (chainId: string, address: string) => + `${chainId}:${address}`; + +const isCacheEntryValid = (entry: CacheEntry): boolean => + Date.now() - entry.timestamp < CACHE_TTL; + +// This hook is used to store and retrieve ENS resolutions for a given chain and address with short living cache +// especially to keep consistency of Name component in send flow +export const useSendFlowEnsResolutions = () => { + const setResolvedAddress = useCallback( + (chainId: string, ensName: string, address: string) => { + if ( + !isValidString(chainId) || + !isValidString(address) || + !isValidString(ensName) + ) { + return; + } + + const lowerCaseChainId = chainId.toLowerCase(); + const lowerCaseAddress = address.toLowerCase(); + const lowerCaseEnsName = ensName.toLowerCase(); + + ensResolutionCache.set( + createCacheKey(lowerCaseChainId, lowerCaseAddress), + { + ensName: lowerCaseEnsName, + timestamp: Date.now(), + }, + ); + }, + [], + ); + + const getResolvedENSName = useCallback( + (chainId: string, address: string): string | undefined => { + if (!isValidString(chainId) || !isValidString(address)) { + return undefined; + } + + const lowerCaseChainId = chainId.toLowerCase(); + const lowerCaseAddress = address.toLowerCase(); + + const entry = ensResolutionCache.get( + createCacheKey(lowerCaseChainId, lowerCaseAddress), + ); + if (entry && isCacheEntryValid(entry)) { + return entry.ensName; + } + + if (entry) { + ensResolutionCache.delete( + createCacheKey(lowerCaseChainId, lowerCaseAddress), + ); + } + return undefined; + }, + [], + ); + + return { + setResolvedAddress, + getResolvedENSName, + }; +}; + +function isValidString(value: unknown) { + return typeof value === 'string' && value.length > 0; +} diff --git a/app/components/hooks/DisplayName/useDisplayName.test.ts b/app/components/hooks/DisplayName/useDisplayName.test.ts index 96356b2888f..811ff74d674 100644 --- a/app/components/hooks/DisplayName/useDisplayName.test.ts +++ b/app/components/hooks/DisplayName/useDisplayName.test.ts @@ -8,6 +8,7 @@ import { useWatchedNFTNames } from './useWatchedNFTNames'; import { useNftNames } from './useNftName'; import { useAccountNames } from './useAccountNames'; import { useAccountWalletNames } from './useAccountWalletNames'; +import { useSendFlowEnsResolutions } from '../../Views/confirmations/hooks/send/useSendFlowEnsResolutions'; const UNKNOWN_ADDRESS_CHECKSUMMED = '0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e'; @@ -43,6 +44,15 @@ jest.mock('./useAccountWalletNames', () => ({ useAccountWalletNames: jest.fn(), })); +jest.mock( + '../../Views/confirmations/hooks/send/useSendFlowEnsResolutions', + () => ({ + useSendFlowEnsResolutions: jest.fn(() => ({ + getResolvedENSName: jest.fn(), + })), + }), +); + describe('useDisplayName', () => { const mockUseWatchedNFTNames = jest.mocked(useWatchedNFTNames); const mockUseFirstPartyContractNames = jest.mocked( @@ -52,6 +62,8 @@ describe('useDisplayName', () => { const mockUseNFTNames = jest.mocked(useNftNames); const mockUseAccountNames = jest.mocked(useAccountNames); const mockUseAccountWalletNames = jest.mocked(useAccountWalletNames); + const mockUseSendFlowEnsResolutions = jest.mocked(useSendFlowEnsResolutions); + const mockGetResolvedENSName = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -61,6 +73,9 @@ describe('useDisplayName', () => { mockUseNFTNames.mockReturnValue([]); mockUseAccountNames.mockReturnValue([]); mockUseAccountWalletNames.mockReturnValue([]); + mockUseSendFlowEnsResolutions.mockReturnValue({ + getResolvedENSName: mockGetResolvedENSName, + } as unknown as ReturnType); }); describe('unknown address', () => { @@ -189,5 +204,23 @@ describe('useDisplayName', () => { }), ); }); + + it('returns ENS name', () => { + mockUseSendFlowEnsResolutions.mockReturnValue({ + getResolvedENSName: jest.fn().mockReturnValue('ensname.eth'), + } as unknown as ReturnType); + + const displayName = useDisplayName({ + type: NameType.EthereumAddress, + value: KNOWN_NFT_ADDRESS_CHECKSUMMED, + variation: CHAIN_IDS.MAINNET, + }); + + expect(displayName).toEqual( + expect.objectContaining({ + name: 'ensname.eth', + }), + ); + }); }); }); diff --git a/app/components/hooks/DisplayName/useDisplayName.ts b/app/components/hooks/DisplayName/useDisplayName.ts index 8b55a09d126..61ea33dc828 100644 --- a/app/components/hooks/DisplayName/useDisplayName.ts +++ b/app/components/hooks/DisplayName/useDisplayName.ts @@ -5,6 +5,7 @@ import { useERC20Tokens } from './useERC20Tokens'; import { useNftNames } from './useNftName'; import { useAccountNames } from './useAccountNames'; import { useAccountWalletNames } from './useAccountWalletNames'; +import { useSendFlowEnsResolutions } from '../../Views/confirmations/hooks/send/useSendFlowEnsResolutions'; export interface UseDisplayNameRequest { preferContractSymbol?: boolean; @@ -99,8 +100,9 @@ export function useDisplayNames( const nftNames = useNftNames(requests); const accountNames = useAccountNames(requests); const accountWalletNames = useAccountWalletNames(requests); + const { getResolvedENSName } = useSendFlowEnsResolutions(); - return requests.map((_request, index) => { + return requests.map(({ value, variation }, index) => { const watchedNftName = watchedNftNames[index]; const firstPartyContractName = firstPartyContractNames[index]; const erc20Token = erc20Tokens[index]; @@ -108,9 +110,11 @@ export function useDisplayNames( nftNames[index] || {}; const accountName = accountNames[index]; const subtitle = accountWalletNames[index]; + const ensName = getResolvedENSName(variation, value); const name = accountName || + ensName || firstPartyContractName || watchedNftName || erc20Token?.name || diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx index 15eb6068caa..4f4eef579bb 100644 --- a/app/util/networks/customNetworks.tsx +++ b/app/util/networks/customNetworks.tsx @@ -106,7 +106,7 @@ export const PopularList = [ rpcUrl: `https://palm-mainnet.infura.io/v3/${infuraProjectId}`, ticker: 'PALM', rpcPrefs: { - blockExplorerUrl: 'https://explorer.palm.io', + blockExplorerUrl: 'https://palm.chainlens.com', imageUrl: 'PALM', imageSource: require('../../images/palm.png'), },