How to implement XRC721 in XDC Network?

Dolly Bhati
Yodaplus
Published in
7 min readOct 8, 2021
Non-Fungible Token implementation in XDC Network

Have you ever thought that the images that you click on your phone can actually be sold using Blockchain?

Yeah, you heard it right.

Of course, I am talking about the booming NFTs, with sales volume hitting $2.5 billion in the first half of 2021. It has seen an upsurge from a total of $13.7 million in 2020.

Huge one!

Let me brief you a little bit about NFTs. They are mostly used to represent easily reproducible items like photos, videos, audio, and other types of digital files. Typically, an NFT is a unique and interchangeable unit of data stored on a digital ledger (blockchain). Blockchain technology is used to establish an authentic public proof of ownership.

Well, it has opened an avenue for digital artists, collectors, and other asset holders to digitize their holdings and trade on the same.

An Example can be as an artist, you have real paintings, and which you want to sell in the digital marketplace. So in your ‘Blue period,’ you have produced seven paintings. So in the digital marketplace, you bring them as seven unique tokens, with a provenance that you own it, and it goes for auction wherein all bidders are assured of the provenance through an XRC721 Smart Contract.

Let’s dig a little deeper into the specifications.

What are XRC721 specifications?

XRC721 specification allows your smart contracts to be interrogated for their name and details about the assets that represent your NFT.

contract IXRC721 {event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);event ApprovalForAll(address indexed owner, address indexed operator, bool approved);/* @dev Returns the number of NFTs in `owner`'s account. */function balanceOf(address owner) public view returns (uint256 balance);/* @dev Returns the owner of the NFT specified by `tokenId`.*/function ownerOf(uint256 tokenId) public view returns (address owner);* - If the caller is not `from`, it must be have been allowed to move this* NFT by either {approve} or {setApprovalForAll}.*/function safeTransferFrom(address from, address to, uint256 tokenId) public;function transferFrom(address from, address to, uint256 tokenId) public;function approve(address to, uint256 tokenId) public;function getApproved(uint256 tokenId) public view returns (address operator);function setApprovalForAll(address operator, bool _approved) public;function isApprovedForAll(address owner, address operator) public view returns (bool);}

Now, do the functions look similar to the XRC20 token. Yes, it does. However, what are the differences? Let us discuss this one by one.

Ownership and Balances

In XRC721 as each token is unique and is and token ID is of type uint256. Thus, the ownerOf function returns the owner of a particular token ID.

// Mapping from token ID to ownermapping(uint256 => address) private _tokenOwner;function ownerOf(uint256 tokenId) public view returns (address) {address owner = _tokenOwner[tokenId];require(owner != address(0), "XRC721: owner query for nonexistent token");return owner;}// Mapping from owner to number of owned tokenmapping(address => Counters.Counter) private _ownedTokensCount;

Similar to an XRC20, there is a balance of XRC721, which gives the total of the NFT tokens a user owns. In the XRC721 case, it is the sum of all tokenIDs a user owns, which is maintained by a counter.

function balanceOf(address owner) public view returns (uint256) {require(owner != address(0), "XRC721: balance query for the zero address");return _ownedTokensCount[owner].current();}

Going back to our example, Alice is putting up 7 paintings of the Blue period in the marketplace, and she has used token IDs 1 to 7. So initially for any token ID say 5, the ownerOf will return Alice, and balanceOf Alice will be 7.
Now say Bob buys painting 5 (tokenID 5) from Alice, ownerOf tokenID 5 will return Bob, balanceOf Bob will be 1, and balanceOf Alice will be 6.

So, while for XRC20, you check against balanceOf an address before a transfer, in the case of XRC721, you check ownerOf a tokenID to validate the user owns a token before a transfer.

Approvals and Transfers

function approve(address to, uint256 tokenId) public {address owner = ownerOf(tokenId);require(to != owner, "XRC721: approval to current owner");require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()),"XRC721: approve caller is not owner nor approved for all");_tokenApprovals[tokenId] = to;emit Approval(owner, to, tokenId);}function setApprovalForAll(address to, bool approved) public {require(to != _msgSender(), "XRC721: approve to caller");_operatorApprovals[_msgSender()][to] = approved;emit ApprovalForAll(_msgSender(), to, approved);}

Similar to XRC20 an owner or an approved owner can transfer tokens on behalf of the owner. The approve function allows setting approval for a particular token ID while setApprovalForAll, allows the owner to approve an operator to transfer all tokens on behalf of him.

function transferFrom(address from, address to, uint256 tokenId) public {//solhint-disable-next-line max-line-lengthrequire(_isApprovedOrOwner(_msgSender(), tokenId), "XRC721: transfer caller is not owner nor approved");_transferFrom(from, to, tokenId);}function _transferFrom(address from, address to, uint256 tokenId) internal {require(ownerOf(tokenId) == from, "XRC721: transfer of token that is not own");require(to != address(0), "XRC721: transfer to the zero address");_clearApproval(tokenId);_ownedTokensCount[from].decrement();_ownedTokensCount[to].increment();_tokenOwner[tokenId] = to;emit Transfer(from, to, tokenId);}

Again similar to XRC20 the transfer, the transferFrom function allows the owner or an approved operator to transfer a specific token ID to another user.

As NFTs are typically traded in a marketplace, which is a smart contract, two additional functions are provided for transfer.

function safeTransferFrom(address from, address to, uint256 tokenId) public {safeTransferFrom(from, to, tokenId, "");}

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
require(_isApprovedOrOwner(_msgSender(), tokenId), "XRC721: transfer caller is not owner nor approved");_safeTransferFrom(from, to, tokenId, _data);}

