Загрузка...
Ton + Python on the example of purchasing automation on Fragment
  1. Апатия
    Апатия Topic starter Jun 5, 2025 444 1206 Jun 15, 2021
    Доброго времени суток! :sueta:


    Сегодня хочу немного рассказать о том как работать с блокчейном TON при помоще Python, а так же разберем, как работает Fragment и купим звездочек при помощи скрипта.

    Предоставленный код является лишь демонстрацией, и требует некоторых доработок для безошибочной работы. Не отправляйте большие суммы без полной отладки кода!

    Код целиком и без пояснений внизу статьи
    А еще под конец статьи по какой то неведомой мне причине ломаются код блоки, возможно лимит, уж извиняйте



    Итого: у нас есть 2 проблемы:
    Первая, это нужно разобраться как нам работать с транзакциями.
    Вторая, как нам работать с фрагментом.

    Итак, начать очевидно стоит с создания кошелька и его инициализации при помощи вызова контракта. В целом этот шаг можно спокойно пропустить, и создать кошелек в том же тонкипере, однако учтите, что он должен быть обязательно инициализирован. Я же хочу это сделать с помощью кода.

    Создание кошелька

    Для создания кошелька мы будем использовать библиотеку
    ⁡tonsdk
    ⁡ , а вернее нам нужно всего 2 метода из нее, это
    ⁡Wallets
    и
    ⁡WalletVersionEnum

    Устанавливаем:
    ⁡pip install tonsdk

    Так же сразу начнем выстраивать небольшую структуру, чтобы не лепить все в кучу, условно для работы с кошельком у нас будет файл
    ⁡WalletUtils.py
    ⁡ В нем мы создадим одноименный класс, в котором у нас пока будут сразу две функции,
    ⁡create_wallet
    ⁡ будет отвечать за создание и сохранения нашего кошелька в файл,
    ⁡_return_wallet_data
    ⁡ же будет вспомогательная для структуризации данных этого кошелька.


    Python
    from json import dumps
    from pathlib import Path

    from tonsdk.contract.wallet import Wallets, WalletVersionEnum

    # Класс с для работы с кошельком
    class WalletUtils:
    @staticmethod
    def _return_wallet_data(mnemonics, pub_k, priv_k, wallet_address):
    """
    Формирует словарь с данными созданного кошелька.
    """
    return {
    "wallet_address": wallet_address.address.to_string(True, True, True),
    "mnemonics": mnemonics,
    "public_key": pub_k.hex(),
    "private_key": priv_k.hex(),
    "wallet_address_testnet": wallet_address.address.to_string(True, True, True, True)
    }

    def create_wallet(self, version="v4r2", save_to_file=False, save_dir="created_wallets/wallets_data.txt"):
    """
    Создаёт новый кошелёк TON заданной версии. По умолчанию используется версия v4r2.

    :param version: Версия кошелька (например, "v3r2", "v4r2").
    :param save_to_file: Сохранять ли данные в файл.
    :param save_dir: Путь до файла, в который будут сохранены данные.
    :return: Кортеж (данные_кошелька, объект_кошелька)
    """
    # Создание кошелька с заданной версией
    mnemonics, pub_k, priv_k, wallet_address = Wallets.create(
    version=getattr(WalletVersionEnum, version),
    workchain=0
    )

    # Формирование словаря с данными
    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    # Если указано — сохраняем в файл
    if save_to_file:
    Path(save_dir).parent.mkdir(parents=True, exist_ok=True)
    with open(save_dir, "a+", encoding="utf-8") as f:
    f.write(dumps(wallet_data, indent=4, ensure_ascii=False) + "\n\n")

    return wallet_data, wallet_address
    Вызваный нами метод
    ⁡Wallets.create
    ⁡ принимает версию и воркчейн кошелька который мы хотим создать, я выбираю версию v4r2 так как она банально современнее, но сильной разницы в том какую вы выбирите в целом нет, мы можем заглянуть в этот метод и увидеть какие версии поддерживаются.

    [IMG]
    Воркчейн, это логическая часть блокчейна, 0 - Основаня клиентская часть, мы будем использовать только ее, с этим можно даже не заморачиваться.

    Сам же метод целиком нам возвращает мнемотику, приватный и публичный ключи, а так же адрес нашего новоиспеченного кошелька, мы это записываем себе.

    А зачем множественное True у наших
    ⁡wallet_address
    ⁡ и
    ⁡wallet_address_testnet
    ⁡ ?

    Если кратко, то так надо. Это самый распространённый вариант — его и стоит использовать по умолчанию.
    Это параметры
    ⁡user_friendly=True url_safe=True bounceable=True
    ⁡ ,а четвертый в
    ⁡wallet_address_testnet
    ⁡ , это
    ⁡is_test_only=True
    ⁡ так мы получим адрес от тестнета, он нужен чтобы тестить наш код без траты реальных средств, покажу чуть позже как это работает.

    Грубо говоря эти 3 тру означают "Верни мне адрес в удобном виде, bounceable, и подходящий для URL."
    user_friendly отвечает за вывод адреса в читаемом виде, а не в хексе, bounceable означает, что адрес должен поддерживать bounceable сообщения и url_safe что мы хотим видеть адрес в безопасном base64 алфавите, без знаков "+" "-" и прочих
    Давайте запустим и посмотрим что вышло, создадим файл
    ⁡main.py
    ⁡ и импортируем туда наш класс, а после вызовем создание кошелька и сразу же его сохраним в файл


    Python
    from wallet.WalletUtils import WalletUtils # Импорт нашего класса

    if __name__ == '__main__':
    WalletUtils = WalletUtils()
    print(WalletUtils.create_wallet(save_to_file=True))
    [IMG]
    Отлично! Мы только что создали кошелек, хотя подождите, ведь блокчейн даже не знает о его существовании!
    Инициализация кошелька

    Для его записи в блокчейн и дальнейшей работы с ним, нам требуется его инициализировать, для этого мы для нашего класса
    ⁡WalletUtils
    ⁡ создадим еще 2 метода,
    ⁡wallet_from_mnemonics
    ⁡ который будет использоватся для получения данных о кошельке из сид фразы, он не обязателен, так как мы уже получили все данные о кошельке и записали их в файл, но очень удобен если у нас сохранена лишь сид фраза нашего кошелька.
    ⁡_init_wallet_async
    ⁡ будет у нас ассинхронной функцией для взаимодействия с контрактом инициализации, ассинхронность нужна для того, чтобы наш код не зависал во время ожидания ответа от сервера, ну и третяя это функция заглушка
    ⁡init_wallet
    ⁡ через которую мы будем запускать нашу ассинхронную функцию.

    Для взаимодействия с блокчейном, воспользуемся библиотекой
    ⁡TonTools
    ⁡ Устанавливаем
    ⁡pip install tontools


    Пишем код. Так же добавим логгинг для красоты происходящего

    Python

    import asyncio
    import logging
    from tonsdk.contract.wallet import Wallets, WalletVersionEnum
    from tonsdk.utils import bytes_to_b64str
    from TonTools import TonCenterClient # Клиент для взаимодействия с блокчейном TON

    class WalletUtils:

    def wallet_from_mnemonics(self, mnemonics: list, version="v4r2"):
    # Восстанавливаем кошелёк из мнемоники
    mnemonics, pub_k, priv_k, wallet_address = Wallets.from_mnemonics(
    mnemonics=mnemonics,
    version=getattr(WalletVersionEnum, version),
    workchain=0
    )

    # Формируем словарь с адресами и ключами
    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    # Возвращаем и данные, и сам объект кошелька
    return wallet_data, wallet_address

    def init_wallet(self, mnemonics, testnet=False):
    # Синхронно запускаем асинхронную инициализацию (удобно для внешнего кода)
    result = asyncio.run(self._init_wallet_async(mnemonics, testnet))
    return result

    async def _init_wallet_async(self, mnemonics, testnet=False):
    # Получаем данные и объект кошелька
    wallet_data, wallet_address = self.wallet_from_mnemonics(mnemonics=mnemonics)

    # Инициализируем TonCenter-клиент для отправки сообщения в сеть
    provider = TonCenterClient(testnet=testnet)

    # Создаём deploy-сообщение для регистрации кошелька в блокчейне
    query = wallet_address.create_init_external_message()

    # Переводим сообщение в base64 — формат, нужный API TonCenter
    deploy_message = bytes_to_b64str(query['message'].to_boc(False))

    # Отправляем сообщение.
    if await provider.send_boc(deploy_message) != 200:
    logging.error('Init error or wallet already init')
    else:
    logging.info("Wallet inited!")
    Python
    import asyncio
    import logging

    from tonsdk.contract.wallet import Wallets, WalletVersionEnum
    from json import dumps
    from pathlib import Path
    from TonTools import TonCenterClient

    from tonsdk.utils import bytes_to_b64str


    class WalletUtils:
    @staticmethod
    def _return_wallet_data(mnemonics, pub_k, priv_k, wallet_address):
    return {
    "wallet_address": wallet_address.address.to_string(True, True, True),
    "mnemonics": mnemonics,
    "public_key": pub_k.hex(),
    "private_key": priv_k.hex(),
    "wallet_address_testnet": wallet_address.address.to_string(True, True, True, True)
    }

    def create_wallet(self, version="v4r2", save_to_file=False, save_dir="created_wallets/wallets_data.txt"):
    mnemonics, pub_k, priv_k, wallet_address = Wallets.create(version=getattr(WalletVersionEnum, version),
    workchain=0)

    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    if save_to_file:
    Path(save_dir).parent.mkdir(parents=True, exist_ok=True)
    with open(save_dir, "a+", encoding="utf-8") as f:
    f.write(dumps(wallet_data, indent=4, ensure_ascii=False) + "\n\n")

    return wallet_data, wallet_address

    def wallet_from_mnemonics(self, mnemonics: list, version="v4r2"):
    mnemonics, pub_k, priv_k, wallet_address = Wallets.from_mnemonics(mnemonics=mnemonics,
    version=getattr(WalletVersionEnum, version),
    workchain=0)

    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    return wallet_data, wallet_address

    def init_wallet(self, mnemonics, testnet=False):
    result = asyncio.run(self._init_wallet_async(mnemonics, testnet))
    return result

    async def _init_wallet_async(self, mnemonics, testnet=False):
    wallet_data, wallet_address = self.wallet_from_mnemonics(mnemonics=mnemonics)
    provider = TonCenterClient(testnet=testnet)
    query = wallet_address.create_init_external_message()
    deploy_message = bytes_to_b64str(query['message'].to_boc(False))

    if await provider.send_boc(deploy_message) != 200:
    logging.error('Init error or wallet already init')
    else:
    logging.info("Wallet inited!")

    Пробуем!

    Python
    import json
    from wallet.WalletUtils import WalletUtils # Импорт нашего класса


    if __name__ == '__main__':
    WalletUtils = WalletUtils()
    # print(WalletUtils.create_wallet(save_to_file=False))

    # Загружаем нашу мнемотику
    with open("created_wallets/wallets_data.txt", "r") as f:
    mnemonics = json.load(f)['mnemonics']

    WalletUtils.init_wallet(mnemonics=mnemonics, testnet=True)
    [IMG]

    Кажется что то пошло не так. А все потому что нам для инициализации обязательно нужен баланс, стоимость процедуры составляет 0.005 TON1,29 рублей
    Так как я работаю с тестовой сетью я просто запрошу 2 тестовых TON через бота Telegram: TESTGIVER_TON_BOT
    Отправляю ему свой testnet адрес кошелька и спустя минуту получаю свои 2 ton, увидеть это можно на сканере testnet.tonviewer.com если вы делаете это в основной сети, то свой кошелек можно смотреть здесь tonviewer.com
    Как можно заметить на кошельке стоит пометка о том, что кошелек еще не инициализирован.

    [IMG]
    После того как получили баланс, запускаем код еще разок.
    [IMG]
    Успех! Смотрим что на сканере.
    [IMG]
    Как видим наш клшелек теперь активен.

    Отправка транзакции

    Отлично, теперь давайте попробуем отправить куда-нибудь транзакцию, например создадим еще кошелек через наш метод
    ⁡WalletUtils.create_wallet
    ⁡ инициализировать его не обязательно, нам просто нужно получить адрес для отправки.

    Теперь напишем код для отправки средств. Для отправки средств продолжим использовать TonTools. Так же не забываем о структуре, создадим файл
    ⁡Transactions.py
    ⁡ опять же с одноименным классом и двумя методами
    ⁡_send_ton_async
    ⁡ для отправки средств и классическая заглушка в виде
    ⁡send_ton
    Python
    import logging
    from TonTools import TonCenterClient, Wallet
    import asyncio
    from tonsdk.utils import from_nano


    class Transactions:
    @staticmethod
    async def _send_ton_async(mnemonics, destination_address, amount, payload, nano_amount, version, testnet, send_mode):
    # Инициализируем TonCenter-клиент (для testnet или mainnet)
    provider = TonCenterClient(testnet=testnet)

    # Импортируем кошелёк из мнемоники
    wallet = Wallet(mnemonics=mnemonics, version=version, provider=provider)

    # Если передаётся сумма в нанотонах — конвертируем в TON
    if nano_amount:
    if isinstance(amount, str):
    amount = int(amount)
    amount = from_nano(amount, "ton")


    # Удаляем переносы строк из payload'а для читаемости лога
    clean_payload = payload.replace("\n", " ")

    # Выводим предупреждение с параметрами транзакции
    logging.warning(f'Sending {amount} TON to {destination_address} with payload: {clean_payload}')

    # Отправляем средства через метод transfer_ton
    if await wallet.transfer_ton(destination_address=destination_address, amount=amount,
    message=payload, send_mode=send_mode) != 200:
    logging.error("Transaction failed!") # Ошибка при отправке
    return 0
    else:
    logging.info("Sending successful!") # Успешная отправка
    return 1

    def send_ton(self, mnemonics, destination_address, amount, payload, nano_amount=True, version='v4r2', testnet=False, send_mode=0):
    # Запускаем асинхронную отправку синхронно через asyncio.run()
    result = asyncio.run(self._send_ton_async(
    mnemonics, destination_address, amount, payload, nano_amount, version, testnet, send_mode
    ))
    return result
    Из новых и неизвестных терминов здесь имеются
    ⁡payload, nano_amount, send_mode

    С payload все просто, грубо говоря это комментарий в транзакции или же memo tag, как любят говорить биржи, служит либо для идентификации платежей той же биржей либо для простой передачи информации вместе с транзакцией.

    nano_amount в контексте нашей функции это флаг, который если мы ставим на True мы даем функции понять, что мы передаем сумму транзакции в так называемых нанотонах, грубо говоря копейках. Так как метод
    ⁡transfer_ton
    из
    ⁡TonTools
    ⁡ ожидает, что мы передадим ему сумму в нормальном виде мы страхуемся и в случае чего переводим нанотоны в обычные тоны, для этого есть метод
    ⁡from_nano
    ⁡ из уже известной нам библиотеки
    ⁡tonsdk


    И последнее, но не по значению это send_mode, очень важная часть транзакции, отвечает за то, в каком виде будет отправлена транзакция, всего есть 2 основных значения, это 0 и 1 , по умолчанию у нас стоит 0, и это означает, что комиссия за транзакцию будет вычитаться из введенной нами суммы, покажу на примере, мы отправляем 1 TON и указываем send_mode 0, допустим комиссия при отправке составила 0.007 TON итого получатель получит
    ⁡1 - 0.007 = 0.993 TON
    ⁡ , но если же мы к нашему send_mode прибавим 1 и отправим ту же транзакцию, то комиссия будет покрыта отдельно с нашего кошелька, а получатель получит ровно ту сумму которую мы указали, то есть 1 TON. Почему это важно? Те кто хоть раз оплачивал при помощи криптовалюты знает, что сервисы требуют отправки точной суммы, но ведь мы не можем заранее знать какая комиссия, чтобы заложить ее в сумму транзакции, для этого мы используем сенд мод 1, чтобы получатель получил ровно ту сумму что мы отправили.

    [IMG]
    Давайте попробуем отправить транзакцию в 0.1 TON с каким-нибудь сообщением.
    Python
    import json
    from wallet.Transactions import Transactions
    # from wallet.WalletUtils import WalletUtils


    if __name__ == '__main__':
    # WalletUtils = WalletUtils()
    # print(WalletUtils.create_wallet(save_to_file=False))
    Transactions = Transactions()

    with open("created_wallets/wallets_data.txt", "r") as f:
    mnemonics = json.load(f)['mnemonics']

    # Используем send_mode 1 и указываем сумму НЕ в нанотонах, транзакция в тестнете
    Transactions.send_ton(mnemonics, "kQDMyvHYZQtwW-AdzWDPviTPa2QQd6O6rYXA6UP8eTPJ5Goh", 0.1, "Hello Lolz",
    nano_amount=False, send_mode=1, testnet=True)
    [IMG]
    [IMG]
    Ура! Получили ровно ту сумму которую указали, так как указали send_mode=1 все работает!

    Это был краткий экскурс в отправку TON, теперь давайте попробуем воспользоваться тем что написали и купить себе звездочек на Fragment.

    Разбираем как работает Fragment


    Итак, наша вторая проблема, это понять как работает Fragmnet, так как официального api у него нет.
    Начнем свое исследование с того, что авторизуемся на фрагмент и подключим наш кошелек через TonKeeper, в который я импортировал созданную нами сид фразу.
    Для начала давайте откроем страницу покупки звездочек и посмотрим что там происходит.
    [IMG]
    Тут нас просят ввести юзернейм и сумму, хорошо, давайте откроем вкладку Network в dev tools и попробуем ввести эти данные, например возьмем телеграм нашего многоуважаемого арбитра BDSM

    При вводе username видим запрос, давайте разбираться!
    Заглянем в ответ от сервера на наш запрос.

    [IMG]
    Из полезной информации хочется сразу выделить "recipient" это id пользователя который использует Fragment и "name" его можем использовать для вывода юзер френдли информации, запоминаем эти 2 параметра.
    Теперь заглянем в полезную нагрузку запроса, чтобы понять как нам его повторить.

    [IMG]
    Сразу здесь видим введенный нами Username, в данном случае это "ANSWERXYZ" и метод "searchStarsRecipient" а так же ужастный строчный параметр в виде hash. Можно подумать "Ужас, что это и откуда, как нам его передавать"
    Давайте разбираться, подобные хэши могут генерироваться js файлами при загрузке страницы, либо хранится прямиком на html страницы, давайте с нее и начнем, просто берем наш хэш и вставляем в поиск по странице

    [IMG]
    Нам повезло! То есть мы можем получить хэш просто отправив любой запрос на страницу. Давайте сразу попробуем воспроизвести в скрипте все что мы сейчас сделали.
    Создаем файл
    ⁡PaymentGet.py
    ⁡ с таким же классом, тут мы будем получать всю информацию для совершения покупки. Для начала напишем функции
    ⁡get_data_for_payment
    ⁡ это будет наша основная функция класса и вспомогательные
    ⁡_hash_get
    ⁡ где будем получать хэш и
    ⁡_update_url
    ⁡ с помощью которой будем обновлять нашу ссылку, добавляя в нее хэщ. Так же в обязательном порядке нам нужно вытащить куки, воспользуемся куки эдитором.

    [IMG]
    Как видим тут всего 4 параметра, давайте запишем их в файл
    ⁡cookies.json
    ⁡ в формате json.

    [IMG]
    Итак продолжим написание кода.
    Python
    import requests
    from re import search
    import logging
    import json


    class PaymentGet:
    def __init__(self):
    # Загружаем cookies из файла cookies.json
    with open('cookies.json', 'r') as file:
    loaded_cookies = json.load(file)

    self.cookies = loaded_cookies

    # Заголовки, имитирующие обычный браузерный запрос
    self.headers = {
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Referer": "https://fragment.com/stars/buy", # Указывает, откуда был произведён запрос
    "X-Requested-With": "XMLHttpRequest", # Указывает, что это AJAX-запрос
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0 (Edition Yx GX)"
    }

    def _hash_get(self):
    # Получаем HTML-страницу покупки звёздочек и вытаскиваем hash из URL внутри неё
    response = requests.get("https://fragment.com/stars/buy", cookies=self.cookies)
    if response.status_code == 200:
    return search(r'api\?hash=([a-zA-Z0-9]+)', response.text).group(1)

    def _update_url(self):
    # Обновлённый URL с нужным hash для дальнейших запросов к API
    return f"https://fragment.com/api?hash={self._hash_get()}"

    def get_data_for_payment(self, recipient, quantity):
    # Логируем попытку отправки звёздочек пользователю
    logging.warning(f"Sending {quantity} stars to @{recipient}...")

    # Получаем рабочий URL API
    url = self._update_url()

    # Отправляем POST-запрос для поиска получателя по юзернейму
    recipient_id_dirt = requests.post(
    url,
    headers=self.headers,
    cookies=self.cookies,
    data=f"query={recipient}&quantity=&method=searchStarsRecipient"
    )

    # Парсим JSON-ответ и извлекаем ID и имя получателя
    recipient_id = recipient_id_dirt.json().get("found", {}).get("recipient", "")
    name = recipient_id_dirt.json().get("found", {}).get("name", "")

    # Выводим результат
    print(f"Name: {name}\nRecipient id: {recipient_id}")
    Python
    from FragmentApi.PaymentGet import PaymentGet

    if __name__ == '__main__':
    PaymentGet = PaymentGet()
    PaymentGet.get_data_for_payment("ANSWERXYZ", 50)
    [IMG]
    Отлично! Мы сымитировали запрос сделаный нами через браузер чуть ранее.

    Теперь давайте вернемся в браузер и нажмем кнопку купить, чтобы понять что нам делать дальше.

    У нас открылось окно подтверждения покупки и отправился запрос, давайте разбираться
    [IMG]
    [IMG]
    Итак здсь мы видим id получателя который мы успешно записали ранее, количество звезд в данном случае 50 и метод, который нам нужно использовать для отправки этого запроса. Теперь взглянем, что нам это дает
    [IMG]
    Итак здесь нам интересен только
    ⁡req_id
    ⁡ так как это очевидно id нашего запроса на покупку, остальные параметры выглядят мусорными. Следующий этап, жмем подтверждение покупки.


    Опа! Новый запрос. Смотрим.
    [IMG]
    Глаза разбегаются. Смотрим, device в целом понятный параметр, он не сильно важен, но на всякий случай запишем, transaction, show_sender и метод с этим тоже все понятно, далее у нас id, это тот id транзакции который мы получали в прошлом шаге и самое страшное это account, в нем много параметров, address это адрес нашего кошелька в hex, выглядит не супер полезно, пропускам, далее идет chain, это айди сети TON он в целом всегда постоянный, давайте его укажем, далее walletStateInit выглядит как абсолютный мусор, пропускаем, ну и последнее это приватный ключ нашего кошелька, возможно важный параметр, давайте его передадим, получить его очень легко, просто вызовем наш уже знакомый метод
    ⁡WalletUtils.wallet_from_mnemonics
    ⁡ и возьмем его оттуда.


    С запросом разобрались, теперь давайте взглянем, что мы получаем взамен.

    [IMG]
    То что нужно! Здесь мы видим адрес на который нужно отправить TON для завершения покупки, так же сумму указанную в нанотонах и зашифрованный memo tag, расшифруем его с помощью модуля
    ⁡base64
    ⁡ и его метода
    ⁡b64decode


    Давайте дополним нашу функцию
    ⁡get_data_for_payment
    ⁡ а так же добавим два новых метода,
    ⁡_payload_get
    ⁡ для составления полезной нагрузки для последнего запроса и
    ⁡_message_decode
    ⁡ для расшифровки memo tag.


    Python
    import requests
    from re import search
    import logging
    from wallet.WalletUtils import WalletUtils
    from urllib.parse import urlencode
    import json
    import base64


    class PaymentGet:
    def __init__(self):
    # Инициализация вспомогательного класса для работы с кошельками
    self.WalletUtils = WalletUtils()

    # Загрузка cookies из файла для авторизации на Fragment
    with open('cookies.json', 'r') as file:
    loaded_cookies = json.load(file)

    self.cookies = loaded_cookies

    # Заголовки, имитирующие поведение браузера
    self.headers = {
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Referer": "https://fragment.com/stars/buy",
    "X-Requested-With": "XMLHttpRequest",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0 (Edition Yx GX)"
    }

    def _hash_get(self):
    # Получение хэша из страницы fragment.com, используется для доступа к API
    response = requests.get("https://fragment.com/stars/buy", cookies=self.cookies)
    if response.status_code == 200:
    return search(r'api\?hash=([a-zA-Z0-9]+)', response.text).group(1)

    def _update_url(self):
    # Возвращает URL с актуальным hash-параметром
    return f"https://fragment.com/api?hash={self._hash_get()}"

    def _payload_get(self, req_id, mnemonics):
    # Формирует тело запроса для получения данных транзакции
    payload = {
    "account": json.dumps({
    "chain": "-239", # -239 = testnet, если бы был 0 — mainnet
    "publicKey": self.WalletUtils.wallet_from_mnemonics(mnemonics)[0]["public_key"]
    }),
    "device": json.dumps({
    "platform": "web",
    "appName": "telegram-wallet", # Имитируем Telegram Wallet
    "appVersion": "1",
    "maxProtocolVersion": 2,
    "features": ["SendTransaction", {"name": "SendTransaction", "maxMessages": 4}]
    }),
    "transaction": 1,
    "id": req_id,
    "show_sender": 0,
    "method": "getBuyStarsLink"
    }
    return urlencode(payload)

    @staticmethod
    def _message_decode(encoded_payload):
    # Декодирует base64-пейлоад из ответа, чтобы получить читаемую строку
    padding_needed = len(encoded_payload) % 4
    if padding_needed != 0:
    encoded_payload += '=' * (4 - padding_needed)
    decoded_payload = base64.b64decode(encoded_payload)
    text_part = decoded_payload.split(b"\x00")[-1].decode("utf-8")
    return text_part

    def get_data_for_payment(self, recipient, quantity, mnemonics):
    # Главная функция: получает адрес, сумму и полезную нагрузку для оплаты звёздочек

    logging.warning(f"Sending {quantity} stars to @{recipient}...")

    url = self._update_url()

    # Получаем ID пользователя по юзернейму
    recipient_id_dirt = requests.post(url, headers=self.headers, cookies=self.cookies,
    data=f"query={recipient}&quantity=&method=searchStarsRecipient")
    recipient_id = recipient_id_dirt.json().get("found", {}).get("recipient", "")

    # Инициализируем покупку и получаем req_id
    req_id_dirt = requests.post(url, headers=self.headers, cookies=self.cookies,
    data=f"recipient={recipient_id}&quantity={quantity}&method=initBuyStarsRequest")
    req_id = req_id_dirt.json().get("req_id", "")

    # Формируем запрос на получение данных транзакции (TON)
    encoded_payload = self._payload_get(req_id, mnemonics)

    # Получаем из Fragment данные для отправки TON: кому, сколько, и какой payload
    buy_payload_dirt = requests.post(url, headers=self.headers, cookies=self.cookies, data=encoded_payload)
    buy_payload = buy_payload_dirt.json()["transaction"]["messages"][0]

    address, amount, encoded_message = buy_payload["address"], buy_payload["amount"], buy_payload["payload"]
    payload = self._message_decode(encoded_message)

    logging.info("Payment data received!")
    logging.warning("Waiting to send transaction...")

    return address, amount, payload
    По всей видимости мы готовы испытать эту машину! Но давайте объеденим получение информации о платеже и отправки транзакции. Создадим файл
    ⁡BuyStars.py

    [CODE=python]from FragmentApi.PaymentGet import PaymentGet
    from wallet.Transactions import Transactions


    def buy_stars(recipient, amount, mnemonics, version="v4r2", testnet=False, send_mode=1):

    # Инициализация классов для получения данных оплаты и отправки транзакции
    payment = PaymentGet()
    transactions = Transactions()

    # Получаем адрес получателя, сумму и payload для перевода TON в обмен на звёзды
    payment_address, payment_amount, payload = payment.get_data_for_payment(
    recipient=recipient,
    quantity=amount,
    mnemonics=mnemonics
    )

    # Отправляем TON с указанными параметрами (адрес, сумма, payload и др.)
    transactions.send_ton(
    mnemonics=mnemonics,
    destination_address=payment_address,
    amount=payment_amount,
    nano_amount=True, # указываем, что сумма в нанотонах
    payload=payload,
    version=version,
    testnet=testnet,
    send_mode=send_mode
    )[/CODE]
    Готово! Осталось передать в функцию
    ⁡buy_stars
    ⁡ юзернейм получателя, количество звездочек и сид фразу нашего кошелька.
    Но перед этим убедитесь, что заполнены cookies, кошелек создан и инициализирован, давайте отправим на наш кошелек немного TON и купим 50 звездочек для проверки.


    [CODE=python]import json
    from FragmentApi.BuyStars import buy_stars

    if __name__ == '__main__':
    with open("created_wallets/wallets_data.txt", "r") as f:
    mnemonics = json.load(f)['mnemonics']

    recipient = input("Recipient: ")
    amount = input("Amount: ")
    buy_stars(recipient, int(amount), mnemonics, send_mode=1, testnet=False)[/CODE]
    [IMG]
    [IMG]

    Победа! Все работает замечательно. Теперь вы настоящий крутой хакер.

    Project/
    ├── created_wallets/
    │ └── wallets_data.txt
    ├── FragmentApi/
    │ ├── BuyStars.py
    │ └── PaymentGet.py
    ├── wallet/
    │ ├── Transactions.py
    │ └── WalletUtils.py
    ├── cookies.json
    └──main.py
    [CODE=python]from FragmentApi.PaymentGet import PaymentGet
    from wallet.Transactions import Transactions


    def buy_stars(recipient, amount, mnemonics, version="v4r2", testnet=False, send_mode=1):
    payment = PaymentGet()
    transactions = Transactions()
    payment_address, payment_amount, payload = payment.get_data_for_payment(recipient=recipient, quantity=amount,
    mnemonics=mnemonics)

    transactions.send_ton(mnemonics=mnemonics, destination_address=payment_address, amount=payment_amount,
    nano_amount=True, payload=payload, version=version, testnet=testnet, send_mode=send_mode)[/CODE]
    [CODE=python]import requests
    from re import search
    import logging
    from wallet.WalletUtils import WalletUtils
    from urllib.parse import urlencode
    import json
    import base64


    class PaymentGet:
    def __init__(self):
    self.WalletUtils = WalletUtils()
    with open('cookies.json', 'r') as file:
    loaded_cookies = json.load(file)

    self.cookies = loaded_cookies
    self.headers = {
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Referer": "[UNFURL]https://fragment.com/stars/buy"[/UNFURL],
    "X-Requested-With": "XMLHttpRequest",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0 (Edition Yx GX)"
    }

    def _hash_get(self):
    response = requests.get("[UNFURL]https://fragment.com/stars/buy"[/UNFURL], cookies=self.cookies)
    if response.status_code == 200:
    return search(r'api\?hash=([a-zA-Z0-9]+)', response.text).group(1)

    def _update_url(self):
    return f"[UNFURL]https://fragment.com/api?hash={self._hash_get()}"[/UNFURL]

    def _payload_get(self, req_id, mnemonics):
    payload = {
    "account": json.dumps({
    "chain": "-239",
    "publicKey": self.WalletUtils.wallet_from_mnemonics(mnemonics)[0]["public_key"]
    }),
    "device": json.dumps({
    "platform": "web",
    "appName": "telegram-wallet",
    "appVersion": "1",
    "maxProtocolVersion": 2,
    "features": ["SendTransaction", {"name": "SendTransaction", "maxMessages": 4}]
    }),
    "transaction": 1,
    "id": req_id,
    "show_sender": 0,
    "method": "getBuyStarsLink"
    }
    return urlencode(payload)

    @staticmethod
    def _message_decode(encoded_payload):
    padding_needed = len(encoded_payload) % 4
    if padding_needed != 0:
    encoded_payload += '=' * (4 - padding_needed)
    decoded_payload = base64.b64decode(encoded_payload)
    text_part = decoded_payload.split(b"\x00")[-1].decode("utf-8")

    return text_part

    def get_data_for_payment(self, recipient, quantity, mnemonics):
    logging.warning(f"Sending {quantity} stars to @{recipient}...")

    url = self._update_url()

    recipient_id_dirt = [UNFURL='https://requests.post']requests.post[/UNFURL](url, headers=self.headers, cookies=self.cookies,
    data=f"query={recipient}&quantity=&method=searchStarsRecipient")
    recipient_id = recipient_id_dirt.json().get("found", {}).get("recipient", "")


    req_id_dirt = [UNFURL='https://requests.post']requests.post[/UNFURL](url, headers=self.headers, cookies=self.cookies,
    data=f"recipient={recipient_id}&quantity={quantity}&method=initBuyStarsRequest")
    req_id = req_id_dirt.json().get("req_id", "")

    encoded_payload = self._payload_get(req_id, mnemonics)

    buy_payload_dirt = [UNFURL='https://requests.post']requests.post[/UNFURL](url, headers=self.headers, cookies=self.cookies, data=encoded_payload)
    buy_payload = buy_payload_dirt.json()["transaction"]["messages"][0]

    address, amount, encoded_message = buy_payload["address"], buy_payload["amount"], buy_payload["payload"]
    payload = self._message_decode(encoded_message)
    [UNFURL='https://logging.info']logging.info[/UNFURL]("Payment data received!")
    logging.warning("Waiting to send transaction...")
    return address, amount, payload[/CODE]
    [CODE=python]import logging

    from TonTools import TonCenterClient, Wallet
    import asyncio
    from tonsdk.utils import from_nano


    class Transactions:
    @staticmethod
    async def _send_ton_async(mnemonics, destination_address, amount, payload, nano_amount, version, testnet, send_mode):
    provider = TonCenterClient(testnet=testnet)
    wallet = Wallet(mnemonics=mnemonics, version=version, provider=provider)
    if nano_amount:
    if isinstance(amount, str):
    amount = int(amount)
    amount = from_nano(amount, "ton")


    clean_payload = payload.replace("\n", " ")
    logging.warning(f'Sending {amount} TON to {destination_address} with payload: {clean_payload}')
    if await wallet.transfer_ton(destination_address=destination_address, amount=amount,
    message=payload, send_mode=send_mode) != 200:
    logging.error("Transaction failed!")
    return 0
    else:
    [UNFURL='https://logging.info']logging.info[/UNFURL]("Sending successful!")
    return 1

    def send_ton(self, mnemonics, destination_address, amount, payload, nano_amount=True, version='v4r2', testnet=False, send_mode=0):
    result = [UNFURL='https://asyncio.run']asyncio.run[/UNFURL](self._send_ton_async(mnemonics, destination_address, amount, payload, nano_amount, version, testnet, send_mode))
    return result[/CODE]
    [CODE=python]import asyncio
    import logging

    from tonsdk.contract.wallet import Wallets, WalletVersionEnum
    from json import dumps
    from pathlib import Path
    from TonTools import TonCenterClient

    from tonsdk.utils import bytes_to_b64str


    class WalletUtils:
    @staticmethod
    def _return_wallet_data(mnemonics, pub_k, priv_k, wallet_address):
    return {
    "wallet_address": wallet_address.address.to_string(True, True, True),
    "mnemonics": mnemonics,
    "public_key": pub_k.hex(),
    "private_key": priv_k.hex(),
    "wallet_address_testnet": wallet_address.address.to_string(True, True, True, True)
    }

    def create_wallet(self, version="v4r2", save_to_file=False, save_dir="created_wallets/wallets_data.txt"):
    mnemonics, pub_k, priv_k, wallet_address = Wallets.create(version=getattr(WalletVersionEnum, version),
    workchain=0)

    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    if save_to_file:
    Path(save_dir).parent.mkdir(parents=True, exist_ok=True)
    with open(save_dir, "a+", encoding="utf-8") as f:
    f.write(dumps(wallet_data, indent=4, ensure_ascii=False) + "\n\n")

    return wallet_data, wallet_address

    def wallet_from_mnemonics(self, mnemonics: list, version="v4r2"):
    mnemonics, pub_k, priv_k, wallet_address = Wallets.from_mnemonics(mnemonics=mnemonics,
    version=getattr(WalletVersionEnum, version),
    workchain=0)

    wallet_data = self._return_wallet_data(mnemonics, pub_k, priv_k, wallet_address)

    return wallet_data, wallet_address

    def init_wallet(self, mnemonics, testnet=False):
    result = [UNFURL='https://asyncio.run']asyncio.run[/UNFURL](self._init_wallet_async(mnemonics, testnet))
    return result

    async def _init_wallet_async(self, mnemonics, testnet=False):
    wallet_data, wallet_address = self.wallet_from_mnemonics(mnemonics=mnemonics)
    provider = TonCenterClient(testnet=testnet)
    query = wallet_address.create_init_external_message()
    deploy_message = bytes_to_b64str(query['message'].to_boc(False))

    if await provider.send_boc(deploy_message) != 200:
    logging.error('Init error or wallet already init')
    else:
    [UNFURL='https://logging.info']logging.info[/UNFURL] ("Wallet inited!")[/CODE]
    [CODE=python]import json
    from FragmentApi.BuyStars import buy_stars

    if __name__ == '__main__':

    with open("created_wallets/wallets_data.txt", "r") as f:
    mnemonics = json.load(f)['mnemonics']

    recipient = input("Recipient: ")
    amount = input("Amount: ")

    buy_stars(recipient, int(amount), mnemonics, send_mode=1, testnet=False)[/CODE]

    *Проект на GitHub*

    Благодарю за прочтение! :smile_ty:
     
    This article was useful for you?
    You can thank the author of the topic by transferring funds to your balance
    Thank the author
    1. убежище
      Апатия, пиздец я это читать не буду. Залей архив на гитхаб и не еби голову мне спс
    2. Апатия Topic starter

