-
Notifications
You must be signed in to change notification settings - Fork 39
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
base: main
Are you sure you want to change the base?
Changes from all commits
5a51e37
8babaae
ff5139b
e2b9d96
1cd1ea4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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. | ||
|
||
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: | ||
|
||
 | ||
|
||
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 | ||
``` |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't you do it with JavaScript code instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
There was a problem hiding this comment.
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"