December 3, 2025

Учимся 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();

Собирается «данные» для кошелька:

  • баланс — 0
  • владелец — сам контракт
  • мастер-адрес джеттона
  • код кошелька

let jetton_wallet: Address = contractAddressExt(0, jetton_code, jetton_data);
self.jetton_wallet = jetton_wallet;
  • высчитывается адрес 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.

🎯 РЕЗЮМЕ ПРОСТЫМИ СЛОВАМИ

  1. Контракт принимает TON и продаёт за них джеттоны.
  2. Курс фиксированный: 1 TON = 2 джеттона.
  3. Покупатель просто отправляет TON на контракт → получает джеттоны.
  4. Контракт вычисляет адрес своего Jetton Wallet при инициализации.
  5. Владелец может снять TON командой "withdraw", но всегда останется 0.01 TON для хранения.

    https://studio.rubeton.app/