Token Extensions를 활용한 동적 메타데이터 NFT

Token Extensions Program을 사용하면 메타데이터 확장을 통해 NFT와 디지털 자산을 생성할 수 있습니다. 이 확장 기능들(메타데이터 포인터와 토큰 메타데이터)을 함께 활용하면 원하는 메타데이터를 네이티브 온체인에 저장할 수 있습니다. 토큰의 mint account에 직접 구성 가능한 키-값 데이터 저장소를 두어 비용과 복잡성을 줄일 수 있습니다.

이 기능은 특히 web3 게임에 매우 유용합니다. 온체인 키-값 저장소 내에 "추가 메타데이터 필드"를 저장할 수 있어, 게임이 NFT 자체 내에 고유한 상태(예: 게임 캐릭터의 스탯이나 인벤토리)를 저장하고 접근할 수 있기 때문입니다.

온체인 프로그램 구축

이 개발자 가이드에서는 Anchor 프로그램을 사용하여 Token Extensions 기반 NFT와 커스텀 메타데이터를 구축하는 방법을 소개합니다. 이 프로그램은 게임 플레이어의 레벨과 수집한 자원을 NFT에 저장합니다.

이 NFT는 Anchor 프로그램에 의해 생성되므로 JavaScript 클라이언트에서 쉽게 민팅할 수 있습니다. 각 NFT는 토큰 메타데이터 인터페이스를 통해 제공되는 기본 구조를 갖습니다:

  • 기본 온체인 필드 - name, symbol, uri
    • uri는 NFT의 오프체인 메타데이터를 담고 있는 오프체인 JSON 파일의 링크입니다
  • 직접 정의하는 커스텀 "추가 필드"도 포함됩니다

이 모든 필드는 NFT의 mint account를 가리키는 메타데이터 확장을 사용하여 저장되며, 누구나 또는 어떤 프로그램에서도 접근할 수 있습니다.

동영상 및 소스 코드

이 예제의 동영상 설명은 Solana Foundation 유튜브 채널에서 확인할 수 있습니다:

게임 내 다른 활용 사례

커스터마이징 가능한 온체인 메타데이터를 갖춘 이러한 NFT는 게임 개발자에게 다양한 흥미로운 가능성을 열어줍니다. 특히 이 메타데이터는 온체인 프로그램에 의해 직접 상호작용하거나 관리될 수 있습니다.

게임 관련 활용 사례들은 다음과 같습니다:

  • 플레이어의 레벨과 XP 저장
  • 현재 무기와 방어구
  • 현재 퀘스트
  • 그 외에도 많습니다!

NFT 민팅

NFT를 생성하려면 다음 단계를 수행해야 합니다:

  1. mint account 생성
  2. mint account 초기화
  3. 메타데이터 포인터 계정 생성
  4. 메타데이터 포인터 계정 초기화
  5. 메타데이터 계정 생성
  6. 메타데이터 계정 초기화
  7. associated token account 생성
  8. associated token account에 토큰 민팅
  9. 민트 권한 동결

Rust 프로그램 코드

다음은 Token Extensions Program을 사용하여 NFT를 민팅하는 Rust 코드입니다:

