TON Project Development Tutorial (1): How to Create an NFT on the TON Chain from the Source Code Perspective

Mario looks at Web3
2024-06-24 13:49:44
Collection
The current official TON documentation has some barriers to learning, so I tried to outline a series of articles about TON Chain project development based on my own learning path, hoping to help everyone quickly get started with TON DApp development.

Author: @Web3Mario(https://x.com/web3_mario)

Abstract: Following the previous article introducing TON technology, I have recently delved into the official TON development documentation. I feel that there is still a bit of a learning curve, as the current documentation seems more like an internal development document, which is not very friendly for new developers. Therefore, I have attempted to organize a series of articles on TON Chain project development based on my own learning trajectory, hoping to help everyone quickly get started with TON DApp development. If there are any errors in the writing, I welcome everyone to point them out so we can learn together.

What are the differences between developing NFTs in EVM and on TON Chain?

Issuing an FT or NFT is usually the most basic requirement for DApp developers. Therefore, I also use this as an entry point for learning. First, let’s understand the differences between developing an NFT in the EVM technology stack and on TON Chain. NFTs based on EVM typically choose to inherit the ERC-721 standard. An NFT refers to a type of indivisible cryptographic asset, where each asset has uniqueness, meaning it possesses certain exclusive characteristics. ERC-721 is a general development paradigm for this type of asset. Let’s look at a common ERC721 contract and see which functions need to be implemented and what information needs to be recorded. The following image shows an ERC721 interface. Unlike FTs, in the transfer interface, the input required is the tokenId to be transferred rather than the quantity. This tokenId is also the most basic embodiment of the uniqueness of the NFT asset. Of course, to carry more attributes, a metadata record is usually kept for each tokenId, which is an external link that stores other extensible data of the NFT, such as a link to a PFP image, certain attribute names, etc.

For developers familiar with Solidity or object-oriented programming, implementing such a smart contract is relatively easy. As long as the necessary data types in the contract are defined, such as some key mapping relationships, and the corresponding logic for modifying these data is developed based on the required functionality, an NFT can be implemented.

However, in TON Chain, everything changes a bit, and there are two core reasons for this difference:

  • In TON, data storage is based on Cells, and the Cells of the same account are implemented through a directed acyclic graph. This leads to the necessity that the data requiring persistent storage cannot grow indefinitely, because in a directed acyclic graph, the depth of the data determines the query cost. When the depth extends infinitely, it may result in excessively high query costs, leading to contract deadlock issues.
  • To pursue high concurrency performance, TON has abandoned the serial execution architecture and adopted a development paradigm specifically designed for parallelism, the Actor model, to reconstruct the execution environment. This results in the impact that smart contracts can only asynchronously call each other by sending so-called internal messages. Note that both state-modifying and read-only calls must adhere to this principle. Additionally, careful consideration must be given to how to handle data rollback in case of failure in asynchronous calls.

Of course, other technical differences have been discussed in detail in the previous article. This article aims to focus on smart contract development, so I will not elaborate further. The above two design principles create significant differences between smart contract development in TON and EVM. In the initial discussion, we know that an NFT contract needs to define some mapping relationships, that is, mappings, to store NFT-related data. The most important of these is owners, which stores the mapping relationship between a tokenID and the address of the NFT owner, determining the ownership of the NFT. The transfer is a modification of that ownership. Since theoretically this is a potentially unbounded data structure, it should be avoided as much as possible. Therefore, it is officially recommended to use the existence of unbounded data structures as a standard for sharding. That is, when there is a similar data storage requirement, it is replaced by the master-slave contract paradigm, managing the data corresponding to each key through the creation of subcontracts. The master contract manages global parameters or helps handle internal information exchange between subcontracts.

This also means that NFTs in TON need to adopt a similar architecture for design, where each NFT is an independent subcontract that stores exclusive data such as owner address, metadata, etc., and is managed by a master contract that oversees global data like NFT name, symbol, total supply, etc.

After clarifying the architecture, the next step is to address the core functional requirements. Since this master-slave contract approach is adopted, it is necessary to clarify which functions are carried by the master contract and which are carried by the subcontracts, as well as how they communicate through internal information. Additionally, when execution errors occur, how to roll back the previous data needs to be considered. Typically, before developing complex large projects, it is necessary to clarify the information flow between each other through a class diagram and carefully think through the rollback logic after internal call failures. Although the above NFT development is simple, similar validation can also be done.

Learning to develop TON smart contracts from source code

TON has chosen to design a static typed language similar to C, called Func, as the smart contract development language. Now let’s learn how to develop TON smart contracts from the source code. I have chosen the NFT example from the official TON documentation for this introduction. Interested friends can check it out themselves. In this case, a simple TON NFT example is implemented. Let’s look at the contract structure, which is divided into two functional contracts and three necessary libraries.

These two main functional contracts are designed according to the principles mentioned above. First, let’s look at the code of the main contract nft-collection:

This introduces the first knowledge point: how to persistently store data in TON smart contracts. We know that in Solidity, persistent data storage is automatically handled by the EVM based on the parameter types. Typically, the state variables of a smart contract will be automatically persisted based on the latest values after execution, and developers do not need to consider this process. However, in Func, this is not the case; developers need to implement the corresponding processing logic themselves. This situation is somewhat similar to how C and C++ need to consider the GC process, while other new programming languages usually automate this part of the logic. Let’s look at the code. First, some necessary libraries are imported, and then we see the first function loaddata, which is used to read the persistently stored data. Its logic is to first return the persistent contract storage cell through getdata. Note that this is implemented by the standard library stdlib.fc, and typically some of its functions can be viewed as system functions.

The return value type of this function is cell, which is the cell type in TVM. In the previous introduction, we already know that all persistent data in the TON blockchain is stored in a cell tree. Each cell can hold up to 1023 bits of arbitrary data and up to four references to other cells. Cells are used as memory in the stack-based TVM. The cell stores tightly encoded data, and to retrieve specific plaintext data from it, the cell must be converted to a type called slice. The cell can be converted to slice type using the begin_parse function, and then data bits and references to other cells can be loaded from the slice to obtain the data in the cell. Note that the calling method in line 15 is a syntactic sugar in Func, allowing direct invocation of the second function of the return value of the first function. Finally, the corresponding data is loaded in the order of data persistence. Note that this process is different from Solidity; it is not called based on a hashmap, so this calling order cannot be disrupted.

In the savedata function, the logic is similar, except that this is a reverse process. This introduces the next knowledge point: a new type called builder, which is a cell builder type. Data bits and references to other cells can be stored in the builder, and then the builder can be finalized into a new cell. First, a builder is created using the standard function begincell, and then related functions are called to store the relevant data. Note that the calling order mentioned above must be consistent with the storage order here. Finally, the new cell is constructed using endcell, at which point the cell is managed in memory, and finally, through the outermost setdata, the persistent storage of that cell can be completed.

Next, let’s look at the business-related functions. First, we need to introduce another knowledge point: how to create a new contract through a contract, which will be frequently used in the previously mentioned master-slave architecture. We know that in TON, the calls between smart contracts are implemented by sending internal messages. This is achieved through a function called sendrawmessage. Note that the first parameter is the encoded cell of the message, and the second parameter is a flag used to indicate the execution method of the transaction. In TON, different execution methods for sending internal messages are set, currently with 3 message Modes and 3 message Flags. A single Mode can be combined with multiple (possibly none) flags to achieve the desired mode. The combination simply means filling in the sum of their values. Below is a description table of Modes and Flags.

Now let’s look at the first main function, deploynftitem. As the name suggests, this is a function used to create or mint a new NFT instance. After some operations, a msg is encoded and sent via sendrawmessage to the internal contract, selecting flag 1 as the sending flag, only specifying the fee indicated in the encoding as the gas fee for this execution. From the previous introduction, we can easily realize that this encoding rule should correspond to the way of creating a new smart contract. Let’s see how this is specifically implemented.

Let’s look directly at line 51. The above two functions are auxiliary functions used to generate the information required for the message, so we will look at them later. This is an encoding process for an internal message to create a smart contract. Some of the numbers in between are actually identifiers used to indicate the requirements of this internal message. Here we introduce the next knowledge point: TON has chosen a binary language called TL-B to describe the execution methods of messages, and different flags are set to implement certain specific functions of internal messages. The two most easily thought of use cases are new contract creation and function calls of already deployed contracts. The method in line 51 corresponds to the former, creating a new nft item contract, which is mainly specified by lines 55, 56, and 57. First, line 55 contains a long string of numbers, which is a series of identifiers. Note that the first parameter of store_uint is the value, and the second is the bit length, which determines that this internal message is for contract creation with the last three flags, and the corresponding binary value is 111 (in decimal, this is 4+2+1), where the first two indicate that the message will carry StateInit data, which is the source code of the new contract and the data required for initialization. The last flag indicates that the internal message is attached, meaning it wishes to execute the relevant logic and the required parameters. Therefore, you will see that in line 66, no data is set for these three bits, indicating a function call to an already deployed contract. The specific encoding rules can be found here.

The encoding rules for StateInit correspond to line 49, calculated by calculatenftitemstateinit. Note that the encoding of stateinit data also follows a predetermined TL-B encoding rule, involving two main parts: the code of the new contract and the initialization data. The encoding order of the data must be consistent with the storage order specified by the new contract's persistent cell. In line 36, we can see that the initialization data includes itemindex, which is similar to the tokenId in ERC721, and the current contract address returned by the standard function myaddress, which is the collection_address. The order of this data is consistent with the declaration in nft-item.

The next knowledge point is that in TON, all ungenerated smart contracts can have their generated addresses pre-calculated. This is similar to the create2 function in Solidity. In TON, the generation of a new address consists of two parts: the workchain identifier and the hash value of the stateinit, concatenated together. The former, as we have learned in previous introductions, needs to be specified for the corresponding TON infinite sharding architecture, currently being a unified value obtained by the standard function workchain. The latter is obtained by the standard function cellhash. Therefore, returning to this example, calculatenftitemaddress is the function for pre-calculating the address of the new contract. The generated value is encoded into the message in line 53 as the receiving address of this internal message. The nft_content corresponds to the initialization call of the created contract, which will be introduced in detail in the next article.

As for sendroyaltyparams, it needs to respond to a read-only request for an internal message. In the previous introduction, we specifically emphasized that in TON, internal messages not only include operations that may modify data, but read-only operations also need to be implemented in this way. Therefore, this contract is for such operations. First, it is worth noting that line 67 indicates the flag for the callback function of the requester after responding to this request, which will return the requested item index and the corresponding royalty data.

Next, let’s introduce the next knowledge point: in TON, smart contracts have only two unified entry points, named recvinternal and recvexternal, where the former is the unified entry point for all internal messages, and the latter is for all external messages. Developers need to respond to different requests in the function based on the requirements, using a switch-like approach according to the different flags specified by the message, where the flags here correspond to the callback function flag in line 67. Returning to this example, first, the message undergoes a null check, and upon passing, the information in the message is parsed. First, in line 83, the sender_address is parsed, which will be used for subsequent permission checks. Note that the ~ operator here is another syntactic sugar. We will not elaborate on this for now. Next, the op operation flag is parsed, and then based on different flags, the corresponding requests are processed. Among them, some logic is used to call the above functions, such as responding to requests for royalty parameters or minting new NFTs and incrementing the global index.

The next knowledge point corresponds to line 108. As you can guess from the naming, the processing logic of this function is similar to the require function in Solidity. In Func, exceptions are thrown using the standard function throwunless, where the first parameter is the error code and the second is a boolean check. If the check is false, an exception is thrown along with the error code. In this line, equalslices is used to determine whether the parsed senderaddress is equal to the owneraddress stored persistently in the contract, performing a permission check.

Finally, to make the code structure clearer, a series of auxiliary functions for obtaining persistent information are introduced, which will not be elaborated on here. Developers can refer to this structure to develop their own smart contracts.

Developing DApps in the TON ecosystem is indeed an interesting endeavor, with significant differences from the EVM development paradigm. Therefore, I will introduce how to develop DApps on TON Chain through a series of articles. Let’s learn together and seize this opportunity. I also welcome everyone to interact with me on Twitter, to brainstorm some new and interesting DApp ideas and develop together.

ChainCatcher reminds readers to view blockchain rationally, enhance risk awareness, and be cautious of various virtual token issuances and speculations. All content on this site is solely market information or related party opinions, and does not constitute any form of investment advice. If you find sensitive information in the content, please click "Report", and we will handle it promptly.
banner
ChainCatcher Building the Web3 world with innovators