Skip to content

Commit

Permalink
Primarily use tokenURI before calling API
Browse files Browse the repository at this point in the history
  • Loading branch information
OGPoyraz committed Jul 10, 2024
1 parent cae7441 commit 96e71b5
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 67 deletions.
2 changes: 2 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3223,6 +3223,8 @@ export default class MetamaskController extends EventEmitter {
),

getNFTTokenInfo: nftController.getNFTTokenInfo.bind(nftController),
getNftInformationFromTokenURI:
nftController.getNftInformationFromTokenURI.bind(nftController),

isNftOwner: nftController.isNftOwner.bind(nftController),

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
"@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A19.0.0#~/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch",
"@metamask/gas-fee-controller@npm:^15.1.1": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch",
"@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch",
"@metamask/assets-controllers@^34.0.0": "npm:@metamask-previews/assets-controllers@34.0.0-preview-3ee9d02a"
"@metamask/assets-controllers@^34.0.0": "npm:@metamask-previews/assets-controllers@34.0.0-preview-f776a395"
},
"dependencies": {
"@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch",
Expand Down
181 changes: 142 additions & 39 deletions ui/hooks/useNftCollectionsMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { renderHook } from '@testing-library/react-hooks';
import { TokensResponse } from '@metamask/assets-controllers';
import { TokenStandard } from '../../shared/constants/transaction';
import { hexToDecimal } from '../../shared/modules/conversion.utils';
import { getCurrentChainId } from '../selectors';
import { getNFTTokenInfo } from '../store/actions';
import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
import {
getNFTTokenInfo,
getNftInformationFromTokenURI,
} from '../store/actions';
import {
useNftCollectionsMetadata,
UseNftCollectionsMetadataRequest,
} from './useNftCollectionsMetadata';

jest.mock('react-redux', () => ({
// TODO: Replace `any` with type
Expand All @@ -16,18 +23,22 @@ jest.mock('../selectors', () => ({
}));

jest.mock('../store/actions', () => ({
getNftInformationFromTokenURI: jest.fn(),
getNFTTokenInfo: jest.fn(),
}));

const CHAIN_ID_MOCK = '0x1';
const ERC_721_ADDRESS_1 = '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb';
const ERC_721_TOKEN_ID_1 = '0x11';
const ERC_721_DECIMAL_TOKEN_ID_1 = hexToDecimal(ERC_721_TOKEN_ID_1);
const ERC_721_NAME_1 = 'Erc 721 1';
const ERC_721_IMG_1 = 'url 1';
const ERC_721_COLLECTION_1_MOCK = {
id: ERC_721_ADDRESS_1,
name: 'Erc 721 1',
name: ERC_721_NAME_1,
slug: 'erc-721-1',
symbol: 'ERC721-1',
imageUrl: 'url',
imageUrl: ERC_721_IMG_1,
};
const ERC_721_TOKEN_MOCK_1 = {
token: {
Expand All @@ -36,15 +47,22 @@ const ERC_721_TOKEN_MOCK_1 = {
tokenId: ERC_721_TOKEN_ID_1,
},
};
const ERC_721_TOKEN_URI_RESPONSE_1_MOCK = {
name: ERC_721_NAME_1,
image: ERC_721_IMG_1,
};

const ERC_721_ADDRESS_2 = '0x06012c8cf97bead5deae237070f9587f8e7a266d';
const ERC_721_TOKEN_ID_2 = '0x12';
const ERC_721_DECIMAL_TOKEN_ID_2 = hexToDecimal(ERC_721_TOKEN_ID_2);
const ERC_721_NAME_2 = 'Erc 721 2';
const ERC_721_IMG_2 = 'url 2';
const ERC_721_COLLECTION_2_MOCK = {
id: ERC_721_ADDRESS_2,
name: 'Erc 721 2',
name: ERC_721_NAME_2,
slug: 'erc-721-2',
symbol: 'ERC721-2',
imageUrl: 'url',
imageUrl: ERC_721_IMG_2,
};
const ERC_721_TOKEN_MOCK_2 = {
token: {
Expand All @@ -53,51 +71,139 @@ const ERC_721_TOKEN_MOCK_2 = {
tokenId: ERC_721_TOKEN_ID_2,
},
};
const ERC_721_TOKEN_URI_RESPONSE_2_MOCK = {
name: ERC_721_NAME_2,
image: ERC_721_IMG_2,
};

const NFT_COLLECTIONS_METADATA_REQUEST = [
{
value: ERC_721_ADDRESS_1,
tokenId: ERC_721_TOKEN_ID_1,
standard: TokenStandard.ERC721,
},
{
value: ERC_721_ADDRESS_2,
tokenId: ERC_721_TOKEN_ID_2,
standard: TokenStandard.ERC721,
},
] as UseNftCollectionsMetadataRequest[];

describe('useNftCollectionsMetadata', () => {
const mockGetCurrentChainId = jest.mocked(getCurrentChainId);
const mockGetNFTTokenInfo = jest.mocked(getNFTTokenInfo);
const mockGetNftInformationFromTokenURI = jest.mocked(
getNftInformationFromTokenURI,
);

beforeEach(() => {
jest.resetAllMocks();
mockGetCurrentChainId.mockReturnValue(CHAIN_ID_MOCK);
mockGetNftInformationFromTokenURI.mockImplementation((address) => {
if (address === ERC_721_ADDRESS_1) {
return Promise.resolve(ERC_721_TOKEN_URI_RESPONSE_1_MOCK);
}
if (address === ERC_721_ADDRESS_2) {
return Promise.resolve(ERC_721_TOKEN_URI_RESPONSE_2_MOCK);
}
return Promise.resolve({});
});
mockGetNFTTokenInfo.mockResolvedValue([
ERC_721_TOKEN_MOCK_1,
ERC_721_TOKEN_MOCK_2,
] as TokensResponse[]);
});

it('calls NFT tokens API and returns the correct data structure', async () => {
it('collects data from tokenURI', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useNftCollectionsMetadata([
{
value: ERC_721_ADDRESS_1,
tokenId: ERC_721_TOKEN_ID_1,
standard: TokenStandard.ERC721,
},
{
value: ERC_721_ADDRESS_2,
tokenId: ERC_721_TOKEN_ID_2,
standard: TokenStandard.ERC721,
},
]),
useNftCollectionsMetadata(NFT_COLLECTIONS_METADATA_REQUEST),
);

await waitForNextUpdate();

expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual({
[`${ERC_721_ADDRESS_1.toLowerCase()}:${ERC_721_TOKEN_ID_1}`]:
ERC_721_COLLECTION_1_MOCK,
[`${ERC_721_ADDRESS_2.toLowerCase()}:${ERC_721_TOKEN_ID_2}`]:
ERC_721_COLLECTION_2_MOCK,
expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(0);

expect(mockGetNftInformationFromTokenURI).toHaveBeenCalledTimes(
NFT_COLLECTIONS_METADATA_REQUEST.length,
);
expect(
result.current[`${ERC_721_ADDRESS_1}:${ERC_721_DECIMAL_TOKEN_ID_1}`],
).toStrictEqual(expect.objectContaining(ERC_721_TOKEN_URI_RESPONSE_1_MOCK));

expect(
result.current[`${ERC_721_ADDRESS_2}:${ERC_721_DECIMAL_TOKEN_ID_2}`],
).toStrictEqual(expect.objectContaining(ERC_721_TOKEN_URI_RESPONSE_2_MOCK));
});

describe('fallbacks into getNFTTokenInfo', () => {
it('if any tokenURI result missing name', async () => {
mockGetNftInformationFromTokenURI.mockImplementation((address) => {
if (address === ERC_721_ADDRESS_1) {
return Promise.resolve(ERC_721_TOKEN_URI_RESPONSE_1_MOCK);
}
if (address === ERC_721_ADDRESS_2) {
return Promise.resolve({});
}
return Promise.resolve({});
});

const { result, waitForNextUpdate } = renderHook(() =>
useNftCollectionsMetadata(NFT_COLLECTIONS_METADATA_REQUEST),
);

await waitForNextUpdate();

expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(1);
expect(mockGetNftInformationFromTokenURI).toHaveBeenCalledTimes(
NFT_COLLECTIONS_METADATA_REQUEST.length,
);

expect(
result.current[`${ERC_721_ADDRESS_1}:${ERC_721_DECIMAL_TOKEN_ID_1}`],
).toStrictEqual(expect.objectContaining(ERC_721_COLLECTION_1_MOCK));

expect(
result.current[`${ERC_721_ADDRESS_2}:${ERC_721_DECIMAL_TOKEN_ID_2}`],
).toStrictEqual(expect.objectContaining(ERC_721_COLLECTION_2_MOCK));
});

it('if tokenURI response rejected', async () => {
mockGetNftInformationFromTokenURI.mockImplementation((address) => {
if (address === ERC_721_ADDRESS_1) {
return Promise.resolve(ERC_721_TOKEN_URI_RESPONSE_1_MOCK);
}
if (address === ERC_721_ADDRESS_2) {
return Promise.reject(new Error('TokenURI is missing'));
}
return Promise.resolve({});
});

const { result, waitForNextUpdate } = renderHook(() =>
useNftCollectionsMetadata(NFT_COLLECTIONS_METADATA_REQUEST),
);

await waitForNextUpdate();

expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(1);
expect(mockGetNftInformationFromTokenURI).toHaveBeenCalledTimes(
NFT_COLLECTIONS_METADATA_REQUEST.length,
);

expect(
result.current[`${ERC_721_ADDRESS_1}:${ERC_721_DECIMAL_TOKEN_ID_1}`],
).toStrictEqual(expect.objectContaining(ERC_721_COLLECTION_1_MOCK));

expect(
result.current[`${ERC_721_ADDRESS_2}:${ERC_721_DECIMAL_TOKEN_ID_2}`],
).toStrictEqual(expect.objectContaining(ERC_721_COLLECTION_2_MOCK));
});
});

describe('does not call NFT tokens API', () => {
describe('does not call getNFTTokenInfo or getNftInformationFromTokenURI', () => {
it('if there are no contracts to fetch', async () => {
renderHook(() => useNftCollectionsMetadata([]));
expect(mockGetNFTTokenInfo).not.toHaveBeenCalled();
expect(mockGetNftInformationFromTokenURI).not.toHaveBeenCalled();
});

it('if there are no valid nft request', async () => {
Expand All @@ -107,31 +213,28 @@ describe('useNftCollectionsMetadata', () => {
value: '0xERC20Address',
standard: TokenStandard.ERC20,
},
{
value: '',
standard: TokenStandard.ERC721,
},
]),
);
expect(mockGetNFTTokenInfo).not.toHaveBeenCalled();
expect(mockGetNftInformationFromTokenURI).not.toHaveBeenCalled();
});
});

it('does memoise result for same requests', async () => {
const { waitForNextUpdate, rerender } = renderHook(() =>
useNftCollectionsMetadata([
{
value: ERC_721_ADDRESS_1,
tokenId: ERC_721_TOKEN_ID_1,
standard: TokenStandard.ERC721,
},
{
value: ERC_721_ADDRESS_2,
tokenId: ERC_721_TOKEN_ID_2,
standard: TokenStandard.ERC721,
},
]),
useNftCollectionsMetadata(NFT_COLLECTIONS_METADATA_REQUEST),
);

await waitForNextUpdate();
rerender();

expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(1);
expect(mockGetNFTTokenInfo).toHaveBeenCalledTimes(0);
expect(mockGetNftInformationFromTokenURI).toHaveBeenCalledTimes(
NFT_COLLECTIONS_METADATA_REQUEST.length,
);
});
});
Loading

0 comments on commit 96e71b5

Please sign in to comment.