什么是压缩NFT以及如何在Solana上铸造一个?

  • QuickNode
  • 发布于 2025-01-30 15:30
  • 阅读 17

本文介绍了在Solana上压缩NFT的概念及其实现方式,重点讲解了Merkle树及其构建方法,并提供了详细的代码示例,帮助开发者进行NFT的铸造和检索。文章还概述了所需的工具和依赖,包括将其与QuickNode集成的步骤,从而有效降低存储成本。

概述

随着对Solana上NFT需求的增长,存储成本的需求也在增加。在Solana上铸造成千上万的NFT可能需要数千美元的租金费用。如果你的业务案例需要数百万个NFT或者更多呢?状态压缩是一种工具,它使你能够将多个账户存储到单个账户中,从而减少存储成本。有效地说,这使我们能够使用Solana的分类帐来验证存储在链外的数据。我们可以通过使用称为Merkle树的加密概念来实现这一点。在本指南中,我们将学习Solana上的压缩和如何铸造及获取压缩NFT。

你将进行的操作

  • 了解Solana上的NFT压缩
  • 编写一个脚本,在Solana的devnet上铸造和获取压缩NFT

你将需要的工具

获取DAS附加功能

如果你还没有,需要一个具有Solana端点的QuickNode账户。你可以在这里注册账户。

新Solana节点

