Jetton Token Development (TON)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Jetton Token Development (TON)
Medium
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Jetton Token Development (TON)

Jetton is the token standard in TON blockchain, analogue of ERC-20 on Ethereum. If you come with Ethereum background, first thing to understand: architecture is fundamentally different. In EVM token is one contract with balance mapping. In TON token is system of contracts: one Jetton Master and N Jetton Wallet (one per each holder). This isn't implementation detail — it's fundamental consequence of TON's sharding architecture.

Architecture: Jetton Master + Jetton Wallet

Jetton Master stores token metadata (name, symbol, decimals, totalSupply) and contains minting logic. It doesn't store balances — doesn't know them.

Jetton Wallet — separate contract for each address. Deployed automatically on first token transfer to address. Stores specific user's balance and handles transfer.

Alice → (transfer) → Alice's Jetton Wallet
Alice's Jetton Wallet → (internal message) → Bob's Jetton Wallet
Bob's Jetton Wallet: receives tokens, increases balance

Why designed this way? TON uses sharding: different contracts can be in different shards. If balances were stored in one place, every transfer would create cross-shard transaction — bottleneck. With distributed wallets, transfer between two wallets is local operation in one shard.

Development on Tact

Tact is high-level language for TON contracts, preferable to FunC for new projects due to readability and type safety.

Jetton Master on Tact:

import "@stdlib/jetton";

message(0x178d4519) TokenTransferInternal {
    queryId: Int as uint64;
    amount: Int as coins;
    from: Address;
    responseAddress: Address?;
    forwardTonAmount: Int as coins;
    forwardPayload: Slice as remaining;
}

contract JettonMaster with Jetton {
    totalSupply: Int as coins = 0;
    owner: Address;
    jettonContent: Cell;
    mintable: Bool;

    init(owner: Address, content: Cell) {
        self.owner = owner;
        self.jettonContent = content;
        self.mintable = true;
    }

    receive(msg: JettonMint) {
        require(sender() == self.owner, "Only owner can mint");
        require(self.mintable, "Minting disabled");

        self.totalSupply += msg.amount;

        let initData: StateInit = self.getJettonWalletInit(msg.receiver);
        let walletAddress: Address = contractAddress(initData);

        send(SendParameters{
            to: walletAddress,
            body: TokenTransferInternal{
                queryId: msg.queryId,
                amount: msg.amount,
                from: myAddress(),
                responseAddress: msg.responseAddress,
                forwardTonAmount: msg.forwardTonAmount,
                forwardPayload: emptySlice(),
            }.toCell(),
            value: msg.tonAmount,
            mode: SendIgnoreErrors,
            code: initData.code,
            data: initData.data,
        });
    }

    fun getJettonWalletInit(owner: Address): StateInit {
        return initOf JettonWallet(owner, myAddress());
    }
}

Jetton Wallet on Tact:

contract JettonWallet with JettonWallet {
    balance: Int as coins = 0;
    owner: Address;
    jettonMaster: Address;

    init(owner: Address, jettonMaster: Address) {
        self.owner = owner;
        self.jettonMaster = jettonMaster;
    }

    receive(msg: TokenTransfer) {
        require(sender() == self.owner, "Not owner");
        require(msg.amount > 0, "Zero amount");
        require(self.balance >= msg.amount, "Insufficient balance");

        self.balance -= msg.amount;

        // Recipient wallet address computed deterministically
        let receiverWalletInit: StateInit = initOf JettonWallet(msg.destination, self.jettonMaster);
        let receiverWallet: Address = contractAddress(receiverWalletInit);

        send(SendParameters{
            to: receiverWallet,
            value: msg.forwardTonAmount + context().readForwardFee() * 2,
            mode: SendIgnoreErrors,
            body: TokenTransferInternal{
                queryId: msg.queryId,
                amount: msg.amount,
                from: self.owner,
                responseAddress: msg.responseAddress,
                forwardTonAmount: msg.forwardTonAmount,
                forwardPayload: msg.forwardPayload,
            }.toCell(),
            code: receiverWalletInit.code,
            data: receiverWalletInit.data,
        });
    }
}

Gas and TON fee: peculiarities

In TON each message (message) carries TON to pay for gas of further operations. On transfer you must send enough TON to cover: gas for sender's Jetton Wallet, gas for recipient's Jetton Wallet (including deploy if new), optional forwardAmount for notifying recipient smart contract.

Typical Jetton transfer cost: 0.05–0.1 TON per operation. For new recipients (wallet deploy) — slightly more (~0.05 TON extra for storage fees). Insufficient TON in message leads to bounce — transaction is cancelled, tokens returned.

