솔라나 cNFT 사용 방법

솔라나 온체인 자산 활용을 위한 가장 저렴하고 빠른 솔루션 cNFT의 사용 방법입니다.

액세스 프로토콜
Access Protocol
14 min readDec 12, 2023

--

블라드(Vlad)는 액세스 프로토콜의 풀 스택 소프트웨어 엔지니어로, 솔라나 프로그램 개발과 백엔드 및 UI를 담당하고 있습니다. 블라드는 전통 핀테크 회사에서 대규모 시스템을 구축하였으며, 이후 웹3로 뛰어들었습니다. 엑스(트위터) 또는 이메일을 통해 블라드에게 연락하실 수 있습니다.

cNFT 생성 비용은 없다고 생각해도 될 정도로 저렴하기 때문에 직접 수수료를 지불하고 사용자에게 이를 부담하게 하지 않아도 됩니다. 하지만 이러한 방법에 대한 예제는 찾아보기 어렵습니다.

https://x.com/itsTaipan/status/1723507510307406179?s=20

따라서 저는 얼마 전 액세스 프로토콜 V2 프로그램 작업을 진행하면서 만든 솔루션의 초안을 공유하기로 결정했습니다. 다음은 온체인 프로그램에서 cNFT를 발행하고 소각하는 방법을 간략히 설명합니다.

use anchor_lang::prelude::*;
use mpl_bubblegum::instructions::BurnCpiBuilder;
use mpl_bubblegum::instructions::MintToCollectionV1CpiBuilder;
use mpl_bubblegum::types::{Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard};
use mpl_token_metadata;
use solana_program::pubkey::Pubkey;
use spl_account_compression::{
Noop, program::SplAccountCompression,
};

declare_id!("HcmjtyqZgSeNFdKvHCBCDNEJHSwrf9KveBrbXQKXPxqN");

#[program]
pub mod cnft_vault {
use super::*;

pub fn mint_cnft<'info>(ctx: Context<'_, '_, '_, 'info, Mint<'info>>,
name: String,
symbol: String,
uri: String,
seller_fee_basis_points: u16) -> Result<()> {
msg!("minting nft");
let burn_ix = MintToCollectionV1CpiBuilder::new(
&ctx.accounts.bubblegum_program.to_account_info(),
)
.tree_config(&ctx.accounts.tree_config.to_account_info())
.leaf_owner(&ctx.accounts.leaf_owner.to_account_info())
.leaf_delegate(&ctx.accounts.leaf_delegate.to_account_info())
.merkle_tree(&ctx.accounts.merkle_tree.to_account_info())
.payer(&ctx.accounts.payer.to_account_info())
.tree_creator_or_delegate(&ctx.accounts.tree_delegate.to_account_info())
.collection_authority(&ctx.accounts.collection_authority.to_account_info())
.collection_authority_record_pda(Some(&ctx
.accounts
.collection_authority_record_pda
.to_account_info()))
.collection_mint(&ctx.accounts.collection_mint.to_account_info())
.collection_metadata(&ctx.accounts.collection_metadata.to_account_info())
.collection_edition(&ctx.accounts.edition_account.to_account_info())
.bubblegum_signer(&ctx.accounts.bubblegum_signer.to_account_info())
.log_wrapper(&ctx.accounts.log_wrapper.to_account_info())
.compression_program(&ctx.accounts.compression_program.to_account_info())
.token_metadata_program(&ctx.accounts.token_metadata_program.to_account_info())
.system_program(&ctx.accounts.system_program.to_account_info())
.metadata(
MetadataArgs {
name,
symbol,
uri,
creators: vec![Creator {
address: ctx.accounts.collection_authority.key(),
verified: true,
share: 100,
}],
seller_fee_basis_points,
primary_sale_happened: false,
is_mutable: false,
edition_nonce: Some(0),
uses: None,
collection: Some(Collection {
verified: true,
key: ctx.accounts.collection_mint.key(),
}),
token_program_version: TokenProgramVersion::Original,
token_standard: Some(TokenStandard::NonFungible),
}
)
.invoke();
Ok(())
}


