How to mint compressed NFTs

La Loutre
13 min readApr 3, 2023

--

Compression is a new technology using Merkle tree wich allows to reduce network storage costs. In the case of NFTs, compression enables the mint of NFTs for a fraction of the cost as before.

Example of network storage cost reduction

You can learn more about compression here and here.

This tutorial aims to explain how to mint compressed NFTs. For this, we will explain how to create Merkle tree and how to mint NFTs into it and we will use Typescript.

Table of contents

Installation

To mint compressed NFTs, we will need some dependencies. To install them, run the following command:

npm install @solana/web3.js @solana/spl-token @solana/spl-account-compression @metaplex-foundation/mpl-bubblegum @metaplex-foundation/mpl-token-metadata

Create the Merkle tree

This section explains how to create the Merkle tree where our compressed NFTs will be stored. It should be noted that several trees can be used for one and the same collection. Moreover, it may be more economical to create 2 smaller trees than one large tree.

We will show the entire code and then talk through what is going on.

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
createCreateTreeInstruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
createAllocTreeIx,
ValidDepthSizePair,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import * as fs from "fs";

async function createTree() {
const connection = new Connection("https://api.devnet.solana.com");

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

const maxDepthSizePair: ValidDepthSizePair = {
maxDepth: 5,
maxBufferSize: 8,
};
const canopyDepth = 2;

const merkleTreeKeypair = Keypair.generate();

const allocTreeIx = await createAllocTreeIx(
connection,
merkleTreeKeypair.publicKey,
payerKeypair.publicKey,
maxDepthSizePair,
canopyDepth
);

const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[Buffer.from(merkleTreeKeypair.publicKey.toBytes())],
BUBBLEGUM_PROGRAM_ID
);

const createTreeIxAccounts = {
treeAuthority: treeAuthority,
merkleTree: merkleTreeKeypair.publicKey,
payer: payerKeypair.publicKey,
treeCreator: payerKeypair.publicKey,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
};

const createTreeIxArgs = {
maxDepth: maxDepthSizePair.maxDepth,
maxBufferSize: maxDepthSizePair.maxBufferSize,
public: false,
};

const createTreeIx = createCreateTreeInstruction(
createTreeIxAccounts,
createTreeIxArgs
);

const Tx = new Transaction().add(allocTreeIx).add(createTreeIx);

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
merkleTreeKeypair,
]);

const treeInfo = {
treeAddress: merkleTreeKeypair.publicKey.toBase58()
};
const data = JSON.stringify(treeInfo);
fs.writeFileSync("treeInfo.json", data);
console.log("File successfuly created!");
}

Okay let’s step through it:

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
createCreateTreeInstruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
createAllocTreeIx,
ValidDepthSizePair,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import * as fs from "fs";

We import first what we need.

const connection = new Connection("https://api.devnet.solana.com");

Then we define the Solana connection we want to use.

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

We get our keypair from the secret (here in Bytes). We do this because we have to sign and send our transaction later.

Note: Don’t forget to use your own private key and never show it!

const maxDepthSizePair: ValidDepthSizePair = {
maxDepth: 5,
maxBufferSize: 8,
};
const canopyDepth = 2;

We define here the parameters of our Merkle tree.

  • maxDepth: number of layers of our tree. It will determine the capacity of our tree, i.e. the number of compressed NFTs we can store in it. In fact, the capacity is equal to 2^maxDepth. For exemple, if you set the value to 3, you can store 2³=8 compressed NFTs.
  • maxBufferSize: maximum number of changes that can be made in one block.
  • canopyDepth: number of tree layers that will be store on-chain. maxDepth — canopyDepth is equal to the number of proofs you will have to send when you make changes in the tree (when you transfer a compressed NFT for example). So be careful not to set it too low because it might make your compressed NFTs not compatible with marketplaces or wallets because of the transaction size limit. If you are not sure of the value to give to canopyDepth you can set it to maxDepth-3. With this value, your compressed NFT will have the same level of “compatibility” as a traditional NFT.

Note: The pair of maxDepth and maxBufferSize can’t take any value. It must take one of the following values.

