
Solana NFT:修改压缩 NFT(2023)
Solana NFT:为什么需要修改
作为 Solana 开发者,与压缩 NFT 交互似乎困难重重。现在,你可以通过 Bubblegum 程序修改压缩 NFT,不必再为此困扰。
铸造压缩 NFT 后,如需对该 NFT 进行任何更改(例如转移、销毁),必须随交易指令提供 Merkle 证明。如果你还不熟悉这个概念,Merkle 证明是一组哈希值,可以证明某个叶节点属于这棵树。
本指南将介绍:
- 修改压缩 NFT 的重要性。
- 为压缩 NFT 设置销毁和转移操作。
- 详细说明数字资产标准(DAS)API 的重要性。
Solana NFT:前置条件
开始之前,请确保你已具备:
- JavaScript/TypeScript 基础知识。
- 对压缩 NFT 有一定了解。
- 已安装 Git。
- npm 或 yarn。
- 已克隆示例代码仓库,可从此处获取。
Solana NFT:环境设置
- 克隆示例代码仓库:
git clone- 进入项目文件夹:
cd compression-examples- 安装 npm:
npm install- 在根目录创建 .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 及其集合,然后将其转移到指定钱包。
请记下资产 ID,因为我们将在销毁操作中使用它。
以下是完整的 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 开发的最新动态,并在我们发布新内容时收到更新


