import type { BaseTransactionOptions, WithOverrides, } from "../../../../transaction/types.js"; import type { NFT, NFTInput } from "../../../../utils/nft/parseNft.js"; import * as BatchBaseURI from "../../__generated__/DropERC721/write/updateBatchBaseURI.js"; import * as BaseURICount from "../../__generated__/IBatchMintMetadata/read/getBaseURICount.js"; import * as BatchAtIndex from "../../__generated__/IBatchMintMetadata/read/getBatchIdAtIndex.js"; import * as GetNFT from "../../read/getNFT.js"; /** * @extension ERC721 */ export type UpdateMetadataParams = { targetTokenId: bigint; newMetadata: NFTInput; }; /** * @internal */ async function getUpdateMetadataParams( options: BaseTransactionOptions, ): Promise { const { contract, targetTokenId, newMetadata } = options; const batchCount = await BaseURICount.getBaseURICount(options); if (batchCount === 0n) { throw new Error( "No base URI set. Please set a base URI before updating metadata", ); } // Look for the batchId & determine the start + end tokenId of the batch let startTokenId = 0n; let endTokenId = 0n; let batchIndex = 0n; for (let i = 0n; i < batchCount; i += 1n) { batchIndex = i; endTokenId = await BatchAtIndex.getBatchIdAtIndex({ contract, index: batchIndex, }); if (endTokenId > targetTokenId) { break; } startTokenId = endTokenId; } const range = Array.from( { length: Number(endTokenId - startTokenId) }, (_, k) => BigInt(k) + startTokenId, ); const BATCH_SIZE = 50; const currentMetadatas = []; for (let i = 0; i < range.length; i += BATCH_SIZE) { const chunk = range.slice(i, i + BATCH_SIZE); const chunkResults = await Promise.all( chunk.map((id) => GetNFT.getNFT({ contract, includeOwner: false, tokenId: id, useIndexer: false, }), ), ); currentMetadatas.push(...chunkResults); if (i + BATCH_SIZE < range.length) { await new Promise((resolve) => setTimeout(resolve, 500)); } // Abort if any of the items failed to load if (currentMetadatas.some((item) => item === undefined || !item.tokenURI)) { throw new Error( `Failed to load all ${range.length} items from batchIndex: ${batchIndex}`, ); } } const newMetadatas: NFTInput[] = []; for (let i = 0; i < currentMetadatas.length; i++) { const { id, metadata } = currentMetadatas[i] as NFT; if (targetTokenId === id) { newMetadatas.push(newMetadata); } else { newMetadatas.push(metadata); } } const { uploadOrExtractURIs } = await import("../../../../utils/ipfs.js"); const batchOfUris = await uploadOrExtractURIs( newMetadatas, contract.client, Number(startTokenId), ); if (!batchOfUris || !batchOfUris.length || !batchOfUris[0]) { throw new Error("Failed to upload batch of new metadatas"); } const baseUri = batchOfUris[0].substring(0, batchOfUris[0].lastIndexOf("/")); // IMPORTANT: The new ipfs URI must have the trailing slash at the end // this is required by the Drop contract const uri = `${baseUri}/`; return { index: batchIndex, uri }; } /** * Update the metadata of the single token in an NFT Drop (DropERC721) collection * For NFT Collection, please use `setTokenURI` * @param options * @returns the prepared transaction * @extension ERC721 * @example * ```ts * import { updateMetadata } from "thirdweb/extensions/erc721"; * * const transaction = updateMetadata({ * contract, * targetTokenId: 0n, * client: thirdwebClient, * newMetadata: { * name: "this is the new nft name", * description: "...", * image: "new image uri" * // ... * } * }); * ``` */ export function updateMetadata( options: BaseTransactionOptions>, ) { const { contract } = options; return BatchBaseURI.updateBatchBaseURI({ asyncParams: async () => getUpdateMetadataParams(options), contract, overrides: options.overrides, }); } /** * Checks if the `updateMetadata` method is supported by the given contract. * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. * @returns A boolean indicating if the `updateMetadata` method is supported. * @extension ERC721 * @example * ```ts * import { isUpdateMetadataSupported } from "thirdweb/extensions/erc721"; * * const supported = isUpdateMetadataSupported(["0x..."]); * ``` */ export function isUpdateMetadataSupported(availableSelectors: string[]) { return ( BaseURICount.isGetBaseURICountSupported(availableSelectors) && BatchBaseURI.isUpdateBatchBaseURISupported(availableSelectors) && BatchAtIndex.isGetBatchIdAtIndexSupported(availableSelectors) && GetNFT.isGetNFTSupported(availableSelectors) ); }