January 19

Пишем софт по раскидыванию баланса с биржи

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

Что будет делать написанный скрипт?

Софт будет раскидывать баланс с биржи OKX на кошельки, которые ты укажешь в файле. Выводить сможешь любой токен и его количество, указанное в настройках.

Что ты должен уметь?

Для понимания материала потребуется базовое знание синтаксиса Python. Если что-то будет непонятно, не бойся обратиться к ChatGPT за помощью.

Где я могу посмотреть код скрипта?

Ты можешь посмотреть и скачать наш код с GitHub репозитория.

Оглавление

Что такое API

Получаем ключи для взаимодействия с OKX API

Создаем проект и структуру файлов

Начинаем писать код

Подготовка и запуск скрипта

Как можно улучшить скрипт

Заключение

Что такое API

Для написания софта нам понадобится работать с OKX API. Так что сначала разберемся с тем, что такое API.

API (Application Programming Interface) — Если упростить, то это набор инструментов, которые позволяют различным программам взаимодействовать друг с другом. В нашем случае API OKX — это способ для нашего софта взаимодействовать с биржей для выполнения операций, таких как вывод средств, просмотр баланса и так далее.

Пример

Ты приходишь в ресторан, и чтобы заказать еду, ты говоришь официанту (API), что именно тебе нужно. Ты не общаешься напрямую с кухней (биржей), а через официанта, который передает заказ. Он также доставляет твою еду (ответ от биржи) обратно к твоему столику.

Получаем ключи для взаимодействия с OKX API

Чтобы пользоваться OKX API нам нужно создать ключи доступа. Делается легко, просто следуй гайду ниже.

1. Переходим сюда и нажимаем "Create API key".

2. Заполняем поля и в Permissions указываем все возможные разрешения, иначе наш скрипт не сможет выводить или торговать токенами. Также можете указать список разрешенных IP для взаимодействия с API. После того как все заполнили жмём на "Submit all".

3. После создания нас вернет обратно. Тут нужно нажать на кнопку "view" рядом с созданным нами API ключем.

4. Теперь необходимо сохранить API-ключ как OKX_API_KEY, Secret key как OKX_API_SECRET, а также Passphrase, который ты указывал при создании API, как OKX_API_PASSPHRASE. Эти данные будут использованы позже.

Создаем проект и структуру файлов

Приступаем к созданию нашего фундамента для софта. Создаем папку, например, okx_spreader и открываем её в VS Code или другом редакторе кода. Далее создаем следующую структуру файлов:

Давай разберем каждый попунктно:

  • modules — директория, в которой хранятся модули для нашего скрипта, такие как okx.py и transfer.py. Эти модули будут реализовывать основную функциональность скрипта.
  • modules/okx.py — модуль, отвечающий за взаимодействие с API биржи OKX.
  • modules/transfer.py — модуль, который управляет выводом средств. Для каждого кошелька он инициирует вывод токенов используя okx.py модуль.
  • main.py — основной файл для запуска нашего софта.
  • recipients.txt — файл, содержащий список адресов, на которые будут переводиться токены.
  • settings.py — файл для хранения различных настроек, необходимых для работы скрипта.
  • utils.py — файл с вспомогательными функциями, которые могут быть использованы в разных модулях проекта.

Начинаем писать код

settings.py

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

  • TRANSFER_AMOUNT_RANGE — диапазон, в котором будет генерироваться сумма перевода (например, от 3 до 5). Это необходимо, чтобы сумма перевода была не фиксированной, а случайной в пределах заданного диапазона.
  • TRANSFER_CURRENCY — тип валюты, который будем переводить, в нашем случае это USDT.
  • TRANSFER_CHAIN — сеть, на которую будем инициировать перевод, например, Arbitrum One.
  • OKX_API_KEY, OKX_API_SECRET, OKX_API_PASSPHRASE — это параметры для аутентификации при работе с API биржи OKX. Помнишь я тебя попросил сохранить данные под определенным неймингом? Самое время будет их заполнить.

