跳到主要内容

TON Cookbook

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

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

Working with contracts' addresses

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

A TON address uniquely identifies contract in blockchain, indicating its workchain and original state hash. Two common formats are used: raw (workchain and HEX-encoded hash separated by the ":" character) and user-friendly (base64-encoded with certain flags).

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

To obtain an address object from a string in your SDK, 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

What flags are there in user-friendly addresses?

Two flags are defined: bounceable/non-bounceable and testnet/any-net. They can be easily detected by looking at the first letter of the address, because it stands for first 6 bits in address encoding, and flags are located there according to TEP-2:

Address beginningBinary formBounceableTestnet-only
E...000100.01yesno
U...010100.01nono
k...100100.01yesyes
0...110100.01noyes
提示

Testnet-only flag doesn't have representation in blockchain at all. Non-bounceable flag makes difference only when used as destination address for a transfer: in this case, it disallows bounce for a message sent; address in blockchain, again, does not contain this flag.

Also, in some libraries, you may notice a serialization parameter called urlSafe. he base64 format is not URL safe, which means that some characters (namely,+ and /) can cause issues when transmitting an 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 Check the Validity of a TON Address?


const TonWeb = require("tonweb")

TonWeb.utils.Address.isValid('...')

Standard wallets in TON ecosystem

How to transfer TON? How to send a text message to another wallet?

Sending messages



Deploying a contract



Most SDKs provide the following process for sending messages from your wallet:

  • You create wallet wrapper (object in your program) of a correct version (in most cases, v3r2; see also wallet versions), using secret key and workchain (usually 0, which stands for basechain).
  • You also create blockchain wrapper, or "client" - object that will route requests to API or liteservers, whichever you choose.
  • Then, you open contract in the blockchain wrapper. This means contract object is no longer abstract and represents actual account in either TON mainnet or testnet.
  • After that, you can form messages you want and send them. You can also send up to 4 messages per request, as described in an advanced manual.
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',
apiKey: 'your-api-key', // Optional, but note that without api-key you need to send requests once per second, and with 0.25 seconds
});

// 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',
})]
});

Writing comments: long strings in snake format

Sometimes 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(slice) {
const str = new TextDecoder('ascii').decode(slice.array); // decode uint8array to string
if (cell.refs.length > 0) {
return str + readStringTail(cell.refs[0].beginParse()); // read next cell
} else {
return str;
}
}

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.beginParse());
console.log(text);

Many SDKs already have functions responsible for parsing and storing long strings. In others, you can work with such cells using recursion, or possibly optimize it out (trick known as "tail calls").

Don't forget that comment message has 32 zero bits (one may say, that its opcode is 0)!

TEP-74 (Jettons Standard)

How to calculate user's Jetton wallet address (offchain)?

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.

信息

JettonMaster in @ton/ton lacks much functionality but has this one present, fortunately.

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 calculate user's Jetton wallet address (offline)?

Calling the GET method every time to get the wallet address can take a lot of time and resources. If we know the Jetton Wallet code and its storage structure in advance, we can get the wallet address without any network requests.

You can get the code using Tonviewer. Let's take jUSDT as an example, the Jetton Master address is EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA. If we go to this address and open the Methods tab, we can see that there is already a get_jetton_data method there. By calling it, we can get the hex form of the cell with the Jetton Wallet code:

