Skip to content

Commit

Permalink
feat: fetch nfts on components (#24547)
Browse files Browse the repository at this point in the history
## **Description**

This PR aims to:
1- Enable nft autodetection by default
2- Show a modal only once if the user disables the nft autodetection
3- Make NFT detection tied to the components that use the NFTs instead
of the 3mins polling strategy.

This PR goes with this core PR:
MetaMask/core#4281

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24547?quickstart=1)

## **Related issues**

Related to: MetaMask/core#4281

## **Manual testing steps**

**Test the nft auto detection modal:**

1. Do a fresh import of the wallet
2. Switch to mainnet.
3. Go to settings => Security and privacy and make sure the toggle
enable NFT auto detection is ON
4. Turn the nft auto detection toggle OFF
5. Go back to home page and you should see a new modal
6. Clicking on `not right now `button should close the modal and closing
on `allow` button should enable the nft auto detection modal.
This modal should be seen only once.

**Test the removal of the polling in the backgound:**

We have the toggle enabled by default now but this should not trigger
calls to NFT-API every 3 mins anymore.
Instead the calls should be triggered only when you click on the NFT
tab.

1. Open the background console and click on Networks tab.
2. Filter by /tokens (so you are able to see only the calls that will
fetch user nfts)
3. Notice that as long as you did not click on the NFT tab, you should
not be able to see any calls made in the backround.
(where the old logic should keep detecting your NFTs every 3 mins as
long as you have MM open)
5. Click on the NFT tab and you should be able to see the requests in
the background to fetch your nfts.
7. You can also click on Send, and click on asset picker and click on
NFT tab, you should be able to see your NFTs there too.
8. Calls should be made only when you click on the NFT tab.

**Test new notice banner behavior:**

Users should see the NFT Notice banner as long as they are on mainnet +
they have NFT detection OFF.
Regardless of whether they have NFTs in the state or not.
Clicking on the"Enable NFT autodetection" should remove the notice
banner and enable the NFT detection without redirecting the user to
settings.


## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->


https://github.com/MetaMask/metamask-extension/assets/10994169/0815ac0c-a60c-400a-9ffe-92451dfc3ded

### **After**

<!-- [screenshots/recordings] -->


https://github.com/MetaMask/metamask-extension/assets/10994169/0b00f1c1-baf1-4205-bca2-991d11e39a2f

Notice banner with toast:


https://github.com/MetaMask/metamask-extension/assets/10994169/c1aaa168-a29f-4b38-b0c7-74aaa1945ba0



## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
sahar-fehri committed Jun 19, 2024
1 parent 038f3d6 commit e54295d
Show file tree
Hide file tree
Showing 47 changed files with 1,075 additions and 177 deletions.
251 changes: 251 additions & 0 deletions .yarn/patches/@metamask-assets-controllers-patch-a3b39b55a6.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
diff --git a/dist/chunk-FMZML3V5.js b/dist/chunk-FMZML3V5.js
index ee6155cd938366918de155e8867c7d359b8ea826..b4dfe838c463a561b0e91532bcb674806fdc52bd 100644
--- a/dist/chunk-FMZML3V5.js
+++ b/dist/chunk-FMZML3V5.js
@@ -5,6 +5,7 @@


