How to create compressed NFTs on Solana

In the evolving world of digital assets, Solana's Compressed NFTs offer a cost-effective, scalable solution for minting NFTs. This technology significantly lowers the expense of creating large NFT collections, addressing scalability challenges. This guide highlights the importance of Compressed NFTs in scaling NFT projects on Solana, contrasting them with traditional NFTs, and explaining their application across various domains like event tickets, gaming, rewards, digital art, and more. We also explore the technical aspects of creating Compressed NFTs, showcasing their transformative impact in the blockchain sphere and their role in driving efficient management and growth in the Web3 digital collectible market.

đź’ˇ Articles
14 August 2024
Article Image

In the burgeoning world of digital assets, scalability has often been a limiting factor, especially when dealing with Non-Fungible Tokens (NFTs). Solana blockchain, known for its fast and cost-efficient transactions, is introducing an innovative solution to this challenge: Compressed NFTs. This technology allows creators and platforms to mint NFTs at a fraction of the conventional cost, enabling the handling of collections at an unprecedented scale.

In this guide, we're going to dissect the mechanics of Compressed NFTs on Solana, demonstrating how they stand as a game-changer for anyone looking to significantly scale their NFT initiatives without the hefty price tag usually associated with such ventures.

Traditional Solana NFTs

Here's how traditional Solana NFTs come to life:

  • Create a new token “Mint”.
  • Create an associated token account ATA for that Mint.
  • Mint a single token into the ATA
  • Store the collection's metadata in an Account on-chain.

Given that the metadata is stored in an account on-chain and storage on Solana doesn't come cheap, you're looking at a hefty expense here.

Compressed NFTs

Enter Compressed NFTs. These ingenious creations store metadata off-chain within the ledger and employ State Compression and Merkle trees to slash storage costs for NFTs dramatically.

So, what’s the savings like? Picture this: A collection of one million NFTs could be minted for ~50 SOL, compared to a staggering ~12,000 SOL for a collection that’s not compressed.

When to use Compressed NFTs?

Contemplating minting a vast number of NFTs? Compressed NFTs are your go-to solution. Here are some prime examples:

  • Event Organizers: Platforms selling tickets as NFTs can churn out millions of Compressed NFTs without breaking a sweat.
  • Web3 Gaming Assets: Think in-game items, characters, or even real estate – all as unique NFTs, made affordable.
  • Rewards Platforms: Platforms that dish out NFTs as rewards for task completion: Perfect for user engagement, these can be anything from completing a game level to fulfilling a learning milestone on educational platforms.
  • Digital Art Platforms: For artists who produce work in large volumes or editions, compressed NFTs make it feasible to launch entire collections without the prohibitive costs.
  • Enterprise Use Cases: Consider a scenario where businesses use NFTs for proof of authenticity and ownership in supply chains. Compressed NFTs enable the minting of product related NFTs on a scale suitable for mass production.
  • Social Media Platforms: New-age social media where users can earn NFTs for content creation or engagement activities. Compressed NFTs ensure this system remains scalable and cost-effective.

In essence, anytime you’re looking at large-scale NFT minting, compressed NFTs are not just an option; they're the smart choice.

Creating Compressed NFTs with code.

Now let's get into the code, The general process of minting a compressed NFT:

  • Create a tree.
  • Create a collection.
  • Mint compressed NFTs to the tree

Creating a Tree

Here you have to make an important choice, choosing Max Depth and Max Buffer Size. This will determine how many cNFTs you can mint in your tree.

These two parameters cannot be chosen arbitrarily and have to be selected from a pre-defined set of values. Check out how to calculate them here.

Your tree size is set by 3 values, each serving a very specific purpose:

  1. maxDepth - used to determine how many NFTs we can have in the tree
  2. maxBufferSize - used to determine how many updates to your tree are possible in the same block
  3. canopyDepth - used to store a portion of the proof on chain, and as such is a large of cost and composability of your compressed NFT collection
const maxDepthSizePair: ValidDepthSizePair = {
    maxDepth: 14,
    maxBufferSize: 64,
};
const canopyDepth = maxDepthSizePair.maxDepth - 5;
const payer = Keypair.generate();
const treeKeypair = Keypair.generate();
const tree = await createTree(connection, payer, treeKeypair, maxDepthSizePair, canopyDepth);

async function createTree(
  connection: Connection,
  payer: Keypair,
  treeKeypair: Keypair,
  maxDepthSizePair: ValidDepthSizePair,
  canopyDepth: number = 0,
) {

  // derive the tree's authority (PDA), owned by Bubblegum
  const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
    [treeKeypair.publicKey.toBuffer()],
    BUBBLEGUM_PROGRAM_ID,
  );

  // 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,
      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
    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!");

    return { treeAuthority, treeAddress: treeKeypair.publicKey };
  } catch (err: any) {
    console.error("\nFailed to create merkle tree:", err);
    throw err;
  }
}

Creating a Collection

Now that we have a tree, we will mint a normal collection NFT using metaplex.

Here’s the complete Process:

  • Create a new token “Mint” with decimal 0.
  • Create an associated token account ATA for that Mint
  • Mint a single token into the ATA
  • Create a Metadata Account
  • Create a Master Addition Account
  • Create a Sized Collection
// define the metadata to be used for creating the NFT collection
  const collectionMetadataV3: CreateMetadataAccountArgsV3 = {
    ...The usual metadata of an NFT
  };

  const collection = await createCollection(connection, payer, collectionMetadataV3);

