Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add useFirstPartyContractName hook #9407

Merged
58 changes: 49 additions & 9 deletions app/components/UI/Name/Name.test.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,73 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { default as Name } from './Name';
import { render } from '@testing-library/react-native';
import { NameType } from './Name.types';
import useDisplayName, {
DisplayNameVariant,
} from '../../hooks/DisplayName/useDisplayName';

jest.mock('../../hooks/DisplayName/useDisplayName', () => ({
__esModule: true,
...jest.requireActual('../../hooks/DisplayName/useDisplayName'),
default: jest.fn(),
}));

const UNKNOWN_ADDRESS_CHECKSUMMED =
'0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e';

const UNKNOWN_ADDRESS_NOT_CHECKSUMMED =
UNKNOWN_ADDRESS_CHECKSUMMED.toLowerCase();
const KNOWN_ADDRESS_CHECKSUMMED = '0x495f947276749Ce646f68AC8c248420045cb7b5e';
const KNOWN_NAME_MOCK = 'Known name';

describe('Name', () => {
const mockStore = configureMockStore();
const initialState = {
settings: { useBlockieIcon: true },
};
const store = mockStore(initialState);

const mockUseDisplayName = (
useDisplayName as jest.MockedFunction<typeof useDisplayName>
).mockReturnValue({
variant: DisplayNameVariant.Unknown,
});

describe('unknown address', () => {
it('displays checksummed address', () => {
const wrapper = render(
<Name
type={NameType.EthereumAddress}
value={UNKNOWN_ADDRESS_NOT_CHECKSUMMED}
/>,
<Provider store={store}>
<Name
type={NameType.EthereumAddress}
value={UNKNOWN_ADDRESS_NOT_CHECKSUMMED}
/>
</Provider>,
);

expect(wrapper.getByText(UNKNOWN_ADDRESS_CHECKSUMMED)).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
});

describe('recognized address', () => {
it('should return name', () => {
mockUseDisplayName.mockReturnValue({
variant: DisplayNameVariant.Recognized,
name: KNOWN_NAME_MOCK,
});

it('should render snapshot correctly', () => {
const wrapper = render(
<Name
type={NameType.EthereumAddress}
value={UNKNOWN_ADDRESS_NOT_CHECKSUMMED}
/>,
<Provider store={store}>
<Name
type={NameType.EthereumAddress}
value={KNOWN_ADDRESS_CHECKSUMMED}
/>
</Provider>,
);

expect(wrapper.getByText(KNOWN_NAME_MOCK)).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
});
Expand Down
97 changes: 96 additions & 1 deletion app/components/UI/Name/__snapshots__/Name.test.tsx.snap

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions app/components/hooks/DisplayName/useDisplayName.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { NETWORKS_CHAIN_ID } from '../../../constants/network';
import { NameType } from '../../UI/Name/Name.types';
import useDisplayName, { DisplayNameVariant } from './useDisplayName';
import { useFirstPartyContractName } from './useFirstPartyContractName';

const UNKNOWN_ADDRESS_CHECKSUMMED =
'0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e';
const KNOWN_NFT_ADDRESS_CHECKSUMMED =
'0x495f947276749Ce646f68AC8c248420045cb7b5e';
const KNOWN_FIRST_PARTY_CONTRACT_NAME = 'MetaMask Pool Staking';

jest.mock('./useFirstPartyContractName', () => ({
useFirstPartyContractName: jest.fn(),
}));

describe('useDisplayName', () => {
const mockUseFirstPartyContractName =
useFirstPartyContractName as jest.MockedFunction<
typeof useFirstPartyContractName
>;

beforeEach(() => {
jest.resetAllMocks();
});

describe('unknown address', () => {
it('should not return a name', () => {
const displayName = useDisplayName(
Expand All @@ -16,4 +34,28 @@ describe('useDisplayName', () => {
});
});
});

describe('recognized address', () => {
it('should return first party contract name', () => {
mockUseFirstPartyContractName.mockReturnValue(
KNOWN_FIRST_PARTY_CONTRACT_NAME,
);

const displayName = useDisplayName(
NameType.EthereumAddress,
KNOWN_NFT_ADDRESS_CHECKSUMMED,
NETWORKS_CHAIN_ID.MAINNET,
);

expect(mockUseFirstPartyContractName).toHaveBeenCalledWith(
KNOWN_NFT_ADDRESS_CHECKSUMMED.toLowerCase(),
NETWORKS_CHAIN_ID.MAINNET,
);

expect(displayName).toEqual({
variant: DisplayNameVariant.Recognized,
name: KNOWN_FIRST_PARTY_CONTRACT_NAME,
});
});
});
});
28 changes: 25 additions & 3 deletions app/components/hooks/DisplayName/useDisplayName.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Hex } from '@metamask/utils';
import { NameType } from '../../UI/Name/Name.types';
import { useFirstPartyContractName } from './useFirstPartyContractName';

/**
* Indicate the source and nature of a display name for a given address.
Expand Down Expand Up @@ -43,8 +45,28 @@ export type DisplayName =
* @param type The NameType to get the display name for.
* @param value The value to get the display name for.
*/
const useDisplayName: (type: NameType, value: string) => DisplayName = () => ({
variant: DisplayNameVariant.Unknown,
});
const useDisplayName: (
type: NameType,
value: string,
chainId?: Hex,
) => DisplayName = (_type, value, chainId) => {
const normalizedValue = value.toLowerCase();

const firstPartyContractName = useFirstPartyContractName(
normalizedValue,
chainId,
);

if (firstPartyContractName) {
return {
variant: DisplayNameVariant.Recognized,
name: firstPartyContractName,
};
}

return {
variant: DisplayNameVariant.Unknown,
};
};

export default useDisplayName;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { selectChainId } from '../../../selectors/networkController';
import { NETWORKS_CHAIN_ID } from '../../../constants/network';
import { useFirstPartyContractName } from './useFirstPartyContractName';

jest.mock('react-redux', () => ({
useSelector: (selector: any) => selector(),
}));

jest.mock('../../../selectors/networkController', () => ({
selectChainId: jest.fn(),
}));

const BRIDGE_NAME_MOCK = 'MetaMask Bridge';
const BRIDGE_MAINNET_ADDRESS_MOCK =
'0x0439e60F02a8900a951603950d8D4527f400C3f1';
const UNKNOWN_ADDRESS_MOCK = '0xabc123';

describe('useFirstPartyContractName', () => {
const selectChainIdMock = jest.mocked(selectChainId);
beforeEach(() => {
jest.resetAllMocks();
selectChainIdMock.mockReturnValue(NETWORKS_CHAIN_ID.MAINNET);
});

it('returns null if no name found', () => {
const name = useFirstPartyContractName(
UNKNOWN_ADDRESS_MOCK,
NETWORKS_CHAIN_ID.MAINNET,
);

expect(name).toBe(null);
});

it('returns name if found', () => {
const name = useFirstPartyContractName(
BRIDGE_MAINNET_ADDRESS_MOCK,
NETWORKS_CHAIN_ID.MAINNET,
);
expect(name).toBe(BRIDGE_NAME_MOCK);
});

it('normalizes addresses to lowercase', () => {
const name = useFirstPartyContractName(
BRIDGE_MAINNET_ADDRESS_MOCK.toUpperCase(),
NETWORKS_CHAIN_ID.MAINNET,
);

expect(name).toBe(BRIDGE_NAME_MOCK);
});
});
35 changes: 35 additions & 0 deletions app/components/hooks/DisplayName/useFirstPartyContractName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useSelector } from 'react-redux';
import { type Hex } from '@metamask/utils';
import { selectChainId } from '../../../selectors/networkController';
import FIRST_PARTY_CONTRACT_NAMES from '../../../constants/first-party-contracts';

export interface UseFirstPartyContractNameRequest {
chainId?: Hex;
value: string;
}

export function useFirstPartyContractNames(
requests: UseFirstPartyContractNameRequest[],
): (string | null)[] {
const currentChainId = useSelector(selectChainId);

return requests.map((request) => {
const chainId = request.chainId ?? currentChainId;
const normalizedValue = request.value.toLowerCase();

return (
Object.keys(FIRST_PARTY_CONTRACT_NAMES).find(
(name) =>
FIRST_PARTY_CONTRACT_NAMES[name]?.[chainId]?.toLowerCase() ===
normalizedValue,
) ?? null
);
});
}

export function useFirstPartyContractName(
value: string,
chainId?: Hex,
): string | null {
return useFirstPartyContractNames([{ value, chainId }])[0];
}
46 changes: 46 additions & 0 deletions app/constants/first-party-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Hex } from '@metamask/utils';
import { NETWORKS_CHAIN_ID } from './network';

/**
* A map of first-party contract names to their addresses on various chains.
*/
const FIRST_PARTY_CONTRACT_NAMES: Record<string, Record<Hex, Hex>> = {
'MetaMask Validator Staking': {
[NETWORKS_CHAIN_ID.MAINNET]: '0xDc71aFFC862fceB6aD32BE58E098423A7727bEbd',
},
'MetaMask Pool Staking': {
[NETWORKS_CHAIN_ID.MAINNET]: '0x1f6692E78dDE07FF8da75769B6d7c716215bC7D0',
},
'MetaMask Pool Staking (v1)': {
[NETWORKS_CHAIN_ID.MAINNET]: '0xc7bE520a13dC023A1b34C03F4Abdab8A43653F7B',
},
'MetaMask Bridge': {
[NETWORKS_CHAIN_ID.MAINNET]: '0x0439e60F02a8900a951603950d8D4527f400C3f1',
[NETWORKS_CHAIN_ID.OPTIMISM]: '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e',
[NETWORKS_CHAIN_ID.BSC]: '0xaEc23140408534b378bf5832defc426dF8604B59',
[NETWORKS_CHAIN_ID.POLYGON]: '0x3A0b42cE6166abB05d30DdF12E726c95a83D7a16',
[NETWORKS_CHAIN_ID.AVAXCCHAIN]:
'0x29106d08382d3c73bF477A94333C61Db1142E1B6',
[NETWORKS_CHAIN_ID.LINEA_MAINNET]:
'0xE3d0d2607182Af5B24f5C3C2E4990A053aDd64e3',
[NETWORKS_CHAIN_ID.ZKSYNC_ERA]:
'0x357B5935482AD8a4A2e181e0132aBd1882E16520',
[NETWORKS_CHAIN_ID.BASE]: '0xa20ECbC821fB54064aa7B5C6aC81173b8b34Df71',
[NETWORKS_CHAIN_ID.ARBITRUM]: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC',
},
'MetaMask Swaps': {
[NETWORKS_CHAIN_ID.MAINNET]: '0x881D40237659C251811CEC9c364ef91dC08D300C',
[NETWORKS_CHAIN_ID.BSC]: '0x1a1ec25DC08e98e5E93F1104B5e5cdD298707d31',
[NETWORKS_CHAIN_ID.POLYGON]: '0x1a1ec25DC08e98e5E93F1104B5e5cdD298707d31',
[NETWORKS_CHAIN_ID.AVAXCCHAIN]:
'0x1a1ec25DC08e98e5E93F1104B5e5cdD298707d31',
[NETWORKS_CHAIN_ID.OPTIMISM]: '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6',
[NETWORKS_CHAIN_ID.LINEA_MAINNET]:
'0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6',
[NETWORKS_CHAIN_ID.ARBITRUM]: '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6',
[NETWORKS_CHAIN_ID.ZKSYNC_ERA]:
'0xf504c1fe13d14DF615E66dcd0ABF39e60c697f34',
},
};

export default FIRST_PARTY_CONTRACT_NAMES;
Loading