|
| 1 | +import { encodePacked } from "viem/utils"; |
| 2 | +import { upload } from "../../../../storage/upload.js"; |
| 3 | +import type { BaseTransactionOptions } from "../../../../transaction/types.js"; |
| 4 | +import { encodeAbiParameters } from "../../../../utils/abi/encodeAbiParameters.js"; |
| 5 | +import { toHex } from "../../../../utils/encoding/hex.js"; |
| 6 | +import { keccak256 } from "../../../../utils/hashing/keccak256.js"; |
| 7 | +import { getBaseUriFromBatch } from "../../../../utils/ipfs.js"; |
| 8 | +import type { NFTInput } from "../../../../utils/nft/parseNft.js"; |
| 9 | +import { |
| 10 | + getBaseURICount, |
| 11 | + isGetBaseURICountSupported, |
| 12 | +} from "../../__generated__/IBatchMintMetadata/read/getBaseURICount.js"; |
| 13 | +import { |
| 14 | + encryptDecrypt, |
| 15 | + isEncryptDecryptSupported, |
| 16 | +} from "../../__generated__/IDelayedReveal/read/encryptDecrypt.js"; |
| 17 | +import { nextTokenIdToMint } from "../../__generated__/IERC721Enumerable/read/nextTokenIdToMint.js"; |
| 18 | +import { |
| 19 | + lazyMint as generatedLazyMint, |
| 20 | + isLazyMintSupported, |
| 21 | +} from "../../__generated__/ILazyMint/write/lazyMint.js"; |
| 22 | +import { hashDelayedRevealPassword } from "../helpers/hashDelayedRevealBatch.js"; |
| 23 | + |
| 24 | +/** |
| 25 | + * @extension ERC721 |
| 26 | + */ |
| 27 | +export type CreateDelayedRevealBatchParams = { |
| 28 | + placeholderMetadata: NFTInput; |
| 29 | + metadata: NFTInput[]; |
| 30 | + password: string; |
| 31 | +}; |
| 32 | + |
| 33 | +/** |
| 34 | + * Creates a batch of encrypted NFTs that can be revealed at a later time. |
| 35 | + * This method is only available on the `DropERC721` contract. |
| 36 | + * |
| 37 | + * @param options {CreateDelayedRevealBatchParams} - The delayed reveal options. |
| 38 | + * @param options.placeholderMetadata {@link NFTInput} - The placeholder metadata for the batch. |
| 39 | + * @param options.metadata {@link NFTInput} - An array of NFT metadata to be revealed at a later time. |
| 40 | + * @param options.password {string} - The password for the reveal. |
| 41 | + * @param options.contract {@link ThirdwebContract} - The NFT contract instance. |
| 42 | + * |
| 43 | + * @returns The prepared transaction to send. |
| 44 | + * |
| 45 | + * @extension ERC721 |
| 46 | + * @example |
| 47 | + * ```ts |
| 48 | + * import { createDelayedRevealBatch } from "thirdweb/extensions/erc721"; |
| 49 | + * |
| 50 | + * const placeholderNFT = { |
| 51 | + * name: "Hidden NFT", |
| 52 | + * description: "Will be revealed next week!" |
| 53 | + * }; |
| 54 | + * |
| 55 | + * const realNFTs = [{ |
| 56 | + * name: "Common NFT #1", |
| 57 | + * description: "Common NFT, one of many.", |
| 58 | + * image: ipfs://..., |
| 59 | + * }, { |
| 60 | + * name: "Super Rare NFT #2", |
| 61 | + * description: "You got a Super Rare NFT!", |
| 62 | + * image: ipfs://..., |
| 63 | + * }]; |
| 64 | + * |
| 65 | + * const transaction = createDelayedRevealBatch({ |
| 66 | + * contract, |
| 67 | + * placeholderMetadata: placeholderNFT, |
| 68 | + * metadata: realNFTs, |
| 69 | + * password: "password123", |
| 70 | + * }); |
| 71 | + * |
| 72 | + * const { transactionHash } = await sendTransaction({ transaction, account }); |
| 73 | + * ``` |
| 74 | + */ |
| 75 | +export function createDelayedRevealBatch( |
| 76 | + options: BaseTransactionOptions<CreateDelayedRevealBatchParams>, |
| 77 | +) { |
| 78 | + if (!options.password) { |
| 79 | + throw new Error("Password is required"); |
| 80 | + } |
| 81 | + |
| 82 | + return generatedLazyMint({ |
| 83 | + asyncParams: async () => { |
| 84 | + const [placeholderUris, startFileNumber] = await Promise.all([ |
| 85 | + upload({ |
| 86 | + client: options.contract.client, |
| 87 | + files: Array(options.metadata.length).fill( |
| 88 | + options.placeholderMetadata, |
| 89 | + ), |
| 90 | + }), |
| 91 | + nextTokenIdToMint({ |
| 92 | + contract: options.contract, |
| 93 | + }), |
| 94 | + ]); |
| 95 | + const placeholderUri = getBaseUriFromBatch(placeholderUris); |
| 96 | + |
| 97 | + const uris = await upload({ |
| 98 | + client: options.contract.client, |
| 99 | + files: options.metadata, |
| 100 | + // IMPORTANT: File number has to be calculated properly otherwise the whole batch will break |
| 101 | + // e.g: If you are uploading a second batch, the file name should never start from `0` |
| 102 | + rewriteFileNames: { |
| 103 | + fileStartNumber: Number(startFileNumber), |
| 104 | + }, |
| 105 | + }); |
| 106 | + |
| 107 | + const baseUri = getBaseUriFromBatch(uris); |
| 108 | + const baseUriId = await getBaseURICount({ |
| 109 | + contract: options.contract, |
| 110 | + }); |
| 111 | + |
| 112 | + const hashedPassword = await hashDelayedRevealPassword( |
| 113 | + baseUriId, |
| 114 | + options.password, |
| 115 | + options.contract, |
| 116 | + ); |
| 117 | + const encryptedBaseURI = await encryptDecrypt({ |
| 118 | + contract: options.contract, |
| 119 | + data: toHex(baseUri), |
| 120 | + key: hashedPassword, |
| 121 | + }); |
| 122 | + |
| 123 | + const chainId = BigInt(options.contract.chain.id); |
| 124 | + const provenanceHash = keccak256( |
| 125 | + encodePacked( |
| 126 | + ["bytes", "bytes", "uint256"], |
| 127 | + [toHex(baseUri), hashedPassword, chainId], |
| 128 | + ), |
| 129 | + ); |
| 130 | + const data = encodeAbiParameters( |
| 131 | + [ |
| 132 | + { name: "baseUri", type: "bytes" }, |
| 133 | + { name: "provenanceHash", type: "bytes32" }, |
| 134 | + ], |
| 135 | + [encryptedBaseURI, provenanceHash], |
| 136 | + ); |
| 137 | + |
| 138 | + return { |
| 139 | + amount: BigInt(options.metadata.length), |
| 140 | + baseURIForTokens: |
| 141 | + placeholderUri.slice(-1) === "/" |
| 142 | + ? placeholderUri |
| 143 | + : `${placeholderUri}/`, |
| 144 | + extraData: data, |
| 145 | + } as const; |
| 146 | + }, |
| 147 | + contract: options.contract, |
| 148 | + }); |
| 149 | +} |
| 150 | + |
| 151 | +/** |
| 152 | + * Checks if the `createDelayedRevealBatch` method is supported by the given contract. |
| 153 | + * @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. |
| 154 | + * @returns A boolean indicating if the `createDelayedRevealBatch` method is supported. |
| 155 | + * @extension ERC721 |
| 156 | + * @example |
| 157 | + * ```ts |
| 158 | + * import { isCreateDelayedRevealBatchSupported } from "thirdweb/extensions/erc721"; |
| 159 | + * const supported = isCreateDelayedRevealBatchSupported(["0x..."]); |
| 160 | + * ``` |
| 161 | + */ |
| 162 | +export function isCreateDelayedRevealBatchSupported( |
| 163 | + availableSelectors: string[], |
| 164 | +) { |
| 165 | + return [ |
| 166 | + isGetBaseURICountSupported(availableSelectors), |
| 167 | + isEncryptDecryptSupported(availableSelectors), |
| 168 | + isLazyMintSupported(availableSelectors), |
| 169 | + ].every(Boolean); |
| 170 | +} |
0 commit comments