关于 Solana 压缩您需要了解的所有信息

本文使用 Bubblegum SDK 和 Umi 来演示创建并发 Merkle 树以及铸造和传输压缩 NFT 的各种方法。熟悉这两种工具非常有价值,因为您可能会在不同的代码库中遇到这两种工具。 Bubblegum SDK 的加入是为了方便学习,因为工作流程使底层机制更加透明,而 Umi 提供了更简洁的工作流程来简化这些流程。

关于 Solana 压缩您需要了解的所有信息
关于 Solana 压缩您需要了解的所有信息

这篇文章是关于什么的?

如果我告诉你现在可以用不到 150 美元铸造一百万个 NFT,你会相信我吗?荒谬!根据区块链铸造这么多 NFT 将花费超过 100 万美元!不是吗?

状态压缩是一种新颖的原语,它利用 Merkle 树和 Solana 的账本来大幅降低存储成本,同时继承 Solana 基础层的安全性和去中心化性。本文旨在对 Solana 上的压缩进行全面深入的探讨。它涵盖了从常见误解到传输压缩 NFT 的所有内容。如果您想了解状态压缩以及如何获取、铸造或传输压缩的 NFT,那么这是您需要开始的唯一一篇文章。

本文假设您已经阅读了我们的文章 加密工具 101 - 哈希函数和 Merkle 树解释。在阅读本文之前阅读它很重要,因为我们假设了解默克尔树。本文还扩展了并发 Merkle 树,并更深入地介绍了它们的大小和创建。

本文使用 Bubblegum SDK 和 Umi 来演示创建并发 Merkle 树以及铸造和传输压缩 NFT 的各种方法。熟悉这两种工具非常有价值,因为您可能会在不同的代码库中遇到这两种工具。 Bubblegum SDK 的加入是为了方便学习,因为工作流程使底层机制更加透明,而 Umi 提供了更简洁的工作流程来简化这些流程。

常见的误解

在深入研究状态压缩和压缩 NFT 的复杂性之前,我们需要澄清一些事情:

Solana 上的压缩与传统压缩相同

这是错误的。传统上,压缩用于减小文件和数据的大小。其主要目标是以比原始文件更少的位数存储或传输数据。压缩算法有两大类:

  • 无损压缩,可以从压缩数据重建原始数据
  • 有损压缩,删除“不太重要”的信息以减小文件大小

压缩的 NFT 并不是经过某种无损或有损压缩算法以使其数据更小的 NFT。这也不是要降低与 NFT 相关的艺术、音乐或元数据的质量或维度。这个概念在 Solana 的背景下呈现出完全不同的形式。相反,它是关于优化底层区块链账本存储与 NFT 相关信息的方式。从帐户的上下文中,我们通过将多个帐户(在本例中为 NFT)聚合到存储在状态中的单个 Merkle 根中,将其压缩到分类帐中。此过程显着降低了存储成本,同时保留了可验证性。

在链外存储压缩数据存在风险并会导致漏洞

这是错误的——您可以通过散列数据并将其 Merkle 根存储在链上来安全地将数据存储在链外。从技术上讲,压缩的 NFT 并不存储在链外。数据仍然在链上,因为任何可以由账本重新导出的东西都被视为在链上。不同之处在于,账户受到状态激励,由验证者保存在内存中,而账本则需要通过存档节点进行访问。状态压缩将两者合并,通过账户中的状态提供账本数据的验证,这仍然保留了 Solana 本身的安全性和去中心化性。我们将在另一节中讨论分类账是什么以及为什么它是安全的。

如果我用来存储树的索引器或 RPC 提供程序出现故障,我可能会丢失并发 Merkle 树

您不会丢失您的树 - 任何有权访问分类帐的人都可以通过重播树的历史来重建整个树。

并发 Merkle 树可以处理并行更新

一个常见的误解是,使用“并发”一词意味着链上 Merkle 树的多次更新可以并行发生。虽然并发 Merkle 树可以在同一块内容纳多个叶子替换,但这些更新由验证器按顺序处理。当验证器收到一批影响链上并发 Merkle 树的交易时,验证器可以在同一个槽中处理。然而,每个时隙的数据不是同时产生的。我们将在下一节什么是状态压缩?中对此进行详细阐述。

树和集合是一样的

并发 Merkle 树与集合不同。单个集合可以使用任意数量的并发 Merkle 树。值得注意的是,NFT 的分组可以与其存储正交。 NFT 可以存在于账户中,也可以压缩到账本中,跨越任意数量的树,一棵树或多棵树。尽管如此,建议仅将并发 Merkle 树用于一个集合,以降低复杂性。

什么是状态压缩?

状态压缩通过创建账本数据的加密哈希并将该哈希存储在帐户中来优化存储。这种方法利用了账本固有的安全性和不变性,同时提供了一个强大的框架来验证账本中存储的数据。

对于构建在 Solana 之上的应用程序来说,这是一种经济高效的解决方案。开发人员现在可以使用账本存储空间,而不是价格更高的基于帐户的存储。因此,状态压缩不仅可以确保数据完整性,而且还是 Solana 上资源分配的一种经济有效的解决方案。

Solana 状态压缩背后的秘密是并发 Merkle 树的使用。并发 Merkle 树经过优化,可以快速连续处理多个交易,从而可以快速转发其证明。这与传统的 Merkle 树不同,传统的 Merkle 树的证明在每次更新时都会失效。并发 Merkle 树存储最近更改的安全更改日志以及根哈希和导出它所需的证明。该变更日志存储在链上专用于该树的帐户中。每个并发 Merkle 树都有一个最大缓冲区大小。该值表示在 Merkle 根仍然有效的情况下可以对树进行的最大更改次数。可以将其视为一组经过计算的证明在需要更新之前可能有多“陈旧”。

因此,当验证者收到多个更新同一槽内链上 Merkle 树的请求时,验证者可以使用树的变更日志作为事实来源。这允许对 Merkle 树进行最大缓冲区大小的并发更改。虽然这不会直接减少链上存储的数据量,但它通过允许同时处理多个更新来提高效率。这意味着即使在高吞吐量环境中,系统也可以保持 Merkle 树提供的“包含证明”完整性。在这里,包含证明仅仅意味着能够证明特定数据元素确实是已被哈希到 Merkle 根中的一组数据的一部分。

这种状态压缩和并发 Merkle 树的巧妙组合为在 Solana 上构建的应用程序提供了极具成本效益的解决方案。有必要讨论 Solana 的状态与其账本之间的差异,以充分理解这些技术的影响。

状态与账本