Valid pair of Depth & Buffer size
const merkleTreeKeypair = Keypair.generate();

We generate a keypair for our tree. In fact, the public key will be the address of our tree on-chain.

const allocTreeIx = await createAllocTreeIx(
connection,
merkleTreeKeypair.publicKey,
payerKeypair.publicKey,
maxDepthSizePair,
canopyDepth
);

We create the instruction to allocate enough on-chain space for our tree.

const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[Buffer.from(merkleTreeKeypair.publicKey.toBytes())],
BUBBLEGUM_PROGRAM_ID
);

We get the treeAuthority address wich is a Program Derived Address (PDA) with the address of our tree as seed. This address is required in the instruction we will use to create our tree.

const createTreeIxAccounts = {
treeAuthority: treeAuthority,
merkleTree: merkleTreeKeypair.publicKey,
payer: payerKeypair.publicKey,
treeCreator: payerKeypair.publicKey,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
};

const createTreeIxArgs = {
maxDepth: maxDepthSizePair.maxDepth,
maxBufferSize: maxDepthSizePair.maxBufferSize,
public: false,
};

const createTreeIx = createCreateTreeInstruction(
createTreeIxAccounts,
createTreeIxArgs
);

We define the required accounts and the arguments that we pass to the createCreateTreeInstruction function in order to create the instruction that will create our tree.

Note: We set public to false to prevent anyone from modifying our tree.

const Tx = new Transaction().add(allocTreeIx).add(createTreeIx);

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
merkleTreeKeypair,
]);

Here we create a new Transaction and add our instructions. Then, we send our transaction.

Note: The “tree” also signs the transaction.

const treeInfo = {
treeAddress: merkleTreeKeypair.publicKey.toBase58()
};
const data = JSON.stringify(treeInfo);
fs.writeFileSync("treeInfo.json", data);

In treeInfo, we store address of our tree since we will need it for the following. We write this information in a json file called treeInfo.json.

Create a NFT collection

This section explains how to create a NFT collection. It is not specific to compression so if you already know how to do it you can skip to the next section.

If not, we will use the Metaplex Standard. We will show the entire code and then talk through what is going on.

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
createAccount,
createMint,
mintTo,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
CreateMetadataAccountArgsV3,
createCreateMetadataAccountV3Instruction,
createCreateMasterEditionV3Instruction,
} from "@metaplex-foundation/mpl-token-metadata";
import * as fs from "fs";

async function createCollection() {
const connection = new Connection("https://api.devnet.solana.com");

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

const mint = await createMint(
connection,
payerKeypair,
payerKeypair.publicKey,
payerKeypair.publicKey,
0,
undefined,
{ commitment: "finalized" }
);

console.log("Mint address:", mint.toBase58());

const tokenAccount = await createAccount(
connection,
payerKeypair,
mint,
payerKeypair.publicKey,
undefined,
{ commitment: "finalized" }
);
console.log("Token account:", tokenAccount.toBase58());

const mintSignature = await mintTo(
connection,
payerKeypair,
mint,
tokenAccount,
payerKeypair,
1,
[],
{ commitment: "finalized" }
);
console.log("mint signature: ", mintSignature);

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

const createMetadataIxAccounts = {
metadata: metadataAccount,
mint: mint,
mintAuthority: payerKeypair.publicKey,
payer: payerKeypair.publicKey,
updateAuthority: payerKeypair.publicKey,
};

const collectionMetadata: CreateMetadataAccountArgsV3 = {
data: {
name: "your-collection-Name",
symbol: "your-collection-symbol",
uri: "your-collection-uri",
sellerFeeBasisPoints: 100,
creators: [
{
address: payerKeypair.publicKey,
verified: false,
share: 100,
},
],
collection: null,
uses: null,
},
isMutable: false,
collectionDetails: {
__kind: "V1",
size: 0,
},
};

const createMetadataIxArgs = {
createMetadataAccountArgsV3: collectionMetadata,
};

const createMetadataIx = createCreateMetadataAccountV3Instruction(
createMetadataIxAccounts,
createMetadataIxArgs
);

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

const createMasterIxAccounts = {
edition: masterEditionAccount,
mint: mint,
mintAuthority: payerKeypair.publicKey,
payer: payerKeypair.publicKey,
updateAuthority: payerKeypair.publicKey,
metadata: metadataAccount,
};

const createMasterEditionIx = createCreateMasterEditionV3Instruction(
createMasterIxAccounts,
{
createMasterEditionArgs: {
maxSupply: 0,
},
}
);

const Tx = new Transaction()
.add(createMetadataIx)
.add(createMasterEditionIx)

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
]);