Comments

    1. Весть
      Весть Jun 5, 2025 Моментальная покупка TRX - https://lolz.live/threads/8610296/ 9551 Aug 8, 2019
      то есть ты просто взял и сделал пересказ официального апи?
       
      1. Апатия Topic starter
        Весть, не совсем, я разбираю библеотеку и не одну, + их документация это буквально темный лес, да в добавок разбор запросов фрагмента
      2. InfernLife
        Весть, давно у фрагмента есть норм апи ?
    2. Арбузим
      Арбузим Jun 5, 2025 1551 Aug 22, 2022
      топ тема спасибо годно респект
       
    3. Георгий_Жуков
      Георгий_Жуков Jun 5, 2025 Рокоссовский даёт звёзды --> lolz.live/threads/8964129/ 762 Jun 4, 2021
      Автору руки целовать, понятным русским языком написано-расписано
       
    4. Robert_PaPa
      Robert_PaPa Jun 6, 2025 0 Feb 17, 2024
      В конструкторе для тг-бота от bot-t можно интегрировать?
       
      1. Апатия Topic starter
        Robert_PaPa, Я сам конструкторами никогда не пользовался, поэтому не подскажу
    5. grecha
      grecha Jun 9, 2025 lolz.live/threads/8217270 - стиль здесь 1027 Aug 13, 2020
      Спасибо что просвящаешь людей в работе с тон блокчейном, у них адекватной информации и работы с коммьюнити нет почти
       
    6. duha12345
      как бля будет в коде W5 кошельки, ваще не нашел че да как, просто ошибку выдает
       
      1. Апатия Topic starter
        duha12345,если ты про версию, v5 еще не представлен в публичной документации тоном, так что никак, v4r2 последняя на данный момент
    7. rasez
      rasez Jun 14, 2025 Стим аккаунты тут - lolz.live/threads/7680775 :da: 1487 Apr 29, 2025
      Годна, а зач надо это ваще. Интересно ток как эт юзать можно
       
      1. Апатия Topic starter
        rasez, автоматический бот по продаже звездочек / премиумов + можно это к фп прикрутить или плёнку и продавать уже там, много кто использует для этого либо платные апи, либо бесплатные, но сомнительные , не ровен час, кто то просто спиздит все бабки через такое апи, а тут работа напрямую с фрагментом, без посредников
    8. id603755193
      как выключить тестнет
       
      1. Апатия Topic starter
      2. id603755193
        Апатия, спасибо, но еще вопрос, можно ли как то без куки? используя сессию какую нибудь
      3. Апатия Topic starter
        id603755193, Куки ведь и есть сессия, тем более они живут дохерилион времени, закинул да забыл, может и можно как то рефрешить сессию до бесконечности и без них, но я честно даже не пытался в этом разбираться
    9. neestxx2
      neestxx2 Jun 27, 2025 0 Jun 26, 2025
      а что делать если вылазит ошибка (500) [IMG]
       
    10. Piloter122
      Piloter122 Jun 28, 2025 1 May 31, 2025
      что это за ошибка?

      Traceback (most recent call last):
      File "c:\Users\Admin\Desktop\fragment_api-main\main.py", line 13, in <module>
      buy_stars(recipient, int(amount), mnemonics, send_mode=1, testnet=False)
      File "c:\Users\Admin\Desktop\fragment_api-main\FragmentApi\BuyStars.py", line 11, in buy_stars
      transactions.send_ton(mnemonics=mnemonics, destination_address=payment_address, amount=payment_amount,
      File "c:\Users\Admin\Desktop\fragment_api-main\wallet\Transactions.py", line 27, in send_ton
      result = asyncio.run(self._send_ton_async(mnemonics, destination_address, amount, payload, nano_amount, version, testnet, send_mode))
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\asyncio\runners.py", line 195, in run
      return runner.run(main)
      ^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\asyncio\runners.py", line 118, in run
      return self._loop.run_until_complete(task)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\asyncio\base_events.py", line 691, in run_until_complete
      return future.result()
      ^^^^^^^^^^^^^^^
      File "c:\Users\Admin\Desktop\fragment_api-main\wallet\Transactions.py", line 18, in _send_ton_async
      if await wallet.transfer_ton(destination_address=destination_address, amount=amount,
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\site-packages\TonTools\Contracts\Wallet.py", line 43, in transfer_ton
      seqno = await self.get_seqno()
      ^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\site-packages\TonTools\Contracts\Wallet.py", line 37, in get_seqno
      return await self.provider.get_wallet_seqno(self.address)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\site-packages\TonTools\Providers\TonCenterClient.py", line 303, in get_wallet_seqno
      data = await self.run_get_method(address=address, method='seqno', stack=[])
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\Admin\AppData\Local\Programs\Python\Python312\Lib\site-packages\TonTools\Providers\TonCenterClient.py", line 98, in run_get_method
      raise GetMethodError(
      TonTools.Providers.TonCenterClient.GetMethodError: get method seqno for address EQDHxZb2QBEav5eSzRgY8Hbk4PVVlJ37owu04QV3S__g_h0I exit code is -13
       
      1. neestxx2
        Piloter122, также было, нужно активировать кошелек (отправить с него любую транзакцию)
    11. Torkusz
      Torkusz Jul 11, 2025 0 Jul 19, 2020
      Годно!
      Для split будет?)
       
      1. Апатия Topic starter
        Torkusz, а у сплита ведь и так апи есть :smile_interested:
      2. Torkusz
    12. F1L122
      F1L122 Jul 12, 2025 0 Aug 21, 2018
      у меня выводит ошибку с тонтулсом(я даже установить его не могу)
       
    13. McDuck
      McDuck Jul 13, 2025 1 Aug 23, 2024
      Приветствую, нашел небольшой баг в коде. Советую исправить в файле Transactions.py:
      Python
      if nano_amount:
      amount = from_nano(amount, "ton")
      на
      Python
      if nano_amount:
      if isinstance(amount, str):
      amount = int(amount)
      amount = from_nano(amount, "ton")
       
      1. Апатия Topic starter
        McDuck, ну просто [IMG] , но да так будет более правильно
    14. ScallyMilano
      ScallyMilano Jul 17, 2025 https://lolz.live/threads/8473775/ - тг звезды по дешману 4953 Mar 27, 2019
       
      1. Апатия Topic starter
        ScallyMilano, такие суммы я не покупал, да и таких проблем не замечал, но да, если так бывает, по хорошему бы добавить проверку на подобное, но у меня тут просто разбор того как это все равботает, а не целиком готовое решение, для использования в промышленных маштабах его по хорошему подкорректировать, пожалуй укажу об этом в начале статьи, спасибо
    15. nurofen
      ChatGPT напиши кратко, что тут написано, а также скажи, нужно ли kyc подтверждение для покупки?
       
      1. ChatGPT
        nurofen, в статье рассказывается о том, как работать с блокчейном TON с помощью Python. Автор объясняет, как создать и инициализировать кошелек, а также как автоматизировать покупки на платформе Fragment с помощью скрипта. В статье также упоминается, что предоставленный код является демонстрацией и требует доработок для безопасной работы.

        Что касается KYC (Know Your Customer) подтверждения, то для покупки на платформе Fragment может потребоваться верификация личности, но это зависит от политики самой платформы. Рекомендую проверить требования Fragment непосредственно на их сайте или в документации, чтобы быть уверенным в необходимости KYC.
    16. ЧАЛЛИК
      ЧАЛЛИК Jul 26, 2025 at 11:19 PM Лучшие, антиабуз домены - lolz.live/threads/111111 3006 Oct 2, 2022
      Можешь мне в тг написать?
       
Loading...
Top