Конфигурация хранится отдельно, чтобы избежать повторного написания одинаковых данных в разных местах кода.

# transfer settings
TRANSFER_AMOUNT_RANGE = [3, 5]  # диапазон суммы перевода с OKX на кошелек
TRANSFER_CURRENCY = "USDT"  # токен для перевода (как записано на OKX)
TRANSFER_CHAIN = "Arbitrum One"  # сеть куда перевести (как записано на OKX)

# exchange settings
OKX_API_KEY = "your_okx_api_key"
OKX_API_SECRET = "your_okx_api_secret"
OKX_API_PASSPHRASE = "your_okx_api_passphrase"

main.py

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

Нам нужно спросить пользователя, хочет ли он начать процесс перевода. Если ответ положительный (пользователь вводит "YES" или "Y" в любом регистре), то запускается функция, которая отвечает за переводы. Если же пользователь отказывается или вводит что-то иное, программа завершится.

from modules.transfer import start_transfer

def main():
    # Спрашиваем пользователя, хочет ли он начать раскидку баланса и сохраняем ответ в переменную choice
    choice = input("Начать раскидку баланса? [Y/N]: ")

    # Если пользователь ответил "YES" или "Y" (в любом регистре), то запускаем функцию start_transfer
    if choice.upper() in ["YES", "Y"]:
        start_transfer()
    else:
        print("Выход...")
        exit()


if __name__ == "__main__":
    main()

Пояснение к коду:

1. Импорт функции start_transfer:

from modules.transfer import start_transfer

Эта строка импортирует функцию start_transfer из модуля transfer, расположенного в папке modules. Функция start_transfer будет использоваться для страта вывода токенов с OKX на кошельки, указанные в файле recipients.txt

2. Определение функции main:

def main():

Функция main — это точка входа в вашу программу. Она будет управлять основным процессом взаимодействия с пользователем.

3. Получение ввода от пользователя:

choice = input("Начать раскидку баланса? [Y/N]: ")

Здесь мы просим пользователя ввести, хочет ли он начать процесс распределения баланса, и сохраняем его ответ в переменной choice.

4. Проверка ввода пользователя:

if choice.upper() in ["YES", "Y"]:

Мы проверяем, ввел ли пользователь "YES" или "Y", используя метод upper() для преобразования ввода в верхний регистр. Это делает проверку нечувствительной к регистру. Вместо нескольких отдельных условий, мы просто проверяем, содержится ли преобразованный ввод в списке ["YES", "Y"]. Например, если пользователь ввел "yes", метод upper() преобразует его в "YES", который присутствует в списке. Таким образом, условие выполняется, и мы попадаем внутрь блока if.

5. Вызов функции start_transfer:

Если пользователь ввел "YES" или "Y", мы вызываем функцию start_transfer, чтобы начать процесс распределения баланса.

start_transfer()

6. Обработка другого ввода:

Если пользователь ввел что-то другое, не "YES" или "Y", то мы выводим сообщение "Выход..." и завершаем программу с помощью стандартной функции exit().

else:
    print("Выход...")
    exit()

7. Запуск функции main:

В конце добавляется проверка, чтобы убедиться, что этот код выполняется только при непосредственном запуске файла, а не при его импорте как модуля. То есть, если файл запускается командой python main.py, а не импортируется через import main.py. Если условие выполняется, вызывается функция main

if __name__ == "__main__":
    main()

modules/transfer.py

Этот файл отвечает за сам процесс перевода средств. Мы будем читать список получателей (адресов кошельков) из файла recipients.txt. Для каждого получателя вызываем функцию withdraw(), которая выполнит перевод на конкретный кошелек через OKX API.

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

from modules.okx import withdraw

def start_transfer():
    with open("recipients.txt", "r") as file:
        recipients = file.readlines()
    
    for recipient in recipients:
        withdraw(recipient.strip())

Пояснение к коду:

1. Импорт функции withdraw:

Сначала мы импортируем функцию withdraw из модуля okx.py. Эта функция будет использоваться для выполнения вывода средств на указанные адреса используя функцию withdraw из okx.py

