compression.ts 11 KB

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