{"author":{"address":"0x9cADD876165BF0BbDaCA076dDAf5A8A6925F0097","user":"https://learnblockchain.cn/people/669"},"content":{"body":"# [压缩 NFT](https://solana.com/zh/developers/courses/state-compression/compressed-nfts)\r\n\r\n[![Compressed NFTs](https://solana.com/\\_next/image?url=%2Fopengraph%2Fdevelopers%2Fcourses%2Fstate-compression%2Fcompressed-nfts\\\u0026w=3840\\\u0026q=75)](https://solana.com/zh/developers/courses/state-compression/compressed-nfts)\r\n\r\n## 概括[＃](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#summary)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#summary)\r\n\r\n* **压缩 NFT（cNFT）**使用**状态压缩对 NFT 数据进行哈希处理，并使用****并发 Merkle 树**结构将哈希存储在账户的链上 。\r\n* cNFT 数据哈希不能用于推断 cNFT 数据，但可以用来 **验证**您看到的 cNFT 数据是否正确。\r\n* 支持 RPC 提供商在 cNFT 铸造时对 cNFT 数据进行链下**索引**，以便您可以使用**Read API**访问数据\r\n* **Metaplex Bubblegum 程序**是**State Compression**程序之上的抽象，使您能够更简单地创建、铸造和管理 cNFT 集合。\r\n\r\n## 课程[​](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#lesson)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#lesson)\r\n\r\n压缩 NFT (cNFT) 顾名思义，其结构占用的账户存储空间比传统 NFT 要少。压缩 NFT 利用一种称为**“状态压缩”**的概念，以大幅降低成本的方式存储数据。\r\n\r\nSolana 的交易成本如此低廉，以至于大多数用户从未想过大规模铸造 NFT 的成本会有多高。使用 Token Metadata Program 设置和铸造 100 万个传统 NFT 的成本约为 24,000 SOL。相比之下，cNFT 可以构建为相同的设置和铸造成本为 10 SOL 或更低。这意味着任何大规模使用 NFT 的人都可以通过使用 cNFT 而不是传统 NFT 将成本降低 1000 倍以上。\r\n\r\n然而，使用 cNFT 可能比较棘手。最终，使用它们所需的工具将充分脱离底层技术，以至于传统 NFT 和 cNFT 之间的开发人员体验将可以忽略不计。但就目前而言，您仍然需要了解底层拼图，所以让我们深入研究吧！\r\n\r\n### [cNFT](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#a-theoretical-overview-of-cnfts)的理论概述[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#a-theoretical-overview-of-cnfts)\r\n\r\n传统 NFT 的大部分成本都归结于账户存储空间。压缩 NFT 使用称为“状态压缩”的概念将数据存储在区块链的**账本状态**中，仅使用账户状态来存储数据的“指纹”或**哈希值**。此哈希值允许您以加密方式验证数据未被篡改。\r\n\r\n**为了存储哈希值并进行验证，我们使用一种称为并发 Merkle 树的**特殊二叉树结构。这种树结构让我们可以以确定性的方式将数据哈希在一起，以计算出存储在链上的单个最终哈希值。这个最终哈希值比所有原始数据的总和要小得多，因此被称为“压缩”。此过程的步骤如下：\r\n\r\n1. 取任意数据\r\n2. 创建此数据的哈希值\r\n3. 将此哈希作为“叶子”存储在树的底部\r\n4. 然后将每对叶子节点哈希合并在一起，创建一个“分支”\r\n5. 然后将每个分支散列在一起\r\n6. 不断爬树，并将相邻的树枝连接在一起\r\n7. 一旦到达树的顶部，就会产生最终的“根哈希”\r\n8. 将根哈希存储在链上，作为每个叶子内数据的可验证证明\r\n9. 任何想要验证自己拥有的数据是否与“事实来源”相符的人都可以经历相同的过程并比较最终的哈希值，而不必将所有数据存储在链上\r\n\r\n上面没有解决的一个问题是，如果无法从帐户中提取数据，如何使数据可用。由于此哈希过程发生在链上，因此所有数据都存在于账本状态中，理论上可以通过从源头重放整个链状态从原始交易中检索数据。但是，在交易发生时让**索引器** 跟踪和索引这些数据要简单得多（尽管仍然很复杂）。这确保了存在一个链下数据“缓存”，任何人都可以访问并随后根据链上根哈希进行验证。\r\n\r\n这个过程*非常复杂*。我们将在下面介绍一些关键概念，但如果您不能立即理解，请不要担心。我们将在状态压缩课程中讨论更多理论，并在本课中主要关注 NFT 的应用。即使您不完全理解状态压缩难题的每个部分，您也将能够在本课结束时使用 cNFT。\r\n\r\n#### 并发 Merkle[树](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#concurrent-merkle-trees)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#concurrent-merkle-trees)\r\n\r\n**Merkle 树**是一种由单个哈希表示的二叉树结构。结构中的每个叶节点都是其内部数据的哈希，而每个分支都是其子叶哈希的哈希。反过来，分支也被哈希在一起，直到最终剩下一个最终的根哈希。\r\n\r\n对叶数据的任何修改都会更改根哈希。当同一槽中的多个交易尝试修改叶数据时，这会导致问题。由于这些交易必须按顺序执行，因此除了第一个交易之外的所有交易都将失败，因为传入的根哈希和证明将被第一个要执行的交易无效。\r\n\r\n并发**Merkle 树**是一种存储最新更改的安全更改日志以及其根哈希和派生该更改的证明的 Merkle 树。当同一时隙中的多个交易尝试修改叶数据时，更改日志可用作事实来源，以允许对树进行并发更改。\r\n\r\n使用并发 Merkle 树时，有三个变量决定树的大小、创建树的成本以及可以对树进行的并发更改的数量：\r\n\r\n1. 最大深度\r\n2. 最大缓冲区大小\r\n3. 冠层深度\r\n\r\n最大**深度**是从任意叶子节点到树根节点的最大跳数。由于 Merkle 树是二叉树，因此每个叶子节点仅与另一个叶子节点相连。因此，逻辑上可以使用最大深度来计算树的节点数`2 ^ maxDepth`。\r\n\r\n**最大缓冲区大小**实际上是在根哈希仍然有效的情况下对单个插槽内的树进行的最大并发更改数。\r\n\r\n树冠**深度**是指针对任何给定证明路径存储在链上的证明节点数。验证任何叶子都需要树的完整证明路径。完整证明路径由树的每一“层”的一个证明节点组成，即最大深度为 14 意味着有 14 个证明节点。每个证明节点都会为交易添加 32 个字节，因此如果不在链上缓存证明节点，大型树很快就会超过最大交易大小限制。\r\n\r\n这三个值（最大深度、最大缓冲区大小和树冠深度）各有优缺点。增加其中任何一个值都会增加用于存储树的账户大小，从而增加创建树的成本。\r\n\r\n选择最大深度相当简单，因为它直接关系到叶子的数量，因此也关系到您可以存储的数据量。如果您需要在一棵树上存储 100 万个 cNFT，请找到使以下表达式成立的最大深度：`2^maxDepth {'\u003e'} 1million`。答案是 20。\r\n\r\n选择最大缓冲区大小实际上是一个吞吐量的问题：您需要多少个并发写入。\r\n\r\n#### SPL 状态压缩和 Noop 程序[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#spl-state-compression-and-noop-programs)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#spl-state-compression-and-noop-programs)\r\n\r\nSPL 状态压缩程序的存在是为了使上述过程在整个 Solana 生态系统中可重复和可组合。它提供了初始化 Merkle 树、管理树叶（即添加、更新、删除数据）和验证树叶数据的指令。\r\n\r\n状态压缩程序还利用了一个单独的“无操作”程序，其主要目的是通过将叶数据记录到分类账状态来使叶数据更容易索引。\r\n\r\n#### 使用账本状态进行[存储](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#use-the-ledger-state-for-storage)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#use-the-ledger-state-for-storage)\r\n\r\nSolana 账本是包含已签名交易的条目列表。理论上，这可以追溯到创世区块。这实际上意味着任何曾经放入交易的数据都存在于账本中。\r\n\r\n当您想要存储压缩数据时，可以将其传递给状态压缩程序，然后对其进行哈希处理并将其作为“事件”发送到 Noop 程序。然后，哈希将存储在相应的并发 Merkle 树中。由于数据通过交易传递，甚至存在于 Noop 程序日志中，因此它将永远存在于账本状态中。\r\n\r\n#### 索引数据以便于查找[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#index-data-for-easy-lookup)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#index-data-for-easy-lookup)\r\n\r\n正常情况下，你通常可以通过获取相应账户来访问链上数据。然而，当使用状态压缩时，事情就没那么简单了。\r\n\r\n如上所述，数据现在存在于账本状态中，而不是帐户中。最容易找到完整数据的地方是在 Noop 指令的日志中，但尽管这些数据在某种意义上将永远存在于账本状态中，但在一段时间后，很可能无法通过验证器访问。\r\n\r\n为了节省空间并提高性能，验证器不会保留创世块中的每笔交易。您能够访问与您的数据相关的 Noop 指令日志的具体时间将因验证器而异，但如果您直接依赖指令日志，最终您将失去对它的访问权限。\r\n\r\n从技术上讲，您*可以*将交易状态重播至创世块，但一般的团队不会这样做，而且这样做肯定不会有很好的性能。\r\n\r\n相反，你应该使用索引器来观察发送到 Noop 程序的事件并将相关数据存储在链外。这样你就不必担心旧数据无法访问。\r\n\r\n### [创建cNFT](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-a-cnft-collection)收藏[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-a-cnft-collection)\r\n\r\n了解了理论背景之后，让我们将注意力转向本课的重点：如何创建 cNFT 收藏。\r\n\r\n幸运的是，您可以使用 Solana Foundation、Solana 开发者社区和 Metaplex 创建的工具来简化此过程。具体来说，我们将通过 Metaplex 的 Umi 库使用`@solana/spl-account-compression`SDK、Metaplex Bubblegum 程序。`@metaplex-foundation/mpl-bubblegum`\r\n\r\n#### 准备元[数据](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#prepare-metadata)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#prepare-metadata)\r\n\r\n在开始之前，你将准备 NFT 元数据，就像使用 Candy Machine 一样。从本质上讲，NFT 只是具有遵循 NFT 标准的元数据的代币。换句话说，它应该是这样的：\r\n\r\n```\r\n{  \"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\"    }  ]}\r\n```\r\n\r\n根据您的使用情况，您可能能够动态生成此文件，或者您可能希望事先为每个 cNFT 准备一个 JSON 文件。您还需要 JSON 引用的任何其他资产，例如`image`上例中显示的 URL。\r\n\r\n#### 创建收藏 NFT [#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-collection-nft)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-collection-nft)\r\n\r\n与有供应的同质化代币相比，NFT 本质上是独一无二的。但是，使用集合将同一系列生产的 NFT 绑定在一起非常重要。集合允许人们发现同一集合中的其他 NFT，并验证各个 NFT 是否确实是集合的成员（而不是其他人生产的类似产品）。\r\n\r\n要让您的 cNFT 成为收藏品的一部分，您需要**在**开始铸造 cNFT 之前创建一个收藏品 NFT。这是一个传统的代币元数据程序 NFT，可作为将您的 cNFT 绑定到单个收藏品中的参考。创建此 NFT 的过程在我们的 [NFT 与 Metaplex 课程中概述](https://solana.com/zh/developers/courses/tokens-and-nfts/nfts-with-metaplex#add-the-nft-to-a-collection)\r\n\r\n```\r\nconst collectionMint = generateSigner(umi); await createNft(umi, {  mint: collectionMint,  name: `My Collection`,  uri,  sellerFeeBasisPoints: percentAmount(0),  isCollection: true, // mint as collection NFT}).sendAndConfirm(umi);\r\n```\r\n\r\n#### 创建 Merkle 树[帐户](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-merkle-tree-account)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#create-merkle-tree-account)\r\n\r\n现在我们开始偏离创建传统 NFT 时使用的过程。用于状态压缩的链上存储机制是一个代表并发 Merkle 树的帐户。此 Merkle 树帐户属于 SPL 状态压缩程序。在执行与 cNFT 相关的任何操作之前，您需要创建一个具有适当大小的空 Merkle 树帐户。\r\n\r\n影响账户规模的变量包括：\r\n\r\n1. 最大深度\r\n2. 最大缓冲区大小\r\n3. 冠层深度\r\n\r\n前两个变量必须从现有的一组有效对中选择。下表显示了有效对以及可以用这些值创建的 cNFT 数量。\r\n\r\n| 最大深度 | 最大缓冲区大小 | cNFT 的最大数量    |\r\n| ---- | ------- | ------------- |\r\n| 3    | 8       | 8             |\r\n| 5    | 8       | 三十二           |\r\n| 14   | 64      | 16,384        |\r\n| 14   | 256     | 16,384        |\r\n| 14   | 1,024   | 16,384        |\r\n| 14   | 2,048   | 16,384        |\r\n| 15   | 64      | 32,768        |\r\n| 16   | 64      | 65,536        |\r\n| 17   | 64      | 131,072       |\r\n| 18   | 64      | 262,144       |\r\n| 19   | 64      | 524,288       |\r\n| 20   | 64      | 1,048,576     |\r\n| 20   | 256     | 1,048,576     |\r\n| 20   | 1,024   | 1,048,576     |\r\n| 20   | 2,048   | 1,048,576     |\r\n| 24   | 64      | 16,777,216    |\r\n| 24   | 256     | 16,777,216    |\r\n| 24   | 512     | 16,777,216    |\r\n| 24   | 1,024   | 16,777,216    |\r\n| 24   | 2,048   | 16,777,216    |\r\n| 二十六  | 512     | 67,108,864    |\r\n| 二十六  | 1,024   | 67,108,864    |\r\n| 二十六  | 2,048   | 67,108,864    |\r\n| 三十   | 512     | 1,073,741,824 |\r\n| 三十   | 1,024   | 1,073,741,824 |\r\n| 三十   | 2,048   | 1,073,741,824 |\r\n\r\n请注意，树上可以存储的 cNFT 数量完全取决于最大深度，而缓冲区大小将决定同一时隙内可以对树进行的并发更改（铸造、转移等）的数量。换句话说，选择与您需要树容纳的 NFT 数量相对应的最大深度，然后根据您预计需要支持的流量选择最大缓冲区大小的选项之一。\r\n\r\n接下来，选择树冠深度。增加树冠深度会增加 cNFT 的可组合性。每当您或其他开发人员的代码尝试验证 cNFT 时，代码都必须传入与树中的“层”数量相同的证明节点。因此，对于最大深度 20，您需要传入 20 个证明节点。这不仅很繁琐，而且由于每个证明节点都是 32 字节，因此可以非常快速地最大化交易大小。\r\n\r\n例如，如果您的树的树冠深度非常低，NFT 市场可能只能支持简单的 NFT 转移，而无法支持您的 cNFT 的链上竞价系统。树冠有效地将证明节点缓存在链上，因此您不必将它们全部传递到交易中，从而允许进行更复杂的交易。\r\n\r\n增加这三个值中的任何一个都会增加帐户的规模，从而增加创建帐户的成本。选择值时请权衡利弊。\r\n\r\n一旦知道了这些值，就可以使用包`createTree`中的方法 `@metaplex-foundation/mpl-bubblegum`来创建树。此指令创建并初始化两个帐户：\r\n\r\n1. 一个`Merkle Tree`账户——它保存着 Merkle 哈希值并用于验证存储数据的真实性。\r\n2. 帐户`Tree Config`——它包含压缩 NFT 特有的附加数据，例如树创建者、树是否公开以及 [其他字段——请参阅 Bubblehum 程序源代码](https://github.com/metaplex-foundation/mpl-bubblegum/blob/42ffed35da6b2a673efacd63030a360eac3ae64e/programs/bubblegum/program/src/state/mod.rs#L17)。\r\n\r\n#### 设置[Umi](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#setting-up-umi)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#setting-up-umi)\r\n\r\n该`mpl-bubblegum`软件包是一个插件，如果没有 Metaplex 的 Umi 库就无法使用。Umi 是一个由 Metaplex 创建的用于为链上程序制作 JS/TS 客户端的框架。\r\n\r\n请注意，Umi 在许多概念上的实现与 web3.js 不同，包括 Keypairs、PublicKeys 和 Connections。不过，将这些项目的 web3.js 版本转换为 Umi 版本很容易。\r\n\r\n首先，我们需要创建一个 Umi 实例\r\n\r\n```\r\nimport { createUmi } from \"@metaplex-foundation/umi-bundle-defaults\";import { clusterApiUrl } from \"@solana/web3.js\"; const umi = createUmi(clusterApiUrl(\"devnet\"));\r\n```\r\n\r\n上述代码初始化了一个空的 Umi 实例，没有附加任何签名者或插件。你可以 [在此 Metaplex 文档页面上找到可用插件的详尽列表](https://developers.metaplex.com/umi/metaplex-umi-plugins)\r\n\r\n下一部分是添加我们的导入并将签名者附加到我们的 Umi 实例。\r\n\r\n```\r\nimport { 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\");\r\n```\r\n\r\n#### 使用 Bubblegum 初始化你的[树](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#use-bubblegum-to-initialize-your-tree)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#use-bubblegum-to-initialize-your-tree)\r\n\r\nUmi 实例化后，我们就可以调用`createTree`方法来实例化 Merkle 树和树配置账户了。\r\n\r\n```\r\nconst merkleTree = generateSigner(umi);const builder = await createTree(umi, {  merkleTree,  maxDepth: 14,  maxBufferSize: 64,});await builder.sendAndConfirm(umi);\r\n```\r\n\r\n提供的三个值，即`merkleTree`、`maxDepth`和`maxBufferSize` 是创建树所必需的，其余的是可选的。例如，`tree creator`默认为 Umi 实例标识，而 \\`public 字段为 false。\r\n\r\n当设置为 true 时，`public`允许任何人从初始化树中进行铸造，如果设置为 false ，则只有树创建者才能从树中进行铸造。\r\n\r\n请随意查看 [create_tree 指令处理程序](https://github.com/metaplex-foundation/mpl-bubblegum/blob/42ffed35da6b2a673efacd63030a360eac3ae64e/programs/bubblegum/program/src/processor/create_tree.rs#L40) 和 [create_tree 的预期帐户](https://github.com/metaplex-foundation/mpl-bubblegum/blob/42ffed35da6b2a673efacd63030a360eac3ae64e/programs/bubblegum/program/src/processor/create_tree.rs#L20)的代码。\r\n\r\n#### 铸造 cNFT [#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#mint-cnfts)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#mint-cnfts)\r\n\r\n初始化 Merkle 树帐户及其对应的 Bubblegum 树配置帐户后，就可以将 cNFT 铸造到树中。Bubblegum 库提供了两条指令，我们可以根据铸造的资产是否属于集合来使用它们。\r\n\r\n这两条指令是\r\n\r\n1. **薄荷V1**\r\n\r\n```\r\nawait 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);\r\n```\r\n\r\n2. **mintToCollectionV1**\r\n\r\n```\r\nawait 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);\r\n```\r\n\r\n这两个函数都需要您传入 NFT 元数据和铸造 cNFT 所需的账户列表，例如`leafOwner`、`merkleTree`账户等。\r\n\r\n### 与 cNFT[交互](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#interact-with-cnfts)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#interact-with-cnfts)\r\n\r\n需要注意的是，cNFT*不是*SPL 代币。这意味着您的代码需要遵循不同的约定来处理 cNFT 功能，例如获取、查询、传输等。\r\n\r\n#### 获取 cNFT[数据](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#fetch-cnft-data)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#fetch-cnft-data)\r\n\r\n从现有 cNFT 获取数据的最简单方法是使用 [数字资产标准读取 API](https://developers.metaplex.com/das-api)（Read API）。请注意，这与标准 JSON RPC 是分开的。要使用 Read API，您需要使用支持的 RPC 提供程序。Metaplex 维护着一个（可能不详尽的） [支持 DAS Read API 的 RPC 提供程序列表](https://developers.metaplex.com/rpc-providers#rpcs-with-das-support)。\r\n\r\n在本课中，我们将使用 [Helius，](https://docs.helius.dev/compression-and-das-api/digital-asset-standard-das-api) 因为它们为 Devnet 提供免费支持。\r\n\r\n您可能需要在 Umi 实例中更新您的 RPC 连接端点\r\n\r\n```\r\nconst umi = createUmi(  \"https://devnet.helius-rpc.com/?api-key=YOUR-HELIUS-API-KEY\",);\r\n```\r\n\r\n要使用 Read API 获取特定的 cNFT，您需要拥有 cNFT 的资产 ID。但是，在铸造 cNFT 后，您最多会获得两条信息：\r\n\r\n1. 交易签名\r\n2. 叶子指数（可能）\r\n\r\n唯一真正的保证是您将拥有交易签名。可以 **从**那里找到叶索引，但这涉及一些相当复杂的解析。简而言之，您必须从中检索相关指令日志`Noop program`并解析它们以找到叶索引。我们将在以后的课程中更深入地介绍这一点。现在，我们假设您知道叶索引。\r\n\r\n对于大多数铸币厂来说，这是一个合理的假设，因为铸币将由您的代码控制，并且可以按顺序设置，以便您的代码可以跟踪每个铸币厂将使用哪个索引。例如，第一个铸币厂将使用索引 0，第二个铸币厂将使用索引 1，等等。\r\n\r\n获得叶索引后，您可以导出 cNFT 对应的资产 ID。使用 Bubblegum 时，资产 ID 是使用 Bubblegum 程序 ID 和以下种子导出的 PDA：\r\n\r\n1. `asset`以 utf8 编码表示的静态字符串\r\n2. Merkle 树地址\r\n3. 叶指数\r\n\r\n索引器本质上是在`Noop program`交易发生时观察交易日志，并存储经过哈希处理并存储在 Merkle 树中的 cNFT 元数据。这使它们能够在请求时显示该数据。索引器使用此资产 ID 来识别特定资产。\r\n\r\n为了简单起见，您可以只使用`findLeafAssetIdPda`Bubblegum 库中的辅助函数。\r\n\r\n```\r\nconst [assetId, bump] = await findLeafAssetIdPda(umi, {  merkleTree,  leafIndex,});\r\n```\r\n\r\n有了资产 ID，获取 cNFT 就相当简单了。只需使用 `getAsset`支持 RPC 提供程序和`dasApi` 库提供的方法：\r\n\r\n```\r\nconst [assetId, bump] = await findLeafAssetIdPda(umi, {  merkleTree,  leafIndex,}); const rpcAsset = await umi.rpc.getAsset(assetId);\r\n```\r\n\r\n这将返回一个 JSON 对象，该对象全面概括了传统 NFT 的链上和链下元数据的组合。例如，您可以在 处找到 cNFT 属性`content.metadata.attributes`，或在 处找到图像 `content.files.uri`。\r\n\r\n#### 查询 cNFT [#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#query-cnfts)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#query-cnfts)\r\n\r\n读取 API 还包括获取多个资产、按所有者、创建者查询等方法。例如，Helius 支持以下方法：\r\n\r\n* `getAsset`\r\n* `getSignaturesForAsset`\r\n* `searchAssets`\r\n* `getAssetProof`\r\n* `getAssetsByOwner`\r\n* `getAssetsByAuthority`\r\n* `getAssetsByCreator`\r\n* `getAssetsByGroup`\r\n\r\n我们不会直接介绍其中的大部分内容，但请务必查看 [Helius 文档](https://docs.helius.dev/compression-and-das-api/digital-asset-standard-das-api) 以了解如何正确使用它们。\r\n\r\n#### 转移 cNFT [#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#transfer-cnfts)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#transfer-cnfts)\r\n\r\n就像标准 SPL 令牌传输一样，安全性至关重要。但是，SPL 令牌传输使验证传输权限变得非常容易。它内置于 SPL 令牌程序和标准签名中。压缩令牌的所有权更难验证。实际验证将在程序端进行，但您的客户端代码需要提供其他信息才能实现。\r\n\r\n虽然 Bubblegum 有一个`createTransferInstruction`辅助函数，但需要比平常更多的组装。具体来说，Bubblegum 程序需要验证 cNFT 的全部数据是否是客户端在进行转移之前所声明的。cNFT 的全部数据都经过哈希处理并存储为 Merkle 树上的单个叶子，而 Merkle 树只是树的所有叶子和分支的哈希。因此，您不能简单地告诉程序要查看哪个帐户并让它将该帐户`authority` 或`owner`字段与交易签名者进行比较。\r\n\r\n相反，您需要提供完整的 cNFT 数据以及任何未存储在树冠中的 Merkle 树证明信息。这样，程序就可以独立证明所提供的 cNFT 数据（以及 cNFT 所有者）是准确的。只有这样，程序才能安全地确定交易签名者是否应该被允许转移 cNFT。\r\n\r\n从广义上讲，这涉及五个步骤：\r\n\r\n1. 从索引器中获取 cNFT 的资产数据\r\n2. 从索引器中获取 cNFT 的证明\r\n3. 从 Solana 区块链中获取 Merkle 树账户\r\n4. `AccountMeta`准备资产证明作为物品清单\r\n5. 构建并发送 Bubblegum 转账指令\r\n\r\n幸运的是，我们可以利用这个`transfer`方法来处理所有这些步骤。\r\n\r\n```\r\nconst assetWithProof = await getAssetWithProof(umi, assetId); await transfer(umi, {  ...assetWithProof,  leafOwner: currentLeafOwner,  newLeafOwner: newLeafOwner.publicKey,}).sendAndConfirm(umi);\r\n```\r\n\r\n### 结论[＃](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#conclusion)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#conclusion)\r\n\r\n我们已经介绍了与 cNFT 交互所需的基本技能，但还没有完全全面。您还可以使用 Bubblegum 执行销毁、验证、委托等操作。我们不会介绍这些，但这些说明类似于铸造和转移过程。如果您需要此附加功能，请查看 [Bubblegum 文档](https://developers.metaplex.com/bubblegum)，了解如何利用它提供的辅助功能。\r\n\r\n## 实验室[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#lab)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#lab)\r\n\r\n让我们开始练习创建和使用 cNFT。我们将一起构建一个尽可能简单的脚本，让我们能够从 Merkle 树中创建 cNFT 集合。\r\n\r\n#### [1. 创建新](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#1-create-a-new-project)项目[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#1-create-a-new-project)\r\n\r\n首先创建并初始化一个空的 NPM 项目并将目录更改为该项目。\r\n\r\n```\r\nmkdir cnft-demonpm init -ycd cnft-demo\r\n```\r\n\r\n安装所有必需的依赖项\r\n\r\n```\r\nnpm 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\r\n```\r\n\r\n在第一个脚本中，我们将学习如何创建树，因此让我们创建文件`create-tree.ts`\r\n\r\n```\r\nmkdir src \u0026\u0026 touch src/create-tree.ts\r\n```\r\n\r\n这个 Umi 实例化代码会在很多文件中重复，因此请随意创建一个包装器文件来实例化它：\r\n\r\n创建树.ts\r\n\r\n```\r\nimport { 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());\r\n```\r\n\r\n在上面的代码中，我们从位于 的系统钱包中加载用户的密钥对钱包`.config/solana/id.json`，实例化一个新的 Umi 实例并将密钥对分配给它。我们还将 Bubblegum 和 dasApi 插件也分配给它。\r\n\r\n#### 2. 创建 Merkle 树账户[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#2-create-the-merkle-tree-account)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#2-create-the-merkle-tree-account)\r\n\r\n我们将从创建 Merkle 树账户开始。为此，我们将使用 `createTree`Metaplex Bubblegum 程序中的方法。\r\n\r\n此函数采用三个默认值\r\n\r\n* `merkleTree`- Merkle 树账户地址\r\n* `maxDepth`- 确定树可容纳的最大叶子数量，从而确定树可包含的最大 cNFT 数量。\r\n* `maxBufferSize`- 确定树中可以并行发生多少个并发更改。\r\n\r\n您还可以提供可选字段，例如\r\n\r\n* `treeCreator`- 树权限的地址，默认为当前 `umi.identity`实例。\r\n* `public`- 确定除树创建者之外的其他人是否能够从树中铸造 cNFT。\r\n\r\n创建树.ts\r\n\r\n```\r\nconst 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!\");\r\n```\r\n\r\n`create-tree.ts`使用 esrun运行脚本\r\n\r\n```\r\nnpx esrun create-tree.ts\r\n```\r\n\r\n请务必记住 Merkle 树地址，因为我们将在下一步铸造压缩 NFT 时使用它。\r\n\r\n您的输出将类似于此\r\n\r\n```\r\nExplorer link: https://explorer.solana.com/address/ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h?cluster=devnetMerkle tree address is : ZwzNxXw83PUmWSypXmqRH669gD3hF9rEjHWPpVghr5h✅ Finished successfully!\r\n```\r\n\r\n恭喜！您已创建了一棵泡泡糖树。点击 Explorer 链接以确保该过程已成功完成，\r\n\r\n![Solana Explorer 包含有关创建的 Merkle 树的详细信息](https://solana-developer-content.vercel.app/assets/courses/unboxed/solana-explorer-create-tree.png)Solana Explorer 包含有关创建的 Merkle 树的详细信息\r\n\r\n#### 3. 将 cNFT 铸币到你的树上[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#3-mint-cnfts-to-your-tree)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#3-mint-cnfts-to-your-tree)\r\n\r\n信不信由你，这就是将树设置为压缩 NFT 所需要做的全部工作！现在让我们将注意力转向铸造。\r\n\r\n首先，我们创建一个名为 的新文件`mint-compressed-nft-to-collection.ts`，添加导入并实例化 Umi\r\n\r\nmint-compressed-nft-to-collection.ts\r\n\r\n```\r\nimport { 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());\r\n```\r\n\r\n我将 [回收已经在 Metaplex 课程中创建的 NFT 合集](https://explorer.solana.com/address/D2zi1QQmtZR5fk7wpA1Fmf6hTY2xy8xVMyNgfq6LsKy1?cluster=devnet) ，但如果您想为本课程创建一个新的合集，请查看 [此 repo 上的代码](https://github.com/solana-developers/professional-education/blob/main/labs/metaplex-umi/create-collection.ts)\r\n\r\n信息\r\n\r\n[在我们的NFT 与 Metaplex 课程](https://solana.com/zh/developers/courses/tokens-and-nfts/nfts-with-metaplex#add-the-nft-to-a-collection)中找到创建 Metaplex Collection NFT 的代码。\r\n\r\n要将压缩的 NFT 铸造到收藏品中，我们需要\r\n\r\n* `leafOwner`- 压缩 NFT 的接收者\r\n* `merkleTree`- 我们在上一步中创建的 Merkle 树地址\r\n* `collection`- 我们的 cNFT 所属的收藏。这不是必需的，如果您的 cNFT 不属于任何收藏，则可以省略它。\r\n* `metadata`- 您的链下元数据。本课不会重点介绍如何准备元数据，但您可以查看 [Metaplex 推荐的结构](https://developers.metaplex.com/token-metadata/token-standard#the-non-fungible-standard)。\r\n\r\n我们的cNFT将使用我们之前准备好的结构。\r\n\r\nnft.json\r\n\r\n```\r\n{  \"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\"    }  ]}\r\n```\r\n\r\n把这些全部写进代码，我们将得到\r\n\r\nmint-compressed-nft-to-collection.ts\r\n\r\n```\r\nconst 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);\r\n```\r\n\r\n第一个语句的区别在于我们返回代表交易签名的字节数组。\r\n\r\n我们需要这个才能获得叶模式，并利用该模式得出资产 ID。\r\n\r\nmint-压缩-nft-到-collection.ts\r\n\r\n```\r\nconst leaf: LeafSchema = await parseLeafFromMintToCollectionV1Transaction(  umi,  uintSig,);const assetId = findLeafAssetIdPda(umi, {  merkleTree,  leafIndex: leaf.nonce,})[0];\r\n```\r\n\r\n一切就绪后，我们现在可以运行脚本了 `mint-compressed-nft-to-collection.ts`\r\n\r\n```\r\nnpx esrun mint-compressed-nft-to-collection.ts\r\n```\r\n\r\n您的输出应该类似于\r\n\r\n```\r\nasset id: D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS✅ Finished successfully!\r\n```\r\n\r\n我们不会返回 Explorer 链接，因为该地址不存在于 Solana 状态，但由支持 DAS API 的 RPC 索引。\r\n\r\n下一步，我们将查询该地址以获取 cNFT 详细信息。\r\n\r\n#### 4. 读取现有的 cNFT 数据[#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#4-read-existing-cnft-data)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#4-read-existing-cnft-data)\r\n\r\n现在我们已经编写了代码来铸造 cNFT，让我们看看是否真的可以获取它们的数据。\r\n\r\n创建新文件`fetch-cnft-details.ts`\r\n\r\n```\r\nfetch-cnft-details.ts\r\n```\r\n\r\n导入我们的包并实例化 Umi。在这里我们最终将使用 `umi.use(dasApi())`我们一直导入的。\r\n\r\n在 Umi 的实例化中，我们将对连接端点进行更改并使用支持 DAS API 的 RPC。\r\n\r\n请务必使用 Helius API 密钥进行更新，您可以从 [开发人员仪表板页面获取该密钥](https://dashboard.helius.dev/signup?redirectTo=onboarding)\r\n\r\n获取-cnft-details.ts\r\n\r\n```\r\nimport { 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());\r\n```\r\n\r\n获取压缩的 NFT 详细信息就像调用上一步中的`getAsset`方法一样简单。`assetId`\r\n\r\n获取-cnft-details.ts\r\n\r\n```\r\nconst assetId = UMIPublicKey(\"D4A8TYkKE5NzkqBQ4mPybgFbAUDN53fwJ64b8HwEEuUS\"); // @ts-ignoreconst rpcAsset = await umi.rpc.getAsset(assetId);console.log(rpcAsset);\r\n```\r\n\r\n让我们首先声明一个`logNftDetails`以 `treeAddress`和为参数的函数`nftsMinted`。\r\n\r\n我们的 console.log 的输出将输出\r\n\r\n```\r\n{  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}\r\n```\r\n\r\n请记住，Read API 还包括获取多个资产、按所有者、创建者等进行查询等方法。请务必查看 [Helius 文档](https://docs.helius.dev/compression-and-das-api/digital-asset-standard-das-api) 以了解可用的内容。\r\n\r\n#### 5. 转移 cNFT [#](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#5-transfer-a-cnft)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#5-transfer-a-cnft)\r\n\r\n我们要添加到脚本中的最后一项是 cNFT 传输。与标准 SPL 代币传输一样，安全性至关重要。但是，与标准 SPL 代币传输不同的是，要构建具有任何类型的状态压缩的安全传输，执行传输的程序需要完整的资产数据。\r\n\r\n幸运的是，我们可以用该方法获取资产数据`getAssetWithProof`。\r\n\r\n让我们首先创建一个新文件`transfer-asset.ts`，并用实例化新 Umi 客户端的代码填充它。\r\n\r\n转移资产.ts\r\n\r\n```\r\nimport { 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());\r\n```\r\n\r\n我们尚未准备好转移资产。使用`assetId`我们的 cNFT，我们可以`transfer`从 Bubblegum 库中调用方法\r\n\r\n转移资产.ts\r\n\r\n```\r\nconst 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!\");\r\n```\r\n\r\n使用 运行我们的脚本`npx esrun transfer-asset.ts`，如果成功的话应该输出类似这样的内容：\r\n\r\n```\r\nExplorer link: https://explorer.solana.com/tx/3sNgN7Gnh5FqcJ7ZuUEXFDw5WeojpwkDjdfvTNWy68YCEJUF8frpnUJdHhHFXAtoopsytzkKewh39Rf7phFQ2hCF?cluster=devnet✅ Finished successfully!\r\n```\r\n\r\n打开浏览器链接，滚动到底部查看你的交易日志，\r\n\r\n![Solana Explorer 显示转移 cnft 指令的日志](https://solana-developer-content.vercel.app/assets/courses/unboxed/solana-explorer-showing-cnft-transfer-logs.png)Solana Explorer 显示转移 cnft 指令的日志\r\n\r\n恭喜！现在您知道如何铸造、读取和传输 cNFT。如果您愿意，可以将最大深度、最大缓冲区大小和冠层深度更新为更大的值，只要您有足够的 Devnet SOL，此脚本将允许您铸造最多 10,000 个 cNFT，而成本仅为铸造 10,000 个传统 NFT 成本的一小部分。\r\n\r\n在 Solana Explorer 上检查 cNFT！与以前一样，如果您遇到任何问题，您应该自行修复，但如果需要， 可以使用[解决方案代码。](https://github.com/solana-foundation/compressed-nfts)\r\n\r\n### 挑战[＃](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#challenge)[](https://solana.com/zh/developers/courses/state-compression/compressed-nfts#challenge)\r\n\r\n现在轮到你自己尝试一下这些概念了！我们目前不会给出过多的规定，但这里有一些想法：\r\n\r\n1. 创建你自己的生产 cNFT 集合\r\n2. 为本课的实验室构建一个 UI，让你可以铸造一个 cNFT 并显示它\r\n3. 看看你是否可以在链上程序中复制一些实验室脚本的功能，即编写一个可以铸造 cNFT 的程序","title":"Solana 创建cNFT"},"history":null,"timestamp":1734932885,"version":1}