b5ee9c7201021301000385000114ff00f4a413f4bcf2c80b0102016202030202cb0405001ba0f605da89a1f401f481f481a9a30201ce06070201580a0b02f70831c02497c138007434c0c05c6c2544d7c0fc07783e903e900c7e800c5c75c87e800c7e800c1cea6d0000b4c7c076cf16cc8d0d0d09208403e29fa96ea68c1b088d978c4408fc06b809208405e351466ea6cc1b08978c840910c03c06f80dd6cda0841657c1ef2ea7c09c6c3cb4b01408eebcb8b1807c073817c160080900113e910c30003cb85360005c804ff833206e953080b1f833de206ef2d29ad0d30731d3ffd3fff404d307d430d0fa00fa00fa00fa00fa00fa00300008840ff2f00201580c0d020148111201f70174cfc0407e803e90087c007b51343e803e903e903534544da8548b31c17cb8b04ab0bffcb8b0950d109c150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c032481c007e401d3232c084b281f2fff274013e903d010c7e800835d270803cb8b13220060072c15401f3c59c3e809dc072dae00e02f33b51343e803e903e90353442b4cfc0407e80145468017e903e9014d771c1551cdbdc150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c0325c007e401d3232c084b281f2fff2741403f1c147ac7cb8b0c33e801472a84a6d8206685401e8062849a49b1578c34975c2c070c00870802c200f1000aa13ccc88210178d4519580a02cb1fcb3f5007fa0222cf165006cf1625fa025003cf16c95005cc2391729171e25007a813a008aa005004a017a014bcf2e2c501c98040fb004300c85004fa0258cf1601cf16ccc9ed5400725269a018a1c882107362d09c2902cb1fcb3f5007fa025004cf165007cf16c9c8801001cb0527cf165004fa027101cb6a13ccc971fb0050421300748e23c8801001cb055006cf165005fa027001cb6a8210d53276db580502cb1fcb3fc972fb00925b33e24003c85004fa0258cf1601cf16ccc9ed5400eb3b51343e803e903e9035344174cfc0407e800870803cb8b0be903d01007434e7f440745458a8549631c17cb8b049b0bffcb8b0b220841ef765f7960100b2c7f2cfc07e8088f3c58073c584f2e7f27220060072c148f3c59c3e809c4072dab33260103ec01004f214013e809633c58073c5b3327b55200087200835c87b51343e803e903e9035344134c7c06103c8608405e351466e80a0841ef765f7ae84ac7cbd34cfc04c3e800c04e81408f214013e809633c58073c5b3327b5520

Now, knowing the Jetton Wallet code, the Jetton Master address and the vault structure, we can manually calculate the wallet address:

import { Address, Cell, beginCell, storeStateInit } from '@ton/core';

const JETTON_WALLET_CODE = Cell.fromBoc(Buffer.from('b5ee9c7201021301000385000114ff00f4a413f4bcf2c80b0102016202030202cb0405001ba0f605da89a1f401f481f481a9a30201ce06070201580a0b02f70831c02497c138007434c0c05c6c2544d7c0fc07783e903e900c7e800c5c75c87e800c7e800c1cea6d0000b4c7c076cf16cc8d0d0d09208403e29fa96ea68c1b088d978c4408fc06b809208405e351466ea6cc1b08978c840910c03c06f80dd6cda0841657c1ef2ea7c09c6c3cb4b01408eebcb8b1807c073817c160080900113e910c30003cb85360005c804ff833206e953080b1f833de206ef2d29ad0d30731d3ffd3fff404d307d430d0fa00fa00fa00fa00fa00fa00300008840ff2f00201580c0d020148111201f70174cfc0407e803e90087c007b51343e803e903e903534544da8548b31c17cb8b04ab0bffcb8b0950d109c150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c032481c007e401d3232c084b281f2fff274013e903d010c7e800835d270803cb8b13220060072c15401f3c59c3e809dc072dae00e02f33b51343e803e903e90353442b4cfc0407e80145468017e903e9014d771c1551cdbdc150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c0325c007e401d3232c084b281f2fff2741403f1c147ac7cb8b0c33e801472a84a6d8206685401e8062849a49b1578c34975c2c070c00870802c200f1000aa13ccc88210178d4519580a02cb1fcb3f5007fa0222cf165006cf1625fa025003cf16c95005cc2391729171e25007a813a008aa005004a017a014bcf2e2c501c98040fb004300c85004fa0258cf1601cf16ccc9ed5400725269a018a1c882107362d09c2902cb1fcb3f5007fa025004cf165007cf16c9c8801001cb0527cf165004fa027101cb6a13ccc971fb0050421300748e23c8801001cb055006cf165005fa027001cb6a8210d53276db580502cb1fcb3fc972fb00925b33e24003c85004fa0258cf1601cf16ccc9ed5400eb3b51343e803e903e9035344174cfc0407e800870803cb8b0be903d01007434e7f440745458a8549631c17cb8b049b0bffcb8b0b220841ef765f7960100b2c7f2cfc07e8088f3c58073c584f2e7f27220060072c148f3c59c3e809c4072dab33260103ec01004f214013e809633c58073c5b3327b55200087200835c87b51343e803e903e9035344134c7c06103c8608405e351466e80a0841ef765f7ae84ac7cbd34cfc04c3e800c04e81408f214013e809633c58073c5b3327b5520', 'hex'))[0];
const JETTON_MASTER_ADDRESS = Address.parse('EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA');
const USER_ADDRESS = Address.parse('UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA');

