Добавить в корзинуПозвонить
Найти в Дзене
Один Rust не п...Rust

NFT на Rust

Для чего нужна данная статья? : Зачем Вам это уметь? : Пример: 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, } Пример: #[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); } } } Пример: 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
Оглавление
ML на RUST без заморочек
Один Rust не п...Rust

Для чего нужна данная статья? :

  • Создание смарт-контракта (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"]'