Skip to main content

TON Cookbook

During product development, various questions often arise regarding interactions with different contracts on TON.

This document is created to gather the best practices from all developers and share them with everyone.

How to convert (user friendly <-> raw), assemble, and extract addresses from strings?

On TON, depending on the service, addresses can be seen in two formats: user-friendly and raw.

User-friendly: EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
Raw: 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

User-friendly addresses are encoded in base64, while raw addresses are encoded in hex. In the raw format, the workchain in which the address is located is written separately before the ":" character, and the case of the characters does not matter.

To obtain an address from a string, you can use the following code:

import { Address } from "@ton/core";


const address1 = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = Address.parse('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');

// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false

console.log(address1.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

console.log(address2.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

How to obtain different types of addresses and determine the address type?

Addresses come in three formats: bounceable, non-bounceable, and testnet. This can be easily understood by looking at the first letter of the address, because it is the first byte (8 bits) that contains flags according to TEP-2:

LetterBinary formBounceableTestnet
E00010001yesno
U01010001nono
k10010001yesyes
011010001noyes

It's important to note that in base64 encoding, each character represents 6 bits of information. As you can observe, in all cases, the last 2 bits remain unchanged, so in this case, we can focus on the first letter. If they changed, it would affect the next character in the address.

Also, in some libraries, you may notice a field called "url safe." The thing is, the base64 format is not url safe, which means there can be issues when transmitting this address in a link. When urlSafe = true, all + symbols are replaced with -, and all / symbols are replaced with _. You can obtain these address formats using the following code:

import { Address } from "@ton/core";

const address = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');

// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false

console.log(address.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHFэ
console.log(address.toString({urlSafe: false})) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString({bounceable: false})) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString({testOnly: true})) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString({bounceable: false, testOnly: true})) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK

How to send standard TON transfer message?

To send a standard TON transfer message, first you need to open your wallet contract, after that, get your wallet seqno. And only after that can you send your TON transfer. Note that if you are using a non-V4 version of the wallet, you will need to rename WalletContractV4 to WalletContract{your wallet version}, for example, WalletContractV3R2.

import { TonClient, WalletContractV4, internal } from "@ton/ton";
import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";

const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
});

// Convert mnemonics to private key
let mnemonics = "word1 word2 ...".split(" ");
let keyPair = await mnemonicToPrivateKey(mnemonics);

// Create wallet contract
let workchain = 0; // Usually you need a workchain 0
let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey });
let contract = client.open(wallet);

// Create a transfer
let seqno: number = await contract.getSeqno();
await contract.sendTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [internal({
value: '1',
to: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N',
body: 'Example transfer body',
})]
});

How to calculate user's Jetton wallet address?

To calculate the user's jetton wallet address, we need to call the "get_wallet_address" get-method of the jetton master contract with user address actually. For this task we can easily use getWalletAddress method from JettonMaster or call master contract by ourselves.

const { Address, beginCell } = require("@ton/core")
const { TonClient, JettonMaster } = require("@ton/ton")

const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});

const jettonMasterAddress = Address.parse('...') // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')

const jettonMaster = client.open(JettonMaster.create(jettonMasterAddress))
console.log(await jettonMaster.getWalletAddress(userAddress))

How to construct a message for a jetton transfer with a comment?

To understand how to construct a message for token transfer, we use TEP-74, which describes the token standard. It's important to note that each token can have its own decimals, which defaults to 9. So, in the example below, we multiply the quantity by 10^9. If decimals were different, you would need to multiply by a different value.

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const jettonWalletAddress = Address.parse('put your jetton wallet address');
const destinationAddress = Address.parse('put destination wallet address');

const forwardPayload = beginCell()
.storeUint(0, 32) // 0 opcode means we have a comment
.storeStringTail('Hello, TON!')
.endCell();

const messageBody = beginCell()
.storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(toNano(5)) // jetton amount, amount * 10^9
.storeAddress(destinationAddress)
.storeAddress(destinationAddress) // response destination
.storeBit(0) // no custom payload
.storeCoins(toNano('0.02')) // forward amount
.storeBit(1) // we store forwardPayload as a reference
.storeRef(forwardPayload)
.endCell();

