Учимся Tact: создаём и разбираем контракт продажи джеттонов в TON
📘 Общая идея контракта
Контракт продаёт джеттоны за TON.
Ты отправляешь TON → контракт отправляет тебе джеттоны по фиксированному курсу.
Владелец контракта может забирать накопившиеся TON командой "withdraw".
📌 Разбор кода по частям
1. Импорты
import "@stdlib/deploy"; import "@stdlib/ownable";
- Deployable — позволяет удобно деплоить контракт.
- Ownable — добавляет механику владельца (owner) + проверку
requireOwner().
2. Объявление контракта
contract RubetonSeller with Deployable, Ownable {
owner: Address;
jetton_wallet: Address;
- owner — владелец, который может делать
withdraw. - jetton_wallet — адрес кошелька джеттона, на который нужно отправлять запрос «выпустить/отправить» джеттоны покупателю.
3. Константы
const TokenCourse: Int = 2;
const MinTonForStorage: Int = ton("0.01");
TokenCourse = 2
Это курс:
1 TON → 2 Rubeton (джеттона).
MinTonForStorage = 0.01 TON
Это минимальный остаток TON, который всегда должен лежать на контракте.
Он нужен, чтобы контракт не "умер" от нехватки газа.
4. init — код инициализации
init(jetton_code: Cell, jetton_master: Address) {
self.owner = address("UQCDnkR1NOwZUzARCLDAY5Z6YuD1k_LVs5iUVUBOiuUJKtOI");
Здесь вручную прописывается owner, владелец контракта.
🧩 Создание Jetton-кошелька контракта
Контракт сам создаёт свой Jetton Wallet.
let jetton_data: Cell = beginCell()
.storeCoins(0)
.storeAddress(myAddress())
.storeAddress(jetton_master)
.storeRef(jetton_code)
.endCell();
Собирается «данные» для кошелька:
let jetton_wallet: Address = contractAddressExt(0, jetton_code, jetton_data); self.jetton_wallet = jetton_wallet;
Теперь контракт знает, куда ему отправлять команду на начисление джеттонов.
5. Метод receive("withdraw") — вывод TON владельцем
Этот метод вызывается, если отправить в контракт текстовое сообщение "withdraw".
receive("withdraw") {
let need_to_withdraw = myBalance() - context().value - self.MinTonForStorage;
myBalance()— сколько TON на контрактеcontext().value— сколько TON пришло с этим сообщением- вычитаем минимальный остаток
Получаем: сколько TON можно вывести, не затронув 0.01 TON для хранения.
Проверки
self.requireOwner(); require(myBalance() >= need_to_withdraw, "balance muss be bigger than withdraw amount");
Отправка TON владельцу
send(SendParameters{
to: sender(),
value: need_to_withdraw,
mode: SendIgnoreErrors + SendRemainingValue,
bounce: true
});
- средства отправляются тому, кто вызвал
"withdraw"(владельцу) SendRemainingValueотдаёт всё указанное количествоSendIgnoreErrorsигнорирует ошибки доставки
6. receive() — обработка обычного платежа TON (покупка джеттонов)
Этот метод вызывается, когда кто-то просто отправляет TON на контракт без текста.
let need_to_send: Int = context().value * self.TokenCourse;
Считается, сколько джеттонов нужно отправить покупателю:
TON × курс = количество джеттонов
Например:
Отправили 5 TON → курс 2 → получишь 10 Rubeton.
🧩 Формируем тело сообщения для Jetton Wallet
let send_body: Cell = beginCell()
.storeUint(0xf8a7ea5, 32) // jetton transfer opcode
.storeUint(0, 64) // query_id
.storeCoins(need_to_send) // amount
.storeAddress(sender()) // recipient
.storeAddress(myAddress()) // response destination
.storeUint(0, 1)
.storeCoins(1) // forward TON amount
.storeUint(0, 1)
.endCell();
Это стандартное тело сообщения “transfer” для джеттонов.
- opcode:
0xf8a7ea5— команда «отправить джеттоны» - отправляем
need_to_send - отправляем их отправителю TON
- пересылаем 1 нанотон вперёд чтобы Jetton Wallet смог выслать внутреннее сообщение
Отправляем сообщение в Jetton Wallet
send(SendParameters{
to: self.jetton_wallet,
value: 0,
mode: SendIgnoreErrors + SendRemainingValue,
bounce: true,
body: send_body
});
Контракт отправляет команду Jetton Wallet:
«Переведи N джеттонов пользователю»
💻 Полный код с комментариями
import "@stdlib/deploy";
import "@stdlib/ownable";
// Контракт-продавец джеттонов Rubeton
contract RubetonSeller with Deployable, Ownable {
owner: Address; // владелец контракта
jetton_wallet: Address; // адрес кошелька контракта для отправки джеттонов
// Курс продажи: 1 TON = 2 RUBETON
const TokenCourse: Int = 2;
// Минимальный TON, который должен оставаться на контракте
const MinTonForStorage: Int = ton("0.01");
// Инициализация контракта
init(jetton_code: Cell, jetton_master: Address) {
// Адрес владельца жёстко прописывается
self.owner = address("UQCDnkR...JKtOI");
// Формируем данные Jetton Wallet для вычисления адреса
let jetton_data = beginCell()
.storeCoins(0) // баланс кошелька = 0
.storeAddress(myAddress()) // владелец — сам контракт
.storeAddress(jetton_master) // мастер-адрес Jetton
.storeRef(jetton_code) // код кошелька
.endCell();
// Вычисляем адрес Jetton Wallet
let jetton_wallet = contractAddressExt(0, jetton_code, jetton_data);
// Сохраняем адрес Jetton Wallet
self.jetton_wallet = jetton_wallet;
}
// ВЫВОД ТОN владельцем
receive("withdraw") {
// Сколько TON можно вывести, оставив минимум для работы
let need_to_withdraw =
myBalance() - context().value - self.MinTonForStorage;
self.requireOwner(); // выводить может только владелец
require(
myBalance() >= need_to_withdraw,
"not enough balance"
);
// Отправляем TON владельцу
send(SendParameters{
to: sender(),
value: need_to_withdraw,
mode: SendIgnoreErrors + SendRemainingValue,
bounce: true
});
}
// ПОКУПКА ДЖЕТТОНОВ: обычный перевод TON без текста
receive() {
// Сколько джеттонов нужно отправить покупателю
let need_to_send = context().value * self.TokenCourse;
// Формируем тело сообщения Jetton Transfer
let send_body = beginCell()
.storeUint(0xf8a7ea5, 32) // opcode: transfer
.storeUint(0, 64) // query_id
.storeCoins(need_to_send) // amount — сколько джеттонов отправить
.storeAddress(sender()) // адрес покупателя
.storeAddress(myAddress()) // адрес контракта
.storeUint(0, 1)
.storeCoins(1) // forward fee
.storeUint(0, 1)
.endCell();
// Команда Jetton Wallet: отправить пользователю джеттоны
send(SendParameters{
to: self.jetton_wallet,
value: 0,
mode: SendIgnoreErrors + SendRemainingValue,
bounce: true,
body: send_body
});
}
}
🎯 Итоговое описание
Контракт RubetonSeller реализует простую и надёжную систему продажи джеттонов:
- Пользователь отправляет TON → контракт автоматически пересчитывает количество джеттонов.
- Контракт формирует Jetton Transfer и передаёт его в Jetton Wallet.
- Jetton Wallet отправляет пользователю нужное количество джеттонов.
- Владелец может вывести накопившиеся TON, но контракт оставляет минимальный резерв для хранения (0.01 TON).
- Курс задаётся константой
TokenCourse.
- Контракт принимает TON и продаёт за них джеттоны.
- Курс фиксированный: 1 TON = 2 джеттона.
- Покупатель просто отправляет TON на контракт → получает джеттоны.
- Контракт вычисляет адрес своего Jetton Wallet при инициализации.
- Владелец может снять TON командой
"withdraw", но всегда останется 0.01 TON для хранения.
https://studio.rubeton.app/