这篇文章深入探讨了 Solana 上的压缩 NFT (cNFT),重点介绍了 account-compression
和 mpl-bubblegum
程序。cNFT 利用 Merkle 树将 NFT 数据存储在链下,从而显著降低成本和复杂性。文章详细解释了 cNFT 与传统 NFT 的区别,以及如何使用相关程序进行创建、Mint、转移和销毁 cNFT。
cNFT
想象一下尝试在 Solana 上铸造 100 万个 NFT。传统的 NFT 将需要 200 万个链上账户:每个 NFT 一个 mint 账户和一个 token 账户,从而导致过高的存储和交易成本。压缩 NFT (cNFT) 通过将 NFT 数据以 Merkle 树的形式存储在链下,而仅将根存储在链上来解决这个问题。这种方法大幅降低了成本,使大规模 NFT 项目从不切实际变为经济可行。
在这篇文章中,我们将探讨 Solana 上的 压缩 NFT (cNFT),重点关注 account-compression
和 mpl-bubblegum
程序。我们将介绍 cNFT 如何使用 Merkle 树在链下存储 NFT 数据,从而显著降低成本和复杂性。本指南专为希望了解 cNFT 内部运作方式而无需深入研究代码库的开发人员和审计人员设计。
我们将首先了解什么是 cNFT,然后深入研究 account-compression
程序,最后探讨 mpl-bubblegum
。
在阅读本文时,你无需打开代码,这里解释了一切!😉
在 Solana 上,NFT 与常规 token 类似。唯一的区别是 NFT 的总供应量为 1。 要持有 NFT,你需要:
两者都在 SPL Token 程序下进行管理。
对于 100 个 NFT,你需要:
总共 200 个账户,每个账户都有自己的公钥和租金存款。尤其是租金成本会迅速增加。
由于我们需要为每个 NFT 提供链上账户,因此扩展 NFT 会很快变得昂贵。例如,在游戏中,你可能需要数百万个 NFT,从而导致数百万个账户。
为了解决这个问题,引入了 压缩 NFT (cNFT)。它们使用 Merkle 树 (Concurrent Merkle Trees) 在链下存储 NFT 数据(ID、所有者、元数据等),并且仅将 Merkle 树的根 存储在链上。
为什么我们将 Merkle 树的根存储在链上账户中?
对于验证,例如,如果有人想转移自己的 NFT,我们会传递基于该 NFT 的叶子,并在根的帮助下,我们验证该叶子是否存在于 Merkle 树上。
树中的每个叶子代表一个完整的 NFT,包括所有者和元数据。无需创建数千个账户,你只需创建一个 Merkle 树账户并使用 Merkle 证明验证所有权。
这大大降低了成本,从数百万个账户减少到 2 个账户。
meme
Account Compression Program 的源代码可在 GitHub 上找到。
这个想法很简单:
account-compression
虽然在本指南中它用于 cNFT,但该程序是通用的,也可以支持其他用例。
每个叶子都可以代表一个 NFT,其中包含其所有者和相关元数据。当发生诸如转移之类的操作时,更改将在链下进行,并且仅使用根在链上验证证明。
叶子的示例:
LeafSchema::new_v1(
asset_id,
leaf_owner.key(),
leaf_delegate.key(),
nonce,
data_hash,
creator_hash,
);
让我们深入研究代码,从 Noop 程序开始。
这是一个占位符程序,什么也不做(no-op 意味着不操作)。它用于发出日志,供链下索引器拾取。(有关无操作日志记录及其计算成本优势的更多信息,请参阅 我们的 100 个 Solana 技巧中的技巧 #11。)
当你通过 noop
发送交易时,它不会进行任何计算,但索引器可以获取调用数据,将其解释为日志并相应地处理这些数据。
use solana_program::{
account_info::AccountInfo, declare_id, entrypoint::ProgramResult, instruction::Instruction,
pubkey::Pubkey,
};
declare_id!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV");
##[cfg(not(feature = "no-entrypoint"))]
solana_program::entrypoint!(noop);
pub fn noop(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}
pub fn instruction(data: Vec<u8>) -> Instruction {
Instruction {
program_id: crate::id(),
accounts: vec![],
data,
}
}
调用示例:
pub fn wrap_event<'info>(
event: &AccountCompressionEvent,
noop_program: &Program<'info, Noop>,
) -> Result<()> {
invoke(
&spl_noop::instruction(event.try_to_vec()?),
&[noop_program.to_account_info()],
)?;
Ok(())
}
event.try_to_vec()?
是我们在链下需要的数据。
这是 Merkle 树管理的核心。它包括以下指令:
让我们简要总结一下关键指令:
初始化一个新的空 Merkle 树。
/// 用于初始化新的 SPL ConcurrentMerkleTree 的上下文
##[derive(Accounts)]
pub struct Initialize<'info> {
#[account(zero)]
/// CHECK: 此账户将被清零,并且将验证大小
pub merkle_tree: UncheckedAccount<'info>,
/// 控制对树的写入访问权限的权限
/// 通常是一个程序,例如,Bubblegum 合约验证叶子是否为有效的 NFT。
pub authority: Signer<'info>,
/// 用于将变更日志作为 cpi 指令数据发出的程序。
pub noop: Program<'info, Noop>,
}
pub fn init_empty_merkle_tree(
ctx: Context<Initialize>,
max_depth: u32,
max_buffer_size: u32,
) -> Result<()> {
此数据必须事先计算好,具体取决于需要。
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (mut header_bytes, rest) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.initialize(
max_depth,
max_buffer_size,
&ctx.accounts.authority.key(),
Clock::get()?.slot,
);
header.serialize(&mut header_bytes)?;
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
let id = ctx.accounts.merkle_tree.key();
let change_log_event = merkle_tree_initialize_empty(&header, id, tree_bytes)?;
wrap_event(
&AccountCompressionEvent::ChangeLog(*change_log_event),
&ctx.accounts.noop,
)?;
update_canopy(canopy_bytes, header.get_max_depth(), None)
}
这是标头的结构:
pub struct ConcurrentMerkleTreeHeader {
/// 账户类型
pub account_type: CompressionAccountType,
/// 版本化的标头
pub header: ConcurrentMerkleTreeHeaderData,
}
我们有两种类型:Uninitialized 和 ConcurrentMerkleTree。
pub struct ConcurrentMerkleTreeHeaderDataV1 {
max_buffer_size: u32,
max_depth: u32,
authority: Pubkey,
creation_slot: u64,
_padding: [u8; 6],
}
它将类型设置为 ConcurrentMerkleTree 并分配其他字段。
merkle_tree_bytes
的其余部分用于 Merkle 树,而另一部分 (rest-merkletreesize) 将用于 canopy_bytes
。spl-concurrent-merkle-tree
和 wrap_event
进行初始化。change_log_event
。我们将把这些数据发送到 noop 程序,以便索引器获取这些数据并在链下进行更改。canopy_bytes
呢?canopy
当我们想要更改叶子时,我们必须提供证明。证明在剩余的账户中提供,因此对于每个节点,我们必须传递一个账户。由于 交易大小限制,我们无法传递许多节点。因此,在某些情况下,当最大深度很高时,我们会在交易中遇到大小限制。
解决方案?Canopy Nodes
这些 canopy_bytes
帮助我们生成和提供证明。这些是树中的一些上层节点,我们将在证明中需要这些节点。因此,我们将这些节点存储在链上账户中,并且在提供证明期间,我们不需要提供所有节点,我们传递一些较低的节点,并且 account-compression 程序会为我们附加这些上层节点。
例如:如果我们有 10 的深度并存储 3 个顶部节点,这 3 个顶部节点是 canopy_bytes
,我们不需要将它们传递给程序。当我们提供剩余的 7 个节点作为证明时,程序将使用这些 canopy_bytes
更新证明。
pub fn update_canopy(
canopy_bytes: &mut [u8],
max_depth: u32,
change_log: Option<&ChangeLogEvent>,
) -> Result<()> {
check_canopy_bytes(canopy_bytes)?;
let canopy = cast_slice_mut::<u8, Node>(canopy_bytes);
let path_len = get_cached_path_length(canopy, max_depth)?;
if let Some(cl_event) = change_log {
match &*cl_event {
ChangeLogEvent::V1(cl) => {
// 从最新的变更日志更新 canopy
for path_node in cl.path.iter().rev().skip(1).take(path_len as usize) {
// node_idx - 2 映射到 canopy 索引
canopy[(path_node.index - 2) as usize] = path_node.node;
}
}
}
}
Ok(())
}
它首先检查 canopy_bytes 的大小是否乘以每个节点,然后确定应将 Merkle 路径中的多少个节点更新到 canopy 中。
然后,如果我们希望事件为我们创建数据,在我们的例子中我们不需要,我们将 change_log 设置为 None。
将 Merkle 树的写入访问权限从一个权限转移到另一个权限。
pub fn transfer_authority(
ctx: Context<TransferAuthority>,
new_authority: Pubkey,
) -> Result<()> {
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (mut header_bytes, _) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid_authority(&ctx.accounts.authority.key())?;
header.set_new_authority(&new_authority);
header.serialize(&mut header_bytes)?;
Ok(())
}
关闭未使用的 Merkle 树并将租金 (lamports) 返回给接收者。
##[derive(Accounts)]
pub struct CloseTree<'info> {
#[account(mut)]
/// CHECK: 此账户在指令中验证
pub merkle_tree: AccountInfo<'info>,
/// 控制对树的写入访问权限的权限
pub authority: Signer<'info>,
/// CHECK: 之后的资金接收者
#[account(mut)]
pub recipient: AccountInfo<'info>,
}
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (header_bytes, rest) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid_authority(&ctx.accounts.authority.key())?;
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
let id = ctx.accounts.merkle_tree.key();
merkle_tree_prove_tree_is_empty(&header, id, tree_bytes)?;
// 关闭 Merkle 树账户
// 1. 移动 lamports
let dest_starting_lamports = ctx.accounts.recipient.lamports();
**ctx.accounts.recipient.lamports.borrow_mut() = dest_starting_lamports
.checked_add(ctx.accounts.merkle_tree.lamports())
.unwrap();
**ctx.accounts.merkle_tree.lamports.borrow_mut() = 0;
// 2. 将所有 CMT 账户字节设置为 0
header_bytes.fill(0);
tree_bytes.fill(0);
canopy_bytes.fill(0);
Ok(())
这是权限可以将新叶子追加到树的函数。
这是我们想要铸造 NFT 时在 Bubblegum 中使用的函数。
##[derive(Accounts)]
pub struct Modify<'info> {
#[account(mut)]
/// CHECK: 此账户在指令中验证
pub merkle_tree: UncheckedAccount<'info>,
/// 控制对树的写入访问权限的权限
/// 通常是一个程序,例如,Bubblegum 合约验证叶子是否为有效的 NFT。
pub authority: Signer<'info>,
/// 用于将变更日志作为 cpi 指令数据发出的程序。
pub noop: Program<'info, Noop>,
}
leaf: [u8; 32]
LeafSchema::new_v1(
asset_id,
leaf_owner.key(),
leaf_delegate.key(),
nonce,
data_hash,
creator_hash,
);
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (header_bytes, rest) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid_authority(&ctx.accounts.authority.key())?;
let id = ctx.accounts.merkle_tree.key();
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
let change_log_event = merkle_tree_append_leaf(&header, id, tree_bytes, &leaf)?;
update_canopy(
canopy_bytes,
header.get_max_depth(),
Some(&change_log_event),
)?;
wrap_event(
&AccountCompressionEvent::ChangeLog(*change_log_event),
&ctx.accounts.noop,
)
尝试在特定索引处插入叶子。如果失败,则追加它。
root: [u8; 32],
leaf: [u8; 32],
index: u32,
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (header_bytes, rest) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid_authority(&ctx.accounts.authority.key())?;
header.assert_valid_leaf_index(index)?;
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
let mut proof = vec![];
for node in ctx.remaining_accounts.iter() {
proof.push(node.key().to_bytes());
}
fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
// 调用 ConcurrentMerkleTree::fill_empty_or_append
let id = ctx.accounts.merkle_tree.key();
let args = &FillEmptyOrAppendArgs {
current_root: root,
leaf,
proof_vec: proof,
index,
};
let change_log_event = merkle_tree_fill_empty_or_append(&header, id, tree_bytes, args)?;
update_canopy(
canopy_bytes,
header.get_max_depth(),
Some(&change_log_event),
)?;
wrap_event(
&AccountCompressionEvent::ChangeLog(*change_log_event),
&ctx.accounts.noop,
)
leaf_index < (1 << self.get_max_depth())
。remaining_accounts
获得。canopy
更新证明,正如我们所说,由于交易限制,由于我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。merkle_tree_append_leaf
作为 change_log_event,而是传递了 merkle_tree_fill_empty_or_append
。这是替换现有叶子的函数。
这是我们想要转移、销毁或对 NFT 执行其他操作时在 Bubblegum 中使用的函数。
root: [u8; 32],
previous_leaf: [u8; 32],
new_leaf: [u8; 32],
index: u32,
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?;
let (header_bytes, rest) =
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid_authority(&ctx.accounts.authority.key())?;
header.assert_valid_leaf_index(index)?;
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
let mut proof = vec![];
for node in ctx.remaining_accounts.iter() {
proof.push(node.key().to_bytes());
}
fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
let id = ctx.accounts.merkle_tree.key();
// 调用 ConcurrentMerkleTree::set_leaf(root, previous_leaf, new_leaf, proof, index)
let args = &SetLeafArgs {
current_root: root,
previous_leaf,
new_leaf,
proof_vec: proof,
index,
};
let change_log_event = merkle_tree_set_leaf(&header, id, tree_bytes, args)?;
update_canopy(
canopy_bytes,
header.get_max_depth(),
Some(&change_log_event),
)?;
wrap_event(
&AccountCompressionEvent::ChangeLog(*change_log_event),
&ctx.accounts.noop,
)
与上面相同:
leaf_index < (1 << self.get_max_depth())
。remaining_accounts
获得。canopy
更新证明,正如我们所说,由于交易限制,我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。merkle_tree_append_leaf
作为 change_log_event,而是传递了 merkle_tree_set_leaf
。验证给定的叶子是否存在于 Merkle 树中。
pub struct VerifyLeaf<'info> {
/// CHECK: 此账户在指令中验证
pub merkle_tree: UncheckedAccount<'info>,
}
root: [u8; 32],
leaf: [u8; 32],
index: u32,
require_eq!(
*ctx.accounts.merkle_tree.owner,
crate::id(),
AccountCompressionError::IncorrectAccountOwner
);
let merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_data()?;
let (header_bytes, rest) =
merkle_tree_bytes.split_at(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
header.assert_valid()?;
header.assert_valid_leaf_index(index)?;
let merkle_tree_size = merkle_tree_get_size(&header)?;
let (tree_bytes, canopy_bytes) = rest.split_at(merkle_tree_size);
let mut proof = vec![];
for node in ctx.remaining_accounts.iter() {
proof.push(node.key().to_bytes());
}
fill_in_proof_from_canopy(canopy_bytes, header.get_max_depth(), index, &mut proof)?;
let id = ctx.accounts.merkle_tree.key();
let args = &ProveLeafArgs {
current_root: root,
leaf,
proof_vec: proof,
index,
};
merkle_tree_prove_leaf(&header, id, tree_bytes, args)?;
Ok(())
leaf_index < (1 << self.get_max_depth())
。remaining_accounts
获得。canopy
更新证明,正如我们所说,由于交易限制,我们无法在剩余账户中传递完整的证明,因此 canopy 将在此处提供帮助。MPL-bubblegum 程序的源代码可在 GitHub 上找到。
该程序处理铸造、转移和销毁 cNFT 的逻辑。它与 account-compression
程序协同工作。
某些指令有两个版本(v1
和 v2
)。这篇文章涵盖了 v1
。
v1 和 v2 之间的区别:
- 使用 MPL Core 集合而不是 Token Metadata 集合。
- 使用精简的
MetadataV2
参数,它消除了 collection verified 标志。在MetadataV2
中,任何包含的集合都自动被认为是已验证的。- 允许在 MPL Core 集合上使用插件,例如 Royalties、Permanent Burn Delegate 等,以授权对 Bubblegum 资产的操作。请注意,
BubblegumV2
插件也必须存在于 MPL Core 集合上,才能用于 Bubblegum。有关兼容的 集合级别插件的列表,请参见 MPL CoreBubblegumV2
插件。- 允许冻结/解冻资产,以及将资产设置为永久不可转让(soulbound)。不可转让类似于冻结,但允许所有者销毁资产,而冻结则不允许。
- 尚未可用,但可以选择指定与资产关联的数据(和模式)。
用于使用 account-compression
在链上创建一个新的 Merkle 树。
pub struct CreateTree<'info> {
#[account(\
init,\
seeds = [merkle_tree.key().as_ref()],\
payer = payer,\
space = TREE_AUTHORITY_SIZE,\
bump,\
)]
pub tree_authority: Account<'info, TreeConfig>,
/// CHECK: This account must be all zeros
#[account(zero)]
pub merkle_tree: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub tree_creator: Signer<'info>,
pub log_wrapper: Program<'info, SplNoop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
}
tree_authority
是一个由 merkle_tree
派生的 PDA。 max_depth: u32,
max_buffer_size: u32,
public: Option<bool>,
let merkle_tree = ctx.accounts.merkle_tree.to_account_info();
check_canopy_size(
ctx.accounts.merkle_tree.data.borrow(),
ctx.accounts.tree_authority.key(),
max_depth,
max_buffer_size,
)?;
let seed = merkle_tree.key();
let seeds = &[seed.as_ref(), &[ctx.bumps.tree_authority]];
let authority = &mut ctx.accounts.tree_authority;
authority.set_inner(TreeConfig {
tree_creator: ctx.accounts.tree_creator.key(),
tree_delegate: ctx.accounts.tree_creator.key(),
total_mint_capacity: 1 << max_depth,
num_minted: 0,
is_public: public.unwrap_or(false),
is_decompressible: DecompressibleState::Disabled,
version: crate::state::leaf_schema::Version::V1,
});
let authority_pda_signer = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.compression_program.to_account_info(),
spl_account_compression::cpi::accounts::Initialize {
authority: ctx.accounts.tree_authority.to_account_info(),
merkle_tree,
noop: ctx.accounts.log_wrapper.to_account_info(),
},
authority_pda_signer,
);
spl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size)
tree_creator
、tree_delegate
、total_mint_capacity
等初始化 TreeConfig。
tree_delegate
能够铸造新 token,tree_creator
也能。spl_account_compression
并创建一个新的空 Merkle 树。将一个新的 cNFT 铸造到树中。
##[derive(Accounts)]
pub struct MintV1<'info> {
#[account(\
mut,\
seeds = [merkle_tree.key().as_ref()],\
bump,\
)]
pub tree_authority: Account<'info, TreeConfig>,
/// CHECK: This account is neither written to nor read from.
pub leaf_owner: AccountInfo<'info>,
/// CHECK: This account is neither written to nor read from.
pub leaf_delegate: AccountInfo<'info>,
/// CHECK: This account is modified in the downstream program
#[account(mut)]
pub merkle_tree: UncheckedAccount<'info>,
pub payer: Signer<'info>,
pub tree_delegate: Signer<'info>,
pub log_wrapper: Program<'info, SplNoop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
}
tree_creator
: 如果它不是 public,那么它必须是 tree_creator 或 tree_delegate。tree_authority
,以及 TreeConfig
的数据是必需的。leaf_owner
和 leaf_delegate
,leaf_delegate
也可以转移或销毁 token。message: MetadataArgs
pub struct MetadataArgs {
/// The name of the asset
pub name: String,
/// The symbol for the asset
pub symbol: String,
/// URI pointing to JSON representing the asset
pub uri: String,
/// Royalty basis points that goes to creators in secondary sales (0-10000)
pub seller_fee_basis_points: u16,
/// Immutable, once flipped, all sales of this metadata are considered secondary.
pub primary_sale_happened: bool,
/// Whether or not the data struct is mutable, default is not
pub is_mutable: bool,
/// nonce for easy calculation of editions, if present
pub edition_nonce: Option<u8>,
/// Token standard. Currently only `NonFungible` is allowed.
pub token_standard: Option<TokenStandard>,
/// Collection
pub collection: Option<Collection>,
/// Uses
pub uses: Option<Uses>,
pub token_program_version: TokenProgramVersion,
pub creators: Vec<Creator>,
}
let payer = ctx.accounts.payer.key();
let incoming_tree_delegate = ctx.accounts.tree_delegate.key();
let merkle_tree = &ctx.accounts.merkle_tree;
// V1 instructions only work with V1 trees.
let authority = &mut ctx.accounts.tree_authority;
require!(
authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
if !authority.is_public {
require!(
incoming_tree_delegate == authority.tree_creator
|| incoming_tree_delegate == authority.tree_delegate,
BubblegumError::TreeAuthorityIncorrect,
);
}
if !authority.contains_mint_capacity(1) {
return Err(BubblegumError::InsufficientMintCapacity.into());
}
// Create a HashSet to store signers to use with creator validation. Any signer can be
// counted as a validated creator.
let mut metadata_auth = HashSet::<Pubkey>::new();
metadata_auth.insert(payer);
metadata_auth.insert(incoming_tree_delegate);
// If there are any remaining accounts that are also signers, they can also be used for
// creator validation.
metadata_auth.extend(
ctx.remaining_accounts
.iter()
.filter(|a| a.is_signer)
.map(|a| a.key()),
);
let leaf = process_mint(
message,
&ctx.accounts.leaf_owner,
Some(&ctx.accounts.leaf_delegate),
metadata_auth,
ctx.bumps.tree_authority,
authority,
merkle_tree,
&ctx.accounts.log_wrapper,
&ctx.accounts.compression_program,
false,
)?;
authority.increment_mint_count();
Ok(leaf)
process_mint
: let asset_id = get_asset_id(&merkle_tree.key(), tree_authority.num_minted);
solana_program::msg!("Leaf asset ID: {}", asset_id);
let leaf_delegate = leaf_delegate.unwrap_or(leaf_owner);
let version = message.version();
let leaf = match version {
Version::V1 => LeafSchema::new_v1(
asset_id,
leaf_owner.key(),
leaf_delegate.key(),
tree_authority.num_minted,
data_hash.to_bytes(),
creator_hash.to_bytes(),
),
Version::V2 => {
let collection_hash = hash_collection_option(message.collection_key())?;
LeafSchema::new_v2(
asset_id,
leaf_owner.key(),
leaf_delegate.key(),
tree_authority.num_minted,
data_hash.to_bytes(),
creator_hash.to_bytes(),
collection_hash,
DEFAULT_ASSET_DATA_HASH,
DEFAULT_FLAGS,
)
}
};
crate::utils::wrap_application_data_v1(version, leaf.to_event().try_to_vec()?, wrapper)?;
append_leaf(
version,
&merkle_tree.key(),
authority_bump,
&compression_program.to_account_info(),
&tree_authority.to_account_info(),
&merkle_tree.to_account_info(),
&wrapper.to_account_info(),
leaf.to_node(),
)?;
由于每个 NFT 必须具有唯一的 ID,因此它基于 Merkle 树和 token 的索引来计算 asset_id
。请注意,每次我们铸造一个新 token 时,num_minted
都会增加,并且在销毁过程中不会减少。
正如我们所见,链上没有创建新账户。以前,我们需要两个账户来拥有一个 NFT。
那么 Bubblegum 是如何做到这一点并检查 cNFT
所有者和其他数据的呢?
每当铸造新的 CNFT 时,不会创建新账户;我们只需根据需要的数据计算 leaf,然后插入 leaf。
每当用户想要转移或对其 NFT 执行操作时,此处不进行验证,并且正如所见,account-compression 也不进行验证,唯一完成的检查是 leaf 必须存在于树中,因此 Bubblegum 巧妙地实现了这一点,不是从用户那里获取 leaf 并对所有者进行验证,而是每次都计算 leaf,因此如果有人想要转移其他用户的 token,则 leaf 将被错误地计算,因此只有当正确的 authority 调用转移时,才会计算出正确的 leaf。以 Burn
指令为例进行检查。
通过将其 Merkle leaf 替换为一个空的 leaf 来销毁 cNFT。
所有者或 delegate 必须签署交易。
pub struct Burn<'info> {
#[account(\
seeds = [merkle_tree.key().as_ref()],\
bump,\
)]
pub tree_authority: Account<'info, TreeConfig>,
/// CHECK: This account is checked in the instruction
pub leaf_owner: UncheckedAccount<'info>,
/// CHECK: This account is checked in the instruction
pub leaf_delegate: UncheckedAccount<'info>,
/// CHECK: This account is modified in the downstream program
#[account(mut)]
pub merkle_tree: UncheckedAccount<'info>,
pub log_wrapper: Program<'info, SplNoop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
}
merkle_tree
、leaf_owner
或 leaf_delegate
(其中之一必须是签署者)。 root: [u8; 32],
data_hash: [u8; 32],
creator_hash: [u8; 32],
nonce: u64,
index: u32,
// V1 instructions only work with V1 trees.
require!(
ctx.accounts.tree_authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
let owner = ctx.accounts.leaf_owner.to_account_info();
let delegate = ctx.accounts.leaf_delegate.to_account_info();
// Burn must be initiated by either the leaf owner or leaf delegate.
require!(
owner.is_signer || delegate.is_signer,
BubblegumError::LeafAuthorityMustSign
);
let merkle_tree = ctx.accounts.merkle_tree.to_account_info();
let asset_id = get_asset_id(&merkle_tree.key(), nonce);
let previous_leaf = LeafSchema::new_v1(
asset_id,
owner.key(),
delegate.key(),
nonce,
data_hash,
creator_hash,
);
let new_leaf = Node::default();
replace_leaf(
Version::V1,
&merkle_tree.key(),
ctx.bumps.tree_authority,
&ctx.accounts.compression_program.to_account_info(),
&ctx.accounts.tree_authority.to_account_info(),
&ctx.accounts.merkle_tree.to_account_info(),
&ctx.accounts.log_wrapper.to_account_info(),
ctx.remaining_accounts,
root,
previous_leaf.to_node(),
new_leaf,
index,
)
previous_leaf
。类似于 burn
,但替换为分配给新所有者的新 leaf。
快速浏览:
new_leaf_owner
账户。cNFT 能够从 Merkle 树中赎回并存在于链上。为此,NFT 必须先被赎回,然后再被解压缩。
准备将 cNFT 从压缩的 Merkle 树转移到链上 NFT。
pub struct Redeem<'info> {
#[account(\
seeds = [merkle_tree.key().as_ref()],\
bump,\
)]
pub tree_authority: Account<'info, TreeConfig>,
#[account(mut)]
pub leaf_owner: Signer<'info>,
/// CHECK: This account is checked in the instruction
pub leaf_delegate: UncheckedAccount<'info>,
/// CHECK: This account is modified in the downstream program
#[account(mut)]
pub merkle_tree: UncheckedAccount<'info>,
#[account(\
init,\
seeds = [\
VOUCHER_PREFIX.as_ref(),\
merkle_tree.key().as_ref(),\
& nonce.to_le_bytes()\
],\
payer = leaf_owner,\
space = VOUCHER_SIZE,\
bump\
)]
pub voucher: Account<'info, Voucher>,
pub log_wrapper: Program<'info, SplNoop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
}
leaf_owner
必须是签署者。voucher
:这是一个账户,当初始化时,意味着 NFT 处于赎回状态,可以被取消或解压缩。 root: [u8; 32],
data_hash: [u8; 32],
creator_hash: [u8; 32],
nonce: u64,
index: u32,
// V1 instructions only work with V1 trees.
require!(
ctx.accounts.tree_authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
if ctx.accounts.tree_authority.is_decompressible == DecompressibleState::Disabled {
return Err(BubblegumError::DecompressionDisabled.into());
}
let leaf_owner = ctx.accounts.leaf_owner.key();
let leaf_delegate = ctx.accounts.leaf_delegate.key();
let merkle_tree = ctx.accounts.merkle_tree.to_account_info();
let asset_id = get_asset_id(&merkle_tree.key(), nonce);
let previous_leaf = LeafSchema::new_v1(
asset_id,
leaf_owner,
leaf_delegate,
nonce,
data_hash,
creator_hash,
);
let new_leaf = Node::default();
replace_leaf(
Version::V1,
&merkle_tree.key(),
ctx.bumps.tree_authority,
&ctx.accounts.compression_program.to_account_info(),
&ctx.accounts.tree_authority.to_account_info(),
&ctx.accounts.merkle_tree.to_account_info(),
&ctx.accounts.log_wrapper.to_account_info(),
ctx.remaining_accounts,
root,
previous_leaf.to_node(),
new_leaf,
index,
)?;
ctx.accounts
.voucher
.set_inner(Voucher::new(previous_leaf, index, merkle_tree.key()));
Ok(())
Node::default()
。is_decompressible
进行额外检查,以确保它能够解压缩。pub struct Voucher {
pub leaf_schema: LeafSchema,
pub index: u32,
pub merkle_tree: Pubkey,
}
取消赎回操作,因此与之前相同;它仅关闭凭证并将 leaf 替换为凭证上的 leaf。
这是必须调用的指令,用于将 cNFT 从链下(Merkle 树上的 leaf)转移到链上账户。它必须在调用 redeem 之后调用。
##[derive(Accounts)]
pub struct DecompressV1<'info> {
#[account(\
mut,\
close = leaf_owner,\
seeds = [\
VOUCHER_PREFIX.as_ref(),\
voucher.merkle_tree.as_ref(),\
voucher.leaf_schema.nonce().to_le_bytes().as_ref()\
],\
bump\
)]
pub voucher: Box<Account<'info, Voucher>>,
#[account(mut)]
pub leaf_owner: Signer<'info>,
/// CHECK: versioning is handled in the instruction
#[account(mut)]
pub token_account: UncheckedAccount<'info>,
/// CHECK: versioning is handled in the instruction
#[account(\
mut,\
seeds = [\
ASSET_PREFIX.as_ref(),\
voucher.merkle_tree.as_ref(),\
voucher.leaf_schema.nonce().to_le_bytes().as_ref(),\
],\
bump\
)]
pub mint: UncheckedAccount<'info>,
/// CHECK:
#[account(\
mut,\
seeds = [mint.key().as_ref()],\
bump,\
)]
pub mint_authority: UncheckedAccount<'info>,
/// CHECK:
#[account(mut)]
pub metadata: UncheckedAccount<'info>,
/// CHECK: Initialized in Token Metadata Program
#[account(mut)]
pub master_edition: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
pub sysvar_rent: Sysvar<'info, Rent>,
/// CHECK:
pub token_metadata_program: Program<'info, MplTokenMetadata>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub log_wrapper: Program<'info, SplNoop>,
}
voucher
:先前初始化的 voucher 账户,用于验证解压缩。它将在此指令结束时关闭,以回收租金。leaf_owner
:必须是签署者。表示压缩资产的合法所有者。注意:leaf_delegate
未 授权执行此操作。token_account
:leaf_owner
和 mint
的关联 token 账户。如果它不存在,将被创建。mint
:从 Merkle 树和资产的 nonce 派生的 PDA。用作解压缩 NFT 的唯一链上 mint。mint_authority
:用于签署铸造和元数据指令的 PDA。暂时分配给你的程序,然后重新分配给系统程序以锁定它。metadata
:通过 Token Metadata 程序创建的元数据账户。存储链上 NFT 数据,例如名称、符号、URI、创建者和集合信息。master_edition
:NFT 的 Master Edition 账户,表明它是一种非同质化资产(1-of-1 或限量版)。大多数市场都需要它来验证资产的唯一性和元数据。 metadata: MetadataArgs
逻辑很简单,但相当长。
// Validate the incoming metadata
let incoming_data_hash = hash_metadata(&metadata)?;
if !cmp_bytes(
&ctx.accounts.voucher.leaf_schema.data_hash(),
&incoming_data_hash,
32,
) {
return Err(BubblegumError::HashingMismatch.into());
}
if !cmp_pubkeys(
&ctx.accounts.voucher.leaf_schema.owner(),
ctx.accounts.leaf_owner.key,
) {
return Err(BubblegumError::AssetOwnerMismatch.into());
}
let voucher = &ctx.accounts.voucher;
match metadata.token_program_version {
TokenProgramVersion::Original => {
if ctx.accounts.mint.data_is_empty() {
invoke_signed(
&system_instruction::create_account(
&ctx.accounts.leaf_owner.key(),
&ctx.accounts.mint.key(),
Rent::get()?.minimum_balance(Mint::LEN),
Mint::LEN as u64,
&spl_token::id(),
),
&[\
ctx.accounts.leaf_owner.to_account_info(),\
ctx.accounts.mint.to_account_info(),\
ctx.accounts.system_program.to_account_info(),\
],
&[&[\
ASSET_PREFIX.as_bytes(),\
voucher.merkle_tree.key().as_ref(),\
voucher.leaf_schema.nonce().to_le_bytes().as_ref(),\
&[ctx.bumps.mint],\
]],
)?;
invoke(
&spl_token::instruction::initialize_mint2(
&spl_token::id(),
&ctx.accounts.mint.key(),
&ctx.accounts.mint_authority.key(),
Some(&ctx.accounts.mint_authority.key()),
0,
)?,
&[\
ctx.accounts.token_program.to_account_info(),\
ctx.accounts.mint.to_account_info(),\
],
)?;
}
if ctx.accounts.token_account.data_is_empty() {
invoke(
&spl_associated_token_account::instruction::create_associated_token_account(
&ctx.accounts.leaf_owner.key(),
&ctx.accounts.leaf_owner.key(),
&ctx.accounts.mint.key(),
&spl_token::ID,
),
&[\
ctx.accounts.leaf_owner.to_account_info(),\
ctx.accounts.mint.to_account_info(),\
ctx.accounts.token_account.to_account_info(),\
ctx.accounts.token_program.to_account_info(),\
ctx.accounts.associated_token_program.to_account_info(),\
ctx.accounts.system_program.to_account_info(),\
ctx.accounts.sysvar_rent.to_account_info(),\
],
)?;
}
// SPL token will check that the associated token account is initialized, that it
// has the correct owner, and that the mint (which is a PDA of this program)
// matches.
invoke_signed(
&spl_token::instruction::mint_to(
&spl_token::id(),
&ctx.accounts.mint.key(),
&ctx.accounts.token_account.key(),
&ctx.accounts.mint_authority.key(),
&[],
1,
)?,
&[\
ctx.accounts.mint.to_account_info(),\
ctx.accounts.token_account.to_account_info(),\
ctx.accounts.mint_authority.to_account_info(),\
ctx.accounts.token_program.to_account_info(),\
],
&[&[\
ctx.accounts.mint.key().as_ref(),\
&[ctx.bumps.mint_authority],\
]],
)?;
}
TokenProgramVersion::Token2022 => return Err(ProgramError::InvalidArgument.into()),
}
msg!("Creating metadata");
CreateMetadataAccountV3CpiBuilder::new(&ctx.accounts.token_metadata_program)
.metadata(&ctx.accounts.metadata)
.mint(&ctx.accounts.mint)
.mint_authority(&ctx.accounts.mint_authority)
.payer(&ctx.accounts.leaf_owner)
.update_authority(&ctx.accounts.mint_authority, true)
.system_program(&ctx.accounts.system_program)
.data(DataV2 {
name: metadata.name.clone(),
symbol: metadata.symbol.clone(),
uri: metadata.uri.clone(),
creators: if metadata.creators.is_empty() {
None
} else {
Some(metadata.creators.iter().map(|c| c.adapt()).collect())
},
collection: metadata.collection.map(|c| c.adapt()),
seller_fee_basis_points: metadata.seller_fee_basis_points,
uses: metadata.uses.map(|u| u.adapt()),
})
.is_mutable(metadata.is_mutable)
.invoke_signed(&[&[\
ctx.accounts.mint.key().as_ref(),\
&[ctx.bumps.mint_authority],\
]])?;
msg!("Creating master edition");
CreateMasterEditionV3CpiBuilder::new(&ctx.accounts.token_metadata_program)
.edition(&ctx.accounts.master_edition)
.mint(&ctx.accounts.mint)
.mint_authority(&ctx.accounts.mint_authority)
.update_authority(&ctx.accounts.mint_authority)
.metadata(&ctx.accounts.metadata)
.payer(&ctx.accounts.leaf_owner)
.system_program(&ctx.accounts.system_program)
.token_program(&ctx.accounts.token_program)
.max_supply(0)
.invoke_signed(&[&[\
ctx.accounts.mint.key().as_ref(),\
&[ctx.bumps.mint_authority],\
]])?;
ctx.accounts
.mint_authority
.to_account_info()
.assign(&System::id());
Ok(())
mint_authority
作为 authority 进行初始化。mint_authority
分配给 crate::id()
,这是创建元数据所必需的。与 SPL 程序不同,它将检查此 PDA 的所有者,因此需要此步骤。mint_authority
分配回 System::id()
。这是一个用于更改 tree.is_decompressible
状态的小指令(默认情况下为 false)。
正如你所记得的,这个变量在赎回过程中被检查。
##[derive(Accounts)]
pub struct SetDecompressibleState<'info> {
#[account(mut, has_one = tree_creator)]
pub tree_authority: Account<'info, TreeConfig>,
pub tree_creator: Signer<'info>,
}
pub(crate) fn set_decompressible_state(
ctx: Context<SetDecompressibleState>,
decompressable_state: DecompressibleState,
) -> Result<()> {
// V1 instructions only work with V1 trees.
require!(
ctx.accounts.tree_authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
ctx.accounts.tree_authority.is_decompressible = decompressable_state;
Ok(())
}
该指令用于将新的压缩 NFT (cNFT) 铸造到 Merkle 树中,并将其分配给经过验证的集合。它仅支持 Bubblegum V1 Merkle 树,并使用 Token Metadata 程序验证集合。
##[derive(Accounts)]
pub struct MintToCollectionV1<'info> {
#[account(\
mut,\
seeds = [merkle_tree.key().as_ref()],\
bump,\
)]
pub tree_authority: Account<'info, TreeConfig>,
/// CHECK: This account is neither written to nor read from.
pub leaf_owner: AccountInfo<'info>,
/// CHECK: This account is neither written to nor read from.
pub leaf_delegate: AccountInfo<'info>,
/// CHECK: This account is modified in the downstream program
#[account(mut)]
pub merkle_tree: UncheckedAccount<'info>,
pub payer: Signer<'info>,
pub tree_delegate: Signer<'info>,
pub collection_authority: Signer<'info>,
/// CHECK: Optional collection authority record PDA. If not present, must be the Bubblegum program address.
pub collection_authority_record_pda: UncheckedAccount<'info>,
/// CHECK: Checked inside the downstream logic
pub collection_mint: UncheckedAccount<'info>,
#[account(mut)]
pub collection_metadata: Box<Account<'info, TokenMetadata>>,
/// CHECK: Checked inside the downstream logic
pub edition_account: UncheckedAccount<'info>,
/// CHECK: Legacy; not used but kept for compatibility
pub bubblegum_signer: UncheckedAccount<'info>,
pub log_wrapper: Program<'info, Noop>,
pub compression_program: Program<'info, SplAccountCompression>,
/// CHECK: Legacy; not used but kept for compatibility
pub token_metadata_program: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
tree_authority
:Merkle 树的 authority。包含权限和铸造容量。leaf_owner
,leaf_delegate
:压缩 NFT 的未来所有者。merkle_tree
:存储压缩 NFT leaf 的 Merkle 树账户。payer
:支付任何指令开销。如果包含在元数据中,也算作创建者 authority。tree_delegate
:授权铸造到 Merkle 树的签署者,必须与 tree_delegate
或 tree_creator
匹配。collection_authority
:验证集合的签署者(必须获得集合的批准)。collection_authority_record_pda
:可选的 PDA,证明对集合的委托 authority。collection_mint
:经过验证的集合的 mint 账户。collection_metadata
:保存链上集合元数据的账户。edition_account
:用于验证集合的版本账户。metadata_args: MetadataArgs
该逻辑执行多个验证并处理铸造:
// V1 instructions only work with V1 trees.
require!(
ctx.accounts.tree_authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
let mut message = metadata_args;
let payer = ctx.accounts.payer.key();
let incoming_tree_delegate = ctx.accounts.tree_delegate.key();
let authority = &mut ctx.accounts.tree_authority;```
let collection_metadata = &ctx.accounts.collection_metadata;
let collection_mint = ctx.accounts.collection_mint.to_account_info();
let edition_account = ctx.accounts.edition_account.to_account_info();
let collection_authority = ctx.accounts.collection_authority.to_account_info();
let collection_authority_record_pda = ctx
.accounts
.collection_authority_record_pda
.to_account_info();
if !authority.is_public {
require!(
incoming_tree_delegate == authority.tree_creator
|| incoming_tree_delegate == authority.tree_delegate,
BubblegumError::TreeAuthorityIncorrect,
);
}
if !authority.contains_mint_capacity(1) {
return Err(BubblegumError::InsufficientMintCapacity.into());
}
// Create a HashSet to store signers to use with creator validation. Any signer can be
// counted as a validated creator.
// 创建一个 HashSet 来存储签名者,以便与创建者验证一起使用。任何签名者都可以被算作已验证的创建者。
let mut metadata_auth = HashSet::<Pubkey>::new();
metadata_auth.insert(payer);
metadata_auth.insert(incoming_tree_delegate);
// If there are any remaining accounts that are also signers, they can also be used for
// creator validation.
// 如果还有其他也是签名者的剩余账户,它们也可以用于创建者验证。
metadata_auth.extend(
ctx.remaining_accounts
.iter()
.filter(|a| a.is_signer)
.map(|a| a.key()),
);
let collection = message
.collection
.as_mut()
.ok_or(BubblegumError::CollectionNotFound)?;
collection.verified = true;
process_collection_verification_mpl_only(
collection_metadata,
&collection_mint,
&collection_authority,
&collection_authority_record_pda,
&edition_account,
collection,
)?;
let leaf = process_mint(
message,
&ctx.accounts.leaf_owner,
Some(&ctx.accounts.leaf_delegate),
metadata_auth,
ctx.bumps.tree_authority,
authority,
merkle_tree,
&ctx.accounts.log_wrapper,
&ctx.accounts.compression_program,
true,
)?;
authority.increment_mint_count();
Ok(leaf)
verified
.This is the process that you could delegate your cNFT
.
这是一个你可以委托你的 cNFT
的过程。
Setting a new leaf_delegate
that can transfer or burn the nft.
设置一个新的 leaf_delegate
,它可以转移或销毁 NFT。
##[derive(Accounts)]
pub struct Delegate<'info> {
#[account(\
seeds = [merkle_tree.key().as_ref()],\
bump,\
)]
/// CHECK: This account is neither written to nor read from.
/// 检查:这个账户既没有被写入也没有被读取。
pub tree_authority: Account<'info, TreeConfig>,
pub leaf_owner: Signer<'info>,
/// CHECK: This account is neither written to nor read from.
/// 检查:这个账户既没有被写入也没有被读取。
pub previous_leaf_delegate: UncheckedAccount<'info>,
/// CHECK: This account is neither written to nor read from.
/// 检查:这个账户既没有被写入也没有被读取。
pub new_leaf_delegate: UncheckedAccount<'info>,
/// CHECK: This account is modified in the downstream program
/// 检查:这个账户在下游程序中被修改
#[account(mut)]
pub merkle_tree: UncheckedAccount<'info>,
pub log_wrapper: Program<'info, SplNoop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
}
leaf_owner
that must be signer(only the owner is able to delegate not delagator).leaf_owner
必须是签名者(只有所有者能够委托,而不是委托者)。 root: [u8; 32],
data_hash: [u8; 32],
creator_hash: [u8; 32],
nonce: u64,
index: u32,
data needed for the calculation of the leaf. 计算 leaf 所需的数据。
// V1 instructions only work with V1 trees.
// V1 指令仅适用于 V1 树。
require!(
ctx.accounts.tree_authority.version == Version::V1,
BubblegumError::UnsupportedSchemaVersion
);
let merkle_tree = ctx.accounts.merkle_tree.to_account_info();
let owner = ctx.accounts.leaf_owner.key();
let previous_delegate = ctx.accounts.previous_leaf_delegate.key();
let new_delegate = ctx.accounts.new_leaf_delegate.key();
let asset_id = get_asset_id(&merkle_tree.key(), nonce);
let previous_leaf = LeafSchema::new_v1(
asset_id,
owner,
previous_delegate,
nonce,
data_hash,
creator_hash,
);
let new_leaf = LeafSchema::new_v1(
asset_id,
owner,
new_delegate,
nonce,
data_hash,
creator_hash,
);
crate::utils::wrap_application_data_v1(
Version::V1,
new_leaf.to_event().try_to_vec()?,
&ctx.accounts.log_wrapper,
)?;
replace_leaf(
Version::V1,
&merkle_tree.key(),
ctx.bumps.tree_authority,
&ctx.accounts.compression_program.to_account_info(),
&ctx.accounts.tree_authority.to_account_info(),
&ctx.accounts.merkle_tree.to_account_info(),
&ctx.accounts.log_wrapper.to_account_info(),
ctx.remaining_accounts,
root,
previous_leaf.to_node(),
new_leaf.to_node(),
index,
)
new_delegate
.verified
.Set a new tree_delegate
for a tree; tree_delegate
can mint new NFTs.
为一个树设置一个新的 tree_delegate
;tree_delegate
可以铸造新的 NFT。
##[derive(Accounts)]
pub struct SetTreeDelegate<'info> {
#[account(\
mut,\
seeds = [merkle_tree.key().as_ref()],\
bump,\
has_one = tree_creator\
)]
pub tree_authority: Account<'info, TreeConfig>,
pub tree_creator: Signer<'info>,
/// CHECK: this account is neither read from or written to
/// 检查:此账户既未读取也未写入
pub new_tree_delegate: UncheckedAccount<'info>,
/// CHECK: Used to derive `tree_authority`
/// 检查:用于派生 `tree_authority`
pub merkle_tree: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
pub(crate) fn set_tree_delegate(ctx: Context<SetTreeDelegate>) -> Result<()> {
ctx.accounts.tree_authority.tree_delegate = ctx.accounts.new_tree_delegate.key();
Ok(())
}
Compressed NFTs (cNFTs) offer a scalable, cost-effective way to manage large volumes of NFTs on Solana, especially for use cases like gaming, collectables, and ticketing. By leveraging Merkle trees and off-chain data storage, developers can drastically reduce the number of on-chain accounts, saving on fees and improving efficiency. (For specific compute costs of Merkle proof verification and other operations, see our cost of security analysis.) 压缩 NFT (cNFT) 提供了一种可扩展、经济高效的方式来管理 Solana 上大量的 NFT,尤其适用于游戏、收藏品和票务等用例。通过利用 Merkle 树和链下数据存储,开发人员可以大大减少链上账户的数量,从而节省费用并提高效率。(有关 Merkle 证明验证和其他操作的具体计算成本,请参阅我们的安全成本分析。)
The combination of account-compression and mpl-bubblegum provides a powerful toolkit for creating, managing, and interacting with cNFTs, without compromising on validation or ownership security. 账户压缩和 mpl-bubblegum 的结合提供了一个强大的工具包,用于创建、管理和交互 cNFT,而不会影响验证或所有权安全。
Whether you're building a high-throughput dApp or auditing an existing system, understanding how these programs work under the hood gives you the edge to optimise and innovate. 无论你是构建高吞吐量的 dApp 还是审计现有系统,了解这些程序在底层是如何工作的,都能让你在优化和创新方面取得优势。
If you’ve made it this far, you now have a solid grasp of how cNFTs work on Solana. The future of NFTs is not just about art, it’s about scale. And cNFTs make that scale possible. 如果你已经走到了这一步,那么你现在已经扎实地掌握了 cNFT 在 Solana 上的工作原理。NFT 的未来不仅仅是关于艺术,而是关于规模。而 cNFT 使这种规模成为可能。
- 原文链接: accretion.xyz/blog/compr...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!