const jettonWalletStateInit = beginCell().store(storeStateInit({
code: JETTON_WALLET_CODE,
data: beginCell()
.storeCoins(0)
.storeAddress(USER_ADDRESS)
.storeAddress(JETTON_MASTER_ADDRESS)
.storeRef(JETTON_WALLET_CODE)
.endCell()
}))
.endCell();
const userJettonWalletAddress = new Address(0, jettonWalletStateInit.hash());

console.log('User Jetton Wallet address:', userJettonWalletAddress.toString());

Most major tokens do not have a different storage structure because they use a standard implementation of the TEP-74 standard. The exception is the new Jetton-with-governance contracts for centralized stablecoins. In these, the difference is the presence of a wallet status field and the absence of a code cell in the vault.

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

To understand how to construct a message for a token transfer, we use TEP-74, which describes the token standard.

Transfer jettons



warning

When displayed, token doesn't usually show count of indivisible units user has; rather, amount is divided by 10 ^ decimals. This value is commonly set to 9, and this allows us to use toNano function. If decimals were different, we would need to multiply by a different value (for instance, if decimals are 6, then we would end up transferring thousand times the amount we wanted).

Of course, one can always do calculation in indivisible units.

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 - if >0, will send notification message
.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..."));

If forward_amount is nonzero, a notification regarding jetton reception is sent to destination contract, as can be seen in the scheme in the top of this section. If response_destination address is not null, toncoins left (they're called "excesses") are sent to that address.

提示

Explorers support comments in jetton notifications, as well as in common TON transfers. Their format is 32 zero bits and then text, preferably UTF-8.

提示

Jetton transfers need careful consideration for fees and amounts behind outgoing messages. For instance, if you "call" transfer with 0.2 TON, you won't be able to forward 0.1 TON and receive 0.1 TON in excess return message.

TEP-62 (NFT Standard)

NFT collections are very different. Actually, NFT contract on TON can be defined as "contract that has appropriate get-method and returns valid metadata". Transfer operation is standardized and quite analogous to jetton's one, so we will not dive into it and rather see additional capabilities provided by most collections you may meet!

warning

Reminder: all methods about NFT below are not bound by TEP-62 to work. Before trying them, please check if your NFT or collection will process those messages in an expected way. Wallet app emulation may prove useful in this case.

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.

Batch mint NFT

信息

Does not specified by NFT standard for /ton-blockchain /token-contract



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 collection's smart contract 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
});
}

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 scenario. 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 of 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.

Third-party: Decentralized Exchanges (DEX)

How to send a swap message to DEX (DeDust)?

DEXs use different protocols for their work. In this example we will interact with DeDust.

DeDust has two exchange paths: jetton <-> jetton or TON <-> jetton. Each has a different scheme. To swap, you need to send jettons (or toncoin) to a specific vault and provide a special payload. Here is the scheme for swapping jetton to jetton or jetton to toncoin:

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