分类账是自创世区块以来 Solana 上发生的客户签署的所有交易的历史记录。它是一种仅追加的数据结构,这意味着事务一旦添加就无法修改或删除。验证器验证添加到分类账中的交易。账本由网络上的多个节点存储,以确保容错。然而,验证者的账本副本可能只包含较新的块,以减少存储空间,因为不需要较旧的块来验证未来的块。

该状态代表 Solana 上所有帐户和程序的当前快照。状态是可变的,并且在处理事务时会发生变化。将状态视为一个高度优化的数据库,可以查询代币余额、程序和帐户。

这是区分两者的简单方法:假设 Alice 的余额为 100 SOL,Bob 的余额也为 100 SOL。 Alice 发送一笔交易给 Bob 10 SOL。一旦经过验证,交易就会被添加到一个区块中,并将该区块附加到分类账中。账本现在有一条不可变的记录,表明 Alice 向 Bob 发送了 10 SOL。同时,州政府会将 Alice 和 Bob 的账户分别更新为 90 和 110 SOL。

两者之间的主要区别可以总结如下:

  • 账本是不可变的并且只能追加,而状态是可变的并且不断变化
  • 分类账是所有交易的历史记录,而状态反映了所有账户和程序的当前状态
  • 账本用于验证,而状态用于执行交易和运行程序

虽然账本充当不可变的历史记录以确保每笔交易都是可验证和可追踪的,但状态充当账本的动态快照,调整以适应实时操作,例如传输和程序执行。重要的是,两者都受到链本身的共识的约束。国家和账本共同构成了 Solana 的支柱,使其能够高效运行,同时维护去中心化信任。

什么是压缩 NFT?

压缩 NFT (cNFT) 使用状态压缩和并发 Merkle 树来降低存储成本。压缩的 NFT 将其元数据存储在分类账上,而不是将每个 NFT 存储在典型的 Solana 帐户中。这可以降低存储成本,同时继承账本的安全性和不变性。

压缩的 NFT 仍然遵循与未压缩的 NFT 完全相同的元数据模式。因此,NFT 和 cNFT 的定义方式相同。

NFT 和 cNFT 的主要区别如下:

  • 压缩的 NFT 可以转换为常规 NFT,但常规 NFT 无法转换为压缩的 NFT
  • 压缩 NFT 不是生 Solana 代币 - 它们没有代币帐户、铸币帐户或元数据。然而,它们确实有一个稳定的标识符(资产 ID)。解压后,NFT 保留相同的标识符。因此,压缩状态下的 NFT 不是原生代币,但如果需要,可以将其制成原生代币
  • 单个并发 Merkle 树账户可以容纳数百万个 NFT
  • 单个集合能够跨越多个树帐户
  • 所有 NFT 修改均通过Bubblegum 程序进行
  • 建议使用 DAS API 调用来读取有关压缩 NFT 的任何信息

有趣的是,我们需要使用 DAS API 来获取有关压缩 NFT 的信息。这是为什么?更重要的是,那是什么?

使用 DAS API 读取压缩的 NFT 元数据

我们需要索引器的帮助,因为 cNFT 的元数据存储在分类账上而不是传统帐户中。尽管您可以通过重放相关交易来获取压缩 NFT 的当前状态,但像 Helius 这样的提供商这样做是为了您的方便。开发人员可以使用数字资产标准(DAS)API(一种开源规范和系统)来获取资产信息。 DAS API 支持压缩和传统或未压缩的 NFT。因此,您可以对两种 NFT 类型使用相同的端点。

Helius 目前支持以下 DAS API 方法:

  • getAsset - 通过ID获取特定资产
  • getAssetBatch - 通过 ID 获取多个资产
  • getAssetProof - 通过id获取压缩资产的 Merkle 证明
  • getAssetProofBatch - 通过 ID 获取多个资产证明
  • getAssetsByOwner - 获取某个地址拥有的资产列表
  • getAssetsByAuthority - 获取具有特定权限的资产列表
  • getAssetsByCreator - 获取由地址创建的资产列表
  • getAssetsByGroup - 通过组键和值获取资产列表
  • searchAssets - 通过各种参数搜索资产
  • getSignaturesForAsset - 获取与压缩资产相关的交易签名列表
  • 分页 - 支持基于页面和键集分页,可一次获取超过 1000 条记录

请参阅Helius DAS API 文档,了解有关每种方法的更多信息。例如,如果您想获取某个地址拥有的所有资产的列表,您可以使用getAssetsByOwner发出以下 POST 请求:


const url = `https://mainnet.helius-rpc.com/?api-key=`

const getAssetsByOwner = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 'my-id',
      method: 'getAssetsByOwner',
      params: {
        ownerAddress: '86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
        page: 1, // Starts at 1
        limit: 1000
      },
    }),
  });
  const { result } = await response.json();
  console.log("Assets by Owner: ", result.items);
};
getAssetsByOwner();

检索压缩资源很方便,但是如果我们想创建自己的资源怎么办?在开始我们的铸造之旅之前,计算构建用于存储这些资产的并发 Merkle 树的大小和相关成本至关重要。

创建并发 Merkle 树的大小和成本

计算尺寸

在链上创建并发 Merkle 树时,有三个重要指标将确定树的大小、创建树的成本以及在保持 Merkle 根有效的同时可以对树进行的并发更改数量:

  • 最大深度
  • 最大缓冲区大小
  • 冠层深度

最大深度是指从树的任何叶子到根的最大跳数。每个叶子仅连接到另一个叶子,作为用于成对散列的叶子对存在。您可以使用以下公式计算树可以容纳的最大叶节点数:numberOfNodes = 2 ^ maxDepth。树深度必须在创建时设置,因此您需要使用此公式来确定存储数据的最低可能最大深度。例如,如果您的目标是在树中存储大约 100 个压缩的 NFT,则maxDepth为 7 就足够了,因为2^7 = 1282^6 = 64。在链上构建并发 Merkle 树时,最大深度是一个重要的成本决定因素。这些成本是在树的创建和缩放过程中预先产生的maxDepth值较高。

最大缓冲区大小是指在 Merkle 根仍然有效的情况下,树可以发生的最大更改次数。变更日志缓冲区的大小是在为并发 Merkle 树创建树时使用maxBufferSize值进行调整和设置的。因此,当验证器在同一槽中收到对树的多个更改请求时,它们可以使用更改日志并允许最多maxBufferSize 的更改,而根仍然有效。

