新消息:Helius 收购 Light Protocol
如何在 Solana 上使用压缩 NFT
博客/开发

Solana NFT:修改压缩 NFT(2023)

开发者关系负责人LinkedIn 上的 Hunter Davis
阅读需 6 分钟

Solana NFT:为什么需要修改

作为 Solana 开发者,与压缩 NFT 交互似乎困难重重。现在,你可以通过 Bubblegum 程序修改压缩 NFT,不必再为此困扰。

铸造压缩 NFT 后,如需对该 NFT 进行任何更改(例如转移、销毁),必须随交易指令提供 Merkle 证明。如果你还不熟悉这个概念,Merkle 证明是一组哈希值,可以证明某个叶节点属于这棵树。

本指南将介绍:

  • 修改压缩 NFT 的重要性。
  • 为压缩 NFT 设置销毁和转移操作。
  • 详细说明数字资产标准(DAS)API 的重要性。

Solana NFT:前置条件

开始之前,请确保你已具备:

  • JavaScript/TypeScript 基础知识。
  • 对压缩 NFT 有一定了解。
  • 已安装 Git。
  • npm 或 yarn。
  • 已克隆示例代码仓库,可从此处获取。

Solana NFT:环境设置

  1. 克隆示例代码仓库:
代码
git clone
  1. 进入项目文件夹:
代码
cd compression-examples
  1. 安装 npm:
代码
npm install
  1. 在根目录创建 .env 文件:
代码
API_KEY=YOUR_API_KEY
SECRET_KEY=YOUR_WALLET_SECRET_KEY

在其中填入你的 Helius API 密钥和付款方的钱包私钥。请确保该钱包中有足够的 SOL 来支付交易费用。

Solana NFT:修改

修改需要获取指令所需的当前证明,以及资产当前的所有权、权限和压缩哈希。你需要从数字资产标准(DAS)API 获取这些数据。该 API 可以简化与压缩资产的交互。

目前可通过 Bubblegum 执行的修改包括:

  • 铸造
  • 转移
  • 销毁
  • 委托、取消委托
  • 赎回、取消赎回
  • 解压缩
  • 验证创建者、设置并验证创建者
  • 验证集合、设置并验证集合

要与压缩资产交互,你需要确保返回以下内容:

  • getAssetProof - 获取用于计算的证明。
  • getAsset - 获取创建者哈希、数据哈希、叶节点 nonce,以及当前所有者/叶节点委托者。
  • getBubblegumAuthorityPDA - 根据树 ID 派生的程序地址。

在这些示例中,我们将演示如何销毁和转移所铸造的压缩 NFT。

NFT:转移

转移操作可以将压缩 NFT 从当前所有者的钱包转移到另一个钱包。压缩 NFT 的销售和上架都会使用此指令,以在市场中显示销售信息。

要在现有代码仓库中进行设置,我们将打开 utils.ts 文件并查看 transferAsset 函数。

我们需要传入以下内容:

  • 连接 - Solana RPC 连接。
  • 当前所有者 - 当前 NFT 所有者的公钥。
  • NFT 的新所有者 - 新所有者的公钥。
  • 资产 ID - 用于获取所传入特定资产的当前详细信息。

在下面的示例 transferAsset 函数中可以看到,我们首先请求资产证明,并设置一个名为 proofPath 的对象。这是向 DAS API 发出的请求,用于返回待转移资产的证明。你需要对其进行计算,以便传入交易指令。

代码
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {
  console.log(
    `Transfering asset ${assetId} from ${owner.publicKey.toBase58()} to ${newOwner.publicKey.toBase58()}.
    This will depend on indexer api calls to fetch the necessary data.`
  );
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  if (!assetProof?.proof || assetProof.proof.length === 0) {
    throw new Error("Proof is empty");
  }
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
  }));
  console.log("Successfully got proof path from RPC.");

// Additional code...

};

你需要返回资产证明,因为它反映了压缩资产的当前状态。每次转移压缩 NFT 都需要此证明。你需要在 proofPath 中进行计算,以便将其传入稍后介绍的交易指令。

现在我们已经获得证明路径,可以通过 DAS API 调用 getAsset,返回叶节点 nonce、叶节点委托者、数据哈希和创建者哈希。

可以按如下方式设置:

代码
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

	// Previous code //

	// Calling getAsset from DAS
  const rpcAsset = await connectionWrapper.getAsset(assetId);
  console.log(
    "Successfully got asset from RPC. Current owner: " +
      rpcAsset.ownership.owner
  );
  if (rpcAsset.ownership.owner !== owner.publicKey.toBase58()) {
    throw new Error(
      `NFT is not owned by the expected owner. Expected ${owner.publicKey.toBase58()} but got ${
        rpcAsset.ownership.owner
      }.`
    );
  }
// Leaf nonce from getAsset call.
  const leafNonce = rpcAsset.compression.leaf_id;
// Locating Tree Authority.
  const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
// Leaf Owner/Delegate from getAsset call.
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);
  );
};

你可以看到,代码发起了 getAsset 请求,以返回所有者/委托者和叶节点 nonce。我们还仅使用树 ID(由 assetProof 请求返回)定义了树权限。

定义完这些内容后,我们就可以开始构建交易指令。