This scheme shows what should be in the forward_payload of your jettons transfer message (transfer#0f8a7ea5).

And the scheme of toncoin to jetton swap:

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

This is the scheme for the body of transfer to the toncoin vault.

First, you need to know the vault addresses of the jettons you will swap or toncoin vault address. This can be done using the get_vault_address get method of the contract Factory. As an argument you need to pass a slice according to the scheme:

native$0000 = Asset; // for ton
jetton$0001 workchain_id:int8 address:uint256 = Asset; // for jetton

Also for the exchange itself, we need the pool address - acquired from get method get_pool_address. As arguments - asset slices according to the scheme above. In response, both methods will return a slice of the address of the requested vault / pool.

This is enough to build the message.

DEXs use different protocols for their work, we need to familiarize ourselves with key concepts and some vital components and also know the TL-B schema involved in doing our swap process correctly. In this tutorial, we deal with DeDust, one of the famous DEX implemented entirely in TON. In DeDust, we have an abstract Asset concept that includes any swappable asset types. Abstraction over asset types simplifies the swap process because the type of asset does not matter, and extra currency or even assets from other chains in this approach will be covered with ease.

Following is the TL-B schema that DeDust introduced for the Asset concept.

native$0000 = Asset; // for ton

jetton$0001 workchain_id:int8 address:uint256 = Asset; // for any jetton,address refer to jetton master address

// Upcoming, not implemented yet.
extra_currency$0010 currency_id:int32 = Asset;

Next, DeDust introduced three components, Vault, Pool, and Factory. These components are contracts or groups of contracts and are responsible for parts of the swap process. The factory acts as finding other component addresses (like vault, and pool) and also building other components. Vault is responsible for receiving transfer messages, holding assets, and just informing the corresponding pool that "user A wants to swap 100 X to Y".

Pool, on the other hand, is responsible for calculating the swap amount based on the predefined formula informing other Vault that are responsible for asset Y, and telling it to pay a calculated amount to the user. Calculations of swap amount are based on a mathematical formula, which means so far we have two different pools, one known as Volatile, that operates based on the commonly used "Constant Product" formula: x * y = k, And the other known as Stable-Swap - Optimized for assets of near-equal value (e.g. USDT / USDC, TON / stTON). It uses the formula: x3 * y + y3 * x = k. So for every swap we need the corresponding Vault and it needs just implement a specific API tailored for interacting with a distinct asset type. DeDust has three implementations of Vault, Native Vault - Handles the native coin (Toncoin). Jetton Vault - Manages jettons and Extra-Currency Vault (upcoming) - Designed for TON extra-currencies.

DeDust provides a special SDk to work with contract, component, and API, it was written in typescript. Enough theory, let's set up our environment to swap one jetton with TON.

npm install --save @ton/core @ton/ton @ton/crypto

we also need to bring DeDust SDK as well.

npm install --save @dedust/sdk

Now we need to initialize some objects.

import { Factory, MAINNET_FACTORY_ADDR } from "@dedust/sdk";
import { Address, TonClient4 } from "@ton/ton";

const tonClient = new TonClient4({
endpoint: "https://mainnet-v4.tonhubapi.com",
});
const factory = tonClient.open(Factory.createFromAddress(MAINNET_FACTORY_ADDR));
//The Factory contract is used to locate other contracts.

The process of swapping has some steps, for example, to swap some TON with Jetton we first need to find the corresponding Vault and Pool then make sure they are deployed. For our example TON and SCALE, the code is as follows :

import { Asset, VaultNative } from "@dedust/sdk";

//Native vault is for TON
const tonVault = tonClient.open(await factory.getNativeVault());
//We use the factory to find our native coin (Toncoin) Vault.

The next step is to find the corresponding Pool, here (TON and SCALE)

import { PoolType } from "@dedust/sdk";

const SCALE_ADDRESS = Address.parse(
"EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE",
);
// master address of SCALE jetton
const TON = Asset.native();
const SCALE = Asset.jetton(SCALE_ADDRESS);

const pool = tonClient.open(
await factory.getPool(PoolType.VOLATILE, [TON, SCALE]),
);

Now we should ensure that these contracts exist since sending funds to an inactive contract could result in irretrievable loss.

import { ReadinessStatus } from "@dedust/sdk";

// Check if the pool exists:
if ((await pool.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Pool (TON, SCALE) does not exist.");
}

// Check if the vault exits:
if ((await tonVault.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Vault (TON) does not exist.");
}

After that, we can send transfer messages with the amount of TON.

import { toNano } from "@ton/core";
import { mnemonicToPrivateKey } from "@ton/crypto";

if (!process.env.MNEMONIC) {
throw new Error("Environment variable MNEMONIC is required.");
}

const mnemonic = process.env.MNEMONIC.split(" ");

const keys = await mnemonicToPrivateKey(mnemonic);
const wallet = tonClient.open(
WalletContractV3R2.create({
workchain: 0,
publicKey: keys.publicKey,
}),
);

const sender = wallet.sender(keys.secretKey);

const amountIn = toNano("5"); // 5 TON

await tonVault.sendSwap(sender, {
poolAddress: pool.address,
amount: amountIn,
gasAmount: toNano("0.25"),
});

To swap Token X with Y, the process is the same, for instance, we send an amount of X token to vault X, vault X receives our asset, holds it, and informs Pool of (X, Y) that this address asks for a swap, now Pool based on calculation informs another Vault, here Vault Y releases equivalent Y to the user who requests swap.

The difference between assets is just about the transfer method for example, for jettons, we transfer them to the Vault using a transfer message and attach a specific forward_payload, but for the native coin, we send a swap message to the Vault, attaching the corresponding amount of TON.

This is the schema for TON and jetton :

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;

So every vault and corresponding Pool is designed for specific swaps and has a special API tailored to special assets.

This was swapping TON with jetton SCALE. The process for swapping jetton with jetton is the same, the only difference is we should provide the payload that was described in the TL-B schema.

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
//find Vault
const scaleVault = tonClient.open(await factory.getJettonVault(SCALE_ADDRESS));
//find jetton address
import { JettonRoot, JettonWallet } from '@dedust/sdk';

const scaleRoot = tonClient.open(JettonRoot.createFromAddress(SCALE_ADDRESS));
const scaleWallet = tonClient.open(await scaleRoot.getWallet(sender.address);

// Transfer jettons to the Vault (SCALE) with corresponding payload

const amountIn = toNano('50'); // 50 SCALE

await scaleWallet.sendTransfer(sender, toNano("0.3"), {
amount: amountIn,
destination: scaleVault.address,
responseAddress: sender.address, // return gas to user
forwardAmount: toNano("0.25"),
forwardPayload: VaultJetton.createSwapPayload({ poolAddress }),
});

Basics of message processing

How to parse transactions of an account (Transfers, Jettons, NFTs)?

The list of transactions on an account can be fetched through getTransactions API method. It returns an array of Transaction objects, with each item having lots of attributes. However, the fields that are the most commonly used are:

  • Sender, Body and Value of the message that initiated this transaction
  • Transaction's hash and logical time (LT)

Sender and Body fields may be used to determine the type of message (regular transfer, jetton transfer, nft transfer etc).

Below is an example on how you can fetch 5 most recent transactions on any blockchain account, parse them depending on the type and print out in a loop.

import { Address, TonClient, beginCell, fromNano } from '@ton/ton';

async function main() {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: '1b312c91c3b691255130350a49ac5a0742454725f910756aff94dfe44858388e',
});

const myAddress = Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'); // address that you want to fetch transactions from

const transactions = await client.getTransactions(myAddress, {
limit: 5,
});

for (const tx of transactions) {
const inMsg = tx.inMessage;

if (inMsg?.info.type == 'internal') {
// we only process internal messages here because they are used the most
// for external messages some of the fields are empty, but the main structure is similar
const sender = inMsg?.info.src;
const value = inMsg?.info.value.coins;

const originalBody = inMsg?.body.beginParse();
let body = originalBody.clone();
if (body.remainingBits < 32) {
// if body doesn't have opcode: it's a simple message without comment
console.log(`Simple transfer from ${sender} with value ${fromNano(value)} TON`);
} else {
const op = body.loadUint(32);
if (op == 0) {
// if opcode is 0: it's a simple message with comment
const comment = body.loadStringTail();
console.log(
`Simple transfer from ${sender} with value ${fromNano(value)} TON and comment: "${comment}"`
);
} else if (op == 0x7362d09c) {
// if opcode is 0x7362d09c: it's a Jetton transfer notification

body.skip(64); // skip query_id
const jettonAmount = body.loadCoins();
const jettonSender = body.loadAddressAny();
const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body;
let forwardPayload = originalForwardPayload.clone();

// IMPORTANT: we have to verify the source of this message because it can be faked
const runStack = (await client.runMethod(sender, 'get_wallet_data')).stack;
runStack.skip(2);
const jettonMaster = runStack.readAddress();
const jettonWallet = (
await client.runMethod(jettonMaster, 'get_wallet_address', [
{ type: 'slice', cell: beginCell().storeAddress(myAddress).endCell() },
])
).stack.readAddress();
if (!jettonWallet.equals(sender)) {
// if sender is not our real JettonWallet: this message was faked
console.log(`FAKE Jetton transfer`);
continue;
}

if (forwardPayload.remainingBits < 32) {
// if forward payload doesn't have opcode: it's a simple Jetton transfer
console.log(`Jetton transfer from ${jettonSender} with value ${fromNano(jettonAmount)} Jetton`);
} else {
const forwardOp = forwardPayload.loadUint(32);
if (forwardOp == 0) {
// if forward payload opcode is 0: it's a simple Jetton transfer with comment
const comment = forwardPayload.loadStringTail();
console.log(
`Jetton transfer from ${jettonSender} with value ${fromNano(
jettonAmount
)} Jetton and comment: "${comment}"`
);
} else {
// if forward payload opcode is something else: it's some message with arbitrary structure
// you may parse it manually if you know other opcodes or just print it as hex
console.log(
`Jetton transfer with unknown payload structure from ${jettonSender} with value ${fromNano(
jettonAmount
)} Jetton and payload: ${originalForwardPayload}`
);
}

console.log(`Jetton Master: ${jettonMaster}`);
}
} else if (op == 0x05138d91) {
// if opcode is 0x05138d91: it's a NFT transfer notification

body.skip(64); // skip query_id
const prevOwner = body.loadAddress();
const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body;
let forwardPayload = originalForwardPayload.clone();

// IMPORTANT: we have to verify the source of this message because it can be faked
const runStack = (await client.runMethod(sender, 'get_nft_data')).stack;
runStack.skip(1);
const index = runStack.readBigNumber();
const collection = runStack.readAddress();
const itemAddress = (
await client.runMethod(collection, 'get_nft_address_by_index', [{ type: 'int', value: index }])
).stack.readAddress();

if (!itemAddress.equals(sender)) {
console.log(`FAKE NFT Transfer`);
continue;
}

if (forwardPayload.remainingBits < 32) {
// if forward payload doesn't have opcode: it's a simple NFT transfer
console.log(`NFT transfer from ${prevOwner}`);
} else {
const forwardOp = forwardPayload.loadUint(32);
if (forwardOp == 0) {
// if forward payload opcode is 0: it's a simple NFT transfer with comment
const comment = forwardPayload.loadStringTail();
console.log(`NFT transfer from ${prevOwner} with comment: "${comment}"`);
} else {
// if forward payload opcode is something else: it's some message with arbitrary structure
// you may parse it manually if you know other opcodes or just print it as hex
console.log(
`NFT transfer with unknown payload structure from ${prevOwner} and payload: ${originalForwardPayload}`
);
}
}

console.log(`NFT Item: ${itemAddress}`);
console.log(`NFT Collection: ${collection}`);
} else {
// if opcode is something else: it's some message with arbitrary structure
// you may parse it manually if you know other opcodes or just print it as hex
console.log(
`Message with unknown structure from ${sender} with value ${fromNano(
value
)} TON and body: ${originalBody}`
);
}
}
}
console.log(`Transaction Hash: ${tx.hash().toString('hex')}`);
console.log(`Transaction LT: ${tx.lt}`);
console.log();
}
}

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

Note that this example covers only the simplest case with incoming messages, where it is enough to fetch the transactions on a single account. If you want to go deeper and handle more complex chains of transactions and messages, you should take tx.outMessages field into an account. It contains the list of the output messages sent by smart-contract in the result of this transaction. To understand the whole logic better, you can read these articles:

This topic is explored in more depth in Payments Processing article.

How to find transaction for a certain TON Connect result?

TON Connect 2 returns only cell which was sent to blockchain, not generated transaction hash (since that transaction may not come to pass, if external message gets lost or timeouts). Provided BOC, though, allows us to search for that exact message in our account history.

提示

You can use an indexer to make the search easier. The provided implementation is for TonClient connected to a RPC.

Prepare retry function for attempts on listening blockchain:


export async function retry<T>(fn: () => Promise<T>, options: { retries: number, delay: number }): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < options.retries; i++) {
try {
return await fn();
} catch (e) {
if (e instanceof Error) {
lastError = e;
}
await new Promise(resolve => setTimeout(resolve, options.delay));
}
}
throw lastError;
}