值得注意的是,只有特定数量的有效maxDepthmaxBufferSize对可用于创建新的并发 Merkle 树帐户。@solana/spl-account-compression 包导出常量ALL_DEPTH_SIZE_PAIRS,它是包含所有有效组合的数字数组的数组。最小值为maxDepth为 3,maxBufferSize为 8,最大值为maxDepth为 30,maxBufferSize为 2048。

Canopy 深度是指存储在账户中的 Merkle 树的子集。这些缓存的证明用于补充通过网络传输的证明,因为它们受到交易限制。当尝试更改叶子的数据时(例如传输 NFT 时),必须使用完整路径来验证叶子的原始所有权。树的最大深度越大,验证所需的证明节点就越多。 Canopy 允许减小证明大小,并避免使用maxDepth的证明大小来验证树。

可以通过从最大深度减去所需的校样尺寸来计算冠层深度。因此,如果您的最大深度为 14,并且希望证明大小为 4,那么您的 canopy 深度将为 10。这意味着每个更新交易只需提交 4 个证明节点。在构建链上并发 Merkle 树时,Canopy 深度也是一个重要的成本决定因素。这些成本是在树的创建和缩放过程中预先产生的,并且具有较高的canopyDepth值。虽然较低的canopyDepth会降低前期成本,但较低的canopyDepth会限制可组合性。这是因为每个更新交易都必须需要更大的证明大小,从而对交易大小限制施加了限制。例如,如果您的树冠深度较低的树用于压缩 NFT,那么 NFT 市场可能只能支持您的收藏的简单转移。一般来说,maxDepth - canopyDepth应小于或等于 10,以获得最大的可组合性。Tensor 的 Tensor cNFT 最大证明长度规范中概述了这一点。

计算成本

存在不同的方法来确定并发 Merkle 树的大小和成本。最简单的方法是使用压缩 NFT 计算器并输入要存储在该树中的压缩 NFT 的数量:

该网站提供了存储所需数量的资产所需的最佳树深度的详细分类,以及基于可组合性的各种成本选项。例如,该图显示,创建存储 1000 万个压缩 NFT 的高度可组合树仅需约 7.67 SOL。考虑到铸造 1000 万个 NFT 的交易成本约为 50 SOL,总成本约为 57.67 SOL。

开发人员还可以使用@solana/spl-account-compression 包来计算给定树大小所需的空间以及为链上树分配所需空间的成本。这可以通过以下脚本来实现:


import { 
    Connection, 
    LAMPORTS_PER_SOL 
} from "@solana/web3.js";

import { 
    getConcurrentMerkleTreeAccountSize, 
    ALL_DEPTH_SIZE_PAIRS 
} from "@solana/spl-account-compression";

const connection = new Connection();

const calculateCosts = async (maxProofSize: number) => {
    await Promise.all(ALL_DEPTH_SIZE_PAIRS.map(async (pair) => {
        const canopy = pair.maxDepth - maxProofSize;
        const size = getConcurrentMerkleTreeAccountSize(pair.maxDepth, pair.maxBufferSize, canopy);
        const numberOfNfts = Math.pow(2, pair.maxDepth);
        const rent = (await connection.getMinimumBalanceForRentExemption(size)) / LAMPORTS_PER_SOL;

        console.log(`maxDepth: ${pair.maxDepth}, maxBufferSize: ${pair.maxBufferSize}, canopy: ${canopy}, numberOfNfts: ${numberOfNfts}, rent: ${rent}`);
    }));
}

await calculateCosts();

在这里,我们从@solana/web3.js@solana/spl-account-compression导入必要的模块。我们需要与mainnet的连接,可以使用 Helius API 密钥建立该连接。函数calculateCosts将maxDepthmaxBufferSizecanopy、可以存储在这棵树中的NFT数量以及SOL中的租金成本记录到控制台。因此,当我们使用所需的证明大小调用calculateCosts时,我们可以在控制台中看到所有可能的树组合。

请注意,某些日志可能会输出:Unable to fetch minibalance for Rentemption。这是因为您指定的maxProofSize帐户太大而无法创建,因此我们无法获取可以免除帐户租金的最低余额。

创建并发 Merkle 树

我们在创建并发Merkle树时需要创建两个账户:

  • 并发 Merkle 树帐户
  • 并发 Merkle 树配置帐户

树账户保存着用于数据验证的Merkle树。我们使用上一节中提到的所需的最大深度、最大缓冲区大小和冠层深度来创建它。该帐户归帐户压缩程序所有,该程序由 Solana 创建和维护。它用于验证压缩的 NFT 的真实性。

树配置帐户是从并发 Merkle 树帐户的地址派生的 PDA。它用于存储其他配置,例如树的创建者和铸造的压缩 NFT 的数量。

Metaplex 将具有关联树配置帐户的并发 Merkle 树称为“Bubblegum 树”。

完整代码


import {
    Connection,
    Keypair,
    PublicKey,
    Transaction,
    sendAndConfirmTransaction,
} from "@solana/web3.js";

import {
    ValidDepthSizePair,
    createAllocTreeIx,
    SPL_NOOP_PROGRAM_ID,
    SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
} from "@solana/spl-account-compression";

import {
    PROGRAM_ID,
    createCreateTreeInstruction
  } from "@metaplex-foundation/mpl-bubblegum";

const createTree = async (
    connection: Connection,
    payer: Keypair,
    treeKeypair: Keypair,
    maxDepthSizePair: ValidDepthSizePair,
    canopyDepth: number = 0,
) => {
    const allocTreeInstruction = await createAllocTreeIx(
        connection,
        treeKeypair.publicKey,
        payer.publicKey,
        maxDepthSizePair,
        canopyDepth,
    );

    const [treeAuthority, ] = PublicKey.findProgramAddressSync(
        [treeKeypair.publicKey.toBuffer()],
        PROGRAM_ID,
    );

    const createTreeInstruction = createCreateTreeInstruction(
        {
            payer: payer.publicKey,
            treeCreator: payer.publicKey,
            treeAuthority,
            merkleTree: treeKeypair.publicKey,
            compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
            logWrapper: SPL_NOOP_PROGRAM_ID,
        },
        {
            maxBufferSize: maxDepthSizePair.maxBufferSize,
            maxDepth: maxDepthSizePair.maxDepth,
            public: false,
        },
        PROGRAM_ID,
    );

    try {
        const transaction = new Transaction().add(allocTreeInstruction).add(createTreeInstruction);
        transaction.feePayer = payer.publicKey;

        const transactionSignature = await sendAndConfirmTransaction(
            connection,
            transaction,
            [treeKeypair, payer],
            {
                commitment: "confirmed",
                skipPreflight: true,
            },
        );

        console.log(`Successfully created a Merkle tree with txt sig: ${transactionSignature}`);
    } catch (error: any) {
        console.error(`Failed to create a Merkle tree with error: ${error}`);
    } 
}