pub fn burn_cnft<'info>(ctx: Context<'_, '_, '_, 'info, BurnAccs<'info>>,
root: [u8; 32],
data_hash: [u8; 32],
creator_hash: [u8; 32],
nonce: u64,
index: u32) -> Result<()> {
msg!("burning nft");

let remaining_accounts: Vec<(&AccountInfo, bool, bool)> = ctx.remaining_accounts
.iter()
.map(|account| (account, account.is_signer, account.is_writable))
.collect();


let burn_ix = BurnCpiBuilder::new(
&ctx.accounts.bubblegum_program.to_account_info(),
)
.tree_config(&ctx.accounts.tree_config.to_account_info())
.leaf_owner(&ctx.accounts.leaf_owner.to_account_info(), true)
.leaf_delegate(&ctx.accounts.leaf_delegate.to_account_info(), false)
.merkle_tree(&ctx.accounts.merkle_tree.to_account_info())
.log_wrapper(&ctx.accounts.log_wrapper.to_account_info())
.compression_program(&ctx.accounts.compression_program.to_account_info())
.system_program(&ctx.accounts.system_program.to_account_info())
.add_remaining_accounts(&remaining_accounts)
.root(root)
.data_hash(data_hash)
.creator_hash(creator_hash)
.nonce(nonce)
.index(index)
.invoke();

Ok(())
}
}

#[error_code]
pub enum MyError {
#[msg("No signer")]
NoSigner
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct MintParams {
uri: String,
}

#[derive(Clone)]
pub struct MplBubblegum;

impl Id for MplBubblegum {
fn id() -> Pubkey {
mpl_bubblegum::ID
}
}

#[derive(Clone)]
pub struct MplTokenMetadata;

impl Id for MplTokenMetadata {
fn id() -> Pubkey {
mpl_token_metadata::ID
}
}

#[derive(Accounts)]
pub struct Mint<'info> {
pub payer: Signer<'info>,

/// CHECK: This account is checked in the instruction
#[account(mut)]
pub tree_config: UncheckedAccount<'info>,

/// 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>,

#[account(mut)]
/// CHECK: unsafe
pub merkle_tree: UncheckedAccount<'info>,

pub tree_delegate: Signer<'info>,

pub collection_authority: Signer<'info>,

/// CHECK: Optional collection authority record PDA.
/// If there is no collecton authority record PDA then
/// this must be the Bubblegum program address.
pub collection_authority_record_pda: UncheckedAccount<'info>,

/// CHECK: This account is checked in the instruction
pub collection_mint: UncheckedAccount<'info>,

/// CHECK:
#[account(mut)]
pub collection_metadata: UncheckedAccount<'info>,
//
/// CHECK: This account is checked in the instruction
pub edition_account: UncheckedAccount<'info>,

/// CHECK: This is just used as a signing PDA.
pub bubblegum_signer: UncheckedAccount<'info>,
pub log_wrapper: Program<'info, Noop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub token_metadata_program: Program<'info, MplTokenMetadata>,
pub bubblegum_program: Program<'info, MplBubblegum>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct BurnAccs<'info> {
/// CHECK: This account is checked in the instruction
#[account(mut)]
pub leaf_owner: Signer<'info>,
/// CHECK: This account is checked in the instruction
#[account(mut)]
pub leaf_delegate: Signer<'info>,
#[account(mut)]
/// CHECK: This account is modified in the downstream program
pub merkle_tree: UncheckedAccount<'info>,
/// CHECK: This account is checked in the instruction
pub tree_config: UncheckedAccount<'info>,
pub log_wrapper: Program<'info, Noop>,
pub compression_program: Program<'info, SplAccountCompression>,
pub bubblegum_program: Program<'info, MplBubblegum>,
pub system_program: Program<'info, System>,
}

메모

1. 많은 예제에서 이전 버전을 사용하지만, 여러분은 최신 버전인 mpl-bubblegum = “1.0.0”을 사용하고 싶을 것입니다.
2. 계정을 더 철저하게 확인할 수 있습니다.
3. 이 접근 방식은 머클 트리 생성을 캡슐화하지 않습니다. 프로그램 외부에서 이 작업을 수행하거나 다른 방법을 추가하여 이 문제를 해결할 수 있습니다. 이 문제를 어떻게 해결했는지 남겨주세요.

이 코드를 사용하는 더 복잡한 앵커 예제는 깃허브 리포지토리에서 찾아볼 수 있습니다.

자유롭게 확인하시고 발견한 문제를 지적하거나 모두를 위해 코드를 개선할 수 있도록 PR을 만들어 주세요.

--

--

액세스 프로토콜
Access Protocol

전세계 모든 디지털 콘텐츠 크리에이터를 위한 새로운 수익 창출 레이어 https://accessprotocol.co