Crown Platform NFT Framework

Artem
Crown Platform
Published in
9 min readJun 20, 2019

Non-fungible Token creation on Crown Core

Since the beginning of the Crown Platform evolution, we wanted to build a public blockchain solution that will enable simple and fast integration with a modern distributed ledger technology. Back in August 2017, the foundational ideas of how the platform should look like were laid down. Since then, we’ve been working hard to realize these ideas and transform them into a tangible form. The general platform overview that is available here explains the main concepts of the Crown Platform. Today we are going to concentrate on the part that evolved to become the Non-Fungible Token Framework or NFT Framework. In the platform overview that part has a name — Registry Subsystem. If you’re not familiar with the term NFT, feel free to read a quick overview here. In simple words, when a token is non-fungible it means it’s not interchangeable, it has unique properties and one asset is not equal to another asset in the same set, as it is with the fungible tokens. They are mostly monetary and interchangeable.

After some experiments with development, we changed terminology and the API interface a bit. So it now aligns with the notion of NFTs that became popular with the rise of Ethereum, ERC-721 smart contract standard and success of the Cryptokitties game. We believe it will be easier for developers to understand the workflow and start using Crown Platform in their applications if we adopt the same abstract concepts that exist on the market. In general, we all need the same technical language which is platform-agnostic to effectively communicate about blockchain software development.

Crown NFT vs Ethereum ERC-721 NFT

The conceptual difference between Ethereum NFTs and Crown NFTs is that in the first case the development process is more code-oriented or smart contracts based. It means that the most part of the business logic is deployed and executed on the EVM. However, in Crown we use the data-oriented approach and the Crown Platform API as a gateway for the integration of your web or any other application with a blockchain solution. There are pros and cons to both approaches. The smart contract approach gives you the flexibility to run arbitrary code on-chain and customize your business-logic in a smart contract, and in many cases, it’s exactly what you need. But sometimes you need a blockchain solution that you can quickly integrate with your existing or developing application using the API. You don’t need to deal with the smart contracts languages and time-consuming development. You need to get clear on the data model for your non-fungible tokens set and how it fits into your architecture. So eventually, you have business-logic running off-chain on your back-end which interacts with the on-chain solution that you integrate using the Crown Platform API. Plus, you don’t have to deal with potential bugs that might be introduced with a smart contract code and are irreversible.

NFT Data Structure

Design Your Blockchain Solution

When you work on an NFT solution based on Ethereum, you need to write Solidity code that implements standard smart contract interface:

/// @title ERC-721 Non-Fungible Token Standard
interface ERC721 /* is ERC165 */
{
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);
function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

The implementation will encapsulate the logic of your NFT blockchain solution. The analog of the smart contract implementation on Crown is an NFT protocol registration. You need to define the set of rules for your non-fungible token solution. First, you need to set a protocol symbol or protocol ID, it should be a string that contains only symbols from the set: .abcdefghijklmnopqrstuvwxyz12345 with the length between 3 and 12 characters. It might be something like: “doc”, “docproof”, “prf”, “ckt” or “cryptoknight”, “lux” etc. This string is used as a unique identifier of the protocol in the system. Also, you can set up a long protocol name that will be more meaningful and can contain up to 24 arbitrary symbols, for example: “Documents”, “Documents Proof”, “Proofs”, “Cryptoknights Game”, “Luxury Goods” etc. As an engineer, you also need to define the metadata mimetype, its schema and whether it will be embedded in the NFT or just contain a link to an IPFS or another resource with the metadata. Usually, it should be an “application/json” format, and the metadata field should contain a link to the JSON file. Embedding metadata directly into the blockchain record only makes sense if it’s shorter than a URL. If the metadata field contains many symbols, the NFT transaction fee will grow exponentially (not implemented yet). You will also need to define some other rules for your protocol that will customize the behavior of your application. Some of the fields of the protocol are:

    /// NF token protocol unique symbol/identifier, can be an a    abbreviated name describing this NF token type
/// Represented as a base32 string, can only contain characters: .abcdefghijklmnopqrstuvwxyz12345
/// Minimum length 3 symbols, maximum length 12 symbols
uint64_t tokenProtocolId;
/// Full name for this NF token type/protocol
/// Minimum length 3 symbols, maximum length 24
std::string tokenProtocolName;
/// URI to schema (json/xml/binary) describing metadata format
std::string tokenMetadataSchemaUri;
/// MIME type describing metadata content type
std::string tokenMetadataMimeType;
/// Defines if metadata is embedded or contains a URI
/// It's recommended to use embedded metadata only if it's shorter than URI
bool isMetadataEmbedded;
/// Owner of the NF token protocol
CKeyID tokenProtocolOwnerId;