const internalMessage = internal({
to: jettonWalletAddress,
value: toNano('0.1'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

To indicate that we want to include a comment, we specify 32 zero bits and then write our comment. We also specify the response destination, which means that a response regarding the successful transfer will be sent to this address. If we don't want a response, we can specify 2 zero bits instead of an address.

How to use NFT batch deploy?

Smart contracts for collections allow deploying up to 250 NFTs in a single transaction. However, it's essential to consider that, in practice, this maximum is around 100-130 NFTs due to the computation fee limit of 1 ton. To achieve this, we need to store information about the new NFTs in a dictionary.

import { Address, Cell, Dictionary, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
import { TonClient } from "@ton/ton";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const nftMinStorage = '0.05';
const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' // for Testnet
});
const ownersAddress = [
Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'),
Address.parse('EQAUTbQiM522Y_XJ_T98QPhPhTmb4nV--VSPiha8kC6kRfPO'),
Address.parse('EQDWTH7VxFyk_34J1CM6wwEcjVeqRQceNwzPwGr30SsK43yo')
];
const nftsMeta = [
'0/meta.json',
'1/meta.json',
'2/meta.json'
];

const getMethodResult = await client.runMethod(collectionAddress, 'get_collection_data');
let nextItemIndex = getMethodResult.stack.readNumber();

To begin with, let's assume that the minimum amount of TON for the storage fee is 0.05. This means that after deploying an NFT, the smart contract of the collection will send this much TON to its balance. Next, we obtain arrays with the owners of the new NFTs and their content. Afterward, we get the next_item_index using the GET method get_collection_data.

    let counter = 0;
const nftDict = Dictionary.empty<number, Cell>();
for (let index = 0; index < 3; index++) {
const metaCell = beginCell()
.storeStringTail(nftsMeta[index])
.endCell();
const nftContent = beginCell()
.storeAddress(ownersAddress[index])
.storeRef(metaCell)
.endCell();
nftDict.set(nextItemIndex, nftContent);
nextItemIndex++;
counter++;
}

/*
We need to write our custom serialization and deserialization
functions to store data correctly in the dictionary since the
built-in functions in the library are not suitable for our case.
*/
const messageBody = beginCell()
.storeUint(2, 32)
.storeUint(0, 64)
.storeDict(nftDict, Dictionary.Keys.Uint(64), {
serialize: (src, builder) => {
builder.storeCoins(toNano(nftMinStorage));
builder.storeRef(src);
},
parse: (src) => {
return beginCell()
.storeCoins(src.loadCoins())
.storeRef(src.loadRef())
.endCell();
}
})
.endCell();

const totalValue = String(
(counter * parseFloat(nftMinStorage) + 0.015 * counter).toFixed(6)
);

const internalMessage = internal({
to: collectionAddress,
value: totalValue,
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

Next, we need to correctly calculate the total transaction cost. The value of 0.015 was obtained through testing, but it can vary for each case. This mainly depends on the content of the NFT, as an increase in content size results in a higher forward fee (the fee for delivery).

How to change the owner of a collection's smart contract?

Changing the owner of a collection is very simple. To do this, you need to specify opcode = 3, any query_id, and the address of the new owner:

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newOwnerAddress = Address.parse('put new owner wallet address');

const messageBody = beginCell()
.storeUint(3, 32) // opcode for changing owner
.storeUint(0, 64) // query id
.storeAddress(newOwnerAddress)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

How to change the content in a collection's smart contract?

To change the content of a smart contract's collection, we need to understand how it is stored. The collection stores all the content in a single cell, inside of which there are two cells: collection content and NFT common content. The first cell contains the collection's metadata, while the second one contains the base URL for the NFT metadata.

Often, the collection's metadata is stored in a format similar to 0.json and continues incrementing, while the address before this file remains the same. It is this address that should be stored in the NFT common content.

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = Address.parse('put royalty address');

const collectionMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newCollectionMeta)
.endCell();
const nftCommonMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newNftCommonMeta)
.endCell();

const contentCell = beginCell()
.storeRef(collectionMetaCell)
.storeRef(nftCommonMetaCell)
.endCell();

const royaltyCell = beginCell()
.storeUint(5, 16) // factor
.storeUint(100, 16) // base
.storeAddress(royaltyAddress) // this address will receive 5% of each sale
.endCell();

const messageBody = beginCell()
.storeUint(4, 32) // opcode for changing content
.storeUint(0, 64) // query id
.storeRef(contentCell)
.storeRef(royaltyCell)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});

const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

Additionally, we need to include royalty information in our message, as they also change using this opcode. It's important to note that it's not necessary to specify new values everywhere. If, for example, only the NFT common content needs to be changed, then all other values can be specified as they were before.

Processing Snake Cells

Some times it's necessary to store long strings (or other large information) while cells can hold maximum 1023 bits. In this case, we can use snake cells. Snake cells are cells that contain a reference to another cell, which, in turn, contains a reference to another cell, and so on.

const TonWeb = require("tonweb");

function writeStringTail(str, cell) {
const bytes = Math.floor(cell.bits.getFreeBits() / 8); // 1 symbol = 8 bits
if(bytes < str.length) { // if we can't write all string
cell.bits.writeString(str.substring(0, bytes)); // write part of string
const newCell = writeStringTail(str.substring(bytes), new TonWeb.boc.Cell()); // create new cell
cell.refs.push(newCell); // add new cell to current cell's refs
} else {
cell.bits.writeString(str); // write all string
}

return cell;
}

function readStringTail(cell) {
const slice = cell.beginParse(); // converting cell to slice
if(cell.refs.length > 0) {
const str = new TextDecoder('ascii').decode(slice.array); // decode uint8array to string
return str + readStringTail(cell.refs[0]); // read next cell
} else {
return new TextDecoder('ascii').decode(slice.array);
}
}

let cell = new TonWeb.boc.Cell();
const str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod, ligula vel lobortis hendrerit, lectus sem efficitur enim, vel efficitur nibh dui a elit. Quisque augue nisi, vulputate vitae mauris sit amet, iaculis lobortis nisi. Aenean molestie ultrices massa eu fermentum. Cras rhoncus ipsum mauris, et egestas nibh interdum in. Maecenas ante ipsum, sodales eget suscipit at, placerat ut turpis. Nunc ac finibus dui. Donec sit amet leo id augue tempus aliquet. Vestibulum eu aliquam ex, sit amet suscipit odio. Vestibulum et arcu dui.";
cell = writeStringTail(str, cell);
const text = readStringTail(cell);
console.log(text);

This example will help you understand how you can work with such cells using recursion.