Skip to content

StormFloof/auction

Repository files navigation

Реализация многораундовой аукционной системы на Node.js + TypeScript, основанная на механике Telegram Gift Auctions.

📺 Для жюри конкурса: см. DEMO.md — демо-материалы, видео, работающий сайт (https://youtu.be/_YQOnP4ZYN0) (https://auction-production-cfa9.up.railway.app/)

🚀 Быстрый старт (для жюри конкурса)

Вариант 1: Docker (полный стек одной командой)

# Запустить MongoDB + API + Workers
docker compose -f docker-compose.full.yml up --build

После запуска контейнеров:

# Создать демо-данные
npm install
npm run seed:demo

Готово! Откройте http://localhost:3000

Вариант 2: Локальный запуск

# 1. MongoDB
docker compose up -d

# 2. Установить зависимости
npm install

# 3. Запустить все сервисы (API + Workers)
npm run start:all

# 4. Создать демо-данные (в другом терминале)
npm run seed:demo

Готово! Откройте http://localhost:3000

Что запускается?

  • API Server (порт 3000) — REST API + UI
  • Worker — автоматическое закрытие раундов
  • Reconcile Worker — проверка финансовых инвариантов
  • MongoDB (порт 27017) — replica set для транзакций
  • Mongo Express (порт 8081) — веб-интерфейс БД

Демо-данные

Команда npm run seed:demo создаёт:

  • 5 тестовых аккаунтов с балансами
  • Демо-аукцион с автоучастниками
  • Запускает аукцион автоматически

Deploy (SSH, root+password)

Локально (Windows):

pip install -r contest-auction/deploy/requirements.txt
python contest-auction/deploy/deploy.py

Enterprise-уровень решений

Финансовая надёжность:

  • Reconcile Worker — автоматическая проверка и исправление финансовых инвариантов
  • Compensating Transactions — восстановление после ошибок capture/release
  • Optimistic Locking — retry механизм для конкурентных операций
  • Hold/Release/Capture модель для безопасной работы со ставками

Безопасность (10/10):

  • XSS Protection — экранирование всех пользовательских данных в UI
  • Rate Limiting — 2000 запросов за 1 минуту с одного IP
  • CORS — настроенная политика межсайтовых запросов
  • Helmet — защитные HTTP-заголовки (CSP, XSS, MIME)
  • Input Validation — Zod-схемы для всех API endpoints

Надёжность и наблюдаемость:

  • 55 тестов — покрытие критических сценариев
  • Prometheus метрики — мониторинг ставок и конфликтов
  • Structured logs (JSON) — удобная диагностика
  • MongoDB транзакции — атомарность операций

Механика:

  • Multi-round аукционы — открытая система, новые участники могут войти в любой раунд
  • Multi-lot — несколько призов в одном аукционе (totalLots = lotsCount × maxRounds)
  • Anti-sniping — продление раунда только при конкурентных ставках (обгон лидера)
  • Carry-over ставок — ставки автоматически переносятся между раундами
  • Открытая система — участники могут входить в любой раунд без ограничений
  • Tie-breaking — честная конкуренция при равных ставках (раньше = лучше)
  • Идемпотентность — безопасные retry операций

Запуск

Можно создать .env (см. .env.example) или не создавать — тогда используются дефолты для локальной Mongo.

cd contest-auction
npm install
npm run dev

По умолчанию npm run dev запускает API и встроенный scheduler (inline worker), который автоматически закрывает раунды. Если вы запускаете отдельный воркер, отключите inline worker:

cd contest-auction

# вариант 1
WORKER_INLINE=0 npm run dev

# вариант 2 (аналогично)
WORKER_EXTERNAL=1 npm run dev

Тесты (55 тестов):

cd contest-auction
npm test

Покрытие критических сценариев:

  • Финансовые операции (hold/release/capture)
  • Конкурентность и race conditions
  • Eligibility и отсев участников
  • Anti-sniping и продления раундов
  • Финализация аукционов
  • Multi-lot механика

UI: GET http://localhost:3000/

MongoDB Replica Set

MongoDB настроен как single-node replica set (rs0) для поддержки транзакций. При первом запуске контейнер автоматически инициализирует replica set через mongo-init.js. Connection string включает ?replicaSet=rs0.

Проверка статуса replica set:

docker exec -it contest-auction-mongo mongosh --eval "rs.status()"

UI: обновлённый тёмный интерфейс (mobile-first), полностью на русском.

Dev Mode (UI)

По умолчанию UI в User Mode (минимализм). Dev Mode включает Dev Panel.

  • query: /?dev=true
  • hotkey: Ctrl+Shift+D
  • кнопка Dev в header (сохраняется в localStorage)

User Mode (UI) — быстрый старт

  1. Откройте UI: GET http://localhost:3000/

  2. В блоке Управление:

  • задайте Пользователь (ник) и сохраните;
  • (опционально) пополните баланс;
  • в разделе Создать аукцион заполните параметры и нажмите Создать и запустить.
  1. Для участников можно скопировать ссылку (кнопка Скопировать ссылку) — Dev Mode не нужен.

  2. Ставка делается через кнопку Сделать ставку (bottom-sheet): можно вводить сумму вручную, использовать +1 шаг / +2 шага / Максимум, ошибки показываются понятным языком.

Worker (автопереходы раундов)

В отдельном процессе:

cd contest-auction
npm run worker

Логи воркера пишутся в stdout в JSON (удобно для сборщиков логов/grep).

После сборки:

cd contest-auction
npm run build
npm run start:worker

Reconcile Worker (проверка финансовых инвариантов)

Reconcile worker проверяет консистентность финансовых операций и автоматически исправляет обнаруженные проблемы:

  • Проверка инварианта total = available + held для всех аккаунтов
  • Обнаружение зависших holds после завершения аукционов
  • Проверка соответствия holds активным ставкам
  • Автоматическое исправление (auto-fix) для безопасных случаев
  • Создание записей для ручного разбора (manual review) для сложных случаев

Запуск:

cd contest-auction
npm run reconcile

Интервал проверки по умолчанию: 5 минут (настраивается через RECONCILE_INTERVAL_MS).

Модели:

  • ReconcileIssue — запись об обнаруженной проблеме с деталями и статусом (detected/resolved/manual_review)

Compensating transactions:

  • При ошибках capture/release в финализации автоматически создаются записи ReconcileIssue
  • Reconcile worker пытается исправить их при следующем запуске
  • После 3 неудачных попыток issue переходит в статус manual_review

Health-check: GET http://localhost:3000/health

Prometheus: GET http://localhost:3000/metrics

Механика аукциона (Multi-round + Multi-lot)

Модель распределения лотов (Telegram Gift Auctions)

ВАЖНО: Аукцион имеет ФИКСИРОВАННОЕ количество призов:

  • totalLots = lotsCount × maxRounds (общее количество призов)
  • В каждом раунде раздается lotsCount призов
  • Аукцион завершается когда розданы ВСЕ totalLots призы
  • Участники без приза просто проигрывают (это нормально)

Пример:

  • lotsCount = 3 (призов в раунде)
  • maxRounds = 4 (максимум раундов)
  • totalLots = 3 × 4 = 12 (всего призов)
  • Раунд 1: 3 победителя, остальные продолжают
  • Раунд 2: 3 победителя, остальные продолжают
  • Раунд 3: 3 победителя, остальные продолжают
  • Раунд 4: 3 победителя, аукцион завершен
  • Всего роздано: 3+3+3+3 = 12 призов ✅

Multi-round система (открытая)

ВАЖНО: Открытая система раундов — новые участники могут войти в любой раунд.

Переход раунда:

  • Каждый раунд имеет фиксированную длительность (roundDurationSec)
  • После закрытия раунда топ-lotsCount участников получают приз и ВЫБЫВАЮТ
  • Все остальные участники продолжают со своими ставками (carry-over)
  • Новые участники могут войти в следующий раунд
  • Аукцион завершается когда розданы все totalLots призы

Ранжирование участников:

  1. Максимальная ставка за ВСЕ раунды (desc)
  2. Время первой ставки с этой суммой (asc) — tie-breaker

Финализация: Аукцион завершается автоматически когда lotsDistributed >= totalLots:

  • Победители раундов уже получили capture (списание) при закрытии раунда
  • Оставшимся участникам возвращается hold (release)
  • Статус меняется на finished

Carry-over ставок между раундами

Важно: Ставки автоматически переносятся между раундами:

  • Участник делает ставку в раунде 1 → hold резервирует средства
  • Переходит в раунд 2 → ставка сохраняется автоматически (carry-over)
  • Участник НЕ обязан делать новую ставку в каждом раунде
  • Используется максимальная ставка по всем раундам для ранжирования
  • Повышает ставку → hold увеличивается на дельту
  • Выигрывает → hold конвертируется в capture (списание)
  • Проигрывает в раунде → hold возвращается (release)

Преимущества:

  • ✅ Простота для участников — не нужно повторять ставку каждый раунд
  • ✅ Справедливость — все ставки учитываются независимо от раунда
  • ✅ Соответствует логике Telegram Gift Auctions

Anti-sniping (защита от ставок в последнюю секунду)

Механизм продления раунда: Раунд продлевается автоматически при выполнении всех условий:

  1. Ставка сделана в окне anti-sniping (snipingWindowSec секунд до конца)
  2. Ставка конкурентная — обгоняет текущего лидера раунда
  3. Не исчерпан лимит продлений (maxExtensionsPerRound)

ВАЖНО: Продление происходит ТОЛЬКО при конкурентных ставках, которые обгоняют текущего лидера. Это защищает от бесконечного продления раунда спамом микроставок.

Конкурентная ставка:

  • Новая ставка должна быть больше ставки текущего лидера раунда
  • Микроставки без обгона лидера НЕ продлевают раунд
  • Защита от спама и манипуляций

Формула продления:

new_endsAt = max(current_endsAt, now) + extendBySec

Пример:

  • snipingWindowSec = 10 (последние 10 секунд раунда)
  • extendBySec = 10 (продлить на 10 секунд)
  • maxExtensionsPerRound = 5 (максимум 5 продлений)

Атомарность: Продление происходит в той же транзакции, что и запись ставки.

Multi-lot (несколько призов)

Ключевые поля:

  • lotsCount — количество призов в каждом раунде
  • maxRounds — максимальное количество раундов
  • totalLots — общее количество призов (lotsCount × maxRounds)
  • winners — массив participantId победителей (заполняется после финализации)
  • winningBids — массив { participantId, amount } с финальными ставками победителей

Пример:

  • lotsCount = 3 (3 приза в каждом раунде)
  • maxRounds = 4 (максимум 4 раунда)
  • totalLots = 12 (всего 12 призов)
  • После 4 раундов роздано 12 призов → финализация

API

  • GET /auctions/:id — возвращает winners и winningBids для завершённых аукционов
  • GET /api/auction/my-wins — получение выигрышей текущего участника
  • POST /auctions/:id/cancel — отменяет аукцион и возвращает все hold'ы участникам

UI: если таймер дошёл до 0с, но сервер ещё не закрыл раунд (например, из-за задержки/продления) — вместо «0с» показывается «Синхронизация…» и UI сам делает рефетч.

Load test (k6)

Требуется установленный k6 (без Grafana/Prometheus):

choco install k6
  1. Поднять Mongo:
cd contest-auction
docker compose up -d
  1. Поднять API (в одном терминале):
cd contest-auction
npm install
npm run dev
  1. Поднять worker (в другом терминале):
cd contest-auction
npm run worker
  1. Прогнать нагрузку (stdout + файл с summary):
cd contest-auction
API_BASE_URL=http://localhost:3000/api \
  DURATION_SEC=60 STEADY_VUS=20 SPIKE_VUS=50 SPIKE_LAST_SEC=10 \
  PARTICIPANTS=100 DEPOSIT_AMOUNT=100000 MIN_INCREMENT=10 \
  npm run load

Anti-sniping режим (интенсивные ставки в последние секунды + проверка продления roundEndsAt):

cd contest-auction
API_BASE_URL=http://localhost:3000/api MODE=anti_sniping \
  ANTI_ROUND_DURATION_SEC=20 ANTI_SNIPING_WINDOW_SEC=10 ANTI_SNIPING_EXTEND_SEC=10 ANTI_SNIPING_MAX_EXTENDS=10 \
  DURATION_SEC=30 STEADY_VUS=10 SPIKE_VUS=50 SPIKE_LAST_SEC=8 \
  PARTICIPANTS=100 DEPOSIT_AMOUNT=100000 MIN_INCREMENT=10 LOTS_COUNT=1 \
  npm run load:sniping

Можно использовать существующий аукцион:

cd contest-auction
API_BASE_URL=http://localhost:3000/api AUCTION_ID=<auctionId> \
  DURATION_SEC=60 STEADY_VUS=20 SPIKE_VUS=50 SPIKE_LAST_SEC=10 \
  PARTICIPANTS=100 DEPOSIT_AMOUNT=100000 MIN_INCREMENT=10 \
  npm run load

Результаты: stdout; также сохраняется JSON-отчёт в contest-auction/load/summary.json.

Bots (нагрузка/демо ставок)

Запуск (сервер + worker в отдельных терминалах, затем боты):

cd contest-auction
npm run dev
cd contest-auction
npm run worker
cd contest-auction
API_BASE_URL=http://localhost:3000/api \
  BOTS=50 CONCURRENCY=30 DURATION_SEC=60 \
  npm run bots

Можно указать существующий аукцион:

cd contest-auction
API_BASE_URL=http://localhost:3000/api AUCTION_ID=<auctionId> \
  BOTS=50 CONCURRENCY=30 DURATION_SEC=60 \
  npm run bots

Автоучастники (live аукцион без Dev Mode)

Серверная настройка автозапуска:

cd contest-auction

# включить по умолчанию для новых/активных аукционов
BOTS_AUTOSTART=1 \
  BOTS_AUTOSTART_STRATEGY=calm \
  BOTS_AUTOSTART_COUNT=20 \
  BOTS_AUTOSTART_TICK_MS=900 \
  npm run dev

Поддерживаемые стратегии: calm / aggressive.

Логика ботов:

  • Бот перебивает человека максимум 1 раз за весь аукцион
  • Боты могут перебивать друг друга без ограничений
  • Защита от доминирования ботов над живыми участниками

В UI (User Mode) при создании аукциона есть переключатель «Автоучастники: Вкл/Выкл». В Dev Mode отображаются расширенные параметры (режим/количество/интервал) — они сохраняются в документе аукциона.

HTTP API (demo)

База: http://localhost:3000/api

1) Тестовый депозит