from modules.okx import withdraw

2. Определение функции start_transfer:

Мы создаем функцию start_transfer, которая будет отвечать за чтение списка адресов и вызов функции withdraw для каждого из них.

def start_transfer():

3. Чтение файла с адресами:

Мы используем контекстный менеджер with, чтобы открыть файл recipients.txt для чтения. Контекстный менеджер гарантирует, что файл будет закрыт автоматически, как только выполнение выйдет из блока with, независимо от того, произошла ли ошибка в процессе работы с файлом. Это делает код более безопасным и удобным, так как не нужно вручную закрывать файл.

Функция open используется для открытия файла. Она принимает два аргумента. Первый — это путь к файлу. В нашем случае файл называется recipients.txt и находится в той же папке, что и скрипт, поэтому достаточно указать только имя файла. Второй аргумент — это режим открытия файла. В данном случае мы используем "r", что означает режим чтения (read). Функция open возвращает объект файла, который мы сохраняем в переменной file.

Файл recipients.txt должен содержать список адресов получателей, где каждый адрес находится на новой строке. Мы считываем все строки файла с помощью метода readlines(), который возвращает список, где каждая строка файла становится отдельным элементом списка.

with open("recipients.txt", "r") as file:
    recipients = file.readlines()

4. Цикл по списку адресов:

Мы перебираем все адреса из списка recipients с помощью цикла for. Для каждого адреса вызываем функцию withdraw, передавая ей в качестве аргумента адрес получателя. Перед этим мы убираем из адреса лишние пробелы и символы новой строки (\n) с помощью метода strip(), чтобы убедиться, что передаем только саму строку адреса без ненужных символов.

for recipient in recipients:
    withdraw(recipient.strip())

modules/okx.py

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

Этот модуль отвечает за выполнение перевода средств через API OKX. В функции withdraw() мы формируем HTTP-запрос с нужными параметрами для перевода. Важным моментом является рандомизация суммы перевода из диапазона, который мы указали в конфигурационном файле.

Каждый запрос к API требует подписи для безопасности, поэтому мы используем алгоритм HMAC для генерации подписи (как того требует OKX API), которая затем будет добавлена в заголовки запроса.

Функция withdraw():

  • Вычисляет случайную сумму для перевода.
  • Формирует тело запроса с нужными параметрами: валютой, суммой, сетью, адресом получателя и другими данными.
  • Подписывает запрос с помощью вспомогательной функции sign_request().
  • Отправляет запрос на сервер API OKX и выводит сообщение об успешности перевода.
import json
import random
import requests
from settings import (
    OKX_API_KEY,
    OKX_API_PASSPHRASE,
    TRANSFER_AMOUNT_RANGE,
    TRANSFER_CHAIN,
    TRANSFER_CURRENCY,
)
from utils import sign_request

OKX_BASE_API_URL = "https://www.okx.com"  # базовый URL для запросов к API OKX

def withdraw(recipient: str):
    method = "POST"
    request_path = "/api/v5/asset/withdrawal"

    amount_to_transfer = random.uniform(
        TRANSFER_AMOUNT_RANGE[0], TRANSFER_AMOUNT_RANGE[1]
    )

    body = {
        "ccy": TRANSFER_CURRENCY,
        "amt": str(amount_to_transfer),
        "dest": "4",  # 4 - кошелек, 3 - биржа
        "chain": f"{TRANSFER_CURRENCY}-{TRANSFER_CHAIN}",
        "toAddr": recipient,
        "walletType": "private",
    }

    timestamp, base64_signature = sign_request(
        method=method, request_path=request_path, body=json.dumps(body)
    )

    headers = {
        "OK-ACCESS-KEY": OKX_API_KEY,
        "OK-ACCESS-SIGN": base64_signature,
        "OK-ACCESS-PASSPHRASE": OKX_API_PASSPHRASE,
        "OK-ACCESS-TIMESTAMP": timestamp,
    }

    response = requests.request(
        method=method,
        url=OKX_BASE_API_URL + request_path,
        headers=headers,
        json=body,
    )

    response_data = response.json()

    if response_data.get("code", "") == "0":
        print(
            f"Инициировал перевод | {amount_to_transfer} {TRANSFER_CURRENCY} | {recipient}"
        )
    else:
        print(f"Не удалось инициировать перевод | {response_data.get('msg', 'No message provided')}")

    return response_data

