本文介绍了在Solana上压缩NFT的概念及其实现方式,重点讲解了Merkle树及其构建方法,并提供了详细的代码示例,帮助开发者进行NFT的铸造和检索。文章还概述了所需的工具和依赖,包括将其与QuickNode集成的步骤,从而有效降低存储成本。
随着对Solana上NFT需求的增长,存储成本的需求也在增加。在Solana上铸造成千上万的NFT可能需要数千美元的租金费用。如果你的业务案例需要数百万个NFT或者更多呢?状态压缩是一种工具,它使你能够将多个账户存储到单个账户中,从而减少存储成本。有效地说,这使我们能够使用Solana的分类帐来验证存储在链外的数据。我们可以通过使用称为Merkle树的加密概念来实现这一点。在本指南中,我们将学习Solana上的压缩和如何铸造及获取压缩NFT。
如果你还没有,需要一个具有Solana端点的QuickNode账户。你可以在这里注册账户。
要使用DAS API,你需要使用安装了DAS附加功能的Solana端点。你可以在你的端点页面上安装DAS附加功能( https://dashboard.quicknode.com/endpoints/YOUR_ENDPOINT_ID/add-ons
)。
在继续之前,请确保将数字资产标准附加功能添加到你的端点。
依赖项 | 版本 |
---|---|
@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利用加密技术有效地在区块链上存储和验证大量数据,该过程的两个关键概念是哈希函数和Merkle树。
哈希:这是将输入(如NFT的元数据或关联的媒体文件)转换为称为哈希的固定大小字节字符串的过程。每个哈希都是唯一的;即使输入稍有变化,生成的哈希也会大相径庭,因此几乎不可能仅从哈希推断出原始输入。这一独特特性确保了在NFT数据发生变化时,能够立即被察觉。
Merkle树 是一种数据结构,用于以一种允许高效和安全地验证数据集内容的方式,存储大型数据集中的各个数据项的哈希。每个数据项(称为leaf
)会被哈希,然后与另一个哈希配对生成新的哈希。这一过程重复进行,直到只剩下一个哈希,称为root
。根哈希用于验证数据的完整性。
来源:Solana & Metaplex基金会
在上面的简单示例图中,想象每个叶子(例如X8
、X9
、X10
、X11
)作为单个NFT。 根哈希X2
充当整个NFT集合的紧凑表示。你可以看到,root
是通过哈希X4
和X5
得来的:
X4
是通过哈希X8
和X9
得出的,并且X5
是通过哈希X10
和X11
得出的。要验证单个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上构建,你需要一个API端点来连接到网络。你可以使用公共节点或者部署和管理自己的基础设施;然而,如果你希望响应速度提高8倍,你可以将重担留给我们。
看一下为什么超过50%的Solana项目选择QuickNode,并在这里注册一个免费账户。我们将使用Solana Devnet端点。
复制HTTP提供者链接:
定义你的Solana端点(确保将端点替换为你自己的),并将其添加到导入的下方:
const endpoint = "https://example.solana-devnet.quiknode.pro/123456/";
要使用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区块链上签署和发送交易的方法。随意创建你自己的元数据或使用以下示例。在你的代码中添加一个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);
此函数将是我们的脚本的主要入口点。它将负责:
让我们来构建它!
在我们创建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()
检查我们的钱包余额。merkleStructure
,其中包括我们的Merkle树的深度、缓冲区大小和树冠深度。我们根据你希望铸造的NFT数量利用我们助手函数计算树冠深度和缓冲区大小。getConcurrentMerkleTreeAccountSize()
和umi.rpc.getRent()
计算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树的账户。createTree()
指令构建器创建新的Merkle树指令。我们将我们的merkleStructure
作为参数传递给构建器。sendAndConfirm()
将指令发送给集群。这将在Solana区块链上创建Merkle树。fetchTreeConfigFromSeeds()
获取树的配置。这将返回树的配置,包括其容量、铸造NFT的数量以及是否是公开和可解压的。我们设置了一个while
循环,如果未发现树,将重试,因为树可能需要几分钟才能编入索引。很好——我们有了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。请注意,我们需要传递我们的leafOwner
和merkleTree
的公钥,以及NFT的metadata
。铸造后,我们使用findLeafAssetIdPda()
获取NFT的铸造地址(资产ID)。
最后,让我们使用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铸造应用的起点。
如果你有疑问、要讨论或者想分享你的构建经验,请在我们的Discord或Twitter上与我们联系!
让我们知道你的任何反馈或新的主题请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!