代码细目

这是在 Solana 上创建并发 Merkle 树的示例函数。要调用此示例函数createTree,必须传递以下参数:

  • 连接- 到全节点 JSON RPC 端点的连接,其类型为Connection
  • payer - 将支付交易费用的帐户,其类型为Keypair
  • treeKeypair - 树的密钥对地址,其类型为Keypair
  • maxDepthSizePair - 有效的maxDepthmaxBufferSize对,其类型为ValidDepthSizePair
  • canopyDepth - 树的树冠深度,其类型为number,设置为默认值0

import {
    Connection,
    Keypair,
    PublicKey,
    Transaction,
    sendAndConfirmTransaction,
} from "@solana/web3.js";

import {
    ValidDepthSizePair,
    createAllocTreeIx,
    SPL_NOOP_PROGRAM_ID,
    SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
} from "@solana/spl-account-compression";

import {
    PROGRAM_ID,
    createCreateTreeInstruction
  } from "@metaplex-foundation/mpl-bubblegum";

首先,我们导入@solana/web3.js@solana/spl-account-compression@metaplex-foundation/mpl-bubblegum以及必要的模块。


const createTree = async (
    connection: Connection,
    payer: Keypair,
    treeKeypair: Keypair,
    maxDepthSizePair: ValidDepthSizePair,
    canopyDepth: number = 0,
) => {
	// Rest of the code
}

在这里,我们使用上述参数定义函数createTree 。


const allocTreeInstruction = await createAllocTreeIx(
		connection,
    treeKeypair.publicKey,
    payer.publicKey,
    maxDepthSizePair,
    canopyDepth,
);

createAllocTreeIx是我们用来创建并发 Merkle 树帐户的辅助函数。 SPL 帐户压缩包建议使用此方法来初始化并发 Merkle 树帐户,因为这些帐户往往非常大,并且可能超出可通过 CPI 分配的限制。在这里,我们创建了在链上分配树账户的指令。这还计算了在链上存储树所需的空间以及成本,所以我们以后不需要担心这个。


const [treeAuthority, ] = PublicKey.findProgramAddressSync(
    [treeKeypair.publicKey.toBuffer()],
		PROGRAM_ID,
);

我们需要派生出Bubblegum程序拥有的权限的树配置帐户。这是创建树的指令createCreateTreeInstruction所必需的,因为我们需要传入treeAuthority作为参数。在这里,我们使用树的公钥和 Bubblegum 程序 ID通过findProgramAddressSync方法导出 PDA 。我们需要解构树权威,因为权威和凹凸都被返回。我省略了凹凸,因为它对于我们的功能来说不是必需的。如果需要,将解构更改为[treeAuthority, Bump]以保存凹凸。


const createTreeInstruction = createCreateTreeInstruction(
		{
	    payer: payer.publicKey,
      treeCreator: payer.publicKey,
      treeAuthority,
      merkleTree: treeKeypair.publicKey,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      logWrapper: SPL_NOOP_PROGRAM_ID,
    },
    {
      maxBufferSize: maxDepthSizePair.maxBufferSize,
      maxDepth: maxDepthSizePair.maxDepth,
      public: false,
    },
    PROGRAM_ID,
);

我们使用Bubblegum SDK 中的createCreateTreeInstruction来构建构建并发 Merkle 树的指令。这将在链上创建一棵树,Bubblegum 程序作为其所有者。createCreateTreeInstruction具有三个参数。第一个是包含帐户的对象,用于设置属性,例如树的创建者。第二个对象与最大深度和最大缓冲区大小有关。它还包括boolean类型的公共参数。将public设置为true将允许任何人从树上铸造压缩的 NFT。否则,只有树创建者或树委托人才能从树中铸造压缩的 NFT。委托账户可以代表树所有者执行操作,例如转移或刻录压缩的 NFT。顺便说一句,您可以使用@metaplex-foundation/mpl-bubblegum包中的createSetTreeDelegateInstruction分配树委托,如下所示:


const changeTreeDelegateTransaction = createSetTreeDelegateInstruction({
		merkleTree: treeKeypair.publicKey
		newTreeDelegate: ,
		treeAuthority,
		treeCreator: treeCreator.publicKey // which in our script would be payer.publicKey
});

我们还传入 Bubblegum 程序的程序 ID。现在回到我们的代码的其余部分:


try {
		const transaction = new Transaction().add(allocTreeInstruction).add(createTreeInstruction);
    transaction.feePayer = payer.publicKey;

    const transactionSignature = await sendAndConfirmTransaction(
	    connection,
	    transaction,
	    [treeKeypair, payer],
	    {
		    commitment: "confirmed",
		    skipPreflight: true,
	    },
    );

    console.log(`Successfully created a Merkle tree with txt sig: ${transactionSignature}`);
} catch (error: any) {
		console.error(`Failed to create a Merkle tree with error: ${error}`);
}

我们将刚刚构建的两条指令添加到交易中并将其发送出去。我们确保树密钥对付款人都签署交易。然后成功的交易签名会记录到控制台。我们将此过程包装在try-catch块中,因此,无论出于何种原因,如果发生错误,都会通过console.error记录到控制台。

使用 Umi 创建并发 Merkle 树

对于新开发人员来说,使用 Bubblegum SDK、Solana 的帐户压缩程序和 Solana 的 web3.js 包可能会非常混乱,并且每次设置都很繁琐。幸运的是,Bubblegum SDK 提供了一个createTree操作,可以为我们处理所有事情,这与 Umi 配合得很好。代码如下:


import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { generateSigner } from '@metaplex-foundation/umi'
import { createTree } from '@metaplex-foundation/mpl-bubblegum'

const umi = createUmi();

const merkleTree = generateSigner(umi);

const builder = await createTree(umi, {
  merkleTree,
  maxDepth: 14,
  maxBufferSize: 64,
});

await builder.sendAndConfirm(umi);

Umi是一个模块化框架,用于为 Solana 程序构建和使用 JavaScript 客户端。它提供了一个零依赖库,其中包含一组其他库可以依赖的核心接口,而不受特定实现的限制。 Umi 由 Metaplex 提供,其文档可以在此处找到。

我们使用 Umi 实例生成签名者,创建 Merkle 树,并发送和确认我们构建的交易。默认情况下,树创建者设置为 Umi 身份,并且public参数设置为 false。这些参数可以自定义,允许自定义树创建者和公共值true也可以传入。这是创建链上并发 Merkle 树的一种更快的方法。

