import { createERC721ContractMetadataValidator, createERC721TokenMetadataValidator, ERC721ContractMetadata, ERC721TokenMetadata, } from '@sloikaxyz/erc721-metadata-ajv'; import { IPFS } from 'ipfs-core-types'; import { CID, IPFSHTTPClient } from 'ipfs-http-client'; import invariant from 'ts-invariant'; import { generatorToArray } from '~/utils/generatorToArray'; type IPFSAddAllOptions = Parameters[1]; export class UploadedMetadataBundle { constructor(public readonly cid: CID) {} } export class MetadataBundle { private files = new Map(); addCollectionMetadata( filename: string, content: ERC721ContractMetadata, ): void { const validate = createERC721ContractMetadataValidator({ strict: true, allErrors: true, }); if (!validate(content)) { // eslint-disable-next-line no-console console.error(filename, 'failed ajv validation', validate.errors); throw new Error(`metadata ${filename} failed validation`); } this.files.set(filename, content); } addTokenMetadata(filename: string, content: ERC721TokenMetadata): void { const validate = createERC721TokenMetadataValidator({ strict: true, allErrors: true, }); if (!validate(content)) { // eslint-disable-next-line no-console console.error(filename, 'failed ajv validation', validate.errors); throw new Error(`metadata ${filename} failed validation`); } this.files.set(filename, content); } async upload( ipfsClient: IPFSHTTPClient, addAllOptions?: IPFSAddAllOptions, ): Promise { const options: IPFSAddAllOptions = { ...addAllOptions, cidVersion: 0, wrapWithDirectory: true, }; const results = await generatorToArray( ipfsClient.addAll(this.getFiles(), options), ); const rootCID = results.find(({ path }) => path === '')?.cid; invariant( typeof rootCID !== 'undefined', 'ipfsClient.addAll expected to contain a root CID', ); return new UploadedMetadataBundle(rootCID); } private *getFiles(): Generator<{ path: string; content: string }> { for (const [path, content] of this.files.entries()) { yield { path, content: JSON.stringify(content), }; } } }