From 5779f3cf202528ea9c94dcea929e6a14b7fdfab6 Mon Sep 17 00:00:00 2001 From: Gustavo Antunes Date: Fri, 29 Oct 2021 12:44:22 -0300 Subject: [PATCH] Custom network + IPFS support (#616) Add support for custom networks and IPFS urls --- src/assets/AssetsContractController.test.ts | 16 +- src/assets/AssetsDetectionController.test.ts | 2 + .../ERC721/ERC721Standard.ts | 2 +- src/assets/CollectiblesController.test.ts | 163 +++++++++++-- src/assets/CollectiblesController.ts | 218 ++++++++++-------- src/assets/assetsUtil.test.ts | 15 ++ src/constants.ts | 1 + src/util.test.ts | 23 ++ src/util.ts | 18 ++ 9 files changed, 347 insertions(+), 111 deletions(-) diff --git a/src/assets/AssetsContractController.test.ts b/src/assets/AssetsContractController.test.ts index 163271ce850..8341ca0301f 100644 --- a/src/assets/AssetsContractController.test.ts +++ b/src/assets/AssetsContractController.test.ts @@ -67,13 +67,17 @@ describe('AssetsContractController', () => { expect(tokenId).toStrictEqual('https://api.godsunchained.com/card/0'); }); - it('should return empty string as URI when address given is not an ERC-721 collectible', async () => { + it('should throw an error when address given is not an ERC-721 collectible', async () => { assetsContract.configure({ provider: MAINNET_PROVIDER }); - const tokenId = await assetsContract.getCollectibleTokenURI( - '0x0000000000000000000000000000000000000000', - '0', - ); - expect(tokenId).toStrictEqual(''); + const result = async () => { + await assetsContract.getCollectibleTokenURI( + '0x0000000000000000000000000000000000000000', + '0', + ); + }; + + const error = 'Contract does not support ERC721 metadata interface.'; + await expect(result).rejects.toThrow(error); }); it('should get ERC-721 collectible name', async () => { diff --git a/src/assets/AssetsDetectionController.test.ts b/src/assets/AssetsDetectionController.test.ts index 7cf2b3cd6b9..b79ae1004bb 100644 --- a/src/assets/AssetsDetectionController.test.ts +++ b/src/assets/AssetsDetectionController.test.ts @@ -447,6 +447,7 @@ describe('AssetsDetectionController', () => { description: 'Description 2573', image: 'image/2573.png', name: 'ID 2573', + standard: 'ERC721', }, ); await assetsDetection.detectCollectibles(); @@ -456,6 +457,7 @@ describe('AssetsDetectionController', () => { description: 'Description 2573', image: 'image/2573.png', name: 'ID 2573', + standard: 'ERC721', tokenId: '2573', }, { diff --git a/src/assets/CollectibleStandards/ERC721/ERC721Standard.ts b/src/assets/CollectibleStandards/ERC721/ERC721Standard.ts index 657e0ae66de..e530467c03a 100644 --- a/src/assets/CollectibleStandards/ERC721/ERC721Standard.ts +++ b/src/assets/CollectibleStandards/ERC721/ERC721Standard.ts @@ -76,7 +76,7 @@ export class ERC721Standard { contract, ); if (!supportsMetadata) { - return ''; + throw new Error('Contract does not support ERC721 metadata interface.'); } return new Promise((resolve, reject) => { contract.tokenURI(tokenId, (error: Error, result: string) => { diff --git a/src/assets/CollectiblesController.test.ts b/src/assets/CollectiblesController.test.ts index 545f8451ee5..42b7ab088f9 100644 --- a/src/assets/CollectiblesController.test.ts +++ b/src/assets/CollectiblesController.test.ts @@ -9,21 +9,29 @@ import { import { AssetsContractController } from './AssetsContractController'; import { CollectiblesController } from './CollectiblesController'; -const ERC721_KUDOSADDRESS = '0x2aea4add166ebf38b63d09a75de1a7b94aa24163'; const CRYPTOPUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'; +const ERC721_KUDOSADDRESS = '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163'; const ERC721_COLLECTIBLE_ADDRESS = '0x60f80121c31a0d46b5279700f9df786054aa5ee5'; const ERC721_COLLECTIBLE_ID = '1144858'; const ERC1155_COLLECTIBLE_ADDRESS = - '0x495f947276749ce646f68ac8c248420045cb7b5e'; + '0x495f947276749Ce646f68AC8c248420045cb7b5e'; const ERC1155_COLLECTIBLE_ID = '40815311521795738946686668571398122012172359753720345430028676522525371400193'; +const ERC1155_DEPRESSIONIST_ADDRESS = + '0x18e8e76aeb9e2d9fa2a2b88dd9cf3c8ed45c3660'; +const ERC1155_DEPRESSIONIST_ID = '36'; const OWNER_ADDRESS = '0x5a3CA5cD63807Ce5e4d7841AB32Ce6B6d9BbBa2D'; const MAINNET_PROVIDER = new HttpProvider( 'https://mainnet.infura.io/v3/341eacb578dd44a1a049cbc5f6fd4035', ); + const OPEN_SEA_HOST = 'https://api.opensea.io'; const OPEN_SEA_PATH = '/api/v1'; +const CLOUDFARE_PATH = 'https://cloudflare-ipfs.com/ipfs'; +const DEPRESSIONIST_IPFS_PATH = + '/QmVChNtStZfPyV8JfKpube3eigQh5rUXqYchPgLc91tWLJ'; + describe('CollectiblesController', () => { let collectiblesController: CollectiblesController; let preferences: PreferencesController; @@ -73,6 +81,7 @@ describe('CollectiblesController', () => { .reply(200, { description: 'Description', image_original_url: 'url', + image_url: 'url', name: 'Name', asset_contract: { schema_name: 'ERC1155', @@ -86,9 +95,9 @@ describe('CollectiblesController', () => { `${OPEN_SEA_PATH}/asset/0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163/1203`, ) .reply(200, { - description: 'Kudos Description', image_original_url: 'Kudos url', name: 'Kudos Name', + description: 'Kudos Description', asset_contract: { schema_name: 'ERC721', }, @@ -119,9 +128,42 @@ describe('CollectiblesController', () => { nock('https://ipfs.gitcoin.co:443') .get('/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov') .reply(200, { - image: 'Kudos Image', - name: 'Kudos Name', + image: 'Kudos Image (from uri)', + name: 'Kudos Name (from uri)', + description: 'Kudos Description (from uri)', }); + + nock(OPEN_SEA_HOST) + .get( + '/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x5a3ca5cd63807ce5e4d7841ab32ce6b6d9bbba2d000000000000010000000001', + ) + .reply(200, { + name: 'name (from contract uri)', + description: null, + external_link: null, + image: 'image (from contract uri)', + animation_url: null, + }); + + nock(OPEN_SEA_HOST) + .get( + '/api/v1/asset/0x495f947276749Ce646f68AC8c248420045cb7b5e/40815311521795738946686668571398122012172359753720345430028676522525371400193', + ) + .reply(200, { + num_sales: 1, + image_original_url: 'image.uri', + name: 'name', + image: 'image', + description: 'description', + asset_contract: { schema_name: 'ERC1155' }, + collection: { name: 'collection', image_uri: 'collection.uri' }, + }); + + nock(CLOUDFARE_PATH).get(DEPRESSIONIST_IPFS_PATH).reply(200, { + name: 'name', + image: 'image', + description: 'description', + }); }); afterEach(() => { @@ -144,6 +186,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles[0]).toStrictEqual({ @@ -152,6 +195,7 @@ describe('CollectiblesController', () => { image: 'image', name: 'name', tokenId: '1', + standard: 'standard', }); expect(collectiblesController.state.collectibleContracts[0]).toStrictEqual({ @@ -169,6 +213,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles[0]).toStrictEqual({ @@ -176,6 +221,7 @@ describe('CollectiblesController', () => { description: 'description', image: 'image', name: 'name', + standard: 'standard', tokenId: '1', }); @@ -183,6 +229,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image-updated', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles[0]).toStrictEqual({ @@ -191,6 +238,7 @@ describe('CollectiblesController', () => { image: 'image-updated', name: 'name', tokenId: '1', + standard: 'standard', }); }); @@ -199,12 +247,14 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); await collectiblesController.addCollectible('0x01', '1', { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles).toHaveLength(1); expect(collectiblesController.state.collectibleContracts).toHaveLength(1); @@ -215,12 +265,14 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); await collectiblesController.addCollectible('0x01', '2', { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles).toHaveLength(2); expect(collectiblesController.state.collectibleContracts).toHaveLength(1); @@ -232,6 +284,7 @@ describe('CollectiblesController', () => { address: '0x01', description: 'Description', imageOriginal: 'url', + image: 'url', name: 'Name', standard: 'ERC1155', tokenId: '1', @@ -240,8 +293,60 @@ describe('CollectiblesController', () => { }); }); - it('should add collectible and get collectible contract information from contract', async () => { + it('should add collectible erc1155 and get collectible contract information from contract', async () => { assetsContract.configure({ provider: MAINNET_PROVIDER }); + await collectiblesController.addCollectible( + ERC1155_COLLECTIBLE_ADDRESS, + ERC1155_COLLECTIBLE_ID, + ); + + expect(collectiblesController.state.collectibles[0]).toStrictEqual({ + address: ERC1155_COLLECTIBLE_ADDRESS, + image: 'image (from contract uri)', + name: 'name (from contract uri)', + description: 'description', + tokenId: + '40815311521795738946686668571398122012172359753720345430028676522525371400193', + collectionName: 'collection', + imageOriginal: 'image.uri', + numberOfSales: 1, + standard: 'ERC1155', + }); + }); + + it('should add collectible erc721 and get collectible contract information from contract and OpenSea', async () => { + assetsContract.configure({ provider: MAINNET_PROVIDER }); + + sandbox + .stub( + collectiblesController, + 'getCollectibleContractInformationFromApi' as any, + ) + .returns(undefined); + + await collectiblesController.addCollectible(ERC721_KUDOSADDRESS, '1203'); + expect(collectiblesController.state.collectibles[0]).toStrictEqual({ + address: ERC721_KUDOSADDRESS, + image: 'Kudos Image (from uri)', + name: 'Kudos Name (from uri)', + description: 'Kudos Description (from uri)', + tokenId: '1203', + collectionImage: 'collection.url', + collectionName: 'Collection Name', + imageOriginal: 'Kudos url', + standard: 'ERC721', + }); + + expect(collectiblesController.state.collectibleContracts[0]).toStrictEqual({ + address: ERC721_KUDOSADDRESS, + name: 'KudosToken', + symbol: 'KDO', + }); + }); + + it('should add collectible erc721 and get collectible contract information only from contract', async () => { + assetsContract.configure({ provider: MAINNET_PROVIDER }); + sandbox .stub( collectiblesController, @@ -254,15 +359,16 @@ describe('CollectiblesController', () => { .returns(undefined); await collectiblesController.addCollectible(ERC721_KUDOSADDRESS, '1203'); expect(collectiblesController.state.collectibles[0]).toStrictEqual({ - address: '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163', - image: 'Kudos Image', - name: 'Kudos Name', + address: ERC721_KUDOSADDRESS, + image: 'Kudos Image (from uri)', + name: 'Kudos Name (from uri)', + description: 'Kudos Description (from uri)', tokenId: '1203', standard: 'ERC721', }); expect(collectiblesController.state.collectibleContracts[0]).toStrictEqual({ - address: '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163', + address: ERC721_KUDOSADDRESS, name: 'KudosToken', symbol: 'KDO', }); @@ -335,7 +441,7 @@ describe('CollectiblesController', () => { expect(collectiblesController.state.collectibles).toStrictEqual([]); expect(collectiblesController.state.collectibleContracts).toStrictEqual([]); await collectiblesController.addCollectible( - '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163', + ERC721_KUDOSADDRESS, '1203', undefined, true, @@ -343,10 +449,11 @@ describe('CollectiblesController', () => { expect(collectiblesController.state.collectibles).toStrictEqual([ { - address: '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163', + address: ERC721_KUDOSADDRESS, description: 'Kudos Description', imageOriginal: 'Kudos url', name: 'Kudos Name', + image: null, standard: 'ERC721', tokenId: '1203', collectionImage: 'collection.url', @@ -356,7 +463,7 @@ describe('CollectiblesController', () => { expect(collectiblesController.state.collectibleContracts).toStrictEqual([ { - address: '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163', + address: ERC721_KUDOSADDRESS, description: 'Kudos Description', logo: 'Kudos url', name: 'Kudos', @@ -371,6 +478,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); collectiblesController.removeCollectible('0x01', '1'); expect(collectiblesController.state.collectibles).toHaveLength(0); @@ -382,12 +490,14 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); await collectiblesController.addCollectible('0x01', '2', { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); collectiblesController.removeCollectible('0x01', '1'); expect(collectiblesController.state.collectibles).toHaveLength(1); @@ -471,12 +581,14 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); await collectiblesController.addCollectible('0x01', '2', { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles).toHaveLength(2); @@ -490,6 +602,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles).toHaveLength(2); expect(collectiblesController.state.ignoredCollectibles).toHaveLength(1); @@ -504,6 +617,7 @@ describe('CollectiblesController', () => { name: 'name', image: 'image', description: 'description', + standard: 'standard', }); expect(collectiblesController.state.collectibles).toHaveLength(1); @@ -575,4 +689,27 @@ describe('CollectiblesController', () => { }; await expect(result).rejects.toThrow(error); }); + + it('should add collectible with metadata hosted in IPFS', async () => { + assetsContract.configure({ provider: MAINNET_PROVIDER }); + await collectiblesController.addCollectible( + ERC1155_DEPRESSIONIST_ADDRESS, + ERC1155_DEPRESSIONIST_ID, + ); + + expect(collectiblesController.state.collectibleContracts[0]).toStrictEqual({ + address: '0x18E8E76aeB9E2d9FA2A2b88DD9CF3C8ED45c3660', + name: "Maltjik.jpg's Depressionists", + symbol: 'DPNS', + }); + + expect(collectiblesController.state.collectibles[0]).toStrictEqual({ + address: '0x18E8E76aeB9E2d9FA2A2b88DD9CF3C8ED45c3660', + tokenId: '36', + image: 'image', + name: 'name', + description: 'description', + standard: 'ERC721', + }); + }); }); diff --git a/src/assets/CollectiblesController.ts b/src/assets/CollectiblesController.ts index 71bdd969987..1656e4869c0 100644 --- a/src/assets/CollectiblesController.ts +++ b/src/assets/CollectiblesController.ts @@ -1,10 +1,23 @@ import { EventEmitter } from 'events'; +import { BN, stripHexPrefix } from 'ethereumjs-util'; import { Mutex } from 'async-mutex'; import { BaseController, BaseConfig, BaseState } from '../BaseController'; import type { PreferencesState } from '../user/PreferencesController'; import type { NetworkState, NetworkType } from '../network/NetworkController'; -import { safelyExecute, handleFetch, toChecksumHexAddress } from '../util'; -import { MAINNET, RINKEBY_CHAIN_ID, ERC721, ERC1155 } from '../constants'; +import { + safelyExecute, + handleFetch, + toChecksumHexAddress, + BNToHex, + getIpfsUrlContentIdentifier, +} from '../util'; +import { + MAINNET, + RINKEBY_CHAIN_ID, + IPFS_DEFAULT_GATEWAY_URL, + ERC721, + ERC1155, +} from '../constants'; import type { ApiCollectible, ApiCollectibleCreator, @@ -87,11 +100,12 @@ export interface CollectibleContract { * @property collectionImage - The image URI of the collectible collection. */ export interface CollectibleMetadata { - name?: string; - description?: string; + name: string | null; + description: string | null; + image: string | null; + standard: string | null; numberOfSales?: number; backgroundColor?: string; - image?: string; imagePreview?: string; imageThumbnail?: string; imageOriginal?: string; @@ -100,7 +114,6 @@ export interface CollectibleMetadata { externalLink?: string; creator?: ApiCollectibleCreator; lastSale?: ApiCollectibleLastSale; - standard?: string; collectionName?: string; collectionImage?: string; } @@ -180,6 +193,7 @@ export class CollectiblesController extends BaseController< ): Promise { const tokenURI = this.getCollectibleApi(contractAddress, tokenId); let collectibleInformation: ApiCollectible; + /* istanbul ignore if */ if (this.openSeaApiKey) { collectibleInformation = await handleFetch(tokenURI, { @@ -188,6 +202,7 @@ export class CollectiblesController extends BaseController< } else { collectibleInformation = await handleFetch(tokenURI); } + const { num_sales, background_color, @@ -209,10 +224,10 @@ export class CollectiblesController extends BaseController< /* istanbul ignore next */ const collectibleMetadata: CollectibleMetadata = Object.assign( {}, - { name }, + { name: name || null }, + { description: description || null }, + { image: image_url || null }, creator && { creator }, - description && { description }, - image_url && { image: image_url }, num_sales && { numberOfSales: num_sales }, background_color && { backgroundColor: background_color }, image_preview_url && { imagePreview: image_preview_url }, @@ -243,24 +258,86 @@ export class CollectiblesController extends BaseController< contractAddress: string, tokenId: string, ): Promise { - const tokenURI = await this.getCollectibleTokenURI( + const result = await this.getCollectibleURIAndStandard( contractAddress, tokenId, ); - const standard = await this.getCollectibleStandard( - contractAddress, - tokenId, - ); - const object = await handleFetch(tokenURI); - const image = Object.prototype.hasOwnProperty.call(object, 'image') - ? 'image' - : /* istanbul ignore next */ 'image_url'; + let tokenURI = result[0]; + const standard = result[1]; - if (standard) { - return { image: object[image], name: object.name, standard }; + if (tokenURI.startsWith('ipfs://')) { + const contentId = getIpfsUrlContentIdentifier(tokenURI); + tokenURI = IPFS_DEFAULT_GATEWAY_URL + contentId; } - return { image: object[image], name: object.name }; + try { + const object = await handleFetch(tokenURI); + // TODO: Check image_url existence. This is not part of EIP721 nor EIP1155 + const image = Object.prototype.hasOwnProperty.call(object, 'image') + ? 'image' + : /* istanbul ignore next */ 'image_url'; + + return { + image: object[image], + name: object.name, + description: object.description, + standard, + }; + } catch { + return { + image: null, + name: null, + description: null, + standard: standard || null, + }; + } + } + + /** + * Retrieve collectible uri with metadata. TODO Update method to use IPFS. + * + * @param contractAddress - Collectible contract address. + * @param tokenId - Collectible token id. + * @returns Promise resolving collectible uri and token standard. + */ + private async getCollectibleURIAndStandard( + contractAddress: string, + tokenId: string, + ): Promise<[string, string]> { + // try ERC721 uri + try { + const uri = await this.getCollectibleTokenURI(contractAddress, tokenId); + return [uri, ERC721]; + } catch { + // Ignore error + } + + // try ERC1155 uri + try { + const tokenURI = await this.uriERC1155Collectible( + contractAddress, + tokenId, + ); + + /** + * According to EIP1155 the URI value allows for ID substitution + * in case the string `{id}` exists. + * https://eips.ethereum.org/EIPS/eip-1155#metadata + */ + + if (!tokenURI.includes('{id}')) { + return [tokenURI, ERC1155]; + } + + const hexTokenId = stripHexPrefix(BNToHex(new BN(tokenId))) + .padStart(64, '0') + .toLowerCase(); + return [tokenURI.replace('{id}', hexTokenId), ERC1155]; + } catch { + // Ignore error + } + + return ['', '']; } /** @@ -274,34 +351,29 @@ export class CollectiblesController extends BaseController< contractAddress: string, tokenId: string, ): Promise { - let information; - - // First try with OpenSea - information = await safelyExecute(async () => { - return await this.getCollectibleInformationFromApi( + const blockchainMetadata = await safelyExecute(async () => { + return await this.getCollectibleInformationFromTokenURI( contractAddress, tokenId, ); }); - if (information) { - return information; - } - - // Then following ERC721 standard - information = await safelyExecute(async () => { - return await this.getCollectibleInformationFromTokenURI( + const openSeaMetadata = await safelyExecute(async () => { + return await this.getCollectibleInformationFromApi( contractAddress, tokenId, ); }); - /* istanbul ignore next */ - if (information) { - return information; - } - /* istanbul ignore next */ - return {}; + return { + ...openSeaMetadata, + name: blockchainMetadata.name ?? openSeaMetadata?.name ?? null, + description: + blockchainMetadata.description ?? openSeaMetadata?.description ?? null, + image: blockchainMetadata.image ?? openSeaMetadata?.image ?? null, + standard: + blockchainMetadata.standard ?? openSeaMetadata?.standard ?? null, + }; } /** @@ -334,20 +406,13 @@ export class CollectiblesController extends BaseController< */ private async getCollectibleContractInformationFromContract( contractAddress: string, - ): Promise { + ): Promise> { const name = await this.getAssetName(contractAddress); const symbol = await this.getAssetSymbol(contractAddress); return { name, symbol, address: contractAddress, - asset_contract_type: null, - created_date: null, - schema_name: null, - total_supply: null, - description: null, - external_link: null, - image_url: null, }; } @@ -360,28 +425,22 @@ export class CollectiblesController extends BaseController< private async getCollectibleContractInformation( contractAddress: string, ): Promise { - let information; - // First try with OpenSea - information = await safelyExecute(async () => { - return await this.getCollectibleContractInformationFromApi( + const blockchainContractData = await safelyExecute(async () => { + return await this.getCollectibleContractInformationFromContract( contractAddress, ); }); - if (information) { - return information; - } - - // Then following ERC721 standard - information = await safelyExecute(async () => { - return await this.getCollectibleContractInformationFromContract( + const openSeaContractData = await safelyExecute(async () => { + return await this.getCollectibleContractInformationFromApi( contractAddress, ); }); - if (information) { - return information; + if (blockchainContractData || openSeaContractData) { + return { ...openSeaContractData, ...blockchainContractData }; } + /* istanbul ignore next */ return { address: contractAddress, @@ -410,6 +469,7 @@ export class CollectiblesController extends BaseController< tokenId: string, collectibleMetadata: CollectibleMetadata, ): Promise { + // TODO: Remove unused return const releaseLock = await this.mutex.acquire(); try { address = toChecksumHexAddress(address); @@ -427,6 +487,7 @@ export class CollectiblesController extends BaseController< existingEntry, ); if (differentMetadata) { + // TODO: Switch to indexToUpdate const indexToRemove = collectibles.findIndex( (collectible) => collectible.address.toLowerCase() === address.toLowerCase() && @@ -492,6 +553,7 @@ export class CollectiblesController extends BaseController< const contractInformation = await this.getCollectibleContractInformation( address, ); + const { asset_contract_type, created_date, @@ -511,6 +573,7 @@ export class CollectiblesController extends BaseController< ) { return collectibleContracts; } + /* istanbul ignore next */ const newEntry: CollectibleContract = Object.assign( {}, @@ -519,7 +582,8 @@ export class CollectiblesController extends BaseController< name && { name }, image_url && { logo: image_url }, symbol && { symbol }, - total_supply !== null && { totalSupply: total_supply }, + total_supply !== null && + typeof total_supply !== 'undefined' && { totalSupply: total_supply }, asset_contract_type && { assetContractType: asset_contract_type }, created_date && { createdDate: created_date }, schema_name && { schemaName: schema_name }, @@ -653,34 +717,6 @@ export class CollectiblesController extends BaseController< return newCollectibleContracts; } - /** - * Method to verify the token standard by querying the metadata uri form the contract. - * - * @param address - Collectible asset contract address. - * @param tokenId - Collectible asset identifier. - * @returns Promise resolving the token standard. - */ - private async getCollectibleStandard( - address: string, - tokenId: string, - ): Promise { - try { - await this.getCollectibleTokenURI(address, tokenId); - return ERC721; - } catch { - // Ignore error - } - - try { - await this.uriERC1155Collectible(address, tokenId); - return ERC1155; - } catch { - // Ignore error - } - - return ''; - } - /** * EventEmitter instance used to listen to specific EIP747 events */ @@ -716,10 +752,10 @@ export class CollectiblesController extends BaseController< * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. * @param options.getAssetName - Gets the name of the asset at the given address. * @param options.getAssetSymbol - Gets the symbol of the asset at the given address. - * @param options.getCollectibleTokenURI - Gets the URI of the NFT at the given address, with the given ID. + * @param options.getCollectibleTokenURI - Gets the URI of the ERC721 token at the given address, with the given ID. * @param options.getOwnerOf - Get the owner of a ERC-721 collectible. * @param options.balanceOfERC1155Collectible - Gets balance of a ERC-1155 collectible. - * @param options.uriERC1155Collectible - Gets uri for ERC-1155 metadata. + * @param options.uriERC1155Collectible - Gets the URI of the ERC1155 token at the given address, with the given ID. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ diff --git a/src/assets/assetsUtil.test.ts b/src/assets/assetsUtil.test.ts index a3b29d16a87..103093f4d7a 100644 --- a/src/assets/assetsUtil.test.ts +++ b/src/assets/assetsUtil.test.ts @@ -5,7 +5,10 @@ describe('assetsUtil', () => { describe('compareCollectiblesMetadata', () => { it('should resolve true if any key is different', () => { const collectibleMetadata: CollectibleMetadata = { + name: 'name', image: 'image', + description: 'description', + standard: 'standard', backgroundColor: 'backgroundColor', imagePreview: 'imagePreview', imageThumbnail: 'imageThumbnail', @@ -19,6 +22,8 @@ describe('assetsUtil', () => { tokenId: '123', name: 'name', image: 'image', + description: 'description', + standard: 'standard', backgroundColor: 'backgroundColor', imagePreview: 'imagePreview', imageThumbnail: 'imageThumbnail', @@ -36,7 +41,10 @@ describe('assetsUtil', () => { it('should resolve true if any key is different as always as metadata is not undefined', () => { const collectibleMetadata: CollectibleMetadata = { + name: 'name', image: 'image', + description: 'description', + standard: 'standard', externalLink: 'externalLink', }; const collectible: Collectible = { @@ -44,6 +52,8 @@ describe('assetsUtil', () => { tokenId: '123', name: 'name', image: 'image', + standard: 'standard', + description: 'description', backgroundColor: 'backgroundColor', externalLink: 'externalLink', }; @@ -56,7 +66,10 @@ describe('assetsUtil', () => { it('should resolve false if no key is different', () => { const collectibleMetadata: CollectibleMetadata = { + name: 'name', image: 'image', + description: 'description', + standard: 'standard', backgroundColor: 'backgroundColor', imagePreview: 'imagePreview', imageThumbnail: 'imageThumbnail', @@ -70,6 +83,8 @@ describe('assetsUtil', () => { tokenId: '123', name: 'name', image: 'image', + standard: 'standard', + description: 'description', backgroundColor: 'backgroundColor', imagePreview: 'imagePreview', imageThumbnail: 'imageThumbnail', diff --git a/src/constants.ts b/src/constants.ts index 29b4a8a3711..07bf5f19149 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ export const MAINNET = 'mainnet'; export const RPC = 'rpc'; export const FALL_BACK_VS_CURRENCY = 'ETH'; +export const IPFS_DEFAULT_GATEWAY_URL = 'https://cloudflare-ipfs.com/ipfs/'; // NETWORKS ID export const RINKEBY_CHAIN_ID = '4'; diff --git a/src/util.test.ts b/src/util.test.ts index 89122171672..e052ea49f95 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -14,6 +14,9 @@ const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0'; const SOME_API = 'https://someapi.com'; const SOME_FAILING_API = 'https://somefailingapi.com'; +const DEFAULT_IPFS_URL = 'ipfs://0001'; +const ALTERNATIVE_IPFS_URL = 'ipfs://ipfs/0001'; + const MAX_FEE_PER_GAS = 'maxFeePerGas'; const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas'; const GAS_PRICE = 'gasPrice'; @@ -1064,4 +1067,24 @@ describe('util', () => { ).not.toThrow(Error); }); }); + + describe('getIpfsUrlContentIdentifier', () => { + it('should return content identifier from default ipfs url', () => { + expect(util.getIpfsUrlContentIdentifier(DEFAULT_IPFS_URL)).toStrictEqual( + '0001', + ); + }); + + it('should return content identifier from alternative ipfs url', () => { + expect( + util.getIpfsUrlContentIdentifier(ALTERNATIVE_IPFS_URL), + ).toStrictEqual('0001'); + }); + + it('should return url if its not a ipfs standard url', () => { + expect(util.getIpfsUrlContentIdentifier(SOME_API)).toStrictEqual( + SOME_API, + ); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 56ec3fccbe5..905f6820bc0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -767,3 +767,21 @@ export function validateMinimumIncrease(proposed: string, min: string) { const errorMsg = `The proposed value: ${proposedDecimal} should meet or exceed the minimum value: ${minDecimal}`; throw new Error(errorMsg); } + +/** + * Extracts content identifier from ipfs url. + * + * @param url - Ipfs url. + * @returns Ipfs content identifier as string. + */ +export function getIpfsUrlContentIdentifier(url: string): string { + if (url.startsWith('ipfs://ipfs/')) { + return url.replace('ipfs://ipfs/', ''); + } + + if (url.startsWith('ipfs://')) { + return url.replace('ipfs://', ''); + } + + return url; +}