diff --git a/src/abis/MetaABI.json b/src/abis/MetaABI.json index 80cf042f..5362bcc1 100644 --- a/src/abis/MetaABI.json +++ b/src/abis/MetaABI.json @@ -26,6 +26,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "contractURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -307,4 +320,4 @@ "type": "event" } ] -} \ No newline at end of file +} diff --git a/src/clients/evm.ts b/src/clients/evm.ts index 9e35bd3a..90521f50 100644 --- a/src/clients/evm.ts +++ b/src/clients/evm.ts @@ -43,6 +43,7 @@ export function getEVMClient(chainId: ChainId | string, clients: Clients): EVMCl } export type EVMClient = { + provider: JsonRpcProvider; call: (ethCalls: EVMCall[]) => Promise; getEventsFrom: (fromBlock: string, toBlock: string, contractFilters: ContractFilter[]) => Promise; getBlockTimestamps: (blockHashes: string[]) => Promise; @@ -107,6 +108,7 @@ const init = async (providerUrl: string): Promise => { const ethcallProvider = new Provider(); await ethcallProvider.init(provider); return { + provider, call: async (ethCalls: EVMCall[]) => { const calls = ethCalls.map(ethCall => { const contract = new Contract(ethCall.contractAddress, MetaABI.abi); diff --git a/src/processors/default/addMetadataObject.ts b/src/processors/default/addMetadataObject.ts index 20e9a641..5a2ec353 100644 --- a/src/processors/default/addMetadataObject.ts +++ b/src/processors/default/addMetadataObject.ts @@ -3,63 +3,38 @@ import { Axios } from 'axios'; import { IPFSClient } from '../../clients/ipfs'; import { Table } from '../../db/db'; import { missingMetadataObject } from '../../triggers/missing'; -import { getMetadataURL } from '../../types/metadata'; -import { NFT, NftFactory } from '../../types/nft'; +import { getMetadataFromURI } from '../../types/metadata'; +import { getNFTMetadataURL, NFT, NftFactory } from '../../types/nft'; import { Clients, Processor } from '../../types/processor'; import { rollPromises } from '../../utils/rollingPromises'; -import { cleanURL } from '../../utils/sanitizers'; const name = 'addMetadataObject'; -const getMetadataObject = async (nft: NFT, timeout: number, axios: Axios, ipfs: IPFSClient, erc721ContractsByAddress: { [key: string]: NftFactory }): Promise<{ +const getNFTMetadata = async (nft: NFT, timeout: number, axios: Axios, ipfs: IPFSClient, erc721ContractsByAddress: { [key: string]: NftFactory }): Promise<{ metadata?: any, metadataError?: any }> => { const address = nft.contractAddress; - const contract = erc721ContractsByAddress[address]; - const contractTypeName = contract?.contractType; + const nftFactory = erc721ContractsByAddress[address]; - const metadataURL = getMetadataURL(nft, contractTypeName); - if (!metadataURL) { + const uri = await getNFTMetadataURL(nft, nftFactory, ipfs); + if (!uri) { return Promise.reject({ message: `Metadata metadataURL missing` }); } - if (metadataURL.startsWith('data:application/json;base64,')){ - try { - const base64 = metadataURL.substring(metadataURL.indexOf(',') + 1); - const data = Buffer.from(base64, 'base64').toString('utf-8') - const metadata = JSON.parse(data); - return { metadata }; - } catch (e: any){ - return { - metadataError: e.toString() - } - } - } - - let queryURL = cleanURL(metadataURL); - if (nft.metadataIPFSHash) { - queryURL = ipfs.getHTTPURL(nft.metadataIPFSHash); - } - - console.info(`Querying for metadata for nft id ${nft.id}: ${queryURL}`) - try { - const response = await axios.get(queryURL, { timeout }); - return { - metadata: response.data - } - } catch (e: any){ - return { - metadataError: e.toString() - } - } + console.info(`Querying for metadata for nft id ${nft.id}: ${uri}`) + const response = await getMetadataFromURI(uri, axios, timeout); + return { + metadata: response.data, + metadataError: response.error + }; } const processorFunction = (erc721ContractsByAddress: { [key: string]: NftFactory }) => async (batch: NFT[], clients: Clients) => { const processMetadataResponse = (nft: NFT) => - getMetadataObject(nft, parseInt(process.env.METADATA_REQUEST_TIMEOUT!), clients.axios, clients.ipfs, erc721ContractsByAddress); + getNFTMetadata(nft, parseInt(process.env.METADATA_REQUEST_TIMEOUT!), clients.axios, clients.ipfs, erc721ContractsByAddress); const results = await rollPromises(batch, processMetadataResponse); diff --git a/src/types/metaFactory-types/zoraDropCreator.ts b/src/types/metaFactory-types/zoraDropCreator.ts index 9f1e2603..7b957176 100644 --- a/src/types/metaFactory-types/zoraDropCreator.ts +++ b/src/types/metaFactory-types/zoraDropCreator.ts @@ -1,29 +1,70 @@ + +import { ethers } from 'ethers'; + +import MetaABI from '../../abis/MetaABI.json'; +import { getMetadataFromURI } from '../../processors/default/addMetadataObject'; import { artistId } from '../../utils/identifiers'; import { formatAddress } from '../address'; import { getFactoryId } from '../chain'; import { MetaFactoryType } from '../metaFactory'; import { NFTContractTypeName, NFTStandard } from '../nft'; +type CreatedDropEventWithMetadata = { + creator: string; + address: string; + artistName: string; + contractURI: string; + animationURL?: string; + animationMimeType?: string; +} + const type: MetaFactoryType = { newContractCreatedEvent: 'CreatedDrop', - creationMetadataToNftFactory: (event: any, autoApprove: boolean, metaFactory) => ({ - id: getFactoryId(metaFactory.chainId, event.args!.editionContractAddress), - address: formatAddress(event.args!.editionContractAddress), + metadataAPI: async (events, clients, metaFactory) => { + if (events.length === 0){ + return {} + } + + const eventMetadatas: CreatedDropEventWithMetadata[] = []; + for (const event of events) { + const creator = (event as any).args!.creator; + const address = formatAddress((event as any).args!.editionContractAddress); + const contract = new ethers.Contract(address, MetaABI.abi, clients.evmChain[metaFactory.chainId].provider); + const creatorENS = await Promise.reject(); // todo - get ens from creator from ens PR; + const artistName = creatorENS || formatAddress(event.args!.creator); + const contractURI = await contract.contractURI(); + const metadataResponse = await getMetadataFromURI(contractURI, clients.axios, parseInt(process.env.METADATA_REQUEST_TIMEOUT!)); + let animationURL, animationMimeType; + if (metadataResponse.data) { + animationURL = metadataResponse.data.metadata.animationURL; + animationMimeType = await Promise.reject(); // todo - use mime call from mimetype PR + } + eventMetadatas.push({ + creator, address, artistName, contractURI, animationURL, animationMimeType + }); + } + + return eventMetadatas; + }, + creationMetadataToNftFactory: (event: any, autoApprove: boolean, metaFactory, + factoryMetadata: CreatedDropEventWithMetadata) => ({ + id: getFactoryId(metaFactory.chainId, factoryMetadata.address), + address: factoryMetadata.address, platformId: 'zora', chainId: metaFactory.chainId, startingBlock: event.blockNumber, contractType: NFTContractTypeName.default, standard: NFTStandard.ERC721, - autoApprove, - approved: autoApprove, + autoApprove: isAudio(factoryMetadata.animationMimeType), //todo - use isAudio from mimetype PR + approved: isAudio(factoryMetadata.animationMimeType), //todo - use isAudio from mimetype PR typeMetadata: { overrides: { artist: { artistId: artistId(metaFactory.chainId, event.args!.creator), - name: formatAddress(event.args!.creator), + name: factoryMetadata.artistName, }, track: { - websiteUrl: `https://create.zora.co/editions/${formatAddress(event.args!.editionContractAddress)}` + websiteUrl: `https://create.zora.co/editions/${factoryMetadata.address}` } } } diff --git a/src/types/metadata.ts b/src/types/metadata.ts index f5ec5841..7a6e8a33 100644 --- a/src/types/metadata.ts +++ b/src/types/metadata.ts @@ -1,6 +1,38 @@ +import { Axios } from 'axios'; + +import { cleanURL } from '../utils/sanitizers'; + import { NFT, NFTContractTypeName } from './nft'; +export const getMetadataFromURI = async (uri: string, axios: Axios, timeout: number) => { + if (uri.startsWith('data:application/json;base64,')) { + try { + const base64 = uri.substring(uri.indexOf(',') + 1); + const data = Buffer.from(base64, 'base64').toString('utf-8') + const metadata = JSON.parse(data); + return { data: metadata }; + } catch (e: any){ + return { + error: e.toString() + } + } + } + + const queryURL = cleanURL(uri); + try { + const response = await axios.get(queryURL, { timeout }); + return { + data: response.data + } + } catch (e: any){ + return { + error: e.toString() + } + } + +} + export const getMetadataURL = (nft: NFT, contractTypeName?: NFTContractTypeName): (string | null | undefined) => { if (contractTypeName === 'zora') { return nft.tokenMetadataURI diff --git a/src/types/nft.ts b/src/types/nft.ts index 6fc09153..81527a9b 100644 --- a/src/types/nft.ts +++ b/src/types/nft.ts @@ -1,10 +1,12 @@ import { ValidContractNFTCallFunction } from '../clients/evm'; +import { IPFSClient } from '../clients/ipfs'; import { ArtistProfile } from './artist'; import { ChainId } from './chain'; import { Contract } from './contract'; import { ExtractorTypes } from './fieldExtractor'; import { LensMediaMetadata, MimeEnum } from './media'; +import { getMetadataURL } from './metadata'; import { NFTFactoryTypes } from './nftFactory'; import { MusicPlatformType } from './platform'; import { Record } from './record'; @@ -103,6 +105,15 @@ export const getNFTMetadataField = (nft: NFT, field: string) => { return nft.metadata[field]; } +export const getNFTMetadataURL = async (nft: NFT, nftFactory: NftFactory, ipfs: IPFSClient) => { + if (nft.metadataIPFSHash) { + return ipfs.getHTTPURL(nft.metadataIPFSHash); + } else { + const contractTypeName = nftFactory?.contractType; + return getMetadataURL(nft, contractTypeName); + } +} + export const getTrait = (nft: NFT, type: string) => { if (!nft.metadata) { console.error({ nft })