请注意,泡泡糖与树冠大小无关。这是因为 Solana 的帐户压缩程序将根据可用帐户空间确定 canopy 大小。只需要分配足够的空间,以便程序能够准确识别要使用的合适的树冠尺寸。

通过直接与 Bubblegum 交互来铸造 cNFT

创建集合

传统上,NFT 使用 Metaplex 标准分组为一个集合。对于压缩 NFT 和“常规”NFT 来说都是如此。创建集合:

  • 创建一个新的代币“mint”
  • 为铸币厂创建关联的代币账户
  • 铸造单个代币
  • 将集合的元数据存储在链上帐户中

虽然与状态压缩或压缩 NFT 主题没有直接关系,因此超出了本文的范围,但我们提供了一个脚本作为创建您自己的集合的参考。您可以在此处访问该脚本。

铸造 NFT 到我们的收藏中

对于新创建的收藏,您需要以下内容才能开始铸造:

  • collectionMint - 收藏品的铸币地址
  • collectionAuthority - 对集合具有权限的帐户
  • collectionMetadata - 集合的元数据帐户
  • EditionAccount - 拥有附加属性的帐户,例如主版本帐户

铸造到集合的完整代码


import {
  Keypair,
  PublicKey,
  Connection,
  Transaction,
  sendAndConfirmTransaction,
  TransactionInstruction,
} from "@solana/web3.js";