Create listener function which will assert specific transaction on certain account with specific incoming external message, equal to body message in boc:


import {Cell, Address, beginCell, storeMessage, TonClient} from "@ton/ton";

const res = tonConnectUI.send(msg); // exBoc in the result of sending message
const exBoc = res.boc;
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: 'INSERT YOUR API-KEY', // https://t.me/tonapibot
});

export async function getTxByBOC(exBoc: string): Promise<string> {

const myAddress = Address.parse('INSERT TON WALLET ADDRESS'); // Address to fetch transactions from

return retry(async () => {
const transactions = await client.getTransactions(myAddress, {
limit: 5,
});
for (const tx of transactions) {
const inMsg = tx.inMessage;
if (inMsg?.info.type === 'external-in') {

const inBOC = inMsg?.body;
if (typeof inBOC === 'undefined') {

reject(new Error('Invalid external'));
continue;
}
const extHash = Cell.fromBase64(exBoc).hash().toString('hex')
const inHash = beginCell().store(storeMessage(inMsg)).endCell().hash().toString('hex')

console.log(' hash BOC', extHash);
console.log('inMsg hash', inHash);
console.log('checking the tx', tx, tx.hash().toString('hex'));


// Assuming `inBOC.hash()` is synchronous and returns a hash object with a `toString` method
if (extHash === inHash) {
console.log('Tx match');
const txHash = tx.hash().toString('hex');
console.log(`Transaction Hash: ${txHash}`);
console.log(`Transaction LT: ${tx.lt}`);
return (txHash);
}
}
}
throw new Error('Transaction not found');
}, {retries: 30, delay: 1000});
}

