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:
- JS (@ton)
- JS (tonweb)
- Go
- Python
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
const TonWeb = require('tonweb');
const address1 = new TonWeb.utils.Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = new TonWeb.utils.Address('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');
// toString arguments: isUserFriendly, isUrlSafe, isBounceable, isTestOnly
console.log(address1.toString(true, true, true)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toString(isUserFriendly = false)); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
console.log(address1.toString(true, true, true)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toString(isUserFriendly = false)); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
func main() {
address1 := address.MustParseAddr("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF")
address2 := address.MustParseRawAddr("0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e")
fmt.Println(address1.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
fmt.Println(rawAddr(address1)) // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
fmt.Println(address2.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
fmt.Println(rawAddr(address2)) // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
}
func rawAddr(addr *address.Address) string {
return fmt.Sprintf("%v:%x", addr.Workchain(), addr.Data())
}
from pytoniq_core import Address
address1 = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF')
address2 = Address('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e')
# to_str() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only
print(address1.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address1.to_str(is_user_friendly=False)) # 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
print(address2.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address2.to_str(is_user_friendly=False)) # 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 beginning | Binary form | Bounceable | Testnet-only |
---|---|---|---|
E... | 000100.01 | yes | no |
U... | 010100.01 | no | no |
k... | 100100.01 | yes | yes |
0... | 110100.01 | no | yes |
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:
- JS (@ton)
- JS (tonweb)
- Go
- Python
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
const TonWeb = require('tonweb');
const address = new TonWeb.utils.Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
// toString arguments: isUserFriendly, isUrlSafe, isBounceable, isTestOnly
console.log(address.toString(true, true, true, false)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address.toString(true, false, true, false)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString(true, true, false, false)); // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString(true, true, true, true)); // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString(true, true, false, true)); // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
func main() {
address := address.MustParseAddr("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF")
fmt.Println(address.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
address.SetBounce(false)
fmt.Println(address.String()) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
address.SetBounce(true)
address.SetTestnetOnly(true) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
fmt.Println(address.String())
address.SetBounce(false) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
fmt.Println(address.String())
}
from pytoniq_core import Address
address = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF')
# to_str() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=False, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
print(address.to_str(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=False)) # UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=True)) # kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
print(address.to_str(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=True)) # 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
How to Check the Validity of a TON Address?
- JS (Tonweb)
- tonutils-go
- ton4j
- ton-kotlin
const TonWeb = require("tonweb")
TonWeb.utils.Address.isValid('...')
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
if _, err := address.ParseAddr("EQCD39VS5j...HUn4bpAOg8xqB2N"); err != nil {
return errors.New("invalid address")
}
/* Maven
<dependency>
<groupId>io.github.neodix42</groupId>
<artifactId>address</artifactId>
<version>0.3.2</version>
</dependency>
*/
try {
Address.of("...");
} catch (Exception e) {
// not valid address
}
try {
AddrStd("...")
} catch(e: IllegalArgumentException) {
// not valid address
}
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.
- JS (@ton) for Wallet V4
- JS (@ton) for Wallet V5
- ton-kotlin
- Python
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',
})]
});
import { TonClient, WalletContractV5R1, internal, SendMode } from "@ton/ton";
import { 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 wallet = WalletContractV5R1.create({
publicKey: keyPair.publicKey,
workChain: 0, // Usually you need a workchain 0
});
let contract = client.open(wallet);
// Create a transfer
let seqno: number = await contract.getSeqno();
await contract.sendTransfer({
secretKey: keyPair.secretKey,
seqno,
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
messages: [
internal({
to: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N',
value: '0.05',
body: 'Example transfer body',
}),
],
});
// Setup liteClient
val context: CoroutineContext = Dispatchers.Default
val json = Json { ignoreUnknownKeys = true }
val config = json.decodeFromString<LiteClientConfigGlobal>(
URI("https://ton.org/global-config.json").toURL().readText()
)
val liteClient = LiteClient(context, config)
val WALLET_MNEMONIC = "word1 word2 ...".split(" ")
val pk = PrivateKeyEd25519(Mnemonic.toSeed(WALLET_MNEMONIC))
val walletAddress = WalletV3R2Contract.address(pk, 0)
println(walletAddress.toString(userFriendly = true, bounceable = false))
val wallet = WalletV3R2Contract(liteClient, walletAddress)
runBlocking {
wallet.transfer(pk, WalletTransfer {
destination = AddrStd("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
bounceable = true
coins = Coins(100000000) // 1 ton in nanotons
messageData = org.ton.contract.wallet.MessageData.raw(
body = buildCell {
storeUInt(0, 32)
storeBytes("Comment".toByteArray())
}
)
sendMode = 0
})
}
from pytoniq import LiteBalancer, WalletV4R2
import asyncio
mnemonics = ["your", "mnemonics", "here"]
async def main():
provider = LiteBalancer.from_mainnet_config(1)
await provider.start_up()
wallet = await WalletV4R2.from_mnemonic(provider=provider, mnemonics=mnemonics)
transfer = {
"destination": "DESTINATION ADDRESS HERE", # please remember about bounceable flags
"amount": int(10**9 * 0.05), # amount sent, in nanoTON
"body": "Example transfer body", # may contain a cell; see next examples
}
await wallet.transfer(**transfer)
await provider.close_all()
asyncio.run(main())
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.
- JS (tonweb)
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.
- @ton/ton
- Manually call get-method
- ton-kotlin
- Python
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))
const { Address, beginCell } = require("@ton/core")
const { TonClient } = require("@ton/ton")
async function getUserWalletAddress(userAddress, jettonMasterAddress) {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});
const userAddressCell = beginCell().storeAddress(userAddress).endCell()
const response = await client.runMethod(jettonMasterAddress, "get_wallet_address", [
{type: "slice", cell: userAddressCell}
])
return response.stack.readAddress()
}
const jettonMasterAddress = Address.parse('...') // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')
getUserWalletAddress(userAddress, jettonMasterAddress)
.then((jettonWalletAddress) => {console.log(jettonWalletAddress)})
// Setup liteClient
val context: CoroutineContext = Dispatchers.Default
val json = Json { ignoreUnknownKeys = true }
val config = json.decodeFromString<LiteClientConfigGlobal>(
URI("https://ton.org/global-config.json").toURL().readText()
)
val liteClient = LiteClient(context, config)
val USER_ADDR = AddrStd("Wallet address")
val JETTON_MASTER = AddrStd("Jetton Master contract address") // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
// we need to send regular wallet address as a slice
val userAddressSlice = CellBuilder.beginCell()
.storeUInt(4, 3)
.storeInt(USER_ADDR.workchainId, 8)
.storeBits(USER_ADDR.address)
.endCell()
.beginParse()
val response = runBlocking {
liteClient.runSmcMethod(
LiteServerAccountId(JETTON_MASTER.workchainId, JETTON_MASTER.address),
"get_wallet_address",
VmStackValue.of(userAddressSlice)
)
}
val stack = response.toMutableVmStack()
val jettonWalletAddress = stack.popSlice().loadTlb(MsgAddressInt) as AddrStd
println("Calculated Jetton wallet:")
println(jettonWalletAddress.toString(userFriendly = true))
from pytoniq import LiteBalancer, begin_cell
import asyncio
async def main():
provider = LiteBalancer.from_mainnet_config(1)
await provider.start_up()
JETTON_MASTER_ADDRESS = "EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE"
USER_ADDRESS = "EQAsl59qOy9C2XL5452lGbHU9bI3l4lhRaopeNZ82NRK8nlA"
result_stack = await provider.run_get_method(address=JETTON_MASTER_ADDRESS, method="get_wallet_address",
stack=[begin_cell().store_address(USER_ADDRESS).end_cell().begin_parse()])
jetton_wallet = result_stack[0].load_address()
print(f"Jetton wallet address for {USER_ADDRESS}: {jetton_wallet.to_str(1, 1, 1)}")
await provider.close_all()
asyncio.run(main())
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:
- JS (@ton/ton)
- Python
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());
from pytoniq_core import Address, Cell, begin_cell
def calculate_jetton_address(
owner_address: Address, jetton_master_address: Address, jetton_wallet_code: str
):
# Recreate from jetton-utils.fc calculate_jetton_wallet_address()
# https://tonscan.org/jetton/EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs#source
data_cell = (
begin_cell()
.store_uint(0, 4)
.store_coins(0)
.store_address(owner_address)
.store_address(jetton_master_address)
.end_cell()
)
code_cell = Cell.one_from_boc(jetton_wallet_code)
state_init = (
begin_cell()
.store_uint(0, 2)
.store_maybe_ref(code_cell)
.store_maybe_ref(data_cell)
.store_uint(0, 1)
.end_cell()
)
state_init_hex = state_init.hash.hex()
jetton_address = Address(f'0:{state_init_hex}')
return jetton_address
Read the entire example here.
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
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.
- JS (@ton)
- JS (tonweb)
- Python
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..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const destinationAddress = new TonWeb.Address('put destination wallet address');
const forwardPayload = new TonWeb.boc.Cell();
forwardPayload.bits.writeUint(0, 32); // 0 opcode means we have a comment
forwardPayload.bits.writeString('Hello, TON!');
/*
Tonweb has a built-in class for interacting with jettons, which has
a method for creating a transfer. However, it has disadvantages, so
we manually create the message body. Additionally, this way we have a
better understanding of what is stored and how it functions.
*/
const jettonTransferBody = new TonWeb.boc.Cell();
jettonTransferBody.bits.writeUint(0xf8a7ea5, 32); // opcode for jetton transfer
jettonTransferBody.bits.writeUint(0, 64); // query id
jettonTransferBody.bits.writeCoins(new TonWeb.utils.BN('5')); // jetton amount, amount * 10^9
jettonTransferBody.bits.writeAddress(destinationAddress);
jettonTransferBody.bits.writeAddress(destinationAddress); // response destination
jettonTransferBody.bits.writeBit(false); // no custom payload
jettonTransferBody.bits.writeCoins(TonWeb.utils.toNano('0.02')); // forward amount
jettonTransferBody.bits.writeBit(true); // we store forwardPayload as a reference
jettonTransferBody.refs.push(forwardPayload);
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const jettonWallet = new TonWeb.token.ft.JettonWallet(tonweb.provider, {
address: 'put your jetton wallet address'
});
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: jettonWallet.address,
amount: tonweb.utils.toNano('0.1'),
seqno: await wallet.methods.seqno().call(),
payload: jettonTransferBody,
sendMode: 3
}).send(); // create transfer and send it
}
main().finally(() => console.log("Exiting..."));
from pytoniq import LiteBalancer, WalletV4R2, begin_cell
import asyncio
mnemonics = ["your", "mnemonics", "here"]
async def main():
provider = LiteBalancer.from_mainnet_config(1)
await provider.start_up()
wallet = await WalletV4R2.from_mnemonic(provider=provider, mnemonics=mnemonics)
USER_ADDRESS = wallet.address
JETTON_MASTER_ADDRESS = "EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE"
DESTINATION_ADDRESS = "EQAsl59qOy9C2XL5452lGbHU9bI3l4lhRaopeNZ82NRK8nlA"
USER_JETTON_WALLET = (await provider.run_get_method(address=JETTON_MASTER_ADDRESS,
method="get_wallet_address",
stack=[begin_cell().store_address(USER_ADDRESS).end_cell().begin_parse()]))[0].load_address()
forward_payload = (begin_cell()
.store_uint(0, 32) # TextComment op-code
.store_snake_string("Comment")
.end_cell())
transfer_cell = (begin_cell()
.store_uint(0xf8a7ea5, 32) # Jetton Transfer op-code
.store_uint(0, 64) # query_id
.store_coins(1 * 10**9) # Jetton amount to transfer in nanojetton
.store_address(DESTINATION_ADDRESS) # Destination address
.store_address(USER_ADDRESS) # Response address
.store_bit(0) # Custom payload is None
.store_coins(1) # Ton forward amount in nanoton
.store_bit(1) # Store forward_payload as a reference
.store_ref(forward_payload) # Forward payload
.end_cell())
await wallet.transfer(destination=USER_JETTON_WALLET, amount=int(0.05*1e9), body=transfer_cell)
await provider.close_all()
asyncio.run(main())
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!
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
- JS (@ton)
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
.
- JS (@ton)
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:
- JS (@ton)
- JS (tonweb)
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..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const collectionAddress = new TonWeb.Address('put your collection address');
const newOwnerAddress = new TonWeb.Address('put new owner wallet address');
const messageBody = new TonWeb.boc.Cell();
messageBody.bits.writeUint(3, 32); // opcode for changing owner
messageBody.bits.writeUint(0, 64); // query id
messageBody.bits.writeAddress(newOwnerAddress);
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: collectionAddress,
amount: tonweb.utils.toNano('0.05'),
seqno: await wallet.methods.seqno().call(),
payload: messageBody,
sendMode: 3
}).send(); // create transfer and send it
}
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.
- JS (@ton)
- JS (tonweb)
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..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://testnet.toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const collectionAddress = new TonWeb.Address('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = new TonWeb.Address('put royalty address');
const collectionMetaCell = new TonWeb.boc.Cell();
collectionMetaCell.bits.writeUint(1, 8); // we have offchain metadata
collectionMetaCell.bits.writeString(newCollectionMeta);
const nftCommonMetaCell = new TonWeb.boc.Cell();
nftCommonMetaCell.bits.writeUint(1, 8); // we have offchain metadata
nftCommonMetaCell.bits.writeString(newNftCommonMeta);
const contentCell = new TonWeb.boc.Cell();
contentCell.refs.push(collectionMetaCell);
contentCell.refs.push(nftCommonMetaCell);
const royaltyCell = new TonWeb.boc.Cell();
royaltyCell.bits.writeUint(5, 16); // factor
royaltyCell.bits.writeUint(100, 16); // base
royaltyCell.bits.writeAddress(royaltyAddress); // this address will receive 5% of each sale
const messageBody = new TonWeb.boc.Cell();
messageBody.bits.writeUint(4, 32);
messageBody.bits.writeUint(0, 64);
messageBody.refs.push(contentCell);
messageBody.refs.push(royaltyCell);
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: collectionAddress,
amount: tonweb.utils.toNano('0.05'),
seqno: await wallet.methods.seqno().call(),
payload: messageBody,
sendMode: 3
}).send(); // create transfer and send it
}
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.
- JS (@ton)
- ton-kotlin
- Python
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 }),
});
Build Asset slice:
val assetASlice = buildCell {
storeUInt(1,4)
storeInt(JETTON_MASTER_A.workchainId, 8)
storeBits(JETTON_MASTER_A.address)
}.beginParse()
Run get methods:
val responsePool = runBlocking {
liteClient.runSmcMethod(
LiteServerAccountId(DEDUST_FACTORY.workchainId, DEDUST_FACTORY.address),
"get_pool_address",
VmStackValue.of(0),
VmStackValue.of(assetASlice),
VmStackValue.of(assetBSlice)
)
}
stack = responsePool.toMutableVmStack()
val poolAddress = stack.popSlice().loadTlb(MsgAddressInt) as AddrStd
Build and transfer message:
runBlocking {
wallet.transfer(pk, WalletTransfer {
destination = JETTON_WALLET_A // yours existing jetton wallet
bounceable = true
coins = Coins(300000000) // 0.3 ton in nanotons
messageData = MessageData.raw(
body = buildCell {
storeUInt(0xf8a7ea5, 32) // op Transfer
storeUInt(0, 64) // query_id
storeTlb(Coins, Coins(100000000)) // amount of jettons
storeSlice(addrToSlice(jettonAVaultAddress)) // destination address
storeSlice(addrToSlice(walletAddress)) // response address
storeUInt(0, 1) // custom payload
storeTlb(Coins, Coins(250000000)) // forward_ton_amount // 0.25 ton in nanotons
storeUInt(1, 1)
// forward_payload
storeRef {
storeUInt(0xe3a0d482, 32) // op swap
storeSlice(addrToSlice(poolAddress)) // pool_addr
storeUInt(0, 1) // kind
storeTlb(Coins, Coins(0)) // limit
storeUInt(0, 1) // next (for multihop)
storeRef {
storeUInt(System.currentTimeMillis() / 1000 + 60 * 5, 32) // deadline
storeSlice(addrToSlice(walletAddress)) // recipient address
storeSlice(buildCell { storeUInt(0, 2) }.beginParse()) // referral (null address)
storeUInt(0, 1)
storeUInt(0, 1)
endCell()
}
}
}
)
sendMode = 3
})
}
This example shows how to swap Toncoins to Jettons.
from pytoniq import Address, begin_cell, LiteBalancer, WalletV4R2
import time
import asyncio
DEDUST_FACTORY = "EQBfBWT7X2BHg9tXAxzhz2aKiNTU1tpt5NsiK0uSDW_YAJ67"
DEDUST_NATIVE_VAULT = "EQDa4VOnTYlLvDJ0gZjNYm5PXfSmmtL6Vs6A_CZEtXCNICq_"
mnemonics = ["your", "mnemonics", "here"]
async def main():
provider = LiteBalancer.from_mainnet_config(1)
await provider.start_up()
wallet = await WalletV4R2.from_mnemonic(provider=provider, mnemonics=mnemonics)
JETTON_MASTER = Address("EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE") # jetton address swap to
TON_AMOUNT = 10**9 # 1 ton - swap amount
GAS_AMOUNT = 10**9 // 4 # 0.25 ton for gas
pool_type = 0 # Volatile pool type
asset_native = (begin_cell()
.store_uint(0, 4) # Asset type is native
.end_cell().begin_parse())
asset_jetton = (begin_cell()
.store_uint(1, 4) # Asset type is jetton
.store_uint(JETTON_MASTER.wc, 8)
.store_bytes(JETTON_MASTER.hash_part)
.end_cell().begin_parse())
stack = await provider.run_get_method(
address=DEDUST_FACTORY, method="get_pool_address",
stack=[pool_type, asset_native, asset_jetton]
)
pool_address = stack[0].load_address()
swap_params = (begin_cell()
.store_uint(int(time.time() + 60 * 5), 32) # Deadline
.store_address(wallet.address) # Recipient address
.store_address(None) # Referall address
.store_maybe_ref(None) # Fulfill payload
.store_maybe_ref(None) # Reject payload
.end_cell())
swap_body = (begin_cell()
.store_uint(0xea06185d, 32) # Swap op-code
.store_uint(0, 64) # Query id
.store_coins(int(1*1e9)) # Swap amount
.store_address(pool_address)
.store_uint(0, 1) # Swap kind
.store_coins(0) # Swap limit
.store_maybe_ref(None) # Next step for multi-hop swaps
.store_ref(swap_params)
.end_cell())
await wallet.transfer(destination=DEDUST_NATIVE_VAULT,
amount=TON_AMOUNT + GAS_AMOUNT, # swap amount + gas
body=swap_body)
await provider.close_all()
asyncio.run(main())
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.
- JS (@ton)
- Python
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...'));
from pytoniq import LiteBalancer, begin_cell
import asyncio
async def parse_transactions(transactions):
for transaction in transactions:
if not transaction.in_msg.is_internal:
continue
if transaction.in_msg.info.dest.to_str(1, 1, 1) != MY_WALLET_ADDRESS:
continue
sender = transaction.in_msg.info.src.to_str(1, 1, 1)
value = transaction.in_msg.info.value_coins
if value != 0:
value = value / 1e9
if len(transaction.in_msg.body.bits) < 32:
print(f"TON transfer from {sender} with value {value} TON")
else:
body_slice = transaction.in_msg.body.begin_parse()
op_code = body_slice.load_uint(32)
# TextComment
if op_code == 0:
print(f"TON transfer from {sender} with value {value} TON and comment: {body_slice.load_snake_string()}")
# Jetton Transfer Notification
elif op_code == 0x7362d09c:
body_slice.load_bits(64) # skip query_id
jetton_amount = body_slice.load_coins() / 1e9
jetton_sender = body_slice.load_address().to_str(1, 1, 1)
if body_slice.load_bit():
forward_payload = body_slice.load_ref().begin_parse()
else:
forward_payload = body_slice
jetton_master = (await provider.run_get_method(address=sender, method="get_wallet_data", stack=[]))[2].load_address()
jetton_wallet = (await provider.run_get_method(address=jetton_master, method="get_wallet_address",
stack=[
begin_cell().store_address(MY_WALLET_ADDRESS).end_cell().begin_parse()
]))[0].load_address().to_str(1, 1, 1)
if jetton_wallet != sender:
print("FAKE Jetton Transfer")
continue
if len(forward_payload.bits) < 32:
print(f"Jetton transfer from {jetton_sender} with value {jetton_amount} Jetton")
else:
forward_payload_op_code = forward_payload.load_uint(32)
if forward_payload_op_code == 0:
print(f"Jetton transfer from {jetton_sender} with value {jetton_amount} Jetton and comment: {forward_payload.load_snake_string()}")
else:
print(f"Jetton transfer from {jetton_sender} with value {jetton_amount} Jetton and unknown payload: {forward_payload} ")
# NFT Transfer Notification
elif op_code == 0x05138d91:
body_slice.load_bits(64) # skip query_id
prev_owner = body_slice.load_address().to_str(1, 1, 1)
if body_slice.load_bit():
forward_payload = body_slice.load_ref().begin_parse()
else:
forward_payload = body_slice
stack = await provider.run_get_method(address=sender, method="get_nft_data", stack=[])
index = stack[1]
collection = stack[2].load_address()
item_address = (await provider.run_get_method(address=collection, method="get_nft_address_by_index",
stack=[index]))[0].load_address().to_str(1, 1, 1)
if item_address != sender:
print("FAKE NFT Transfer")
continue
if len(forward_payload.bits) < 32:
print(f"NFT transfer from {prev_owner}")
else:
forward_payload_op_code = forward_payload.load_uint(32)
if forward_payload_op_code == 0:
print(f"NFT transfer from {prev_owner} with comment: {forward_payload.load_snake_string()}")
else:
print(f"NFT transfer from {prev_owner} with unknown payload: {forward_payload}")
print(f"NFT Item: {item_address}")
print(f"NFT Collection: {collection}")
print(f"Transaction hash: {transaction.cell.hash.hex()}")
print(f"Transaction lt: {transaction.lt}")
MY_WALLET_ADDRESS = "EQAsl59qOy9C2XL5452lGbHU9bI3l4lhRaopeNZ82NRK8nlA"
provider = LiteBalancer.from_mainnet_config(1)
async def main():
await provider.start_up()
transactions = await provider.get_transactions(address=MY_WALLET_ADDRESS, count=5)
await parse_transactions(transactions)
await provider.close_all()
asyncio.run(main())
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:
- @ton/ton
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.
- @ton/ton
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.
- @ton/ton
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();