Пояснение:

На данном этапе у тебя, вероятно, возникло множество вопросов, так что давай разберемся по порядку.

Откуда мы знаем, куда и как обращаться к OKX API?

У любого качественного API есть документация, и у OKX она тоже есть. Вот тут можно посмотреть как использовать их API для вывода средств.

Что такое HTTP?

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

Что такое HMAC и зачем нужно подписывать запросы?

  1. Для безопасности. Подпись запроса помогает убедиться, что никто не подменил данные, которые ты отправляешь на сервер. Без подписи кто-то мог бы изменить твой запрос и, например, перевести деньги на свой кошелек. Подпись защищает от этого.
  2. Требование API OKX. OKX требует, чтобы все запросы были подписаны. Если запрос не подписан, API просто отклонит его. В документации OKX подробно объясняется, как правильно подписывать запросы, чтобы сервер принял их.

HMAC — метод, с помощью которого мы создаем подпись для наших запросов. Он использует специальный секретный ключ и алгоритм, который помогает создать уникальную "отпечаток" данных запроса, чтобы сервер знал, что запрос действительно пришел от тебя и не был изменен.

Пояснение к коду:

1. Импорт необходимых библиотек и настроек:

Мы импортируем стандартные библиотеки json, random и requests для работы с JSON-данными, генерации случайных чисел и отправки HTTP-запросов, соответственно. Также импортируем несколько настроек из файла settings.py, таких как ключи для API, диапазон сумм для перевода, сеть и валюту. И из файла utils.py импортируется функция sign_request, которая используется для создания подписи запроса.

import json
import random
import requests
from settings import (
    OKX_API_KEY,
    OKX_API_PASSPHRASE,
    TRANSFER_AMOUNT_RANGE,
    TRANSFER_CHAIN,
    TRANSFER_CURRENCY,
)
from utils import sign_request

2. Базовый URL для запросов к API OKX:

Здесь определяется базовый URL, по которому будут отправляться запросы к API биржи OKX. Если проще, то просто указываем домен OKX вместе с https схемой.

OKX_BASE_API_URL = "https://www.okx.com"

3. Определение функции withdraw:

Функция withdraw принимает один аргумент — recipient, который представляет собой адрес получателя перевода. Мы его передаем из modules/transfer.py, если вдруг ты забыл.

def withdraw(recipient: str):

4. Подготовка запроса:

method = "POST"
request_path = "/api/v5/asset/withdrawal"

Устанавливаем метод HTTP запроса (POST) и путь к API-методу для вывода средств. Мы пишем именно такие значение, так как в документации OKX API по выводу средств указан именно такое метод и данный request_path

5. Генерация случайной суммы для перевода:

С помощью функции random.uniform генерируется случайное число с плавующей запятой в пределах диапазона, заданного в настройках TRANSFER_AMOUNT_RANGE. Это будет сумма, которую мы будем выводить.

amount_to_transfer = random.uniform(
    TRANSFER_AMOUNT_RANGE[0], TRANSFER_AMOUNT_RANGE[1]
)

6. Создание тела запроса:

Формируем словарь body, который будет передан в запросе. Здесь мы указываем валюту перевода, сумму, тип назначения (кошелек или биржа), сеть и адрес получателя.

Ты можешь заметить, что "dest" может быть как 4, так и 3. Если бы мы хотели провести внутрибиржевый перевод, то нам понадобилось указать тут цифру 3 и вместа адреса указать email или другой индификатор OKX аккаунта.

body = {
    "ccy": TRANSFER_CURRENCY,
    "amt": str(amount_to_transfer),
    "dest": "4",  # 4 - кошелек, 3 - биржа
    "chain": f"{TRANSFER_CURRENCY}-{TRANSFER_CHAIN}",
    "toAddr": recipient,
    "walletType": "private",
}

