Skip to content

Commit

Permalink
feat: add mimeType to metadata object & handle for custom IPFS baseURL
Browse files Browse the repository at this point in the history
  • Loading branch information
ethandaya committed Jul 3, 2021
1 parent d252922 commit b390e08
Show file tree
Hide file tree
Showing 21 changed files with 150 additions and 66 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zoralabs/nft-metadata",
"version": "0.0.1",
"version": "0.0.2",
"description": "generic nft metadata parsers",
"author": "Zora",
"license": "MIT",
Expand All @@ -11,6 +11,10 @@
"files": [
"dist"
],
"dependencies": {
"cross-fetch": "^3.1.4",
"@zoralabs/core": "^1.0.4"
},
"devDependencies": {
"@ethersproject/address": "5.3.0",
"@ethersproject/contracts": "5.3.0",
Expand Down Expand Up @@ -63,8 +67,5 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"dependencies": {
"@zoralabs/core": "^1.0.4"
}
}
24 changes: 19 additions & 5 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { JsonRpcProvider } from '@ethersproject/providers'
import { getAddress } from '@ethersproject/address'
import { fetcherLookup } from './fetchers'
import { parserLookup } from './parsers'
import { IPFS_IO_GATEWAY } from './utils/ipfs'

export interface NftMetadata {
metadata: any
Expand All @@ -11,15 +12,23 @@ export interface NftMetadata {

ownerAddress: string

tokenURI: string
contentURI: string
tokenURL: string

contentURL: string
contentURLMimeType: string

previewURL?: string
previewURLMimeType?: string

externalLink?: string
attributes?: Record<string, any>[]
}

export class Parser {
constructor(private readonly provider: JsonRpcProvider) {}
constructor(
private readonly provider: JsonRpcProvider,
private readonly ipfsBaseURL: string = IPFS_IO_GATEWAY,
) {}

public async fetchUnderlyingContractData(
contractAddress: string,
Expand All @@ -35,7 +44,13 @@ export class Parser {
tokenURI: string,
) {
const parser = parserLookup(contractAddress)
return parser(this.provider, contractAddress, tokenId, tokenURI)
return parser(
this.provider,
this.ipfsBaseURL,
contractAddress,
tokenId,
tokenURI,
)
}

public async fetchAndParseTokenMeta(
Expand All @@ -56,7 +71,6 @@ export class Parser {
)

return {
tokenURI,
ownerAddress,
...parsedMetadata,
}
Expand Down
2 changes: 2 additions & 0 deletions src/parsers/artblocksMetadataParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { parseGenericMetadata } from './openseaMetadataParser'

export async function parseArtblocksMetadata(
provider: JsonRpcProvider,
ipfsBaseURL: string,
contractAddress: string,
tokenId: string,
tokenURI: string,
) {
const baseMeta = await parseGenericMetadata(
provider,
ipfsBaseURL,
contractAddress,
tokenId,
tokenURI,
Expand Down
2 changes: 2 additions & 0 deletions src/parsers/hashmasksMetadataParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { parseGenericMetadata } from './openseaMetadataParser'

export async function parseHashmasksMetadata(
provider: JsonRpcProvider,
ipfsBaseURL: string,
contractAddress: string,
tokenId: string,
tokenURI: string,
) {
const baseMeta = await parseGenericMetadata(
provider,
ipfsBaseURL,
contractAddress,
tokenId,
tokenURI,
Expand Down
35 changes: 26 additions & 9 deletions src/parsers/makersplaceMetadataParser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { getIPFSUrl } from '../utils/ipfs'
import { fetchMetadata } from '../utils/fetch'
import { fetchMetadata, fetchMimeType } from '../utils/fetch'

export async function parseMakersplaceMetadata(
_: JsonRpcProvider,
__: string,
___: string,
____: string,
tokenURI: string,
) {
const publicTokenURI = getIPFSUrl(
Expand All @@ -14,9 +15,16 @@ export async function parseMakersplaceMetadata(
)
const metadata = await fetchMetadata(tokenURI)

const imageURI = metadata.imageUrl
? getIPFSUrl(metadata.imageUrl, 'https://ipfsgateway.makersplace.com')
: null
if (!metadata.imageUrl) {
throw new Error(
`Invalid metadata required imageUrl key from metadata missing`,
)
}

const imageURI = getIPFSUrl(
metadata.imageUrl,
'https://ipfsgateway.makersplace.com',
)

const animationURI =
metadata?.properties?.preview_media_file2 &&
Expand All @@ -28,14 +36,23 @@ export async function parseMakersplaceMetadata(
: null

const { name, description, attributes } = metadata
const contentURI = animationURI || imageURI
const previewURI = imageURI && animationURI ? imageURI : undefined
const contentURL = animationURI || imageURI
const previewURL = imageURI && animationURI ? imageURI : undefined

const contentURLMimeType = animationURI
? 'video/mp4'
: await fetchMimeType(contentURL)
const previewURLMimeType = previewURL
? await fetchMimeType(previewURL)
: undefined

return {
metadata,
tokenURI: publicTokenURI,
contentURI,
...(previewURI && { previewURI }),
tokenURL: publicTokenURI,
contentURL,
contentURLMimeType,
...(previewURL && { previewURL }),
...(previewURLMimeType && { previewURLMimeType }),
...(name && { name }),
...(description && { description }),
...(attributes && { attributes }),
Expand Down
27 changes: 18 additions & 9 deletions src/parsers/openseaMetadataParser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { getIPFSUrl } from '../utils/ipfs'
import { fetchMetadata } from '../utils/fetch'
import { fetchMetadata, fetchMimeType } from '../utils/fetch'
import { NftMetadata } from '../parser'

export async function parseGenericMetadata(
_: JsonRpcProvider,
ipfsBaseURL: string,
__: string,
___: string,
tokenURI: string,
) {
): Promise<NftMetadata> {
const publicTokenURI = getIPFSUrl(tokenURI)
const metadata = await fetchMetadata(tokenURI)

Expand All @@ -17,20 +19,27 @@ export async function parseGenericMetadata(
)
}

const imageURI = getIPFSUrl(metadata.image)
const imageURI = getIPFSUrl(metadata.image, ipfsBaseURL)
const animationURI = metadata?.animation_url
? getIPFSUrl(metadata.animation_url)
? getIPFSUrl(metadata.animation_url, ipfsBaseURL)
: null

const { name, description, attributes, external_url: externalURL } = metadata
const contentURI = animationURI || imageURI
const previewURI = imageURI && animationURI ? imageURI : undefined
const contentURL = animationURI || imageURI
const previewURL = imageURI && animationURI ? imageURI : undefined

const contentURLMimeType = await fetchMimeType(contentURL)
const previewURLMimeType = previewURL
? await fetchMimeType(previewURL)
: undefined

return {
metadata,
tokenURI: publicTokenURI,
contentURI,
...(previewURI && { previewURI }),
tokenURL: publicTokenURI,
contentURL,
contentURLMimeType,
...(previewURL && { previewURL }),
...(previewURLMimeType && { previewURLMimeType }),
...(name && { name }),
...(description && { description }),
...(attributes && { attributes }),
Expand Down
16 changes: 11 additions & 5 deletions src/parsers/zoraMetadataParser.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { getIPFSUrl } from '../utils/ipfs'
import { MediaFactory } from '@zoralabs/core/dist/typechain'
import { fetchMetadata } from '../utils/fetch'
import { fetchMetadata, fetchMimeType } from '../utils/fetch'

export async function parseZoraMetadata(
provider: JsonRpcProvider,
ipfsBaseURL: string,
contractAddress: string,
tokenId: string,
tokenURI: string,
) {
const publicTokenURI = getIPFSUrl(tokenURI)
const publicTokenURI = getIPFSUrl(tokenURI, ipfsBaseURL)
const metadata = await fetchMetadata(tokenURI)

const ZContract = MediaFactory.connect(contractAddress, provider)
const contentURI = await ZContract.tokenURI(tokenId)
const contentURL = getIPFSUrl(contentURI, ipfsBaseURL)

const { name, description, externalURL } = metadata
const { name, description, externalURL, mimeType } = metadata
const contentURLMimeType = mimeType
? mimeType
: await fetchMimeType(contentURL)

return {
metadata,
tokenURI: publicTokenURI,
contentURI,
tokenURL: publicTokenURI,
contentURL,
contentURLMimeType,
...(name && { name }),
...(description && { description }),
...(externalURL && { externalURL }),
Expand Down
14 changes: 14 additions & 0 deletions src/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,17 @@ export async function fetchMetadata(uri: string) {
const resp = await fetch(cloudflareIPFSURI)
return resp.json()
}

export async function fetchMimeType(uri: string) {
try {
const resp = await fetch(uri, { method: 'HEAD' })
return resp.headers.get('content-type')
} catch (e) {
console.error(
`Failed to fetch mimetype for uri: ${uri} because: ${
e?.message || 'Unknown Error occured'
}`,
)
return
}
}
16 changes: 10 additions & 6 deletions tests/contracts/artblocks.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ const ART_BLOCKS_CRITERIA = {
metadata: METADATA_STUB,
name: METADATA_STUB.name,
description: METADATA_STUB.description,
tokenURI: 'https://api.artblocks.io/token/91000216',
contentURI: METADATA_STUB.animation_url,
previewURI: METADATA_STUB.image,
tokenURL: 'https://api.artblocks.io/token/91000216',
contentURL: METADATA_STUB.animation_url,
contentURLMimeType: 'text/html; charset=utf-8',
previewURL: METADATA_STUB.image,
previewURLMimeType: 'image/png',
attributes: METADATA_STUB.traits,
externalURL: METADATA_STUB.external_url,
},
Expand All @@ -33,9 +35,11 @@ const ART_BLOCKS_CURATED_CRITERIA = {
metadata: CURATED_METADATA_STUB,
name: CURATED_METADATA_STUB.name,
description: CURATED_METADATA_STUB.description,
tokenURI: 'https://api.artblocks.io/token/100',
contentURI: CURATED_METADATA_STUB.animation_url,
previewURI: CURATED_METADATA_STUB.image,
tokenURL: 'https://api.artblocks.io/token/100',
contentURL: CURATED_METADATA_STUB.animation_url,
contentURLMimeType: 'text/html; charset=utf-8',
previewURL: CURATED_METADATA_STUB.image,
previewURLMimeType: 'image/png',
attributes: CURATED_METADATA_STUB.traits,
externalURL: CURATED_METADATA_STUB.external_url,
},
Expand Down
8 changes: 5 additions & 3 deletions tests/contracts/bonsai.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ const BONSAI_CRITERIA = {
metadata: METADATA_STUB,
name: METADATA_STUB.name,
description: METADATA_STUB.description,
tokenURI:
tokenURL:
'https://ipfs.io/ipfs/QmXPSFmFqfDTMbLePfGTuYa2Vm9CoqsU11ypiMm1nKL8V9/100',
contentURI: METADATA_STUB.animation_url,
previewURI: METADATA_STUB.image,
contentURL: METADATA_STUB.animation_url,
contentURLMimeType: 'video/mp4',
previewURL: METADATA_STUB.image,
previewURLMimeType: 'image/png',
attributes: METADATA_STUB.attributes,
},
}
Expand Down
5 changes: 3 additions & 2 deletions tests/contracts/boredApe.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ const BORED_APE_CRITERIA = {
},
output: {
metadata: BORED_APE_METADATA_STUB,
tokenURI:
tokenURL:
'https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/1',
contentURI:
contentURL:
'https://ipfs.io/ipfs/QmPbxeGcXhYQQNgsC6a36dDyYUcHgMLnGKnF8pVFmGsvqi',
contentURLMimeType: 'image/png',
attributes: BORED_APE_METADATA_STUB.attributes,
},
}
Expand Down
5 changes: 3 additions & 2 deletions tests/contracts/cryptovoxels.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const CRYPTOVOXELS_CRITERIA = {
metadata: METADATA_STUB,
name: METADATA_STUB.name,
description: METADATA_STUB.description,
tokenURI: 'https://www.cryptovoxels.com/p/100',
contentURI: METADATA_STUB.image,
tokenURL: 'https://www.cryptovoxels.com/p/100',
contentURL: METADATA_STUB.image,
contentURLMimeType: 'image/png',
attributes: METADATA_STUB.attributes,
externalURL: METADATA_STUB.external_url,
},
Expand Down
5 changes: 3 additions & 2 deletions tests/contracts/decentraland.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const DECENTRALAND_CRITERIA = {
output: {
metadata: METADATA_STUB,
name: METADATA_STUB.name,
tokenURI:
tokenURL:
'https://api.decentraland.org/v2/contracts/0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d/tokens/100',
contentURI: METADATA_STUB.image,
contentURL: METADATA_STUB.image,
contentURLMimeType: 'image/png',
attributes: METADATA_STUB.attributes,
externalURL: METADATA_STUB.external_url,
},
Expand Down
5 changes: 3 additions & 2 deletions tests/contracts/hashmasks.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const BORED_APE_CRITERIA = {
metadata: METADATA_STUB,
name: 'Chocolate',
description: METADATA_STUB.description,
tokenURI: 'https://hashmap.azurewebsites.net/getMask/3837',
contentURI: METADATA_STUB.image,
tokenURL: 'https://hashmap.azurewebsites.net/getMask/3837',
contentURL: METADATA_STUB.image,
contentURLMimeType: 'image/jpeg',
attributes: METADATA_STUB.attributes,
externalURL: METADATA_STUB.external_url,
},
Expand Down
Loading

0 comments on commit b390e08

Please sign in to comment.