Разработка Jetton-токена (TON)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка Jetton-токена (TON)
Средний
~2-3 дня
Часто задаваемые вопросы

Направления блокчейн-разработки

Этапы блокчейн-разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    919
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1147
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    609
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    884

Разработка Jetton-токена (TON)

Jetton — стандарт токенов в TON блокчейне, аналог ERC-20 на Ethereum. Если вы пришли с Ethereum-бэкграундом, первое что нужно понять: архитектура принципиально другая. В EVM токен — это один контракт с mapping балансов. В TON токен — это система контрактов: один Jetton Master и N Jetton Wallet (по одному на каждого держателя). Это не особенность реализации — это фундаментальное следствие архитектуры TON с шардингом.

Архитектура: Jetton Master + Jetton Wallet

Jetton Master хранит метаданные токена (name, symbol, decimals, totalSupply) и содержит логику минтинга. Он не хранит балансы — он их не знает.

Jetton Wallet — отдельный контракт для каждого адреса. Развёртывается автоматически при первой отправке токенов на адрес. Хранит баланс конкретного пользователя и обрабатывает transfer.

Alice → (transfer) → Alice's Jetton Wallet
Alice's Jetton Wallet → (internal message) → Bob's Jetton Wallet
Bob's Jetton Wallet: принимает токены, увеличивает баланс

Почему так устроено? TON использует sharding: разные контракты могут находиться в разных шардах. Если бы балансы хранились в одном месте, каждый transfer создавал бы cross-shard транзакцию — узкое место. С distributed wallet-ами transfer между двумя кошельками — локальная операция в одном шарде.

Разработка на Tact

Tact — высокоуровневый язык для TON контрактов, предпочтительнее FunC для новых проектов из-за читаемости и типобезопасности.

Jetton Master на 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 на 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;

        // Адрес кошелька получателя вычисляется детерминированно
        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 и TON fee: особенности

В TON каждое сообщение (message) несёт TON для оплаты gas дальнейших операций. При transfer вы должны отправить достаточно TON, чтобы покрыть: gas для Jetton Wallet отправителя, gas для Jetton Wallet получателя (включая деплой, если он новый), опциональный forwardAmount для уведомления смарт-контракта получателя.

Типичная стоимость Jetton transfer: 0.05–0.1 TON на операцию. Для новых получателей (деплой кошелька) — немного дороже (~0.05 TON extra на storage fees). Недостаточное количество TON в сообщении приведёт к bounce — транзакция отменяется, токены возвращаются.

Bounce handling — обязательная часть реализации. TON уникален тем, что failed транзакции возвращают сообщение отправителю через bounce. Jetton Wallet должен обрабатывать bounced сообщения и возвращать баланс:

bounced(msg: bounced<TokenTransferInternal>) {
    // Возвращаем баланс если transfer не прошёл
    self.balance += msg.amount;
}

Метаданные токена

TON использует off-chain метаданные через стандарт TEP-64. Метаданные хранятся в IPFS или на HTTPS, ссылка записывается в Jetton Master:

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

В контракте ссылка на метаданные кодируется в cell как snake-encoded string или offchain content URL. Стандарт предусматривает оба формата: полностью on-chain (все данные в cell) или off-chain со snake URL.

Интеграция с TON Connect и кошельками

Для DApp интеграции — TON Connect 2.0. SDK для 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);

        // Получаем адрес Jetton Wallet отправителя
        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,
        });
    }
}

Тестирование с Blueprint

Blueprint — официальный тестовый фреймворк для TON контрактов. Поддерживает sandbox окружение с симуляцией блокчейна:

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 к Alice
        await jettonMaster.sendMint(deployer.getSender(), alice.address, toNano('1000'));

        // Проверяем баланс
        const aliceWallet = await jettonMaster.getWalletAddress(alice.address);
        const balance = await aliceWallet.getBalance();
        expect(balance).toEqual(toNano('1000'));

        // Transfer от Alice к 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: что чаще всего ломается

Недостаточно TON в сообщении. Самая частая причина failed transfer-ов. Решение: всегда проверять minTonsForStorage + tonBalanceBeforeMsg в тестах.

Некорректная обработка bounce. Если Jetton Wallet получателя не существует и кошелёк не будет создан (недостаточно TON) — сообщение вернётся как bounced. Без правильного bounce handler баланс отправителя потеряется навсегда.

Ошибки в вычислении адреса кошелька. Адрес Jetton Wallet — детерминированная функция от (owner_address, jetton_master_address, init_code). Любая ошибка в init_data приведёт к тому, что вычисленный off-chain адрес не совпадёт с тем, что деплоит контракт.

Срок разработки стандартного Jetton с mint/burn и базовой документацией: 3–5 рабочих дней. С кастомной логикой (комиссии, whitelist, vesting) — 1–2 недели.