curl -X POST "http://localhost:3000/api/accounts/u1/deposit" \
  -H "content-type: application/json" \
  -d '{"amount":"1000","currency":"RUB"}'

Проверка баланса/hold:

curl "http://localhost:3000/api/accounts/u1?currency=RUB"

2) Создать аукцион

curl -X POST "http://localhost:3000/api/auctions" \
  -H "content-type: application/json" \
  -d '{
    "code":"A-001",
    "title":"Gift auction demo",
    "currency":"RUB",
    "roundDurationSec":30,
    "minIncrement":"10",
    "topK":3,
    "snipingWindowSec":10,
    "extendBySec":10,
    "maxExtensionsPerRound":10
  }'

3) Стартовать аукцион

curl -X POST "http://localhost:3000/api/auctions/<auctionId>/start"

4) Сделать/повысить ставку

curl -X POST "http://localhost:3000/api/auctions/<auctionId>/bids" \
  -H "content-type: application/json" \
  -d '{"participantId":"u1","amount":"100","idempotencyKey":"u1-r1-1"}'

5) Статус аукциона (включая лидеров текущего раунда)

curl "http://localhost:3000/api/auctions/<auctionId>?leaders=10"

6) Лидерборд конкретного раунда

curl "http://localhost:3000/api/auctions/<auctionId>/rounds/1/leaderboard?limit=10"

7) Закрыть текущий раунд (demo, без scheduler)

curl -X POST "http://localhost:3000/api/auctions/<auctionId>/rounds/close"

Сборка и старт

cd contest-auction
npm run build
npm run start