With the continuous development of blockchain technology, Non-Fungible Tokens (NFTs) have become a hot topic in the digital asset sector. The Open Network (TON), as an emerging blockchain platform, has attracted many developers with its efficiency and scalability. This article will guide you from beginner to practice, teaching you how to write NFT-item contracts using the FunC language on the TON blockchain.
Introduction
FunC is a functional programming language used for the TON blockchain; it is concise and efficient, making it ideal for developing smart contracts. In this article, we will interpret a segment of NFT-item contract code to introduce you to the basic syntax and programming techniques of FunC, taking your first steps in writing NFT contracts.
Prerequisites
Before you start writing contracts, make sure you have installed the TON development environment and are familiar with the following basic concepts:
- The basic principles of the TON blockchain
- The concept of Non-Fungible Tokens (NFTs)
- Basic knowledge of smart contracts
Now, let’s officially begin writing the NFT-item contract.
Analyzing the Contract Structure
Included Files
When writing an NFT-item contract, you need to include the following files:
stdlib.fc
: The standard library file, containing basic data structures and operations.op-codes.fc
: Operation code definitions, used to define message operation types.utils.fc
: Utility functions.error.fc
: Macros and functions related to error handling.constants.fc
: Definitions of constants, which may include network rates and storage fees.
These files provide the infrastructure needed to write contracts.
Global Variables
In the contract, we define the following global variables:
data::index
: The index number of the NFT, used to uniquely identify an NFT.init?
: A boolean value indicating whether the NFT has been initialized.data::collection_address
: The address of the NFT collection, used to associate the NFT with the collection.data::owner_address
: The current owner’s address of the NFT, representing the ownership of the NFT.data::content
: The content of the NFT, which may be a cell containing NFT data.
Functions and Procedures
Here are some key functions and procedures defined in the contract:
load_data()
: Load NFT-related information from the contract’s data area.store_data()
: Store NFT-related information in the contract’s data area.send_msg()
: Send a message to a specified address on the blockchain, which can carry a certain amount and operation code.transfer_ownership()
: Transfer ownership of the NFT to a new address, allowing for the transfer of remaining balances to the new owner or response address.recv_internal()
: Process received internal messages, executing different logic based on the operation code.
Writing Contract Code
Next, we will explain how to write these functions and procedures step by step.
Loading and Storing Data
() load_data() impure inline {
slice ds = get_data().begin_parse();
data::index = ds~load_uint(64);
data::collection_address = ds~load_msg_addr();
init? = false;
if (ds.slice_bits() > 0) {
init? = true;
data::owner_address = ds~load_msg_addr();
data::content = ds~load_ref();
}
}
() store_data() impure inline {
set_data(begin_cell()
.store_uint(data::index, 64)
.store_slice(data::collection_address)
.store_slice(data::owner_address)
.store_ref(data::content)
.end_cell());
}
In the load_data()
and store_data()
functions, we implement the functionality to load and store NFT information from and to the data area, respectively. These functions are frequently called during contract execution.
Sending Messages
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op, 32)
.store_uint(query_id, 64);
if (~ builder_null?(payload)) {
msg = msg.store_builder(payload);
}
send_raw_message(msg.end_cell(), send_mode);
}
The send_msg()
function is used to send messages on the blockchain. By specifying the recipient address, amount, operation code, query ID, message payload, and send mode, we can interact with other contracts or accounts.
Transferring Ownership
() transfer_ownership(int my_balance, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline {
throw_unless(error::not_owner, equal_slices_bits(sender_address, data::owner_address));
slice new_owner_address = in_msg_body~load_msg_addr();
force_chain(new_owner_address);
slice response_destination = in_msg_body~load_msg_addr();
in_msg_body~load_int(1); ;; this nft don't use custom_payload
int forward_amount = in_msg_body~load_coins();
int rest_amount = my_balance - const::min_tons_for_storage;
if (forward_amount) {
rest_amount -= (forward_amount + fwd_fees);
}
int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
if (need_response) {
rest_amount -= fwd_fees;
}
throw_unless(error::not_enough_balance, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
if (forward_amount) {
builder payload = begin_cell().store_slice(data::owner_address).store_slice(in_msg_body);
send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, payload, 1); ;; paying fees, revert on errors
}
if (need_response) {
force_chain(response_destination);
send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors
}
data::owner_address = new_owner_address;
store_data();
}
The transfer_ownership()
function is the core of implementing NFT ownership transfer. During the transfer, we need to verify that the sender is the current owner and handle the transfer of remaining balances and the sending of response messages.
Processing Internal Messages
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
return ();
}
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) { ;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs
load_data();
if (~ init?) {
throw_unless(error::not_collection_owner, equal_slices_bits(data::collection_address, sender_address));
data::owner_address = in_msg_body~load_msg_addr();
data::content = in_msg_body~load_ref();
store_data();
if (in_msg_body.slice_data_empty?() == false) {
var forward_amount = in_msg_body~load_coins();
if (forward_amount) {
builder payload = begin_cell().store_slice(data::collection_address).store_slice(in_msg_body);
send_msg(data::owner_address, forward_amount, op::ownership_assigned(), 0, payload, 3); ;; paying fees, ignore errors
}
}
return ();
}
int op = in_msg_body~load_uint(32);
int query_id = in_msg_body~load_uint(64);
if (op == op::transfer()) {
transfer_ownership(my_balance, sender_address, query_id, in_msg_body, fwd_fee);
return ();
}
if (op == op::get_static_data()) {
builder payload = begin_cell().store_uint(data::index, 256).store_slice(data::collection_address);
send_msg(sender_address, 0, op::report_static_data(), query_id, payload, 64); ;; carry all the remaining value of the inbound message
return ();
}
throw(0xffff);
}
The recv_internal()
function is used to process received internal messages. Depending on the message’s operation code, it executes the corresponding logic, such as transferring ownership or returning static data.
Implementing GET Method
(int, int, slice, slice, cell) get_nft_data() method_id {
load_data();
return (init?, data::index, data::collection_address, data::owner_address, data::content);
}
The get_nft_data()
method provides an interface to obtain the initialization status, index number, collection address, owner address, and content of the NFT.
Error Handling
Error handling is crucial when writing contracts. Here are some common error handling methods:
throw_unless()
: Throws an error if a condition is not met.throw()
: Throws an error code.
These error handling functions help ensure that the contract can safely stop executing and provide error prompts when unexpected situations arise.
Points to Note
When writing contracts, here are some important points to keep in mind:
force_chain()
: Ensures that subsequent messages are sent to the specified address.muldiv()
: Used for calculating fees, especially when transferring funds.preload_uint()
: Preloads and parses uint values, used to check if a response is needed.
Complete Contract Example
Below is a simplified complete example of an NFT-item contract, combining the parts mentioned above into a full contract framework:
#include "imports/stdlib.fc";
#include "imports/op-codes.fc";
#include "imports/utils.fc";
#include "imports/error.fc";
#include "imports/constants.fc";
global int data::index;
global int init?;
global slice data::collection_address;
global slice data::owner_address;
global cell data::content;
() load_data() impure inline {
// Implement data loading logic
}
() store_data() impure inline {
// Implement data storing logic
}
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
// Implement message sending logic
}
() transfer_ownership(int my_balance, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline {
// Implement ownership transfer logic
}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
// Implement internal message processing logic
}
(int, int, slice, slice, cell) get_nft_data() method_id {
// Implement logic to get NFT data
}
In this contract, the recv_internal()
function is the entry point of the contract. Each function’s specific implementation needs to be completed according to the contract’s business logic.
Conclusion
Through the explanation in this article, you should now have a basic understanding of how to write NFT-item contracts in FunC for the TON blockchain. From including the necessary files, defining global variables, to writing key functions and procedures, and handling errors, each step is an important part of building a robust and secure NFT contract.
Of course, writing a complete NFT contract requires more practice and attention to detail. You can improve your contract writing skills by reading the TON official documentation, participating in community discussions, and continuously testing and optimizing your contracts. Remember, security and user experience are two key factors in smart contract development; always prioritize them. Wishing you success in writing outstanding NFT contracts on the TON blockchain!