import {
  SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

import {
  PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
  MetadataArgs,
  createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";

import {
  PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";

export async function mintCompressedNFT(
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  collectionMint: PublicKey,
  collectionMetadata: PublicKey,
  collectionMasterEditionAccount: PublicKey,
  compressedNFTMetadata: MetadataArgs,
  receiverAddress?: PublicKey
) {
  const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);

  const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
    [Buffer.from("collection_cpi", "utf8")],
    BUBBLEGUM_PROGRAM_ID
  );

  const mintInstructions: TransactionInstruction[] = [];

  const metadataArgs = Object.assign(compressedNFTMetadata, {
    collection: { key: collectionMint, verified: false },
  });

  mintInstructions.push(
    createMintToCollectionV1Instruction(
      {
        payer: payer.publicKey,

        merkleTree: treeAddress,
        treeAuthority,
        treeDelegate: payer.publicKey,
        leafOwner: receiverAddress || payer.publicKey,
        leafDelegate: payer.publicKey,

        collectionAuthority: payer.publicKey,
        collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
        collectionMint: collectionMint,
        collectionMetadata: collectionMetadata,
        editionAccount: collectionMasterEditionAccount,

        compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
        logWrapper: SPL_NOOP_PROGRAM_ID,
        bubblegumSigner: bubblegumSigner,
        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
      },
      {
        metadataArgs,
      }
    )
  );

  try {
    const txt = new Transaction().add(...mintInstructions);

    txt.feePayer = payer.publicKey;

    const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

    console.log(`Successfully minted a cNFT with the txt sig: ${transactionSignature}`);

  } catch (error: any) {
    console.error(`Failed to mint cNFT with error: ${error}`);
  }
}

打破铸造过程


import {
  Keypair,
  PublicKey,
  Connection,
  Transaction,
  sendAndConfirmTransaction,
  TransactionInstruction,
} from "@solana/web3.js";

import {
  SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

import {
  PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
  MetadataArgs,
  createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";

import {
  PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";

首先,我们导入@solana/web3.js@solana/spl-account-compression@metaplex-foundation/mpl-bubblegum@metaplex-foundation/mpl-token-metadata以及必要的模块。


export async function mintCompressedNFT(
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  collectionMint: PublicKey,
  collectionMetadata: PublicKey,
  collectionMasterEditionAccount: PublicKey,
  compressedNFTMetadata: MetadataArgs,
  receiverAddress?: PublicKey
) {
	// Rest of the code
}

我们定义mintCompressedNFT,它接受相当多的参数:

  • 连接- 用于与 Solana 交互的连接对象
  • payer - 支付交易费用的账户
  • treeAddress - 并发 Merkle 树的账户
  • collectionMint - 收藏品的铸币地址
  • collectionMetadata - 集合的元数据帐户
  • collectionMasterEditionAccount - 主版帐户
  • compressedNFTMetadata - 特定于要铸造的 cNFT 的元数据
  • receiveAddress - 一个可选的公钥地址,新铸造的 cNFT 将发送到该地址

const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID); 

const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
    [Buffer.from("collection_cpi", "utf8")],
    BUBBLEGUM_PROGRAM_ID
  );

在这里,我们正在寻找必要的 PDA,并忽略它们的颠簸。首先,我们派生出树的权威的PDA,然后我们派生出一个PDA作为压缩铸币的签名者。我们需要包含collection_cpi,因为它是 Bubblegum 程序所需的自定义前缀。


const mintInstructions: TransactionInstruction[] = [];

我们将mintInstructions设置为TransactionInstruction的空数组。如果需要的话,这允许我们同时铸造多个 cNFT。


const metadataArgs = Object.assign(compressedNFTMetadata, {
    collection: { key: collectionMint, verified: false },
});

metadataArgs确保我们的压缩 NFT 元数据格式正确。使用createMintToCollectionV1Instruction将 NFT 铸造到集合中需要将已验证字段设置为false才能使交易成功,尽管它会自动验证集合。


mintInstructions.push(
    createMintToCollectionV1Instruction(
      {
        payer: payer.publicKey,

        merkleTree: treeAddress,
        treeAuthority,
        treeDelegate: payer.publicKey,
        leafOwner: receiverAddress || payer.publicKey,
        leafDelegate: payer.publicKey,

        collectionAuthority: payer.publicKey,
        collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
        collectionMint: collectionMint,
        collectionMetadata: collectionMetadata,
        editionAccount: collectionMasterEditionAccount,

        compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
        logWrapper: SPL_NOOP_PROGRAM_ID,
        bubblegumSigner: bubblegumSigner,
        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
      },
      {
        metadataArgs,
      }
  	)
);

我们在指令中添加了一颗薄荷糖。只要交易保持在字节大小限制内,我们就可以在同一交易中添加多个铸币厂。在这里,我们使用createMintToCollectionV1Instruction从我们的集合中铸造压缩的 NFT。该指令包含两个对象,一个对象包含处理该指令所需的帐户,另一个对象向程序提供指令数据。从前面的部分来看,大多数参数应该很熟悉。请注意,您可以在 mint 中设置任何委托地址,但通常应与leafOwner相同。无论如何,在转移 cNFT 时,委托会自动清除。我们将付款人设置为委托人,因为如果未提供接收者地址,他们也将是接收 cNFT 的人。


try {
    const txt = new Transaction().add(...mintInstructions);

    txt.feePayer = payer.publicKey;

    const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

    console.log(`Successfully minted a cNFT with the txt sig: ${transactionSignature}`);

  } catch (error: any) {
    console.error(`Failed to mint cNFT with error: ${error}`);
  }

然后我们构建交易,将付款人设置为FeePayer,然后发送交易。我们将此逻辑包含在try-catch块中,以防发送和确认交易时出现任何错误。如果确实发生任何错误,我们会使用console.error将它们记录到控制台。

使用 Umi 铸造 cNFT

Bubblegum 程序通过 Umi 提供两种铸造流程:

  • 铸造 NFT 而不将其与集合关联
  • 将 NFT 铸造到给定的集合中。

没有收藏的铸造

Bubblegum 的MintV1指令允许从 Bubblegum 树中铸造压缩的 NFT,而无需集合。如果这棵树是公共的,那么任何人都可以在这棵树上铸造。否则,只有树创建者或委托人可以使用此指令。以下是如何在没有集合的情况下铸造压缩的 NFT:


import { none } from '@metaplex-foundation/umi'
import { mintV1 } from '@metaplex-foundation/mpl-bubblegum'

await mintV1(umi, {
  leafOwner,
  merkleTree,
  metadata: {
    name: 'My Compressed NFT',
    uri: 'https://example.com/my-cnft.json',
    sellerFeeBasisPoints: 500, // 5%
    collection: none(),
    creators: [
      { address: umi.identity.publicKey, verified: false, share: 100 },
    ],
  },
}).sendAndConfirm(umi);

此代码片段来自 Metaplex 文档中有关使用 Bubblegum 铸造 cNFT 的内容。在这里,我们使用 Umi 的实例来铸造 cNFT。mintV1指令的其他参数如下:

  • leafOwner要铸造的 cNFT 的所有者
  • merkleTree是并发 Merkle 树账户地址 cNFT 将从中铸造
  • 数据是包含要铸造的 cNFT 元数据的对象。这包括 cNFT 的名称、它的 URI、我们未向任何人调用的它的集合,以及它的创建者。可以提供收集对象,但将创建者中的已验证字段设置为false,因为指令中未请求收集权限。创建者还可以通过将验证字段设置为 true 来验证自己,并将创建者提供为其余帐户中的签名者。

mintV1指令还包含许多可选字段,因为函数的输入类型为MintV1InstructionAccountsMintV1InstructionArgs。这些类型定义如下:


// Accounts
export type MintV1InstructionAccounts = {
  treeConfig?: PublicKey | Pda;
  leafOwner: PublicKey | Pda;
  leafDelegate?: PublicKey | Pda;
  merkleTree: PublicKey | Pda;
  payer?: Signer;
  treeCreatorOrDelegate?: Signer;
  logWrapper?: PublicKey | Pda;
  compressionProgram?: PublicKey | Pda;
  systemProgram?: PublicKey | Pda;
};

MintV1InstructionArgs是类型中难以捉摸的类型,可归结为具有元数据字段的对象。该元数据字段的类型为MetadataArgsArgs,定义如下:


export type MetadataArgsArgs = {
  /** The name of the asset */
  name: string;
  /** The symbol for the asset */
  symbol?: string;
  /** URI pointing to JSON representing the asset */
  uri: string;
  /** Royalty basis points that goes to creators in secondary sales (0-10000) */
  sellerFeeBasisPoints: number;
  primarySaleHappened?: boolean;
  isMutable?: boolean;
  /** nonce for easy calculation of editions, if present */
  editionNonce?: OptionOrNullable;
  /** Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. */
  tokenStandard?: OptionOrNullable;
  /** Collection */
  collection: OptionOrNullable;
  /** Uses */
  uses?: OptionOrNullable;
  tokenProgramVersion?: TokenProgramVersionArgs;
  creators: Array;
};

mintV1及其所有相关类型的完整函数定义可以在此处找到。但是,至少使用 Umi 实例,如果您传入必要的元数据、叶子所有者和并发 Merkle 树帐户,您可以在没有集合的情况下铸造 cNFT。

铸造收藏品

Bubblegum 提供了mintToCollectionV1作为直接将 cNFT 铸造到给定集合的便捷方法。该指令的输入属于MintToCollectionV1InstructionAccountsMintToCollectionV1InstructionArgs 类型,最终是MetadataArgsArgs类型的对象。MintToCollectionV1InstructionAccounts的类型定义如下:


// Accounts
export type MintToCollectionV1InstructionAccounts = {
  treeConfig?: PublicKey | Pda;
  leafOwner: PublicKey | Pda;
  leafDelegate?: PublicKey | Pda;
  merkleTree: PublicKey | Pda;
  payer?: Signer;
  treeCreatorOrDelegate?: Signer;
  collectionAuthority?: Signer;
  /**
   * If there is no collecton authority record PDA then
   * this must be the Bubblegum program address.
   */

  collectionAuthorityRecordPda?: PublicKey | Pda;
  collectionMint: PublicKey | Pda;
  collectionMetadata?: PublicKey | Pda;
  collectionEdition?: PublicKey | Pda;
  bubblegumSigner?: PublicKey | Pda;
  logWrapper?: PublicKey | Pda;
  compressionProgram?: PublicKey | Pda;
  tokenMetadataProgram?: PublicKey | Pda;
  systemProgram?: PublicKey | Pda;
};

关键参数是收藏造币厂、收藏机构和收藏机构记录PDA。使用委托催收机构时,必须提供委托记录PDA,以确保该机构有权管理催收NFT。元数据参数必须包含一个集合对象,其地址字段与集合 mint 参数匹配,并且其已验证字段设置为false。创建者还可以通过签署交易并将自己添加为剩余帐户来验证自己。

以下是如何使用集合铸造压缩 NFT:


import { none } from '@metaplex-foundation/umi'
import { mintToCollectionV1 } from '@metaplex-foundation/mpl-bubblegum'

await mintToCollectionV1(umi, {
  leafOwner,
  merkleTree,
  collectionMint,
  metadata: {
    name: 'My Compressed NFT',
    uri: 'https://example.com/my-cnft.json',
    sellerFeeBasisPoints: 500, // 5%
    collection: { key: collectionMint, verified: false },
    creators: [
      { address: umi.identity.publicKey, verified: false, share: 100 },
    ],
  },
}).sendAndConfirm(umi);

此代码片段可以在有关使用 Bubblegum 铸造 cNFT 的 Metaplex 文档中找到。同样,我们使用 Umi 的实例来铸造压缩的 NFT。与mintV1一样,我们传入leafOwnermerkleTree。然而,这一次我们传入了collectionMint。在元数据字段中,我们传入一个集合对象,其中键与collectionMint匹配,并且已验证字段设置为false。请注意,Umi 身份设置为默认收集机构。这可以通过将可选的collectionAuthority字段设置为自定义收集机构来更改。

使用 Helius 铸造 cNFT

在 Helius,我们提供了Mint API,允许您铸造压缩的 NFT,而无需额外的麻烦。我们承担 Solana 费用、Merkle 树创建,并将您的链下元数据上传到Arweave。我们还确保交易已成功提交并得到网络确认,因此您不必担心自己轮询。我们还解析交易中的资产 ID,以便您立即将其与DAS API一起使用。

要让 Helius 将 NFT 铸造到您的收藏中,必须授予其收藏权限。必须将权限委托给以下帐户之一,具体取决于您的集群:

  • 开发网络:2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC
  • 主网:HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R

以下是使用 Helius Mint API 铸造 cNFT 的方法:


const url = `https://mainnet.helius-rpc.com/?api-key=`;

const mintCompressedNft = async () => {
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            jsonrpc: '2.0',
            id: 'helius-test',
            method: 'mintCompressedNft',
            params: {
                name: 'Exodia the Forbidden One',
                symbol: 'ETFO',
                owner: 'DCQnfUH6mHA333mzkU22b4hMvyqcejUBociodq8bB5HF',
                description:
                    'Exodia the Forbidden One is a powerful, legendary creature composed of five parts: ' +
                    'the Right Leg, Left Leg, Right Arm, Left Arm, and the Head. When all five parts are assembled, Exodia becomes an unstoppable force.',
                attributes: [
                    {
                        trait_type: 'Type',
                        value: 'Legendary',
                    },
                    {
                        trait_type: 'Power',
                        value: 'Infinite',
                    },
                    {
                        trait_type: 'Element',
                        value: 'Dark',
                    },
                    {
                        trait_type: 'Rarity',
                        value: 'Mythical',
                    },
                ],
                imageUrl:
                    'https://cdna.artstation.com/p/assets/images/images/052/118/830/large/julie-almoneda-03.jpg?1658992401',
                externalUrl: 'https://www.yugioh-card.com/en/',
                sellerFeeBasisPoints: 6900,
            },
        }),
    });
    const { result } = await response.json();
    console.log('Minted asset: ', result.assetId);
};
mintCompressedNft();

