Для чего нужна данная статья? :
- Создание смарт-контракта (Anchor) для выпуска NFT.
- Использование Metaplex Token Metadata для управления метаданными NFT.
- Борьба с ботами через проверку кошелька владельца (whitelist).
- Роялти (creators) для выплаты комиссии создателям.
- Сложная система прав доступа (минт только для владельца).
- Интерактивное CLI-приложение на Rust для взаимодействия с контрактом.
Зачем Вам это уметь? :
1. NFT в блокчейне Solana (Anchor)
- Использование Anchor Framework (упрощает разработку программ для Solana).
- Использование библиотеки mpl-token-metadata (Metaplex) для управления метаданными NFT.
- Разработка программ на Solana с помощью solana_program и borsh для сериализации данных.
Пример:
use anchor_lang::prelude::*;
#[program]
pub mod nft_minter {
use super::*;
pub fn mint_nft(ctx: Context<MintNFT>, uri: String) -> Result<()> {
let nft = &mut ctx.accounts.nft;
nft.owner = *ctx.accounts.owner.key;
nft.uri = uri;
Ok(())
}
}
#[account]
pub struct NFT {
pub owner: Pubkey,
pub uri: String,
}
2. NFT в блокчейне Ethereum (EVM, Substrate)
2.1. Через Ink! (Substrate)
- ink! — это язык смарт-контрактов для Substrate (Polkadot).
- Можно создать контракт NFT с помощью psp34, который является аналогом ERC-721.
Пример:
#[ink::contract]
mod my_nft {
use ink::storage::Mapping;
#[ink(storage)]
pub struct MyNFT {
tokens: Mapping<u128, AccountId>,
}
impl MyNFT {
#[ink(constructor)]
pub fn new() -> Self {
Self {
tokens: Mapping::new(),
}
}
#[ink(message)]
pub fn mint(&mut self, id: u128, to: AccountId) {
self.tokens.insert(id, &to);
}
}
}
2.2. Через EVM-смарт-контракты с помощью ethers-rs
- Можно взаимодействовать с контрактами ERC-721 и ERC-1155 на Ethereum.
- Использование ethers-rs для вызова функций контрактов.
Пример:
use ethers::prelude::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let provider = Provider::<Http>::try_from("https://mainnet.infura.io/v3/YOUR_INFURA_KEY")?;
let contract = Abi::load("erc721.abi")?;
let nft = Contract::new("0x1234...5678".parse::<Address>()?, contract, provider);
let owner: Address = nft.method::<_, Address>("ownerOf", 1u64)?.call().await?;
println!("Owner of token 1: {:?}", owner);
Ok(())
}
3. NFT на NEAR (Rust-based smart contracts)
- NEAR использует Rust для разработки контрактов.
- Можно использовать near_sdk и стандарт NEP-171.
Пример:
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{near_bindgen, AccountId};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct NFT {
owner: AccountId,
metadata: String,
}
impl Default for NFT {
fn default() -> Self {
Self {
owner: "example.testnet".parse().unwrap(),
metadata: "example_uri".to_string(),
}
}
}
4. NFT на StarkNet (Cairo, но с взаимодействием через Rust)
- Хотя StarkNet использует Cairo, можно взаимодействовать с NFT-контрактами через Rust с помощью starknet-rs.
Пример:
use starknet::core::types::FieldElement;
use starknet::providers::Provider;
#[tokio::main]
async fn main() {
let provider = Provider::<Http>::try_from("https://alpha4.starknet.io")?;
let contract = Contract::new("0xNFT_CONTRACT_ADDRESS".parse::<FieldElement>()?, provider);
let owner = contract.call::<_, FieldElement>("ownerOf", (1,)).await?;
println!("NFT owner: {:?}", owner);
}
5. NFT на Aptos (Move, но с взаимодействием через Rust)
Aptos использует Move для контрактов, но Rust можно использовать для взаимодействия через aptos-sdk.
Пример:
use aptos_sdk::rest_client::Client;
#[tokio::main]
async fn main() {
let client = Client::new("https://fullnode.mainnet.aptos.com");
let nft_data = client.get_nft("0xNFT_CONTRACT_ADDRESS").await.unwrap();
println!("{:?}", nft_data);
}
Создание смарт-контракта (Anchor) для выпуска NFT
Перед началом установи Solana CLI и Anchor:
# Установить Solana CLI
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
# Установить Anchor
cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked
Создадим смарт-контракт programs/nft_minter/src/lib.rs:
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount, Mint};
use mpl_token_metadata::instruction::{create_metadata_accounts_v3, create_master_edition_v3};
use mpl_token_metadata::state::Creator;
use solana_program::{
program::invoke_signed,
system_instruction,
pubkey::Pubkey,
};
declare_id!("NFTPrgm111111111111111111111111111111111111");
#[program]
pub mod nft_minter {
use super::*;
pub fn mint_nft(ctx: Context<MintNFT>, uri: String, name: String, symbol: String) -> Result<()> {
let metadata_account = ctx.accounts.metadata.key();
let master_edition_account = ctx.accounts.master_edition.key();
let mint = ctx.accounts.mint.key();
let authority = ctx.accounts.authority.key();
let creators = vec![Creator {
address: authority,
verified: true,
share: 100, // 100% дохода создателю
}];
let metadata_ix = create_metadata_accounts_v3(
mpl_token_metadata::ID,
metadata_account,
mint,
authority,
authority,
name,
symbol,
uri,
Some(creators),
10, // 10% роялти
true,
false,
None,
None,
);
invoke_signed(
&metadata_ix,
&[
ctx.accounts.metadata.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.authority.to_account_info(),
],
&[&[b"metadata"]],
)?;
let master_edition_ix = create_master_edition_v3(
mpl_token_metadata::ID,
master_edition_account,
mint,
authority,
metadata_account,
authority,
Some(1),
);
invoke_signed(
&master_edition_ix,
&[
ctx.accounts.master_edition.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.metadata.to_account_info(),
ctx.accounts.authority.to_account_info(),
],
&[&[b"master_edition"]],
)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct MintNFT<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
mint::decimals = 0,
mint::authority = authority,
mint::freeze_authority = authority
)]
pub mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = mint,
associated_token::authority = authority
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub metadata: UncheckedAccount<'info>,
#[account(mut)]
pub master_edition: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
Соберем и задеплоим контракт:
anchor build
anchor deploy
Создадим Rust-приложение в app/src/main.rs, которое взаимодействует с контрактом.
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
system_instruction,
};
use std::fs;
fn main() {
let client = RpcClient::new("https://api.devnet.solana.com");
let payer = Keypair::new();
let program_id = "NFTPrgm111111111111111111111111111111111111".parse().unwrap();
let nft_uri = "https://example.com/metadata.json";
let nft_name = "Super NFT".to_string();
let nft_symbol = "SNFT".to_string();
let instructions = vec![system_instruction::create_account(
&payer.pubkey(),
&program_id,
1_000_000_000, // 1 SOL для NFT
0,
&program_id,
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let blockhash = client.get_latest_blockhash().unwrap();
transaction.sign(&[&payer], blockhash);
let signature = client.send_and_confirm_transaction(&transaction).unwrap();
println!("NFT Minted! Tx Signature: {:?}", signature);
}
Создадим смарт-контракт для продажи NFT (programs/nft_minter/src/lib.rs).
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount, Mint, Transfer};
use solana_program::{program::invoke, system_instruction};
declare_id!("NFTMarket11111111111111111111111111111111111");
#[program]
pub mod nft_market {
use super::*;
/// Листинг NFT на продажу
pub fn list_nft(ctx: Context<ListNFT>, price: u64) -> Result<()> {
let sale = &mut ctx.accounts.sale;
sale.owner = *ctx.accounts.owner.key;
sale.mint = *ctx.accounts.mint.key;
sale.price = price;
sale.active = true;
Ok(())
}
/// Покупка NFT
pub fn buy_nft(ctx: Context<BuyNFT>) -> Result<()> {
let sale = &mut ctx.accounts.sale;
require!(sale.active, CustomError::ListingInactive);
require!(ctx.accounts.buyer.lamports() >= sale.price, CustomError::InsufficientFunds);
// Перевод SOL продавцу
invoke(
&system_instruction::transfer(
ctx.accounts.buyer.key,
&sale.owner,
sale.price,
),
&[
ctx.accounts.buyer.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
// Перевод NFT покупателю
let cpi_accounts = Transfer {
from: ctx.accounts.seller_token_account.to_account_info(),
to: ctx.accounts.buyer_token_account.to_account_info(),
authority: ctx.accounts.owner.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
anchor_spl::token::transfer(cpi_ctx, 1)?;
sale.active = false;
Ok(())
}
}
#[derive(Accounts)]
pub struct ListNFT<'info> {
#[account(mut)]
pub owner: Signer<'info>,
pub mint: Account<'info, Mint>,
#[account(init, payer = owner, space = 8 + 32 + 32 + 8 + 1)]
pub sale: Account<'info, Sale>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct BuyNFT<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
#[account(mut)]
pub owner: AccountInfo<'info>,
#[account(mut)]
pub sale: Account<'info, Sale>,
#[account(mut)]
pub seller_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub buyer_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct Sale {
pub owner: Pubkey,
pub mint: Pubkey,
pub price: u64,
pub active: bool,
}
#[error_code]
pub enum CustomError {
#[msg("Недостаточно средств для покупки")]
InsufficientFunds,
#[msg("Этот NFT неактивен для продажи")]
ListingInactive,
}
Теперь создадим аукцион. Аукцион завершается автоматически, когда истекает время.
Открываем programs/nft_minter/src/lib.rs и добавляем код:
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
use mpl_token_metadata::instruction::{create_metadata_accounts_v3, create_master_edition_v3};
use solana_program::{program::invoke_signed, system_instruction};
declare_id!("NFTTrd111111111111111111111111111111111111");
#[program]
pub mod nft_marketplace {
use super::*;
// 1. Выставить NFT на продажу
pub fn list_nft(ctx: Context<ListNFT>, price: u64) -> Result<()> {
let sale = &mut ctx.accounts.sale;
sale.owner = *ctx.accounts.owner.key;
sale.mint = *ctx.accounts.mint.key;
sale.price = price;
sale.active = true;
Ok(())
}
// 2. Купить NFT
pub fn buy_nft(ctx: Context<BuyNFT>) -> Result<()> {
let sale = &mut ctx.accounts.sale;
require!(sale.active, CustomError::NFTNotListed);
// Перевод SOL владельцу
invoke_signed(
&system_instruction::transfer(
ctx.accounts.buyer.key,
&sale.owner,
sale.price,
),
&[
ctx.accounts.buyer.to_account_info(),
ctx.accounts.owner.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
&[],
)?;
// Передача NFT покупателю
let cpi_accounts = Transfer {
from: ctx.accounts.seller_nft_account.to_account_info(),
to: ctx.accounts.buyer_nft_account.to_account_info(),
authority: ctx.accounts.owner.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
token::transfer(CpiContext::new(cpi_program, cpi_accounts), 1)?;
sale.active = false;
Ok(())
}
// 3. Создать аукцион
pub fn start_auction(ctx: Context<StartAuction>, min_bid: u64) -> Result<()> {
let auction = &mut ctx.accounts.auction;
auction.owner = *ctx.accounts.owner.key;
auction.mint = *ctx.accounts.mint.key;
auction.min_bid = min_bid;
auction.highest_bid = 0;
auction.highest_bidder = None;
auction.active = true;
Ok(())
}
// 4. Сделать ставку
pub fn place_bid(ctx: Context<PlaceBid>, bid: u64) -> Result<()> {
let auction = &mut ctx.accounts.auction;
require!(auction.active, CustomError::AuctionNotActive);
require!(bid > auction.highest_bid, CustomError::BidTooLow);
auction.highest_bid = bid;
auction.highest_bidder = Some(*ctx.accounts.bidder.key);
Ok(())
}
// 5. Завершить аукцион
pub fn end_auction(ctx: Context<EndAuction>) -> Result<()> {
let auction = &mut ctx.accounts.auction;
require!(auction.active, CustomError::AuctionNotActive);
require!(auction.highest_bid > 0, CustomError::NoBids);
let highest_bidder = auction.highest_bidder.unwrap();
// Перевод SOL владельцу
invoke_signed(
&system_instruction::transfer(
&highest_bidder,
&auction.owner,
auction.highest_bid,
),
&[
ctx.accounts.system_program.to_account_info(),
],
&[],
)?;
// Передача NFT победителю аукциона
let cpi_accounts = Transfer {
from: ctx.accounts.owner_nft_account.to_account_info(),
to: ctx.accounts.winner_nft_account.to_account_info(),
authority: ctx.accounts.owner.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
token::transfer(CpiContext::new(cpi_program, cpi_accounts), 1)?;
auction.active = false;
Ok(())
}
}
// Состояние продажи NFT
#[account]
pub struct Sale {
pub owner: Pubkey,
pub mint: Pubkey,
pub price: u64,
pub active: bool,
}
// Состояние аукциона
#[account]
pub struct Auction {
pub owner: Pubkey,
pub mint: Pubkey,
pub min_bid: u64,
pub highest_bid: u64,
pub highest_bidder: Option<Pubkey>,
pub active: bool,
}
// Ошибки
#[error_code]
pub enum CustomError {
#[msg("NFT не выставлен на продажу")]
NFTNotListed,
#[msg("Аукцион не активен")]
AuctionNotActive,
#[msg("Ставка слишком низкая")]
BidTooLow,
#[msg("Нет ставок")]
NoBids,
}
// Контексты для функций
#[derive(Accounts)]
pub struct ListNFT<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(init, payer = owner, space = 8 + 32 + 32 + 8 + 1)]
pub sale: Account<'info, Sale>,
pub mint: Account<'info, Mint>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct BuyNFT<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
#[account(mut)]
pub owner: AccountInfo<'info>,
#[account(mut, has_one = mint)]
pub sale: Account<'info, Sale>,
#[account(mut)]
pub seller_nft_account: Account<'info, TokenAccount>,
#[account(mut)]
pub buyer_nft_account: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct StartAuction<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(init, payer = owner, space = 8 + 32 + 32 + 8 + 8 + 32 + 1)]
pub auction: Account<'info, Auction>,
pub mint: Account<'info, Mint>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct PlaceBid<'info> {
#[account(mut)]
pub bidder: Signer<'info>,
#[account(mut, has_one = mint)]
pub auction: Account<'info, Auction>,
pub mint: Account<'info, Mint>,
}
#[derive(Accounts)]
pub struct EndAuction<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(mut, has_one = mint)]
pub auction: Account<'info, Auction>,
#[account(mut)]
pub owner_nft_account: Account<'info, TokenAccount>,
#[account(mut)]
pub winner_nft_account: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
Создадим аукцион:
solana program invoke --args '["start_auction", "MIN_BID"]'
Сделаем ставку:
solana program invoke --args '["place_bid", "BID_AMOUNT"]'
Завершим аукцион:
solana program invoke --args '["end_auction"]'