Note: the protocol registration is not necessary for the upcoming testnet release 0.13.9 (pre-release before 0.14.0), you can start NFTs registration with arbitrary protocol symbol to test your solution.

NF Token Registration and API interface

So now comes the fun part. As an engineer, you will be interacting with the Crown Platform API interface to issue new NFTs, query different information about them for your users, etc. I am going to provide you a couple of examples of how you can use the API through the wallet command line interface and a small code example.

All the NFT framework APIs start with the nftoken command. The subcommands list is: register(issue)|list|get|getbytxid|totalsupply|balanceof|ownerof.

In order to register/issue a new NFT instance you need to call the corresponding remote procedure:

nftoken register/issue "nfTokenProtocol" "tokenId" "tokenOwnerAddr" "tokenMetadataAdminAddr" "metadata"Creates and sends a new non-fungible token transaction.
Arguments:
1. "nfTokenProtocol" (string, required) A non-fungible token protocol symbol to use in the token creation. The protocol name must be valid and registered previously.
2. "nfTokenId" (string, required) The token id in hex. The token id must be unique in the token type space."3."nfTokenOwnerAddr" (string, required) The token owner key, can be used in any operations with the token. The private key belonging to this address may be or may be not known in your wallet.4. "nfTokenMetadataAdminAddr" (string, optional, default = \"0\"). The metadata token administration key, can be used to modify token metadata. The private key does not have to be known by your wallet. Can be set to 0.5. "nfTokenMetadata" (string, optional) metadata describing the token. It may contain text or binary in hex/base64.

Let’s imagine we are building a crypto collectibles game — CryptoKnights. It’s a game where you can own digital assets that represent unique knights. Every other instance has different properties based on the DNA generation algorithm. Just like with Ethereum Cryptokitties game. Let’s say the protocol symbol will be “ckt”. In order to register an instance of a CryptoKnight, you have to run a command like this one:

nftoken issue ckt 2772eeb3a5486f773ad7e47413424356da55db94c7f8e0528fcba5079ddeb8ed CRWJKC453SVnczLQD6opqJVMuHDqNWwaJAgV CRWD4W7PauajWooq8vmtzaxNzEEED5yL3FdZ “https://ipfs.io/ipfs/QmPiYzMQbSPxsKC2b6CHEUHWfqFHjX9bHSu6YVpiopzvTx"

As a result you will get a transaction ID: ce1444b48cdd83761afa79f3089d2d433a417ea3a6eb2cfc403dc0bf4e7f48c6. To uniquely identify an NFT you can use the combination of the protocol ID and the token ID. Or transaction hash.

As an architect of your application, you have to choose a strategy of generating NF token IDs. The restriction is pretty simple — it must be unique in the space of your protocol. It can be a hash of a document that uniquely identifies your tokens, it also can be a simple counter, unique seed, etc. In the example above I calculate SHA-256 hash of the DNA uniquely identifying a new CryptoKnight instance.

After the transaction is mature enough, the NFT is considered registered and can be used as an existing digital asset. The link to the metadata can look something like this:

{
“name”: “Percival”,
“description”: “A knight of the Round Table”,
“image”: “https://percival-image.link
}

Now you can query data from the blockchain to process and display it in your application. In order to get a single instance of an NFT you can simply use the get API:

nftoken get ckt 2772eeb3a5486f773ad7e47413424356da55db94c7f8e0528fcba5079ddeb8ed

As a result, you will have a JSON document that looks something like this:

{
“blockHash” : “000001b031c4832eb185b05475bb37a6d320b6a531a48cc1cdf9300760e84388”,
“registrationTxHash” : “ce1444b48cdd83761afa79f3089d2d433a417ea3a6eb2cfc403dc0bf4e7f48c6”,
“height” : 5197,
“timestamp” : 1561019190,
“nftProtocolId” : “ckt”,
“nftId” : “2772eeb3a5486f773ad7e47413424356da55db94c7f8e0528fcba5079ddeb8ed”,
“nftOwnerKeyId” : “CRWJKC453SVnczLQD6opqJVMuHDqNWwaJAgV”,
“metadataAdminKeyId” : “CRWD4W7PauajWooq8vmtzaxNzEEED5yL3FdZ”,
“metadata” : “https://ipfs.io/ipfs/QmPiYzMQbSPxsKC2b6CHEUHWfqFHjX9bHSu6YVpiopzvTx
}

You can also request the information about the NFT instance using the transaction ID:

nftoken getbytxid ce1444b48cdd83761afa79f3089d2d433a417ea3a6eb2cfc403dc0bf4e7f48c6

