Skip to content

Added example to mint compressed NFTs #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions compressed-nfts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

### Node Patch ###
# Serverless Webpack directories
.webpack/

# Optional stylelint cache

# SvelteKit build / generate output
.svelte-kit

# End of https://www.toptal.com/developers/gitignore/api/nodes
69 changes: 69 additions & 0 deletions compressed-nfts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

[Solana](https://solana.com) has a huge problem. Compressed NFTs are way too cheap!

# Compressed NFTs

Compressed NFTs are a new class of non-fungible tokens that utilize compression techniques to reduce the size of the digital assets they represent. By compressing the underlying data, these NFTs allow developers to store more information within the blockchain at a lower cost, while maintaining the integrity and uniqueness of the asset.

## Working of Compressed NFTs

Unlike traditional NFTs, which store all their metadata directly on the blockchain, compressed NFTs store their metadata in a special structure called a Merkle tree. This Merkle tree has a root, which is stored on the blockchain, and leaves, which are stored off-chain in the Solana ledger.

When a compressed NFT is created and confirmed on the blockchain, Solana RPC providers are used to handle and store the data associated with the NFT off-chain. This approach helps to reduce the storage costs on the blockchain.

To manage the data and enable efficient data queries between RPC providers and on-chain smart contracts, compressed NFTs use indexers. These indexers help organize and retrieve transaction data related to the compressed NFTs. It's important to note that existing smart contracts need to be modified to interact with compressed NFTs. However, if necessary, compressed NFTs can be decompressed to work with unmodified Solana programs.

## How do NFTs get compressed?

1. The Bubblegum program, developed by Metaplex, plays a role in verifying the metadata (information) linked to an NFT (non-fungible token).

2. When Bubblegum is activated, it uses a process called "account-compression" to add a new data point (called a "leaf") to the Merkle tree structure.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "invoked" instead of "activated"


3. This "account-compression" process updates the Merkle tree on the Solana blockchain, reflecting the latest state of the NFT ecosystem.

4. Whenever there are changes to the Merkle tree, such as the addition of new NFTs, these changes are recorded and written onto the Solana blockchain for everyone to see and verify.

Compressed NFT metadata is stored in a special structure called a Merkle tree, but this tree is not directly on the Solana blockchain. Instead, it is stored off-chain using Solana RPC service providers. These service providers help manage and store the metadata data for the compressed NFTs.

To make it more cost-effective, the actual data is stored off-chain, while only a small proof of that data, called the Merkle tree root, is stored on the Solana blockchain. This means that the blockchain contains a compact representation of the entire dataset.

By storing the data off-chain and keeping only the Merkle tree root on-chain, the cost of storing and processing the NFT metadata is significantly reduced. This allows for more efficient management of compressed NFTs while still maintaining the integrity and security of the data

## Cost of Compressed NFTs

In fact, the larger the collection of NFTs, the greater the reduction in minting costs with compression, as demonstrated in the image below:

![](https://assets-global.website-files.com/5f973c97cf5aea614f93a26c/63c92ed90d7c51169250d500_SpfybawohX9MOEVXckdnnSXCJYgYjt9LeoF5Em02yZDypwdK8F06LdfEGb1iyWUiJmCPQ017IfxhDZo5Mt6c-OMwX1V4jvRzy-C_K3JyLKS9yE4kbCUji3mXb0hoHEszj5603Jrgl3bPQLxaoI9dXiZBzuDhByyO7sk_dw-P7xU9Yd2c4bdTS4p2SQ-x7w.png)

Because the most significant outlay when minting conventional NFTs is paying for storage space on Solana, which compressed NFTs remove, the majority of the remaining costs are simple transaction fees.

## Minting Compressed NFTs

To mint the Compressed NFTs, first we have to store the metadata by creating a Merkle Tree.
1. For the **Merkle Tree** Account, generate a Keypair that starts with TRE (just for convenience, you can go on with any account).

```bash
solana-keygen grind --starts-with TRE:1
```

This will create an account into the directory that starts with *TRE*

2. Similarly, create accounts using solana-cli for compressed NFTs and Collection as well.
3. Replace the `TREE` with the keypair for Merkle Tree, `COLL` with your collection keypair and `CNFT` with your keypair that will hold these compressed NFTs.
4. Now, first create the Merkle tree by running -

```bash
ts-node createTree.ts
```

5. Then create an NFT collection (optional) by running

```bash
ts-node createCollectionNFT.ts
```

6. Or you can also mint 1:1 by

```bash
ts-node mintOneCNFT.ts
```
137 changes: 137 additions & 0 deletions compressed-nfts/createCollectionNFT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { createCreateMetadataAccountV3Instruction, createCreateMasterEditionV3Instruction, createSetCollectionSizeInstruction } from "@metaplex-foundation/mpl-token-metadata";
import { TOKEN_PROGRAM_ID, createAccount, createMint, mintTo } from "@solana/spl-token";
import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import {
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import { loadWalletKey } from "./utils";


// Creates a metaplex collection NFT
export const initCollection = async (
connection: Connection,
payer: Keypair
) => {
const cmintKey = loadWalletKey("COLL.json"); //load your collection account keypair

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔑 - I would rather see you generate keypairs using JavaScript code, than loading ones from local disk.

Users that are checking out these scripts should be able to clone, install dependencies (yarn | npm ) and then run the script and get their cNFT

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want users to directly clone and mint the cNFTs, should I add the keypair json to it?
I am generating keypairs using solana-cli tho

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't you do it with JavaScript code instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! That would be better. Let me check if I can generate using the keypairs with fixed starting letters like we do using grind in solana-cli

// const collectionMint = cmintKey.publicKey;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove this comment?

const collectionMint = await createMint(
connection,
payer,
payer.publicKey,
payer.publicKey,
0,
cmintKey,
{commitment: "finalized"},
TOKEN_PROGRAM_ID
);
console.log("1")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these logs really necessary?

const collectionTokenAccount = await createAccount(
connection, payer, collectionMint, payer.publicKey, undefined, {commitment: "finalized"}, TOKEN_PROGRAM_ID
);
console.log("2")
await mintTo(connection, payer, collectionMint, collectionTokenAccount, payer, 1, [], {commitment: "finalized"});

console.log("3")
const [collectionMetadataAccount, _b] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata", "utf8"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
collectionMint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
);
const collectionMeatadataIX = createCreateMetadataAccountV3Instruction(
{
metadata: collectionMetadataAccount,
mint: collectionMint,
mintAuthority: payer.publicKey,
payer: payer.publicKey,
updateAuthority: payer.publicKey,
},
{
createMetadataAccountArgsV3: {
data: {
name: "Minting cNFT",
symbol: "CNFT",
uri: "https://shdw-drive.genesysgo.net/QZNGUVnJgkw6sGQddwZVZkhyUWSUXAjXF9HQAjiVZ55/collection.json",
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null,
},
isMutable: true,
collectionDetails: null,
},
}, TOKEN_METADATA_PROGRAM_ID
);
const [collectionMasterEditionAccount, _b2] =
PublicKey.findProgramAddressSync(
[
Buffer.from("metadata", "utf8"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
collectionMint.toBuffer(),
Buffer.from("edition", "utf8"),
],
TOKEN_METADATA_PROGRAM_ID
);
const collectionMasterEditionIX = createCreateMasterEditionV3Instruction(
{
edition: collectionMasterEditionAccount,
mint: collectionMint,
mintAuthority: payer.publicKey,
payer: payer.publicKey,
updateAuthority: payer.publicKey,
metadata: collectionMetadataAccount,
tokenProgram: TOKEN_PROGRAM_ID
},
{
createMasterEditionArgs: {
maxSupply: 0,
},
},
TOKEN_METADATA_PROGRAM_ID
);

const sizeCollectionIX = createSetCollectionSizeInstruction(
{
collectionMetadata: collectionMetadataAccount,
collectionAuthority: payer.publicKey,
collectionMint: collectionMint,
},
{
setCollectionSizeArgs: { size: 10000 },
},
TOKEN_METADATA_PROGRAM_ID
);

let tx = new Transaction()
.add(collectionMeatadataIX)
.add(collectionMasterEditionIX)
.add(sizeCollectionIX);
try {
await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
});
console.log(
"Successfullyf created NFT collection with collection address: " +
collectionMint.toBase58()
);
return {
collectionMint,
collectionMetadataAccount,
collectionMasterEditionAccount,
};
} catch (e) {
console.error("Failed to init collection: ", e);
throw e;
}
};

async function main(){

const keypair = loadWalletKey("CNFT.json");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, see 🔑

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

initCollection(connection, keypair);
}
main();
Loading