压缩NFT(cNFT)
压缩 NFT (cNFT) 顾名思义,其结构占用的账户存储空间比传统 NFT 要少。压缩 NFT 利用一种称为“状态压缩”的概念,以大幅降低成本的方式存储数据。
Solana 的交易成本如此低廉,以至于大多数用户从未想过大规模铸造 NFT 的成本会有多高。使用 Token Metadata Program 设置和铸造 100 万个传统 NFT 的成本约为 24,000 SOL。相比之下,cNFT 可以构建为相同的设置和铸造成本为 10 SOL 或更低。这意味着任何大规模使用 NFT 的人都可以通过使用 cNFT 而不是传统 NFT 将成本降低 1000 倍以上。
然而,使用 cNFT 可能比较棘手。最终,使用它们所需的工具将充分脱离底层技术,以至于传统 NFT 和 cNFT 之间的开发人员体验将可以忽略不计。但就目前而言,您仍然需要了解底层拼图,所以让我们深入研究吧!
传统 NFT 的大部分成本都归结于账户存储空间。压缩 NFT 使用称为“状态压缩”的概念将数据存储在区块链的账本状态中,仅使用账户状态来存储数据的“指纹”或哈希值。此哈希值允许您以加密方式验证数据未被篡改。
为了存储哈希值并进行验证,我们使用一种称为并发 Merkle 树的特殊二叉树结构。这种树结构让我们可以以确定性的方式将数据哈希在一起,以计算出存储在链上的单个最终哈希值。这个最终哈希值比所有原始数据的总和要小得多,因此被称为“压缩”。此过程的步骤如下:
上面没有解决的一个问题是,如果无法从帐户中提取数据,如何使数据可用。由于此哈希过程发生在链上,因此所有数据都存在于账本状态中,理论上可以通过从源头重放整个链状态从原始交易中检索数据。但是,在交易发生时让索引器 跟踪和索引这些数据要简单得多(尽管仍然很复杂)。这确保了存在一个链下数据“缓存”,任何人都可以访问并随后根据链上根哈希进行验证。
这个过程非常复杂。我们将在下面介绍一些关键概念,但如果您不能立即理解,请不要担心。我们将在状态压缩课程中讨论更多理论,并在本课中主要关注 NFT 的应用。即使您不完全理解状态压缩难题的每个部分,您也将能够在本课结束时使用 cNFT。
Merkle 树是一种由单个哈希表示的二叉树结构。结构中的每个叶节点都是其内部数据的哈希,而每个分支都是其子叶哈希的哈希。反过来,分支也被哈希在一起,直到最终剩下一个最终的根哈希。
对叶数据的任何修改都会更改根哈希。当同一槽中的多个交易尝试修改叶数据时,这会导致问题。由于这些交易必须按顺序执行,因此除了第一个交易之外的所有交易都将失败,因为传入的根哈希和证明将被第一个要执行的交易无效。
并发Merkle 树是一种存储最新更改的安全更改日志以及其根哈希和派生该更改的证明的 Merkle 树。当同一时隙中的多个交易尝试修改叶数据时,更改日志可用作事实来源,以允许对树进行并发更改。
使用并发 Merkle 树时,有三个变量决定树的大小、创建树的成本以及可以对树进行的并发更改的数量:
最大深度是从任意叶子节点到树根节点的最大跳数。由于 Merkle 树是二叉树,因此每个叶子节点仅与另一个叶子节点相连。因此,逻辑上可以使用最大深度来计算树的节点数2 ^ maxDepth
。
最大缓冲区大小实际上是在根哈希仍然有效的情况下对单个插槽内的树进行的最大并发更改数。
树冠深度是指针对任何给定证明路径存储在链上的证明节点数。验证任何叶子都需要树的完整证明路径。完整证明路径由树的每一“层”的一个证明节点组成,即最大深度为 14 意味着有 14 个证明节点。每个证明节点都会为交易添加 32 个字节,因此如果不在链上缓存证明节点,大型树很快就会超过最大交易大小限制。
这三个值(最大深度、最大缓冲区大小和树冠深度)各有优缺点。增加其中任何一个值都会增加用于存储树的账户大小,从而增加创建树的成本。
选择最大深度相当简单,因为它直接关系到叶子的数量,因此也关系到您可以存储的数据量。如果您需要在一棵树上存储 100 万个 cNFT,请找到使以下表达式成立的最大深度:2^maxDepth {'>'} 1million
。答案是 20。
选择最大缓冲区大小实际上是一个吞吐量的问题:您需要多少个并发写入。
SPL 状态压缩程序的存在是为了使上述过程在整个 Solana 生态系统中可重复和可组合。它提供了初始化 Merkle 树、管理树叶(即添加、更新、删除数据)和验证树叶数据的指令。
状态压缩程序还利用了一个单独的“无操作”程序,其主要目的是通过将叶数据记录到分类账状态来使叶数据更容易索引。
Solana 账本是包含已签名交易的条目列表。理论上,这可以追溯到创世区块。这实际上意味着任何曾经放入交易的数据都存在于账本中。
当您想要存储压缩数据时,可以将其传递给状态压缩程序,然后对其进行哈希处理并将其作为“事件”发送到 Noop 程序。然后,哈希将存储在相应的并发 Merkle 树中。由于数据通过交易传递,甚至存在于 Noop 程序日志中,因此它将永远存在于账本状态中。
正常情况下,你通常可以通过获取相应账户来访问链上数据。然而,当使用状态压缩时,事情就没那么简单了。
如上所述,数据现在存在于账本状态中,而不是帐户中。最容易找到完整数据的地方是在 Noop 指令的日志中,但尽管这些数据在某种意义上将永远存在于账本状态中,但在一段时间后,很可能无法通过验证器访问。
为了节省空间并提高性能,验证器不会保留创世块中的每笔交易。您能够访问与您的数据相关的 Noop 指令日志的具体时间将因验证器而异,但如果您直接依赖指令日志,最终您将失去对它的访问权限。
从技术上讲,您可以将交易状态重播至创世块,但一般的团队不会这样做,而且这样做肯定不会有很好的性能。
相反,你应该使用索引器来观察发送到 Noop 程序的事件并将相关数据存储在链外。这样你就不必担心旧数据无法访问。
了解了理论背景之后,让我们将注意力转向本课的重点:如何创建 cNFT 收藏。
幸运的是,您可以使用 Solana Foundation、Solana 开发者社区和 Metaplex 创建的工具来简化此过程。具体来说,我们将通过 Metaplex 的 Umi 库使用@solana/spl-account-compression
SDK、Metaplex Bubblegum 程序。@metaplex-foundation/mpl-bubblegum
在开始之前,你将准备 NFT 元数据,就像使用 Candy Machine 一样。从本质上讲,NFT 只是具有遵循 NFT 标准的元数据的代币。换句话说,它应该是这样的:
{ "name": "My Collection", "symbol": "MC", "description": "My Collection description", "image": "https://lvvg33dqzykc2mbfa4ifua75t73tchjnfjbcspp3n3baabugh6qq.arweave.net/XWpt7HDOFC0wJQcQWgP9n_cxHS0qQik9-27CAAaGP6E", "attributes": [ { "trait_type": "Background", "value": "transparent" }, { "trait_type": "Shape", "value": "sphere" }, { "trait_type": "Resolution", "value": "1920x1920" } ]}
根据您的使用情况,您可能能够动态生成此文件,或者您可能希望事先为每个 cNFT 准备一个 JSON 文件。您还需要 JSON 引用的任何其他资产,例如image
上例中显示的 URL。
与有供应的同质化代币相比,NFT 本质上是独一无二的。但是,使用集合将同一系列生产的 NFT 绑定在一起非常重要。集合允许人们发现同一集合中的其他 NFT,并验证各个 NFT 是否确实是集合的成员(而不是其他人生产的类似产品)。
要让您的 cNFT 成为收藏品的一部分,您需要在开始铸造 cNFT 之前创建一个收藏品 NFT。这是一个传统的代币元数据程序 NFT,可作为将您的 cNFT 绑定到单个收藏品中的参考。创建此 NFT 的过程在我们的 NFT 与 Metaplex 课程中概述
const collectionMint = generateSigner(umi); await createNft(umi, { mint: collectionMint, name: `My Collection`, uri, sellerFeeBasisPoints: percentAmount(0), isCollection: true, // mint as collection NFT}).sendAndConfirm(umi);
现在我们开始偏离创建传统 NFT 时使用的过程。用于状态压缩的链上存储机制是一个代表并发 Merkle 树的帐户。此 Merkle 树帐户属于 SPL 状态压缩程序。在执行与 cNFT 相关的任何操作之前,您需要创建一个具有适当大小的空 Merkle 树帐户。
影响账户规模的变量包括:
前两个变量必须从现有的一组有效对中选择。下表显示了有效对以及可以用这些值创建的 cNFT 数量。
最大深度 | 最大缓冲区大小 | cNFT 的最大数量 |
---|---|---|
3 | 8 | 8 |
5 | 8 | 三十二 |
14 | 64 | 16,384 |
14 | 256 | 16,384 |
14 | 1,024 | 16,384 |
14 | 2,048 | 16,384 |
15 | 64 | 32,768 |
16 | 64 | 65,536 |
17 | 64 | 131,072 |
18 | 64 | 262,144 |
19 | 64 | 524,288 |
20 | 64 | 1,048,576 |
20 | 256 | 1,048,576 |
20 | 1,024 | 1,048,576 |
20 | 2,048 | 1,048,576 |
24 | 64 | 16,777,216 |
24 | 256 | 16,777,216 |
24 | 512 | 16,777,216 |
24 | 1,024 | 16,777,216 |
24 | 2,048 | 16,777,216 |
二十六 | 512 | 67,108,864 |
二十六 | 1,024 | 67,108,864 |
二十六 | 2,048 | 67,108,864 |
三十 | 512 | 1,073,741,824 |
三十 | 1,024 | 1,073,741,824 |
三十 | 2,048 | 1,073,741,824 |
请注意,树上可以存储的 cNFT 数量完全取决于最大深度,而缓冲区大小将决定同一时隙内可以对树进行的并发更改(铸造、转移等)的数量。换句话说,选择与您需要树容纳的 NFT 数量相对应的最大深度,然后根据您预计需要支持的流量选择最大缓冲区大小的选项之一。
接下来,选择树冠深度。增加树冠深度会增加 cNFT 的可组合性。每当您或其他开发人员的代码尝试验证 cNFT 时,代码都必须传入与树中的“层”数量相同的证明节点。因此,对于最大深度 20,您需要传入 20 个证明节点。这不仅很繁琐,而且由于每个证明节点都是 32 字节,因此可以非常快速地最大化交易大小。
例如,如果您的树的树冠深度非常低,NFT 市场可能只能支持简单的 NFT 转移,而无法支持您的 cNFT 的链上竞价系统。树冠有效地将证明节点缓存在链上,因此您不必将它们全部传递到交易中,从而允许进行更复杂的交易。
增加这三个值中的任何一个都会增加帐户的规模,从而增加创建帐户的成本。选择值时请权衡利弊。
一旦知道了这些值,就可以使用包createTree
中的方法 @metaplex-foundation/mpl-bubblegum
来创建树。此指令创建并初始化两个帐户:
Merkle Tree
账户——它保存着 Merkle 哈希值并用于验证存储数据的真实性。Tree Config
——它包含压缩 NFT 特有的附加数据,例如树创建者、树是否公开以及 其他字段——请参阅 Bubblehum 程序源代码。该mpl-bubblegum
软件包是一个插件,如果没有 Metaplex 的 Umi 库就无法使用。Umi 是一个由 Metaplex 创建的用于为链上程序制作 JS/TS 客户端的框架。
请注意,Umi 在许多概念上的实现与 web3.js 不同,包括 Keypairs、PublicKeys 和 Connections。不过,将这些项目的 web3.js 版本转换为 Umi 版本很容易。
首先,我们需要创建一个 Umi 实例
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { clusterApiUrl } from "@solana/web3.js"; const umi = createUmi(clusterApiUrl("devnet"));
上述代码初始化了一个空的 Umi 实例,没有附加任何签名者或插件。你可以 在此 Metaplex 文档页面上找到可用插件的详尽列表
下一部分是添加我们的导入并将签名者附加到我们的 Umi 实例。
import { dasApi } from "@metaplex-foundation/digital-asset-standard-api";import { createTree, mplBubblegum } from "@metaplex-foundation/mpl-bubblegum";import { keypairIdentity } from "@metaplex-foundation/umi";import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { getKeypairFromFile } from "@solana-developers/helpers";import { clusterApiUrl } from "@solana/web3.js"; const umi = createUmi(clusterApiUrl("devnet")); // load keypair from local file system// See https://github.com/solana-developers/helpers?tab=readme-ov-file#get-a-keypair-from-a-keypair-fileconst localKeypair = await getKeypairFromFile(); // convert to Umi compatible keypairconst umiKeypair = umi.eddsa.createKeypairFromSecretKey(localKeypair.secretKey); // load the MPL Bubblegum program, dasApi plugin and assign a signer to our umi instanceumi.use(keypairIdentity(umiKeypair)).use(mplBubblegum()).use(dasApi()); console.log("Loaded UMI with Bubblegum");
Umi 实例化后,我们就可以调用createTree
方法来实例化 Merkle 树和树配置账户了。
const merkleTree = generateSigner(umi);const builder = await createTree(umi, { merkleTree, maxDepth: 14, maxBufferSize: 64,});await builder.sendAndConfirm(umi);
提供的三个值,即merkleTree
、maxDepth
和maxBufferSize
是创建树所必需的,其余的是可选的。例如,tree creator
默认为 Umi 实例标识,而 `public 字段为 false。
当设置为 true 时,public
允许任何人从初始化树中进行铸造,如果设置为 false ,则只有树创建者才能从树中进行铸造。
请随意查看 create_tree 指令处理程序 和 create_tree 的预期帐户的代码。
初始化 Merkle 树帐户及其对应的 Bubblegum 树配置帐户后,就可以将 cNFT 铸造到树中。Bubblegum 库提供了两条指令,我们可以根据铸造的资产是否属于集合来使用它们。
这两条指令是
await mintV1(umi, { leafOwner, merkleTree, metadata: { name: "My Compressed NFT", uri: "https://example.com/my-cnft.json", sellerFeeBasisPoints: 0, // 0% collection: none(), creators: [ { address: umi.identity.publicKey, verified: false, share: 100 }, ], },}).sendAndConfirm(umi);
await mintToCollectionV1(umi, { leafOwner, merkleTree, collectionMint, metadata: { name: "My Compressed NFT", uri: "https://example.com/my-cnft.json", sellerFeeBasisPoints: 0, // 0% collection: { key: collectionMint, verified: false }, creators: [ { address: umi.identity.publicKey, verified: false, share: 100 }, ], },}).sendAndConfirm(umi);
这两个函数都需要您传入 NFT 元数据和铸造 cNFT 所需的账户列表,例如leafOwner
、merkleTree
账户等。
需要注意的是,cNFT不是SPL 代币。这意味着您的代码需要遵循不同的约定来处理 cNFT 功能,例如获取、查询、传输等。
从现有 cNFT 获取数据的最简单方法是使用 数字资产标准读取 API(Read API)。请注意,这与标准 JSON RPC 是分开的。要使用 Read API,您需要使用支持的 RPC 提供程序。Metaplex 维护着一个(可能不详尽的) 支持 DAS Read API 的 RPC 提供程序列表。
在本课中,我们将使用 Helius, 因为它们为 Devnet 提供免费支持。
您可能需要在 Umi 实例中更新您的 RPC 连接端点
const umi = createUmi( "https://devnet.helius-rpc.com/?api-key=YOUR-HELIUS-API-KEY",);
要使用 Read API 获取特定的 cNFT,您需要拥有 cNFT 的资产 ID。但是,在铸造 cNFT 后,您最多会获得两条信息:
唯一真正的保证是您将拥有交易签名。可以 从那里找到叶索引,但这涉及一些相当复杂的解析。简而言之,您必须从中检索相关指令日志Noop program
并解析它们以找到叶索引。我们将在以后的课程中更深入地介绍这一点。现在,我们假设您知道叶索引。
对于大多数铸币厂来说,这是一个合理的假设,因为铸币将由您的代码控制,并且可以按顺序设置,以便您的代码可以跟踪每个铸币厂将使用哪个索引。例如,第一个铸币厂将使用索引 0,第二个铸币厂将使用索引 1,等等。
获得叶索引后,您可以导出 cNFT 对应的资产 ID。使用 Bubblegum 时,资产 ID 是使用 Bubblegum 程序 ID 和以下种子导出的 PDA:
asset
以 utf8 编码表示的静态字符串索引器本质上是在Noop program
交易发生时观察交易日志,并存储经过哈希处理并存储在 Merkle 树中的 cNFT 元数据。这使它们能够在请求时显示该数据。索引器使用此资产 ID 来识别特定资产。
为了简单起见,您可以只使用findLeafAssetIdPda
Bubblegum 库中的辅助函数。
const [assetId, bump] = await findLeafAssetIdPda(umi, { merkleTree, leafIndex,});
有了资产 ID,获取 cNFT 就相当简单了。只需使用 getAsset
支持 RPC 提供程序和dasApi
库提供的方法:
const [assetId, bump] = await findLeafAssetIdPda(umi, { merkleTree, leafIndex,}); const rpcAsset = await umi.rpc.getAsset(assetId);
这将返回一个 JSON 对象,该对象全面概括了传统 NFT 的链上和链下元数据的组合。例如,您可以在 处找到 cNFT 属性content.metadata.attributes
,或在 处找到图像 content.files.uri
。
读取 API 还包括获取多个资产、按所有者、创建者查询等方法。例如,Helius 支持以下方法:
getAsset
getSignaturesForAsset
searchAssets
getAssetProof
getAssetsByOwner
getAssetsByAuthority
getAssetsByCreator
getAssetsByGroup
我们不会直接介绍其中的大部分内容,但请务必查看 Helius 文档 以了解如何正确使用它们。
就像标准 SPL 令牌传输一样,安全性至关重要。但是,SPL 令牌传输使验证传输权限变得非常容易。它内置于 SPL 令牌程序和标准签名中。压缩令牌的所有权更难验证。实际验证将在程序端进行,但您的客户端代码需要提供其他信息才能实现。
虽然 Bubblegum 有一个createTransferInstruction
辅助函数,但需要比平常更多的组装。具体来说,Bubblegum 程序需要验证 cNFT 的全部数据是否是客户端在进行转移之前所声明的。cNFT 的全部数据都经过哈希处理并存储为 Merkle 树上的单个叶子,而 Merkle 树只是树的所有叶子和分支的哈希。因此,您不能简单地告诉程序要查看哪个帐户并让它将该帐户authority
或owner
字段与交易签名者进行比较。
相反,您需要提供完整的 cNFT 数据以及任何未存储在树冠中的 Merkle 树证明信息。这样,程序就可以独立证明所提供的 cNFT 数据(以及 cNFT 所有者)是准确的。只有这样,程序才能安全地确定交易签名者是否应该被允许转移 cNFT。
从广义上讲,这涉及五个步骤:
AccountMeta
准备资产证明作为物品清单幸运的是,我们可以利用这个transfer
方法来处理所有这些步骤。
const assetWithProof = await getAssetWithProof(umi, assetId); await transfer(umi, { ...assetWithProof, leafOwner: currentLeafOwner, newLeafOwner: newLeafOwner.publicKey,}).sendAndConfirm(umi);
我们已经介绍了与 cNFT 交互所需的基本技能,但还没有完全全面。您还可以使用 Bubblegum 执行销毁、验证、委托等操作。我们不会介绍这些,但这些说明类似于铸造和转移过程。如果您需要此附加功能,请查看 Bubblegum 文档,了解如何利用它提供的辅助功能。
让我们开始练习创建和使用 cNFT。我们将一起构建一个尽可能简单的脚本,让我们能够从 Merkle 树中创建 cNFT 集合。
首先创建并初始化一个空的 NPM 项目并将目录更改为该项目。
mkdir cnft-demonpm init -ycd cnft-demo
安装所有必需的依赖项
npm i @solana/web3.js@1 @solana-developers/helpers@2.5.2 @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-bubblegum @metaplex-foundation/digital-asset-standard-api @metaplex-foundation/umi-bundle-defaults npm i --save-dev esrun
在第一个脚本中,我们将学习如何创建树,因此让我们创建文件create-tree.ts
mkdir src && touch src/create-tree.ts
这个 Umi 实例化代码会在很多文件中重复,因此请随意创建一个包装器文件来实例化它:
创建树.ts
import { dasApi } from "@metaplex-foundation/digital-asset-standard-api";import { createTree, mplBubblegum } from "@metaplex-foundation/mpl-bubblegum";import { generateSigner, keypairIdentity } from "@metaplex-foundation/umi";import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { getExplorerLink, getKeypairFromFile,} from "@solana-developers/helpers";import { clusterApiUrl } from "@solana/web3.js"; const umi = createUmi(clusterApiUrl("devnet")); // load keypair from local file system// See https://github.com/solana-developers/helpers?tab=readme-ov-file#get-a-keypair-from-a-keypair-fileconst localKeypair = await getKeypairFromFile(); // convert to Umi compatible keypairconst umiKeypair = umi.eddsa.createKeypairFromSecretKey(localKeypair.secretKey); // load the MPL Bubblegum program, dasApi plugin and assign a signer to our umi instanceumi.use(keypairIdentity(umiKeypair)).use(mplBubblegum()).use(dasApi());
在上面的代码中,我们从位于 的系统钱包中加载用户的密钥对钱包.config/solana/id.json
,实例化一个新的 Umi 实例并将密钥对分配给它。我们还将 Bubblegum 和 dasApi 插件也分配给它。
我们将从创建 Merkle 树账户开始。为此,我们将使用 createTree
Metaplex Bubblegum 程序中的方法。
此函数采用三个默认值
merkleTree
- Merkle 树账户地址maxDepth
- 确定树可容纳的最大叶子数量,从而确定树可包含的最大 cNFT 数量。maxBufferSize
- 确定树中可以并行发生多少个并发更改。您还可以提供可选字段,例如
treeCreator
- 树权限的地址,默认为当前 umi.identity
实例。public
- 确定除树创建者之外的其他人是否能够从树中铸造 cNFT。创建树.ts
const merkleTree = generateSigner(umi);const builder = await createTree(umi, { merkleTree, maxDepth: 14, maxBufferSize: 64,});await builder.sendAndConfirm(umi); let explorerLink = getExplorerLink("address", merkleTree.publicKey, "devnet");console.log(`Explorer link: ${explorerLink}`);console.log("Merkle tree address is :", merkleTree.publicKey);console.log("✅ Finished successfully!");
create-tree.ts
使用 esrun运行脚本
npx esrun create-tree.ts
请务必记住 Merkle 树地址,因为我们将在下一步铸造压缩 NFT 时使用它。
您的输出将类似于此
Explorer link: https://explorer.solana.com/address/ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h?cluster=devnetMerkle tree address is : ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h✅ Finished successfully!
恭喜!您已创建了一棵泡泡糖树。点击 Explorer 链接以确保该过程已成功完成,
Solana Explorer 包含有关创建的 Merkle 树的详细信息
信不信由你,这就是将树设置为压缩 NFT 所需要做的全部工作!现在让我们将注意力转向铸造。
首先,我们创建一个名为 的新文件mint-compressed-nft-to-collection.ts
,添加导入并实例化 Umi
mint-compressed-nft-to-collection.ts
import { dasApi } from "@metaplex-foundation/digital-asset-standard-api";import { findLeafAssetIdPda, LeafSchema, mintToCollectionV1, mplBubblegum, parseLeafFromMintToCollectionV1Transaction,} from "@metaplex-foundation/mpl-bubblegum";import { keypairIdentity, publicKey as UMIPublicKey,} from "@metaplex-foundation/umi";import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { getKeypairFromFile } from "@solana-developers/helpers";import { clusterApiUrl } from "@solana/web3.js"; const umi = createUmi(clusterApiUrl("devnet")); // load keypair from local file system// See https://github.com/solana-developers/helpers?tab=readme-ov-file#get-a-keypair-from-a-keypair-fileconst localKeypair = await getKeypairFromFile(); // convert to Umi compatible keypairconst umiKeypair = umi.eddsa.createKeypairFromSecretKey(localKeypair.secretKey); // load the MPL Bubblegum program, dasApi plugin and assign a signer to our umi instanceumi.use(keypairIdentity(umiKeypair)).use(mplBubblegum()).use(dasApi());
我将 回收已经在 Metaplex 课程中创建的 NFT 合集 ,但如果您想为本课程创建一个新的合集,请查看 此 repo 上的代码
信息
在我们的NFT 与 Metaplex 课程中找到创建 Metaplex Collection NFT 的代码。
要将压缩的 NFT 铸造到收藏品中,我们需要
leafOwner
- 压缩 NFT 的接收者merkleTree
- 我们在上一步中创建的 Merkle 树地址collection
- 我们的 cNFT 所属的收藏。这不是必需的,如果您的 cNFT 不属于任何收藏,则可以省略它。metadata
- 您的链下元数据。本课不会重点介绍如何准备元数据,但您可以查看 Metaplex 推荐的结构。我们的cNFT将使用我们之前准备好的结构。
nft.json
{ "name": "My NFT", "symbol": "MN", "description": "My NFT Description", "image": "https://lycozm33rkk5ozjqldiuzc6drazmdp5d5g3g7foh3gz6rz5zp7va.arweave.net/XgTss3uKlddlMFjRTIvDiDLBv6Pptm-Vx9mz6Oe5f-o", "attributes": [ { "trait_type": "Background", "value": "transparent" }, { "trait_type": "Shape", "value": "sphere" } ]}
把这些全部写进代码,我们将得到
mint-compressed-nft-to-collection.ts
const merkleTree = UMIPublicKey("ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h"); const collectionMint = UMIPublicKey( "D2zi1QQmtZR5fk7wpA1Fmf6hTY2xy8xVMyNgfq6LsKy1",); const uintSig = await( await mintToCollectionV1(umi, { leafOwner: umi.identity.publicKey, merkleTree, collectionMint, metadata: { name: "My NFT", uri: "https://chocolate-wet-narwhal-846.mypinata.cloud/ipfs/QmeBRVEmASS3pyK9YZDkRUtAham74JBUZQE3WD4u4Hibv9", sellerFeeBasisPoints: 0, // 0% collection: { key: collectionMint, verified: false }, creators: [ { address: umi.identity.publicKey, verified: false, share: 100, }, ], }, }).sendAndConfirm(umi),).signature; const b64Sig = base58.deserialize(uintSig);console.log(b64Sig);
第一个语句的区别在于我们返回代表交易签名的字节数组。
我们需要这个才能获得叶模式,并利用该模式得出资产 ID。
mint-压缩-nft-到-collection.ts
const leaf: LeafSchema = await parseLeafFromMintToCollectionV1Transaction( umi, uintSig,);const assetId = findLeafAssetIdPda(umi, { merkleTree, leafIndex: leaf.nonce,})[0];
一切就绪后,我们现在可以运行脚本了 mint-compressed-nft-to-collection.ts
npx esrun mint-compressed-nft-to-collection.ts
您的输出应该类似于
asset id: D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS✅ Finished successfully!
我们不会返回 Explorer 链接,因为该地址不存在于 Solana 状态,但由支持 DAS API 的 RPC 索引。
下一步,我们将查询该地址以获取 cNFT 详细信息。
现在我们已经编写了代码来铸造 cNFT,让我们看看是否真的可以获取它们的数据。
创建新文件fetch-cnft-details.ts
fetch-cnft-details.ts
导入我们的包并实例化 Umi。在这里我们最终将使用 umi.use(dasApi())
我们一直导入的。
在 Umi 的实例化中,我们将对连接端点进行更改并使用支持 DAS API 的 RPC。
请务必使用 Helius API 密钥进行更新,您可以从 开发人员仪表板页面获取该密钥
获取-cnft-details.ts
import { dasApi } from "@metaplex-foundation/digital-asset-standard-api";import { mplBubblegum } from "@metaplex-foundation/mpl-bubblegum";import { keypairIdentity, publicKey as UMIPublicKey,} from "@metaplex-foundation/umi";import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { getKeypairFromFile } from "@solana-developers/helpers"; const umi = createUmi( "https://devnet.helius-rpc.com/?api-key=YOUR-HELIUS-API-KEY",); // load keypair from local file system// See https://github.com/solana-developers/helpers?tab=readme-ov-file#get-a-keypair-from-a-keypair-fileconst localKeypair = await getKeypairFromFile(); // convert to Umi compatible keypairconst umiKeypair = umi.eddsa.createKeypairFromSecretKey(localKeypair.secretKey); // load the MPL Bubblegum program, dasApi plugin and assign a signer to our umi instanceumi.use(keypairIdentity(umiKeypair)).use(mplBubblegum()).use(dasApi());
获取压缩的 NFT 详细信息就像调用上一步中的getAsset
方法一样简单。assetId
获取-cnft-details.ts
const assetId = UMIPublicKey("D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS"); // @ts-ignoreconst rpcAsset = await umi.rpc.getAsset(assetId);console.log(rpcAsset);
让我们首先声明一个logNftDetails
以 treeAddress
和为参数的函数nftsMinted
。
我们的 console.log 的输出将输出
{ interface: 'V1_NFT', id: 'D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS', content: { '$schema': 'https://schema.metaplex.com/nft1.0.json', json_uri: 'https://chocolate-wet-narwhal-846.mypinata.cloud/ipfs/QmeBRVEmASS3pyK9YZDkRUtAham74JBUZQE3WD4u4Hibv9', files: [ [Object] ], metadata: { attributes: [Array], description: 'My NFT Description', name: 'My NFT', symbol: '', token_standard: 'NonFungible' }, links: { image: 'https://lycozm33rkk5ozjqldiuzc6drazmdp5d5g3g7foh3gz6rz5zp7va.arweave.net/XgTss3uKlddlMFjRTIvDiDLBv6Pptm-Vx9mz6Oe5f-o' } }, authorities: [ { address: '4sk8Ds1T4bYnN4j23sMbVyHYABBXQ53NoyzVrXGd3ja4', scopes: [Array] } ], compression: { eligible: false, compressed: true, data_hash: '2UgKwnTkguefRg3P5J33UPkNebunNMFLZTuqvnBErqhr', creator_hash: '4zKvSQgcRhJFqjQTeCjxuGjWydmWTBVfCB5eK4YkRTfm', asset_hash: '2DwKkMFYJHDSgTECiycuBApMt65f3N1ZwEbRugRZymwJ', tree: 'ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h', seq: 4, leaf_id: 3 }, grouping: [ { group_key: 'collection', group_value: 'D2zi1QQmtZR5fk7wpA1Fmf6hTY2xy8xVMyNgfq6LsKy1' } ], royalty: { royalty_model: 'creators', target: null, percent: 0, basis_points: 0, primary_sale_happened: false, locked: false }, creators: [ { address: '4kg8oh3jdNtn7j2wcS7TrUua31AgbLzDVkBZgTAe44aF', share: 100, verified: false } ], ownership: { frozen: false, delegated: false, delegate: null, ownership_model: 'single', owner: '4kg8oh3jdNtn7j2wcS7TrUua31AgbLzDVkBZgTAe44aF' }, supply: { print_max_supply: 0, print_current_supply: 0, edition_nonce: null }, mutable: true, burnt: false}
请记住,Read API 还包括获取多个资产、按所有者、创建者等进行查询等方法。请务必查看 Helius 文档 以了解可用的内容。
我们要添加到脚本中的最后一项是 cNFT 传输。与标准 SPL 代币传输一样,安全性至关重要。但是,与标准 SPL 代币传输不同的是,要构建具有任何类型的状态压缩的安全传输,执行传输的程序需要完整的资产数据。
幸运的是,我们可以用该方法获取资产数据getAssetWithProof
。
让我们首先创建一个新文件transfer-asset.ts
,并用实例化新 Umi 客户端的代码填充它。
转移资产.ts
import { dasApi } from "@metaplex-foundation/digital-asset-standard-api";import { getAssetWithProof, mplBubblegum, transfer,} from "@metaplex-foundation/mpl-bubblegum";import { keypairIdentity, publicKey as UMIPublicKey,} from "@metaplex-foundation/umi";import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";import { base58 } from "@metaplex-foundation/umi/serializers";import { getExplorerLink, getKeypairFromFile,} from "@solana-developers/helpers";import { clusterApiUrl } from "@solana/web3.js"; const umi = createUmi(clusterApiUrl("devnet")); // load keypair from local file system// See https://github.com/solana-developers/helpers?tab=readme-ov-file#get-a-keypair-from-a-keypair-fileconst localKeypair = await getKeypairFromFile(); // convert to Umi compatible keypairconst umiKeypair = umi.eddsa.createKeypairFromSecretKey(localKeypair.secretKey); // load the MPL Bubblegum program, dasApi plugin and assign a signer to our umi instanceumi.use(keypairIdentity(umiKeypair)).use(mplBubblegum()).use(dasApi());
我们尚未准备好转移资产。使用assetId
我们的 cNFT,我们可以transfer
从 Bubblegum 库中调用方法
转移资产.ts
const assetId = UMIPublicKey("D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS"); //@ts-ignoreconst assetWithProof = await getAssetWithProof(umi, assetId); let uintSig = await( await transfer(umi, { ...assetWithProof, leafOwner: umi.identity.publicKey, newLeafOwner: UMIPublicKey("J63YroB8AwjDVjKuxjcYFKypVM3aBeQrfrVmNBxfmThB"), }).sendAndConfirm(umi),).signature; const b64sig = base58.deserialize(uintSig); let explorerLink = getExplorerLink("transaction", b64sig, "devnet");console.log(`Explorer link: ${explorerLink}`);console.log("✅ Finished successfully!");
使用 运行我们的脚本npx esrun transfer-asset.ts
,如果成功的话应该输出类似这样的内容:
Explorer link: https://explorer.solana.com/tx/3sNgN7Gnh5FqcJ7ZuUEXFDw5WeojpwkDjdfvTNWy68YCEJUF8frpnUJdHhHFXAtoopsytzkKewh39Rf7phFQ2hCF?cluster=devnet✅ Finished successfully!
打开浏览器链接,滚动到底部查看你的交易日志,
Solana Explorer 显示转移 cnft 指令的日志
恭喜!现在您知道如何铸造、读取和传输 cNFT。如果您愿意,可以将最大深度、最大缓冲区大小和冠层深度更新为更大的值,只要您有足够的 Devnet SOL,此脚本将允许您铸造最多 10,000 个 cNFT,而成本仅为铸造 10,000 个传统 NFT 成本的一小部分。
在 Solana Explorer 上检查 cNFT!与以前一样,如果您遇到任何问题,您应该自行修复,但如果需要, 可以使用解决方案代码。
现在轮到你自己尝试一下这些概念了!我们目前不会给出过多的规定,但这里有一些想法:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!