Although NFTs are mostly known to record image ownership, they can be used for many cases:
- Tickets (shows, parking, airplane, hotel reservation...)
- Access control
- Identity Verification (including birth certificates and KYC)
- Documents (including certificates and licenses)
- Academic and Professional Credentials
- Attendance/Participation Credentials
- Awards
- Medical Records
- Voting and DAOs
- Intellectual Property and Patents
- Engineering/Architectural design (including usage rights)
- Product Authenticity and Supply Chain (food, pharmaceuticals, fashion...)
- Real-estate property (including rent)
- Ownership tracking
- History tracking (eg. car maintainance services)
- Insurance
- Record shares of resources
- Gaming
- Sports
When creating a token, the token issuer can store data in 2 places:
- Token Id
- Metadata
The Token Id is a string of up to 128 bytes. It must be unique for each token.
Whenever a new token is created the contract will check if another token with the same id already exists.
The data stored in the token id is immutable.
The Metadata is the data stored attached to the token id, in the format of key-value pairs.
The token issuer can set the metadata either at mint time or later, using the set_metadata
function.
The metadata can be either mutable, immutable or incremental.
The token creator can mark a specific metadata key as immutable using the
make_metadata_immutable
function. This is done just once for each unique key.
After this property is set for a metadata key, it cannot be removed.
If the metadata is immutable, it means that once it is set on a token it can no longer be modified.
It gives the guarantee to the owner that the creator/issuer will not modify or remove this specific metadata.
The token creator can mark a specific metadata key as incremental using the
make_metadata_incremental
function. This is done just once for each unique key.
After this property is set for a metadata key, it cannot be removed.
If the metadata is incremental, it means that once it is set on a token it can only be incremented.
It gives the guarantee to the owner that the creator/issuer can only increment this value. Useful for expiration time, for example.
Any metadata key that is not marked as immutable or incremental is mutable.
The token issuer can modify the metadata by either updating the value or removing the whole key-value pair from the token.
This is done using the set_metadata
and remove_metadata
functions.
Mutable data must be stored as metadata.
Immutable data can be in either places. Store it in the token id if the data is used to uniquely identify the token. Otherwise it can be stored as metadata.
Example:
For tickets we can store the event name, location, date, and seat number in the token id. By using the same format of storing this data we can check if the ticket is already issued, retrieve the owner, etc.
The data in the token id can be arranged in a key-value format, like this:
event=Josh_Concert,date=1645379800,place=Clark_Stadium,section=green,seat=15D,team=away
Or as a comma separated list, like this:
Josh_Concert,1645379800,Clark_Stadium,green,D15,away
Or whatever format suits your needs, just remembering that it must be a string with up to 128 bytes.
You can also store version info in the beginning, just in case you change the format later.
It is also common to store just an incremental number as the token id, and additional data either in the metadata or in external storage.
Non-fungible token contracts are not meant to be used to store lots of data for each token. Its main use it to store some key information on the token and, if there is more data related to it, store them externally. Either in normal databases or in storage-dedicated blockchains, like IPFS, Filecoin, Storj or Bittorrent.
For artwork, we recommend to store the image/video/music on a storage-dedicated blockchain and then store the identifier in the token id.
For real-state, you can store the property identifier on the token and the remaining, like documents, on a descentralized storage or private database.
Similar for Academic Credentials, Documents, Patents, Designs...
The tokens can be created at contract creation time and also later, if the
contract includes the mintable
extension.
The tokens created at the contract creation are named Initial Supply
They can be informed as a list of token ids:
local initial_supply = {
"Josh_Concert,1645379800,Clark_Stadium,D01",
"Josh_Concert,1645379800,Clark_Stadium,D02",
"Josh_Concert,1645379800,Clark_Stadium,D03"
}
Or as a list of token ids and metadata:
local initial_supply = {
{ "Josh_Concert,1645379800,Clark_Stadium,D01", {key1 = value1, key2 = value2} },
{ "Josh_Concert,1645379800,Clark_Stadium,D02", {key1 = value1, key2 = value2} },
{ "Josh_Concert,1645379800,Clark_Stadium,D03", {key1 = value1, key2 = value2} },
}
To use this feature the contract must include the mintable
extension.
The contract creator can mint new tokens using the mint
function, informing
the token owner, the token id and metadata.
Example call from a contract:
contract.call(token, "mint", to, tokenId, {key = value, key2 = value2})
Example transaction:
"type": CALL,
"payload": {
"function": "mint",
"args": [to, tokenId, {"key": value, "key2": value2}]
}
It is also possible to allow other accounts to mint tokens, using the addMinter
function. For more info, check the mintable extension.
It is possible to set a limit on the number of tokens that a contract can contain.
This is done at contract creation time, informing the maximum amount
in the options, as max_supply
.
Here is an example with a max supply of 1000:
local token = contract.call(arc2_factory, "new_arc2_nft", name, symbol,
initial_supply, {mintable=true,max_supply=1000})
When a token is non-transferable, it cannot be transferred to other users.
It is useful for identity related badges, like KYC, certificates, credentials, lisenses, awards...
There are 2 ways in which a token can be non-transferable:
- If the token contract has the
non_transferable
extension - If the specific token has the
non_transferable
metadata
On the first case, all tokens on the contract are non-transferable.
On the second case, the contract can have both transferable and non-transferable tokens.
The non_transferable
metadata can only be set at token creation/mint time, but it can
be removed later.
When a token is recallable, it can be transferred (seized) by the token creator.
There are 2 ways in which a token can be recallable:
- If the token contract has the
recallable
extension - If the specific token has the
recallable
metadata
On the first case, all tokens on the contract are recallable.
On the second case, the contract can have both recallable and non-recallable tokens.
The recallable
metadata can only be set at token creation/mint time, but it can
be removed later.
Non-Transferable | Recallable | Behavior |
---|---|---|
Only the owner (and authorized accounts) can transfer | ||
✅ | No one can transfer it | |
✅ | The owner, authorized, and creator/issuer can transfer | |
✅ | ✅ | Only the creator/issuer can transfer |
This defines the interface and behaviors for aergo non-fungible token contract.
The following is an interface contract declaring the required functions to meet the ARC2 standard.
-- Get the token name
-- @type query
-- @return (string) name of this token
function name() end
-- Get the token symbol
-- @type query
-- @return (string) symbol of this token
function symbol() end
-- Count of all NFTs
-- @type query
-- @return (integer) the number of non-fungible tokens on this contract
function totalSupply() end
-- Count of all NFTs assigned to an owner
-- @type query
-- @param owner (address) a target address
-- @return (integer) the number of NFT tokens of owner
function balanceOf(owner) end
-- Find the owner of an NFT
-- @type query
-- @param tokenId (str128) the NFT id
-- @return (address) the address of the owner of the NFT
function ownerOf(tokenId) end
-- Transfer a token
-- @type call
-- @param to (address) the receiver address
-- @param tokenId (str128) the NFT token to send
-- @param ... (Optional) additional data, is sent unaltered in a call to 'nonFungibleReceived' on 'to'
-- @return value returned from the 'nonFungibleReceived' callback, or nil
-- @event transfer(from, to, tokenId)
function transfer(to, tokenId, ...) end
-- Returns a JSON string containing the list of ARC2 extensions
-- that were included on the contract.
-- @type query
function arc2_extensions() end
These additional functions below are also present in the core module
The enumerable
functions:
-- Retrieves the next token in the contract
-- @type query
-- @param prev_index (integer) the index of the previous returned token. use `0` in the first call
-- @param max_items (integer) the maximum number of items to return
-- @return (index, tokenId) the index of the next token and its token id, or `nil,nil` if no more tokens
function nextToken(prev_index, max_items)
-- Retrieves the token from the given user at the given position
-- @type query
-- @param user (address) ..
-- @param position (integer) the position of the token in the incremental sequence
-- @param max_items (integer) the maximum number of items to return
-- @return tokenId (str128) the token id, or `nil` if no more tokens on this account
function tokenFromUser(user, position, max_items)
-- Store non-fungible token metadata
-- @type call
-- @param tokenId (str128) the non-fungible token id
-- @param metadata (table) lua table containing key-value pairs
function set_metadata(tokenId, metadata)
-- Remove non-fungible token metadata
-- @type call
-- @param tokenId (str128) the non-fungible token id
-- @param list (table) lua table containing list of keys to remove
function remove_metadata(tokenId, list)
-- Retrieve non-fungible token metadata
-- @type query
-- @param tokenId (str128) the non-fungible token id
-- @param key (string) the metadata key
-- @return (string) if key is nil, return all metadata from token,
-- otherwise return the value linked to the key
function get_metadata(tokenId, key)
-- Mark a specific metadata key as immutable. This means that once this metadata
-- is set on a token, it can no longer be modified. And once this property is set
-- on a metadata, it cannot be removed. It gives the guarantee to the owner that
-- the creator/issuer will not modify or remove this specific metadata.
-- @type call
-- @param key (string) the metadata key
function make_metadata_immutable(key)
-- Mark a specific metadata key as incremental. This means that once this metadata
-- is set on a token, it can only be incremented. Useful for expiration time.
-- Once this property is set on a metadata, it cannot be removed. It gives the
-- guarantee to the owner that the creator/issuer can only increment this value.
-- @type call
-- @param key (string) the metadata key
function make_metadata_incremental(key)
-- Retrieve the list of immutable and incremental metadata
-- @type query
-- @return (string) a JSON object with each metadata as key and the property
-- as value. Example: {"expiration": "incremental", "index": "immutable"}
function get_metadata_info()
-- Indicate if an account is a minter
-- @type query
-- @param account (address)
-- @return (bool) true/false
function isMinter(account)
-- Add an account to the minters group
-- @type call
-- @param account (address)
-- @event addMinter(account)
function addMinter(account)
-- Remove an account from the minters group
-- @type call
-- @param account (address)
-- @event removeMinter(account)
function removeMinter(account)
-- Renounce the minter role
-- @type call
-- @event removeMinter(caller)
function renounceMinter()
-- Mint a new non-fungible token
-- @type call
-- @param to (address) recipient's address
-- @param tokenId (str128) the non-fungible token id
-- @param metadata (table) lua table containing key-value pairs
-- @param ... additional data, is sent unaltered in a call to 'nonFungibleReceived' on 'to'
-- @return value returned from the 'nonFungibleReceived' callback, or nil
-- @event mint(to, tokenId)
function mint(to, tokenId, metadata, ...)
-- Retrieve the Max Supply
-- @type query
-- @return amount (integer) the maximum amount of tokens that can be active on the contract
function maxSupply()
-- Burn a non-fungible token
-- @type call
-- @param tokenId (str128) the identifier of the token to be burned
-- @event burn(owner, tokenId)
function burn(tokenId) end
-- Approve an account to operate on the given non-fungible token.
-- Use `nil` on the operator to remove the approval
-- @type call
-- @param operator (address) the new approved NFT controller
-- @param tokenId (str128) the NFT token to be controlled
-- @event approve(owner, operator, tokenId)
function approve(operator, tokenId) end
-- Get the approved operator address for a given non-fungible token
-- @type query
-- @param tokenId (str128) the NFT token to find the approved operator
-- @return (address) the approved operator address for this NFT, or nil if there is none
function getApproved(tokenId) end
-- Allow an operator to control all the sender's tokens
-- @type call
-- @param operator (address) the operator address
-- @param approved (boolean) true if the operator is approved, false to revoke approval
-- @event approvalForAll(owner, operator, approved)
function setApprovalForAll(operator, approved) end
-- Check if the given operator is approved to control the owner's tokens
-- @type query
-- @param owner (address) owner address
-- @param operator (address) operator address
-- @return (bool) true/false
function isApprovedForAll(owner, operator) end
-- Transfer a non-fungible token of 'from' to 'to'
-- @type call
-- @param from (address) the owner address
-- @param to (address) the receiver address
-- @param tokenId (str128) the non-fungible token to send
-- @param ... (Optional) additional data, is sent unaltered in a call to 'nonFungibleReceived' on 'to'
-- @return value returned from the 'nonFungibleReceived' callback, or nil
-- @event transfer(from, to, tokenId, operator)
function transferFrom(from, to, tokenId, ...) end
-- Indicate whether an account has the pauser role
-- @type query
-- @param account (address)
-- @return (bool) true/false
function isPauser(account)
-- Grant the pauser role to an account
-- @type call
-- @param account (address)
-- @event addPauser(account)
function addPauser(account)
-- Remove the pauser role form an account
-- @type call
-- @param account (address)
-- @event removePauser(account)
function removePauser(account)
-- Renounce the granted pauser role
-- @type call
-- @event removePauser(account)
function renouncePauser()
-- Indicate if the contract is paused
-- @type query
-- @return (bool) true/false
function paused()
-- Put the contract in a paused state
-- @type call
-- @event pause(caller)
function pause()
-- Return the contract to the normal state
-- @type call
-- @event unpause(caller)
function unpause()
-- Add accounts to the blacklist
-- @type call
-- @param account_list (list of address)
-- @event addToBlacklist(account_list)
function addToBlacklist(account_list)
-- Remove accounts from the blacklist
-- @type call
-- @param account_list (list of address)
-- @event removeFromBlacklist(account_list)
function removeFromBlacklist(account_list)
-- Indicate if an account is on the blacklist
-- @type query
-- @param account (address)
function isOnBlacklist(account)
There are no exported functions
It is used to make non-transferable tokens (badges)
There are no exported functions
It is used to make recallable tokens
It is used to search for tokens using REGEX pattern.
-- Find the next token that matches the given query
-- @type query
-- @param query (table) lua table containing parameters used by the search
-- @param prev_index (integer) the index of the previous returned token. use `0` in the first call
-- @return (index, tokenId) the index of the next found token and its token id
function findToken(query, prev_index)
Check instructions bellow for usage and examples
This function is required to receive ARC2 non-fungible tokens
Contracts that want to receive non-fungible tokens must implement the following function in order to receive them. In most cases it also means that your contract must have methods to transfer these tokens.
If your contract is not intended to handle non-fungible tokens, then do not add this function. In this case any attempt to transfer a NFT to your contract will fail (as expected).
This function can also be used to define how to handle the tokens they receive.
-- The ARC2 smart contract calls this function on the recipient contract after a 'transfer' or 'mint'
-- @type call
-- @param operator (address) the address that called the token 'transfer' or 'mint'
-- @param from (address) the sender's address (nil if being minted)
-- @param tokenId (str128) the non-fungible token id
-- @param ... additional data, by-passed from 'transfer' or 'mint' arguments
function nonFungibleReceived(operator, from, tokenId, ...)
-- do nothing
end
⚠️ ATTENTION: anyone can call this function!⚠️ Do NOT assume a token was received just because this function was called!
If a token was really sent, we can get the token contract address with:
local token_contract = system.getSender()
But on most cases the function can be empty.
To list tokens we use the nextToken
function.
function nextToken(prev_index)
To list tokens from an user we use the tokenFromUser
function.
function tokenFromUser(user, position)
To find tokens with specific properties we use the findToken
function.
function findToken(query, prev_index)
It retrieves the first token found that matches the query. The query is a lua table that can contain these fields:
- owner - the owner of the token (address)
- contains - check if the tokenId contains this string
- pattern - check if the tokenId matches this Lua regex pattern
- metadata - check if the token metadata matches this sub-query
The sub-query is a lua table containing the metadata key, the operator to use and the value, in this format:
metadata = { key = {op = ">", value = 10} }
The operators are: = > >= < <= between match
and negated: != !between !match
The prev_index
must be 0
in the first call.
For the next calls, just inform the returned index from the previous call.
The returned value is index, tokenId
If no token is found with the given query, it returns nil, nil
List all tokens on the contract:
local index = 0
local tokenId
do
index, tokenId = nextToken(index)
if tokenId then
...
end
while index > 0
List the tokens from an user:
local position = 1
while true do
local tokenId = tokenFromUser(owner, position)
if tokenId == nil then break end
...
position = position + 1
end
List the tokens that contain a specific text:
local index = 0
local tokenId
do
index, tokenId = findToken({contains=text}, index)
if tokenId then
...
end
while index > 0
List the tokens that match a specific regex pattern:
local index = 0
local tokenId
do
index, tokenId = findToken({pattern=text}, index)
if tokenId then
...
end
while index > 0
List the tokens from an user that match a specific regex pattern:
local index = 0
local tokenId
do
index, tokenId = findToken({owner=address,pattern=text}, index)
if tokenId then
...
end
while index > 0
List the tokens from an user that have not expired yet:
local query = {owner=address, metadata={expiration = {op=">", value=1645379800}}}
local index = 0
local tokenId
do
index, tokenId = findToken(query, index)
if tokenId then
...
end
while index > 0