可以在我们的文档中找到此代码片段和请求架构的进一步细分。

请注意,如果您不填写uri字段,我们将构建一个 JSON 文件并代表您将其上传到 Arweave。该文件将遵循v1.0 Metaplex JSON 标准,并将通过Irys(以前称为 Bundlr)上传。

转移 cNFT

传输压缩的 NFT 的一般步骤如下:

  • 从索引器获取cNFT的资产数据
  • 从索引器获取 cNFT 的证明
  • 从 Solana 获取并发 Merkle 树账户
  • 准备资产证明
  • 构建并发送转账交易

使用 Umi 和 Metaplex 极大地简化了这个过程,但本节将演示幕后发生的事情。下面我们将概述如何使用 web3.js 和 Metaplex 传输压缩的 NFT。

通过直接与泡泡糖互动进行传输

在使用脚本执行传输之前,我们需要获取有关压缩 NFT 的一些信息。首先,我们需要使用DAS API 上的getAsset方法来检索压缩的 NFT 元数据。在这里,我们正在寻找data_hashcreator_hashownerdelegateleaf_id


// Example getAsset call:
const url = `https://mainnet.helius-rpc.com/?api-key=`

const getAsset = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 'my-id',
      method: 'getAsset',
      params: {
        id: ''
      },
    }),
  });
  const { result } = await response.json();
  console.log("Asset: ", result);
};
getAsset();

成功响应的部分如下所示:


{
  ...
  },
  "compression": {
    "eligible": true,
    "compressed": true,
    "data_hash": "string",
    "creator_hash": "string",
    "asset_hash": "string",
    "tree": "string",
    "seq": 0,
    "leaf_id": 0
  ...
	"ownership": {
    ...
    "delegate": "string",
    "ownership_model": "string",
    "owner": "string",
    ...
  }
}

一旦我们获得了必要的信息,我们需要使用getAssetProof方法来检索证明tree_id(树的地址)。这是一个调用示例:


const url = `https://mainnet.helius-rpc.com/?api-key=`

const getAssetProof = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 'my-id',
      method: 'getAssetProof',
      params: {
        id: ''
      },
    }),
  });
  const { result } = await response.json();
  console.log("Assets Proof: ", result);
};
getAssetProof();

成功的响应如下所示:


{
  "root": "string",
  "proof": [
    "string"
  ],
  "node_index": 0,
  "leaf": "string",
  "tree_id": "string"
}

现在有了 root、proof 和 tree_id,我们就可以跳转到传输脚本了。

完整代码


import { Connection, Keypair, AccountMeta, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createTransferInstruction, PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
import {
  ConcurrentMerkleTreeAccount,
  SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

const transferCompressedNFT = async (
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  proof: string[],
  root: string,
  dataHash: string,
  creatorHash: string,
  leafId: number,
  owner: string,
  newLeafOwner: PublicKey,
  delegate: string
) => {
  const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);

  const treeAuthority = treeAccount.getAuthority();
  const canopyDepth = treeAccount.getCanopyDepth();

  const proofPath: AccountMeta[] = proof
    .map((node: string) => ({
      pubkey: new PublicKey(node),
      isSigner: false,
      isWritable: false,
    }))
    .slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));

  const leafOwner = new PublicKey(owner);
  const leafDelegate = new PublicKey(delegate);

  const transferInstruction = createTransferInstruction(
    {
      merkleTree: treeAddress,
      treeAuthority,
      leafOwner,
      leafDelegate,
      newLeafOwner,
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,
    },
    {
      root: [...new PublicKey(root.trim()).toBytes()],
      dataHash: [...new PublicKey(dataHash.trim()).toBytes()],
      creatorHash: [...new PublicKey(creatorHash.trim()).toBytes()],
      nonce: leafId,
      index: leafId,
    },
    PROGRAM_ID
  );

  try {
    const txt = new Transaction().add(transferInstruction);
    txt.feePayer = payer.publicKey;

    const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

    console.log(`Successfully transfered the cNFT with txt sig: ${transactionSignature}`);
  } catch (error: any) {
    console.error(`Failed to transfer cNFT with error: ${error}`);
  }
};

