Author: @Web3Mario
Abstract: Following the previous article about the introduction of TON technology, I have studied the official development documents of TON in depth during this period. I feel that there are still some barriers to learning. The current document content seems to be more like an internal development document, which is not very friendly to new developers. Therefore, I try to sort out a series of articles about the development of the TON Chain project based on my own learning trajectory, hoping to help everyone quickly get started with TON DApp development. If there are any errors in the text, you are welcome to correct them and learn together.
What are the differences between developing NFTs in EVM and developing NFTs on TON Chain
Issuing an FT or NFT is usually the most basic requirement for DApp developers. Therefore, I also use this as a learning entry. First, let us understand the following differences between developing an NFT in the EVM technology stack and in TON Chain. EVM-based NFTs usually choose to inherit the ERC-721 standard. The so-called NFT refers to an indivisible type of cryptographic asset, and each asset is unique, that is, it has certain exclusive characteristics. ERC-721 is a common development paradigm for this type of asset. Let's see what functions a common ERC721 contract needs to implement and what information it needs to record. The figure below is an ERC721 interface. It can be seen that unlike FT, what needs to be entered in the transfer interface is the tokenId to be transferred rather than the amount. This tokenId is also the most basic embodiment of the uniqueness of NFT assets. Of course, in order to carry more attributes, a metadata is usually recorded for each tokenId. This metadata is an external link that saves other extensible data of the NFT, such as a link to a PFP picture, certain attribute names, etc.
For developers who are familiar with Solidity or object-oriented, it is easy to implement such a smart contract. As long as the data types required in the contract are defined, such as some key mapping relationships, and the corresponding modification logic of these data is developed according to the required functions, an NFT can be implemented.
However, in TON Chain, all this is different. There are two core reasons for the difference:
In TON, data storage is based on Cell, and the Cell of the same account is implemented through a directed acyclic graph. This results in the data that needs to be stored for a long time not being able to grow without boundaries, because for a directed acyclic graph, the query cost is determined by the depth of the data. When the depth is infinitely extended, it may cause the query cost to be too high, which will cause the contract to fall into a deadlock problem.
In order to pursue high concurrency performance, TON abandoned the serial execution architecture and adopted a development paradigm designed for parallelism, the Actor model, to reconstruct the execution environment. This has an impact that smart contracts can only be called asynchronously by sending so-called internal messages. Note that this principle must be followed for both state modification type and read-only type calls. In addition, it is also necessary to carefully consider how to handle data rollback if the asynchronous call fails.
Of course, other technical differences have been discussed in detail in the previous article. This article hopes to focus on smart contract development, so it will not be discussed in detail. The above two design principles make smart contract development in TON very different from EVM. In the initial discussion, we know that some mapping relationships, that is, mapping, need to be defined in an NFT contract to save NFT-related data. The most important one is owners. This mapping stores the mapping relationship between the owner address of the NFT corresponding to a tokenID, which determines the ownership of the NFT. Transferring is a modification of the ownership. Since this is a data structure that can be boundless in theory, it needs to be avoided as much as possible. Therefore, the official recommendation is to use whether there is a boundless data structure as the standard for sharding. That is, when there is a similar data storage requirement, the master-slave contract paradigm is used instead, and the data corresponding to each key is managed by creating a sub-contract. And the master contract manages global parameters or helps process internal information interactions between sub-contracts.
This means that NFTs in TON also need to be designed with a similar architecture. Each NFT is an independent sub-contract that stores exclusive data such as owner address and metadata, and manages global data such as NFT name, symbol, total supply, etc. through a master contract.
After clarifying the architecture, the next step is to address the needs of core functions. Since the master-slave contract approach is adopted, it is necessary to clarify which functions are carried by the main contract and which functions are carried by the sub-contract, and what internal information is communicated between the two. At the same time, when an execution error occurs, how to roll back the previous data. Usually, before developing a complex large-scale project, it is necessary to use a class diagram to clarify the information flow between each other and carefully think about the rollback logic after the internal call fails. Of course, although the above NFT development is simple, similar verification can also be done.
Learn to develop TON smart contracts from source code
TON chose to design a C-like, statically typed language called Func as the smart contract development language, so let's learn how to develop TON smart contracts from the source code. I chose the NFT example in the TON official document to introduce it. Interested friends can check it out by themselves. In this case, a simple TON NFT example is implemented. Let's take a 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 above principles. First, let's take a look at the code of the main contract nft-collection:
This introduces the first knowledge point, how to persist data in TON smart contracts. We know that in Solidity, the persistent storage of data is automatically handled by EVM according to the type of parameters. Normally, the state variables of smart contracts will be automatically persisted according to the latest values after the execution ends, and developers do not need to consider this process. But this is not the case in Func. Developers need to implement the corresponding processing logic themselves. This situation is a bit similar to the process of considering GC in C and C++, but other new development languages usually automate this part of the logic. Let's look at the code. First, we introduce some required libraries. Then we see the first function load_data, which is used to read the data stored persistently. Its logic is to first return the persistent contract storage cell through get_data. Note that this is implemented by the standard library stdlib.fc. Generally, some of its functions can be used as system functions.
The return value type of this function is cell, which is the cell type in TVM. In the previous introduction, we have learned that all persistent data in the TON blockchain is stored in the cell tree. Each cell has 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. Cells store compactly encoded data. To obtain the specific plaintext data in them, you need to convert the cell to a type called slice. Cells can be converted to slice types through the begin_parse function, and then the data in the cell can be obtained by loading data bits from the slice and references to other cells. Note that this calling method in line 15 is a syntax sugar in func, which can directly call the second function of the return value of the first function. And finally load the corresponding data in the order of data persistence. Note that this process is different from solidity, and it is not called according to hashmap, so the calling order cannot be messed up.
In the save_data function, the logic is similar, except that this is a reverse process, which introduces the next knowledge point, a new type builder, which is the type of cell builder. Data bits and references to other cells can be stored in the builder, and then the builder can eventually be transformed into a new cell. First, create a builder through the standard function begin_cell, and store related functions in turn through the store related functions. Note that the calling order above and the storage order here need to be consistent. Finally, the new cell construction is completed through end_cell. At this time, the cell is managed in memory, and finally through the outermost set_data, the persistent storage of the cell can be completed.
Next, let's look at the business-related functions. First, we need to introduce the next knowledge point, how to create a new contract through a contract, which will be frequently used in the master-slave architecture just introduced. We know that in TON, calls between smart contracts are implemented by sending internal messages. This is implemented through a function called send_raw_message. Note that the first parameter is the cell encoded with the message, and the second parameter is the flag, which is used to indicate the difference in the execution method of the transaction. Different execution methods for sending internal messages are set in TON. There are currently 3 message modes and 3 message flags. A single mode can be combined with multiple (perhaps no) flags to obtain the desired mode. Combining just means filling in the sum of their values. The following is a description table of Modes and Flags:
Then let's look at the first main function, deploy_nft_item. As the name suggests, this is a function used to create or mint a new NFT instance. After some operations to encode a msg, the internal contract is sent through send_raw_message, and the flag 1 sending flag is selected, and only the fee specified in the encoding is used as the gas fee for this execution. After the above introduction, it is easy to realize that this encoding rule should correspond to the way to create a new smart contract. Let's take a look at how it is implemented.
Let's look directly at line 51. The two functions above are auxiliary functions for generating the information required for the message, so we will look at it later. This is an encoding process for creating an internal message of a smart contract. Some of the numbers in the middle are actually some identification bits, which are used to illustrate the needs of the internal message. Here we need to introduce the next knowledge point. TON chose a binary language called TL-B to describe the execution mode of the message, and implement internal messages with certain specific functions according to the setting of different mark bits. The two most easily thought of usage scenarios are new contract creation and deployed contract function calls. The method of line 51 corresponds to the former, creating a new nft item contract, which is mainly specified by lines 55, 56, and 57. First of all, the long string of numbers in line 55 is a series of identification bits. Note that the first input parameter of store_uint is the value, and the second is the bit length. The last three flag bits determine that the internal message is created by the contract, and the corresponding binary value is 111 (4+2+1 in decimal). The first two indicate that the message will be accompanied by StateInit data, which is the source code of the new contract and the data required for initialization. The latter flag indicates that the internal message is attached, that is, it is hoped to execute the relevant logic and the required parameters. Therefore, you will see that the three bits of data are not set in the 66th line of code, indicating a function call to the deployed contract. The specific encoding rules can be viewed here.
Then the encoding rules of StateInit correspond to the 49th line of code, which is calculated by calculate_nft_item_state_init. Note that the encoding of stateinit data also follows an established TL-B encoding rule. In addition to some flag bits, it mainly involves two parts: new contract code and initialization data. The encoding order of data needs to be consistent with the storage order of the persistent cell specified by the new contract. In line 36, we can see that the initialization data has item_index, which is similar to the tokenId in ERC721, and the current contract address returned by the standard function my_address, which is 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 pre-calculate their generated addresses, which is similar to the create2 function in Solidity. In TON, the generation of new addresses consists of two parts, the workchain identifier and the hash value of stateinit. The former, as we have already known in the previous introduction, needs to be specified in order to correspond to the infinite sharding architecture of TON, and is currently a unified value. It is obtained by the standard function workchain. The latter is obtained by the standard function cell_hash. So back to this example, calculate_nft_item_address is the function that pre-calculates the address of the new contract. The generated value is encoded into message in line 53 as the receiving address of the internal message. nft_content corresponds to the initialization call of the created contract, and the specific implementation will be introduced in the next article.
As for send_royalty_params, it needs to be a response to an internal message of a read-only request. In the previous introduction, we emphasized that in TON, internal messages not only contain operations that may modify data, but also read-only operations need to be implemented in this way. Therefore, this contract is such an operation. First of all, it is worth noting that line 67 represents the mark of the requester callback function after responding to the request. Write it down as the returned data, which are 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 entrances, named recv_internal and recv_external. The former is the unified call entrance for all internal messages, and the latter is the unified call entrance for all external messages. Developers need to use a switch-like method to respond to different requests according to the different flags specified by the message according to the needs within the function. The flags here are the callback function flags in line 67 above. Back to this example, first check the empty position of the message, and then parse the information in the message separately. First, parse the sender_address in line 83. This parameter will be used for subsequent permission checks. Note that the ~ operator here is another syntax sugar. I won't expand it here. Next, parse the op operation flag, and then process the corresponding requests according to different flags. The above functions are called separately according to certain logic. For example, respond to requests for royalty parameters, or cast new nfts, and increment the global index.
The next knowledge point corresponds to line 108. I believe everyone can also know the processing logic of this function through the name. Similar to the require function in Solidity, Func uses the standard function throw_unless to throw an exception. The first input parameter is the error code, and the second is to check the bit Boolean value. If it is false, an exception is thrown with the error code. In this line, equal_slices is used to determine whether the sender_address parsed above is equal to the owner_address stored persistently by the contract, and the permission is determined.
Finally, in order to make the code structure clearer, a series of auxiliary functions that help obtain persistent information are started. I will not introduce them here. Developers can refer to this structure to develop their own smart contracts.
DApp development in the TON ecosystem is really interesting, and it is very different from the development paradigm of EVM, so I will introduce how to develop DApp in TON Chain through a series of articles.