txRes = getTxByBOC(exBOC);
console.log(txRes);

How to find transaction or message hash?

信息

Be careful with the hash definition. It can be either a transaction hash or a message hash. These are different things.

To get transaction hash you need to use hash method of a transaction. To get external message hash you need to build message cell using storeMessage method and then use hash method of this cell.

import { storeMessage, TonClient } from '@ton/ton';
import { Address, beginCell } from '@ton/core';

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

const transactions = await tonClient.getTransactions(Address.parse('[ADDRESS]'), { limit: 10 });
for (const transaction of transactions) {
// ful transaction hash
const transactionHash = transaction.hash();

const inMessage = transaction.inMessage;
if (inMessage?.info.type === 'external-in') {
const inMessageCell = beginCell().store(storeMessage(inMessage)).endCell();
// external-in message hash
const inMessageHash = inMessageCell.hash();
}

// also you can get hash of out messages if needed
for (const outMessage of transaction.outMessages.values()) {
const outMessageCell = beginCell().store(storeMessage(outMessage)).endCell();
const outMessageHash = outMessageCell.hash();
}
}

Also you can get hash of message when building it. Note, this is the same hash as the hash of the message sent to initiate transaction like in previous example.

import { mnemonicNew, mnemonicToPrivateKey } from '@ton/crypto';
import { internal, TonClient, WalletContractV4 } from '@ton/ton';
import { toNano } from '@ton/core';

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

const mnemonic = await mnemonicNew();
const keyPair = await mnemonicToPrivateKey(mnemonic);
const wallet = tonClient.open(WalletContractV4.create({ publicKey: keyPair.publicKey, workchain: 0 }));
const transfer = await wallet.createTransfer({
secretKey: keyPair.secretKey,
seqno: 0,
messages: [
internal({
to: wallet.address,
value: toNano(1)
})
]
});
const inMessageHash = transfer.hash();