Following the previous article introducing TON technology, I have delved into the official TON development documentation recently. I feel that it is somewhat challenging to learn, as the current content seems more like internal development documentation, which is not very friendly to new developers. Therefore, I have tried to organize a series of articles about TON Chain project development based on my own learning experience, hoping to help everyone quickly get started with TON DApp development. Corrections to any errors in the text are welcome, let’s learn together.
What are the differences between developing NFT on EVM and developing NFT on TON Chain
Issuing an FT or NFT is usually the most basic requirement for DApp developers. So I also use this as a starting point for learning. First, let’s understand the differences between developing an NFT in the EVM technology stack and on the TON Chain. EVM-based NFTs typically choose to inherit the ERC-721 standard. NFT refers to a type of indivisible encrypted asset, and each asset has uniqueness, that is, it has certain exclusive characteristics. ERC-721 is a common development paradigm for this type of asset. Let’s take a look at what functions and information a typical ERC-721 contract needs to implement. The figure below shows an ERC-721 interface. As you can see, unlike FT, the transfer interface requires the input of the tokenId to be transferred instead of the quantity. This tokenId is the most basic manifestation of the uniqueness of the NFT asset. Of course, to carry more properties, a metadata is usually recorded for each tokenId. This metadata is an external link that stores other expandable data of the NFT, such as a link to a PFP image, certain attribute names, etc.
For developers familiar with Solidity or familiar with object-oriented development, implementing such a smart contract is an easy task, as long as the data types needed in the contract are defined, such as some key mapping relationships, and the corresponding logic for modifying these data according to the required functions are developed, an NFT can be implemented.
However, in the TON Chain, this all becomes quite different, with two different core reasons:
In TON, the storage of data is based on Cells, and the Cells of the same account are implemented through a directed acyclic graph. This leads to the fact that the data that needs to be persisted cannot grow boundlessly, because for a directed acyclic graph, the query cost is determined by the data depth. When the depth extends indefinitely, it may result in excessively high query costs, leading to contract deadlock issues.
In order to pursue high concurrent performance, TON abandoned the architecture of serial execution and adopted a development paradigm specifically designed for parallelism, the Actor model, to reconstruct the execution environment. This has led to an impact, that smart contracts can only be asynchronously invoked between each other through the sending of so-called internal messages. Note that both state-modifying and read-only invocations need to follow this principle. In addition, careful consideration needs to be given to how to handle data rollback in case of failure during asynchronous invocation.
Of course, there has been a detailed discussion of other technical differences in the previous article, and this article hopes to focus on smart contract development, so it will not be discussed in depth. The above two design principles have made a big difference between smart contract development in TON and EVM. In the previous discussion, we know that a NFT contract needs to define some mapping relationships, namely mapping, to store data related to NFT. The most important one is the owners, which stores the mapping relationship between a tokenID and the owner address of the NFT, determining the ownership of the NFT. Transfers modify this ownership. Since theoretically this is a data structure without boundaries, it should be avoided as much as possible. Therefore, the official recommendation is to use the existence of a boundary-less data structure as the criterion for sharding. That is, when there is a similar data storage requirement, it is replaced by the paradigm of master-slave contracts, and each key corresponds to data is managed by creating a sub-contract. The master contract manages global parameters or helps to process internal information interaction between sub-contracts.
This also means that NFTs in TON need to be designed with a similar architecture, where each NFT is an independent sub-contract, storing exclusive data such as owner address, metadata, etc., and managed by a master contract for global data, such as NFT name, symbol, total supply, etc.
Once the architecture is clear, the next step is to address the requirements of the core functions. Due to the use of this master-slave contract approach, it is necessary to clarify which functions are carried out by the master contract and which functions are carried out by the sub-contract, and how the internal information is communicated between the two. At the same time, when execution errors occur, how to rollback the previous data. In general, before developing complex large projects, it is necessary to create a class diagram and clarify the flow of information between each other, and carefully consider the rollback logic after internal call failures. Although the NFT development mentioned above is simple, similar validations can also be done.
Learn to develop TON smart contracts from the source code
TON has chosen to design a C-like, statically-typed language named Func as the smart contract development language. Next, let’s learn how to develop TON smart contracts from the source code. I have chosen the NFT example from the official TON documentation to introduce. Interested friends can look it up on their own. In this case, a simple TON NFT example is implemented. Let’s take a look at the contract structure, which consists of two functional contracts and three necessary libraries.
The 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 persistently store data in TON smart contracts. We know that in Solidity, the persistence of data is automatically processed by the EVM according to the type of the parameter. In general, the state variables of smart contracts will be automatically persisted according to the latest values after the execution is completed, and developers do not need to consider this process. However, the situation is not the same in Func. Developers need to implement the corresponding processing logic by 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 the first function load_data used to read the 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, and some of its functions can usually be used as system functions.
The return type of this function is cell, which is a cell type in TVM. As introduced earlier, we already know that all persistent data in the TON Blockchain is stored in the cell tree. Each cell can have up to 1023 bits of arbitrary data and up to four references to other cells. In TVM, cell is used as memory. The data saved in the cell is tightly encoded, and to obtain specific plaintext data, the cell needs to be converted to a type called slice. The cell can be transformed into a slice type through the begin_parse function, and then the 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 of the code is a syntax sugar in a 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 does not rely on hashmap calls, so the calling order cannot be changed.
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 the cell builder. Data bits and references to other cells can be stored in the builder, which can then be eventually converted into new cells. First, create a builder through the standard function begin_cell, and store the related functions through the store-related functions in turn, noting that the call order above needs to be consistent with the storage order here. Finally, the new cell is built through end_cell, and the cell is managed in memory, and finally the persistent storage of the cell can be completed through the outermost set_data.
Next, let’s take a look at the business-related functions. First, we need to introduce a knowledge point, how to create a new contract through a contract, which will be frequently used in the master-slave architecture introduced earlier. We know that the invocation between smart contracts in TON is achieved by sending internal messages. This is implemented through a function called send_raw_message. Note that the first parameter is the encoded cell of the message, and the second parameter is a flag that indicates the difference in the execution mode of the transaction. In TON, different execution modes for sending internal messages have been set, currently there are 3 message modes and 3 message flags. A single mode can be combined with multiple (perhaps none) flags to obtain the desired mode. Combining simply means filling in the sum of their values. The following table provides descriptions of Modes and Flags:
So let’s take a look at the first major function, deploy_nft_item, as the name suggests, this is a function for creating or minting new NFT instances. After encoding a msg through some operations, it is sent to the internal contract via send_raw_message, choosing the sending flag 1, and using only the specified fee in the encoding as the gas fee for this execution. Through the previous introduction, it is easy to realize that this encoding rule should correspond to the way of creating a new smart contract. Let’s see how it is implemented specifically.
Let’s look directly at line 51, the above two functions are auxiliary functions for generating the information required for the message, so we will look at it later, this is a coding process for creating internal messages for smart contracts, some numbers in the middle are actually some identification bits, used to explain the needs of the internal message, here to introduce the next knowledge point, TON chose a binary language called TL-B to describe the execution of the message, And according to the internal messages that set different marker bits to achieve some specific functions, the two most common use cases are the creation of a new contract and the function call of the deployed contract. This method of line 51 corresponds to the former, creating a new NFT item contract, and this is mainly specified by lines 55, 56, and 57. First of all, the 55 lines of this large string of numbers is a series of identification bits, note that the first input parameter of store_uint is the numeric value, the second is the bit length, which determines that the internal message is created by the contract is the last three tag bits, and the corresponding binary value is 111 (decimal is 4+ 2 + 1), where 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 tag indicates that the internal message is attached, i.e. the logic that you want to execute and the parameters you need. So you’ll see that the three digits are not set in line 66, which indicates a function call to the deployed contract. The specific encoding rules can be found here.
So the encoding rule of StateInit corresponds to line 49 of the code, calculated through calculate_nft_item_state_init. Note that the encoding of stateinit data also follows a certain TL-B encoding rule. Apart from some flag bits, it mainly involves two parts: the new contract code and the initialization data. The encoding order of the data needs to be consistent with the storage order of the specified persistent cell of the new contract. At line 36, it can be seen that the initialization data includes item_index, similar to the tokenId in ERC 721, and the current contract address returned by the standard function my_address, which is the 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 be pre-computed after their generated Address, which is similar to the create 2 function in Solidity, where the generation of new Address in TON is composed of two parts, the workchain identifier bit and the hash value of stateinit, the former we already knew in the previous introduction needs to be specified in order to correspond to the TON infinite Sharding architecture, and is currently a uniform value. Obtained by the standard function workchain. The latter is obtained by the standard function cell_hash. So going back to the example, calculate_nft_item_address is the function that precomputes the Address of the new contract. And encode the generated value in line 53 into message as the receiving Address for that internal message. NFT_content corresponds to the initialization call to the created contract, which will be explained in the next article intermediary
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 specially emphasized that in TON, internal messages not only include operations that may modify data, but also read-only operations need to be implemented in this way. Therefore, this contract is for such operations. First of all, it is worth noting that line 67 represents the tag of the callback function to the requester after responding to the request, which is the data returned. They 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 entry points, named recv_internal and recv_external. The former is the unified invocation entry point for all internal messages, while the latter is the unified invocation entry point for all external messages. Developers need to respond to different requests based on the different flags specified by the message, similar to using a switch statement, within the function according to their needs. The flags mentioned in line 67 are the callback function flags. Now let’s go back to the example. First, perform a null check on the message. If it passes, parse the information in the message separately. In line 83, parse and obtain the sender_address, which will be used for subsequent permission checks. Note the ~ operator here, which is another syntax sugar. Let’s not go into detail about it for now. Next, parse the op operation flag, and then process the corresponding request based on different flags. For example, respond to requests for the royalty parameter or mint a new NFT and increment the global index.
The next knowledge point corresponds to line 108. I believe everyone can understand the processing logic of this function through the naming. Similar to the require function in Solidity, the Func function throws an exception through the standard function throw_unless. The first parameter is the error code, and the second parameter is the boolean value of the check bit. If the bit is false, an exception is thrown with the error code attached. In this line, equal_slices is used to determine whether the sender_address parsed above is equal to the owner_address persisted in this contract, for permission judgment.
Finally, in order to make the code structure clearer, a series of auxiliary functions were idle to help obtain persistent information. I won’t go into details here. Developers can refer to this structure to develop their own smart contracts.
Developing DApps in the TON ecosystem is an interesting task, as it differs greatly from the development paradigm of EVM. Therefore, I will be introducing how to develop DApps on the 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, and exchange some new and interesting DApp ideas for development.
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
TON Project Development Tutorial (1): How to Create an NFT on TON Chain from the Source Code Perspective
Original author: @Web3 Mario(_mario)
Following the previous article introducing TON technology, I have delved into the official TON development documentation recently. I feel that it is somewhat challenging to learn, as the current content seems more like internal development documentation, which is not very friendly to new developers. Therefore, I have tried to organize a series of articles about TON Chain project development based on my own learning experience, hoping to help everyone quickly get started with TON DApp development. Corrections to any errors in the text are welcome, let’s learn together.
What are the differences between developing NFT on EVM and developing NFT on TON Chain
Issuing an FT or NFT is usually the most basic requirement for DApp developers. So I also use this as a starting point for learning. First, let’s understand the differences between developing an NFT in the EVM technology stack and on the TON Chain. EVM-based NFTs typically choose to inherit the ERC-721 standard. NFT refers to a type of indivisible encrypted asset, and each asset has uniqueness, that is, it has certain exclusive characteristics. ERC-721 is a common development paradigm for this type of asset. Let’s take a look at what functions and information a typical ERC-721 contract needs to implement. The figure below shows an ERC-721 interface. As you can see, unlike FT, the transfer interface requires the input of the tokenId to be transferred instead of the quantity. This tokenId is the most basic manifestation of the uniqueness of the NFT asset. Of course, to carry more properties, a metadata is usually recorded for each tokenId. This metadata is an external link that stores other expandable data of the NFT, such as a link to a PFP image, certain attribute names, etc.
For developers familiar with Solidity or familiar with object-oriented development, implementing such a smart contract is an easy task, as long as the data types needed in the contract are defined, such as some key mapping relationships, and the corresponding logic for modifying these data according to the required functions are developed, an NFT can be implemented.
However, in the TON Chain, this all becomes quite different, with two different core reasons:
Of course, there has been a detailed discussion of other technical differences in the previous article, and this article hopes to focus on smart contract development, so it will not be discussed in depth. The above two design principles have made a big difference between smart contract development in TON and EVM. In the previous discussion, we know that a NFT contract needs to define some mapping relationships, namely mapping, to store data related to NFT. The most important one is the owners, which stores the mapping relationship between a tokenID and the owner address of the NFT, determining the ownership of the NFT. Transfers modify this ownership. Since theoretically this is a data structure without boundaries, it should be avoided as much as possible. Therefore, the official recommendation is to use the existence of a boundary-less data structure as the criterion for sharding. That is, when there is a similar data storage requirement, it is replaced by the paradigm of master-slave contracts, and each key corresponds to data is managed by creating a sub-contract. The master contract manages global parameters or helps to process internal information interaction between sub-contracts.
This also means that NFTs in TON need to be designed with a similar architecture, where each NFT is an independent sub-contract, storing exclusive data such as owner address, metadata, etc., and managed by a master contract for global data, such as NFT name, symbol, total supply, etc.
Once the architecture is clear, the next step is to address the requirements of the core functions. Due to the use of this master-slave contract approach, it is necessary to clarify which functions are carried out by the master contract and which functions are carried out by the sub-contract, and how the internal information is communicated between the two. At the same time, when execution errors occur, how to rollback the previous data. In general, before developing complex large projects, it is necessary to create a class diagram and clarify the flow of information between each other, and carefully consider the rollback logic after internal call failures. Although the NFT development mentioned above is simple, similar validations can also be done.
Learn to develop TON smart contracts from the source code
TON has chosen to design a C-like, statically-typed language named Func as the smart contract development language. Next, let’s learn how to develop TON smart contracts from the source code. I have chosen the NFT example from the official TON documentation to introduce. Interested friends can look it up on their own. In this case, a simple TON NFT example is implemented. Let’s take a look at the contract structure, which consists of two functional contracts and three necessary libraries.
The 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 persistently store data in TON smart contracts. We know that in Solidity, the persistence of data is automatically processed by the EVM according to the type of the parameter. In general, the state variables of smart contracts will be automatically persisted according to the latest values after the execution is completed, and developers do not need to consider this process. However, the situation is not the same in Func. Developers need to implement the corresponding processing logic by 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 the first function load_data used to read the 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, and some of its functions can usually be used as system functions.
The return type of this function is cell, which is a cell type in TVM. As introduced earlier, we already know that all persistent data in the TON Blockchain is stored in the cell tree. Each cell can have up to 1023 bits of arbitrary data and up to four references to other cells. In TVM, cell is used as memory. The data saved in the cell is tightly encoded, and to obtain specific plaintext data, the cell needs to be converted to a type called slice. The cell can be transformed into a slice type through the begin_parse function, and then the 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 of the code is a syntax sugar in a 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 does not rely on hashmap calls, so the calling order cannot be changed.
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 the cell builder. Data bits and references to other cells can be stored in the builder, which can then be eventually converted into new cells. First, create a builder through the standard function begin_cell, and store the related functions through the store-related functions in turn, noting that the call order above needs to be consistent with the storage order here. Finally, the new cell is built through end_cell, and the cell is managed in memory, and finally the persistent storage of the cell can be completed through the outermost set_data.
Next, let’s take a look at the business-related functions. First, we need to introduce a knowledge point, how to create a new contract through a contract, which will be frequently used in the master-slave architecture introduced earlier. We know that the invocation between smart contracts in TON is achieved by sending internal messages. This is implemented through a function called send_raw_message. Note that the first parameter is the encoded cell of the message, and the second parameter is a flag that indicates the difference in the execution mode of the transaction. In TON, different execution modes for sending internal messages have been set, currently there are 3 message modes and 3 message flags. A single mode can be combined with multiple (perhaps none) flags to obtain the desired mode. Combining simply means filling in the sum of their values. The following table provides descriptions of Modes and Flags:
So let’s take a look at the first major function, deploy_nft_item, as the name suggests, this is a function for creating or minting new NFT instances. After encoding a msg through some operations, it is sent to the internal contract via send_raw_message, choosing the sending flag 1, and using only the specified fee in the encoding as the gas fee for this execution. Through the previous introduction, it is easy to realize that this encoding rule should correspond to the way of creating a new smart contract. Let’s see how it is implemented specifically.
Let’s look directly at line 51, the above two functions are auxiliary functions for generating the information required for the message, so we will look at it later, this is a coding process for creating internal messages for smart contracts, some numbers in the middle are actually some identification bits, used to explain the needs of the internal message, here to introduce the next knowledge point, TON chose a binary language called TL-B to describe the execution of the message, And according to the internal messages that set different marker bits to achieve some specific functions, the two most common use cases are the creation of a new contract and the function call of the deployed contract. This method of line 51 corresponds to the former, creating a new NFT item contract, and this is mainly specified by lines 55, 56, and 57. First of all, the 55 lines of this large string of numbers is a series of identification bits, note that the first input parameter of store_uint is the numeric value, the second is the bit length, which determines that the internal message is created by the contract is the last three tag bits, and the corresponding binary value is 111 (decimal is 4+ 2 + 1), where 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 tag indicates that the internal message is attached, i.e. the logic that you want to execute and the parameters you need. So you’ll see that the three digits are not set in line 66, which indicates a function call to the deployed contract. The specific encoding rules can be found here.
So the encoding rule of StateInit corresponds to line 49 of the code, calculated through calculate_nft_item_state_init. Note that the encoding of stateinit data also follows a certain TL-B encoding rule. Apart from some flag bits, it mainly involves two parts: the new contract code and the initialization data. The encoding order of the data needs to be consistent with the storage order of the specified persistent cell of the new contract. At line 36, it can be seen that the initialization data includes item_index, similar to the tokenId in ERC 721, and the current contract address returned by the standard function my_address, which is the 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 be pre-computed after their generated Address, which is similar to the create 2 function in Solidity, where the generation of new Address in TON is composed of two parts, the workchain identifier bit and the hash value of stateinit, the former we already knew in the previous introduction needs to be specified in order to correspond to the TON infinite Sharding architecture, and is currently a uniform value. Obtained by the standard function workchain. The latter is obtained by the standard function cell_hash. So going back to the example, calculate_nft_item_address is the function that precomputes the Address of the new contract. And encode the generated value in line 53 into message as the receiving Address for that internal message. NFT_content corresponds to the initialization call to the created contract, which will be explained in the next article intermediary
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 specially emphasized that in TON, internal messages not only include operations that may modify data, but also read-only operations need to be implemented in this way. Therefore, this contract is for such operations. First of all, it is worth noting that line 67 represents the tag of the callback function to the requester after responding to the request, which is the data returned. They 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 entry points, named recv_internal and recv_external. The former is the unified invocation entry point for all internal messages, while the latter is the unified invocation entry point for all external messages. Developers need to respond to different requests based on the different flags specified by the message, similar to using a switch statement, within the function according to their needs. The flags mentioned in line 67 are the callback function flags. Now let’s go back to the example. First, perform a null check on the message. If it passes, parse the information in the message separately. In line 83, parse and obtain the sender_address, which will be used for subsequent permission checks. Note the ~ operator here, which is another syntax sugar. Let’s not go into detail about it for now. Next, parse the op operation flag, and then process the corresponding request based on different flags. For example, respond to requests for the royalty parameter or mint a new NFT and increment the global index.
The next knowledge point corresponds to line 108. I believe everyone can understand the processing logic of this function through the naming. Similar to the require function in Solidity, the Func function throws an exception through the standard function throw_unless. The first parameter is the error code, and the second parameter is the boolean value of the check bit. If the bit is false, an exception is thrown with the error code attached. In this line, equal_slices is used to determine whether the sender_address parsed above is equal to the owner_address persisted in this contract, for permission judgment.
Finally, in order to make the code structure clearer, a series of auxiliary functions were idle to help obtain persistent information. I won’t go into details here. Developers can refer to this structure to develop their own smart contracts.
Developing DApps in the TON ecosystem is an interesting task, as it differs greatly from the development paradigm of EVM. Therefore, I will be introducing how to develop DApps on the 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, and exchange some new and interesting DApp ideas for development.