Skip to content

Commit e922468

Browse files
authored
fix: avoid multiple calls to refresh nft metadata (#4325)
## Explanation This PR updates the logic of `updateNftMetadata` function and adds a mutex to synchronize state updates. Inside `getNftInformation` function, before returning the image, we prioritize checking for api result then fallback to checking if there was an image in the blockchain result. The nft detection will be enabled by default in the future, this will avoid making unnecessary state updates when the image string returned from NFT-API is different than the string returned from `blockchainMetadata`. ## References * Related to MetaMask/metamask-mobile#9759 ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/assets-controllers` - **Added**: Added Mutex lock in the `updateNftMetadata` function. ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate
1 parent 9927f20 commit e922468

File tree

2 files changed

+152
-43
lines changed

2 files changed

+152
-43
lines changed

packages/assets-controllers/src/NftController.test.ts

+104-1
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,7 @@ describe('NftController', () => {
13801380
nftController.state.allNfts[selectedAddress][ChainId.mainnet][0],
13811381
).toStrictEqual({
13821382
address: ERC721_KUDOSADDRESS,
1383-
image: 'Kudos Image (directly from tokenURI)',
1383+
image: 'url',
13841384
name: 'Kudos Name (directly from tokenURI)',
13851385
description: 'Kudos Description (directly from tokenURI)',
13861386
tokenId: ERC721_KUDOS_TOKEN_ID,
@@ -3917,5 +3917,108 @@ describe('NftController', () => {
39173917

39183918
expect(spy).toHaveBeenCalledTimes(1);
39193919
});
3920+
3921+
it('should call getNftInformation only one time per interval', async () => {
3922+
const tokenURI = 'https://api.pudgypenguins.io/lil/4';
3923+
const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI);
3924+
const { nftController, triggerPreferencesStateChange } = setupController({
3925+
options: {
3926+
getERC721TokenURI: mockGetERC721TokenURI,
3927+
},
3928+
});
3929+
const selectedAddress = OWNER_ADDRESS;
3930+
const spy = jest.spyOn(nftController, 'updateNft');
3931+
const testNetworkClientId = 'sepolia';
3932+
await nftController.addNft('0xtest', '3', {
3933+
nftMetadata: { name: '', description: '', image: '', standard: '' },
3934+
networkClientId: testNetworkClientId,
3935+
});
3936+
3937+
nock('https://api.pudgypenguins.io/lil').get('/4').reply(200, {
3938+
name: 'name pudgy',
3939+
image: 'url pudgy',
3940+
description: 'description pudgy',
3941+
});
3942+
const testInputNfts: Nft[] = [
3943+
{
3944+
address: '0xtest',
3945+
description: null,
3946+
favorite: false,
3947+
image: null,
3948+
isCurrentlyOwned: true,
3949+
name: null,
3950+
standard: 'ERC721',
3951+
tokenId: '3',
3952+
tokenURI: 'https://api.pudgypenguins.io/lil/4',
3953+
},
3954+
];
3955+
3956+
// Make first call to updateNftMetadata should trigger state update
3957+
await nftController.updateNftMetadata({
3958+
nfts: testInputNfts,
3959+
networkClientId: testNetworkClientId,
3960+
});
3961+
expect(spy).toHaveBeenCalledTimes(1);
3962+
3963+
expect(
3964+
nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0],
3965+
).toStrictEqual({
3966+
address: '0xtest',
3967+
description: 'description pudgy',
3968+
image: 'url pudgy',
3969+
name: 'name pudgy',
3970+
tokenId: '3',
3971+
standard: 'ERC721',
3972+
favorite: false,
3973+
isCurrentlyOwned: true,
3974+
tokenURI: 'https://api.pudgypenguins.io/lil/4',
3975+
});
3976+
3977+
spy.mockClear();
3978+
3979+
// trigger calling updateNFTMetadata again on the same account should not trigger state update
3980+
const spy2 = jest.spyOn(nftController, 'updateNft');
3981+
await nftController.updateNftMetadata({
3982+
nfts: testInputNfts,
3983+
networkClientId: testNetworkClientId,
3984+
});
3985+
// No updates to state should be made
3986+
expect(spy2).toHaveBeenCalledTimes(0);
3987+
3988+
// trigger preference change and change selectedAccount
3989+
const testNewAccountAddress = 'OxDifferentAddress';
3990+
triggerPreferencesStateChange({
3991+
...getDefaultPreferencesState(),
3992+
selectedAddress: testNewAccountAddress,
3993+
});
3994+
3995+
spy.mockClear();
3996+
await nftController.addNft('0xtest', '4', {
3997+
nftMetadata: { name: '', description: '', image: '', standard: '' },
3998+
networkClientId: testNetworkClientId,
3999+
});
4000+
4001+
const testInputNfts2: Nft[] = [
4002+
{
4003+
address: '0xtest',
4004+
description: null,
4005+
favorite: false,
4006+
image: null,
4007+
isCurrentlyOwned: true,
4008+
name: null,
4009+
standard: 'ERC721',
4010+
tokenId: '4',
4011+
tokenURI: 'https://api.pudgypenguins.io/lil/4',
4012+
},
4013+
];
4014+
4015+
const spy3 = jest.spyOn(nftController, 'updateNft');
4016+
await nftController.updateNftMetadata({
4017+
nfts: testInputNfts2,
4018+
networkClientId: testNetworkClientId,
4019+
});
4020+
// When the account changed, and updateNftMetadata is called state update should be triggered
4021+
expect(spy3).toHaveBeenCalledTimes(1);
4022+
});
39204023
});
39214024
});