// calculate the space need for the mint account with the desired extensions
let space = ExtensionType::try_calculate_account_len::<Mint>(
&[ExtensionType::MetadataPointer])
.unwrap();
// This is the space required for the metadata account.
// We put the metadata into the mint account at the end so we
// don't need to create and additional account.
// Then the metadata pointer points back to the mint account.
// Using this technique, only one account is needed for both the mint
// information and the metadata.
let meta_data_space = 250;
let lamports_required = (Rent::get()?).minimum_balance(space + meta_data_space);
msg!(
"Create Mint and metadata account size and cost: {} lamports: {}",
space as u64,
lamports_required
);
system_program::create_account(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
system_program::CreateAccount {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.mint.to_account_info(),
},
),
lamports_required,
space as u64,
&ctx.accounts.token_program.key(),
)?;
// Assign the mint to the token program
system_program::assign(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
system_program::Assign {
account_to_assign: ctx.accounts.mint.to_account_info(),
},
),
&token_2022::ID,
)?;
// Initialize the metadata pointer (Need to do this before initializing the mint)
let init_meta_data_pointer_ix =
spl_token_2022::extension::metadata_pointer::instruction::initialize(
&Token2022::id(),
&ctx.accounts.mint.key(),
Some(ctx.accounts.nft_authority.key()),
Some(ctx.accounts.mint.key()),
)
.unwrap();
invoke(
&init_meta_data_pointer_ix,
&[
ctx.accounts.mint.to_account_info(),
ctx.accounts.nft_authority.to_account_info()
],
)?;
// Initialize the mint cpi
let mint_cpi_ix = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token_2022::InitializeMint2 {
mint: ctx.accounts.mint.to_account_info(),
},
);
token_2022::initialize_mint2(
mint_cpi_ix,
0,
&ctx.accounts.nft_authority.key(),
None).unwrap();
// We use a PDA as a mint authority for the metadata account because
// we want to be able to update the NFT from the program.
let seeds = b"nft_authority";
let bump = ctx.bumps.nft_authority;
let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];
msg!("Init metadata {0}", ctx.accounts.nft_authority.to_account_info().key);
// Init the metadata account
let init_token_meta_data_ix =
&spl_token_metadata_interface::instruction::initialize(
&spl_token_2022::id(),
ctx.accounts.mint.key,
ctx.accounts.nft_authority.to_account_info().key,
ctx.accounts.mint.key,
ctx.accounts.nft_authority.to_account_info().key,
"Beaver".to_string(),
"BVA".to_string(),
"https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string(),
);
invoke_signed(
init_token_meta_data_ix,
&[ctx.accounts.mint.to_account_info().clone(), ctx.accounts.nft_authority.to_account_info().clone()],
signer,
)?;
// Update the metadata account with an additional metadata field in this case the player level
invoke_signed(
&spl_token_metadata_interface::instruction::update_field(
&spl_token_2022::id(),
ctx.accounts.mint.key,
ctx.accounts.nft_authority.to_account_info().key,
spl_token_metadata_interface::state::Field::Key("level".to_string()),
"1".to_string(),
),
&[
ctx.accounts.mint.to_account_info().clone(),
ctx.accounts.nft_authority.to_account_info().clone(),
],
signer
)?;
// Create the associated token account
associated_token::create(
CpiContext::new(
ctx.accounts.associated_token_program.to_account_info(),
associated_token::Create {
payer: ctx.accounts.signer.to_account_info(),
associated_token: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
},
))?;
// Mint one token to the associated token account of the player
token_2022::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token_2022::MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.nft_authority.to_account_info(),
},
signer
),
1,
)?;
// Freeze the mint authority so no more tokens can be minted to make it an NFT
token_2022::set_authority(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token_2022::SetAuthority {
current_authority: ctx.accounts.nft_authority.to_account_info(),
account_or_mint: ctx.accounts.mint.to_account_info(),
},
signer
),
AuthorityType::MintTokens,
None,
)?;

JavaScript 클라이언트 코드

클라이언트에서 NFT 민팅을 호출하는 것은 매우 간단합니다:

const nftAuthority = PublicKey.findProgramAddressSync(
[Buffer.from("nft_authority")],
program.programId
);
const mint = new Keypair();
const destinationTokenAccount = getAssociatedTokenAddressSync(
mint.publicKey,
publicKey,
false,
TOKEN_2022_PROGRAM_ID
);
const transaction = await program.methods
.mintNft()
.accounts({
signer: publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
tokenAccount: destinationTokenAccount,
mint: mint.publicKey,
rent: web3.SYSVAR_RENT_PUBKEY,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
nftAuthority: nftAuthority[0]
})
.signers([mint])
.transaction();
console.log("transaction", transaction);
const txSig = await sendTransaction(transaction, connection, {
signers: [mint],
skipPreflight: true
});
console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);

빠른 시작 예제

위 예제는 Solana Games Preset을 기반으로 하며, 이 게임을 위한 JavaScript 및 Unity 클라이언트가 포함된 스캐폴드를 생성합니다. Solana Anchor 프로그램과의 상호작용 설정도 포함되어 있습니다.

다음 명령어로 직접 실행해 볼 수 있습니다:

npx create-solana-game gameName

로컬 환경 설정

