跳到主要内容

Gear 非同质化代币

介绍

非同质化代币(NFT)是区块链上唯一的加密代币,用于证明数字资产的所有权,如数字艺术或游戏资产。与同质化代币的区别在于,同质化代币存储的是一个值,而非同质化代币存储的是一个加密证书。在底层,一个非同质化代币由一个唯一的代币标识符或代币 ID 组成,它被映射到一个所有者标识符,并存储在一个 NFT 智能合约中。

token_idaddress

当一个给定代币 ID 的所有者希望将其转让给另一个用户时,很容易验证所有权并将代币重新分配给新的所有者。

非同质化代币实现

每个非同质化代币合约必须支持的功能:

  • transfer(to, token_id) 是一个函数,允许你将带有 token_id 的代币转移到 to 帐户;

  • approve(approved_account, token_id) - 是一个函数,允许你将处置代币的权利交给指定的approved_account。这个功能在市场或拍卖会上很有用,因为当所有者想出售他的代币时,他们可以把它放在市场/拍卖会上,所以合约将能够在某个时候把这个代币发送给新的所有者。

  • mint(to, token_id, metadata) - 是一个创建新代币的函数。元数据可以包括关于代币的任何信息:它可以是一个指向特定资源的链接,也可以是对代币的描述,等等。

  • burn(from, token_id) - 是一个函数,用于从合约中删除带有 token_id 的代币。

NFT 合约的实现为gear-contract-libraries/non_fungible_token

要使用默认实现需要在 Cargo.toml 配置:

gear-lib = { git = "https://github.com/gear-dapps/gear-lib.git" }
gear-lib-derive = { git = "https://github.com/gear-dapps/gear-lib.git" }

non-fungible 合约的存储状态在结构 NFTState 中定义:

#[derive(Debug, Default)]
pub struct NFTState {
pub name: String,
pub symbol: String,
pub base_uri: String,
pub owner_by_id: BTreeMap<TokenId, ActorId>,
pub token_approvals: BTreeMap<TokenId, Vec<ActorId>>,
pub token_metadata_by_id: BTreeMap<TokenId, Option<TokenMetadata>>,
pub tokens_for_owner: BTreeMap<ActorId, Vec<TokenId>>,
pub royalties: Option<Royalties>,
}

要重复使用默认结构,你需要派生出 NFTStateKeeper,并用 #[NFTStateField] 属性标记相应的字段。你也可以在 NF 合约中添加字段。例如,在合约中添加所有者的地址和token_id,它将跟踪当前的代币数量。

use derive_traits::{NFTStateKeeper, NFTCore, NFTMetaState};
use gear_contract_libraries::non_fungible_token::{nft_core::*, state::*, token::*};

#[derive(Debug, Default, NFTStateKeeper, NFTCore, NFTMetaState)]
pub struct NFT {
#[NFTStateField]
pub token: NFTState,
pub token_id: TokenId,
pub owner: ActorId,
}

为了继承默认的逻辑功能,你需要派生出 NFTCore 。相应地,为了读取合约状态,你需要 NFTMetaState

让我们来写一下 NFT 合约的整体实现。首先,我们定义消息,它将初始化合约和合约要处理的消息。

#[derive(Debug, Encode, Decode, TypeInfo)]
pub struct InitNFT {
pub name: String,
pub symbol: String,
pub base_uri: String,
}

pub enum NFTAction {
Mint {
to: ActorId,
token_id: TokenId,
},
Burn {
token_id: TokenId,
},
Transfer {
to: ActorId,
token_id: TokenId,
},
Approve {
to: ActorId,
token_id: TokenId,
},
}

NFT 合约实现:

#[derive(Debug, Default, NFTStateKeeper, NFTCore, NFTMetaState)]
pub struct NFT {
#[NFTStateField]
pub token: NFTState,
pub token_id: TokenId,
pub owner: ActorId,
}

static mut CONTRACT: Option<NFT> = None;

#[no_mangle]
pub unsafe extern "C" fn init() {
let config: InitNFT = msg::load().expect("Unable to decode InitNFT");
let mut nft = NFT::default();
nft.token.name = config.name;
nft.token.symbol = config.symbol;
nft.token.base_uri = config.base_uri;
nft.owner = msg::source();
CONTRACT = Some(nft);
}

#[no_mangle]
pub unsafe extern "C" fn handle() {
let action: NFTAction = msg::load().expect("Could not load msg");
let nft = CONTRACT.get_or_insert(NFT::default());
match action {
NFTAction::Mint { to, token_id } => NFTCore::mint(&to, token_id, None),
NFTAction::Burn { token_id } => NFTCore::burn(nft, token_id),
NFTAction::Transfer { to, token_id } => NFTCore::transfer(nft, &to, token_id),
NFTAction::Approve { to, token_id } => NFTCore::approve(nft, &to, token_id),
}
}

制定你的非同质化代币合约

接下来,让我们重写一下 mint函数的实现。mint 函数会为发送 Mint 消息的账户创建代币,并将元数据作为输入参数。

pub enum NFTAction {
Mint {
token_metadata: TokenMetadata,
token_id: TokenId,
},

TokenMetadata 同样定义在 gear NFT library:

#[derive(Debug, Default, Encode, Decode, Clone, TypeInfo)]
pub struct TokenMetadata {
// ex. "CryptoKitty #100"
pub name: String,
// free-form description
pub description: String,
// URL to associated media, preferably to decentralized, content-addressed storage
pub media: String,
// URL to an off-chain JSON file with more info.
pub reference: String,
}

为我们的新函数定义一个 trait,它将扩展默认的NFTCore

pub trait MyNFTCore: NFTCore {
fn mint(&mut self, token_metadata: TokenMetadata);
}

并编写该 trait 的实现:

impl MyNFTCore for NFT {
fn mint(&mut self, token_metadata: TokenMetadata) {
NFTCore::mint(self, &msg::source(), self.token_id, Some(token_metadata));
self.token_id = self.token_id.saturating_add(U256::one());
}
}

因此,有必要对 handle 函数进行修改。

#[no_mangle]
pub unsafe extern "C" fn handle() {
let action: NFTAction = msg::load().expect("Could not load msg");
let nft = CONTRACT.get_or_insert(NFT::default());
match action {
NFTAction::Mint { token_metadata } => MyNFTCore::mint(token_metadata),
NFTAction::Burn { token_id } => NFTCore::burn(nft, token_id),
NFTAction::Transfer { to, token_id } => NFTCore::transfer(nft, &to, token_id),
NFTAction::Approve { to, token_id } => NFTCore::approve(nft, &to, token_id),
}
}

总结

Gear 提供了一个可重复使用的,具有 gNFT 协议的核心功能。通过使用对象组合,该库可以在自定义的 NFT 合约实现中使用,以减少重复代码。

Gear 提供的合约实例的源代码可在 GitHub 上找到 nft-example/src

也请看基于 gtest 的智能合约测试实现的例子:nft-example/tests

关于测试在 Gear 上编写的智能合约的更多细节,请参考这篇文章 程序测试