Keep in mind that it means using different abstraction level, so it’s recommended to use the token ID to address NFTs. In this case, you interact on a level of your application business-logic.

To query multiple instances you will use the nftoken list API. This command is pretty flexible so you can request and process all existing NFTs, or only those belonging to a specific protocol or belonging to a specific owner or both. You also can set up the requesting blockchain height and amount of records. All of this will give you the flexibility to adapt requests to your needs. For example, to request all CryptoKnights tokens you will write:

nftoken list ckt

or to get all the instances belonging to the owner of the address CRWJKC453SVnczLQD6opqJVMuHDqNWwaJAgV your command will look like this:

nftoken list * CRWJKC453SVnczLQD6opqJVMuHDqNWwaJAgV

Other API documentation details are available using the help command of your node instance and will be available in the project wiki soon. You can also request additional information using the APIs:

nftoken totalsupply ckt
nftoken balanceof CRWJKC453SVnczLQD6opqJVMuHDqNWwaJAgV
nftoken ownerof ckt 2772eeb3a5486f773ad7e47413424356da55db94c7f8e0528fcba5079ddeb8ed

Code Example

This code example is a fragment of a simple application that receives a file as an input and registers an NFT with a unique hash of that file as a token ID, proving the existence and ownership of the underlying data.

int main()
{
CURL * curl = curl_easy_init();
curl_slist * headers = nullptr;
Nft newNft;
if (curl != nullptr)
{
std::string getNewAddr(R"({"jsonrpc": "1.0", "id": "nftest", "method": "getnewaddress", "params": [] })");
headers = curl_slist_append(headers, R"("content-type: text/plain;")"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, "http://127.0.0.1:43001/");
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, getNewAddr.size());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, getNewAddr.c_str());
curl_easy_setopt(curl, CURLOPT_USERPWD, "crownrpc:bogus");
curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_TRY);
// Gen owner address
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, GetNewAddressFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &newNft.OwnerAddress);
curl_easy_perform(curl);
// Gen admin address
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &newNft.AdminAddress);
curl_easy_perform(curl);
// Register new NFT
rapidjson::Document nfTokenRegDoc;
nfTokenRegDoc.SetObject();
nfTokenRegDoc.AddMember("jsonrpc", "1.0", nfTokenRegDoc.GetAllocator());
nfTokenRegDoc.AddMember("id", "nftest", nfTokenRegDoc.GetAllocator());
nfTokenRegDoc.AddMember("method", "nftoken", nfTokenRegDoc.GetAllocator());
rapidjson::Value params(rapidjson::kArrayType);
params.PushBack(rapidjson::Value("register").Move(), nfTokenRegDoc.GetAllocator());
params.PushBack(rapidjson::Value("doc").Move(), nfTokenRegDoc.GetAllocator());
params.PushBack(rapidjson::Value(rapidjson::StringRef(newNft.TokenId.c_str())).Move(), nfTokenRegDoc.GetAllocator());
params.PushBack(rapidjson::Value(rapidjson::StringRef(newNft.OwnerAddress.c_str())).Move(), nfTokenRegDoc.GetAllocator());
params.PushBack(rapidjson::Value(rapidjson::StringRef(newNft.AdminAddress.c_str())).Move(), nfTokenRegDoc.GetAllocator());
params.PushBack(rapidjson::Value(rapidjson::StringRef(newNft.Metadata.c_str())).Move(), nfTokenRegDoc.GetAllocator());
nfTokenRegDoc.AddMember("params", params, nfTokenRegDoc.GetAllocator()); rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
nfTokenRegDoc.Accept(writer);
std::string nftRegJsonStr = buffer.GetString();
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, nftRegJsonStr.size());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, nftRegJsonStr.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NfTokenRegFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &newNft);
curl_easy_perform(curl);
}
return 0;
}

In the example above we generate two new Crown addresses and then make a call to register a new NFT within the “doc” protocol space. And that’s it. You now have a non-fungible token instance registered on the Crown blockchain. We use a local node here for registration on a sandbox blockchain. (Note: the information of how to set up and use Crown sandboxes will be available soon). It’s really up to you what language or library to use for the development. In the near future, I will update the Crown Bitcore library to support NFTs out of the box, so it’s easier for web developers to integrate Crown blockchain solution in their applications.

Thank you for your attention. Stay tuned for the upcoming 0.14 Emerald testnet release this week and more technical documentation on the wiki.

--

--

Artem
Crown Platform

Technology Leader | Solutions Architect | Web3 Builder Focused on ReFi & Blockchain Research | R&D | Product Vision | Blockchain | ex-Boson Protocol, RSK