要使用DAS API,你需要使用安装了DAS附加功能的Solana端点。你可以在你的端点页面上安装DAS附加功能( https://dashboard.quicknode.com/endpoints/YOUR_ENDPOINT_ID/add-ons)。

DAS附加功能

在继续之前,请确保将数字资产标准附加功能添加到你的端点。

本指南中使用的依赖项

依赖项 版本
@metaplex-foundation/digital-asset-standard-api ^1.0.0
@metaplex-foundation/umi ^0.8.10
@metaplex-foundation/umi-bundle-defaults ^0.8.10
@metaplex-foundation/mpl-bubblegum ^3.1.2
@metaplex-foundation/mpl-token-metadata ^3.1.2
@solana/spl-account-compression ^0.2.0
@solana/spl-token ^0.3.11
@solana/web3.js ^1.87.6

让我们开始吧!

什么是压缩NFT?

压缩NFT利用加密技术有效地在区块链上存储和验证大量数据,该过程的两个关键概念是哈希函数和Merkle树。

  • 哈希:这是将输入(如NFT的元数据或关联的媒体文件)转换为称为哈希的固定大小字节字符串的过程。每个哈希都是唯一的;即使输入稍有变化,生成的哈希也会大相径庭,因此几乎不可能仅从哈希推断出原始输入。这一独特特性确保了在NFT数据发生变化时,能够立即被察觉。

  • Merkle树 是一种数据结构,用于以一种允许高效和安全地验证数据集内容的方式,存储大型数据集中的各个数据项的哈希。每个数据项(称为leaf)会被哈希,然后与另一个哈希配对生成新的哈希。这一过程重复进行,直到只剩下一个哈希,称为root。根哈希用于验证数据的完整性。

Merkle树来源:Solana & Metaplex基金会

在上面的简单示例图中,想象每个叶子(例如X8X9X10X11)作为单个NFT。 根哈希X2充当整个NFT集合的紧凑表示。你可以看到,root是通过哈希X4X5得来的:

  • X4是通过哈希X8X9得出的,并且
  • X5是通过哈希X10X11得出的。

要验证单个NFT,只需少量哈希反向追溯到根,而无需使用整个集合。这对于拥有数百或数千个NFT的集合特别有用,使转移和验证等操作的资源需求更少。

压缩NFT的关键概念

要创建压缩NFT,我们首先需要创建一个可以存储NFT数据的Merkle树。这样做需要了解几个关键参数:

  • 深度: 代表Merkle树中的层级数,从根节点到叶节点,每个叶节点可以是一个NFT。这最终由要存储在树中的NFT数量决定。树越深,可以存储的NFT数量越多,但验证单个NFT所需的哈希数量也越多。

  • 最大缓冲区大小: 由于在Solana上用户可能同时修改同一树中的多个NFT,因此我们需要能够支持对树的更改,而不会导致其中一项更改使另一项无效。Solana使用一种特殊类型的Merkle树,称为并发Merkle树,以支持这一点。maxBufferSize实质上为管理树的证明更新和更改设置了变更日志。

  • 树冠深度: 树冠是缓存和存储在链上的证明节点的数量。较大的树冠有助于减少验证NFT所需提取的证明数量。这里在成本和可组合性之间存在平衡。较大的树冠将减少需要提取的证明数量(因此使得程序更容易与NFT交互),但也会增加在链上存储树的成本。

有了这些,我们继续创建一个吧!

设置新项目

mkdir compressed-nft && cd compressed-nft && echo > app.ts

我们将使用来自Metaplex和Solana的几个包来创建和获取压缩NFT:

依赖项 描述
@metaplex-foundation/umi-bundle-defaults Metaplex的预配置Umi包。
@metaplex-foundation/umi Solana开发的核心Umi框架。
@metaplex-foundation/mpl-token-metadata MetaplexToken元数据合约库。
@metaplex-foundation/mpl-bubblegum Metaplex用于NFT压缩的库。
@solana/spl-account-compression 用于账户压缩的Solana程序库。
@solana/web3.js Solana的JavaScript API,用于与区块链交互。
@metaplex-foundation/digital-asset-standard-api 用于获取Solana数字资产数据的JS API。

安装必要的依赖项:

yarn init -y
yarn add @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-bubblegum @solana/spl-account-compression @solana/web3.js@1 @metaplex-foundation/digital-asset-standard-api

npm init -y
npm i @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-bubblegum @solana/spl-account-compression @solana/web3.js@1 @metaplex-foundation/digital-asset-standard-api

导入依赖项

在所选代码编辑器中打开app.ts,在第1行导入以下内容:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { none } from '@metaplex-foundation/umi';
import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata';
import {
    mplBubblegum,
    createTree,
    fetchTreeConfigFromSeeds,
    MetadataArgsArgs,
    mintV1,
    findLeafAssetIdPda
} from '@metaplex-foundation/mpl-bubblegum';
import {
    getConcurrentMerkleTreeAccountSize,
    ALL_DEPTH_SIZE_PAIRS,
} from "@solana/spl-account-compression";
import {
    PublicKey,
    Umi,
    createSignerFromKeypair,
    generateSigner,
    keypairIdentity
} from '@metaplex-foundation/umi';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { DasApiAsset, dasApi } from '@metaplex-foundation/digital-asset-standard-api';

这些导入将允许我们创建一个Umi的实例(Metaplex的JS框架)并使用Solana的压缩程序来创建和获取压缩NFT。

建立与Solana的连接

使用QuickNode端点连接到Solana集群

要在Solana上构建,你需要一个API端点来连接到网络。你可以使用公共节点或者部署和管理自己的基础设施;然而,如果你希望响应速度提高8倍,你可以将重担留给我们。

看一下为什么超过50%的Solana项目选择QuickNode,并在这里注册一个免费账户。我们将使用Solana Devnet端点。

复制HTTP提供者链接:

定义你的Solana端点(确保将端点替换为你自己的),并将其添加到导入的下方:

const endpoint = "https://example.solana-devnet.quiknode.pro/123456/";

创建Umi实例

要使用Metaplex的Bubblegum程序铸造压缩NFT,我们必须使用我们的端点创建一个Umi实例。Umi是一个简化与Solana区块链交互的JS框架,它提供了一套帮助你在Solana上构建的工具。首先,你需要一个密钥对来签署交易。你可以使用solana-keygen命令行工具创建一个新的密钥对,或使用现有的:

solana-keygen new --no-bip39-passphrase --outfile ./my-keypair.json

你应该会看到如下输出:

Generating a new keypair
Wrote new keypair to ./my-keypair.json
========================================================================
pubkey: E9tb...NzT7                         # 👈 这是你的公钥
========================================================================

前往QuickNode多链水龙头获取一些Devnet SOL,为你的账户提供资金。

在你的端点下,使用你的私钥创建Umi实例:

const umi = createUmi(endpoint)
    .use(mplTokenMetadata())
    .use(mplBubblegum());
    .use(dasApi());
const secret = new Uint8Array(/* 📋 在此粘贴你的私钥,例如 [0, 0, ... 0, 0] */);
const myKeypair = umi.eddsa.createKeypairFromSecretKey(secret);
const wallet = createSignerFromKeypair(umi, myKeypair);
umi.use(keypairIdentity(wallet));

我们的Umi实例将用于向devnet集群发送交易并查询DAS API。我们使用了四个插件:

  • mplTokenMetadata():此插件提供了一组与MetaplexToken元数据程序交互的方法。
  • mplBubblegum():此插件提供了一组与Metaplex Bubblegum程序(压缩NFT)交互的方法。
  • dasApi():此插件提供了一组与数字资产标准API交互的方法。
  • keypairIdentity():此插件提供了一组使用我们的密钥对在Solana区块链上签署和发送交易的方法。

定义NFT元数据

随意创建你自己的元数据或使用以下示例。在你的代码中添加一个metadata对象:

const metadata: MetadataArgsArgs = {
    name: 'QN Pixel',
    symbol: 'QNPIX',
    uri: "https://qn-shared.quicknode-ipfs.com/ipfs/QmQFh6WuQaWAMLsw9paLZYvTsdL5xJESzcoSxzb6ZU3Gjx",
    sellerFeeBasisPoints: 500,
    collection: none(),
    creators: [],
};

IPFS网关

如果你希望使用自己的元数据,可以用自定义元数据替换metadata对象。要将.json和图像文件上传到IPFS,你可以使用QuickNode IPFS网关

定义助手函数

我们创建了一对助手函数,帮助我们计算Merkle树的深度和缓冲区大小,并打印资产的详细信息。将以下代码添加到你的app.ts文件:

function calculateDepthForNFTs(nftCount: number): number {
    let depth = 0;
    while (2 ** depth < nftCount) {
        depth++;
    }
    return depth;
}

function calcuateMaxBufferSize(nodes: number): number {
    let defaultDepthPair = ALL_DEPTH_SIZE_PAIRS[0];
    let maxDepth = defaultDepthPair.maxDepth;
    const allDepthSizes = ALL_DEPTH_SIZE_PAIRS.flatMap(
        (pair) => pair.maxDepth,
    ).filter((item, pos, self) => self.indexOf(item) == pos);

    for (let i = 0; i <= allDepthSizes.length; i++) {
        if (Math.pow(2, allDepthSizes[i]) >= nodes) {
            maxDepth = allDepthSizes[i];
            break;
        }
    }
    return ALL_DEPTH_SIZE_PAIRS.filter((pair) => pair.maxDepth == maxDepth)?.[0]
        ?.maxBufferSize ?? defaultDepthPair.maxBufferSize;
}

async function printAsset(umi: Umi, assetId: PublicKey<string>, retries = 5, retryDelay = 5000) {
    while (retries > 0) {
        try {
            const asset = await umi.rpc.getAsset(assetId);
            printAssetDetails(asset, true, false);
            return;
        } catch (e) {
            await new Promise((resolve) => setTimeout(resolve, retryDelay));
            retries--;
        }
    }
}

function printAssetDetails(asset: DasApiAsset, showAttributes = true, showJson = false): void {
    const { name, token_standard: standard, attributes } = asset.content.metadata;
    const { compressed } = asset.compression;
    const { json_uri, files } = asset.content;

    const imgUrl = files?.find(file => file.mime === 'image/png' || file.mime === 'image/jpeg')?.uri;

    console.table({
        name,
        standard,
        compressed,
        json_uri,
        imgUrl
    });
    if (showAttributes && attributes) {
        console.table(attributes);
    }
    if (showJson) {
        console.log(JSON.stringify(asset, null, 2));
    }
}

尽管我们不会逐一详细介绍这些,但每个函数的简要概述如下:

  • calculateDepthForNFTs(): 该函数根据我们想要存储的NFT数量计算Merkle树的深度。通过找到大于或等于NFT数量的最小2的幂来进行计算。
  • calcuateMaxBufferSize(): 该函数根据树中的节点数计算Merkle树的最大缓冲区大小。利用预定义的深度-大小对(来自spl-account-compression包),ALL_DEPTH_SIZE_PAIRS来进行计算。
  • printAssetDetails(): 该函数解析并以易于阅读的格式将资产的详细信息打印到控制台。
  • printAsset(): 该函数从DAS API获取资产,包含重试逻辑。它会最多尝试获取资产5次,并在每次尝试之间延迟5秒——这在我们等待资产铸造和索引时非常有用。一旦找到,它将调用printAssetDetails()将资产日志到控制台。

概述你的主函数

将以下代码添加到你的app.ts文件,以概述我们将要创建的main函数:

const main = async ({ nftCount, umi, metadata }: { nftCount: number, umi: Umi, metadata: MetadataArgsArgs}) => {
    // 0 - 检查成本
    console.log(`👾 为 ${nftCount.toLocaleString()} 压缩NFT初始化Merkle树.`);

    // 1 - 创建Merkle树
    console.log(`   创建Merkle树...${merkleTree.publicKey.toString()}`);

    // 2 - 铸造NFT
    console.log(`🎨 铸造一个示例NFT`);

    // 3 - 获取NFT
    console.log(`   从链上获取(这可能需要几分钟)...`);
}

main({ nftCount: 10_000, umi, metadata }).catch(console.error);

此函数将是我们的脚本的主要入口点。它将负责:

  1. 创建Merkle树,
  2. 铸造NFT,以及
  3. 从链上获取NFT。

让我们来构建它!

预检查

在我们创建Merkle树之前,先检查创建和存储树的费用。我们可以通过计算树的深度和缓冲区大小来实现。将以下代码添加到你app.ts文件的main函数的适当部分:

    // 0 - 检查成本
    console.log(`👾 为 ${nftCount.toLocaleString()} 压缩NFT初始化Merkle树.`);

    const balance = await umi.rpc.getBalance(umi.payer.publicKey);
    console.log(`   钱包余额: ◎${(Number(balance.basisPoints) / LAMPORTS_PER_SOL).toLocaleString()}`);

    const merkleStructure = {
        maxDepth: calculateDepthForNFTs(nftCount),
        maxBufferSize: calcuateMaxBufferSize(nftCount),
        canopyDepth: 0,
    }

    const canopyDepth = merkleStructure.maxDepth > 20 ? merkleStructure.maxDepth - 10 :
        merkleStructure.maxDepth > 10 ? 10 :
            Math.floor(merkleStructure.maxDepth / 2);

    merkleStructure.canopyDepth = canopyDepth;

    console.log(`   最大深度: ${merkleStructure.maxDepth}`);
    console.log(`   最大缓冲区大小: ${merkleStructure.maxBufferSize}`);
    console.log(`   树冠深度: ${merkleStructure.canopyDepth}`);

    const requiredSpace = getConcurrentMerkleTreeAccountSize(
        merkleStructure.maxDepth,
        merkleStructure.maxBufferSize,
        merkleStructure.canopyDepth,
    );
    console.log(`   总大小: ${requiredSpace.toLocaleString()} 字节.`);

    const { basisPoints } = await umi.rpc.getRent(requiredSpace);
    const storageCost = Number(basisPoints);

    if (Number(balance.basisPoints) < storageCost) {
        throw new Error(`资金不足,需至少 ◎${(storageCost / LAMPORTS_PER_SOL).toLocaleString(undefined)} 用于存储`);
    }

    console.log(`   总费用: ◎ ${(storageCost / LAMPORTS_PER_SOL).toLocaleString(undefined)}`);

在这里,我们做了几件事:

  • 首先,我们使用umi.rpc.getBalance()检查我们的钱包余额。
  • 接下来,我们定义我们的Merkle Structure,merkleStructure,其中包括我们的Merkle树的深度、缓冲区大小和树冠深度。我们根据你希望铸造的NFT数量利用我们助手函数计算树冠深度和缓冲区大小。
  • 我们还创建了一些逻辑,以根据树的深度确定树冠深度。这是成本和可组合性之间的平衡,最终将取决于你项目的要求。
  • 最后,我们使用getConcurrentMerkleTreeAccountSize()umi.rpc.getRent()计算Merkle树所需的空间和租金。然后检查我们是否有足够的资金来支付存储成本。

让我们继续创造我们的Merkle树。

创建Merkle树

接下来,添加一些功能,从集群发送请求以创建树。然后,在树创建后,我们将获取其配置。将以下代码添加到你的main函数中:

    // 1 - 创建Merkle树
    const merkleTree = generateSigner(umi);
    console.log(`   创建Merkle树...${merkleTree.publicKey.toString()}`);

    const builder = await createTree(umi, {
        merkleTree,
        maxDepth: merkleStructure.maxDepth,
        maxBufferSize: merkleStructure.maxBufferSize,
        canopyDepth: merkleStructure.canopyDepth,
    });
    console.log(`   发送请求(这可能需要几分钟)...`);
    const { blockhash, lastValidBlockHeight } = await umi.rpc.getLatestBlockhash();
    await builder.sendAndConfirm(umi, {
        send: { commitment: 'finalized' },
        confirm: { strategy: { type: 'blockhash', blockhash, lastValidBlockHeight } },
    });

    let treeFound = false;
    while (!treeFound) {
        try {
            const treeConfig = await fetchTreeConfigFromSeeds(umi, {
                merkleTree: merkleTree.publicKey,
            });
            treeFound = true;
            console.log(`🌲 Merkle树创建: ${merkleTree.publicKey.toString()}. 配置:`)
            console.log(`     - 总铸造容量 ${Number(treeConfig.totalMintCapacity).toLocaleString()}`);
            console.log(`     - 已铸造数量: ${Number(treeConfig.numMinted).toLocaleString()}`);
            console.log(`     - 是否公开: ${treeConfig.isPublic}`);
            console.log(`     - 是否可解压: ${treeConfig.isDecompressible}`);
        } catch (error) {
            await new Promise((resolve) => setTimeout(resolve, 5000));
        }
    }

让我们逐步解释:

  • 首先,我们使用generateSigner()为Merkle树创建一个新的签名者。这将是保存Merkle树的账户。
  • 然后我们使用Umi的createTree()指令构建器创建新的Merkle树指令。我们将我们的merkleStructure作为参数传递给构建器。
  • 然后使用sendAndConfirm()将指令发送给集群。这将在Solana区块链上创建Merkle树。
  • 最后,我们使用fetchTreeConfigFromSeeds()获取树的配置。这将返回树的配置,包括其容量、铸造NFT的数量以及是否是公开和可解压的。我们设置了一个while循环,如果未发现树,将重试,因为树可能需要几分钟才能编入索引。

铸造NFT

很好——我们有了Merkle树,接下来可以铸造NFT。将以下代码添加到你的main函数中:

    // 2 - 铸造NFT
    console.log(`🎨 铸造一个示例NFT`);

    const leafOwner = generateSigner(umi).publicKey;
    await mintV1(umi, { leafOwner, merkleTree: merkleTree.publicKey, metadata }).sendAndConfirm(umi);
    const assetId = findLeafAssetIdPda(umi, { merkleTree: merkleTree.publicKey, leafIndex: 0 });
    console.log(`🍃 NFT铸造完成: ${assetId[0].toString()}`);

在这里,我们简单地为我们的新叶(将存储NFT的地方)创建一个新账户,并使用mintV1()铸造NFT。请注意,我们需要传递我们的leafOwnermerkleTree的公钥,以及NFT的metadata。铸造后,我们使用findLeafAssetIdPda()获取NFT的铸造地址(资产ID)。

获取NFT

最后,让我们使用DAS API从链上获取NFT。由于我们已经进行了大量设置,我们只需调用printAsset()并传入我们的assetId。将以下代码添加到你的main函数:

    // 3 - 获取NFT
    console.log(`   从链上获取(这可能需要几分钟)...`);
    await printAsset(umi, assetId[0]);

如果你想了解更多关于DAS API的信息,可以查看我们的DAS API文档DAS API指南

干得好!你可以在GitHub找到我们脚本的完整代码。

运行脚本

要运行脚本,请在终端中执行以下命令:

ts-node app.ts

你应该看到如下输出:

qn@guides compressed-nft % ts-node app
👾 为 10,000 压缩NFT初始化Merkle树.
   钱包余额: ◎4.533
   最大深度: 14
   最大缓冲区大小: 64
   树冠深度: 10
   总大小: 97,272 字节.
   总费用: ◎ 0.678
   创建Merkle树...H4tsJtvJqGaPAXkUJXqAfe3vsWp8zLewyKoscRkNyGpw
   发送请求(这可能需要几分钟)...
🌲 Merkle树创建: H4tsJtvJqGaPAXkUJXqAfe3vsWp8zLewyKoscRkNyGpw. 配置:
     - 总铸造容量 16,384
     - 已铸造数量: 0
     - 是否公开: false
     - 是否可解压: 1
🎨 铸造一个示例NFT
🍃 NFT铸造完成: 5BS5Tk2N7516RK5ZdUBqJRYHVNofexjLk6qdfTKEWuCx
   从链上获取(这可能需要几分钟)...
┌────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐
│  (index)   │                                           值                                            │
├────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│    name    │                                         'QN Pixel'                                         │
│  standard  │                                       'NonFungible'                                        │
│ compressed │                                            true                                            │
│  json_uri  │ 'https://qn-shared.quicknode-ipfs.com/ipfs/QmQFh6WuQaWAMLsw9paLZYvTsdL5xJESzcoSxzb6ZU3Gjx' │
│   imgUrl   │ 'https://qn-shared.quicknode-ipfs.com/ipfs/QmZkvx76VSidDznVhyRoPsRkJY6ujqrEMKte25ppAp9YV4' │
└────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────┬────────┬──────────────┐
│ (index) │ 值     │  trait_type  │
├─────────┼────────┼──────────────┤
│    0    │ 'Blue' │ '背景'       │
└─────────┴────────┴──────────────┘

🔥 干得不错!

继续构建

你已经成功在Solana上创建并获取压缩NFT!你现在可以使用此脚本作为构建你自己压缩NFT铸造应用的起点。

如果你有疑问、要讨论或者想分享你的构建经验,请在我们的DiscordTwitter上与我们联系!

我们 ❤️ 反馈!

让我们知道你的任何反馈或新的主题请求。我们很乐意听取你的意见。

资源

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。