Author: @Web3Mario (https://x.com/web3_mario)
Abstract: Following the previous article about the introduction of TON technology, I have studied the official TON development documents 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 point. First, let us understand the difference 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 encrypted assets, and each asset is unique, that is, there are certain exclusive characteristics. ERC-721 is a general development paradigm for this type of asset. Let's take a look at 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, the tokenId to be transferred rather than the amount needs to be entered in the transfer interface. 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 programming, 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, everything is different. There are two core reasons for this difference:
In TON, data storage is based on Cell, and the Cell of the same account is implemented through a directed acyclic graph. This means that the data that needs to be stored cannot grow indefinitely, because for a directed acyclic graph, the query cost is determined by the depth of the data. When the depth is infinitely extended, the query cost may be too high, causing the contract to fall into a deadlock problem.
In pursuit of 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 resulted in 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 and read-only 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. Transfer 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 the existence of a boundless data structure as the standard for sharding. That is, when there is a similar data storage requirement, it is replaced by the master-slave contract paradigm, and the data corresponding to each key is managed by creating a sub-contract. And the master contract manages global parameters or helps to handle 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 the owner's address and metadata, and manages global data such as NFT name, symbol, total supply, etc. through a main 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 has chosen to design a C-like, statically typed language called Func as a smart contract development language. Next, 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 the smart contract will be automatically persisted according to the latest value after the execution, 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 somewhat 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 take a look at the code. First, we introduce some required libraries, and then we see that the first function load_data is used to read the persistently stored data. 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. Usually, some of these 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 already know 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 type through the begin_parse function, and then the data in the cell can be obtained by loading data bits and references to other cells from the slice. Note that this calling method in line 15 of the code is a syntactic 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 the 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 in the above text must be consistent with the storage order here. 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 after the message is encoded, and the second parameter is the flag, which is used to indicate the difference in the execution method of the transaction. Different internal message sending execution methods 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. Combination simply means filling in the sum of their values. The following is a description table of Modes and Flags:
So 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, it encodes a msg, sends the internal contract through send_raw_message, and selects the flag 1 sending flag, and only uses the fee specified in the encoding 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 method of the message, and implemented internal messages with certain specific functions according to the setting of different flag 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 bit is 111 (4+2+1 in decimal), of which 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 next flag indicates the internal message attachment, that is, the related logic and required parameters are expected to be executed. Therefore, you will see that the three bits of data are not set in the 66th line of code, which indicates a function call to the deployed contract. The specific encoding rules can be found here.
Then the encoding rule of StateInit corresponds to 49 lines 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 markers, 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 point of knowledge 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 identification bit and the hash value of stateinit. We have already known in the previous introduction that the former needs to be specified in order to correspond to the infinite sharding architecture of TON, and it 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. And the generated value is encoded into message in line 53 as the receiving address of the internal message. And 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 specifically emphasized that in TON, internal messages not only contain operations that may modify data, but read-only operations also 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's 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 flag here is the callback function flag in line 67 above. Back to this example, first check the message for empty positions, 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 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 a check 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 persistently stored in the contract to make a permission judgment.
Finally, in order to make the code structure clearer, a series of auxiliary functions are created to help obtain persistent information. 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. Therefore, I will introduce how to develop DApp in TON Chain through a series of articles. Let's learn together and seize this opportunity. You are also welcome to interact with me on Twitter to collide with some new and interesting DApp ideas and develop together.