From 5a51e37fae15359bc5421efd40f1a85a05d9dedc Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Thu, 15 Jun 2023 01:10:33 +0530 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8C=B2=20creating=20merkle=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compressed-nfts/createTree.ts | 61 +++++++++++++++++++++ compressed-nfts/mintOneCNFT.ts | 96 ++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 compressed-nfts/createTree.ts create mode 100644 compressed-nfts/mintOneCNFT.ts diff --git a/compressed-nfts/createTree.ts b/compressed-nfts/createTree.ts new file mode 100644 index 0000000..528f15a --- /dev/null +++ b/compressed-nfts/createTree.ts @@ -0,0 +1,61 @@ +import { createCreateTreeInstruction, PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum"; +import { loadWalletKey, sendVersionedTx } from "./utils"; +import { Connection, Keypair, PublicKey, SystemProgram, Transaction, VersionedMessage } from "@solana/web3.js"; +import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, ValidDepthSizePair, getConcurrentMerkleTreeAccountSize } from "@solana/spl-account-compression"; +import { SYSTEM_PROGRAM_ID } from "@raydium-io/raydium-sdk"; + +async function createTree() { + // Load the wallet key for the user who will create the merkle tree + const keypair = loadWalletKey("CNFT.json"); + + // Create a connection to the network + const connection = new Connection("https://api.devnet.solana.com"); + + // Load the wallet key for the merkle tree account + const merkleTree = loadWalletKey("TREE.json"); + + // Find the tree authority public key and bump seed + const [treeAuthority, _bump] = PublicKey.findProgramAddressSync( + [merkleTree.publicKey.toBuffer()], + BUBBLEGUM_PROGRAM_ID, + ); + + // Define the depth and buffer size of the merkle tree + const depthSizePair: ValidDepthSizePair = { + maxDepth: 14, + maxBufferSize: 64, + }; + + // Calculate the required account space for the merkle tree + const space = getConcurrentMerkleTreeAccountSize(depthSizePair.maxDepth, depthSizePair.maxBufferSize); + + // Create an account instruction to allocate space for the merkle tree + const createAccountIx = SystemProgram.createAccount({ + newAccountPubkey: merkleTree.publicKey, + fromPubkey: keypair.publicKey, + space: space, + lamports: await connection.getMinimumBalanceForRentExemption(space), + programId: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + }); + + // Create a merkle tree instruction + const createTreeIx = createCreateTreeInstruction({ + merkleTree: merkleTree.publicKey, + treeAuthority: treeAuthority, + payer: keypair.publicKey, + treeCreator: keypair.publicKey, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + logWrapper: SPL_NOOP_PROGRAM_ID, + systemProgram: SYSTEM_PROGRAM_ID, + }, { + maxDepth: depthSizePair.maxDepth, + maxBufferSize: depthSizePair.maxBufferSize, + public: false, + }); + + // Send the transaction with both instructions + const sx = await sendVersionedTx(connection, [createAccountIx, createTreeIx], keypair.publicKey, [keypair, merkleTree]); + console.log(sx); +} + +createTree(); diff --git a/compressed-nfts/mintOneCNFT.ts b/compressed-nfts/mintOneCNFT.ts new file mode 100644 index 0000000..6061989 --- /dev/null +++ b/compressed-nfts/mintOneCNFT.ts @@ -0,0 +1,96 @@ +import { createCreateTreeInstruction, PROGRAM_ID as BUBBLEGUM_PROGRAM_ID, createMintToCollectionV1Instruction, TokenProgramVersion } from "@metaplex-foundation/mpl-bubblegum"; +import { loadWalletKey, sendVersionedTx } from "./utils"; +import { Connection, Keypair, PublicKey, SystemProgram, Transaction, VersionedMessage } from "@solana/web3.js"; +import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, ValidDepthSizePair, getConcurrentMerkleTreeAccountSize } from "@solana/spl-account-compression"; +import { + PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID, +} from "@metaplex-foundation/mpl-token-metadata"; + +async function mintOneCNFT() { + // Load the wallet key for the user who will mint the CNFT + const keypair = loadWalletKey("CNFT.json"); + + // Create a connection to the Solana network + const connection = new Connection("https://api.devnet.solana.com"); + + // Load the wallet key for the merkle tree account + const merkleTree = loadWalletKey("TREE.json").publicKey; + + // load the Merkle Tree account + + // Find the tree authority public key and bump seed + const [treeAuthority, _bump] = PublicKey.findProgramAddressSync( + [merkleTree.toBuffer()], + BUBBLEGUM_PROGRAM_ID, + ); + + // Define the collection mint public key + const collectionMint = new PublicKey("COLL"); //Replace with your Collection Account + + // Find the collection metadata account public key + const [collectionMetadataAccount, _b1] = PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata", "utf8"), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + collectionMint.toBuffer(), + ], + TOKEN_METADATA_PROGRAM_ID + ); + + // Find the collection edition account public key + const [collectionEditionAccount, _b2] = PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata", "utf8"), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + collectionMint.toBuffer(), + Buffer.from("edition", "utf8"), + ], + TOKEN_METADATA_PROGRAM_ID + ); + + // Find the bubblegum signer public key + const [bgumSigner, __] = PublicKey.findProgramAddressSync( + [Buffer.from("collection_cpi", "utf8")], + BUBBLEGUM_PROGRAM_ID + ); + + // Create the mint-to-collection instruction + const ix = await createMintToCollectionV1Instruction({ + treeAuthority: treeAuthority, + leafOwner: keypair.publicKey, + leafDelegate: keypair.publicKey, + merkleTree: merkleTree, + payer: keypair.publicKey, + treeDelegate: keypair.publicKey, + logWrapper: SPL_NOOP_PROGRAM_ID, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + collectionAuthority: keypair.publicKey, + collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID, + collectionMint: collectionMint, + collectionMetadata: collectionMetadataAccount, + editionAccount: collectionEditionAccount, + bubblegumSigner: bgumSigner, + tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, + }, { + metadataArgs: { + collection: { key: collectionMint, verified: false }, + creators: [], + isMutable: true, + name: "Just a cNFT", + primarySaleHappened: true, + sellerFeeBasisPoints: 0, + symbol: "cNFT", + uri: "https://shdw-drive.genesysgo.net/QZNGUVnJgkw6sGQddwZVZkhyUWSUXAjXF9HQAjiVZ55/collection.json", + uses: null, + tokenStandard: null, + editionNonce: null, + tokenProgramVersion: TokenProgramVersion.Original + } + }); + + // Send the transaction with the mint-to-collection instruction + const sx = await sendVersionedTx(connection, [ix], keypair.publicKey, [keypair]); + console.log(sx); +} + +mintOneCNFT(); From 8babaae1c1325495629a3dc5b5698740e56715b2 Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Thu, 15 Jun 2023 01:12:31 +0530 Subject: [PATCH 2/5] create NFT Collection --- compressed-nfts/createCollectionNFT.ts | 137 +++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 compressed-nfts/createCollectionNFT.ts diff --git a/compressed-nfts/createCollectionNFT.ts b/compressed-nfts/createCollectionNFT.ts new file mode 100644 index 0000000..f96e46b --- /dev/null +++ b/compressed-nfts/createCollectionNFT.ts @@ -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 + // const collectionMint = cmintKey.publicKey; + const collectionMint = await createMint( + connection, + payer, + payer.publicKey, + payer.publicKey, + 0, + cmintKey, + {commitment: "finalized"}, + TOKEN_PROGRAM_ID + ); + console.log("1") + 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"); + const connection = new Connection("https://api.devnet.solana.com"); + + initCollection(connection, keypair); +} +main(); \ No newline at end of file From ff5139b1e817bc4439795a7a495929048688fd64 Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Thu, 15 Jun 2023 01:13:07 +0530 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=99=88=20added=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compressed-nfts/.gitignore | 144 +++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 compressed-nfts/.gitignore diff --git a/compressed-nfts/.gitignore b/compressed-nfts/.gitignore new file mode 100644 index 0000000..463942b --- /dev/null +++ b/compressed-nfts/.gitignore @@ -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 From e2b9d96d659d2881a69fc6ef33289f29c94b7e4b Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Thu, 15 Jun 2023 01:13:41 +0530 Subject: [PATCH 4/5] =?UTF-8?q?=E2=84=B9=EF=B8=8F=20Added=20README=20file?= =?UTF-8?q?=20with=20instructions=20and=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compressed-nfts/README.md | 69 ++++++++++++++++++++++++++++++++++++ compressed-nfts/package.json | 7 ++++ 2 files changed, 76 insertions(+) create mode 100644 compressed-nfts/README.md create mode 100644 compressed-nfts/package.json diff --git a/compressed-nfts/README.md b/compressed-nfts/README.md new file mode 100644 index 0000000..ed1d1c4 --- /dev/null +++ b/compressed-nfts/README.md @@ -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: + +![](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 +``` \ No newline at end of file diff --git a/compressed-nfts/package.json b/compressed-nfts/package.json new file mode 100644 index 0000000..920f604 --- /dev/null +++ b/compressed-nfts/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@metaplex-foundation/mpl-bubblegum": "^0.6.2", + "@raydium-io/raydium-sdk": "^1.3.0-beta.17", + "@solana/spl-token": "^0.3.7" + } +} From 1cd1ea4116e9de0484cd8685cd593019c257e469 Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Thu, 15 Jun 2023 01:13:54 +0530 Subject: [PATCH 5/5] utils.ts --- compressed-nfts/utils.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 compressed-nfts/utils.ts diff --git a/compressed-nfts/utils.ts b/compressed-nfts/utils.ts new file mode 100644 index 0000000..f49ed93 --- /dev/null +++ b/compressed-nfts/utils.ts @@ -0,0 +1,24 @@ +import { Connection, Keypair, PublicKey, Signer, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; + +export function loadWalletKey(keypairFile:string): Keypair { + const fs = require ("fs") + return Keypair. fromSecretKey( + new Uint8Array(JSON.parse(fs.readFileSync(keypairFile).toString())), + ); +} +export async function sendVersionedTx( + connection: Connection, + instructions: TransactionInstruction[], + payer: PublicKey, + signers: Signer[]){ + let latestBlockhash = await connection.getLatestBlockhash() + const messageLegacy = new TransactionMessage({ + payerKey: payer, + recentBlockhash: latestBlockhash.blockhash, + instructions, + }).compileToLegacyMessage(); + const transation = new VersionedTransaction(messageLegacy) + transation.sign(signers); + const signature = await connection.sendTransaction(transation); + return signature; + } \ No newline at end of file