Разработка Jetton-стандарта на TON
Если вы разрабатывали токены на EVM — TON потребует перестройки мышления. Не косметической, а принципиальной. Модель аккаунтов здесь противоположная: вместо центрального контракта с mapping(address → balance) каждый держатель токена имеет собственный смарт-контракт — Jetton Wallet. Это асинхронная, шардированная архитектура с message-passing вместо прямых вызовов. И у неё есть конкретные последствия для разработки.
Архитектура Jetton: два контракта
Jetton Master — центральный контракт, хранит метаданные токена (name, symbol, decimals, total_supply) и умеет минтить новые Jetton Wallets.
Jetton Wallet — один экземпляр на каждого держателя. Хранит баланс конкретного адреса. При transfer — Jetton Wallet отправителя шлёт сообщение Jetton Wallet получателя.
Transfer flow (TEP-74):
User A → [internal message] → Jetton Wallet A → [internal message] → Jetton Wallet B → User B notified
Это не один атомарный вызов функции как в EVM — это цепочка асинхронных сообщений. Если Jetton Wallet получателя ещё не существует — он создаётся в момент первого получения токена, и отправитель платит за деплой (~0.04 TON storage deposit).
Стандарт TEP-74 и TEP-64
TEP-74 (Fungible tokens) — основной стандарт Jetton. Определяет структуру сообщений:
;; Jetton Wallet: обработка transfer сообщения
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; op::transfer = 0xf8a7ea5
if (op == op::transfer()) {
int query_id = in_msg_body~load_uint(64);
int jetton_amount = in_msg_body~load_coins();
slice to_owner_address = in_msg_body~load_msg_addr();
slice response_address = in_msg_body~load_msg_addr();
cell custom_payload = in_msg_body~load_maybe_ref();
int forward_ton_amount = in_msg_body~load_coins();
slice forward_payload = in_msg_body;
;; Проверяем баланс
throw_unless(error::not_enough_jettons, jetton_amount <= balance);
balance -= jetton_amount;
save_data();
;; Отправляем сообщение на Jetton Wallet получателя
var msg_body = begin_cell()
.store_uint(op::internal_transfer(), 32)
.store_uint(query_id, 64)
.store_coins(jetton_amount)
.store_slice(my_address()) ;; from_address
.store_slice(response_address)
.store_coins(forward_ton_amount)
.store_slice(forward_payload)
.end_cell();
;; Адрес Jetton Wallet получателя вычисляется детерминировано
var to_wallet_address = calc_jetton_wallet_address(to_owner_address);
send_raw_message(
begin_cell()
.store_uint(0x18, 6)
.store_slice(to_wallet_address)
.store_coins(forward_ton_amount + min_ton_for_storage)
.store_uint(1, 107)
.store_ref(msg_body)
.end_cell(),
64 ;; carry remaining gas
);
}
}
TEP-64 (Token Data Standard) — стандарт метаданных. Metadataмогут храниться on-chain (в cell структурах) или off-chain (snake-encoded URI):
;; Jetton Master: get-метод для метаданных
cell get_jetton_data() method_id {
return begin_cell()
.store_coins(total_supply)
.store_int(mintable, 1)
.store_slice(admin_address)
.store_ref(jetton_content) ;; cell с метаданными
.store_ref(jetton_wallet_code)
.end_cell();
}
Метаданные в jetton_content — либо off-chain URI (0x01 prefix), либо on-chain snake-encoded dictionary:
# Структура on-chain metadata cell:
{
"name": "My Token",
"description": "Token description",
"symbol": "MTK",
"decimals": "9",
"image": "https://example.com/logo.png"
}
Tact vs FunC: выбор языка
FunC — нативный язык TON. Низкоуровневый, похож на C. Полный контроль над gas и структурой ячеек. Производительность максимальная, но читаемость кода — минимальная.
Tact — высокоуровневый язык, компилируется в FunC. Синтаксис ближе к TypeScript. Значительно проще для разработчиков приходящих с EVM. На сегодня (2024–2025) — рекомендуемый выбор для новых проектов:
// Jetton Master на Tact
import "@stdlib/deploy";
import "@stdlib/jetton";
contract JettonMaster with Deployable, Jetton {
totalSupply: Int as coins;
owner: Address;
content: Cell;
mintable: Bool;
init(owner: Address, content: Cell) {
self.totalSupply = 0;
self.owner = owner;
self.content = content;
self.mintable = true;
}
receive(msg: TokenMint) {
require(sender() == self.owner, "Not owner");
require(self.mintable, "Not mintable");
self.totalSupply += msg.amount;
// Деплой Jetton Wallet для получателя
let winit: StateInit = self.getJettonWalletInit(msg.receiver);
let walletAddress: Address = contractAddress(winit);
send(SendParameters{
to: walletAddress,
value: ton("0.05"),
mode: SendIgnoreErrors,
bounce: false,
body: TokenTransferInternal{
queryId: 0,
amount: msg.amount,
from: myAddress(),
responseAddress: msg.receiver,
forwardTonAmount: 0,
forwardPayload: emptySlice(),
}.toCell(),
code: winit.code,
data: winit.data,
});
}
}
Газ и storage: специфика TON
В TON газ устроен иначе чем в EVM. Ключевые отличия:
Storage fee — аккаунты платят аренду за хранение данных. Если баланс TON на Jetton Wallet упадёт до нуля — аккаунт заморозится, данные потеряются. Это реальная проблема для holder'ов с пылевыми балансами. Стандартный минимум для Jetton Wallet: ~0.05 TON.
Forward TON — при отправке Jetton с forward_ton_amount > 0 получатель контракт получает уведомление с прикреплёнными TON. Это паттерн для интеграции с DeFi протоколами TON: вместо approve + transferFrom вы делаете transfer с payload в forward_payload, который целевой контракт обрабатывает при получении Jetton.
// Transfer с payload для DeFi интеграции
// Аналог ERC-20 approve+transferFrom, но в TON-стиле
message(0x7362d09c) TokenNotification {
queryId: Int as uint64;
amount: Int as coins;
from: Address;
forwardPayload: Slice as remaining; // кастомные данные
}
// В целевом контракте (например, DEX)
receive(msg: TokenNotification) {
// Токены получены, msg.forwardPayload содержит инструкции
// например: "swap to другой токен"
let swapInstruction: SwapPayload = SwapPayload.fromSlice(msg.forwardPayload);
self.executeSwap(msg.from, msg.amount, swapInstruction);
}
Тестирование
Sandbox (Blueprint) — официальный фреймворк для тестирования TON контрактов в TypeScript:
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { JettonMaster } from '../wrappers/JettonMaster';
import { JettonWallet } from '../wrappers/JettonWallet';
import '@ton/test-utils';
describe('Jetton', () => {
let blockchain: Blockchain;
let deployer: SandboxContract<TreasuryContract>;
let jettonMaster: SandboxContract<JettonMaster>;
beforeEach(async () => {
blockchain = await Blockchain.create();
deployer = await blockchain.treasury('deployer');
jettonMaster = blockchain.openContract(
await JettonMaster.fromInit(deployer.address, buildMetadataCell())
);
await jettonMaster.send(deployer.getSender(), { value: toNano('0.1') }, {
$$type: 'Deploy',
queryId: 0n,
});
});
it('should mint tokens', async () => {
const receiver = await blockchain.treasury('receiver');
const mintResult = await jettonMaster.send(
deployer.getSender(),
{ value: toNano('0.2') },
{
$$type: 'TokenMint',
queryId: 0n,
amount: toNano('1000'),
receiver: receiver.address,
}
);
expect(mintResult.transactions).toHaveTransaction({
from: jettonMaster.address,
deploy: true, // Jetton Wallet задеплоен
success: true,
});
const walletAddress = await jettonMaster.getGetWalletAddress(receiver.address);
const wallet = blockchain.openContract(JettonWallet.fromAddress(walletAddress));
const data = await wallet.getGetWalletData();
expect(data.balance).toBe(toNano('1000'));
});
});
Особенности кастомного Jetton
Кастомизация Jetton — это изменение Jetton Wallet контракта. Все дополнительные features (transfer tax, whitelist, vesting) реализуются в recv_internal Jetton Wallet, а не в Master. Это нетипично для EVM разработчиков.
Transfer tax:
;; В Jetton Wallet, перед отправкой internal_transfer
if (has_transfer_tax()) {
int tax_amount = (jetton_amount * tax_bps) / 10000;
int send_amount = jetton_amount - tax_amount;
;; Отправляем tax на treasury wallet
send_jettons(treasury_wallet_address, tax_amount, query_id);
;; Отправляем остаток получателю
send_jettons(to_wallet_address, send_amount, query_id);
}
Что входит в работу
Разработка Jetton Master + Jetton Wallet на Tact (или FunC по требованию), тестирование через Blueprint sandbox, деплой на TON mainnet, верификация через tonviewer.com, wrapper-скрипты на TypeScript для интеграции. Срок: 5–10 дней для стандартного Jetton без кастомной логики, 2–4 недели для Jetton с кастомными механиками (vesting, whitelist, tax).







