compression.ts 11 KB

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