Розробка 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 транзакцію — вузьке місце. З розподіленими гаманцями 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');

        // Мінтимо 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 тижні.