diff --git a/packages/assets-controllers/src/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts index 3b2fa76f72..38b31b0d0d 100644 --- a/packages/assets-controllers/src/NftDetectionController.test.ts +++ b/packages/assets-controllers/src/NftDetectionController.test.ts @@ -22,10 +22,7 @@ import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; import { FakeProvider } from '../../../tests/fake-provider'; import { advanceTime } from '../../../tests/helpers'; import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; -import { - buildCustomNetworkClientConfiguration, - buildMockGetNetworkClientById, -} from '../../network-controller/tests/helpers'; +import { buildMockGetNetworkClientById } from '../../network-controller/tests/helpers'; import { Source } from './constants'; import { getDefaultNftControllerState } from './NftController'; import { @@ -35,8 +32,6 @@ import { type AllowedEvents, } from './NftDetectionController'; -const DEFAULT_INTERVAL = 180000; - const controllerName = 'NftDetectionController' as const; const defaultSelectedAccount = createMockInternalAccount(); @@ -303,15 +298,13 @@ describe('NftDetectionController', () => { sinon.restore(); }); - it('should poll and detect NFTs on interval while on mainnet', async () => { + it('should call detect NFTs on mainnet', async () => { const mockGetSelectedAccount = jest .fn() .mockReturnValue(defaultSelectedAccount); await withController( { - options: { - interval: 10, - }, + options: {}, mockGetSelectedAccount, }, async ({ controller, controllerEvents }) => { @@ -322,12 +315,9 @@ describe('NftDetectionController', () => { ...getDefaultPreferencesState(), useNftDetection: true, }); - // Wait for detect call triggered by preferences state change to settle - await advanceTime({ - clock, - duration: 1, - }); + // call detectNfts + await controller.detectNfts(); expect(mockNfts.calledOnce).toBe(true); await advanceTime({ @@ -335,151 +325,34 @@ describe('NftDetectionController', () => { duration: 10, }); - expect(mockNfts.calledTwice).toBe(true); - }, - ); - }); - - it('should poll and detect NFTs by networkClientId on interval while on mainnet', async () => { - await withController( - { - options: {}, - }, - async ({ controller }) => { - const spy = jest - .spyOn(controller, 'detectNfts') - .mockImplementation(() => { - return Promise.resolve(); - }); - - controller.startPollingByNetworkClientId('mainnet', { - address: '0x1', - }); - - await advanceTime({ clock, duration: 0 }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(2); - await advanceTime({ clock, duration: DEFAULT_INTERVAL }); - expect(spy.mock.calls).toMatchObject([ - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - ]); + expect(mockNfts.calledTwice).toBe(false); }, ); }); - it('should not rely on the currently selected chain to poll for NFTs when a specific chain is being targeted for polling', async () => { - await withController( - { - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-AAAA-AAAA-AAAA': buildCustomNetworkClientConfiguration({ - chainId: '0x1337', - }), - }, - }, - async ({ controller, controllerEvents }) => { - const spy = jest - .spyOn(controller, 'detectNfts') - .mockImplementation(() => { - return Promise.resolve(); - }); - - controller.startPollingByNetworkClientId('mainnet', { - address: '0x1', + it('should call detect NFTs by networkClientId on mainnet', async () => { + await withController(async ({ controller }) => { + const spy = jest + .spyOn(controller, 'detectNfts') + .mockImplementation(() => { + return Promise.resolve(); }); - await advanceTime({ clock, duration: 0 }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(2); - await advanceTime({ clock, duration: DEFAULT_INTERVAL }); - expect(spy.mock.calls).toMatchObject([ - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - ]); + // call detectNfts + await controller.detectNfts({ + networkClientId: 'mainnet', + userAddress: '0x1', + }); - controllerEvents.triggerNetworkStateChange({ - ...defaultNetworkState, - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - }); - await advanceTime({ clock, duration: DEFAULT_INTERVAL }); - expect(spy.mock.calls).toMatchObject([ - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - ]); - }, - ); + expect(spy.mock.calls).toMatchObject([ + [ + { + networkClientId: 'mainnet', + userAddress: '0x1', + }, + ], + ]); + }); }); it('should detect mainnet truthy', async () => { @@ -514,110 +387,44 @@ describe('NftDetectionController', () => { ); }); - it('should not autodetect while not on mainnet', async () => { - await withController(async ({ controller }) => { - const mockNfts = sinon.stub(controller, 'detectNfts'); - - await controller.start(); - await advanceTime({ clock, duration: DEFAULT_INTERVAL }); - - expect(mockNfts.called).toBe(false); + it('should return when detectNfts is called on a not supported network for detection', async () => { + const selectedAddress = '0x1'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, }); - }); - - it('should respond to chain ID changing when using legacy polling', async () => { - const mockAddNft = jest.fn(); - const pollingInterval = 100; - const selectedAccount = createMockInternalAccount({ address: '0x1' }); const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); - await withController( { - options: { - interval: pollingInterval, - addNft: mockAddNft, - disabled: false, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-AAAA-AAAA-AAAA': buildCustomNetworkClientConfiguration({ - chainId: '0x123', - }), - }, mockNetworkState: { - selectedNetworkClientId: 'mainnet', + selectedNetworkClientId: 'goerli', }, mockPreferencesState: {}, mockGetSelectedAccount, }, - async ({ controller, controllerEvents }) => { - await controller.start(); - // await clock.tickAsync(pollingInterval); + async ({ controller }) => { + const mockNfts = sinon.stub(controller, 'detectNfts'); - expect(mockAddNft).toHaveBeenNthCalledWith( - 1, - '0xCE7ec4B2DfB30eB6c0BB5656D33aAd6BFb4001Fc', - '2577', - { - nftMetadata: { - description: - "Redacted Remilio Babies is a collection of 10,000 neochibi pfpNFT's expanding the Milady Maker paradigm with the introduction of young J.I.T. energy and schizophrenic reactionary aesthetics. We are #REMILIONAIREs.", - image: 'https://imgtest', - imageOriginal: 'https://remilio.org/remilio/632.png', - imageThumbnail: 'https://imgSmall', - name: 'Remilio 632', - rarityRank: 8872, - rarityScore: 343.443, - standard: 'ERC721', - }, - userAddress: '0x1', - source: Source.Detected, - }, - ); - expect(mockAddNft).toHaveBeenNthCalledWith( - 2, - '0x0B0fa4fF58D28A88d63235bd0756EDca69e49e6d', - '2578', - { - nftMetadata: { - description: 'Description 2578', - image: 'https://imgtest', - imageOriginal: 'https://remilio.org/remilio/632.png', - imageThumbnail: 'https://imgSmall', - name: 'ID 2578', - rarityRank: 8872, - rarityScore: 343.443, - standard: 'ERC721', - }, - userAddress: '0x1', - source: Source.Detected, - }, - ); - expect(mockAddNft).toHaveBeenNthCalledWith( - 3, - '0xebE4e5E773AFD2bAc25De0cFafa084CFb3cBf1eD', - '2574', - { - nftMetadata: { - description: 'Description 2574', - image: 'image/2574.png', - imageOriginal: 'imageOriginal/2574.png', - name: 'ID 2574', - standard: 'ERC721', - }, - userAddress: '0x1', - source: Source.Detected, - }, - ); + // nock + const mockApiCall = nock(NFT_API_BASE_URL) + .get(`/users/${selectedAddress}/tokens`) + .query({ + continuation: '', + limit: '50', + chainIds: '1', + includeTopBid: true, + }) + .reply(200, { + tokens: [], + }); - controllerEvents.triggerNetworkStateChange({ - ...defaultNetworkState, - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + // call detectNfts + await controller.detectNfts({ + networkClientId: 'goerli', + userAddress: selectedAddress, }); - await clock.tickAsync(pollingInterval); - // Not 6 times, which is what would happen if detectNfts were called - // again - expect(mockAddNft).toHaveBeenCalledTimes(3); + expect(mockNfts.called).toBe(true); + expect(mockApiCall.isDone()).toBe(false); }, ); }); @@ -845,7 +652,7 @@ describe('NftDetectionController', () => { ); }); - it('should not autodetect NFTs that exist in the ignoreList', async () => { + it('should not detect NFTs that exist in the ignoreList', async () => { const mockAddNft = jest.fn(); const mockGetSelectedAccount = jest.fn(); const mockGetNftState = jest.fn().mockImplementation(() => { @@ -942,7 +749,7 @@ describe('NftDetectionController', () => { it('should not detectNfts when disabled is false and useNftDetection is true', async () => { await withController( - { options: { disabled: false, interval: 10 } }, + { options: { disabled: false } }, async ({ controller, controllerEvents }) => { const mockNfts = sinon.stub(controller, 'detectNfts'); controllerEvents.triggerPreferencesStateChange({ @@ -956,13 +763,6 @@ describe('NftDetectionController', () => { }); expect(mockNfts.calledOnce).toBe(false); - - await advanceTime({ - clock, - duration: 10, - }); - - expect(mockNfts.calledTwice).toBe(false); }, ); }); @@ -1000,7 +800,7 @@ describe('NftDetectionController', () => { ); }); - it('should do nothing when the request to Nft API fails', async () => { + it('should not call addNFt when the request to Nft API call throws', async () => { const selectedAccount = createMockInternalAccount({ address: '0x3' }); nock(NFT_API_BASE_URL) .get(`/users/${selectedAccount.address}/tokens`) @@ -1033,7 +833,8 @@ describe('NftDetectionController', () => { }); mockAddNft.mockReset(); - await controller.detectNfts(); + // eslint-disable-next-line jest/require-to-throw-message + await expect(() => controller.detectNfts()).rejects.toThrow(); expect(mockAddNft).not.toHaveBeenCalled(); }, @@ -1047,23 +848,8 @@ describe('NftDetectionController', () => { }); const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); await withController( - { - mockPreferencesState: {}, - mockGetSelectedAccount, - }, + { mockPreferencesState: {}, mockGetSelectedAccount }, async ({ controller, controllerEvents }) => { - // This mock is for the initial detect call after preferences change - nock(NFT_API_BASE_URL) - .get(`/users/${selectedAddress}/tokens`) - .query({ - continuation: '', - limit: '50', - chainIds: '1', - includeTopBid: true, - }) - .reply(200, { - tokens: [], - }); controllerEvents.triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useNftDetection: true, @@ -1125,7 +911,7 @@ describe('NftDetectionController', () => { ); }); - it('should only re-detect when relevant settings change', async () => { + it('should not call detectNfts when settings change', async () => { const mockGetSelectedAccount = jest .fn() .mockReturnValue(defaultSelectedAccount); @@ -1146,7 +932,7 @@ describe('NftDetectionController', () => { }); } await advanceTime({ clock, duration: 1 }); - expect(detectNfts.callCount).toBe(1); + expect(detectNfts.callCount).toBe(0); // Irrelevant preference changes shouldn't trigger a detection controllerEvents.triggerPreferencesStateChange({ @@ -1155,7 +941,33 @@ describe('NftDetectionController', () => { securityAlertsEnabled: true, }); await advanceTime({ clock, duration: 1 }); - expect(detectNfts.callCount).toBe(1); + expect(detectNfts.callCount).toBe(0); + }, + ); + }); + + it('should only updates once when detectNfts called twice', async () => { + const mockAddNft = jest.fn(); + const mockGetSelectedAccount = jest.fn(); + const selectedAddress = '0x9'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); + await withController( + { + options: { addNft: mockAddNft, disabled: false }, + mockPreferencesState: {}, + mockGetSelectedAccount, + }, + async ({ controller, controllerEvents }) => { + mockGetSelectedAccount.mockReturnValue(selectedAccount); + controllerEvents.triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + useNftDetection: true, + }); + await Promise.all([controller.detectNfts(), controller.detectNfts()]); + + expect(mockAddNft).toHaveBeenCalledTimes(1); }, ); }); @@ -1278,13 +1090,8 @@ async function withController( }, }; - try { - return await testFunction({ - controller, - controllerEvents, - }); - } finally { - controller.stop(); - controller.stopAllPolling(); - } + return await testFunction({ + controller, + controllerEvents, + }); } diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index 5251e78916..63088a597f 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -1,13 +1,14 @@ import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; import { - fetchWithErrorHandling, toChecksumHexAddress, ChainId, NFT_API_BASE_URL, NFT_API_VERSION, - NFT_API_TIMEOUT, + convertHexToDecimal, + handleFetch, } from '@metamask/controller-utils'; import type { NetworkClientId, @@ -16,12 +17,12 @@ import type { NetworkControllerStateChangeEvent, NetworkControllerGetStateAction, } from '@metamask/network-controller'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { PreferencesControllerGetStateAction, PreferencesControllerStateChangeEvent, PreferencesState, } from '@metamask/preferences-controller'; +import { createDeferredPromise, type Hex } from '@metamask/utils'; import { Source } from './constants'; import { @@ -30,10 +31,10 @@ import { type NftMetadata, } from './NftController'; -const DEFAULT_INTERVAL = 180000; - const controllerName = 'NftDetectionController'; +export type NFTDetectionControllerState = Record; + export type AllowedActions = | AddApprovalRequest | NetworkControllerGetStateAction @@ -52,6 +53,7 @@ export type NftDetectionControllerMessenger = RestrictedControllerMessenger< AllowedActions['type'], AllowedEvents['type'] >; +const supportedNftDetectionNetworks: Hex[] = [ChainId.mainnet]; /** * @type ApiNft @@ -398,41 +400,36 @@ export type Metadata = { }; /** - * Controller that passively polls on a set interval for NFT auto detection + * Controller that passively detects nfts for a user address */ -export class NftDetectionController extends StaticIntervalPollingController< +export class NftDetectionController extends BaseController< typeof controllerName, - Record, + NFTDetectionControllerState, NftDetectionControllerMessenger > { - #intervalId?: ReturnType; - - #interval: number; - #disabled: boolean; readonly #addNft: NftController['addNft']; readonly #getNftState: () => NftControllerState; + #inProcessNftFetchingUpdates: Record<`${Hex}:${string}`, Promise>; + /** * The controller options * * @param options - The controller options. - * @param options.interval - The pooling interval. * @param options.messenger - A reference to the messaging system. * @param options.disabled - Represents previous value of useNftDetection. Used to detect changes of useNftDetection. Default value is true. * @param options.addNft - Add an NFT. * @param options.getNftState - Gets the current state of the Assets controller. */ constructor({ - interval = DEFAULT_INTERVAL, messenger, disabled = false, addNft, getNftState, }: { - interval?: number; messenger: NftDetectionControllerMessenger; disabled: boolean; addNft: NftController['addNft']; @@ -444,8 +441,8 @@ export class NftDetectionController extends StaticIntervalPollingController< metadata: {}, state: {}, }); - this.#interval = interval; this.#disabled = disabled; + this.#inProcessNftFetchingUpdates = {}; this.#getNftState = getNftState; this.#addNft = addNft; @@ -454,53 +451,6 @@ export class NftDetectionController extends StaticIntervalPollingController< 'PreferencesController:stateChange', this.#onPreferencesControllerStateChange.bind(this), ); - - this.setIntervalLength(this.#interval); - } - - async _executePoll( - networkClientId: string, - options: { address: string }, - ): Promise { - await this.detectNfts({ networkClientId, userAddress: options.address }); - } - - /** - * Start polling for the currency rate. - */ - async start() { - if (!this.isMainnet() || this.#disabled) { - return; - } - - await this.#startPolling(); - } - - /** - * Stop polling for the currency rate. - */ - stop() { - this.#stopPolling(); - } - - #stopPolling() { - if (this.#intervalId) { - clearInterval(this.#intervalId); - } - } - - /** - * Starts a new polling interval. - * - */ - async #startPolling(): Promise { - this.#stopPolling(); - await this.detectNfts(); - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.#intervalId = setInterval(async () => { - await this.detectNfts(); - }, this.#interval); } /** @@ -533,57 +483,43 @@ export class NftDetectionController extends StaticIntervalPollingController< #onPreferencesControllerStateChange({ useNftDetection }: PreferencesState) { if (!useNftDetection !== this.#disabled) { this.#disabled = !useNftDetection; - if (useNftDetection) { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.start(); - } else { - this.stop(); - } } } - #getOwnerNftApi({ address, next }: { address: string; next?: string }) { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${NFT_API_BASE_URL}/users/${address}/tokens?chainIds=1&limit=50&includeTopBid=true&continuation=${ + #getOwnerNftApi({ + chainId, + address, + next, + }: { + chainId: string; + address: string; + next?: string; + }) { + return `${ + NFT_API_BASE_URL as string + }/users/${address}/tokens?chainIds=${chainId}&limit=50&includeTopBid=true&continuation=${ next ?? '' }`; } - async #getOwnerNfts(address: string) { - let nftApiResponse: ReservoirResponse; - let nfts: TokensResponse[] = []; - let next; - - do { - nftApiResponse = await fetchWithErrorHandling({ - url: this.#getOwnerNftApi({ address, next }), - options: { - headers: { - Version: NFT_API_VERSION, - }, - }, - timeout: NFT_API_TIMEOUT, - }); - - if (!nftApiResponse) { - return nfts; - } - - const newNfts = - nftApiResponse.tokens?.filter( - (elm) => - elm.token.isSpam === false && - (elm.blockaidResult?.result_type - ? elm.blockaidResult?.result_type === BlockaidResultType.Benign - : true), - ) ?? []; - - nfts = [...nfts, ...newNfts]; - } while ((next = nftApiResponse.continuation)); - - return nfts; + async #getOwnerNfts( + address: string, + chainId: Hex, + cursor: string | undefined, + ) { + // Convert hex chainId to number + const convertedChainId = convertHexToDecimal(chainId).toString(); + const url = this.#getOwnerNftApi({ + chainId: convertedChainId, + address, + next: cursor, + }); + const nftApiResponse: ReservoirResponse = await handleFetch(url, { + headers: { + Version: NFT_API_VERSION, + }, + }); + return nftApiResponse; } /** @@ -602,8 +538,19 @@ export class NftDetectionController extends StaticIntervalPollingController< options?.userAddress ?? this.messagingSystem.call('AccountsController:getSelectedAccount') .address; + + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + /* istanbul ignore if */ - if (!this.isMainnet() || this.#disabled) { + if (!supportedNftDetectionNetworks.includes(chainId) || this.#disabled) { return; } /* istanbul ignore else */ @@ -611,74 +558,103 @@ export class NftDetectionController extends StaticIntervalPollingController< return; } - const apiNfts = await this.#getOwnerNfts(userAddress); - const addNftPromises = apiNfts.map(async (nft) => { - const { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - tokenId: token_id, - contract, - kind, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - image: image_url, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - imageSmall: image_thumbnail_url, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention - 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 > 0) { - ignored = ignoredNfts.find((c) => { - /* istanbul ignore next */ - return ( - c.address === toChecksumHexAddress(contract) && - c.tokenId === token_id - ); - }); - } - - /* istanbul ignore else */ - if (!ignored) { - /* istanbul ignore next */ - const nftMetadata: 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 }, - ); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const updateKey: `${Hex}:${string}` = `${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, + } = createDeferredPromise({ suppressUnhandledRejection: true }); + this.#inProcessNftFetchingUpdates[updateKey] = inProgressUpdate; - await this.#addNft(contract, token_id, { - nftMetadata, - userAddress, - source: Source.Detected, - networkClientId: options?.networkClientId, + let next; + let apiNfts: TokensResponse[] = []; + let resultNftApi: ReservoirResponse; + 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, + contract, + kind, + image: imageUrl, + imageSmall: imageThumbnailUrl, + metadata: { imageOriginal: imageOriginalUrl } = {}, + 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) => { + /* istanbul ignore next */ + return ( + c.address === toChecksumHexAddress(contract) && + c.tokenId === tokenId + ); + }); + } + + /* istanbul ignore else */ + if (!ignored) { + /* istanbul ignore next */ + const nftMetadata: NftMetadata = Object.assign( + {}, + { name }, + description && { description }, + imageUrl && { image: imageUrl }, + imageThumbnailUrl && { imageThumbnail: imageThumbnailUrl }, + imageOriginalUrl && { imageOriginal: imageOriginalUrl }, + kind && { standard: kind.toUpperCase() }, + lastSale && { lastSale }, + attributes && { attributes }, + topBid && { topBid }, + rarityRank && { rarityRank }, + rarityScore && { rarityScore }, + collection && { collection }, + ); + + await this.#addNft(contract, tokenId, { + nftMetadata, + userAddress, + source: Source.Detected, + networkClientId: options?.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]; + } } }