compression.ts 12 KB

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