Gear Non-Fungible Token
Non-fungible tokens (NFTs) are unique cryptographic tokens on a blockchain used to prove ownership of digital assets, such as digital art or gaming assets. The difference from fungible tokens is that fungible tokens store value, while non-fungible tokens store a cryptographic certificate.
Under the hood, a non-fungible token consists of a unique token identifier, or token ID, which is mapped to an owner identifier and stored inside an NFT smart contract.
When the owner of a given token ID wishes to transfer it to another user, it is easy to verify ownership and reassign the token to a new owner.
This article explains the programming interface, data structure, basic functions, and their purposes. It can be used as-is or modified to suit your scenarios. Anyone can easily create their own application and run it on the Gear-powered network.
How to run
⚒️ Build program
- Get the source code of NFT contract
- Build contracts as described in program/README.md.
🏗️ Upload program
- You can deploy a program using idea.gear-tech.io.
- In the network selector choose
Staging Testnet
orDevelopment
(in this case, you should have a local node running) - Upload program
nft.opt.wasm
from/target/wasm32-unknown-unknown/release/
- Upload metadata file
meta.txt
- Specify
init payload
and calculate gas!
Non-fungible-token implementation
The functions that must be supported by each non-fungible-token contract:
- transfer(to, token_id) - is a function that allows you to transfer a token with the
token_id
number to theto
account; - approve(to, token_id) - is a function that allows you to give the right to dispose of the token to the specified account
to
. This functionality can be useful on marketplaces or auctions as when the owner wants to sell his token, they can put it on a marketplace/auction, so the contract will be able to send this token to the new owner at some point; - mint(to, metadata) is a function that creates a new token to the
to
account.metadata
can include any information about the token: it can be a link to a specific resource, a description of the token, etc; - burn(token_id) is a function that removes the token with the mentioned
token_id
from the contract.
The non-fungible-token contract contains the following information:
pub struct Nft {
pub owner_by_id: HashMap<TokenId, ActorId>,
pub token_approvals: HashMap<TokenId, ActorId>,
pub token_metadata_by_id: HashMap<TokenId, TokenMetadata>,
pub tokens_for_owner: HashMap<ActorId, HashSet<TokenId>>,
pub token_id: TokenId,
pub owner: ActorId,
pub collection: Collection,
pub config: Config,
}
owner_by_id
- token and owner id pairtoken_approvals
- token id pair and approved ownerstoken_metadata_by_id
- a pair of token id and token metadatatokens_for_owner
- a pair of owner id and the id of all its tokenstoken_id
- current token idowner
- collection ownercollection
- information about this collectionconfig
- configuration of collection
Where TokenMetadata
, Collection
and Config
contains the following information:
pub struct TokenMetadata {
pub name: String,
pub description: String,
pub media: String,
pub reference: String,
}
pub struct Collection {
pub name: String,
pub description: String,
}
pub struct Config {
pub max_mint_count: Option<u128>,
}
Initialization
To initialize a contract, it needs to be passed Config
and Collection
structures
pub struct InitNft {
pub collection: Collection,
pub config: Config,
}
Action
pub enum NftAction {
Mint {
to: ActorId,
token_metadata: TokenMetadata,
},
Burn {
token_id: TokenId,
},
Transfer {
to: ActorId,
token_id: TokenId,
},
Approve {
to: ActorId,
token_id: TokenId,
},
GetOwner {
token_id: TokenId,
},
CheckIfApproved {
to: ActorId,
token_id: TokenId,
},
}
Event
pub enum NftEvent {
Minted {
to: ActorId,
token_metadata: TokenMetadata,
},
Burnt {
token_id: TokenId,
},
Transferred {
from: ActorId,
to: ActorId,
token_id: TokenId,
},
Approved {
owner: ActorId,
approved_account: ActorId,
token_id: TokenId,
},
Owner {
owner: ActorId,
token_id: TokenId,
},
CheckIfApproved {
to: ActorId,
token_id: TokenId,
approved: bool,
},
}
Contract implementation
#[no_mangle]
extern fn handle() {
let action: NftAction = msg::load().expect("Could not load NftAction");
let nft = unsafe { NFT.as_mut().expect("`NFT` is not initialized.") };
let result = match action {
NftAction::Mint { to, token_metadata } => nft.mint(&to, token_metadata),
NftAction::Burn { token_id } => nft.burn(token_id),
NftAction::Transfer { to, token_id } => nft.transfer(&to, token_id),
NftAction::Approve { to, token_id } => nft.approve(&to, token_id),
NftAction::GetOwner { token_id } => nft.owner(token_id),
NftAction::CheckIfApproved { to, token_id } => nft.is_approved_to(&to, token_id),
};
msg::reply(result, 0).expect("Failed to encode or reply with `NftEvent`.");
}
impl Nft {
/// Mint a new nft using `TokenMetadata`
fn mint(&mut self, to: &ActorId, token_metadata: TokenMetadata) -> NftEvent {
self.check_config();
self.check_zero_address(to);
self.owner_by_id.insert(self.token_id, *to);
self.tokens_for_owner
.entry(*to)
.and_modify(|tokens| {
tokens.insert(self.token_id);
})
.or_insert_with(|| HashSet::from([self.token_id]));
self.token_metadata_by_id
.insert(self.token_id, token_metadata.clone());
self.token_id += 1;
NftEvent::Minted {
to: *to,
token_metadata,
}
}
/// Burn nft by `TokenId`
fn burn(&mut self, token_id: TokenId) -> NftEvent {
let owner = *self
.owner_by_id
.get(&token_id)
.expect("NonFungibleToken: token does not exist");
self.check_owner(&owner);
self.owner_by_id.remove(&token_id);
self.token_metadata_by_id.remove(&token_id);
if let Some(tokens) = self.tokens_for_owner.get_mut(&owner) {
tokens.remove(&token_id);
if tokens.is_empty() {
self.tokens_for_owner.remove(&owner);
}
}
self.token_approvals.remove(&token_id);
NftEvent::Burnt { token_id }
}
/// Transfer token from `token_id` to address `to`
fn transfer(&mut self, to: &ActorId, token_id: TokenId) -> NftEvent {
let owner = *self
.owner_by_id
.get(&token_id)
.expect("NonFungibleToken: token does not exist");
self.can_transfer(token_id, &owner);
self.check_zero_address(to);
// assign new owner
self.owner_by_id
.entry(token_id)
.and_modify(|owner| *owner = *to);
// push token to new owner
self.tokens_for_owner
.entry(*to)
.and_modify(|tokens| {
tokens.insert(token_id);
})
.or_insert_with(|| HashSet::from([token_id]));
// remove token from old owner
if let Some(tokens) = self.tokens_for_owner.get_mut(&owner) {
tokens.remove(&token_id);
if tokens.is_empty() {
self.tokens_for_owner.remove(&owner);
}
}
// remove approvals if any
self.token_approvals.remove(&token_id);
NftEvent::Transferred {
from: owner,
to: *to,
token_id,
}
}
/// Approve token from `token_id` to address `to`
fn approve(&mut self, to: &ActorId, token_id: TokenId) -> NftEvent {
let owner = self
.owner_by_id
.get(&token_id)
.expect("NonFungibleToken: token does not exist");
self.check_owner(owner);
self.check_zero_address(to);
self.check_approve(&token_id);
self.token_approvals.insert(token_id, *to);
NftEvent::Approved {
owner: *owner,
approved_account: *to,
token_id,
}
}
/// Get `ActorId` of the nft owner with `token_id`
fn owner(&self, token_id: TokenId) -> NftEvent {
let owner = self
.owner_by_id
.get(&token_id)
.expect("NonFungibleToken: token does not exist");
NftEvent::Owner {
owner: *owner,
token_id,
}
}
/// Get confirmation about approval to address `to` and `token_id`
fn is_approved_to(&self, to: &ActorId, token_id: TokenId) -> NftEvent {
if !self.owner_by_id.contains_key(&token_id) {
panic!("Token does not exist")
}
self.token_approvals.get(&token_id).map_or_else(
|| NftEvent::IsApproved {
to: *to,
token_id,
approved: false,
},
|approval_id| NftEvent::IsApproved {
to: *to,
token_id,
approved: *approval_id == *to,
},
)
}
//...
}
Program metadata and state
Metadata interface description:
pub struct NftMetadata;
impl Metadata for NftMetadata {
type Init = In<InitNft>;
type Handle = InOut<NftAction, NftEvent>;
type Reply = ();
type Others = ();
type Signal = ();
type State = InOut<StateQuery, StateReply>;
}
It is possible to get a partial state:
pub enum StateQuery {
All,
Config,
Collection,
Owner,
CurrentTokenId,
OwnerById { token_id: TokenId },
TokenApprovals { token_id: TokenId },
TokenMetadata { token_id: TokenId },
OwnerTokens { owner: ActorId },
}
pub enum StateReply {
All(State),
Config(Config),
Collection(Collection),
Owner(ActorId),
CurrentTokenId(TokenId),
OwnerById(Option<ActorId>),
TokenApprovals(Option<ActorId>),
TokenMetadata(Option<TokenMetadata>),
OwnerTokens(Option<Vec<TokenId>>),
}
To display the contract state information, the state()
function is used:
#[no_mangle]
extern fn state() {
let nft = unsafe { NFT.take().expect("Unexpected error in taking state") };
let query: StateQuery = msg::load().expect("Unable to load the state query");
match query {
StateQuery::All => {
msg::reply(StateReply::All(nft.into()), 0).expect("Unable to share the state");
}
StateQuery::Config => {
msg::reply(StateReply::Config(nft.config), 0).expect("Unable to share the state");
}
StateQuery::Collection => {
msg::reply(StateReply::Collection(nft.collection), 0)
.expect("Unable to share the state");
}
StateQuery::Owner => {
msg::reply(StateReply::Owner(nft.owner), 0).expect("Unable to share the state");
}
StateQuery::CurrentTokenId => {
msg::reply(StateReply::CurrentTokenId(nft.token_id), 0)
.expect("Unable to share the state");
}
StateQuery::OwnerById { token_id } => {
msg::reply(
StateReply::OwnerById(nft.owner_by_id.get(&token_id).cloned()),
0,
)
.expect("Unable to share the state");
}
StateQuery::TokenApprovals { token_id } => {
let approval = nft.token_approvals.get(&token_id).cloned();
msg::reply(StateReply::TokenApprovals(approval), 0).expect("Unable to share the state");
}
StateQuery::TokenMetadata { token_id } => {
msg::reply(
StateReply::TokenMetadata(nft.token_metadata_by_id.get(&token_id).cloned()),
0,
)
.expect("Unable to share the state");
}
StateQuery::OwnerTokens { owner } => {
let tokens = nft
.tokens_for_owner
.get(&owner)
.map(|hashset| hashset.iter().cloned().collect());
msg::reply(StateReply::OwnerTokens(tokens), 0).expect("Unable to share the state");
}
}
}
Conclusion
An NFT smart contract source code is available on Github.
See also an example of the smart contract testing implementation based on gtest
and gclient
: gear-foundation/dapps/contracts/nft/tests.
For more details about testing smart contracts written on Gear, refer to this article: Program Testing.