Collection
The full source code is in nft-collection.fc file. The TEP-62 standard only requires the implementation of get‑methods:get_collection_data(), get_nft_address_by_index(int index), and get_nft_content(int index, cell individual_content). The TEP‑66 standard also adds the royalty_params() get‑method, a get_royalty_params message for requesting royalty parameters on‑chain, and a report_royalty_params message for the response containing these parameters.
All other behavior is implementation‑specific. This includes any kind of modifications to the logic of minting new items, managing ownership of the collection, changing metadata, and other features. The reference implementation, for example, implements the ownership management feature and two ways of minting items: singular and batched. Both of these are outside the scope of the standards but are common and needed in most cases. These features are optional and can be removed or replaced.
| Capability | Requirement |
|---|---|
get_collection_data (get-method) | Required by TEP-62 |
get_nft_address_by_index (get-method) | Required by TEP-62 |
get_nft_content (get-method) | Required by TEP-62, data format follows TEP-64 |
royalty_params (get-method) | Required by TEP-66 |
get_royalty_params (inbound internal message) | Required by TEP-66 |
report_royalty_params (outbound internal message) | Required by TEP-66 |
Deploy single item, op=1 (inbound internal message, owner-only) | Optional |
Deploy batch of items, op=2 (inbound internal message, owner-only) | Optional |
Change owner, op=3 (inbound internal message, owner-only) | Optional |
op means a 32‑bit operation code placed at the start of an internal message body. Contracts use it to dispatch messages to different handlers. The numeric values are local to this contract; they just need to be unique within its dispatcher. See the Messages overview for more background.
Storage
The storage scheme itself is not defined by these standards, so it can be arbitrary. The reference collection contract contains 5 fields:owner_address, next_item_index, content, nft_item_code, and royalty_params. Some of them are self‑explanatory, while content and royalty_params need additional explanation below.
Storage loading and saving is implemented with simple functions load_data and save_data:
content cell in this contract contains 2 cells in references: collection_content containing metadata of the collection itself, and common_content containing the common prefix for individual items’ metadata.
The royalty_params cell contains 3 values: royalty_factor which is a numerator, royalty_base which is a denominator, and royalty_address which is the address where royalties are sent.
Child contracts
The collection and item contracts implement a classic parent–child pattern, where the collection acts as the parent and items are children. Therefore, the collection implements functionality to deploy its children, and the contract keeps thenft_item_code field in storage.
The contract uses helper functions to compose StateInit — roughly, the package that holds a contract’s initial code and data used to derive its address and deploy it — calculate addresses, and send deploy messages for NFT items.
First, calculate_nft_item_state_init composes a StateInit cell of an NFT item with a given item_index. It composes a data cell following the expected storage schema of the item contract. Then the data and code cells are placed in a final StateInit cell following the format required by the TON blockchain. It uses the store_dict builder to encode optional references; it does not store dictionaries. This can be replaced with a more self‑explanatory store_maybe_ref builder which does the same thing. It stores a single bit 0 in a builder if the cell is null, otherwise bit 1 and a cell as a reference.
calculate_nft_item_address function takes state_init composed by calculate_nft_item_state_init, along with wc (the workchain for the resulting address). It calculates the address by hashing the StateInit and composing a MsgAddressInt slice according to the format required by the TON blockchain.
deploy_nft_item builds the StateInit, derives the address, and sends the deploy message. It calls calculate_nft_item_state_init to compose the StateInit, then calculate_nft_item_address with workchain() to derive the contract’s workchain (reference implementation uses 0 for BaseChain), and finally composes and sends the deploy message with the StateInit and content (using send_raw_message).
.store_uint(0x18, 6) and .store_uint(0x10, 6):
-
These 6 leading bits encode the start of
CommonMsgInfofor an internal message:int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress …(see message layout). Concretely, the 6 bits are:tag=0— selectsint_msg_info$0(internal message info).ihr_disabled— set to1to disable IHR (Instant Hypercube Routing). IHR is a legacy delivery mode and is not used in TON; contracts should always keep it disabled. See Hypercube routing.bounce— if1, the message will bounce back on errors during compute/action phases; if0, it will not bounce. We usebounce=1(0x18) for deploys to child items so the collection gets its funds back on failure, andbounce=0(0x10) for royalty replies to avoid bounces and ensure crediting wallets/uninitialized addresses. See Bounce phase.bounced— set to0in outbound messages you build. This bit is meaningful only for inbound messages: when the blockchain generates a bounce, the inbound copy hasbounced=1. Validators construct such bounce messages themselves; your outbound value does not make a message “bounced”. In normal sends, keep it0.src— we serializeaddr_none(the00tag). Validators replace it with the current contract address during the action phase, so you don’t need to spend bits storing the full source address. Seesend_raw_message.
.store_uint(0x18, 6)→011000:ihr_disabled=1,bounce=1(bounceable),bounced=0,src=addr_none. Used for deploy messages to child items..store_uint(0x10, 6)→010000:ihr_disabled=1,bounce=0(non‑bounceable),bounced=0,src=addr_none. Used for replies likereport_royalty_paramsto avoid bounces.
.store_slice(nft_address)), followed byvalue, fees, timestamps, and optionallyStateInitand body, as detailed below.
.store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1):
-
The right‑hand argument (bit width) is the total size of the remaining header fields we serialize in one shot, in this order (mapped to the TL‑B schema):
1—ExtraCurrencyCollectionpresence bit insidevalue:CurrencyCollection(seeCurrencyCollectionand the credit phase).4— length prefix forihr_fee:Grams(encoded asVarUInteger 16: 4‑bit lengthlenthenlenbytes; writing0000here encodes zero). SeeLDGRAMS.4— length prefix forfwd_fee:Grams(sameVarUInteger 16rule;0000means zero). SeeLDGRAMS.64—created_lt:uint64fromCommonMsgInfo(we write0; validators rewrite the actual logical time). See message layout.32—created_at:uint32fromCommonMsgInfo(we write0; validators rewrite the block Unix time). See message layout.1—init:(Maybe (Either StateInit ^StateInit))presence flag (0 = noStateInit, 1 = present). See message layout.1— selector for theEitherinsideinit(0 = inlineStateInitfollows, 1 =^StateInitby reference). In our case it is1, and.store_ref(state_init)follows. See message layout.1— selector forbody:(Either X ^X)(0 = inline body follows, 1 = body is provided by reference). In our case it is1, and.store_ref(nft_content)follows. See message layout.
-
The left‑hand value
4 + 2 + 1sets the three one‑bit selectors at the end of this header block to1(while earlier bits remain0). In order, they encode:initis present (Maybe …= 1),initis stored by reference (Either StateInit ^StateInit= 1),- body is stored by reference (
Either X ^X= 1). See message layout.
.store_uint(0, …) calls followed by individual one‑bit writes.
These three functions are used for minting new items and returning item addresses in get‑methods.
Internal messages
NFT standards only require one message to be processed in a specific way:get_royalty_params. All other logic for processing incoming messages can be arbitrary, but this reference contract shows a good example of how it can be organized for NFTs.
The recv_internal function starts with incoming message parsing boilerplate:
op and query_id from incoming message body, and loads the storage data:
Request royalty parameters
Theget_royalty_params message is the only inbound message handling required by TEP-66. On receipt, the contract sends a report_royalty_params message back to the sender with the royalty_params field from storage. The helper send_royalty_params is mostly boilerplate: it sends a non‑bounceable message (see bounce behavior) with a body following the standard and carries the value of the incoming message.
Why non‑bounceable? The reply can go to any sender, including wallets or even uninitialized accounts. Making the response non‑bounceable ensures the value forwarded with mode = 64 credits the requester even if it cannot execute code. It also avoids accidental bounces.
1), deploy a batch of items (opcode 2), and change owner (opcode 3). The chosen opcodes are arbitrary.
Deploy single item
It parses the incoming message and expects it to contain 3 values:item_index:uint64, amount:Coins, and content:^Cell (TL‑B notation — the ^ means this field is stored in a separate referenced cell, i.e., the message carries a reference to a child cell that contains the content). It checks the item index to be less than or equal to the next_item_index from storage. Then it calls a helper function deploy_nft_item, which deploys the new NFT item. Lastly, if the item index equals next_item_index, it increments next_item_index to update the last item index.
Batch deploy items
This feature is useful when you need to deploy many NFTs at once, for example when minting some initial items. It takes a singledeploy_list dictionary from the message where keys are item indices and values are cells containing amounts of TON to attach to deployment, and content cells, similar to the values parsed by the singular deploy feature above. It traverses the dictionary in ascending order of indices and limits the batch to ≤249 items to stay comfortably under the action list limit of 255 actions per transaction. This reference contract chooses a lower cap than the maximum as a safety margin.
The logic is straightforward. The loop uses ~udict::delete_get_min(64) to get and delete the item with the minimum unsigned 64‑bit key. It does that on each iteration and increments counter each iteration to avoid exceeding the action list limit. The deployment logic is the same as for the singular deployment feature above.
Change owner
The last incoming message handler is for changing ownership of the collection. It parses thenew_owner address from the incoming message body and saves contract storage with it instead of the previous owner.
0xffff, which is a common exit code for unknown‑opcode errors:
Get-methods
The collection contract implements only the get‑methods required by the standards:get_collection_data, get_nft_address_by_index, royalty_params, and get_nft_content.
Return collection data
The get‑methodget_collection_data returns next_item_index (typically the number minted if minted sequentially), collection content, and an owner address. It loads the contract storage, loads the first reference from the content cell (which contains the collection content), and returns those values.
Return item address by index
The get‑methodget_nft_address_by_index takes an item index and returns the item’s address. It uses helper functions calculate_nft_item_state_init and calculate_nft_item_address to calculate the address.
Return royalty parameters
The get‑methodroyalty_params returns royalty parameters: numerator, denominator, and destination, as required by the standard. These values are stored in a royalty cell in data, which this method parses and returns.
Compose full item content
The get‑methodget_nft_content takes an item index and item content, and returns full content of the item. It composes and returns a cell following the off‑chain format for metadata from the TEP‑64 standard.
This reference implementation works with an off‑chain metadata format, joining a common prefix stored in the collection contract with individual item content from the item contract. The specific way of composing the full item’s content is not forced by the standard, and can be implemented in any way.
Since item content in the off‑chain format is a URL pointing to a JSON with all the metadata fields, the common prefix is usually a URL to the endpoint for getting metadata for each item or a directory with all the JSON documents, for example https://example-collection.com/items/. Then each item can contain just the suffix of the full URL, such as 123.json.
Composing a full URL means joining these two strings, which this method does. This is the most common approach, but it’s possible to implement any custom logic for composing item content, like calculating the suffix right in the get‑method from the index and allowing items to not hold any additional content.
The common prefix for content is stored as a second reference in the content cell from the collection’s contract storage. The resulting cell is then composed with an off‑chain tag 0x01, followed by a common prefix joined with individual content in snake format.
Item
The full source code is in nft-item.fc file. The TEP‑62 standard definestransfer and get_static_data messages that must be processed by the item contract, and a single get_nft_data get‑method that it must implement. In this contract, the reference implementation does not go beyond the requirements and only implements these three core features.
| Capability | Requirement |
|---|---|
get_nft_data (get-method) | Required by TEP-62, data format follows TEP-64 |
transfer (inbound internal message) | Required by TEP-62 |
excesses, ownership_assigned (outbound internal messages) | Required by TEP-62 |
get_static_data, report_static_data (internal messages) | Required by TEP-62 |
Storage
Same as for the collection, the storage schema itself is not defined by the standard and it can be arbitrary. This contract stores only the essentials: theindex of the item, collection_address, owner_address, and content with individual item content. An important consideration with this contract is that it has two states: initialized or not initialized. It is made this way to properly support deployments only by the collection contract. When the item is not initialized yet, it only has an index and collection address. Below are load_data and store_data for the item contract. Note that load_data also returns as the first value the boolean initialization flag, equal to -1 if the item is initialized and 0 if not.
Internal messages
There is a boilerplate helper function for sending messages in this contract, for convenience. It composes and sends a message by taking high‑level arguments likeop and payload:
recv_internal function begins with boilerplate code for parsing flags, the sender address, and estimating the forward fee. The forward fee estimation assumes that all outbound messages will not be larger than the inbound message, and calculates the original forward fee of the inbound message as an upper bound. The muldiv operation multiplying the fwd_fee by 3 and dividing by 2 is an outdated way of doing that, as there exists a get_original_fwd_fee function which mostly does the same thing (see TVM instructions → GETORIGINALFWDFEE).
content and owner_address:
op and query_id from the incoming message body:
transfer or get_static_data messages required by the standard.
Transfer ownership
Thetransfer message’s structure is defined by the standard. It must contain, apart from the opcode and query_id: new_owner (the destination address of the item transfer), response_destination (an address where the contract should send the remaining TON left after the transfer along with a confirmation message), custom_payload for extensions of the standard and additional custom logic, forward_amount (amount of TON to send to a new owner with the item transfer), and forward_payload (a cell for any arbitrary data to send to a new owner with the item transfer).
The transfer message handler checks whether the sender_address equals owner_address, then parses fields from the message. While parsing, it checks them for validity. The new owner’s address must have a workchain equal to a constant specified in source code. This is a safety measure made to prevent troubles with differences in transaction fees between workchains.
Then it checks all the TON amounts to make sure it has enough balance to complete the transfer. After that it finally completes the transfer by updating the owner_address field in storage. It also sends from 0 to 2 messages, depending on whether a response is required and whether there is any forward_amount. The custom_payload is ignored in this implementation.
The response message has the excesses opcode and only contains the original query_id, with its only purpose being forwarding the remaining TON value of the transfer message to some address, usually the sender address.
The ownership_assigned message is sent only if forward_amount is greater than 0. It has the ownership_assigned opcode and includes query_id, previous owner_address, and the forward_payload, which at this point in code is the only thing left in in_msg_body from the original transfer message. This message is often used for processing item transfers in destination smart contracts, as well as attaching comments to transfers, and attaching any TON value by specifying forward_amount.
Report static data
Onget_static_data, respond with report_static_data containing the item’s index and collection address. This handler composes and sends a message with the index and collection address in the body.
0xffff, which is a common exit code for unknown‑opcode errors:
Get-methods
The standard only requires theget_nft_data get‑method.
Return item data
The get‑methodget_nft_data returns the init? flag (whether the item is initialized), index, collection_address, owner_address, and individual_content. In this implementation, it returns all the values from load_data as is, including the logic for setting the init? flag.
Importantly, the content returned by this contract follows the format required by the collection contract, where it only includes the suffix of the URL pointing to an off‑chain JSON with metadata. But this is not forced by the standard, and the format can be arbitrary. The only requirements are that the collection’s get_nft_content must return full content following TEP‑64, and that if the item has no collection it must return full content right away at this point, but the latter is out of scope here.