代码
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

	// Previous code //

	// Transfer Instruction.
  let transferIx = createTransferInstruction(
    {
      treeAuthority, // Tree authority
      leafOwner: new PublicKey(rpcAsset.ownership.owner), // Current NFT owner
      leafDelegate: leafDelegate, // Leaf delegate/owner returned
      newLeafOwner: newOwner.publicKey, // New wallet to transfer our NFT.
      merkleTree: new PublicKey(assetProof.tree_id), // Merkle tree public key.
      logWrapper: SPL_NOOP_PROGRAM_ID, // NOOP program ID.
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, // Compression Program ID.
      anchorRemainingAccounts: proofPath, // Proofs computed to pass in
    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)), // Root from getAssetProof returned
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim()) // Data Hash from getAsset
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim()) // Creator Hash from getAsset
      ),
      nonce: leafNonce, // Leaf nonce from getAsset
      index: leafNonce,
    }
  );
};

要对转移的资产执行此操作,你需要传入由 getAsset、getAssetProof 和 Metaplex SDK 返回的所有变量:叶节点委托者/所有者、数据哈希、根和叶节点 nonce。

你还会注意到,我们传入了几个程序 ID。它们是 compression 和 noop 程序的公钥,这些程序已在文件顶部导入。你也可以直接以公钥形式传入这些值。

完成这些设置后,就可以发送交易指令:

代码
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

 // Previous code //

  const tx = new Transaction().add(transferIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to transfer compressed asset", e);
    throw e;
  }
};

在上面的代码中,我们使用 tx 定义交易,并为来自 solana/web3.js 的 transferIx 添加 Transaction 类型。

然后,我们传入以下内容来提交交易:

  • Solana 连接
  • 交易指令
  • 付款方
  • 确认级别

现在可以运行以下命令来提交这笔交易:

代码
npm run e2e

此函数将铸造一个压缩 NFT 及其集合,然后将其转移到指定钱包。

以下是完整的 transferAsset 函数:

代码
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {
  console.log(
    `Transfering asset ${assetId} from ${owner.publicKey.toBase58()} to ${newOwner.publicKey.toBase58()}.
    This will depend on indexer api calls to fetch the necessary data.`
  );
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  if (!assetProof?.proof || assetProof.proof.length === 0) {
    throw new Error("Proof is empty");
  }
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
  }));
  console.log("Successfully got proof path from RPC.");

  const rpcAsset = await connectionWrapper.getAsset(assetId);
  console.log(
    "Successfully got asset from RPC. Current owner: " +
      rpcAsset.ownership.owner
  );
  if (rpcAsset.ownership.owner !== owner.publicKey.toBase58()) {
    throw new Error(
      `NFT is not owned by the expected owner. Expected ${owner.publicKey.toBase58()} but got ${
        rpcAsset.ownership.owner
      }.`
    );
  }
  const leafNonce = rpcAsset.compression.leaf_id;
  const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);
  let transferIx = createTransferInstruction(
    {
      treeAuthority,
      leafOwner: new PublicKey(rpcAsset.ownership.owner),
      leafDelegate: leafDelegate,
      newLeafOwner: newOwner.publicKey,
      merkleTree: new PublicKey(assetProof.tree_id),
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,
    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)),
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim())
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim())
      ),
      nonce: leafNonce,
      index: leafNonce,
    }
  );
  const tx = new Transaction().add(transferIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to transfer compressed asset", e);
    throw e;
  }
};

NFT: 销毁

销毁 cNFT 会从 Merkle 树中彻底移除该 NFT。这支持多种用例,例如销毁以换取奖励、移除垃圾资产,或满足自定义需求。Tensor 的铸造活动就使用了这种机制:销毁 10 个 cNFT,以铸造 1 个新 cNFT。

要销毁压缩 NFT,我们需要传入以下内容:

  • 连接 - Solana RPC 连接。
  • 当前所有者 - 当前 NFT 所有者的公钥。
  • 资产 ID - 用于获取所传入特定资产的当前详细信息。

可以在 utils.ts 的 burnAsset 下看到相关代码:

代码
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  const rpcAsset = await connectionWrapper.getAsset(assetId);
  const leafNonce = rpcAsset.compression.leaf_id;
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
}));
const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);

// Remaining code //

}

这与 transferAsset 相同,因为它使用 getAssetProof 返回根和树 ID。你还会调用 getAsset,以返回创建者哈希、数据哈希、叶节点 nonce 和当前所有者。

这样,你可以轻松确定修改压缩资产时需要返回哪些值。

现在,我们可以在这段代码下方设置交易指令:

代码
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {

	// Previous code //

// Burn transaction instruction //
  const burnIx = createBurnInstruction(
    {
      treeAuthority,
      leafOwner: new PublicKey(rpcAsset.ownership.owner),
      leafDelegate,
      merkleTree: new PublicKey(assetProof.tree_id),
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,

    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)),
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim())
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim())
      ),
      nonce: leafNonce,
      index: leafNonce,
    }
  );
 // Later code... //
};

完成销毁设置后,可以使用与转移指令非常相似的方式提交交易:

代码
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {

// Previous code //

const tx = new Transaction().add(burnIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to burn compressed asset", e);
    throw e;
  }
}

然后,我们传入以下内容来提交交易:

  • Solana 连接
  • 交易指令
  • 付款方
  • 确认级别

运行此函数后,它会销毁你所提供 ID 对应的资产。这会从树中移除资产数据,并在压缩资产上设置 burnt=true 标记。默认情况下,使用 DAS API 时仍会返回这些已销毁的资产。

现在可以在终端中运行:

代码
npm run burn -- --assetId=

Solana NFT:总结

压缩 NFT 为参与 Solana 上的 NFT 生态系统提供了一种灵活方式。开发者可以通过 DAS API 获取 Merkle 证明,并使用精确的交易指令修改这些独特的数字资产并与之交互。掌握转移和销毁操作的细节后,开发者就能构建更可靠的应用。

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新