分解代码


import { Connection, Keypair, AccountMeta, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";

import { createTransferInstruction, PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";

import {
  ConcurrentMerkleTreeAccount,
  SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";

我们导入@solana/web3.js@metaplex-foundation/mpl-bubblegum@solana/spl-account-compression以及必要的模块。


const transferCompressedNFT = async (
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  proof: string[],
  root: string,
  dataHash: string,
  creatorHash: string,
  leafId: number,
  owner: string,
  newLeafOwner: PublicKey,
  delegate: string
) => {
	// Rest of the code
}

我们定义了transferCompressedNFT函数,我们将在其中解析证明路径,构建传输指令并执行它。


  const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);

  const treeAuthority = treeAccount.getAuthority();
  const canopyDepth = treeAccount.getCanopyDepth();

我们从区块链中获取并发的 Merkle 树账户,并提取树的权限和树冠深度。构建传输指令需要这些值。


const proofPath: AccountMeta[] = proof
    .map((node: string) => ({
      pubkey: new PublicKey(node),
      isSigner: false,
      isWritable: false,
    }))
    .slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));

简而言之,我们将证明地址列表解析为AccountMeta类型的有效数组。AccountMeta是用于定义交易的账户元数据。这包括账户的公钥、指令是否需要与公钥匹配的交易签名以及公钥是否可以作为读写账户加载。

我们从数组的开头开始提取完整证明的一部分,并确保我们只有proof.length - canopyDepth数量的证明值。我们这样做是为了删除已经缓存在链上冠层中的树部分。然后我们将每个剩余的证明值构造为有效的AccountMeta。之所以这样做,是因为证明是在转账指令中以“额外账户”的形式在链上提交的。


const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);

然后,我们将leafOwner设置为所有者参数,并将leafDelegate 设置委托参数。


const transferInstruction = createTransferInstruction(
    {
      merkleTree: treeAddress,
      treeAuthority,
      leafOwner,
      leafDelegate,
      newLeafOwner,
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,
    },
    {
      root: [...new PublicKey(root.trim()).toBytes()],
      dataHash: [...new PublicKey(dataHash.trim()).toBytes()],
      creatorHash: [...new PublicKey(creatorHash.trim()).toBytes()],
      nonce: leafId,
      index: leafId,
    },
    PROGRAM_ID
  );

我们使用Bubblegum SDK 中的createTransferInstruction辅助函数构建transferInstruction 。请注意,rootdataHashCreatorHash是从 DAS API 作为字符串返回的,因此我们必须将它们转换为PublicKey类型,然后转换为字节数组。


try {
    const txt = new Transaction().add(transferInstruction);
    txt.feePayer = payer.publicKey;

    const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
      commitment: "confirmed",
      skipPreflight: true,
    });

    console.log(`Successfully transfered the cNFT with txt sig: ${transactionSignature}`);
  } catch (error: any) {
    console.error(`Failed to transfer cNFT with error: ${error}`);
  }

通过我们构建的指令,我们将其添加到新交易中并将其发送到 Solana。如果有任何错误,我们将使用console.error将其记录到控制台。

如果您遇到有关并发 Merkle 树的错误,则您的 RPC 可能为并发 Merkle 树证明提供过时或不正确的数据。由于缓存问题,这种情况有时会发生。为了解决这个问题,您可以尝试对 RPC 提供的证明进行客户端验证:


const merkleTreeProof: MerkleTreeProof = {
    leafIndex: leafId,
    leaf: new PublicKey(leaf).toBuffer(),
    root: new PublicKey(root).toBuffer(),
    proof: proof.map((node: string) => new PublicKey(node).toBuffer()),
};

const currentRoot = treeAccount.getCurrentRoot();
const rpcRoot = new PublicKey(root).toBuffer();

console.log(new PublicKey(currentRoot).toBase58() === new PublicKey(rpcRoot).toBase58());

请注意,您还需要使用getAssetProof DAS API调用返回的值。这不是必需的,因为实际的证明验证是在链上执行的,但是,这可能有助于错误处理。

这样,您可以再次调用getAsset来查看leafDelegate是一个空值,并且叶子有一个新的所有者!

与 Umi 一起传输


import { getAssetWithProof, transfer } from '@metaplex-foundation/mpl-bubblegum'

const assetWithProof = await getAssetWithProof(umi, assetId)
await transfer(umi, {
  ...assetWithProof,
  leafOwner: currentLeafOwner,
  newLeafOwner: newLeafOwner.publicKey,
}).sendAndConfirm(umi);

该代码来自 Metaplex 有关传输压缩 NFT 的文档。

Bubblegum 提供了非常简单易用的传输指令。首先,它接受一个 Umi 实例。然后它接受一个对象,该对象包含资产及其证明、叶所有者和新叶所有者的信息。要获取资产及其必要的证明,我们可以使用Bubblegum 也提供的getAssetWithProof方法。请注意,叶委托可以用来代替叶所有者 - 只需一个有权授权转移的帐户。使用.sendAndConfirm()方法,我们发送启动传输的交易,然后使用我们的 Umi 实例进行确认。

结论

恭喜!我们以非常全面的方式探索了 Solana 上的状态压缩和压缩 NFT。我们已经了解了并发 Merkle 树的复杂性,揭开了常见误解的神秘面纱,并深入研究了 Solana 的账本。撇开理论不谈,我们已经学习了如何使用 Solana 的 web3.js、Metaplex 和 Helius 的强大功能来获取、铸造和传输 cNFT!

在交易和存储成本受到限制的环境中,Solana 的状态压缩具有革命性意义。压缩可大幅降低成本,而不会影响安全性或去中心化。这是一种范式转变,为艺术家、收藏家和开发商等开辟了前所未有的可能性。

如果您已经做到了这一点,谢谢您!您完全有能力为这个激动人心的前沿领域做出贡献。继续前进 - 为您的链上 MMORPG 铸造一千万个 NFT 集合,构建一个利用账本力量的去中心化应用程序,或者只是与社区分享您新发现的知识。预测未来的最好方法就是创造未来。

💡
原文链接:All You Need to Know About Compression on Solana
本文由SlerfTools翻译,转载请注明出处。

SlerfTools专为Solana设计的工具箱,致力于简化区块链操作,提供无编程全可视化界面,使发币管理流动性无代码创建Dapp等复杂过程变得安全简单。