function _safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) internal {
_transferFrom(from, to, tokenId);require(_checkOnERC721Received(from, to, tokenId, _data), "XRC721: transfer to non XRC721Receiver implementer");}

Now the first two functions are the same, except there is an additional data component. This ‘data’ information is passed on to the intended recipient be it EOA or a smart contract, but specifically meant for a receiving smart contract, which can do additional processing using the ‘data’ element.

Now, how is it achieved? Note in the actual implementation of _safeTransferFrom the line -

require(_checkOnERC721Received(from, to, tokenId, _data), "XRC721: transfer to non XRC721Receiver implementer");

The implementation of _checkOnERC721Received

function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)internalreturns (bool){if (!to.isContract()) {return true;}bytes4 retval = IXRC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data);return (retval == _XRC721_RECEIVED);}

So, what is done if the transfer is to an EOA, which the function returns true, and if the transfer is to a smart contract, it expects the receiving smart contract to return an acceptance by implementing a call back function onERC721Received.
A typical example of onERC721Received in a receiving smart contract is as follows -

function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4){//Function Signature //bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));return = 0x150b7a02;}

Token Metadata

As typically an NFT is associated with an underlying asset and tradable in a marketplace, it is necessary to have a mechanism, which provides detail of the underlying NFT. It is implemented using a metadata-uri functionality. So, to extend the XRC20 concept an NFT, as well as having a name and a symbol, it also has a metadata-uri, which is an uri of a typical json file.
The json can have its own components like name, description, a thumbnail picture, a certified provenance letter, and so on.

function _setTokenURI(uint256 tokenId, string calldata uri) external {require(_exists(tokenId), "XRC721Metadata: URI set of nonexistent token");_tokenURIs[tokenId] = uri;}function tokenURI(uint256 tokenId) external view returns (string memory) {require(_exists(tokenId), "XRC721Metadata: URI query for nonexistent token");return _tokenURIs[tokenId];}

A typical JSON schema can be modeled like:

{"title": "Asset Metadata","type": "object","properties": {"name": {"type": "string","description": "Identifies the asset to which this NFT represents"},"description": {"type": "string","description": "Describes the asset to which this NFT represents"},"image": {"type": "string","description": "A URI pointing to a resource with mime-type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."}}}

And Finally constructor and Minting

So, now we have understood XRC721 vis-à-vis XRC20, it is time to wrap up, on creating an XRC721 token.
As a minimum constructor.

constructor(string memory name, string memory symbol) public {_name = name;_symbol = symbol;// register the supported interfaces to conform to XRC721 via XRC165_registerInterface(_INTERFACE_ID_XRC721_METADATA);}

So, an NFT token is created with a name and symbol and it registers the interface (TL;DR).

Once a token is created, you set the corresponding metadata-uri using the _setTokenURI for each tokenID you create, as the JSON metadata is tokenID specific.

Typically an XRC20 can be created with a totalSupply. However, in case of XRC721 it is advisable to mint and burn facilities, as new NFTs are added.

function _mint(address to, uint256 tokenId) internal {require(to != address(0), "XRC721: mint to the zero address");require(!_exists(tokenId), "XRC721: token already minted");_tokenOwner[tokenId] = to;_ownedTokensCount[to].increment();emit Transfer(address(0), to, tokenId);}function _safeMint(address to, uint256 tokenId, bytes memory _data) internal {_mint(to, tokenId);require(_checkOnERC721Received(address(0), to, tokenId, _data),"XRC721: transfer to non XRC721Receiver implementer");}function mint(address to, uint256 tokenId) public onlyMinter returns (bool) {_mint(to, tokenId);return true;}function safeMint(address to, uint256 tokenId) public onlyMinter returns (bool) {_safeMint(to, tokenId);return true;}function safeMint(address to, uint256 tokenId, bytes memory _data) public onlyMinter returns (bool) {_safeMint(to, tokenId, _data);return true;}

As you can see there is a normal mint and safeMint, which are similar to transferFrom and safeTransferFrom, wherein it is recommended to use the safeMint functions with or without data, as is appropriate for the business case.

Note: As regards call back we have used the standard Ethereum onERC721received for standard wallet support.

Signing Off

There has been an upsurge in blockchain-based solutions in the market, lately. Also, the world has witnessed many different unconventional types of currencies and digital assets set to step especially into the financial market. Indubitably, every other miner and investor is profiting from decentralized digital currencies like bitcoin. Well, our new player — NFT — is also gaining traction and is set to change the digital asset industry completely.

What do you think about this change of NFT disrupting the digital asset industry?

Let me know in the comments below.

--

--

Dolly Bhati
Yodaplus

A technophile with a soul of travel yogi — writing experience in blockchain, cryptocurrency, dApps, software development, yoga, etc.