|
@@ -0,0 +1,404 @@
|
|
|
|
+import {
|
|
|
|
+ Keypair,
|
|
|
|
+ PublicKey,
|
|
|
|
+ Connection,
|
|
|
|
+ Transaction,
|
|
|
|
+ sendAndConfirmTransaction,
|
|
|
|
+ TransactionInstruction,
|
|
|
|
+} from "@solana/web3.js";
|
|
|
|
+import {
|
|
|
|
+ createAccount,
|
|
|
|
+ createMint,
|
|
|
|
+ mintTo,
|
|
|
|
+ TOKEN_PROGRAM_ID,
|
|
|
|
+} from "@solana/spl-token";
|
|
|
|
+import {
|
|
|
|
+ SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
|
|
|
|
+ createAllocTreeIx,
|
|
|
|
+ ValidDepthSizePair,
|
|
|
|
+ SPL_NOOP_PROGRAM_ID,
|
|
|
|
+} from "@solana/spl-account-compression";
|
|
|
|
+import {
|
|
|
|
+ PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
|
|
|
|
+ MetadataArgs,
|
|
|
|
+ computeCreatorHash,
|
|
|
|
+ computeDataHash,
|
|
|
|
+ createCreateTreeInstruction,
|
|
|
|
+ createMintToCollectionV1Instruction,
|
|
|
|
+} from "@metaplex-foundation/mpl-bubblegum";
|
|
|
|
+import {
|
|
|
|
+ PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
|
|
|
|
+ CreateMetadataAccountArgsV3,
|
|
|
|
+ createCreateMetadataAccountV3Instruction,
|
|
|
|
+ createCreateMasterEditionV3Instruction,
|
|
|
|
+ createSetCollectionSizeInstruction,
|
|
|
|
+} from "@metaplex-foundation/mpl-token-metadata";
|
|
|
|
+
|
|
|
|
+// import local helper functions
|
|
|
|
+import { explorerURL, extractSignatureFromFailedTransaction } from "./helpers";
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ Helper function to create a merkle tree on chain, including allocating
|
|
|
|
+ all the space required to store all the nodes
|
|
|
|
+*/
|
|
|
|
+export async function createTree(
|
|
|
|
+ connection: Connection,
|
|
|
|
+ payer: Keypair,
|
|
|
|
+ treeKeypair: Keypair,
|
|
|
|
+ maxDepthSizePair: ValidDepthSizePair,
|
|
|
|
+ canopyDepth: number = 0
|
|
|
|
+) {
|
|
|
|
+ console.log("Creating a new Merkle tree...");
|
|
|
|
+ console.log("treeAddress:", treeKeypair.publicKey.toBase58());
|
|
|
|
+
|
|
|
|
+ // derive the tree's authority (PDA), owned by Bubblegum
|
|
|
|
+ const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
|
|
|
|
+ [treeKeypair.publicKey.toBuffer()],
|
|
|
|
+ BUBBLEGUM_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+ console.log("treeAuthority:", treeAuthority.toBase58());
|
|
|
|
+
|
|
|
|
+ // allocate the tree's account on chain with the `space`
|
|
|
|
+ // NOTE: this will compute the space needed to store the tree on chain (and the lamports required to store it)
|
|
|
|
+ const allocTreeIx = await createAllocTreeIx(
|
|
|
|
+ connection,
|
|
|
|
+ treeKeypair.publicKey,
|
|
|
|
+ payer.publicKey,
|
|
|
|
+ maxDepthSizePair,
|
|
|
|
+ canopyDepth
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // create the instruction to actually create the tree
|
|
|
|
+ const createTreeIx = createCreateTreeInstruction(
|
|
|
|
+ {
|
|
|
|
+ payer: payer.publicKey,
|
|
|
|
+ treeCreator: payer.publicKey,
|
|
|
|
+ treeAuthority,
|
|
|
|
+ merkleTree: treeKeypair.publicKey,
|
|
|
|
+ compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
|
|
|
|
+ // NOTE: this is used for some on chain logging
|
|
|
|
+ logWrapper: SPL_NOOP_PROGRAM_ID,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ maxBufferSize: maxDepthSizePair.maxBufferSize,
|
|
|
|
+ maxDepth: maxDepthSizePair.maxDepth,
|
|
|
|
+ public: false,
|
|
|
|
+ },
|
|
|
|
+ BUBBLEGUM_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // create and send the transaction to initialize the tree
|
|
|
|
+ const tx = new Transaction().add(allocTreeIx).add(createTreeIx);
|
|
|
|
+ tx.feePayer = payer.publicKey;
|
|
|
|
+
|
|
|
|
+ // send the transaction
|
|
|
|
+ const txSignature = await sendAndConfirmTransaction(
|
|
|
|
+ connection,
|
|
|
|
+ tx,
|
|
|
|
+ // ensuring the `treeKeypair` PDA and the `payer` are BOTH signers
|
|
|
|
+ [treeKeypair, payer],
|
|
|
|
+ {
|
|
|
|
+ commitment: "confirmed",
|
|
|
|
+ skipPreflight: true,
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ console.log("\nMerkle tree created successfully!");
|
|
|
|
+ console.log(explorerURL({ txSignature }));
|
|
|
|
+
|
|
|
|
+ // return useful info
|
|
|
|
+ return { treeAuthority, treeAddress: treeKeypair.publicKey };
|
|
|
|
+ } catch (err: any) {
|
|
|
|
+ console.error("\nFailed to create merkle tree:", err);
|
|
|
|
+
|
|
|
|
+ // log a block explorer link for the failed transaction
|
|
|
|
+ await extractSignatureFromFailedTransaction(connection, err);
|
|
|
|
+
|
|
|
|
+ throw err;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Create an NFT collection on-chain, using the regular Metaplex standards
|
|
|
|
+ * with the `payer` as the authority
|
|
|
|
+ */
|
|
|
|
+export async function createCollection(
|
|
|
|
+ connection: Connection,
|
|
|
|
+ payer: Keypair,
|
|
|
|
+ metadataV3: CreateMetadataAccountArgsV3
|
|
|
|
+) {
|
|
|
|
+ // create and initialize the SPL token mint
|
|
|
|
+ console.log("Creating the collection's mint...");
|
|
|
|
+ const mint = await createMint(
|
|
|
|
+ connection,
|
|
|
|
+ payer,
|
|
|
|
+ // mint authority
|
|
|
|
+ payer.publicKey,
|
|
|
|
+ // freeze authority
|
|
|
|
+ payer.publicKey,
|
|
|
|
+ // decimals - use `0` for NFTs since they are non-fungible
|
|
|
|
+ 0
|
|
|
|
+ );
|
|
|
|
+ console.log("Mint address:", mint.toBase58());
|
|
|
|
+
|
|
|
|
+ // create the token account
|
|
|
|
+ console.log("Creating a token account...");
|
|
|
|
+ const tokenAccount = await createAccount(
|
|
|
|
+ connection,
|
|
|
|
+ payer,
|
|
|
|
+ mint,
|
|
|
|
+ payer.publicKey
|
|
|
|
+ // undefined, undefined,
|
|
|
|
+ );
|
|
|
|
+ console.log("Token account:", tokenAccount.toBase58());
|
|
|
|
+
|
|
|
|
+ // mint 1 token ()
|
|
|
|
+ console.log("Minting 1 token for the collection...");
|
|
|
|
+ const mintSig = await mintTo(
|
|
|
|
+ connection,
|
|
|
|
+ payer,
|
|
|
|
+ mint,
|
|
|
|
+ tokenAccount,
|
|
|
|
+ payer,
|
|
|
|
+ // mint exactly 1 token
|
|
|
|
+ 1,
|
|
|
|
+ // no `multiSigners`
|
|
|
|
+ [],
|
|
|
|
+ undefined,
|
|
|
|
+ TOKEN_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+ // console.log(explorerURL({ txSignature: mintSig }));
|
|
|
|
+
|
|
|
|
+ // derive the PDA for the metadata account
|
|
|
|
+ const [metadataAccount, _bump] = PublicKey.findProgramAddressSync(
|
|
|
|
+ [
|
|
|
|
+ Buffer.from("metadata", "utf8"),
|
|
|
|
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
|
|
|
|
+ mint.toBuffer(),
|
|
|
|
+ ],
|
|
|
|
+ TOKEN_METADATA_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+ console.log("Metadata account:", metadataAccount.toBase58());
|
|
|
|
+
|
|
|
|
+ // create an instruction to create the metadata account
|
|
|
|
+ const createMetadataIx = createCreateMetadataAccountV3Instruction(
|
|
|
|
+ {
|
|
|
|
+ metadata: metadataAccount,
|
|
|
|
+ mint: mint,
|
|
|
|
+ mintAuthority: payer.publicKey,
|
|
|
|
+ payer: payer.publicKey,
|
|
|
|
+ updateAuthority: payer.publicKey,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ createMetadataAccountArgsV3: metadataV3,
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // derive the PDA for the metadata account
|
|
|
|
+ const [masterEditionAccount, _bump2] = PublicKey.findProgramAddressSync(
|
|
|
|
+ [
|
|
|
|
+ Buffer.from("metadata", "utf8"),
|
|
|
|
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
|
|
|
|
+ mint.toBuffer(),
|
|
|
|
+ Buffer.from("edition", "utf8"),
|
|
|
|
+ ],
|
|
|
|
+ TOKEN_METADATA_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+ console.log("Master edition account:", masterEditionAccount.toBase58());
|
|
|
|
+
|
|
|
|
+ // create an instruction to create the metadata account
|
|
|
|
+ const createMasterEditionIx = createCreateMasterEditionV3Instruction(
|
|
|
|
+ {
|
|
|
|
+ edition: masterEditionAccount,
|
|
|
|
+ mint: mint,
|
|
|
|
+ mintAuthority: payer.publicKey,
|
|
|
|
+ payer: payer.publicKey,
|
|
|
|
+ updateAuthority: payer.publicKey,
|
|
|
|
+ metadata: metadataAccount,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ createMasterEditionArgs: {
|
|
|
|
+ maxSupply: 0,
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // create the collection size instruction
|
|
|
|
+ const collectionSizeIX = createSetCollectionSizeInstruction(
|
|
|
|
+ {
|
|
|
|
+ collectionMetadata: metadataAccount,
|
|
|
|
+ collectionAuthority: payer.publicKey,
|
|
|
|
+ collectionMint: mint,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ setCollectionSizeArgs: { size: 50 },
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // construct the transaction with our instructions, making the `payer` the `feePayer`
|
|
|
|
+ const tx = new Transaction()
|
|
|
|
+ .add(createMetadataIx)
|
|
|
|
+ .add(createMasterEditionIx)
|
|
|
|
+ .add(collectionSizeIX);
|
|
|
|
+ tx.feePayer = payer.publicKey;
|
|
|
|
+
|
|
|
|
+ // send the transaction to the cluster
|
|
|
|
+ const txSignature = await sendAndConfirmTransaction(
|
|
|
|
+ connection,
|
|
|
|
+ tx,
|
|
|
|
+ [payer],
|
|
|
|
+ {
|
|
|
|
+ commitment: "confirmed",
|
|
|
|
+ skipPreflight: true,
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ console.log("\nCollection successfully created!");
|
|
|
|
+ console.log(explorerURL({ txSignature }));
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error("\nFailed to create collection:", err);
|
|
|
|
+
|
|
|
|
+ // log a block explorer link for the failed transaction
|
|
|
|
+ await extractSignatureFromFailedTransaction(connection, err);
|
|
|
|
+
|
|
|
|
+ throw err;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // return all the accounts
|
|
|
|
+ return { mint, tokenAccount, metadataAccount, masterEditionAccount };
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Mint a single compressed NFTs to any address
|
|
|
|
+ */
|
|
|
|
+export async function mintCompressedNFT(
|
|
|
|
+ connection: Connection,
|
|
|
|
+ payer: Keypair,
|
|
|
|
+ treeAddress: PublicKey,
|
|
|
|
+ collectionMint: PublicKey,
|
|
|
|
+ collectionMetadata: PublicKey,
|
|
|
|
+ collectionMasterEditionAccount: PublicKey,
|
|
|
|
+ compressedNFTMetadata: MetadataArgs,
|
|
|
|
+ receiverAddress?: PublicKey
|
|
|
|
+) {
|
|
|
|
+ // derive the tree's authority (PDA), owned by Bubblegum
|
|
|
|
+ const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
|
|
|
|
+ [treeAddress.toBuffer()],
|
|
|
|
+ BUBBLEGUM_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // derive a PDA (owned by Bubblegum) to act as the signer of the compressed minting
|
|
|
|
+ const [bubblegumSigner, _bump2] = PublicKey.findProgramAddressSync(
|
|
|
|
+ // `collection_cpi` is a custom prefix required by the Bubblegum program
|
|
|
|
+ [Buffer.from("collection_cpi", "utf8")],
|
|
|
|
+ BUBBLEGUM_PROGRAM_ID
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // create an array of instruction, to mint multiple compressed NFTs at once
|
|
|
|
+ const mintIxs: TransactionInstruction[] = [];
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * correctly format the metadata args for the nft to mint
|
|
|
|
+ * ---
|
|
|
|
+ * note: minting an nft into a collection (via `createMintToCollectionV1Instruction`)
|
|
|
|
+ * will auto verify the collection. But, the `collection.verified` value inside the
|
|
|
|
+ * `metadataArgs` must be set to `false` in order for the instruction to succeed
|
|
|
|
+ */
|
|
|
|
+ const metadataArgs = Object.assign(compressedNFTMetadata, {
|
|
|
|
+ collection: { key: collectionMint, verified: false },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * compute the data and creator hash for display in the console
|
|
|
|
+ *
|
|
|
|
+ * note: this is not required to do in order to mint new compressed nfts
|
|
|
|
+ * (since it is performed on chain via the Bubblegum program)
|
|
|
|
+ * this is only for demonstration
|
|
|
|
+ */
|
|
|
|
+ const computedDataHash = new PublicKey(
|
|
|
|
+ computeDataHash(metadataArgs)
|
|
|
|
+ ).toBase58();
|
|
|
|
+ const computedCreatorHash = new PublicKey(
|
|
|
|
+ computeCreatorHash(metadataArgs.creators)
|
|
|
|
+ ).toBase58();
|
|
|
|
+ console.log("computedDataHash:", computedDataHash);
|
|
|
|
+ console.log("computedCreatorHash:", computedCreatorHash);
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ Add a single mint to collection instruction
|
|
|
|
+ ---
|
|
|
|
+ But you could all multiple in the same transaction, as long as your
|
|
|
|
+ transaction is still within the byte size limits
|
|
|
|
+ */
|
|
|
|
+ mintIxs.push(
|
|
|
|
+ createMintToCollectionV1Instruction(
|
|
|
|
+ {
|
|
|
|
+ payer: payer.publicKey,
|
|
|
|
+
|
|
|
|
+ merkleTree: treeAddress,
|
|
|
|
+ treeAuthority,
|
|
|
|
+ treeDelegate: payer.publicKey,
|
|
|
|
+
|
|
|
|
+ // set the receiver of the NFT
|
|
|
|
+ leafOwner: receiverAddress || payer.publicKey,
|
|
|
|
+ // set a delegated authority over this NFT
|
|
|
|
+ leafDelegate: payer.publicKey,
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ You can set any delegate address at mint, otherwise should
|
|
|
|
+ normally be the same as `leafOwner`
|
|
|
|
+ NOTE: the delegate will be auto cleared upon NFT transfer
|
|
|
|
+ ---
|
|
|
|
+ in this case, we are setting the payer as the delegate
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ // collection details
|
|
|
|
+ collectionAuthority: payer.publicKey,
|
|
|
|
+ collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
|
|
|
|
+ collectionMint: collectionMint,
|
|
|
|
+ collectionMetadata: collectionMetadata,
|
|
|
|
+ editionAccount: collectionMasterEditionAccount,
|
|
|
|
+
|
|
|
|
+ // other accounts
|
|
|
|
+ compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
|
|
|
|
+ logWrapper: SPL_NOOP_PROGRAM_ID,
|
|
|
|
+ bubblegumSigner: bubblegumSigner,
|
|
|
|
+ tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ metadataArgs,
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // construct the transaction with our instructions, making the `payer` the `feePayer`
|
|
|
|
+ const tx = new Transaction().add(...mintIxs);
|
|
|
|
+ tx.feePayer = payer.publicKey;
|
|
|
|
+
|
|
|
|
+ // send the transaction to the cluster
|
|
|
|
+ const txSignature = await sendAndConfirmTransaction(
|
|
|
|
+ connection,
|
|
|
|
+ tx,
|
|
|
|
+ [payer],
|
|
|
|
+ {
|
|
|
|
+ commitment: "confirmed",
|
|
|
|
+ skipPreflight: true,
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ console.log("\nSuccessfully minted the compressed NFT!");
|
|
|
|
+ console.log(explorerURL({ txSignature }));
|
|
|
|
+
|
|
|
|
+ return txSignature;
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error("\nFailed to mint compressed NFT:", err);
|
|
|
|
+
|
|
|
|
+ // log a block explorer link for the failed transaction
|
|
|
|
+ await extractSignatureFromFailedTransaction(connection, err);
|
|
|
|
+
|
|
|
|
+ throw err;
|
|
|
|
+ }
|
|
|
|
+}
|