7. Создание подписи запроса:

Для безопасности запросов к API необходимо подписывать их. Мы используем функцию sign_request (напишем её позже) для создания подписи. Она возвращает timestamp и signature.

timestamp, base64_signature = sign_request(
    method=method, request_path=request_path, body=json.dumps(body)
)

8. Подготовка заголовков для запроса:

Здесь мы формируем заголовки для HTTP-запроса, включая ключ API, подпись, passphrase и метку времени.

headers = {
    "OK-ACCESS-KEY": OKX_API_KEY,
    "OK-ACCESS-SIGN": base64_signature,
    "OK-ACCESS-PASSPHRASE": OKX_API_PASSPHRASE,
    "OK-ACCESS-TIMESTAMP": timestamp,
}

9. Отправка запроса:

Мы отправляем HTTP-запрос с помощью библиотеки requests, передавая метод, сформированный URL, заголовки и тело запроса.

response = requests.request(
    method=method,
    url=OKX_BASE_API_URL + request_path,
    headers=headers,
    json=body,
)

10. Обработка ответа:

Полученный ответ от OKX API парсим как JSON.

JSON — это широко используемый формат данных для обмена информацией с API. Он универсален и совместим с большинством языков программирования, что делает его удобным для применения в различных системах.

Далее проверяем ответ:

Если в ответе код "0", это означает, что перевод был успешно инициирован, и мы выводим сообщение с информацией о переводе. В противном случае выводим сообщение об ошибке.

Спросишь как мы узнали, что будет в ответе? Тут есть два варианта:

  1. Выполнить запрос и запринтить сообщение
  2. Подглядеть в документацию OKX API.
response_data = response.json()
if response_data.get("code", "") == "0":
    print(
        f"Инициировал перевод | {amount_to_transfer} {TRANSFER_CURRENCY} | {recipient}"
    )
else:
    print(f"Не удалось инициировать перевод | {response_data.get('msg', 'No message provided')}")

11. Возврат результата:

Функция возвращает полученные данные ответа от API, чтобы мы могли использовать их дальше, если это необходимо. В нашем случае ответ вернется обратно в modules/transfer.py

return response_data

utils.py

Здесь мы распишем различные вспомогательные функции, такие как подпись запроса к OKX API или получение timestamp.

import hmac
from base64 import b64encode
from datetime import datetime, timezone
from typing import Tuple
from settings import OKX_API_SECRET

def sign_request(method: str, request_path: str, body: str = "") -> Tuple[str, str]:
    timestamp = get_iso_timestamp()
    message = timestamp + method + request_path + body
    signature = hmac.new(OKX_API_SECRET.encode(), message.encode(), "SHA256").digest()
    base64_signature = b64encode(signature).decode()

    return timestamp, base64_signature

def get_iso_timestamp() -> str:
    utc_timestamp = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
    utc_timestamp = utc_timestamp.replace("+00:00", "Z")
    return utc_timestamp

Пояснение к коду:

1. Импорт необходимых библиотек:

Импортируем необходимые библиотеки: hmac для создания подписи к запросу для вывода, b64encode для кодирования подписи в формат base64, как того требует OKX API, datetime и timezone для работы с текущим временем в UTC, а также Tuple для аннотирования типов возвращаемых значений функции. Кроме того, из файла settings загружается секретный ключ API — OKX_API_SECRET, который используется для генерации подписи.

import hmac
from base64 import b64encode
from datetime import datetime, timezone
from typing import Tuple
from settings import OKX_API_SECRET

2. Функция sign_request:

Функция sign_request используется для создания подписи запроса к API OKX. Она принимает три аргумента: HTTP-метод запроса (method), путь запроса (request_path) и тело запроса (body), которое по умолчанию пустое.

def sign_request(method: str, request_path: str, body: str = "") -> Tuple[str, str]:

3. Получение timestamp:

