compression.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {
  2. PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
  3. type MetadataArgs,
  4. computeCreatorHash,
  5. computeDataHash,
  6. createCreateTreeInstruction,
  7. createMintToCollectionV1Instruction,
  8. } from '@metaplex-foundation/mpl-bubblegum';
  9. import {
  10. type CreateMetadataAccountArgsV3,
  11. PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
  12. createCreateMasterEditionV3Instruction,
  13. createCreateMetadataAccountV3Instruction,
  14. createSetCollectionSizeInstruction,
  15. } from '@metaplex-foundation/mpl-token-metadata';
  16. import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, type ValidDepthSizePair, createAllocTreeIx } from '@solana/spl-account-compression';
  17. import { TOKEN_PROGRAM_ID, createAccount, createMint, mintTo } from '@solana/spl-token';
  18. import { type Connection, type Keypair, PublicKey, Transaction, type TransactionInstruction, sendAndConfirmTransaction } from '@solana/web3.js';
  19. // import local helper functions
  20. import { explorerURL, extractSignatureFromFailedTransaction } from './helpers';
  21. /*
  22. Helper function to create a merkle tree on chain, including allocating
  23. all the space required to store all the nodes
  24. */
  25. export async function createTree(
  26. connection: Connection,
  27. payer: Keypair,
  28. treeKeypair: Keypair,
  29. maxDepthSizePair: ValidDepthSizePair,
  30. canopyDepth = 0,
  31. ) {
  32. console.log('Creating a new Merkle tree...');
  33. console.log('treeAddress:', treeKeypair.publicKey.toBase58());
  34. // derive the tree's authority (PDA), owned by Bubblegum
  35. const [treeAuthority, _bump] = PublicKey.findProgramAddressSync([treeKeypair.publicKey.toBuffer()], BUBBLEGUM_PROGRAM_ID);
  36. console.log('treeAuthority:', treeAuthority.toBase58());
  37. // allocate the tree's account on chain with the `space`
  38. // NOTE: this will compute the space needed to store the tree on chain (and the lamports required to store it)
  39. const allocTreeIx = await createAllocTreeIx(connection, treeKeypair.publicKey, payer.publicKey, maxDepthSizePair, canopyDepth);
  40. // create the instruction to actually create the tree
  41. const createTreeIx = createCreateTreeInstruction(
  42. {
  43. payer: payer.publicKey,
  44. treeCreator: payer.publicKey,
  45. treeAuthority,
  46. merkleTree: treeKeypair.publicKey,
  47. compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  48. // NOTE: this is used for some on chain logging
  49. logWrapper: SPL_NOOP_PROGRAM_ID,
  50. },
  51. {
  52. maxBufferSize: maxDepthSizePair.maxBufferSize,
  53. maxDepth: maxDepthSizePair.maxDepth,
  54. public: false,
  55. },
  56. BUBBLEGUM_PROGRAM_ID,
  57. );
  58. try {
  59. // create and send the transaction to initialize the tree
  60. const tx = new Transaction().add(allocTreeIx).add(createTreeIx);
  61. tx.feePayer = payer.publicKey;
  62. // send the transaction
  63. const txSignature = await sendAndConfirmTransaction(
  64. connection,
  65. tx,
  66. // ensuring the `treeKeypair` PDA and the `payer` are BOTH signers
  67. [treeKeypair, payer],
  68. {
  69. commitment: 'confirmed',
  70. skipPreflight: true,
  71. },
  72. );
  73. console.log('\nMerkle tree created successfully!');
  74. console.log(explorerURL({ txSignature }));
  75. // return useful info
  76. return { treeAuthority, treeAddress: treeKeypair.publicKey };
  77. } catch (err: any) {
  78. console.error('\nFailed to create merkle tree:', err);
  79. // log a block explorer link for the failed transaction
  80. await extractSignatureFromFailedTransaction(connection, err);
  81. throw err;
  82. }
  83. }
  84. /**
  85. * Create an NFT collection on-chain, using the regular Metaplex standards
  86. * with the `payer` as the authority
  87. */
  88. export async function createCollection(connection: Connection, payer: Keypair, metadataV3: CreateMetadataAccountArgsV3) {
  89. // create and initialize the SPL token mint
  90. console.log("Creating the collection's mint...");
  91. const mint = await createMint(
  92. connection,
  93. payer,
  94. // mint authority
  95. payer.publicKey,
  96. // freeze authority
  97. payer.publicKey,
  98. // decimals - use `0` for NFTs since they are non-fungible
  99. 0,
  100. );
  101. console.log('Mint address:', mint.toBase58());
  102. // create the token account
  103. console.log('Creating a token account...');
  104. const tokenAccount = await createAccount(
  105. connection,
  106. payer,
  107. mint,
  108. payer.publicKey,
  109. // undefined, undefined,
  110. );
  111. console.log('Token account:', tokenAccount.toBase58());
  112. // mint 1 token ()
  113. console.log('Minting 1 token for the collection...');
  114. const mintSig = await mintTo(
  115. connection,
  116. payer,
  117. mint,
  118. tokenAccount,
  119. payer,
  120. // mint exactly 1 token
  121. 1,
  122. // no `multiSigners`
  123. [],
  124. undefined,
  125. TOKEN_PROGRAM_ID,
  126. );
  127. // console.log(explorerURL({ txSignature: mintSig }));
  128. // derive the PDA for the metadata account
  129. const [metadataAccount, _bump] = PublicKey.findProgramAddressSync(
  130. [Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
  131. TOKEN_METADATA_PROGRAM_ID,
  132. );
  133. console.log('Metadata account:', metadataAccount.toBase58());
  134. // create an instruction to create the metadata account
  135. const createMetadataIx = createCreateMetadataAccountV3Instruction(
  136. {
  137. metadata: metadataAccount,
  138. mint: mint,
  139. mintAuthority: payer.publicKey,
  140. payer: payer.publicKey,
  141. updateAuthority: payer.publicKey,
  142. },
  143. {
  144. createMetadataAccountArgsV3: metadataV3,
  145. },
  146. );
  147. // derive the PDA for the metadata account
  148. const [masterEditionAccount, _bump2] = PublicKey.findProgramAddressSync(
  149. [Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition', 'utf8')],
  150. TOKEN_METADATA_PROGRAM_ID,
  151. );
  152. console.log('Master edition account:', masterEditionAccount.toBase58());
  153. // create an instruction to create the metadata account
  154. const createMasterEditionIx = createCreateMasterEditionV3Instruction(
  155. {
  156. edition: masterEditionAccount,
  157. mint: mint,
  158. mintAuthority: payer.publicKey,
  159. payer: payer.publicKey,
  160. updateAuthority: payer.publicKey,
  161. metadata: metadataAccount,
  162. },
  163. {
  164. createMasterEditionArgs: {
  165. maxSupply: 0,
  166. },
  167. },
  168. );
  169. // create the collection size instruction
  170. const collectionSizeIX = createSetCollectionSizeInstruction(
  171. {
  172. collectionMetadata: metadataAccount,
  173. collectionAuthority: payer.publicKey,
  174. collectionMint: mint,
  175. },
  176. {
  177. setCollectionSizeArgs: { size: 50 },
  178. },
  179. );
  180. try {
  181. // construct the transaction with our instructions, making the `payer` the `feePayer`
  182. const tx = new Transaction().add(createMetadataIx).add(createMasterEditionIx).add(collectionSizeIX);
  183. tx.feePayer = payer.publicKey;
  184. // send the transaction to the cluster
  185. const txSignature = await sendAndConfirmTransaction(connection, tx, [payer], {
  186. commitment: 'confirmed',
  187. skipPreflight: true,
  188. });
  189. console.log('\nCollection successfully created!');
  190. console.log(explorerURL({ txSignature }));
  191. } catch (err) {
  192. console.error('\nFailed to create collection:', err);
  193. // log a block explorer link for the failed transaction
  194. await extractSignatureFromFailedTransaction(connection, err);
  195. throw err;
  196. }
  197. // return all the accounts
  198. return { mint, tokenAccount, metadataAccount, masterEditionAccount };
  199. }
  200. /**
  201. * Mint a single compressed NFTs to any address
  202. */
  203. export async function mintCompressedNFT(
  204. connection: Connection,
  205. payer: Keypair,
  206. treeAddress: PublicKey,
  207. collectionMint: PublicKey,
  208. collectionMetadata: PublicKey,
  209. collectionMasterEditionAccount: PublicKey,
  210. compressedNFTMetadata: MetadataArgs,
  211. receiverAddress?: PublicKey,
  212. ) {
  213. // derive the tree's authority (PDA), owned by Bubblegum
  214. const [treeAuthority, _bump] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);
  215. // derive a PDA (owned by Bubblegum) to act as the signer of the compressed minting
  216. const [bubblegumSigner, _bump2] = PublicKey.findProgramAddressSync(
  217. // `collection_cpi` is a custom prefix required by the Bubblegum program
  218. [Buffer.from('collection_cpi', 'utf8')],
  219. BUBBLEGUM_PROGRAM_ID,
  220. );
  221. // create an array of instruction, to mint multiple compressed NFTs at once
  222. const mintIxs: TransactionInstruction[] = [];
  223. /**
  224. * correctly format the metadata args for the nft to mint
  225. * ---
  226. * note: minting an nft into a collection (via `createMintToCollectionV1Instruction`)
  227. * will auto verify the collection. But, the `collection.verified` value inside the
  228. * `metadataArgs` must be set to `false` in order for the instruction to succeed
  229. */
  230. const metadataArgs = Object.assign(compressedNFTMetadata, {
  231. collection: { key: collectionMint, verified: false },
  232. });
  233. /**
  234. * compute the data and creator hash for display in the console
  235. *
  236. * note: this is not required to do in order to mint new compressed nfts
  237. * (since it is performed on chain via the Bubblegum program)
  238. * this is only for demonstration
  239. */
  240. const computedDataHash = new PublicKey(computeDataHash(metadataArgs)).toBase58();
  241. const computedCreatorHash = new PublicKey(computeCreatorHash(metadataArgs.creators)).toBase58();
  242. console.log('computedDataHash:', computedDataHash);
  243. console.log('computedCreatorHash:', computedCreatorHash);
  244. /*
  245. Add a single mint to collection instruction
  246. ---
  247. But you could all multiple in the same transaction, as long as your
  248. transaction is still within the byte size limits
  249. */
  250. mintIxs.push(
  251. createMintToCollectionV1Instruction(
  252. {
  253. payer: payer.publicKey,
  254. merkleTree: treeAddress,
  255. treeAuthority,
  256. treeDelegate: payer.publicKey,
  257. // set the receiver of the NFT
  258. leafOwner: receiverAddress || payer.publicKey,
  259. // set a delegated authority over this NFT
  260. leafDelegate: payer.publicKey,
  261. /*
  262. You can set any delegate address at mint, otherwise should
  263. normally be the same as `leafOwner`
  264. NOTE: the delegate will be auto cleared upon NFT transfer
  265. ---
  266. in this case, we are setting the payer as the delegate
  267. */
  268. // collection details
  269. collectionAuthority: payer.publicKey,
  270. collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
  271. collectionMint: collectionMint,
  272. collectionMetadata: collectionMetadata,
  273. editionAccount: collectionMasterEditionAccount,
  274. // other accounts
  275. compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  276. logWrapper: SPL_NOOP_PROGRAM_ID,
  277. bubblegumSigner: bubblegumSigner,
  278. tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
  279. },
  280. {
  281. metadataArgs,
  282. },
  283. ),
  284. );
  285. try {
  286. // construct the transaction with our instructions, making the `payer` the `feePayer`
  287. const tx = new Transaction().add(...mintIxs);
  288. tx.feePayer = payer.publicKey;
  289. // send the transaction to the cluster
  290. const txSignature = await sendAndConfirmTransaction(connection, tx, [payer], {
  291. commitment: 'confirmed',
  292. skipPreflight: true,
  293. });
  294. console.log('\nSuccessfully minted the compressed NFT!');
  295. console.log(explorerURL({ txSignature }));
  296. return txSignature;
  297. } catch (err) {
  298. console.error('\nFailed to mint compressed NFT:', err);
  299. // log a block explorer link for the failed transaction
  300. await extractSignatureFromFailedTransaction(connection, err);
  301. throw err;
  302. }
  303. }