TON Cookbook: Best Practices for The Open Network Development

ยท

Introduction

In the product development process, developers often encounter various questions about interacting with different contracts on TON. This document aims to collect and share best practices from all developers to help streamline development workflows on The Open Network.

Working with Contract Addresses

Converting Between Address Formats

TON addresses uniquely identify contracts on the blockchain, indicating their workchain and raw state hash. There are two common formats:

  1. Raw format: Workchain and HEX-encoded hash separated by ":"
  2. User-friendly format: Base64-encoded with specific flags

Example conversion:

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

Code example using @ton/core:

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

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

console.log(address1.toString()); // User-friendly
console.log(address1.toRawString()); // Raw format

User-Friendly Address Flags

User-friendly addresses contain flags indicating:

These flags appear in the first 6 bits of the encoded address:

Address PrefixBinary FormBounceableTestnet Only
E...000100.01YesNo
U...010100.01NoNo
k...100100.01YesYes
0...110100.01NoYes

Example generating different flag combinations:

console.log(address.toString({bounceable: false})); // Non-bounceable
console.log(address.toString({testOnly: true})); // Testnet only

Validating TON Addresses

To validate TON addresses:

const TonWeb = require("tonweb");
TonWeb.utils.Address.isValid('...');

Standard Wallets in TON Ecosystem

Transferring TON and Sending Messages

Most SDKs follow this workflow for sending messages from wallets:

  1. Create wallet wrapper with private key and workchain (usually 0)
  2. Create blockchain client object
  3. Open the contract on the blockchain
  4. Compose and send messages (up to 4 per request)

Example transfer with @ton/core:

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

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

// Create wallet from mnemonics
let mnemonics = "word1 word2 ...".split(" ");
let keyPair = await mnemonicToPrivateKey(mnemonics);

let wallet = WalletContractV4.create({workchain: 0, publicKey: keyPair.publicKey});
let contract = client.open(wallet);

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

Storing Long Strings with Snake Format

For strings exceeding a cell's 1023-bit limit, use snake format cells that reference other cells:

const TonWeb = require("tonweb");

function writeStringTail(str, cell) {
  const bytes = Math.floor(cell.bits.getFreeBits()/8);
  if(bytes < str.length) {
    cell.bits.writeString(str.substring(0, bytes));
    const newCell = writeStringTail(str.substring(bytes), new TonWeb.boc.Cell());
    cell.refs.push(newCell);
  } else {
    cell.bits.writeString(str);
  }
  return cell;
}

TEP-74 (Jetton Standard)

Calculating User's Jetton Wallet Address

To calculate a user's jetton wallet address off-chain:

import {TonClient, JettonMaster} from "@ton/ton";

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

Alternatively, manually calculate using jetton wallet code:

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

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());

Constructing Jetton Transfer Messages

For jetton transfers with comments:

const forwardPayload = beginCell()
  .storeUint(0, 32) // Comment opcode
  .storeStringTail('Hello, TON!')
  .endCell();

const messageBody = beginCell()
  .storeUint(0x0f8a7ea5, 32) // Jetton transfer opcode
  .storeUint(0, 64) // Query id
  .storeCoins(toNano(5)) // Jetton amount
  .storeAddress(destinationAddress)
  .storeAddress(destinationAddress) // Response destination
  .storeBit(0) // No custom payload
  .storeCoins(toNano('0.02')) // Forward amount
  .storeBit(1) // Store forwardPayload as reference
  .storeRef(forwardPayload)
  .endCell();

TEP-62 (NFT Standard)

Bulk NFT Deployment

For deploying multiple NFTs in one transaction (up to 100-130 NFTs):

const nftDict = Dictionary.empty();
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++;
}

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);
    }
  })
  .endCell();

Changing Collection Ownership

To change collection ownership:

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

Working with Decentralized Exchanges (DEX)

Swapping on DeDust

Example swapping TON to jetton:

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

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

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

For jetton to jetton swaps:

const scaleVault = tonClient.open(await factory.getJettonVault(SCALE_ADDRESS));
await scaleWallet.sendTransfer(sender, toNano("0.3"), {
  amount: amountIn,
  destination: scaleVault.address,
  responseAddress: sender.address,
  forwardAmount: toNano("0.25"),
  forwardPayload: VaultJetton.createSwapPayload({poolAddress})
});

Message Processing Fundamentals

Parsing Account Transactions

To parse different transaction types:

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

for(const tx of transactions) {
  const inMsg = tx.inMessage;
  if(inMsg?.info.type == 'internal') {
    const sender = inMsg?.info.src;
    const value = inMsg?.info.value.coins;
    const body = inMsg?.body.beginParse();
    
    if(body.remainingBits < 32) {
      console.log(`Simple transfer from ${sender}`);
    } else {
      const op = body.loadUint(32);
      if(op == 0) {
        const comment = body.loadStringTail();
        console.log(`Transfer with comment: "${comment}"`);
      } else if(op == 0x7362d09c) {
        console.log(`Jetton transfer notification`);
      } else if(op == 0x05138d91) {
        console.log(`NFT transfer notification`);
      }
    }
  }
}

Frequently Asked Questions (FAQ)

How do I find transactions for specific TON Connect results?

export async function getTxByBOC(exBoc: string): Promise<string> {
  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 extHash = Cell.fromBase64(exBoc).hash().toString('hex')
        const inHash = beginCell().store(storeMessage(inMsg)).endCell().hash().toString('hex')
        if(extHash === inHash) {
          return tx.hash().toString('hex');
        }
      }
    }
    throw new Error('Transaction not found');
  }, {retries: 30, delay: 1000});
}

What's the difference between transaction hash and message hash?

๐Ÿ‘‰ Learn more about TON message handling

How can I optimize my TON smart contracts for gas efficiency?

Key optimization strategies include:

  1. Minimizing cell operations
  2. Using tail recursion where possible
  3. Optimizing data storage structures
  4. Reducing complex computations in frequently called methods