Мы вызываем функцию get_iso_timestamp, чтобы получить метку времени в формате ISO и записать ответ в переменнюу timestamp.

timestamp = get_iso_timestamp()

4. Формирование сообщения для подписи:

Для создания подписи мы формируем строку сообщения, которое включает в себя:

  • Метку времени (timestamp).
  • HTTP-метод запроса (method).
  • Путь запроса (request_path).
  • Тело запроса (body), которое может быть пустым.

Это также расписано в документации OKX API

message = timestamp + method + request_path + body

5. Создание подписи с помощью HMAC:

Мы используем алгоритм HMAC с SHA256 для создания хэш-значения подписи. Хэш создается с использованием секретного ключа API (OKX_API_SECRET) и строки message, которую мы сформировали ранее.

Для обеих переменных — OKX_API_SECRET и message — применяется метод encode(). Это преобразует их из строкового представления в байтовое, так как функция hmac.new() требует, чтобы входные данные были в виде байтов.

Затем создается объект HMAC с использованием метода hmac.new(), где передаются:

  • Секретный ключ (OKX_API_SECRET.encode()),
  • Сообщение (message.encode()),
  • Алгоритм хэширования ("SHA256").

Метод .digest() возвращает итоговое хэш-значение подписи в виде байтов. Это значение используется для аутентификации запросов к API.

signature = hmac.new(OKX_API_SECRET.encode(), message.encode(), "SHA256").digest()

6. Конвертация подписи в формат base64

После того как мы подписали наш запрос, мы конвертируем это в формат base64 для передачи в заголовках HTTP-запроса.

BASE64 — это способ кодирования строки, но не шифрования. То есть, любой текст, закодированный в base64, можно легко расшифровать. Этот формат используется исключительно для удобства представления данных.

base64_signature = b64encode(signature).decode()

7. Возврат timestamp и подписи

Функция возвращает два значения: временную метку и подпись в формате base64.

return timestamp, base64_signature

8. Определение функции get_iso_timestamp

Эта функция получает текущее время в формате ISO 8601. Мы используем datetime.now(timezone.utc) для получения текущего времени в UTC. Затем, с помощью метода isoformat(timespec="milliseconds"), мы получаем строку с точностью до миллисекунд. В конце заменяем строку "+00:00" на "Z", чтобы привести метку времени к стандарту ISO 8601, который используется в OKX API.

def get_iso_timestamp() -> str:
    utc_timestamp = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
    utc_timestamp = utc_timestamp.replace("+00:00", "Z")
    return utc_timestamp

Подготовка и запуск скрипта

Окей, мы написали полностью код, теперь нам нужно сделать необходимую подготовку для его запуска.

1. Создаем виртуальное окружение.

python -m venv .venv

Далее нам нужно активировать его

.venv\Scripts\activate

Готово, теперь у нас есть изолированное Python окружение для работы с написанным скриптом.

2. Устанавливаем необходимые зависимости

Мы использовали в основном стандартные Python библиотеки, правда одна одна сторонняя все же есть. Это requests, она используется для выполнения HTTP запросов, давай её установим.

pip install requests

3. Указываем адреса в recipients.txt

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

4. Запускаем

Теперь мы готовы к запуску, для этого вводим следующую команду:

python main.py

После того как скрипт запущен он должен спросить нас, готовы ли мы начать раскидку баланса.

Как можно улучшить скрипт

Мы с тобой написали лишь базовый функционал, который можно расширять. Например, ты можешь добавить проверку баланса на OKX перед переводом средств или добавить ожидания выполнения транзакции.

Небольшие наводки, если вдруг тебе станет это интересно:

  1. Для проверки баланса можешь использовать эту ручку.
  2. Если ты захотел добавить ожидание тразакции, то тебе понадобится вытащить из ответа Withdrawal ID (хранится в data["wdId"]) и затем использовать эту ручку.

Заключение

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

Если статья тебе понравилась, сохрани её 📋, чтобы не потерять.

Не забудь написать в комментариях 👇, что именно ты хотел бы увидеть в следующий раз!

AIO Study | Site