이 예제를 로컬에서 실행하려면 Anchor CLI 설치 및 구성을 포함한 Solana 개발을 위한 로컬 환경 설정이 되어 있어야 합니다. 아직 설정하지 않으셨다면 앞서 링크된 설정 가이드를 참고하여 진행하세요.

프로젝트 구조

Anchor 프로젝트는 다음과 같이 구성되어 있습니다:

진입점은 lib.rs 파일에 있습니다. 여기서 프로그램 ID와 명령어를 정의합니다. 명령어는 instructions 폴더에 정의되어 있으며, 상태는 state 폴더에 정의되어 있습니다.

호출은 lib.rs 파일로 도착한 후 instructions로 전달됩니다. instructions는 state를 호출하여 데이터를 가져오고 업데이트합니다.

NFT 민팅 명령어는 instructions 폴더에서 찾을 수 있습니다.

├── src
├── instructions
│ ├── chop_tree.rs
│ ├── init_player.rs
│ ├── mint_nft.rs
│ └── update_energy.rs
├── state
│ ├── game_data.rs
│ ├── mod.rs
│ └── player_data.rs
├── lib.rs
└── constants.rs
└── errors.rs

Anchor 프로그램

create-solana-game 도구로 생성된 Anchor 프로그램 설정을 완료하려면:

  1. cd program으로 프로그램 디렉토리로 이동
  2. anchor build를 실행하여 프로그램 빌드
  3. anchor deploy를 실행하여 프로그램 배포
  4. 터미널에서 프로그램 ID를 복사하여 lib.rs, anchor.toml, Unity 프로젝트의 AnchorService, 그리고 JavaScript를 사용하는 경우 anchor.ts 파일에 붙여넣기
  5. 다시 빌드 및 배포

NextJS 클라이언트

create-solana-game 도구로 생성된 NextJS 클라이언트 설정을 완료하려면:

  1. programIdapp/utils/anchor.ts에 복사
  2. cd app으로 앱 디렉토리로 이동
  3. yarn install을 실행하여 Node 의존성 설치
  4. yarn dev를 실행하여 클라이언트 시작
  5. Anchor 프로그램을 변경한 후에는 프로그램의 타입을 클라이언트로 복사하여 사용할 수 있도록 하세요. TypeScript 타입은 target/idl 폴더에서 찾을 수 있습니다.

로컬에서 예제 실행

Anchor의 test 명령어에 --detach 플래그를 사용하면 프로그램이 배포된 Solana 로컬 테스트 validator를 시작하고 구성합니다(테스트 완료 후에도 validator가 계속 실행됩니다):

cd program
anchor test --detach

그런 다음 Solana Explorer를 로컬 테스트 validator(anchor test 명령어 실행 시 시작됨)를 사용하도록 설정하면 트랜잭션을 확인할 수 있습니다:

https://explorer.solana.com/?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899

프로그램은 이미 net에 배포되어 있으므로 devnet에서 사용해 볼 수 있습니다. JavaScript 클라이언트에는 NFT를 민팅하는 버튼도 있습니다. JavaScript 클라이언트 시작하기:

cd app
yarn install
yarn dev

Unity 프로젝트 열기

먼저 Unity Version 2021.3.32.f1(또는 유사 버전)으로 Unity 프로젝트를 열고, GameScene 또는 LoginScene을 연 후 플레이를 누르세요. 좌측 하단의 에디터 로그인 버튼을 사용하세요.

devnet SOL을 받을 수 없는 경우, 콘솔에서 주소를 복사하여 devnet SOL을 받는 방법 가이드의 안내를 따르세요.

Unity에서 Solana 테스트 validator에 연결

devnet SOL 유지 관리에 대한 부담을 피하려면 Unity 내에서 실행 중인 로컬 테스트 validator에 연결할 수 있습니다. 지갑 홀더 게임 오브젝트에 다음 링크를 추가하세요:

http://localhost:8899
ws://localhost:8900

JavaScript 클라이언트 실행

웹 브라우저를 통해 게임 및 프로그램과 상호작용할 수 있도록 JavaScript 클라이언트를 시작하려면:

  • 저장소 내 app 디렉토리 열기
  • Node 의존성 설치
  • dev 명령어를 실행하여 개발 서버 시작
cd app
yarn install
yarn dev

프로그램을 변경하고 자체 프로그램에 연결하려면 아래 단계를 따르세요.

Is this page helpful?