diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0cceb966d5db..e04e4efcd46a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -591,28 +591,24 @@ export default class MetamaskController extends EventEmitter { state: initState.TokenListController, }); - this.assetsContractController = new AssetsContractController( - { - chainId: getCurrentChainId({ metamask: this.networkController.state }), - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), - onNetworkDidChange: (cb) => - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - () => { - const networkState = this.networkController.state; - return cb(networkState); - }, - ), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - }, - { - provider: this.provider, - }, - initState.AssetsContractController, - ); + const assetsContractControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'AssetsContractController', + allowedActions: [ + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'NetworkController:getSelectedNetworkClient', + 'NetworkController:getState', + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + ], + }); + this.assetsContractController = new AssetsContractController({ + messenger: assetsContractControllerMessenger, + chainId: getCurrentChainId({ metamask: this.networkController.state }), + }); const tokensControllerMessenger = this.controllerMessenger.getRestricted({ name: 'TokensController', @@ -648,32 +644,18 @@ export default class MetamaskController extends EventEmitter { `${this.networkController.name}:getNetworkClientById`, 'AccountsController:getSelectedAccount', 'AccountsController:getAccount', + 'AssetsContractController:getERC721AssetName', + 'AssetsContractController:getERC721AssetSymbol', + 'AssetsContractController:getERC721TokenURI', + 'AssetsContractController:getERC721OwnerOf', + 'AssetsContractController:getERC1155BalanceOf', + 'AssetsContractController:getERC1155TokenURI', ], }); this.nftController = new NftController({ state: initState.NftController, messenger: nftControllerMessenger, chainId: getCurrentChainId({ metamask: this.networkController.state }), - getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( - this.assetsContractController, - ), - getERC721AssetSymbol: - this.assetsContractController.getERC721AssetSymbol.bind( - this.assetsContractController, - ), - getERC721TokenURI: this.assetsContractController.getERC721TokenURI.bind( - this.assetsContractController, - ), - getERC721OwnerOf: this.assetsContractController.getERC721OwnerOf.bind( - this.assetsContractController, - ), - getERC1155BalanceOf: - this.assetsContractController.getERC1155BalanceOf.bind( - this.assetsContractController, - ), - getERC1155TokenURI: this.assetsContractController.getERC1155TokenURI.bind( - this.assetsContractController, - ), onNftAdded: ({ address, symbol, tokenId, standard, source }) => this.metaMetricsController.trackEvent({ event: MetaMetricsEventName.NftAdded, diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 304da136a56f..e9711e5467ab 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -76,7 +76,15 @@ "TextEncoder": true }, "packages": { - "@metamask/assets-controllers>multiformats": true + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true + } + }, + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "console.warn": true, + "crypto.subtle.digest": true } }, "@ensdomains/content-hash>multihashes": { @@ -693,13 +701,13 @@ "setTimeout": true }, "packages": { + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true, "@ethereumjs/tx>@ethereumjs/util": true, "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>multiformats": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -735,14 +743,6 @@ "uuid": true } }, - "@metamask/assets-controllers>multiformats": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "console.warn": true, - "crypto.subtle.digest": true - } - }, "@metamask/base-controller": { "globals": { "setTimeout": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 304da136a56f..e9711e5467ab 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -76,7 +76,15 @@ "TextEncoder": true }, "packages": { - "@metamask/assets-controllers>multiformats": true + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true + } + }, + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "console.warn": true, + "crypto.subtle.digest": true } }, "@ensdomains/content-hash>multihashes": { @@ -693,13 +701,13 @@ "setTimeout": true }, "packages": { + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true, "@ethereumjs/tx>@ethereumjs/util": true, "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>multiformats": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -735,14 +743,6 @@ "uuid": true } }, - "@metamask/assets-controllers>multiformats": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "console.warn": true, - "crypto.subtle.digest": true - } - }, "@metamask/base-controller": { "globals": { "setTimeout": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 304da136a56f..e9711e5467ab 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -76,7 +76,15 @@ "TextEncoder": true }, "packages": { - "@metamask/assets-controllers>multiformats": true + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true + } + }, + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "console.warn": true, + "crypto.subtle.digest": true } }, "@ensdomains/content-hash>multihashes": { @@ -693,13 +701,13 @@ "setTimeout": true }, "packages": { + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true, "@ethereumjs/tx>@ethereumjs/util": true, "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>multiformats": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -735,14 +743,6 @@ "uuid": true } }, - "@metamask/assets-controllers>multiformats": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "console.warn": true, - "crypto.subtle.digest": true - } - }, "@metamask/base-controller": { "globals": { "setTimeout": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 756f31eaeb55..ba37c2883157 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -76,7 +76,15 @@ "TextEncoder": true }, "packages": { - "@metamask/assets-controllers>multiformats": true + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true + } + }, + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "console.warn": true, + "crypto.subtle.digest": true } }, "@ensdomains/content-hash>multihashes": { @@ -785,13 +793,13 @@ "setTimeout": true }, "packages": { + "@ensdomains/content-hash>multicodec>uint8arrays>multiformats": true, "@ethereumjs/tx>@ethereumjs/util": true, "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>multiformats": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -827,14 +835,6 @@ "uuid": true } }, - "@metamask/assets-controllers>multiformats": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "console.warn": true, - "crypto.subtle.digest": true - } - }, "@metamask/base-controller": { "globals": { "setTimeout": true diff --git a/package.json b/package.json index 7605e5042066..99ed1e060cf5 100644 --- a/package.json +++ b/package.json @@ -304,7 +304,7 @@ "@metamask/address-book-controller": "^5.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "^36.0.0", + "@metamask/assets-controllers": "^37.0.0", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.6.0", "@metamask/browser-passworder": "^4.3.0", diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap index d03df016c1b3..0a025bc47ff0 100644 --- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap @@ -1,6 +1,189 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NFT Details should match minimal props and state snapshot 1`] = ` +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+ C +
+
+
+ +
+
+
+
+

+ MUNK #1 +

+
+
+

+

+
+
+

+ Contract address +

+
+ + +
+
+
+

+ Token ID +

+

+ 1 +

+
+
+

+ Token standard +

+

+ ERC721 +

+
+
+ +
+
+
+ Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted. +
+
+
+
+
+
+ +`; + +exports[`NFT Details should match minimal props and state snapshot 2`] = `
`; + +exports[`NFT Details should match minimal props and state snapshot 3`] = `
`; diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap new file mode 100644 index 000000000000..dfedee737c93 --- /dev/null +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NFT full image should match snapshot 1`] = ` +
+
+
+
+
+
+

+

+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`NFT full image should match snapshot 2`] = `
`; diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.test.js b/ui/components/app/assets/nfts/nft-details/nft-details.test.js index 65c3ba339e5f..350dc4813c6a 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.test.js +++ b/ui/components/app/assets/nfts/nft-details/nft-details.test.js @@ -19,8 +19,17 @@ import { } from '../../../../../store/actions'; import { CHAIN_IDS } from '../../../../../../shared/constants/network'; import { mockNetworkState } from '../../../../../../test/stub/networks'; +import { + getAssetImageURL, + shortenAddress, +} from '../../../../../helpers/utils/util'; import NftDetails from './nft-details'; +jest.mock('../../../../../helpers/utils/util', () => ({ + getAssetImageURL: jest.fn(), + shortenAddress: jest.fn(), +})); + jest.mock('copy-to-clipboard'); const mockHistoryPush = jest.fn(); @@ -62,13 +71,20 @@ describe('NFT Details', () => { jest.clearAllMocks(); }); - it('should match minimal props and state snapshot', () => { + it('should match minimal props and state snapshot', async () => { + getAssetImageURL.mockResolvedValue( + 'https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link', + ); + shortenAddress.mockReturnValue('0xDc738...06414'); + const { container } = renderWithProvider( , mockStore, ); - expect(container).toMatchSnapshot(); + await waitFor(() => { + expect(container).toMatchSnapshot(); + }); }); it(`should route to '/' route when the back button is clicked`, () => { diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.tsx b/ui/components/app/assets/nfts/nft-details/nft-details.tsx index 0064dc38976c..8a857da43989 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-details.tsx @@ -18,10 +18,7 @@ import { AlignItems, } from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; -import { - getAssetImageURL, - shortenAddress, -} from '../../../../../helpers/utils/util'; +import { shortenAddress } from '../../../../../helpers/utils/util'; import { getNftImageAlt } from '../../../../../helpers/utils/nfts'; import { getCurrentChainId, @@ -73,6 +70,7 @@ import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../../ import { getConversionRate } from '../../../../../ducks/metamask/metamask'; import { Numeric } from '../../../../../../shared/modules/Numeric'; import { addUrlProtocolPrefix } from '../../../../../../app/scripts/lib/util'; +import useGetAssetImageUrl from '../../../../../hooks/useGetAssetImageUrl'; import NftDetailInformationRow from './nft-detail-information-row'; import NftDetailInformationFrame from './nft-detail-information-frame'; import NftDetailDescription from './nft-detail-description'; @@ -110,9 +108,10 @@ export default function NftDetails({ nft }: { nft: Nft }) { const nftImageAlt = getNftImageAlt(nft); const nftSrcUrl = imageOriginal ?? image; - const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway); const isIpfsURL = nftSrcUrl?.startsWith('ipfs:'); - const isImageHosted = image?.startsWith('https:'); + const isImageHosted = + image?.startsWith('https:') || image?.startsWith('http:'); + const nftImageURL = useGetAssetImageUrl(imageOriginal ?? image, ipfsGateway); const hasFloorAskPrice = Boolean( collection?.floorAsk?.price?.amount?.usd && @@ -165,6 +164,7 @@ export default function NftDetails({ nft }: { nft: Nft }) { }; const { chainId } = currentChain; + useEffect(() => { trackEvent({ event: MetaMetricsEventName.NftDetailsOpened, diff --git a/ui/components/app/assets/nfts/nft-details/nft-full-image.test.js b/ui/components/app/assets/nfts/nft-details/nft-full-image.test.js new file mode 100644 index 000000000000..193025a402ac --- /dev/null +++ b/ui/components/app/assets/nfts/nft-details/nft-full-image.test.js @@ -0,0 +1,45 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { toHex } from '@metamask/controller-utils'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; +import mockState from '../../../../../../test/data/mock-state.json'; +import NftFullImage from './nft-full-image'; + +const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; +const nfts = mockState.metamask.allNfts[selectedAddress][toHex(5)]; +const mockAsset = nfts[0].address; +const mockId = nfts[0].tokenId; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useHistory: () => ({ + push: jest.fn(), + }), + useParams: () => ({ + asset: mockAsset, + id: mockId, + }), + }; +}); + +describe('NFT full image', () => { + const mockStore = configureMockStore([thunk])(mockState); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should match snapshot', async () => { + const { container } = renderWithProvider(, mockStore); + + await waitFor(() => { + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx index 3d09cba1ddc4..64e2a3191c0e 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useHistory, useParams } from 'react-router-dom'; -import { getAssetImageURL } from '../../../../../helpers/utils/util'; import { getNftImageAlt } from '../../../../../helpers/utils/nfts'; import { getCurrentNetwork, getIpfsGateway } from '../../../../../selectors'; @@ -23,6 +22,7 @@ import { } from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { ASSET_ROUTE } from '../../../../../helpers/constants/routes'; +import useGetAssetImageUrl from '../../../../../hooks/useGetAssetImageUrl'; export default function NftFullImage() { const t = useI18nContext(); @@ -37,10 +37,10 @@ export default function NftFullImage() { const ipfsGateway = useSelector(getIpfsGateway); const currentChain = useSelector(getCurrentNetwork); + const nftImageURL = useGetAssetImageUrl(imageOriginal ?? image, ipfsGateway); const nftImageAlt = getNftImageAlt(nft); const nftSrcUrl = imageOriginal ?? image; - const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway); const isIpfsURL = nftSrcUrl?.startsWith('ipfs:'); const isImageHosted = image?.startsWith('https:'); const history = useHistory(); diff --git a/ui/components/app/assets/nfts/nfts-items/collection-image.component.test.tsx b/ui/components/app/assets/nfts/nfts-items/collection-image.component.test.tsx new file mode 100644 index 000000000000..726ca26508b9 --- /dev/null +++ b/ui/components/app/assets/nfts/nfts-items/collection-image.component.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { screen } from '@testing-library/react'; +import mockState from '../../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; +import { getIpfsGateway, getOpenSeaEnabled } from '../../../../../selectors'; +import { CollectionImageComponent } from './collection-image.component'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +jest.mock('../../../../../selectors', () => ({ + ...jest.requireActual('../../../../../selectors'), + getIpfsGateway: jest.fn(), + getOpenSeaEnabled: jest.fn(), +})); +const mockStore = configureMockStore([thunk])(mockState); +describe('CollectionImageComponent', () => { + const useSelectorMock = useSelector as jest.Mock; + beforeEach(() => { + jest.resetAllMocks(); + }); + it('should show collection first letter when ipfs is not enabled', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getIpfsGateway) { + return undefined; + } + return undefined; + }); + + const props = { + collectionName: 'NFT Collection', + collectionImage: 'ipfs://', + }; + + const { getByText } = renderWithProvider( + , + mockStore, + ); + + expect(getByText('N')).toBeInTheDocument(); + }); + + it('should show collection first letter when opensea is not enabled', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getOpenSeaEnabled) { + return false; + } + return undefined; + }); + + const props = { + collectionName: 'Test NFT Collection', + collectionImage: 'https://image.png', + }; + + const { getByText } = renderWithProvider( + , + mockStore, + ); + + expect(getByText('T')).toBeInTheDocument(); + }); + + it('should show collection image', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getOpenSeaEnabled) { + return true; + } + return undefined; + }); + + const props = { + collectionName: 'Test NFT Collection', + collectionImage: 'https://image.png', + }; + + renderWithProvider(, mockStore); + + expect(screen.getAllByRole('img')).toHaveLength(1); + }); +}); diff --git a/ui/components/app/assets/nfts/nfts-items/collection-image.component.tsx b/ui/components/app/assets/nfts/nfts-items/collection-image.component.tsx new file mode 100644 index 000000000000..24f34714dd95 --- /dev/null +++ b/ui/components/app/assets/nfts/nfts-items/collection-image.component.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { useSelector } from 'react-redux'; + +import { getIpfsGateway, getOpenSeaEnabled } from '../../../../../selectors'; +import useGetAssetImageUrl from '../../../../../hooks/useGetAssetImageUrl'; +import { Box } from '../../../../component-library'; + +export const CollectionImageComponent = ({ + collectionImage, + collectionName, +}: { + collectionImage: string; + collectionName: string; +}) => { + const ipfsGateway = useSelector(getIpfsGateway); + const openSeaEnabled = useSelector(getOpenSeaEnabled); + const nftImageURL = useGetAssetImageUrl(collectionImage, ipfsGateway); + + const renderCollectionImage = () => { + if (collectionImage?.startsWith('ipfs') && !ipfsGateway) { + return ( +
+ {collectionName?.[0]?.toUpperCase() ?? null} +
+ ); + } + if (!openSeaEnabled && !collectionImage?.startsWith('ipfs')) { + return ( +
+ {collectionName?.[0]?.toUpperCase() ?? null} +
+ ); + } + + if (collectionImage) { + return ( + {collectionName} + ); + } + return ( +
+ {collectionName?.[0]?.toUpperCase() ?? null} +
+ ); + }; + + return {renderCollectionImage()}; +}; diff --git a/ui/components/app/assets/nfts/nfts-items/nfts-items.js b/ui/components/app/assets/nfts/nfts-items/nfts-items.js index 9e8196669b0e..c44de72b261b 100644 --- a/ui/components/app/assets/nfts/nfts-items/nfts-items.js +++ b/ui/components/app/assets/nfts/nfts-items/nfts-items.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -22,7 +22,6 @@ import { getIpfsGateway, getSelectedInternalAccount, getCurrentNetwork, - getOpenSeaEnabled, } from '../../../../../selectors'; import { ASSET_ROUTE, @@ -46,6 +45,8 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../../shared/constants/metametrics'; +import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils'; +import { CollectionImageComponent } from './collection-image.component'; const width = (isModal) => { const env = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; @@ -78,7 +79,8 @@ export default function NftsItems({ const currentChain = useSelector(getCurrentNetwork); const t = useI18nContext(); const ipfsGateway = useSelector(getIpfsGateway); - const openSeaEnabled = useSelector(getOpenSeaEnabled); + + const [updatedNfts, setUpdatedNfts] = useState([]); const trackEvent = useContext(MetaMetricsContext); const sendAnalytics = useSelector(getSendAnalyticProperties); @@ -116,39 +118,40 @@ export default function NftsItems({ dispatch, ]); - const history = useHistory(); + const getAssetImageUrlAndUpdate = async (image, nft) => { + const nftImage = await getAssetImageURL(image, ipfsGateway); + const updatedNFt = { + ...nft, + ipfsImageUpdated: nftImage, + }; + return updatedNFt; + }; - const renderCollectionImage = (collectionImage, collectionName) => { - if (collectionImage?.startsWith('ipfs') && !ipfsGateway) { - return ( -
- {collectionName?.[0]?.toUpperCase() ?? null} -
- ); - } - if (!openSeaEnabled && !collectionImage?.startsWith('ipfs')) { - return ( -
- {collectionName?.[0]?.toUpperCase() ?? null} -
- ); - } + useEffect(() => { + const promisesArr = []; + const modifyItems = async () => { + for (const key of collectionsKeys) { + const { nfts } = collections[key]; + for (const singleNft of nfts) { + const { image, imageOriginal } = singleNft; - if (collectionImage) { - return ( - {collectionName} - ); - } - return ( -
- {collectionName?.[0]?.toUpperCase() ?? null} -
- ); - }; + const isImageHosted = + image?.startsWith('https:') || image?.startsWith('http:'); + if (!isImageHosted) { + promisesArr.push( + getAssetImageUrlAndUpdate(imageOriginal ?? image, singleNft), + ); + } + } + } + const settled = await Promise.all(promisesArr); + setUpdatedNfts(settled); + }; + + modifyItems(); + }, []); + + const history = useHistory(); const updateNftDropDownStateKey = (key, isExpanded) => { const newCurrentAccountState = { @@ -198,6 +201,19 @@ export default function NftsItems({ if (!nfts.length) { return null; } + const getSource = (isImageHosted, nft) => { + if (!isImageHosted) { + const found = updatedNfts.find( + (elm) => + elm.tokenId === nft.tokenId && + isEqualCaseInsensitive(elm.address, nft.address), + ); + if (found) { + return found.ipfsImageUpdated; + } + } + return nft.image; + }; const isExpanded = nftsDropdownState[selectedAddress]?.[chainId]?.[key]; return ( @@ -220,7 +236,10 @@ export default function NftsItems({ alignItems={AlignItems.center} className="nfts-items__collection-header" > - {renderCollectionImage(collectionImage, collectionName)} + { const { image, address, tokenId, name, imageOriginal, tokenURI } = nft; - const nftImage = getAssetImageURL( - imageOriginal ?? image, - ipfsGateway, - ); const nftImageAlt = getNftImageAlt(nft); - const isImageHosted = image?.startsWith('https:'); - const nftImageURL = imageOriginal?.startsWith('ipfs') - ? nftImage - : image; + const isImageHosted = + image?.startsWith('https:') || image?.startsWith('http:'); + + const source = getSource(isImageHosted, nft); + const isIpfsURL = ( imageOriginal ?? image ?? @@ -271,9 +287,8 @@ export default function NftsItems({ className="nfts-items__item-wrapper" > { @@ -168,7 +172,7 @@ export const AssetPickerAmount = ({ standardizedAsset = { type: asset.type, image: - getAssetImageURL(asset.details.image, ipfsGateway) || + nftImageURL || (tokenList && asset.details?.address && tokenList[asset.details.address.toLowerCase()]?.iconUrl), diff --git a/ui/components/multichain/pages/send/components/recipient-content.tsx b/ui/components/multichain/pages/send/components/recipient-content.tsx index d6c8f00b446c..5c32bb5f3b66 100644 --- a/ui/components/multichain/pages/send/components/recipient-content.tsx +++ b/ui/components/multichain/pages/send/components/recipient-content.tsx @@ -45,13 +45,13 @@ import { getTokenList, getUseExternalServices, } from '../../../../../selectors'; +import useGetAssetImageUrl from '../../../../../hooks/useGetAssetImageUrl'; ///: END:ONLY_INCLUDE_IF import type { Quote } from '../../../../../ducks/send/swap-and-send-utils'; import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils'; import { AssetPicker } from '../../../asset-picker-amount/asset-picker'; import { TabName } from '../../../asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; -import { getAssetImageURL } from '../../../../../helpers/utils/util'; import { SendHexData, SendPageRow, QuoteCard } from '.'; export const SendPageRecipientContent = ({ @@ -94,6 +94,11 @@ export const SendPageRecipientContent = ({ const tokenList = useSelector(getTokenList) as TokenListMap; const ipfsGateway = useSelector(getIpfsGateway); + const nftImageURL = useGetAssetImageUrl( + sendAsset.details?.image ?? null, + ipfsGateway, + ); + isSwapAllowed = isSwapsChain && !isSwapAndSendDisabledForNetwork && @@ -169,7 +174,7 @@ export const SendPageRecipientContent = ({ ? nativeCurrencyImageUrl : tokenList && sendAsset.details && - (getAssetImageURL(sendAsset.details?.image, ipfsGateway) || + (nftImageURL || tokenList[sendAsset.details.address?.toLowerCase()] ?.iconUrl), symbol: sendAsset?.details?.symbol || nativeCurrencySymbol, diff --git a/ui/components/ui/identicon/identicon.component.js b/ui/components/ui/identicon/identicon.component.js index 50b9075acb8c..0eb05822d292 100644 --- a/ui/components/ui/identicon/identicon.component.js +++ b/ui/components/ui/identicon/identicon.component.js @@ -12,6 +12,9 @@ const getStyles = (diameter) => ({ width: diameter, borderRadius: diameter / 2, }); +const getImage = async (image, ipfsGateway) => { + return await getAssetImageURL(image, ipfsGateway); +}; export default class Identicon extends Component { static propTypes = { @@ -65,6 +68,7 @@ export default class Identicon extends Component { state = { imageLoadingError: false, + imageUrl: '', }; static defaultProps = { @@ -79,9 +83,25 @@ export default class Identicon extends Component { watchedNftContracts: {}, }; + loadImage = async () => { + const result = await getImage(this.props.image, this.props.ipfsGateway); + this.setState({ imageUrl: result }); + }; + + async componentDidMount() { + this.loadImage(); + } + + async componentDidUpdate(prevProps) { + if (prevProps.image !== this.props.image) { + this.loadImage(); + } + } + renderImage() { - const { className, diameter, alt, imageBorder, ipfsGateway } = this.props; + const { className, diameter, alt, imageBorder } = this.props; let { image } = this.props; + const { imageUrl } = this.state; if (Array.isArray(image) && image.length) { image = image[0]; @@ -91,7 +111,7 @@ export default class Identicon extends Component { typeof image === 'string' && image.toLowerCase().startsWith('ipfs://') ) { - image = getAssetImageURL(image, ipfsGateway); + image = imageUrl; } return ( diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 7eeab828a750..051dec05d1f9 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -562,7 +562,7 @@ export const sanitizeMessage = (msg, primaryType, types) => { return { value: sanitizedStruct, type: primaryType }; }; -export function getAssetImageURL(image, ipfsGateway) { +export async function getAssetImageURL(image, ipfsGateway) { if (!image || typeof image !== 'string') { return ''; } @@ -593,7 +593,7 @@ export function getAssetImageURL(image, ipfsGateway) { // In the future, we can look into solving the root cause, which might require // no longer using multiform's CID.parse() method within the assets-controller try { - return getFormattedIpfsUrl(ipfsGateway, image, true); + return await getFormattedIpfsUrl(ipfsGateway, image, true); } catch (e) { logErrorWithMessage(e); return ''; diff --git a/ui/hooks/useGetAssetImageUrl.test.ts b/ui/hooks/useGetAssetImageUrl.test.ts new file mode 100644 index 000000000000..a6fe1bdc68a3 --- /dev/null +++ b/ui/hooks/useGetAssetImageUrl.test.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-dom/test-utils'; +import { getAssetImageURL } from '../helpers/utils/util'; +import useGetAssetImageUrl from './useGetAssetImageUrl'; + +jest.mock('../helpers/utils/util', () => ({ + getAssetImageURL: jest.fn(), +})); + +const mockGetAssetImageURL = getAssetImageURL as jest.Mock; +const testIpfsGateway = 'dweb.link'; +describe('useGetAssetImageUrl', () => { + it('should return data successfully', async () => { + const testIpfsImg = + 'ipfs://bafybeieazx4q4ofby24w6n6ftmpad65k4u3vkavv6qnmsazwoe6gaced7m/728.png'; + const expectedRes = + 'https://bafybeieazx4q4ofby24w6n6ftmpad65k4u3vkavv6qnmsazwoe6gaced7m.ipfs.dweb.link/728.png'; + + mockGetAssetImageURL.mockResolvedValueOnce(expectedRes); + let result; + + await act(async () => { + result = renderHook(() => + useGetAssetImageUrl(testIpfsImg, testIpfsGateway), + ); + }); + + expect((result as unknown as Record).result.current).toEqual( + expectedRes, + ); + }); + + it('should return data successfully when image is null', async () => { + mockGetAssetImageURL.mockResolvedValueOnce(''); + const testImage = null; + let result; + await act(async () => { + result = renderHook(() => + useGetAssetImageUrl(testImage, testIpfsGateway), + ); + }); + expect((result as unknown as Record).result.current).toEqual( + '', + ); + }); +}); diff --git a/ui/hooks/useGetAssetImageUrl.ts b/ui/hooks/useGetAssetImageUrl.ts new file mode 100644 index 000000000000..b84a588ff68d --- /dev/null +++ b/ui/hooks/useGetAssetImageUrl.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; +import { getAssetImageURL } from '../helpers/utils/util'; + +const useGetAssetImageUrl = (image: string | null, ipfsGateway: string) => { + const [imageUrl, setImageUrl] = useState(''); + + useEffect(() => { + const getAssetImgUrl = async () => { + const assetImageUrl = await getAssetImageURL(image, ipfsGateway); + setImageUrl(assetImageUrl); + }; + + getAssetImgUrl(); + }, [image, ipfsGateway]); + + return imageUrl; +}; + +export default useGetAssetImageUrl; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js index c5be9de7c9f8..822db143d29a 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { ethErrors, serializeError } from 'eth-rpc-errors'; @@ -59,6 +59,7 @@ import { PRIMARY } from '../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../hooks/useUserPreferencedCurrency'; import { useCurrencyDisplay } from '../../hooks/useCurrencyDisplay'; import { useOriginMetadata } from '../../hooks/useOriginMetadata'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; const ConfirmAddSuggestedNFT = () => { const t = useContext(I18nContext); @@ -80,6 +81,7 @@ const ConfirmAddSuggestedNFT = () => { const accountName = useSelector((state) => getAddressBookEntryOrAccountName(state, selectedAddress), ); + const [suggestedNftsWithImages, setSuggestedNftsWithImages] = useState([]); const networkName = NETWORK_TO_NAME_MAP[chainId] || networkIdentifier; @@ -152,6 +154,32 @@ const ConfirmAddSuggestedNFT = () => { } } + useEffect(() => { + const addImageUrlToSuggestedNFTs = async () => { + const suggestedNftWithImages = await Promise.all( + suggestedNfts.map(async (item) => { + const imgUrl = await getAssetImageURL( + item.requestData.asset.image, + ipfsGateway, + ); + return { + ...item, + requestData: { + ...item.requestData, + asset: { + ...item.requestData.asset, + assetImageUrl: imgUrl, + }, + }, + }; + }), + ); + setSuggestedNftsWithImages(suggestedNftWithImages); + }; + + addImageUrlToSuggestedNFTs(); + }, []); // Empty dependency array to run only on mount + return ( { ({ id, requestData: { - asset: { address, tokenId, symbol, image, name }, + asset: { address, tokenId, symbol, name }, }, }) => { - const nftImageURL = getAssetImageURL(image, ipfsGateway); + const found = suggestedNftsWithImages.find( + (elm) => + elm.requestData.asset.tokenId === tokenId && + isEqualCaseInsensitive( + elm.requestData.asset.address, + address, + ), + ); + + const nftImageURL = found + ? found.requestData.asset.assetImageUrl + : ''; + const blockExplorerLink = getTokenTrackerLink( address, chainId, diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js index 177bffb7d3bd..003e88f16351 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js @@ -11,6 +11,7 @@ import mockState from '../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../test/jest/rendering'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { mockNetworkState } from '../../../test/stub/networks'; +import * as util from '../../helpers/utils/util'; import ConfirmAddSuggestedNFT from '.'; const PENDING_NFT_APPROVALS = { @@ -87,23 +88,25 @@ describe('ConfirmAddSuggestedNFT Component', () => { jest.clearAllMocks(); }); - it('should render one suggested NFT', () => { - renderComponent({ - 1: { - id: '1', - origin: 'https://www.opensea.io', - time: 1, - type: ApprovalType.WatchAsset, - requestData: { - asset: { - address: '0x8b175474e89094c44da98b954eedeac495271d0a', - name: 'CryptoKitty', - tokenId: '15', - standard: 'ERC721', + it('should render one suggested NFT', async () => { + await act(async () => + renderComponent({ + 1: { + id: '1', + origin: 'https://www.opensea.io', + time: 1, + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + name: 'CryptoKitty', + tokenId: '15', + standard: 'ERC721', + }, }, }, - }, - }); + }), + ); expect(screen.getByText('Add suggested NFTs')).toBeInTheDocument(); expect(screen.getByText('https://www.opensea.io')).toBeInTheDocument(); @@ -118,29 +121,35 @@ describe('ConfirmAddSuggestedNFT Component', () => { expect(screen.getByRole('button', { name: 'Add NFT' })).toBeInTheDocument(); }); - it('should match snapshot', () => { - const container = renderComponent({ - 1: { - id: '1', - origin: 'https://www.opensea.io', - time: 1, - type: ApprovalType.WatchAsset, - requestData: { - asset: { - address: '0x8b175474e89094c44da98b954eedeac495271d0a', - name: 'CryptoKitty', - tokenId: '15', - standard: 'ERC721', + it('should match snapshot', async () => { + let container; + await act( + async () => + (container = renderComponent({ + 1: { + id: '1', + origin: 'https://www.opensea.io', + time: 1, + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + name: 'CryptoKitty', + tokenId: '15', + standard: 'ERC721', + }, + }, }, - }, - }, - }); + })), + ); expect(container).toMatchSnapshot(); }); - it('should render a list of suggested NFTs', () => { - renderComponent({ ...PENDING_NFT_APPROVALS, ...PENDING_TOKEN_APPROVALS }); + it('should render a list of suggested NFTs', async () => { + await act(async () => + renderComponent({ ...PENDING_NFT_APPROVALS, ...PENDING_TOKEN_APPROVALS }), + ); for (const { requestData: { asset }, @@ -215,4 +224,60 @@ describe('ConfirmAddSuggestedNFT Component', () => { }), ); }); + + it('should show suggested NFTs with default image', async () => { + await act(async () => + renderComponent({ + 1: { + id: '1', + origin: 'https://www.opensea.io', + time: 1, + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + name: 'CryptoKitty', + tokenId: '15', + standard: 'ERC721', + }, + }, + }, + }), + ); + + expect(screen.getByText('CryptoKitty')).toBeInTheDocument(); + expect(screen.getByText(`#15`)).toBeInTheDocument(); + expect(screen.getAllByRole('img')).toHaveLength(1); + const defaultImg = screen.getByTestId(`nft-default-image`); + expect(defaultImg).toBeInTheDocument(); + }); + + it('should show suggested NFTs with image', async () => { + const expectedRes = + 'https://bafybeieazx4q4ofby24w6n6ftmpad65k4u3vkavv6qnmsazwoe6gaced7m.ipfs.dweb.link/728.png'; + + jest.spyOn(util, 'getAssetImageURL').mockResolvedValue(expectedRes); + await act(async () => + renderComponent({ + 1: { + id: '1', + origin: 'https://www.opensea.io', + time: 1, + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + name: 'CryptoKitty', + tokenId: '15', + standard: 'ERC721', + }, + }, + }, + }), + ); + + expect(screen.getByText('CryptoKitty')).toBeInTheDocument(); + expect(screen.getByText(`#15`)).toBeInTheDocument(); + expect(screen.getAllByRole('img')).toHaveLength(2); + }); }); diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 6df86587abce..4d5a83763213 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -16,7 +16,7 @@ import InfoTooltip from '../../../../../../components/ui/info-tooltip'; import NicknamePopovers from '../../../../../../components/app/modals/nickname-popovers'; import { ORIGIN_METAMASK } from '../../../../../../../shared/constants/app'; import SiteOrigin from '../../../../../../components/ui/site-origin'; -import { getAssetImageURL } from '../../../../../../helpers/utils/util'; +import useGetAssetImageUrl from '../../../../../../hooks/useGetAssetImageUrl'; const ConfirmPageContainerSummary = (props) => { const { @@ -36,6 +36,7 @@ const ConfirmPageContainerSummary = (props) => { const ipfsGateway = useSelector(getIpfsGateway); const txData = useSelector(txDataSelector); + const nftImageURL = useGetAssetImageUrl(image, ipfsGateway); const { txParams = {} } = txData; const { to: txParamsToAddress } = txParams; @@ -66,14 +67,12 @@ const ConfirmPageContainerSummary = (props) => { const checksummedAddress = toChecksumHexAddress(contractAddress); const renderImage = () => { - const imagePath = getAssetImageURL(image, ipfsGateway); - if (image) { return ( ); } else if (contractAddress) { diff --git a/yarn.lock b/yarn.lock index adc43681e805..5fd8f17c33a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4752,7 +4752,7 @@ __metadata: languageName: node linkType: hard -"@metamask/abi-utils@npm:^2.0.2, @metamask/abi-utils@npm:^2.0.4": +"@metamask/abi-utils@npm:^2.0.2, @metamask/abi-utils@npm:^2.0.3, @metamask/abi-utils@npm:^2.0.4": version: 2.0.4 resolution: "@metamask/abi-utils@npm:2.0.4" dependencies: @@ -4849,7 +4849,7 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.0.1, @metamask/approval-controller@npm:^7.0.2": +"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.0.2": version: 7.0.2 resolution: "@metamask/approval-controller@npm:7.0.2" dependencies: @@ -4861,45 +4861,41 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^36.0.0": - version: 36.0.0 - resolution: "@metamask/assets-controllers@npm:36.0.0" +"@metamask/assets-controllers@npm:^37.0.0": + version: 37.0.0 + resolution: "@metamask/assets-controllers@npm:37.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/abi-utils": "npm:^2.0.2" - "@metamask/accounts-controller": "npm:^17.2.0" - "@metamask/approval-controller": "npm:^7.0.1" - "@metamask/base-controller": "npm:^6.0.1" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/base-controller": "npm:^6.0.2" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.0.1" + "@metamask/controller-utils": "npm:^11.0.2" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/keyring-controller": "npm:^17.1.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^20.0.0" - "@metamask/polling-controller": "npm:^9.0.0" - "@metamask/preferences-controller": "npm:^13.0.0" + "@metamask/polling-controller": "npm:^9.0.1" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^9.1.0" "@types/bn.js": "npm:^5.1.5" "@types/uuid": "npm:^8.3.0" async-mutex: "npm:^0.5.0" bn.js: "npm:^5.2.1" cockatiel: "npm:^3.1.2" + immer: "npm:^9.0.6" lodash: "npm:^4.17.21" - multiformats: "npm:^9.5.2" + multiformats: "npm:^13.1.0" single-call-balance-checker-abi: "npm:^1.0.0" uuid: "npm:^8.3.2" peerDependencies: - "@metamask/accounts-controller": ^17.0.0 + "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^20.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/7855bf544e77c4a1dc233748941981b9f09f1633cc1f7aac06cdc6555e7ee2690cff0866d55ae5ea012c6d05bf61511b39f886ae9b1c498a9154bf9520cdf199 + checksum: 10/89798930cb80a134263ce82db736feebd064fe6c999ddcf41ca86fad81cfadbb9e37d1919a6384aaf6d3aa0cb520684e7b8228da3b9bc1e70e7aea174a69c4ac languageName: node linkType: hard @@ -4980,7 +4976,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.1, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0": +"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0": version: 11.3.0 resolution: "@metamask/controller-utils@npm:11.3.0" dependencies: @@ -6020,20 +6016,19 @@ __metadata: languageName: node linkType: hard -"@metamask/polling-controller@npm:^9.0.0": - version: 9.0.0 - resolution: "@metamask/polling-controller@npm:9.0.0" +"@metamask/polling-controller@npm:^9.0.1": + version: 9.0.1 + resolution: "@metamask/polling-controller@npm:9.0.1" dependencies: - "@metamask/base-controller": "npm:^6.0.0" - "@metamask/controller-utils": "npm:^11.0.0" - "@metamask/network-controller": "npm:^20.0.0" - "@metamask/utils": "npm:^8.3.0" + "@metamask/base-controller": "npm:^6.0.2" + "@metamask/controller-utils": "npm:^11.0.2" + "@metamask/utils": "npm:^9.1.0" "@types/uuid": "npm:^8.3.0" fast-json-stable-stringify: "npm:^2.1.0" uuid: "npm:^8.3.2" peerDependencies: "@metamask/network-controller": ^20.0.0 - checksum: 10/5e3abd84dcb3fb128add949bbda78a34d509f56b71d27f60f8ff3fd5de116424dc9e202c812c5f3c233d1489740c376b9de47f28faa387e64a198d246f962baf + checksum: 10/e9e8c51013290a2e4b2817ba1e0915783474f6a55fe614e20acf92bf707e300bec1fa612c8019ae9afe9635d018fb5d5b106c8027446ba12767220db91cf1ee5 languageName: node linkType: hard @@ -6065,18 +6060,6 @@ __metadata: languageName: node linkType: hard -"@metamask/preferences-controller@npm:^13.0.0": - version: 13.0.0 - resolution: "@metamask/preferences-controller@npm:13.0.0" - dependencies: - "@metamask/base-controller": "npm:^6.0.0" - "@metamask/controller-utils": "npm:^11.0.0" - peerDependencies: - "@metamask/keyring-controller": ^17.0.0 - checksum: 10/99d39de8adb9a43bbb5972d70feabfba6bac0c1d71d1567838e506e661f7fd293205e2c83361f7166ce72b38437d79cd0da8a7a14dd584f59b5f583dcb4d769b - languageName: node - linkType: hard - "@metamask/profile-sync-controller@npm:^0.8.0": version: 0.8.0 resolution: "@metamask/profile-sync-controller@npm:0.8.0" @@ -26138,7 +26121,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "npm:^36.0.0" + "@metamask/assets-controllers": "npm:^37.0.0" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.6.0" @@ -27471,7 +27454,14 @@ __metadata: languageName: node linkType: hard -"multiformats@npm:^9.4.2, multiformats@npm:^9.5.2": +"multiformats@npm:^13.1.0": + version: 13.2.2 + resolution: "multiformats@npm:13.2.2" + checksum: 10/6e673320e9b06d5fdbbf2bde0d3132f13fac94fb40f36d646265b5c38eba4a28c40a2c76b4efa0c1a23517fe87320e540e9ef7f28d71c1cc3239c91bf6770ce6 + languageName: node + linkType: hard + +"multiformats@npm:^9.4.2": version: 9.9.0 resolution: "multiformats@npm:9.9.0" checksum: 10/ad55c7d480d22f4258a68fd88aa2aab744fe0cb1e68d732fc886f67d858b37e3aa6c2cec12b2960ead7730d43be690931485238569952d8a3d7f90fdc726c652