Bounce handling — mandatory part of implementation. TON is unique in that failed transactions return message to sender via bounce. Jetton Wallet should handle bounced messages and return balance:

bounced(msg: bounced<TokenTransferInternal>) {
    // Return balance if transfer failed
    self.balance += msg.amount;
}

Token metadata

TON uses off-chain metadata via TEP-64 standard. Metadata is stored on IPFS or HTTPS, link recorded in Jetton Master:

{
    "name": "My Token",
    "description": "Token description",
    "symbol": "MTK",
    "decimals": "9",
    "image": "https://example.com/logo.png",
    "image_data": null
}

In contract link to metadata is encoded in cell as snake-encoded string or offchain content URL. Standard provides both formats: completely on-chain (all data in cell) or off-chain with snake URL.

Integration with TON Connect and wallets

For DApp integration — TON Connect 2.0. SDK for React:

import { TonConnectUI, useTonConnectUI } from '@tonconnect/ui-react';
import { toNano, Address, beginCell } from '@ton/core';
import { JettonMaster } from './wrappers/JettonMaster';

function TransferButton({ jettonAddress, recipient, amount }) {
    const [tonConnectUI] = useTonConnectUI();

    async function handleTransfer() {
        const jettonMaster = JettonMaster.createFromAddress(Address.parse(jettonAddress));
        const senderAddress = Address.parse(tonConnectUI.wallet!.account.address);

        // Get sender's Jetton Wallet address
        const senderWalletAddress = await jettonMaster.getWalletAddress(client, senderAddress);

        const transferPayload = beginCell()
            .storeUint(0xf8a7ea5, 32)  // op::transfer
            .storeUint(0, 64)           // query_id
            .storeCoins(toNano(amount))
            .storeAddress(Address.parse(recipient))
            .storeAddress(senderAddress)  // response_destination
            .storeBit(false)              // no custom_payload
            .storeCoins(toNano("0.01"))   // forward_ton_amount
            .storeBit(false)              // no forward_payload
            .endCell();

        await tonConnectUI.sendTransaction({
            messages: [{
                address: senderWalletAddress.toString(),
                amount: toNano("0.05").toString(),
                payload: transferPayload.toBoc().toString("base64"),
            }],
            validUntil: Math.floor(Date.now() / 1000) + 360,
        });
    }
}

Testing with Blueprint

Blueprint is official testing framework for TON contracts. Supports sandbox environment with blockchain simulation:

import { Blockchain, SandboxContract } from '@ton/sandbox';
import { JettonMaster } from '../wrappers/JettonMaster';

describe('Jetton', () => {
    let blockchain: Blockchain;
    let jettonMaster: SandboxContract<JettonMaster>;

    beforeEach(async () => {
        blockchain = await Blockchain.create();
        const deployer = await blockchain.treasury('deployer');

        jettonMaster = blockchain.openContract(
            JettonMaster.createFromConfig({ owner: deployer.address, ... }, code)
        );

        await jettonMaster.sendDeploy(deployer.getSender(), toNano('0.05'));
    });

    it('should mint and transfer', async () => {
        const alice = await blockchain.treasury('alice');
        const bob = await blockchain.treasury('bob');

        // Mint to Alice
        await jettonMaster.sendMint(deployer.getSender(), alice.address, toNano('1000'));

        // Check balance
        const aliceWallet = await jettonMaster.getWalletAddress(alice.address);
        const balance = await aliceWallet.getBalance();
        expect(balance).toEqual(toNano('1000'));

        // Transfer from Alice to Bob
        await aliceWallet.sendTransfer(alice.getSender(), bob.address, toNano('100'));
        const bobWallet = await jettonMaster.getWalletAddress(bob.address);
        expect(await bobWallet.getBalance()).toEqual(toNano('100'));
    });
});

TON specifics: what most often breaks

Insufficient TON in message. Most common reason for failed transfers. Solution: always check minTonsForStorage + tonBalanceBeforeMsg in tests.

Incorrect bounce handling. If recipient Jetton Wallet doesn't exist and wallet won't be created (insufficient TON) — message returns as bounced. Without proper bounce handler sender's balance is lost forever.

Errors in wallet address calculation. Jetton Wallet address is deterministic function of (owner_address, jetton_master_address, init_code). Any error in init_data leads to off-chain computed address not matching deployed address.

Timeline for standard Jetton with mint/burn and basic documentation: 3–5 working days. With custom logic (fees, whitelist, vesting) — 1–2 weeks.