[RFC] NRC721: NFTs on Nervos Blockchain

NRC721 — NFTs on Nervos Blockchain

Pouring extensibility on Nervos Network

Nervos network works in a unique way to maximize decentralization while remaining minimal, flexible, and secure, but this uniqueness makes its operation substantially different from existing blockchains, thus making it necessary to define its own standards.

The Nervos Ecosystem offers a multilayer architecture to achieve scalability, security, and decentralization. The Nervos Blockchain is Layer 1 of this ecosystem and was thought to store the Common Knowledge Base generated in it. L1 Nervos network has a unique architecture, making it difficult to extrapolate existing standards from other blockchains.

This article proposes a standard for NFTs creation and handling and an example of an implementation of it with a modular programming perspective. This standard is aimed to provide a standardized structure so developers, users, and stakeholders have a common understanding of how things are done, making it easier to have integration between different Dapps and projects.

NRC-721 proposal is inspired by Openzeppelin’s implementation of ERC-721 from the Ethereum Blockchain as we consider this to be a mature, tested implementation proposal that actively makes it easier for Ethereum developers to deploy and integrate NFTs into their applications.

We aim to provide similar simplicity and lower the onboarding bar to the Nervos ecosystem.

NRC-721

Factory Cell

In order to store the common data without repeating and paying for unnecessary storage space in the blockchain a factory Cell is defined to store common data:

Type:
- code_hash: TYPE_ID or Custom Script
- type: "type"
- args: TYPE_ID [32 bytes]

Data:
- name: length<uint8> + text<utf-8> (max 255 char)
- symbol: length<uint8> + text<utf-8> (max 255 char)
- base_token_uri: length<uint8> + text<utf-8> (max 255 char)

Lock: <user defined>

Since we need some mechanism to ensure that this cell is unique, we recommend using the Type ID proposed here. For that purpose, the nervos blockchain offers a built-in type_id script that can be used without the need of developing and deploying a custom script, but, when looking for custom features on the factory, a specific script should be developed implementing the type_id functionality, plus the custom features. The data on this cell should be verified on the NFT script to ensure compatibility with the standard, so the script should require using the factory as a dependency.

The purpose ofbase_token_uri is to provide a detailed information source for the token. The path to fetch this info would use the base_token_uri as the base path along with the token id:base_token_uri/token_id, and it should return a JSON object with the extra info. For this JSON object, we adopt Ethereum’s ERC721 Metadata JSON Schema :

{
   "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."
      }
   }
}

Referencing the Ethereum standard will allow straightforward cross-chain integration with blockchains that have already adopted it.

Token Cells

The tokens cells contain the information of each unique token created from the factory and will be governed by the NFT script.

Type:
- code_hash: NFT_Script
- type: "type"
- args: Factory_code_hash<32 bytes> + Factory_type<uint8> + Factory_args<32 bytes> + TOKEN_ID

Data:
- Token specific data (can be empty)

Lock: <user defined>

Using the factory cell type fields in the args of the token allows getting the information of the factory from the token Cell. This script should provide verification to ensure that each token has a unique TOKEN_ID, we propose using the Type_id logic here to achieve that.

Using NRC-721 in your application

Following we provide a summarized flow of how your Dapp would take advantage of this standard:

NRC721— Deploy your NFTs in minutes

Deploying your NFTs in Nervos, without the hassle

This implementation of the standard aims to make development on Nervos Layer 1 easier and more secure by leveraging the use of a core with the basic standard features, plus extensions that can easily be added to give each application custom behavior. This core and extensions are previously tested and audited, speeding up new developments while maintaining security and reliability.

Blockchain development in Layer 1 of Nervos ecosystem is accomplished through low-level languages, mainly RUST. This allows for efficient and compact code, but can substantially increase the onboarding time when developers arrive at this ecosystem.

To help speed up the onboarding of new developers and simplify the work for the experienced ones, we propose the following code structure that leverages the use of metaprogramming to build modular scripts that can be adapted to specific use cases while maintaining efficiency and security.

Rust implementation

Our implementation consists of two repositories: NRC-721 and NRC-721-template. The first one contains the base implementation of the type-script for a custom NFT. It also contains optional behaviors that can be added using a single line of code, increasing the capabilities of the NRC-721. The second repository contains a template for creating custom NFTs.

Custom NFT creation procedure

In order to compile the custom NFT, it is necessary to have ckb-capsule installed and working.

Then the following steps will suffice to edit and build a new NFT.

First, we need to clone the NRC-721-template repository. Then, to define custom behavior we have two main points to introduce the desired logic. Editing the file contracts/custom_nft/src/entry.rs, we find two sections marked through code comments.

Composite script section: allows defining a list of basic behaviors that we want our NFT to implement. In this section, we can choose from the basic behaviors available in the extensions module.

Custom behavior section: this section allows defining custom behaviors through Rust code.

// Custom behavior

pub struct Custom;
impl Custom {
    pub fn handle_creation(_nft_type: &Script) -> Result<(), Error> { Ok(()) }
    pub fn handle_update(_nft_type: &Script) -> Result<(), Error> { Ok(()) }
    pub fn handle_destroying(_nft_type: &Script) -> Result<(), Error> { Ok(()) }
}

// Composite script

use nrc_721::extensions::OnlyOwner;

