Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/UI/Transactions/TransactionsFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -292,6 +304,7 @@ const {
selectNetworkConfigurations,
selectProviderType,
selectRpcUrl,
selectProviderConfig,
} = jest.requireMock('../../../selectors/networkController');
const { selectEVMEnabledNetworks, selectNonEVMEnabledNetworks } =
jest.requireMock('../../../selectors/networkEnablementController');
Expand All @@ -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}`,
);
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -410,8 +429,10 @@ describe('UnifiedTransactionsView', () => {

it('pull-to-refresh calls updateIncomingTransactions', async () => {
const { UNSAFE_getAllByType } = render(<UnifiedTransactionsView />);

const [rc] = UNSAFE_getAllByType(RefreshControl);
rc.props.onRefresh();
await rc.props.onRefresh();

expect(updateIncomingTransactions).toHaveBeenCalled();
});

Expand Down Expand Up @@ -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';
Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand All @@ -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;
Expand All @@ -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', {
Expand All @@ -410,6 +444,10 @@ const UnifiedTransactionsView = ({
providerType,
blockExplorerUrl,
selectedAccountGroupEvmAddress,
popularListBlockExplorer,
enabledEVMChainIds,
configBlockExplorerUrl,
hasEvmChainsEnabled,
]);

const allNonEvmChainsAreSolana = useMemo(
Expand Down Expand Up @@ -461,7 +499,7 @@ const UnifiedTransactionsView = ({
return (
<TransactionsFooter
chainId={enabledEVMChainIds[0]}
providerType={providerType}
providerType={configBlockExplorerUrl ? providerType : undefined}
rpcBlockExplorer={blockExplorerUrl}
onViewBlockExplorer={onViewBlockExplorer}
/>
Expand Down Expand Up @@ -494,6 +532,7 @@ const UnifiedTransactionsView = ({
showEvmFooter,
showNonEvmExplorerLink,
showNonEvmFooter,
configBlockExplorerUrl,
]);

const [refreshing, setRefreshing] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof useSendType>);
mockUseSendFlowEnsResolutions.mockReturnValue({
setResolvedAddress: mockSetResolvedAddress,
} as unknown as ReturnType<typeof useSendFlowEnsResolutions>);
});

it('return function to validate name', () => {
const { result } = renderHookWithProvider(
() => useNameValidation(),
Expand Down Expand Up @@ -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<typeof useSendType>);
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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),
Expand All @@ -34,7 +43,7 @@ export const useNameValidation = () => {
error: strings('send.could_not_resolve_name'),
};
},
[fetchResolutions],
[fetchResolutions, isEvmSendType, setResolvedAddress],
);

return {
Expand Down
Loading
Loading