packages/assets-controllers/src/NftController.ts

+48-42
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ export class NftController extends BaseController<
729729
name: blockchainMetadata?.name ?? nftApiMetadata?.name ?? null,
730730
description:
731731
blockchainMetadata?.description ?? nftApiMetadata?.description ?? null,
732-
image: blockchainMetadata?.image ?? nftApiMetadata?.image ?? null,
732+
image: nftApiMetadata?.image ?? blockchainMetadata?.image ?? null,
733733
standard:
734734
blockchainMetadata?.standard ?? nftApiMetadata?.standard ?? null,
735735
tokenURI: blockchainMetadata?.tokenURI ?? null,
@@ -1450,56 +1450,62 @@ export class NftController extends BaseController<
14501450
userAddress?: string;
14511451
networkClientId?: NetworkClientId;
14521452
}) {
1453-
const chainId = this.#getCorrectChainId({ networkClientId });
1453+
const releaseLock = await this.#mutex.acquire();
14541454

1455-
const nftsWithChecksumAdr = nfts.map((nft) => {
1456-
return {
1457-
...nft,
1458-
address: toChecksumHexAddress(nft.address),
1459-
};
1460-
});
1461-
const nftMetadataResults = await Promise.all(
1462-
nftsWithChecksumAdr.map(async (nft) => {
1463-
const resMetadata = await this.#getNftInformation(
1464-
nft.address,
1465-
nft.tokenId,
1466-
networkClientId,
1467-
);
1455+
try {
1456+
const chainId = this.#getCorrectChainId({ networkClientId });
1457+
1458+
const nftsWithChecksumAdr = nfts.map((nft) => {
14681459
return {
1469-
nft,
1470-
newMetadata: resMetadata,
1460+
...nft,
1461+
address: toChecksumHexAddress(nft.address),
14711462
};
1472-
}),
1473-
);
1474-
1475-
// We want to avoid updating the state if the state and fetched nft info are the same
1476-
const nftsWithDifferentMetadata: NftUpdate[] = [];
1477-
const { allNfts } = this.state;
1478-
const stateNfts = allNfts[userAddress]?.[chainId] || [];
1479-
1480-
nftMetadataResults.forEach((singleNft) => {
1481-
const existingEntry: Nft | undefined = stateNfts.find(
1482-
(nft) =>
1483-
nft.address.toLowerCase() === singleNft.nft.address.toLowerCase() &&
1484-
nft.tokenId === singleNft.nft.tokenId,
1463+
});
1464+
const nftMetadataResults = await Promise.all(
1465+
nftsWithChecksumAdr.map(async (nft) => {
1466+
const resMetadata = await this.#getNftInformation(
1467+
nft.address,
1468+
nft.tokenId,
1469+
networkClientId,
1470+
);
1471+
return {
1472+
nft,
1473+
newMetadata: resMetadata,
1474+
};
1475+
}),
14851476
);
14861477

1487-
if (existingEntry) {
1488-
const differentMetadata = compareNftMetadata(
1489-
singleNft.newMetadata,
1490-
existingEntry,
1478+
// We want to avoid updating the state if the state and fetched nft info are the same
1479+
const nftsWithDifferentMetadata: NftUpdate[] = [];
1480+
const { allNfts } = this.state;
1481+
const stateNfts = allNfts[userAddress]?.[chainId] || [];
1482+
1483+
nftMetadataResults.forEach((singleNft) => {
1484+
const existingEntry: Nft | undefined = stateNfts.find(
1485+
(nft) =>
1486+
nft.address.toLowerCase() === singleNft.nft.address.toLowerCase() &&
1487+
nft.tokenId === singleNft.nft.tokenId,
14911488
);
14921489

1493-
if (differentMetadata) {
1494-
nftsWithDifferentMetadata.push(singleNft);
1490+
if (existingEntry) {
1491+
const differentMetadata = compareNftMetadata(
1492+
singleNft.newMetadata,
1493+
existingEntry,
1494+
);
1495+
1496+
if (differentMetadata) {
1497+
nftsWithDifferentMetadata.push(singleNft);
1498+
}
14951499
}
1496-
}
1497-
});
1500+
});
14981501

1499-
if (nftsWithDifferentMetadata.length !== 0) {
1500-
nftsWithDifferentMetadata.forEach((elm) =>
1501-
this.updateNft(elm.nft, elm.newMetadata, userAddress, chainId),
1502-
);
1502+
if (nftsWithDifferentMetadata.length !== 0) {
1503+
nftsWithDifferentMetadata.forEach((elm) =>
1504+
this.updateNft(elm.nft, elm.newMetadata, userAddress, chainId),
1505+
);
1506+
}
1507+
} finally {
1508+
releaseLock();
15031509
}
15041510
}
15051511

0 commit comments

Comments
 (0)