console.log("Transaction signature: ", signature);

const collectionInfo = {
collectionMint: mint.toBase58(),
collectionMetadata: metadataAccount.toBase58(),
collectionMasterEdition: masterEditionAccount.toBase58(),
};

const data = JSON.stringify(collectionInfo);
fs.writeFileSync("collectionInfo.json", data);
console.log("File successfuly created!");
}

Okay let’s step through it:

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
createAccount,
createMint,
mintTo,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
CreateMetadataAccountArgsV3,
createCreateMetadataAccountV3Instruction,
createCreateMasterEditionV3Instruction,
} from "@metaplex-foundation/mpl-token-metadata";
import * as fs from "fs";

We import first what we need.

const connection = new Connection("https://api.devnet.solana.com");

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

Then we define the Solana connection we want to use and we get our keypair from the secret (here in Bytes).

Note: Don’t forget to use your own private key and never show it!

const mint = await createMint(
connection,
payerKeypair,
payerKeypair.publicKey,
payerKeypair.publicKey,
0,
undefined,
{ commitment: "finalized" }
);

console.log("Mint address:", mint.toBase58());

We create and initialize the mint address of our NFT collection. We display it in order to see the good progress of our script.

You can find the documentation of the createMint function here if you want to understand each parameters.

Note: We set the decimals at 0 because we want to create a NFT

const tokenAccount = await createAccount(
connection,
payerKeypair,
mint,
payerKeypair.publicKey,
undefined,
{ commitment: "finalized" }
);
console.log("Token account:", tokenAccount.toBase58());

We create and initialize the token account in which we will mint our NFT. We display it in order to see the good progress of our script.

You can find the documentation of the createAccount function here if you want to understand each parameters.

const mintSignature = await mintTo(
connection,
payerKeypair,
mint,
tokenAccount,
payerKeypair,
1,
[],
{ commitment: "finalized" }
);
console.log("mint signature: ", mintSignature);

We mint one token in our token account and we display the signature of the transaction to see the good progress of our script.

You can find the documentation of the mintTo function here if you want to understand each parameters.

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

We get the metadataAccount address wich is a Program Derived Address (PDA). It is in this account that we will store the metadata of our collection. You can learn more about this account here.

const createMetadataIxAccounts = {
metadata: metadataAccount,
mint: mint,
mintAuthority: payerKeypair.publicKey,
payer: payerKeypair.publicKey,
updateAuthority: payerKeypair.publicKey,
};

const collectionMetadata: CreateMetadataAccountArgsV3 = {
data: {
name: "your-collection-Name",
symbol: "your-collection-symbol",
uri: "your-collection-uri",
sellerFeeBasisPoints: 100,
creators: [
{
address: payerKeypair.publicKey,
verified: false,
share: 100,
},
],
collection: null,
uses: null,
},
isMutable: false,
collectionDetails: {
__kind: "V1",
size: 0,
},
};

const createMetadataIxArgs = {
createMetadataAccountArgsV3: collectionMetadata,
};

const createMetadataIx = createCreateMetadataAccountV3Instruction(
createMetadataIxAccounts,
createMetadataIxArgs
);

We define the required accounts and the arguments that we pass to the createCreateMetadataAccountV3Instruction function in order to create the instruction that will create our metadataAccount. You can find more info about this function here.

It is in the arguments that we will specify the information of our collection (name, creator(s), royalties, uri, etc). Don’t forget to modify it with the information of your collection.

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