define_script! { ComposedScript(Base, OnlyOwner, Custom) { } }

Once the behavior definition is completed we can build the typeScript using Capsule.

capsule build

The resulting script can be tested by a test included in the repository using capsule again.

capsule test

In order to use the script in production we must build it in release mode.

capsule build --release

A Javascript SDK to simplify integration into Dapps

Having defined a standard for NFTs allows creating pre-built tools to interact with them. The SDK developed by our team serves this purpose, it allows creating, reading, and managing factory Cells and NFTs in a simple way, so Dapps developers can adopt our standard without the need to get into the transaction implementation details. The source code along with examples of usage are available in Github.

Installing and setup

The SDK is available on npm package manager, it can be installed by running:

npm install @rather-labs/nrc-721-sdk

To run the examples available in the repository a testnet address and private key will be needed, they can be generated using this tool. We will also need some ckb to create the cells, on testnet we can get this ckb by inserting our address in this faucet.

The examples are set up to connect to a node and indexer running locally, a local testnet or dev node can be run following the instructions here, and here to run a local indexer. Nervos also maintains a public testnet node and indexer that can be used just by setting the URLs:

nodeUrl = "http://3.235.223.161:18114";indexerUrl = "http://3.235.223.161:18116";

Minting a Factory Cell

To be able to create NFTs we first need to mint a factory cell, that will be used to mint NFTs from it and store common info, so it acts as a collection. Unless some custom functionality is needed, the factory can be created using the Type ID logic that is built into Nervos blockchain, so no contract deployment is needed.

The factory can be minted just by indicating the basic info it will contain:

const { rawTransaction, typeScript, usedCapacity } = 
await factoryCell.mint({
    name: "Test token factory",
    symbol: "TTF",
    baseTokenUri: "http://testtoken.com",
    sourceAddress: OWNER_ADDRESS,
    targetAddress: OWNER_ADDRESS,
    fee: 0.0001,
});

the full example code can be found here. After minting our factory cell we will have its typeScript that will be used to identify our cell in the blockchain and will be needed to mint our NFTs.

Now we have our factory and from the previous section we should also have a deployed contract to govern our NFTs, so we are ready to mint our tokens, we just need the typeScript of our contract cell. We have also deployed our base NRC-721 contract in the testnet and is available to anyone to test the SDK integration, you can find its typeScript in our Github.

We are now ready to mint our NFTs using this example, indicating our contract and factory typeScripts. A data object can also be included, by default it will be serialized using JSON and stored in the data part of the NFT cell:

const {
    rawTransaction,
    nftTypeScript,
    usedCapacity
} =
await nftCell.mint({
    nftContractTypeScript,
    factoryTypeScript,
    sourceAddress: OWNER_ADDRESS,
    targetAddress: OWNER_ADDRESS,
    fee: 0.0001,
    data: {
        someKey: "SomeValue",
        anotherKey: "AnotherValue",
    },
});

The minted token can be retrieved from the blockchain using the SDK as shown in this example. We will be able to get all the info for the token and factory, so we can now use it on our Dapp:

Nft Token Id: d9ae00e88a7c2c701df1a7ee410b05a67c0a13fc50bfa0bbe05553d4ddb7e75e

Nft Token URI:
[http://testtoken.com/d9ae00e88a7c2c701df1a7ee410b05a67c0a13fc50bfa0bbe05553d4ddb7e75e](http://test-token.com/d9ae00e88a7c2c701df1a7ee410b05a67c0a13fc50bfa0bbe05553d4ddb7e75e)

Nft Cell Data: { someKey: 'SomeValue', anotherKey: 'AnotherValue' }

Factory Data: {
    name: 'Test token factory',
    symbol: 'TTF',
    baseTokenUri: 'http://test-token.com'
}
3 Likes

It’s a very good proposal, but due to CKB’s state payment design, it requires a lot of capital cost to store NFT data in Cell. Now there are some proposals to greatly reduce the NFT operating cost on CKB. They all use vector commitment, such as Sparse Merkle Tree, the state root is stored in the cell, and the state is stored off-chain. The layered architecture is more appropriate to the Nervos philosophy… like RFC: Compact UDT Lock - English / CKB Development & Technical Discussion - Nervos Talk[RFC] CoTA: A Compact Token Aggregator Standard for Extremely Low Cost NFTs and FTs - English / CKB Development & Technical Discussion - Nervos Talk

1 Like

Hi, this is a very clear proposal, especially the composable design, which makes custom NFTs possible

But we found some issues to ask for advice

We found that the implementation is not consistent with the proposal, is it possible that the proposal is out of date?

  • factory cell data seems to have an implied prefix of blake2b_16("NRC-721F").
  • string in the implementation is size(u16be) | utf8(bytes) and in the proposal is length(u8) | utf8(bytes).

There is also the issue of NFT identity, we know that ERC721 can be identified by contract + token_id, NRC721 looks like it can be identified by NFT cell type args, but NRC721 may have some special features because it is likely that most of the factory cell will use type id as type script, NFT cell type args currently uses factory_cell_type_code_hash | factory_cell_type_hash_type | factory_cell_ type_args | token_id for identity, one may see a lot of similar type id prefixes in the application layer, and the second is that NFT token occupies more capacity, can NFT type script use factory_cell_type_hash | token_id to replace it?