var _controllerutils = require('@metamask/controller-utils');
+var _utils = require('@metamask/utils');
var _pollingcontroller = require('@metamask/polling-controller');
var DEFAULT_INTERVAL = 18e4;
var BlockaidResultType = /* @__PURE__ */ ((BlockaidResultType2) => {
@@ -14,6 +15,8 @@ var BlockaidResultType = /* @__PURE__ */ ((BlockaidResultType2) => {
BlockaidResultType2["Malicious"] = "Malicious";
return BlockaidResultType2;
})(BlockaidResultType || {});
+const supportedNftDetectionNetworks= [_controllerutils.ChainId.mainnet];
+var inProcessNftFetchingUpdates;
var NftDetectionController = class extends _pollingcontroller.StaticIntervalPollingControllerV1 {
/**
* Creates an NftDetectionController instance.
@@ -50,6 +53,7 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
* Name of this controller used during composition
*/
this.name = "NftDetectionController";
+ this.inProcessNftFetchingUpdates= {};
/**
* Checks whether network is mainnet or not.
*
@@ -72,11 +76,6 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
const { selectedAddress: previouslySelectedAddress, disabled } = this.config;
if (selectedAddress !== previouslySelectedAddress || !useNftDetection !== disabled) {
this.configure({ selectedAddress, disabled: !useNftDetection });
- if (useNftDetection) {
- this.start();
- } else {
- this.stop();
- }
}
});
onNetworkStateChange(({ selectedNetworkClientId }) => {
@@ -92,34 +91,33 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
this.setIntervalLength(this.config.interval);
}
getOwnerNftApi({
+ chainId,
address,
next
}) {
- return `${_controllerutils.NFT_API_BASE_URL}/users/${address}/tokens?chainIds=1&limit=50&includeTopBid=true&continuation=${next ?? ""}`;
+ return `${_controllerutils.NFT_API_BASE_URL}/users/${address}/tokens?chainIds=${chainId}&limit=50&includeTopBid=true&continuation=${next ?? ""}`;
}
- async getOwnerNfts(address) {
- let nftApiResponse;
- let nfts = [];
- let next;
- do {
- nftApiResponse = await _controllerutils.fetchWithErrorHandling.call(void 0, {
- url: this.getOwnerNftApi({ address, next }),
- options: {
- headers: {
- Version: "1"
- }
+ async getOwnerNfts(
+ address,
+ chainId,
+ cursor,
+ ) {
+ // Convert hex chainId to number
+ const convertedChainId = (0, _controllerutils.convertHexToDecimal)(chainId).toString();
+ const url = this.getOwnerNftApi({
+ chainId: convertedChainId,
+ address,
+ next: cursor,
+ });
+
+ const nftApiResponse = await _controllerutils.handleFetch.call(void 0, url,
+ {
+ headers: {
+ Version: "1"
},
- timeout: 15e3
- });
- if (!nftApiResponse) {
- return nfts;
}
- const newNfts = nftApiResponse.tokens.filter(
- (elm) => elm.token.isSpam === false && (elm.blockaidResult?.result_type ? elm.blockaidResult?.result_type === "Benign" /* Benign */ : true)
- );
- nfts = [...nfts, ...newNfts];
- } while (next = nftApiResponse.continuation);
- return nfts;
+ );
+ return nftApiResponse;
}
async _executePoll(networkClientId, options) {
await this.detectNfts({ networkClientId, userAddress: options.address });
@@ -169,62 +167,103 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
networkClientId,
userAddress
} = { userAddress: this.config.selectedAddress }) {
- if (!this.isMainnet() || this.disabled) {
+ const { chainId } = this.config;
+ if (!supportedNftDetectionNetworks.includes(chainId) || this.disabled) {
return;
}
if (!userAddress) {
return;
}
- const apiNfts = await this.getOwnerNfts(userAddress);
- const addNftPromises = apiNfts.map(async (nft) => {
- const {
- tokenId: token_id,
- contract,
- kind,
- image: image_url,
- imageSmall: image_thumbnail_url,
- metadata: { imageOriginal: image_original_url } = {},
- name,
- description,
- attributes,
- topBid,
- lastSale,
- rarityRank,
- rarityScore,
- collection
- } = nft.token;
- let ignored;
- const { ignoredNfts } = this.getNftState();
- if (ignoredNfts.length) {
- ignored = ignoredNfts.find((c) => {
- return c.address === _controllerutils.toChecksumHexAddress.call(void 0, contract) && c.tokenId === token_id;
- });
- }
- if (!ignored) {
- const nftMetadata = Object.assign(
- {},
- { name },
- description && { description },
- image_url && { image: image_url },
- image_thumbnail_url && { imageThumbnail: image_thumbnail_url },
- image_original_url && { imageOriginal: image_original_url },
- kind && { standard: kind.toUpperCase() },
- lastSale && { lastSale },
- attributes && { attributes },
- topBid && { topBid },
- rarityRank && { rarityRank },
- rarityScore && { rarityScore },
- collection && { collection }
- );
- await this.addNft(contract, token_id, {
- nftMetadata,
- userAddress,
- source: "detected" /* Detected */,
- networkClientId
+
+ const updateKey = `${chainId}:${userAddress}`;
+ if (updateKey in this.inProcessNftFetchingUpdates) {
+ // This prevents redundant updates
+ // This promise is resolved after the in-progress update has finished,
+ // and state has been updated.
+ await this.inProcessNftFetchingUpdates[updateKey];
+ return;
+ }
+ const {
+ promise: inProgressUpdate,
+ resolve: updateSucceeded,
+ reject: updateFailed
+ } = _utils.createDeferredPromise.call(void 0, { suppressUnhandledRejection: true });
+ this.inProcessNftFetchingUpdates[updateKey] = inProgressUpdate;
+
+ let next;
+ let apiNfts= [];
+ let resultNftApi;
+
+ try{
+ do {
+ resultNftApi = await this.getOwnerNfts(userAddress, chainId, next)
+ apiNfts = resultNftApi.tokens.filter(
+ (elm) =>
+ elm.token.isSpam === false &&
+ (elm.blockaidResult?.result_type
+ ? elm.blockaidResult?.result_type === BlockaidResultType.Benign
+ : true),
+ );
+ const addNftPromises = apiNfts.map(async (nft) => {
+ const {
+ tokenId: token_id,
+ contract,
+ kind,
+ image: image_url,
+ imageSmall: image_thumbnail_url,
+ metadata: { imageOriginal: image_original_url } = {},
+ name,
+ description,
+ attributes,
+ topBid,
+ lastSale,
+ rarityRank,
+ rarityScore,
+ collection,
+ } = nft.token;
+
+ let ignored;
+ /* istanbul ignore else */
+ const { ignoredNfts } = this.getNftState();
+ if (ignoredNfts.length) {
+ ignored = ignoredNfts.find((c) => {
+ return c.address === _controllerutils.toChecksumHexAddress.call(void 0, contract) && c.tokenId === token_id;
+ });
+ }
+ /* istanbul ignore else */
+ if (!ignored) {
+ const nftMetadata = Object.assign(
+ {},
+ { name },
+ description && { description },
+ image_url && { image: image_url },
+ image_thumbnail_url && { imageThumbnail: image_thumbnail_url },
+ image_original_url && { imageOriginal: image_original_url },
+ kind && { standard: kind.toUpperCase() },
+ lastSale && { lastSale },
+ attributes && { attributes },
+ topBid && { topBid },
+ rarityRank && { rarityRank },
+ rarityScore && { rarityScore },
+ collection && { collection }
+ );
+ await this.addNft(contract, token_id, {
+ nftMetadata,
+ userAddress,
+ source: "detected" /* Detected */,
+ networkClientId
+ });
+ }
});
- }
- });
- await Promise.all(addNftPromises);
+ await Promise.all(addNftPromises);
+ } while ((next = resultNftApi.continuation));
+ updateSucceeded();
+ } catch (error){
+ updateFailed(error);
+ throw error;
+ } finally {
+ delete this.inProcessNftFetchingUpdates[updateKey];
+ }
}
};
var NftDetectionController_default = NftDetectionController;
3 changes: 0 additions & 3 deletions app/_locales/de/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/el/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/es/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/fr/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/hi/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/id/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions app/_locales/ja/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e54295d

Please sign in to comment.