We get the masterEditionAccount address wich is a Program Derived Address (PDA). It is this account that we will prove the Non-Fungibility of our token. You can learn more about this account here.

const createMasterIxAccounts = {
edition: masterEditionAccount,
mint: mint,
mintAuthority: payerKeypair.publicKey,
payer: payerKeypair.publicKey,
updateAuthority: payerKeypair.publicKey,
metadata: metadataAccount,
};

const createMetadataIxArgs = {
createMetadataAccountArgsV3: collectionMetadata,
};

const createMetadataIx = createCreateMetadataAccountV3Instruction(
createMetadataIxAccounts,
createMetadataIxArgs
);

We define the required accounts that we pass in the createCreateMasterEditionV3Instruction function in order to create the instruction that will create our masterEditionAccount. You can find more info about this function here.

const Tx = new Transaction()
.add(createMetadataIx)
.add(createMasterEditionIx)

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
]);

Here we create a new Transaction and add our instructions. Then, we send our transaction.

const collectionInfo = {
collectionMint: mint.toBase58(),
collectionMetadata: metadataAccount.toBase58(),
collectionMasterEdition: masterEditionAccount.toBase58(),
};

const data = JSON.stringify(collectionInfo);
fs.writeFileSync("collectionInfo.json", data);

In collectionInfo, we gather information from the collection such as the mint address, the Metadata account address and the Master Edition account address since we will need them for the following. We write this information in a json file called collectionInfo.json.

Mint compressed NFTs to the tree

In this section, we will explain how to mint compressed NFTs to our tree. In order to do that we will need:

  • the address of our tree
  • the mint address of our NFT collection
  • the address of the Metadata account of our NFT collection
  • the address of the Master Edition of our NFT collection
  • the metadata of the compressed NFTs to mint

So be sure to have this information.

We will show the entire code and then talk through what is going on.

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
MetadataArgs,
TokenProgramVersion,
TokenStandard,
createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

async function mintCompressedNFT() {
const connection = new Connection("https://api.devnet.solana.com");

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

const treeAddress = new PublicKey(
"paste-your-tree-address-here"
);
const collectionMint = new PublicKey(
"paste-your-collection-mint-address-here"
);
const collectionMetadata = new PublicKey(
"paste-your-collection-metadata-account-address-here"
);
const collectionMasterEdition = new PublicKey(
"paste-your-collection-master-edition-account-address-here"
);
const receiverAddress = new PublicKey(
"paste-the-receiver-address-here"
);

const compressedNFTMetadata: MetadataArgs = {
name: "your-NFT-Name",
symbol: "your-NFT-symbol",
uri: "your-NFT-uri",
creators: [
{
address: payerKeypair.publicKey,
verified: false,
share: 100,
},
],
editionNonce: 0,
uses: null,
collection: {
verified: false,
key: collectionMint,
},
primarySaleHappened: false,
sellerFeeBasisPoints: 100,
isMutable: false,
tokenProgramVersion: TokenProgramVersion.Original,
tokenStandard: TokenStandard.NonFungible,
};

const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[treeAddress.toBuffer()],
BUBBLEGUM_PROGRAM_ID
);

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

const createMintIxAccounts = {
merkleTree: treeAddress,
collectionAuthority: payerKeypair.publicKey,
collectionMint: collectionMint,
collectionMetadata: collectionMetadata,
editionAccount: collectionMasterEdition,
collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
treeAuthority: treeAuthority,
payer: payerKeypair.publicKey,
treeDelegate: payerKeypair.publicKey,
leafOwner: receiverAddress,
leafDelegate: receiverAddress,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumSigner: bubblegumSigner,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
};

const createMintIxArgs = {
metadataArgs: compressedNFTMetadata,
};

const mintIx = createMintToCollectionV1Instruction(
createMintIxAccounts,
createMintIxArgs
);

const Tx = new Transaction().add(mintIx);

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
]);

console.log("Transaction signature: ", signature);
}

Okay let’s step through it:

import {
Connection,
PublicKey,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
MetadataArgs,
TokenProgramVersion,
TokenStandard,
createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

We import first what we need.

const connection = new Connection("https://api.devnet.solana.com");

const payerKeypair = Keypair.fromSecretKey(
Uint8Array.from([
170, 253, 179, 56, 200, 101, 142, 90, 196, 51, 126, 187, 154, 219, 76, 204,
211, 117, 129, 71, 5, 106, 16, 242, 178, 90, 10, 74, 44, 110, 121, 144, 9, 99,
203, 120, 227, 221, 251, 160, 64, 114, 36, 140, 16, 53, 55, 8, 45, 183, 170,
241, 163, 34, 151, 60, 92, 97, 1, 60, 54, 230, 217, 162
])
);

Then we define the Solana connection we want to use and we get our keypair from the secret (here in Bytes).

Note: Don’t forget to use your own private key and never show it!

const treeAddress = new PublicKey(
"paste-your-tree-address-here"
);
const collectionMint = new PublicKey(
"paste-your-collection-mint-address-here"
);
const collectionMetadata = new PublicKey(
"paste-your-collection-metadata-account-address-here"
);
const collectionMasterEdition = new PublicKey(
"paste-your-collection-master-edition-account-address-here"
);

We define the addresses we need. If you have used the scripts of the first 2 sections, you will find them in the created json files.

const receiverAddress = new PublicKey(
"paste-the-receiver-address-here"
);

We define the adress of the receiver. It can be your address or a friend’s address if you want to airdrop your NFT.

const compressedNFTMetadata: MetadataArgs = {
name: "your-NFT-Name",
symbol: "your-NFT-symbol",
uri: "your-NFT-uri",
creators: [
{
address: payerKeypair.publicKey,
verified: false,
share: 100,
},
],
editionNonce: 0,
uses: null,
collection: {
verified: false,
key: collectionMint,
},
primarySaleHappened: false,
sellerFeeBasisPoints: 100,
isMutable: true,
tokenProgramVersion: TokenProgramVersion.Original,
tokenStandard: TokenStandard.NonFungible,
};

We set here the metadata of our NFT. Don’t forget to modify it with the metadata of your NFT. Basically you have to change the name, the symbol, the uri, the creators, the sellerFeeBasisPoints and the isMutable fields.

const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[treeAddress.toBuffer()],
BUBBLEGUM_PROGRAM_ID
);

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

We get the treeAuthority address as well as the bubblegumSigner address which are both Program Derived Addresses (PDAs). These addresses are mandatory in the function we will use to mint our compressed NFT.

const createMintIxAccounts = {
merkleTree: treeAddress,
collectionAuthority: payerKeypair.publicKey,
collectionMint: collectionMint,
collectionMetadata: collectionMetadata,
editionAccount: collectionMasterEdition,
collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
treeAuthority: treeAuthority,
payer: payerKeypair.publicKey,
treeDelegate: payerKeypair.publicKey,
leafOwner: receiverAddress,
leafDelegate: receiverAddress,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumSigner: bubblegumSigner,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
};

const createMintIxArgs = {
metadataArgs: compressedNFTMetadata,
};

const mintIx = createMintToCollectionV1Instruction(
createMintIxAccounts,
createMintIxArgs
);

We define the required accounts and the arguments that we pass to the createMintToCollectionV1Instruction function in order to create the instruction that will mint our compressed NFT.

const Tx = new Transaction().add(mintIx);

const signature = await sendAndConfirmTransaction(connection, Tx, [
payerKeypair,
]);

Here we create a new Transaction and add our instructions. Then, we send our transaction.

And voilà! You have just minted your first compressed NFT!

Complete code

By rewriting the scripts a bit and putting them together, it is possible to make scripts for production use. These are available here.

You will find 2 scripts (createTreeAndCollection.ts & mintCompressedNFT.ts) as well as a assets.json file (gathers the metadata of the NFT compressed to mint) that you will have to modify according to your needs.

Once modified, you will just have to run the first script to create your Merkle tree and your NFT collection and then run the second one to mint your compressed NFTs.

--

--