async function createCollection(
  connection: Connection,
  payer: Keypair,
  metadataV3: CreateMetadataAccountArgsV3,
) {

  const mint = await createMint(
    connection,
    payer,
    payer.publicKey,
    payer.publicKey,
    0,
  );

  const tokenAccount = await createAccount(
    connection,
    payer,
    mint,
    payer.publicKey,
  );

  await mintTo(
    connection,
    payer,
    mint,
    tokenAccount,
    payer,
    1,
    [],
    undefined,
    TOKEN_PROGRAM_ID,
  );

  const [metadataAccount, _bump] = PublicKey.findProgramAddressSync(
    [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
    TOKEN_METADATA_PROGRAM_ID,
  );

  const createMetadataIx = createCreateMetadataAccountV3Instruction(
    {
      metadata: metadataAccount,
      mint: mint,
      mintAuthority: payer.publicKey,
      payer: payer.publicKey,
      updateAuthority: payer.publicKey,
    },
    {
      createMetadataAccountArgsV3: metadataV3,
    },
  );

  const [masterEditionAccount, _bump2] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("metadata"),
      TOKEN_METADATA_PROGRAM_ID.toBuffer(),
      mint.toBuffer(),
      Buffer.from("edition"),
    ],
    TOKEN_METADATA_PROGRAM_ID,
  );

  const createMasterEditionIx = createCreateMasterEditionV3Instruction(
    {
      edition: masterEditionAccount,
      mint: mint,
      mintAuthority: payer.publicKey,
      payer: payer.publicKey,
      updateAuthority: payer.publicKey,
      metadata: metadataAccount,
    },
    {
      createMasterEditionArgs: {
        maxSupply: 0,
      },
    },
  );

  const collectionSizeIX = createSetCollectionSizeInstruction(
    {
      collectionMetadata: metadataAccount,
      collectionAuthority: payer.publicKey,
      collectionMint: mint,
    },
    {
      setCollectionSizeArgs: { size: 10000 },
    },
  );

  try {
    const tx = new Transaction()
      .add(createMetadataIx)
      .add(createMasterEditionIx)
      .add(collectionSizeIX);
    tx.feePayer = payer.publicKey;

    await sendAndConfirmTransaction(connection, tx, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

  } catch (err) {
    console.error("\nFailed to create collection:", err);
    throw err;
  }

  return { mint, tokenAccount, metadataAccount, masterEditionAccount };
}

Mint Compressed NFTs into the collection.

After creating the collection, we’re now ready to mint an NFT into our tree and it’s pretty straightforward using the createMintToCollectionV1Instruction helper provided to us by metaplex.

const compressedNFTMetadata: MetadataArgs = {
    ... The Usual NFT Metadata
  };

const receiver = Keypair.generate()

await mintCompressedNFT(
  connection,
  payer,
  treeKeypair.publicKey,
  collection.mint,
  collection.metadataAccount,
  collection.masterEditionAccount,
  compressedNFTMetadata,
  receiver.publicKey,
);

async function mintCompressedNFT(
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  collectionMint: PublicKey,
  collectionMetadata: PublicKey,
  collectionMasterEditionAccount: PublicKey,
  compressedNFTMetadata: MetadataArgs,
  receiverAddress: PublicKey,
) {
  const treeAuthority = PublicKey.findProgramAddressSync(
    [treeAddress.toBuffer()],
    BUBBLEGUM_PROGRAM_ID,
  )[0]

  const bubblegumSigner = PublicKey.findProgramAddressSync(
    [Buffer.from("collection_cpi")],
    BUBBLEGUM_PROGRAM_ID,
  )[0]

  const metadataArgs = Object.assign(compressedNFTMetadata, {
    collection: { key: collectionMint, verified: false },
  });

  const mintIx: TransactionInstruction =
    createMintToCollectionV1Instruction(
      {
        payer: payer.publicKey,
        merkleTree: treeAddress,
        treeAuthority,
        treeDelegate: payer.publicKey,
        leafOwner: receiverAddress,
        leafDelegate: receiverAddress,
        collectionAuthority: payer.publicKey,
        collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
        collectionMint: collectionMint,
        collectionMetadata: collectionMetadata,
        editionAccount: collectionMasterEditionAccount,
        compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
        logWrapper: SPL_NOOP_PROGRAM_ID,
        bubblegumSigner: bubblegumSigner,
        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
      },
      {
        metadataArgs,
      },
    )

  try {
    const tx = new Transaction().add(mintIx);
    tx.feePayer = payer.publicKey;

    const txSignature = await sendAndConfirmTransaction(connection, tx, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

    return txSignature;
  } catch (err) {
    console.error("\nFailed to mint compressed NFT:", err);
    throw err;
  }
}

Repository: compressed-nfts

Conclusion

Compressed NFTs, without a doubt, represent an ingenious evolution within the blockchain space, addressing critical issues of cost and scalability that have long been barriers in the NFT landscape. the decision between traditional NFTs and Compressed NFTs is more than a strategic choice; it's a pivotal business move that could significantly influence your project's scalability and economic feasibility.

Implementing such advanced technology, however, might seem daunting. That's where Antematter comes into play. If you're considering a blockchain solution capable of handling expansive growth or seeking expertise in seamlessly integrating Compressed NFTs into your business model, Antematter is equipped with the knowledge and tools to facilitate your transition into this new, efficient era of digital asset management.

By harnessing the power of Compressed NFTs, not only do you optimize operational costs, but you also position your venture at the forefront of the digital collectibles space, ready to meet the demands of the rapidly evolving world of Web3.

This article is written by Hamza Khalid , Full-Stack Engineer at Antematter.io