104 Commits
dev ... main

Author SHA1 Message Date
NW
2f3459b670 mart litle update 2025-03-06 16:13:11 +00:00
NW
0c10772261 Update Start Process 2025-03-02 11:21:35 +00:00
NW
c8b6e3ceb3 litle update 2025-02-05 16:40:00 +00:00
NW
23b7f8b4bd big update WG-TOR bot connecting 2025-02-03 09:43:25 +00:00
NW
633a27164b upgrade comission wallet function 2025-01-26 22:21:13 +00:00
NW
25c74342f9 package lock file recreate 2025-01-25 13:37:03 +00:00
NW
ae1cd45aea create functional commission 2025-01-25 13:35:22 +00:00
NW
5ec8267253 Удалить package-lock.json 2025-01-25 13:34:33 +00:00
NW
79ee8b90f0 Добавить package-lock.json 2025-01-25 13:27:14 +00:00
NW
3a58b73112 update package 2025-01-25 09:31:56 +00:00
NW
fa09e81ddf crypto mnemonic case 2025-01-25 01:13:10 +00:00
NW
24aebd0bcf update packege file 2025-01-24 18:45:35 +00:00
NW
fcd89bc345 update calculate user balance in admin section 2025-01-09 20:13:45 +00:00
NW
dd18e74529 update calculate user balance 2025-01-09 20:07:44 +00:00
NW
f9356c6bbe update user purchase list 2025-01-09 13:25:35 +00:00
NW
18647091cf minor edits to aesthetics and functionality 2025-01-08 18:26:50 +00:00
NW
5ae148a2ba update planned wallets function 2025-01-08 16:20:43 +00:00
NW
66f5251795 update check ETH USDT USDC balance function 2025-01-08 12:01:02 +00:00
NW
e64f185eda separate wallet ETH USDT USDC 2025-01-02 19:31:28 +00:00
NW
22f76c64a6 delet TRON wallet type 2025-01-02 16:19:39 +00:00
NW
c9bcb09221 udpdate wallet function 2024-12-24 09:19:14 +00:00
NW
3129525a1e update user and admin wallet function 2024-12-23 20:44:56 +00:00
NW
a970a188db new user registration function 2024-12-18 19:46:29 +00:00
NW
b224b3f331 update UserService 2024-12-18 16:16:41 +00:00
NW
bfb9a55e36 update viev balance 2024-12-17 00:19:53 +00:00
NW
4aebb4e41b update user info page 2024-12-17 00:05:59 +00:00
NW
a575f75faf user catalog navigation upgrade 2024-12-16 23:56:09 +00:00
NW
21465022b3 whallets upgrade function 2024-12-16 23:43:44 +00:00
NW
d51bc9f0b9 User start registration update function 2024-12-16 12:37:44 +00:00
NW
2cfa37ea86 fix bug back navigation 2024-12-15 02:04:43 +00:00
NW
9d9e0e80ad Bug update function 2024-12-14 23:12:36 +00:00
NW
682246675e update handleProductSelection 2024-12-14 15:06:22 +00:00
NW
2aea225e2e update back category 2024-12-14 15:02:50 +00:00
NW
d918de0386 docker file update 2024-12-14 13:46:03 +00:00
NW
12d29c66b9 Update DistrictSelection back button 2024-12-14 13:16:22 +00:00
NW
207b9a829c delet subcatecory viev line 2024-12-14 13:10:23 +00:00
NW
3843dcb094 Update handleBuyProduct 2024-12-14 13:07:46 +00:00
NW
eea5d9b9e7 revert 99137e4e97
revert Update Detailed Product Viev
2024-12-14 12:54:50 +00:00
NW
057d1536bb Check bug delet category 2024-12-14 10:47:22 +00:00
NW
99137e4e97 Update Detailed Product Viev 2024-12-14 00:37:24 +00:00
NW
a400d12d16 Delet subcategory function in handleCategorySelection 2024-12-13 18:24:24 +00:00
NW
95d5fe644d Delet Subcategory Function 2024-12-13 16:41:41 +00:00
NW
3e78e231f3 0 update adminHandlers 2024-12-13 13:41:49 +00:00
NW
13a2d67474 rewrite sampleProduct 2024-12-05 18:43:00 +00:00
1323ed5
37083ca5bc Merge pull request 'feature/user-section' (#38) from feature/user-section into main
Reviewed-on: #38
2024-12-05 18:31:21 +00:00
Artyom Ashirov
82ffa81141 account deletion 2024-12-05 21:29:32 +03:00
Artyom Ashirov
e3b82bb3dd pay with main balance 2024-12-05 16:18:27 +03:00
1323ed5
ba15d09823 Merge pull request 'main' (#36) from main into feature/user-section
Reviewed-on: #36
2024-12-04 20:12:21 +00:00
NW
ea7f5dbd11 Update handleCategoryUpdate 2024-11-25 23:58:05 +00:00
NW
0d8230e93c Add handleCategoryUpdate and handleEditCategory 2024-11-25 23:07:01 +00:00
1323ed5
7fd4cfa671 Merge pull request 'refactoring' (#34) from refactoring into main
Reviewed-on: #34
2024-11-25 13:31:53 +00:00
Artyom Ashirov
64e6397570 bug fixes 2024-11-25 16:22:28 +03:00
Artyom Ashirov
5d4f56e265 refactoring 2024-11-23 05:03:30 +03:00
NW
dcc678a42b update gitignore 2024-11-22 11:21:00 +00:00
NW
68a220de2e update docker file 2024-11-22 10:03:53 +00:00
1323ed5
275a6ab493 Merge pull request 'feature/user-section' (#33) from feature/user-section into main
Reviewed-on: #33
2024-11-21 15:01:49 +00:00
Artyom Ashirov
1f7d1a144c bonus balance 2024-11-21 17:58:45 +03:00
1323ed5
0371f70546 Merge pull request 'main' (#32) from main into feature/user-section
Reviewed-on: #32
2024-11-21 11:51:41 +00:00
1323ed5
3c942da274 Merge pull request 'feature/admin-section' (#31) from feature/admin-section into main
Reviewed-on: #31
2024-11-21 11:50:21 +00:00
Artyom Ashirov
a760cb2d23 Bonus balance editing 2024-11-21 14:48:45 +03:00
Artyom Ashirov
d4eb5d46c3 Zip error fix 2024-11-21 13:43:41 +03:00
1323ed5
3153ecf325 Merge pull request 'main' (#30) from main into feature/admin-section
Reviewed-on: #30
2024-11-21 10:29:14 +00:00
1323ed5
97073eb939 Merge pull request 'Old purchase method removed' (#29) from feature/user-section into main
Reviewed-on: #29
2024-11-21 10:27:28 +00:00
Artyom Ashirov
627f9e417e Old purchase method removed 2024-11-21 13:24:04 +03:00
1323ed5
c1276e1187 Merge pull request 'purchase viewing' (#28) from feature/user-section into main
Reviewed-on: #28
2024-11-20 15:20:16 +00:00
Artyom Ashirov
1cb0467f6c purchase viewing 2024-11-20 17:43:51 +03:00
1323ed5
563d9d27c7 Merge pull request 'main' (#27) from main into feature/admin-section
Reviewed-on: #27
2024-11-19 17:57:35 +00:00
1323ed5
56698c28c4 Merge pull request 'feature/user-section' (#26) from feature/user-section into main
Reviewed-on: #26
2024-11-19 17:56:20 +00:00
Artyom Ashirov
a35bbbf3d9 Recalculate balance 2024-11-19 19:19:05 +03:00
Artyom Ashirov
6fa273f0b6 Item purchase 2024-11-19 05:01:13 +03:00
1323ed5
626435a3de Merge pull request 'main' (#24) from main into feature/user-section
Reviewed-on: #24
2024-11-17 11:57:14 +00:00
NW
1b1a9468a0 Merge pull request 'feature/admin-section' (#23) from feature/admin-section into main
Reviewed-on: #23
2024-11-17 00:48:01 +00:00
NW
072067eb4a Merge branch 'main' into feature/admin-section 2024-11-17 00:45:10 +00:00
Artyom Ashirov
b45aa35527 Database import 2024-11-16 20:55:47 +03:00
Artyom Ashirov
e3f2e87fcc export database 2024-11-16 19:18:50 +03:00
Artyom Ashirov
44ae5a6631 Delete photos after exit from card 2024-11-16 18:04:32 +03:00
Artyom Ashirov
ec96f67dfe Product edition 2024-11-16 17:53:43 +03:00
1323ed5
aed9a2ba56 Merge pull request 'feature/admin-section' (#22) from feature/admin-section into main
Reviewed-on: #22
2024-11-15 12:45:54 +00:00
Artyom Ashirov
772cd738ca merge glitch solved 2024-11-15 15:43:00 +03:00
1323ed5
18e09119b3 Merge pull request 'main' (#21) from main into feature/admin-section
Reviewed-on: #21
2024-11-15 12:35:23 +00:00
1323ed5
d760c77593 Merge pull request 'feature/admin-section' (#20) from feature/admin-section into main
Reviewed-on: #20
2024-11-15 12:34:09 +00:00
Artyom Ashirov
89a7a8b9c5 product deletion 2024-11-15 09:15:44 +03:00
Artyom Ashirov
e1eda05afe products pagination 2024-11-15 08:19:54 +03:00
Artyom Ashirov
df3149e59a product photo fix 2024-11-15 07:31:46 +03:00
Artyom Ashirov
de5e405093 import from json 2024-11-15 07:07:02 +03:00
Artyom Ashirov
4251f1a0bd user deletion/blocking 2024-11-15 06:18:55 +03:00
NW
7c79e7fd94 Merge pull request 'feature/admin-section' (#16) from feature/admin-section into main
Reviewed-on: #16
2024-11-14 23:55:23 +00:00
NW
f7d21d9d0d Обновить .gitea/workflows/dev.yaml 2024-11-14 23:32:46 +00:00
Artyom Ashirov
373e8e2567 user deletion/blocking 2024-11-15 02:26:13 +03:00
Artyom Ashirov
b45f7daa6f add location state reset 2024-11-15 00:29:42 +03:00
Artyom Ashirov
ebad9da439 Another back to admin fix 2024-11-15 00:25:34 +03:00
NW
82afcf9854 Обновить .gitea/workflows/dev.yaml 2024-11-14 21:18:41 +00:00
NW
9c4253ba58 Обновить .gitea/workflows/dev.yaml 2024-11-14 21:16:34 +00:00
Artyom Ashirov
f504d5fb7b Back to admin menu fix 2024-11-15 00:15:13 +03:00
NW
38ba2356bc Обновить .gitea/workflows/dev.yaml 2024-11-14 21:09:33 +00:00
NW
1cabe48594 Обновить .gitea/workflows/dev.yaml 2024-11-14 21:07:50 +00:00
NW
5eec9586b7 Обновить .gitea/workflows/prod.yaml 2024-11-14 20:16:38 +00:00
NW
97079ce1d5 Обновить .gitea/workflows/demo.yaml 2024-11-14 20:15:51 +00:00
NW
f737d46802 Обновить .gitea/workflows/dev.yaml 2024-11-14 20:13:18 +00:00
NW
82132c7466 Merge pull request 'feature/admin-section' (#12) from feature/admin-section into main
Reviewed-on: #12
2024-11-14 19:32:30 +00:00
Artyom Ashirov
d506d79367 users pagination 2024-11-14 21:27:32 +03:00
Artyom Ashirov
93290dee1c admin access 2024-11-14 20:29:57 +03:00
Artyom Ashirov
2beaa324fa Users location edition added 2024-11-14 20:16:17 +03:00
Artyom Ashirov
52779d20ab Nickname added 2024-11-14 18:24:04 +03:00
50 changed files with 12725 additions and 7681 deletions

View File

@@ -0,0 +1,30 @@
name: "Telegram Shop Bot CI [DEMO]"
on:
push:
branches: ["demo"]
pull_request:
branches: ["demo"]
jobs:
lint:
runs-on: ubuntu-latest # Запускаем на универсальной платформе
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js 22
run: |
# Устанавливаем nvm (Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 22
nvm use 22
node -v # Проверка установленной версии Node.js
- name: Install dependencies
run: npm install
- name: Run lint
run: npm run lint

View File

@@ -8,29 +8,17 @@ on:
branches:
- dev
env:
GITEA_HOST: 192.168.2.29:3000
jobs:
lint:
runs-on: ubuntu-latest # Запуск на Ubuntu
steps:
- name: Check DNS Resolution
run: |
echo "Проверка DNS-резолвинга"
curl -I http://${{ env.GITEA_HOST }}
- name: Checkout code
run: |
echo "Клонируем репозиторий"
git clone http://${{ env.GITEA_HOST }}/Telegram-Market/telegram-shop.git /workspace/Telegram-Market/telegram-shop
cd /workspace/Telegram-Market/telegram-shop
git checkout 2d36bc514fa832c8c6b538631537dfb88a1ac6ca
uses: actions/checkout@v3
- name: Set up Node.js 22
run: |
echo "Устанавливаем Node.js 22"
curl -sL http://deb.nodesource.com/setup_22.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # Проверка установленной версии Node.js
@@ -39,14 +27,7 @@ jobs:
echo "Устанавливаем зависимости"
npm install
- name: Initialize ESLint configuration
run: |
echo "Инициализируем конфигурацию ESLint"
if [ ! -f .eslintrc.json ]; then
npm init @eslint/config
fi
- name: Run lint
run: |
echo "Запуск линтинга"
npm run lint
npm run lint

View File

@@ -0,0 +1,30 @@
name: "Telegram Shop Bot CI [PROD]"
on:
push:
branches: ["prod"]
pull_request:
branches: ["prod"]
jobs:
lint:
runs-on: ubuntu-latest # Запускаем на универсальной платформе
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js 22
run: |
# Устанавливаем nvm (Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 22
nvm use 22
node -v # Проверка установленной версии Node.js
- name: Install dependencies
run: npm install
- name: Run lint
run: npm run lint

2
.gitignore vendored
View File

@@ -1 +1 @@
node_modules
db

View File

@@ -1,11 +1,26 @@
FROM node:22
FROM node:22-alpine
# Устанавливаем необходимые пакеты
RUN apk update && \
apk add --no-cache \
wireguard-tools \
iptables \
iproute2 \
openresolv \
bash \
curl && \
rm -rf /var/cache/apk/*
# Рабочая директория
WORKDIR /app
COPY package*.json /app/
COPY src/ /app/src/
COPY db/shop.db /app/shop.db
# Копируем зависимости и устанавливаем их
COPY package*.json ./
RUN npm install
CMD ["node", "src/index.js"]
# Копируем скрипт запуска
COPY ./wg/start.sh /app/start.sh
RUN chmod +x /app/start.sh
# Команда для запуска
CMD ["/bin/bash", "/app/start.sh"]

155
README.md
View File

@@ -1,53 +1,139 @@
**Универсальный Телеграмм Магазин**
# Универсальный Телеграмм Магазин
**Описание проекта**:
"Универсальный Телеграмм Магазин" — это телеграмм-бот, предназначенный для организации и управления онлайн-продажами товаров и услуг через популярную платформу Telegram. Магазин включает функционал как для пользователей, так и для администраторов, обеспечивая удобное взаимодействие с товарами, балансами, кошельками и покупками.
"Универсальный Телеграмм Магазин" — это телеграмм-бот для организации онлайн-продаж через Telegram. Проект предоставляет полный цикл управления магазином, включая работу с криптовалютами, управление товарами и пользователями, а также интеграцию с VPN через WireGuard для безопасных транзакций.
**Основные технологии**:
- Node.js + Telegraf для работы с Telegram API
- SQLite для хранения данных
- Docker для контейнеризации
- WireGuard для защищенных соединений
- Поддержка криптокошельков (Bitcoin, Ethereum, Litecoin)
Проект включает несколько ключевых разделов для удобной работы пользователей и администраторов, а также позволяет интегрировать систему криптокошельков для расчетов, управления товарами и отслеживания покупок.
### Цели проекта:
- Создание удобного и универсального интерфейса для покупок через Telegram.
- Обеспечение безопасности и простоты транзакций с использованием криптовалют и традиционных средств.
- Внедрение эффективной системы управления для администраторов, с возможностью мониторинга пользователей, товаров, кошельков и комиссий.
- Реализация системы профилей с возможностью редактирования, управления балансами и удаления аккаунтов.
### Основной функционал
#### Для пользователей:
- Просмотр товаров по категориям с фильтрацией по локациям
- Совершение покупок с использованием криптовалют
- Управление криптокошельками (создание, пополнение, просмотр баланса)
- Просмотр истории транзакций и покупок
- Настройка профиля (локация, контактные данные)
- Подключение к защищенному VPN через WireGuard для безопасных транзакций
#### Для администраторов:
- Полное управление товарами и категориями
- Управление пользователями (блокировка, редактирование балансов)
- Контроль транзакций и комиссий
- Создание дампов базы данных с автоматическим списанием комиссии (% от балансов кошельков)
- Управление локациями и настройками VPN
- Мониторинг активности пользователей
- SaaS система с автоматическим расчетом комиссий:
- Комиссия за оборот по магазину перед выгрузкой кошельков
---
### Структура проекта:
### Установка и запуск
#### 1. **Пользовательский раздел**
Пользователи могут:
- Просматривать и покупать товары, управлять своим балансом.
- Следить за историей покупок.
- Пополнять свои криптокошельки.
- Управлять своим профилем, изменяя локацию и удаляя аккаунт.
#### Требования:
- Node.js 18+
- Docker и Docker Compose
- Telegram Bot Token
- SQLite connection string
- WireGuard конфигурация
#### 2. **Административный раздел**
Администраторы могут:
- Управлять пользователями: блокировать, удалять и редактировать балансы.
- Управлять товарами: добавлять, редактировать, удалять товары и категории.
- Управлять кошельками: контролировать пополнения и комиссионные платежи.
- Создавать дампы для переноса базы данных магазина.
#### 1. Установка зависимостей:
```bash
npm install
```
#### 2. Настройка конфигурации:
Создайте файл `.env` в корне проекта со следующим содержимым:
```env
TELEGRAM_BOT_TOKEN=your_bot_token
MONGO_URI=mongodb://localhost:27017/telegram_shop
WIREGUARD_CONFIG_PATH=./wg/config/wg0.conf
```
#### 3. Запуск через Docker:
```bash
docker-compose up -d
```
#### 4. Настройка WireGuard:
1. Сгенерируйте ключи:
```bash
wg genkey | tee privatekey | wg pubkey > publickey
```
2. Настройте wg0.conf:
```ini
[Interface]
PrivateKey = <your_private_key>
Address = 10.0.0.1/24
ListenPort = 51820
[Peer]
PublicKey = <client_public_key>
AllowedIPs = 10.0.0.2/32
```
#### 5. Запуск бота:
```bash
npm start
```
---
### Основной функционал:
### Структура проекта
#### 1. **Покупки и товары**
- **Продукты**: Пользователи могут выбирать товары по категориям, проверять наличие средств и совершать покупки.
- **Профиль**: В разделе профиля можно изменять локацию, а также удалять аккаунт.
- **История покупок**: Пользователи могут отслеживать свои покупки с описанием товаров и статусов.
- **Кошельки**: Возможность добавлять новые криптокошельки, пополнять их через QR-коды и просматривать историю транзакций.
```
├── src/
├── config/ # Конфигурация приложения
├── context/ # Контекст и состояния бота
├── handlers/ # Обработчики команд
│ ├── models/ # Модели данных
│ ├── services/ # Бизнес-логика
│ ├── utils/ # Вспомогательные утилиты
│ └── index.js # Точка входа
├── wg/ # Конфигурация WireGuard
├── docker-compose.yml # Docker конфигурация
└── Dockerfile # Docker образ
```
#### 2. **Администрирование**
- **Управление пользователями**: Администратор может просматривать информацию о пользователях, управлять их балансами, блокировать или удалять аккаунты.
- **Управление товарами**: Добавление новых товаров, редактирование существующих и управление их категориями.
- **Создание дампов**: Администратор может создать дамп магазина, чтобы перенести данные на другой сервер или сохранить их для архивации.
---
#### 3. **Работа с криптовалютами**
- Поддержка различных типов криптокошельков (биткойн, эфириум, лайткоин и другие).
- Проверка баланса кошельков через общедоступные API.
- Управление комиссионными, которые необходимы для загрузки дампа магазина.
### Разработка
#### Запуск в режиме разработки:
```bash
npm run dev
```
#### Линтинг и форматирование:
```bash
npm run lint
npm run format
```
#### Тестирование:
```bash
npm test
```
---
### Лицензия
Проект распространяется под лицензией MIT. Подробнее см. в файле LICENSE.
---
### Контакты
По вопросам сотрудничества и поддержки:
- Email: support@telegram-shop.com
- Telegram: @telegram_shop_support
---
@@ -87,4 +173,3 @@
### Заключение:
**Универсальный Телеграмм Магазин** предоставляет эффективное решение для организации торговых процессов в Telegram, с возможностью работы с криптовалютами и традиционными средствами. Проект ориентирован на пользователей, которые ценят удобство, безопасность и скорость совершения покупок. Для администраторов — это мощный инструмент для управления товаром, пользователями и финансовыми потоками магазина.

BIN
corrupt-photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

View File

@@ -1,17 +1,39 @@
version: "3.3"
services:
telegram_shop_prod:
build:
context: .
dockerfile: ./Dockerfile
hostname: telegram_shop_prod
container_name: telegram_shop_prod
restart: always
environment:
- BOT_TOKEN=7626758249:AAEdcbXJpW1VsnJJtc8kZ5VBsYMFR242wgk
- ADMIN_IDS=732563549,390431690
- SUPPORT_LINK=https://t.me/neroworm
- CATALOG_PATH=./catalog
volumes:
- ./db:/app/db/
telegram_shop_prod:
build:
context: .
dockerfile: ./Dockerfile
hostname: telegram_shop_prod
container_name: telegram_shop_prod
restart: always
environment:
- WG_ENABLED=false # Включение/выключение WireGuard (true/false)
- BOT_TOKEN=7626758249:AAEdcbXJpW1VsnJJtc8kZ5VBsYMFR242wgk # Токен Telegram бота
- ADMIN_IDS=732563549,390431690,217546867 # ID администраторов через запятую
- SUPPORT_LINK=https://t.me/neroworm # Ссылка на поддержку
- CATALOG_PATH=./catalog # Путь к каталогу товаров
- COMMISSION_ENABLED=true # Включение комиссии (true/false)
- COMMISSION_PERCENT=5 # Процент комиссии
# Кошельки для комиссий:
- COMMISSION_WALLET_BTC=bc1qyourbtcaddress # Bitcoin
- COMMISSION_WALLET_LTC=ltc1qyourltcaddress # Litecoin
- COMMISSION_WALLET_USDT=0x654dbef74cae96f19aa03e1b0abf569b111572cc # USDT (ERC-20)
- COMMISSION_WALLET_USDC=0xYourUsdcAddress # USDC (ERC-20)
- COMMISSION_WALLET_ETH=0xYourEthAddress # Ethereum
volumes:
- ./db:/app/db/ # Синхронизация базы данных
- ./src:/app/src/ # Синхронизация исходного кода
- ./package.json:/app/package.json # Синхронизация package.json
- ./package-lock.json:/app/package-lock.json # Синхронизация package-lock.json
- ./wg/config/wg0.conf:/etc/wireguard/wg0.conf # Монтируем конфиг WireGuard
- ./wg/config/resolv.conf:/etc/resolv.conf # Монтируем resolv.conf
- ./wg/start.sh:/app/start.sh # Монтируем start.sh
cap_add: # Необходимо для работы WireGuard
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1 # Необходимо для маршрутизации
privileged: true # Даем контейнеру повышенные привилегии
networks:
default:

10340
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"lint": "eslint ."
"dev": "nodemon src/index.js"
},
"dependencies": {
"archiver": "^7.0.1",
"axios": "^1.7.7",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^6.1.6",
"crypto-js": "^4.2.0",
"decompress": "^4.2.1",
"dotenv": "^16.3.1",
"ecpair": "^2.1.0",
"ethereumjs-util": "^7.1.5",
@@ -19,8 +19,7 @@
"node-telegram-bot-api": "^0.64.0",
"sqlite3": "^5.1.6",
"tiny-secp256k1": "^2.2.3",
"tronweb": "^5.3.2",
"eslint": "^8.0.0"
"csv-writer": "^1.6.0"
},
"devDependencies": {
"nodemon": "^3.0.2"

View File

@@ -1,6 +1,18 @@
export default {
BOT_TOKEN: process.env.BOT_TOKEN,
ADMIN_IDS: process.env.ADMIN_IDS.split(","),
SUPPORT_LINK: process.env.SUPPORT_LINK,
CATALOG_PATH: process.env.CATALOG_PATH
};
export default {
BOT_TOKEN: process.env.BOT_TOKEN,
ADMIN_IDS: process.env.ADMIN_IDS.split(","),
SUPPORT_LINK: process.env.SUPPORT_LINK,
CATALOG_PATH: process.env.CATALOG_PATH,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY || '9о234893245wer6ga3425670',
// Commission settings
COMMISSION_ENABLED: process.env.COMMISSION_ENABLED === 'true',
COMMISSION_PERCENT: parseFloat(process.env.COMMISSION_PERCENT) || 0,
COMMISSION_WALLETS: {
BTC: process.env.COMMISSION_WALLET_BTC,
LTC: process.env.COMMISSION_WALLET_LTC,
USDT: process.env.COMMISSION_WALLET_USDT,
USDC: process.env.COMMISSION_WALLET_USDC,
ETH: process.env.COMMISSION_WALLET_ETH
}
};

View File

@@ -1,3 +1,5 @@
// database.js
import sqlite3 from 'sqlite3';
import { promisify } from 'util';
import { dirname } from 'path';
@@ -90,9 +92,13 @@ const initDb = async () => {
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id TEXT UNIQUE NOT NULL,
username TEXT,
country TEXT,
city TEXT,
district TEXT,
status INTEGER DEFAULT 0,
total_balance REAL DEFAULT 0,
bonus_balance REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
@@ -105,13 +111,24 @@ const initDb = async () => {
wallet_type TEXT NOT NULL,
address TEXT NOT NULL,
derivation_path TEXT NOT NULL,
encrypted_mnemonic TEXT NOT NULL,
mnemonic TEXT NOT NULL,
balance REAL DEFAULT 0, -- Добавлена колонка для хранения баланса
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, wallet_type)
)
`);
// Check if balance column exists in crypto_wallets table
const balanceExists = await checkColumnExists('crypto_wallets', 'balance');
if (!balanceExists) {
await db.runAsync(`
ALTER TABLE crypto_wallets
ADD COLUMN balance REAL DEFAULT 0
`);
console.log('Column balance added to crypto_wallets table');
}
// Create transactions table
await db.runAsync(`
CREATE TABLE IF NOT EXISTS transactions (
@@ -161,7 +178,6 @@ const initDb = async () => {
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
subcategory_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
private_data TEXT,
@@ -173,27 +189,37 @@ const initDb = async () => {
hidden_description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
FOREIGN KEY (subcategory_id) REFERENCES subcategories(id) ON DELETE CASCADE
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
)
`);
// Create purchases table
await db.runAsync(`
CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
wallet_type TEXT NOT NULL,
tx_hash TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
total_price REAL NOT NULL CHECK (total_price > 0),
purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
wallet_type TEXT NOT NULL,
tx_hash TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
total_price REAL NOT NULL CHECK (total_price > 0),
purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'pending',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
)
`);
// Проверка наличия поля status в таблице purchases
const statusExists = await checkColumnExists('purchases', 'status');
if (!statusExists) {
await db.runAsync(`
ALTER TABLE purchases
ADD COLUMN status TEXT DEFAULT 'pending'
`);
console.log('Column status added to purchases table');
}
// Create locations table
await db.runAsync(`
CREATE TABLE IF NOT EXISTS locations (
@@ -218,18 +244,6 @@ const initDb = async () => {
)
`);
// Create subcategories table
await db.runAsync(`
CREATE TABLE IF NOT EXISTS subcategories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE(category_id, name)
)
`);
// Commit transaction
await db.runAsync('COMMIT');
console.log('Database tables initialized successfully');

17
src/context/bot.js Normal file
View File

@@ -0,0 +1,17 @@
import TelegramBot from "node-telegram-bot-api";
import config from "../config/config.js";
const initBot = () => {
try {
const bot = new TelegramBot(config.BOT_TOKEN, {polling: true});
console.log('Bot initialized successfully');
return bot;
} catch (error) {
console.error('Failed to initialize bot:', error);
process.exit(1);
}
};
const bot = initBot();
export default bot;

View File

@@ -0,0 +1,2 @@
const userStates = new Map();
export default userStates;

View File

@@ -1,35 +0,0 @@
import db from '../config/database.js';
import config from '../config/config.js';
export default class AdminHandler {
constructor(bot) {
this.bot = bot;
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleAdminCommand(msg) {
const chatId = msg.chat.id;
if (!this.isAdmin(msg.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const keyboard = {
reply_markup: {
keyboard: [
['👥 Manage Users', '📦 Manage Products'],
['💰 Manage Wallets', '📍 Manage Locations'],
['💾 Database Backup']
],
resize_keyboard: true
}
};
await this.bot.sendMessage(chatId, 'Admin Panel:', keyboard);
}
}

View File

@@ -0,0 +1,161 @@
import config from '../../config/config.js';
import fs from "fs";
import db from "../../config/database.js";
import archiver from "archiver";
import decompress from "decompress";
import bot from "../../context/bot.js";
import userStates from "../../context/userStates.js";
export default class AdminDumpHandler {
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
static async handleDump(msg) {
const chatId = msg.chat.id;
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const keyboard = {
inline_keyboard: [
[
{text: '📥 Import dump', callback_data: 'import_database'},
{text: '📤 Export dump', callback_data: 'export_database'},
],
[{text: '« Back', callback_data: 'admin_menu'}]
]
}
await bot.sendMessage(chatId, 'Choose an option', {reply_markup: keyboard});
}
static async handleExportDatabase(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const tables = [
"categories",
"crypto_wallets",
"locations",
"products",
"purchases",
"transactions",
"users"
]
const dumpPath = "./dump"
try {
fs.rmdirSync(dumpPath, {recursive: true, force: true});
fs.rmSync(`${dumpPath}/dump.zip`);
} catch (e) {
}
fs.mkdirSync(dumpPath);
for (const table of tables) {
const result = await db.allAsync(`SELECT * FROM ${table}`);
const tableData = JSON.stringify(result);
fs.writeFileSync(`${dumpPath}/${table}.json`, tableData);
}
const archive = archiver('zip', {zlib: { level: 9 } });
archive.directory(dumpPath, false);
const output = fs.createWriteStream('./dump.zip');
archive.pipe(output);
await archive.finalize();
output.on('close', () => {
bot.sendDocument(chatId, './dump.zip', {caption: 'Database dump'});
});
}
static async handleImportDatabase(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
userStates.set(chatId, { action: 'upload_database_dump' });
await bot.editMessageText(
'Please upload database dump',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
}
);
}
static async getDumpStatistic() {
const tables = [
"categories",
"crypto_wallets",
"locations",
"products",
"purchases",
"transactions",
"users"
]
const stat = {}
for (const table of tables) {
const jsonContent = await fs.readFileSync(`./dump/${table}.json`, 'utf8');
const data = JSON.parse(jsonContent);
stat[table] = data.length
}
return stat;
}
static async handleDumpImport(msg) {
const chatId = msg.chat.id;
const state = userStates.get(chatId);
if (!state || state.action !== 'upload_database_dump') {
return false;
}
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
if (msg.document) {
if (!msg.document.file_name.endsWith('.zip')) {
await bot.sendMessage(chatId, 'Please upload a .zip file.');
return true;
}
const file = await bot.getFile(msg.document.file_id);
const fileContent = await bot.downloadFile(file.file_id, '.');
await decompress(fileContent, './dump');
const statistics = await this.getDumpStatistic();
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
userStates.delete(chatId);
} else {
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
return true;
}
}
static async confirmImport(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
}
}

View File

@@ -0,0 +1,30 @@
import config from '../../config/config.js';
import bot from "../../context/bot.js";
export default class AdminHandler {
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
static async handleAdminCommand(msg) {
const chatId = msg.chat.id;
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const keyboard = {
reply_markup: {
keyboard: [
['👥 Manage Users', '📦 Manage Products'],
['💰 Manage Wallets', '📍 Manage Locations'],
['💾 Database Backup']
],
resize_keyboard: true
}
};
await bot.sendMessage(chatId, 'Admin Panel:', keyboard);
}
}

View File

@@ -1,23 +1,24 @@
import db from '../config/database.js';
import Validators from '../utils/validators.js';
import config from '../config/config.js';
import db from '../../config/database.js';
import Validators from '../../utils/validators.js';
import config from '../../config/config.js';
import userStates from "../../context/userStates.js";
import bot from "../../context/bot.js";
export default class AdminLocationHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
isAdmin(userId) {
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleAddLocation(callbackQuery) {
static async handleAddLocation(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
this.userStates.set(chatId, { action: 'add_location' });
userStates.set(chatId, { action: 'add_location' });
await this.bot.editMessageText(
await bot.editMessageText(
'Please enter the location in the following format:\nCountry|City|District',
{
chat_id: chatId,
@@ -29,15 +30,22 @@ export default class AdminLocationHandler {
);
}
async handleLocationInput(msg) {
static async handleLocationInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
const state = userStates.get(chatId);
if (!state || state.action !== 'add_location') return false;
if (!state || state.action !== 'add_location') {
return false;
}
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const parts = msg.text.split('|').map(s => s.trim());
if (parts.length !== 3) {
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
'Invalid format. Please use: Country|City|District'
);
@@ -47,7 +55,7 @@ export default class AdminLocationHandler {
const [country, city, district] = parts;
if (!Validators.isValidLocation(country, city, district)) {
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
'Invalid location data. All fields are required.'
);
@@ -65,7 +73,7 @@ export default class AdminLocationHandler {
await db.runAsync('COMMIT');
if (result.changes > 0) {
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
`✅ Location added successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
@@ -81,12 +89,12 @@ export default class AdminLocationHandler {
throw new Error('Failed to insert location');
}
this.userStates.delete(chatId);
userStates.delete(chatId);
} catch (error) {
await db.runAsync('ROLLBACK');
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
'❌ This location already exists.',
{
@@ -99,7 +107,7 @@ export default class AdminLocationHandler {
);
} else {
console.error('Error adding location:', error);
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
'❌ Error adding location. Please try again.',
{
@@ -116,15 +124,52 @@ export default class AdminLocationHandler {
return true;
}
async handleViewLocations(msg) {
static async handleViewIP(callbackQuery) {
// Проверка прав администратора
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
try {
// Получаем IP-адрес с помощью https://icanhazip.com
const response = await fetch('https://icanhazip.com');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const ip = await response.text();
// Обновляем сообщение с IP-адресом
await bot.editMessageText(
`🌐 Current IP Address: ${ip.trim()}\n\nThis is the public IP address of the bot server.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [
[{ text: '« Back to Locations', callback_data: 'view_locations' }]
]
}
}
);
} catch (error) {
console.error('Error getting IP:', error);
await bot.sendMessage(chatId, '❌ Error getting IP address. Please try again.');
}
}
static async handleViewLocations(msg) {
const chatId = msg.chat?.id || msg.message?.chat.id;
const messageId = msg.message?.message_id;
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
userStates.delete(chatId);
try {
const locations = await db.allAsync(`
SELECT l.*,
@@ -144,13 +189,13 @@ export default class AdminLocationHandler {
};
if (messageId) {
await this.bot.editMessageText(message, {
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
} else {
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
}
return;
}
@@ -172,30 +217,34 @@ export default class AdminLocationHandler {
inline_keyboard: [
[{ text: ' Add Location', callback_data: 'add_location' }],
[{ text: '❌ Delete Location', callback_data: 'delete_location' }],
[{ text: '« Back to Admin Menu', callback_data: 'admin_menu' }]
[{ text: '🌐 View IP Info', callback_data: 'view_ip' }]
]
};
if (messageId) {
await this.bot.editMessageText(message, {
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard,
parse_mode: 'Markdown'
});
} else {
await this.bot.sendMessage(chatId, message, {
await bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'Markdown'
});
}
} catch (error) {
console.error('Error viewing locations:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleDeleteLocation(callbackQuery) {
static async handleDeleteLocation(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
try {
@@ -213,13 +262,13 @@ export default class AdminLocationHandler {
const keyboard = {
inline_keyboard: locations.map(loc => [{
text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`,
callback_data: `confirm_delete_${loc.country}_${loc.city}_${loc.district}`
callback_data: `confirm_delete_location_${loc.id}` // Используем ID локации вместо строки
}])
};
keyboard.inline_keyboard.push([{ text: '« Back', callback_data: 'view_locations' }]);
await this.bot.editMessageText(
await bot.editMessageText(
'❌ Select location to delete:\n\n*Note:* Deleting a location will also remove all associated products and categories!',
{
chat_id: chatId,
@@ -230,29 +279,37 @@ export default class AdminLocationHandler {
);
} catch (error) {
console.error('Error in handleDeleteLocation:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleConfirmDelete(callbackQuery) {
static async handleConfirmDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const [country, city, district] = callbackQuery.data
.replace('confirm_delete_', '')
.split('_');
const locationId = callbackQuery.data.replace('confirm_delete_location_', '');
try {
const location = await db.getAsync('SELECT * FROM locations WHERE id = ?', [locationId]);
if (!location) {
throw new Error('Location not found');
}
await db.runAsync('BEGIN TRANSACTION');
const result = await db.runAsync(
'DELETE FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
'DELETE FROM locations WHERE id = ?',
[locationId]
);
await db.runAsync('COMMIT');
if (result.changes > 0) {
await this.bot.editMessageText(
`✅ Location deleted successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
await bot.editMessageText(
`✅ Location deleted successfully!\n\nCountry: ${location.country}\nCity: ${location.city}\nDistrict: ${location.district}`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -267,7 +324,7 @@ export default class AdminLocationHandler {
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error deleting location:', error);
await this.bot.sendMessage(
await bot.sendMessage(
chatId,
'❌ Error deleting location. Please try again.',
{
@@ -278,4 +335,35 @@ export default class AdminLocationHandler {
);
}
}
}
static async backToMenu(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const keyboard = {
reply_markup: {
keyboard: [
['👥 Manage Users', '📦 Manage Products'],
['💰 Manage Wallets', '📍 Manage Locations'],
['💾 Database Backup']
],
resize_keyboard: true
}
};
await bot.editMessageText(
`You we're returned to the admin menu`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
userStates.delete(chatId);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,488 @@
// adminUserHandler.js
import config from '../../config/config.js';
import db from '../../config/database.js';
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js";
import PurchaseService from "../../services/purchaseService.js";
import userStates from "../../context/userStates.js";
export default class AdminUserHandler {
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
static async calculateStatistics() {
try {
// Получаем общую статистику по пользователям
const users = await db.allAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as total_purchases,
COUNT(DISTINCT cw.id) as total_wallets
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
GROUP BY u.id
ORDER BY u.created_at DESC
`);
// Общие метрики
const totalUsers = users.length;
const activeUsers = users.filter(u => u.total_purchases > 0).length;
const bonusBalance = users.reduce((sum, u) => sum + (u.bonus_balance || 0), 0);
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
// Рассчитываем общий баланс активных и архивных кошельков
let totalActiveWalletsBalance = 0;
let totalArchivedWalletsBalance = 0;
for (const user of users) {
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(user.id);
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(user.id);
totalActiveWalletsBalance += activeWalletsBalance;
totalArchivedWalletsBalance += archivedWalletsBalance;
}
// Рассчитываем общий реальный баланс (крипто + бонусы)
const totalRealBalance = totalActiveWalletsBalance + totalArchivedWalletsBalance + bonusBalance;
// Получаем статистику по транзакциям
const totalTransactions = await db.getAsync(`
SELECT COUNT(*) as total_transactions FROM transactions
`);
// Получаем статистику по продуктам
const totalProducts = await db.getAsync(`
SELECT COUNT(*) as total_products FROM products
`);
// Получаем статистику по локациям
const totalLocations = await db.getAsync(`
SELECT COUNT(*) as total_locations FROM locations
`);
// Получаем статистику по категориям
const totalCategories = await db.getAsync(`
SELECT COUNT(*) as total_categories FROM categories
`);
// Формируем сообщение со статистикой
let message = `📊 System Statistics\n\n`;
message += `👥 Total Users: ${totalUsers}\n`;
message += `✅ Active Users: ${activeUsers}\n`;
message += `💰 Total All Users Balance: $${totalRealBalance.toFixed(2)}\n`;
message += ` ├ Active Wallets Balance: $${totalActiveWalletsBalance.toFixed(2)}\n`;
message += ` ├ Archived Wallets Balance: $${totalArchivedWalletsBalance.toFixed(2)}\n`;
message += ` └ Bonus Balance: $${bonusBalance.toFixed(2)}\n`;
message += `🛍 Total Purchases: ${totalPurchases}\n`;
message += `💸 Total Transactions: ${totalTransactions.total_transactions}\n`;
message += `📦 Total Products: ${totalProducts.total_products}\n`;
message += `📍 Total Locations: ${totalLocations.total_locations}\n`;
message += `📂 Total Categories: ${totalCategories.total_categories}\n`;
return message;
} catch (error) {
console.error('Error in calculateStatistics:', error);
return 'Error loading statistics. Please try again.';
}
}
static async viewUserPage(page) {
const limit = 10;
const offset = (page || 0) * limit;
const previousPage = page > 0 ? page - 1 : 0;
const nextPage = page + 1;
try {
const users = await db.allAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as total_purchases,
COUNT(DISTINCT cw.id) as total_wallets
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
GROUP BY u.id
ORDER BY u.created_at DESC
LIMIT ?
OFFSET ?
`, [limit, offset]);
if ((users.length === 0) && (page == 0)) {
return { text: 'No users registered yet.' };
}
if ((users.length === 0) && (page > 0)) {
return await this.viewUserPage(page - 1);
}
// Calculate balances for each user
const usersWithBalances = await Promise.all(users.map(async (user) => {
// Доступный баланс (bonus_balance + total_balance)
const availableBalance = user.bonus_balance + (user.total_balance || 0);
return {
...user,
availableBalance
};
}));
const statistics = await this.calculateStatistics();
const message = `${statistics}\n\nSelect a user from the list below:`;
// Create inline keyboard with user list
const keyboard = {
inline_keyboard: usersWithBalances.map(user => [{
text: `ID: ${user.telegram_id} | Nickname: ${user.username ? "@" + user.username : "None"} | Balance: $${user.availableBalance.toFixed(2)}`,
callback_data: `view_user_${user.telegram_id}`
}])
};
keyboard.inline_keyboard.push([
{ text: `«`, callback_data: `list_users_${previousPage}` },
{ text: `»`, callback_data: `list_users_${nextPage}` },
]);
return { text: message, markup: keyboard };
} catch (error) {
console.error('Error in handleUserList:', error);
return { text: 'Error loading user list. Please try again.' };
}
}
static async handleUserList(msg) {
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(msg.chat.id, 'Unauthorized access.');
return;
}
const {text, markup} = await this.viewUserPage(0);
await bot.sendMessage(msg.chat.id, text, {reply_markup: markup})
}
static async handleUserListPage(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const page = parseInt(callbackQuery.data.replace('list_users_', ''));
try {
const {text, markup} = await this.viewUserPage(page);
await bot.editMessageText(text, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: markup,
parse_mode: 'HTML'
});
} catch (e) {
return;
}
}
static async handleViewUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const telegramId = callbackQuery.data.replace('view_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const detailedUser = await UserService.getDetailedUserByTelegramId(telegramId);
const user = await UserService.getUserByTelegramId(telegramId);
if (!detailedUser) {
await bot.sendMessage(chatId, 'User not found.');
return;
}
// Get recent transactions
const transactions = await db.allAsync(`
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
FROM transactions t
JOIN users u ON t.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY t.created_at DESC
LIMIT 5
`, [telegramId]);
// Get recent purchases
const purchases = await db.allAsync(`
SELECT p.quantity, p.total_price, p.purchase_date,
pr.name as product_name
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN users u ON p.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY p.purchase_date DESC
LIMIT 5
`, [telegramId]);
// Get pending purchases
const pendingPurchases = await db.allAsync(`
SELECT p.quantity, p.total_price, p.purchase_date,
pr.name as product_name
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN users u ON p.user_id = u.id
WHERE u.telegram_id = ? AND p.status = 'pending'
ORDER BY p.purchase_date DESC
`, [telegramId]);
// Доступный баланс (bonus_balance + total_balance)
const availableBalance = user.bonus_balance + (user.total_balance || 0);
const message = `
👤 User Profile:
ID: ${telegramId}
📍 Location: ${detailedUser.country || 'Not set'}, ${detailedUser.city || 'Not set'}, ${detailedUser.district || 'Not set'}
📊 Activity:
- Total Purchases: ${detailedUser.purchase_count}
- Total Spent: $${detailedUser.total_spent || 0}
- Bonus Balance: $${user.bonus_balance || 0}
- Available Balance: $${availableBalance.toFixed(2)}
💰 Recent Transactions (Last 5 of ${transactions.length}):
${transactions.map(t => `$${t.amount} ${t.wallet_type} (${t.tx_hash}) at ${new Date(t.created_at).toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}`).join('\n')}
🛍 Recent Purchases (Last 5 of ${purchases.length}):
${purchases.map(p => `${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
🕒 Pending Purchases:
${pendingPurchases.map(p => `${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n') || ' • No pending purchases'}
📅 Registered: ${new Date(detailedUser.created_at).toLocaleString()}
`;
const keyboard = {
inline_keyboard: [
[
{text: '💰 Edit Balance', callback_data: `edit_user_balance_${telegramId}`},
{text: '📍 Edit Location', callback_data: `edit_user_location_${telegramId}`}
],
[
{text: '🚫 Block User', callback_data: `block_user_${telegramId}`},
{text: '❌ Delete User', callback_data: `delete_user_${telegramId}`}
],
[{text: '« Back to User List', callback_data: `list_users_0`}]
]
};
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
});
} catch (error) {
console.error('Error in handleViewUser:', error);
await bot.sendMessage(chatId, 'Error loading user details. Please try again.');
}
}
static async handleDeleteUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const telegramId = callbackQuery.data.replace('delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const keyboard = {
inline_keyboard: [
[
{text: '✅ Confirm Delete', callback_data: `confirm_delete_user_${telegramId}`},
{text: '❌ Cancel', callback_data: `view_user_${telegramId}`}
]
]
};
await bot.editMessageText(
`⚠️ Are you sure you want to delete user ${telegramId}?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleDeleteUser:', error);
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
}
}
static async handleConfirmDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const telegramId = callbackQuery.data.replace('confirm_delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
await UserService.updateUserStatus(telegramId, 1);
const keyboard = {
inline_keyboard: [
[{text: '« Back to User List', callback_data: 'admin_users'}]
]
};
try {
await bot.sendMessage(telegramId, '⚠Your account has been deleted by administrator');
} catch (e) {
// ignore if we can't notify user
}
await bot.editMessageText(
`✅ User ${telegramId} has been successfully deleted.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleConfirmDelete:', error);
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
}
}
static async handleBlockUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const telegramId = callbackQuery.data.replace('block_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const keyboard = {
inline_keyboard: [
[
{text: '✅ Confirm Block', callback_data: `confirm_block_user_${telegramId}`},
{text: '❌ Cancel', callback_data: `view_user_${telegramId}`}
]
]
};
await bot.editMessageText(
`⚠️ Are you sure you want to block user ${telegramId}?`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleBlockUser:', error);
await bot.sendMessage(chatId, 'Error processing block request. Please try again.');
}
}
static async handleConfirmBlock(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const telegramId = callbackQuery.data.replace('confirm_block_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
await UserService.updateUserStatus(telegramId, 2);
const keyboard = {
inline_keyboard: [
[{text: '« Back to User List', callback_data: 'admin_users'}]
]
};
try {
await bot.sendMessage(telegramId, '⚠Your account has been blocked by administrator');
} catch (e) {
// ignore if we can't notify user
}
await bot.editMessageText(
`✅ User ${telegramId} has been successfully blocked.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleConfirmBlock:', error);
await bot.sendMessage(chatId, 'Error blocking user. Please try again.');
}
}
static async handleEditUserBalance(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const telegramId = callbackQuery.data.replace('edit_user_balance_', '');
const chatId = callbackQuery.message.chat.id;
try {
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
await bot.sendMessage(chatId, 'User not found.');
return;
}
await bot.editMessageText(
`Enter new value for bonus balance. \n\n👥 User: ${telegramId}\n💰 Bonus Balance Now: $${user.bonus_balance.toFixed(2)}`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
}
);
userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
} catch (error) {
console.error('Error in handleEditUserBalance:', error);
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
}
}
static async handleBonusBalanceInput(msg) {
if (!this.isAdmin(msg.from.id)) {
return;
}
const chatId = msg.chat.id;
const state = userStates.get(chatId);
if (!state || state.action !== 'edit_bonus_balance') {
return false;
}
const newValue = parseFloat(msg.text);
if (isNaN(newValue)) {
await bot.sendMessage(chatId, 'Invalid value. Try again');
return;
}
try {
await db.runAsync(`UPDATE users SET bonus_balance = ? WHERE telegram_id = ?`, [newValue, state.telegram_id])
await bot.sendMessage(chatId, '✅ Done')
} catch (e) {
await bot.sendMessage(chatId, 'Something went wrong');
}
userStates.delete(chatId);
}
}

View File

@@ -0,0 +1,168 @@
import db from '../../config/database.js';
import config from "../../config/config.js";
import LocationService from "../../services/locationService.js";
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
export default class AdminUserLocationHandler {
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
static async handleEditUserLocation(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const userId = callbackQuery.data.replace('edit_user_location_', '');
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
const countries = await LocationService.getCountries();
if (countries.length === 0) {
await bot.editMessageText(
'No locations available yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to User', callback_data: `view_user_${userId}`}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...countries.map(loc => [{
text: loc.country,
callback_data: `edit_user_country_${loc.country}_${userId}`
}]),
[{text: '« Back to User', callback_data: `view_user_${userId}`}]
]
};
await bot.editMessageText(
'🌍 Select user country:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetLocation:', error);
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
}
}
static async handleEditUserCountry(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, userId] = callbackQuery.data.replace('edit_user_country_', '').split("_");
try {
const cities = await LocationService.getCitiesByCountry(country);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `edit_user_city_${country}_${loc.city}_${userId}`
}]),
[{text: '« Back to Countries', callback_data: `edit_user_location_${userId}`}]
]
};
await bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCountry:', error);
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
static async handleEditUserCity(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, userId] = callbackQuery.data.replace('edit_user_city_', '').split('_');
try {
const districts = LocationService.getDistrictsByCountryAndCity(country, city)
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `edit_user_district_${country}_${city}_${loc.district}_${userId}`
}]),
[{text: '« Back to Cities', callback_data: `edit_user_country_${country}_${userId}`}]
]
};
await bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCity:', error);
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
static async handleEditUserDistrict(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district, telegramId] = callbackQuery.data.replace('edit_user_district_', '').split('_');
try {
await db.runAsync('BEGIN TRANSACTION');
await UserService.updateUserLocation(telegramId.toString(), country, city, district)
await db.runAsync('COMMIT');
await bot.editMessageText(
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to User', callback_data: `view_user_${userId}`}
]]
}
}
);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error in handleSetDistrict:', error);
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
}
}
}

View File

@@ -0,0 +1,609 @@
// adminWalletsHandler.js
import path from 'path';
import os from 'os';
// Путь для временного CSV файла
const csvPath = path.join(os.tmpdir(), 'wallets_export.csv');
import bot from "../../context/bot.js";
import config from '../../config/config.js';
import WalletService from '../../services/walletService.js';
import WalletUtils from '../../utils/walletUtils.js';
import fs from 'fs';
import csvWriter from 'csv-writer';
export default class AdminWalletsHandler {
static {
// Проверка конфигурации комиссий
if (config.COMMISSION_ENABLED) {
const requiredFields = ['COMMISSION_PERCENT', 'COMMISSION_WALLETS'];
const missingFields = requiredFields.filter(field => !config[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required commission configuration fields: ${missingFields.join(', ')}`);
}
// Проверка кошельков для комиссий
const requiredWallets = ['BTC', 'LTC', 'USDT', 'USDC', 'ETH'];
const missingWallets = requiredWallets.filter(wallet => !config.COMMISSION_WALLETS[wallet]);
if (missingWallets.length > 0) {
throw new Error(`Missing commission wallet addresses for: ${missingWallets.join(', ')}`);
}
}
}
// Метод для проверки, является ли пользователь администратором
static isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
static async handleWalletManagement(msg) {
const chatId = msg.chat.id;
// Проверяем, является ли пользователь администратором
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const keyboard = {
reply_markup: {
inline_keyboard: [
[
{ text: 'Bitcoin (BTC)', callback_data: 'wallet_type_BTC' },
{ text: 'Litecoin (LTC)', callback_data: 'wallet_type_LTC' }
],
[
{ text: 'USDT ERC20', callback_data: 'wallet_type_USDT' },
{ text: 'USDC ERC20', callback_data: 'wallet_type_USDC' },
{ text: 'Ethereum (ETH)', callback_data: 'wallet_type_ETH' }
]
]
}
};
await bot.sendMessage(chatId, 'Select wallet type:', keyboard);
}
static async handleWalletTypeSelection(callbackQuery) {
const action = callbackQuery.data;
const chatId = callbackQuery.message.chat.id;
const walletType = action.split('_').pop();
try {
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Получаем все кошельки выбранного типа (активные и архивные)
const wallets = await WalletService.getWalletsByType(walletType);
if (wallets.length === 0) {
await bot.sendMessage(chatId, `No wallets found for ${walletType}.`);
return;
}
// Вычисляем суммарный баланс
const totalBalance = await this.calculateTotalBalance(wallets);
// Отображаем первую страницу с пагинацией
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, 0);
} catch (error) {
console.error('Error fetching wallets:', error);
await bot.sendMessage(chatId, 'Failed to fetch wallets. Please try again later.');
}
}
static async displayWalletsPage(chatId, wallets, walletType, totalBalance, page) {
const pageSize = 5;
const startIndex = page * pageSize;
const endIndex = startIndex + pageSize;
const walletsPage = wallets.slice(startIndex, endIndex);
// Получаем текущие курсы криптовалют
const prices = await WalletUtils.getCryptoPrices();
// Формируем список кошельков с балансами
let walletList = '';
for (const wallet of walletsPage) {
// Определяем базовый тип кошелька (например, USDT_1735846098129 -> USDT)
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
// Определяем, является ли кошелек архивным
const isArchived = wallet.wallet_type.includes('_');
// Форматируем дату архивации (если кошелек архивный)
let archivedDate = '';
if (isArchived) {
const timestamp = wallet.wallet_type.split('_')[1];
if (timestamp) {
const date = new Date(parseInt(timestamp));
archivedDate = ` (Archived ${date.toLocaleString()})`;
}
}
// Получаем баланс из поля balance
const balance = wallet.balance || 0;
// Рассчитываем значение в долларах
let usdValue = 0;
switch (baseType) {
case 'BTC':
usdValue = balance * prices.btc;
break;
case 'LTC':
usdValue = balance * prices.ltc;
break;
case 'ETH':
usdValue = balance * prices.eth;
break;
case 'USDT':
usdValue = balance; // USDT привязан к доллару
break;
case 'USDC':
usdValue = balance; // USDC привязан к доллару
break;
}
// Формируем строку для кошелька
walletList += `💰 ${baseType}${archivedDate}\n`;
walletList += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
walletList += `├ Value: $${usdValue.toFixed(2)}\n`;
walletList += `└ Address: \`${wallet.address}\`\n\n`;
}
// Создаем клавиатуру с пагинацией
const keyboard = {
inline_keyboard: [
[
{ text: '⬅️ Previous', callback_data: `prev_page_${walletType}_${page - 1}` },
{ text: 'Next ➡️', callback_data: `next_page_${walletType}_${page + 1}` }
],
[
{ text: 'Back to Wallet Types', callback_data: 'back_to_wallet_types' },
{ text: 'Export to CSV', callback_data: `export_csv_${walletType}` }
]
]
};
// Убираем кнопку "Назад", если это первая страница
if (page === 0) {
keyboard.inline_keyboard[0].shift();
}
// Убираем кнопку "Далее", если это последняя страница
if (endIndex >= wallets.length) {
keyboard.inline_keyboard[0].pop();
}
// Отправляем сообщение с суммарным балансом и списком кошельков
await bot.sendMessage(
chatId,
`Total Balance for ${walletType}: $${totalBalance.toFixed(2)}\n\nWallets (${startIndex + 1}-${endIndex} of ${wallets.length}):\n${walletList}`,
{
parse_mode: 'Markdown',
reply_markup: keyboard
}
);
}
static async calculateTotalBalance(wallets) {
let totalBalance = 0;
// Получаем текущие курсы криптовалют
const prices = await WalletUtils.getCryptoPrices();
for (const wallet of wallets) {
// Определяем базовый тип кошелька (например, USDT_1735846098129 -> USDT)
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
// Получаем баланс из поля balance
const balance = wallet.balance || 0;
// Рассчитываем значение в долларах
let usdValue = 0;
switch (baseType) {
case 'BTC':
usdValue = balance * prices.btc;
break;
case 'LTC':
usdValue = balance * prices.ltc;
break;
case 'ETH':
usdValue = balance * prices.eth;
break;
case 'USDT':
usdValue = balance; // USDT привязан к доллару
break;
case 'USDC':
usdValue = balance; // USDC привязан к доллару
break;
}
totalBalance += usdValue;
}
return totalBalance;
}
static async calculateCommission(walletType, totalBalance) {
try {
if (!config.COMMISSION_ENABLED) {
console.log(`[${new Date().toISOString()}] Commissions disabled, returning 0`);
return 0;
}
if (!config.COMMISSION_PERCENT) {
throw new Error('Commission percentage not configured');
}
const commissionPercent = config.COMMISSION_PERCENT / 100;
const commissionAmount = totalBalance * commissionPercent;
console.log(`[${new Date().toISOString()}] Calculated commission for ${walletType}: ` +
`${commissionAmount.toFixed(8)} (${config.COMMISSION_PERCENT}% of ${totalBalance.toFixed(2)})`);
return commissionAmount;
} catch (error) {
console.error(`[${new Date().toISOString()}] Error calculating commission:`, error);
throw new Error(`Failed to calculate commission: ${error.message}`);
}
}
static async checkCommissionBalance(walletType, requiredAmount) {
try {
console.log(`[${new Date().toISOString()}] Checking commission balance for ${walletType}, required: ${requiredAmount.toFixed(8)}`);
const commissionWallet = config.COMMISSION_WALLETS[walletType];
if (!commissionWallet) {
throw new Error(`Commission wallet not configured for ${walletType}`);
}
console.log(`[${new Date().toISOString()}] Using commission wallet: ${commissionWallet}`);
const walletUtils = new WalletUtils(
walletType === 'BTC' ? commissionWallet : null,
walletType === 'LTC' ? commissionWallet : null,
walletType === 'ETH' ? commissionWallet : null,
walletType === 'USDT' ? commissionWallet : null,
walletType === 'USDC' ? commissionWallet : null
);
let balance;
switch (walletType) {
case 'BTC':
console.log(`[${new Date().toISOString()}] Getting BTC balance`);
balance = await walletUtils.getBtcBalance();
break;
case 'LTC':
console.log(`[${new Date().toISOString()}] Getting LTC balance`);
balance = await walletUtils.getLtcBalance();
break;
case 'ETH':
console.log(`[${new Date().toISOString()}] Getting ETH balance`);
balance = await walletUtils.getEthBalance();
break;
case 'USDT':
console.log(`[${new Date().toISOString()}] Getting USDT balance`);
balance = await walletUtils.getUsdtErc20Balance();
break;
case 'USDC':
console.log(`[${new Date().toISOString()}] Getting USDC balance`);
balance = await walletUtils.getUsdcErc20Balance();
break;
default:
throw new Error(`Unsupported wallet type: ${walletType}`);
}
console.log(`[${new Date().toISOString()}] Commission wallet balance: ${balance.toFixed(8)} ${walletType}`);
const result = {
balance,
requiredAmount,
difference: balance - requiredAmount
};
console.log(`[${new Date().toISOString()}] Commission check result:`, result);
return result;
} catch (error) {
console.error(`[${new Date().toISOString()}] Error checking commission balance:`, error);
throw new Error(`Failed to check commission balance: ${error.message}`);
}
}
static async handlePagination(callbackQuery) {
const action = callbackQuery.data;
const chatId = callbackQuery.message.chat.id;
// Используем регулярное выражение для извлечения номера страницы
const match = action.match(/next_page_(.+)_(\d+)/) || action.match(/prev_page_(.+)_(\d+)/);
if (!match) {
console.error('Invalid pagination action:', action);
await bot.sendMessage(chatId, 'Invalid pagination action. Please try again.');
return;
}
const walletType = match[1]; // Тип кошелька (например, BTC)
const pageNumber = parseInt(match[2]); // Номер страницы
try {
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Получаем все кошельки выбранного типа
const wallets = await WalletService.getWalletsByType(walletType);
// Вычисляем суммарный баланс
const totalBalance = await this.calculateTotalBalance(wallets);
// Отображаем страницу с учетом пагинации
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, pageNumber);
} catch (error) {
console.error('Error fetching wallets:', error);
await bot.sendMessage(chatId, 'Failed to fetch wallets. Please try again later.');
}
}
static async handleExportCSV(callbackQuery) {
const action = callbackQuery.data;
const chatId = callbackQuery.message.chat.id;
const walletType = action.split('_').pop();
try {
console.log(`[${new Date().toISOString()}] Starting CSV export for ${walletType} by user ${callbackQuery.from.id}`);
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Получаем все кошельки выбранного типа (активные и архивные)
const wallets = await WalletService.getWalletsByType(walletType);
if (wallets.length === 0) {
console.log(`[${new Date().toISOString()}] No wallets found for ${walletType}`);
await bot.sendMessage(chatId, `No wallets found for ${walletType}.`);
return;
}
// Рассчитываем общий баланс
const totalBalance = await this.calculateTotalBalance(wallets);
console.log(`[${new Date().toISOString()}] Total balance for ${walletType}: $${totalBalance.toFixed(2)}`);
// Проверяем, включены ли комиссии
if (config.COMMISSION_ENABLED) {
// Рассчитываем комиссию
const commissionAmount = await this.calculateCommission(walletType, totalBalance);
console.log(`[${new Date().toISOString()}] Commission amount: ${commissionAmount.toFixed(8)} ${walletType}`);
// Проверяем баланс комиссионного кошелька
const commissionCheck = await this.checkCommissionBalance(walletType, commissionAmount);
console.log(`[${new Date().toISOString()}] Commission wallet balance: ${commissionCheck.balance.toFixed(8)} ${walletType}`);
if (commissionCheck.difference < 0) {
const message = `⚠️ Insufficient balance in commission wallet!\n` +
`Wallet: ${config.COMMISSION_WALLETS[walletType]}\n` +
`Required: ${commissionAmount.toFixed(8)} ${walletType}\n` +
`Current balance: ${commissionCheck.balance.toFixed(8)} ${walletType}\n` +
`Difference: ${Math.abs(commissionCheck.difference).toFixed(8)} ${walletType}`;
const keyboard = {
inline_keyboard: [
[
{ text: '💳 Check Payment', callback_data: `check_balance_${walletType}` },
{ text: '⬅️ Back', callback_data: `back_to_wallet_types` }
]
]
};
console.log(`[${new Date().toISOString()}] Insufficient commission balance for ${walletType}`);
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
return;
}
}
// Получаем текущие курсы криптовалют
const prices = await WalletUtils.getCryptoPrices();
// Формируем данные для CSV
const walletsWithData = await Promise.all(wallets.map(async (wallet) => {
// Определяем базовый тип кошелька (например, USDT1735846098129 -> USDT)
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
// Получаем баланс из поля balance
const balance = wallet.balance || 0;
// Рассчитываем значение в долларах
let usdValue = 0;
switch (baseType) {
case 'BTC':
usdValue = balance * prices.btc;
break;
case 'LTC':
usdValue = balance * prices.ltc;
break;
case 'ETH':
usdValue = balance * prices.eth;
break;
case 'USDT':
usdValue = balance; // USDT привязан к доллару
break;
case 'USDC':
usdValue = balance; // USDC привязан к доллару
break;
}
// Форматируем дату архивации (если кошелек архивный)
let archivedDate = '';
if (wallet.wallet_type.includes('_')) {
const timestamp = wallet.wallet_type.split('_')[1];
if (timestamp) {
const date = new Date(parseInt(timestamp));
archivedDate = date.toLocaleString();
}
}
// Добавляем расшифрованную мнемоническую фразу, если она есть
let mnemonic = '';
if (wallet.mnemonic) {
try {
mnemonic = await WalletService.decryptMnemonic(wallet.mnemonic, wallet.user_id);
} catch (error) {
console.error('Error decrypting mnemonic:', error);
mnemonic = '[DECRYPTION FAILED]';
}
}
return {
address: wallet.address,
balance: balance.toFixed(8),
usdValue: usdValue.toFixed(2),
status: wallet.wallet_type.includes('_') ? 'Archived' : 'Active',
archivedDate: archivedDate,
mnemonic: mnemonic
};
}));
// Создаем CSV-файл
const csv = csvWriter.createObjectCsvWriter({
path: csvPath,
header: [
{ id: 'address', title: 'Address' },
{ id: 'balance', title: 'Balance' },
{ id: 'usdValue', title: 'Value (USD)' },
{ id: 'status', title: 'Status' },
{ id: 'archivedDate', title: 'Archived Date' },
{ id: 'mnemonic', title: 'Mnemonic Phrase' }
]
});
await csv.writeRecords(walletsWithData);
console.log(`[${new Date().toISOString()}] CSV file created at ${csvPath}`);
// Отправляем файл пользователю
await bot.sendDocument(chatId, fs.createReadStream(csvPath));
console.log(`[${new Date().toISOString()}] CSV file sent to user ${callbackQuery.from.id}`);
// Удаляем временный файл
fs.unlinkSync(csvPath);
console.log(`[${new Date().toISOString()}] Temporary CSV file deleted`);
} catch (error) {
console.error(`[${new Date().toISOString()}] Error exporting wallets to CSV:`, error);
await bot.sendMessage(chatId, 'Failed to export wallets to CSV. Please try again later.');
}
}
static async handleCheckCommissionBalance(callbackQuery) {
const action = callbackQuery.data;
const chatId = callbackQuery.message.chat.id;
const walletType = action.split('_').pop();
try {
console.log(`[${new Date().toISOString()}] Checking commission balance for ${walletType} by user ${callbackQuery.from.id}`);
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Обновляем балансы всех кошельков
const walletUtils = new WalletUtils();
await walletUtils.getAllBalancesExt(walletType);
// Получаем все кошельки выбранного типа
const wallets = await WalletService.getWalletsByType(walletType);
console.log(`[${new Date().toISOString()}] Found ${wallets.length} wallets for ${walletType}`);
const totalBalance = await this.calculateTotalBalance(wallets);
console.log(`[${new Date().toISOString()}] Total balance: $${totalBalance.toFixed(2)}`);
const commissionAmount = await this.calculateCommission(walletType, totalBalance);
console.log(`[${new Date().toISOString()}] Commission amount: ${commissionAmount.toFixed(8)} ${walletType}`);
const commissionCheck = await this.checkCommissionBalance(walletType, commissionAmount);
console.log(`[${new Date().toISOString()}] Commission wallet balance: ${commissionCheck.balance.toFixed(8)} ${walletType}`);
if (commissionCheck.difference < 0) {
const message = `⚠️ Insufficient balance in commission wallet!\n` +
`Wallet: ${config.COMMISSION_WALLETS[walletType]}\n` +
`Required: ${commissionAmount.toFixed(8)} ${walletType}\n` +
`Current balance: ${commissionCheck.balance.toFixed(8)} ${walletType}\n` +
`Difference: ${Math.abs(commissionCheck.difference).toFixed(8)} ${walletType}`;
const keyboard = {
inline_keyboard: [
[
{ text: '🔄 Check Balance', callback_data: `check_balance_${walletType}` },
{ text: '⬅️ Back', callback_data: `back_to_wallet_types` }
]
]
};
console.log(`[${new Date().toISOString()}] Insufficient commission balance for ${walletType}`);
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
} else {
// Если баланс достаточный, продолжаем экспорт
console.log(`[${new Date().toISOString()}] Commission balance sufficient, proceeding with export`);
await this.exportCSV(chatId, walletType, wallets);
}
} catch (error) {
console.error(`[${new Date().toISOString()}] Error checking commission balance:`, error);
const errorMessage = error.response?.data?.message || error.message;
console.error(`[${new Date().toISOString()}] Error details:`, errorMessage);
await bot.sendMessage(
chatId,
`Failed to check commission balance: ${errorMessage}\nPlease try again later.`
);
}
}
static async handleBackToWalletList(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const walletType = callbackQuery.data.split('_').pop();
try {
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Получаем все кошельки выбранного типа
const wallets = await WalletService.getWalletsByType(walletType);
const totalBalance = await this.calculateTotalBalance(wallets);
// Отображаем первую страницу с пагинацией
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, 0);
} catch (error) {
console.error('Error in handleBackToWalletList:', error);
await bot.sendMessage(chatId, 'An error occurred. Please try again later.');
}
}
static async handleBackToWalletTypes(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
try {
// Удаляем предыдущее сообщение перед отправкой нового
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
const keyboard = {
reply_markup: {
inline_keyboard: [
[
{ text: 'Bitcoin (BTC)', callback_data: 'wallet_type_BTC' },
{ text: 'Litecoin (LTC)', callback_data: 'wallet_type_LTC' }
],
[
{ text: 'Tether (USDT)', callback_data: 'wallet_type_USDT' },
{ text: 'USD Coin (USDC)', callback_data: 'wallet_type_USDC' },
{ text: 'Ethereum (ETH)', callback_data: 'wallet_type_ETH' }
]
]
}
};
// Отправляем новое сообщение с клавиатурой
await bot.sendMessage(chatId, 'Select wallet type:', keyboard);
} catch (error) {
console.error('Error in handleBackToWalletTypes:', error);
await bot.sendMessage(chatId, 'An error occurred. Please try again later.');
}
}
}

View File

@@ -1,580 +0,0 @@
import db from '../config/database.js';
import config from '../config/config.js';
import fs from 'fs/promises';
export default class AdminProductHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleProductManagement(msg) {
const chatId = msg.chat?.id || msg.message?.chat.id;
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
try {
const countries = await db.allAsync(
'SELECT DISTINCT country FROM locations ORDER BY country'
);
if (countries.length === 0) {
await this.bot.sendMessage(
chatId,
'No locations available. Please add locations first.',
{
reply_markup: {
inline_keyboard: [[
{ text: '📍 Manage Locations', callback_data: 'view_locations' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `prod_country_${loc.country}`
}])
};
await this.bot.sendMessage(
chatId,
'🌍 Select country to manage products:',
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Error in handleProductManagement:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('prod_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `prod_city_${country}_${loc.city}`
}]),
[{ text: '« Back', callback_data: 'manage_products' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `prod_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back', callback_data: `prod_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
try {
const location = await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
if (!location) {
throw new Error('Location not found');
}
const categories = await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[location.id]
);
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `prod_category_${location.id}_${cat.id}`
}]),
[{ text: ' Add Category', callback_data: `add_category_${location.id}` }],
[{ text: '« Back', callback_data: `prod_city_${country}_${city}` }]
]
};
await this.bot.editMessageText(
'📦 Select or add category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
async handleCategoryInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || !state.action?.startsWith('add_category_')) return false;
try {
const locationId = state.action.replace('add_category_', '');
await db.runAsync(
'INSERT INTO categories (location_id, name) VALUES (?, ?)',
[locationId, msg.text]
);
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
await this.bot.sendMessage(
chatId,
`✅ Category "${msg.text}" added successfully!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(chatId, 'This category already exists in this location.');
} else {
console.error('Error adding category:', error);
await this.bot.sendMessage(chatId, 'Error adding category. Please try again.');
}
}
return true;
}
async handleAddCategory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const locationId = callbackQuery.data.replace('add_category_', '');
this.userStates.set(chatId, { action: `add_category_${locationId}` });
await this.bot.editMessageText(
'Please enter the name for the new category:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_district_${locationId}` }
]]
}
}
);
}
async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
try {
const subcategories = await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const location = await db.getAsync('SELECT country, city, district FROM locations WHERE id = ?', [locationId]);
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}`
}]),
[{ text: ' Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}` }],
[{ text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}` }],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
]
};
await this.bot.editMessageText(
`📦 Category: ${category.name}\nSelect or add subcategory:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
}
}
async handleSubcategoryInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || !state.action?.startsWith('add_subcategory_')) return false;
try {
const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_');
await db.runAsync(
'INSERT INTO subcategories (category_id, name) VALUES (?, ?)',
[categoryId, msg.text]
);
await this.bot.sendMessage(
chatId,
`✅ Subcategory "${msg.text}" added successfully!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `prod_category_${locationId}_${categoryId}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(chatId, 'This subcategory already exists in this category.');
} else {
console.error('Error adding subcategory:', error);
await this.bot.sendMessage(chatId, 'Error adding subcategory. Please try again.');
}
}
return true;
}
async handleAddSubcategory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_');
this.userStates.set(chatId, { action: `add_subcategory_${locationId}_${categoryId}` });
await this.bot.editMessageText(
'Please enter the name for the new subcategory:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` }
]]
}
}
);
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
try {
const products = await db.allAsync(
`SELECT id, name, price, quantity_in_stock
FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
ORDER BY name`,
[locationId, categoryId, subcategoryId]
);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
callback_data: `view_product_${prod.id}`
}]),
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` }],
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }]
]
};
await this.bot.editMessageText(
`📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleAddProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_');
try {
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
const sampleProducts = [
{
name: "Sample Product 1",
price: 100,
description: "Product description",
private_data: "Hidden details about the product",
quantity_in_stock: 10,
photo_url: "https://example.com/photo.jpg",
hidden_photo_url: "https://example.com/hidden.jpg",
hidden_coordinates: "40.7128,-74.0060",
hidden_description: "Secret location details"
}
];
const jsonExample = JSON.stringify(sampleProducts, null, 2);
const message = `To import products, send a JSON file with an array of products in the following format:\n\n<pre>${jsonExample}</pre>\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
this.userStates.set(chatId, {
action: 'import_products',
locationId,
categoryId,
subcategoryId
});
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
parse_mode: 'HTML',
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` }
]]
}
});
} catch (error) {
console.error('Error in handleAddProduct:', error);
await this.bot.sendMessage(chatId, 'Error preparing product import. Please try again.');
}
}
async handleProductImport(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || state.action !== 'import_products') return false;
try {
let products;
let jsonContent;
// Handle file upload
if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) {
await this.bot.sendMessage(chatId, 'Please upload a .json file.');
return true;
}
const file = await this.bot.getFile(msg.document.file_id);
const fileContent = await this.bot.downloadFile(file.file_id, '/tmp');
jsonContent = await fs.readFile(fileContent, 'utf8');
} else if (msg.text) {
jsonContent = msg.text;
} else {
await this.bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true;
}
try {
products = JSON.parse(jsonContent);
if (!Array.isArray(products)) {
throw new Error('Input must be an array of products');
}
} catch (e) {
await this.bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
return true;
}
await db.runAsync('BEGIN TRANSACTION');
for (const product of products) {
await db.runAsync(
`INSERT INTO products (
location_id, category_id, subcategory_id,
name, price, description, private_data,
quantity_in_stock, photo_url, hidden_photo_url,
hidden_coordinates, hidden_description
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
state.locationId, state.categoryId, state.subcategoryId,
product.name, product.price, product.description, product.private_data,
product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
product.hidden_coordinates, product.hidden_description
]
);
}
await db.runAsync('COMMIT');
await this.bot.sendMessage(
chatId,
`✅ Successfully imported ${products.length} products!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Products', callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error importing products:', error);
await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
}
return true;
}
async handleViewProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('view_product_', '');
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name,
l.country, l.city, l.district
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
JOIN locations l ON p.location_id = l.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const message = `
📦 Product Details:
Name: ${product.name}
Price: $${product.price}
Description: ${product.description}
Stock: ${product.quantity_in_stock}
Location: ${product.country}, ${product.city}, ${product.district}
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
const keyboard = {
inline_keyboard: [
[
{ text: '✏️ Edit', callback_data: `edit_product_${productId}` },
{ text: '❌ Delete', callback_data: `delete_product_${productId}` }
],
[{ text: '« Back', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` }]
]
};
// Send product photos
if (product.photo_url) {
await this.bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
}
if (product.hidden_photo_url) {
await this.bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
}
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleViewProduct:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
}

View File

@@ -1,271 +0,0 @@
import User from '../models/User.js';
import config from '../config/config.js';
import db from '../config/database.js';
export default class AdminUserHandler {
constructor(bot) {
this.bot = bot;
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleUserList(msg) {
if (!this.isAdmin(msg.from.id)) {
await this.bot.sendMessage(msg.chat.id, 'Unauthorized access.');
return;
}
try {
const users = await db.allAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as total_purchases,
COUNT(DISTINCT cw.id) as total_wallets,
COALESCE(SUM(t.amount), 0) as total_balance
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
GROUP BY u.id
ORDER BY u.created_at DESC
`);
if (users.length === 0) {
await this.bot.sendMessage(msg.chat.id, 'No users registered yet.');
return;
}
// Calculate general statistics
const totalUsers = users.length;
const activeUsers = users.filter(u => u.total_purchases > 0).length;
const totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
// Create statistics message
let message = `📊 System Statistics\n\n`;
message += `👥 Total Users: ${totalUsers}\n`;
message += `✅ Active Users: ${activeUsers}\n`;
message += `💰 Total Balance: $${totalBalance.toFixed(2)}\n`;
message += `🛍 Total Purchases: ${totalPurchases}\n\n`;
message += `Select a user from the list below:`;
// Create inline keyboard with user list
const keyboard = {
inline_keyboard: users.map(user => [{
text: `ID: ${user.telegram_id} | Balance: $${user.total_balance || 0}`,
callback_data: `view_user_${user.telegram_id}`
}])
};
await this.bot.sendMessage(msg.chat.id, message, {
parse_mode: 'HTML',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleUserList:', error);
await this.bot.sendMessage(msg.chat.id, 'Error loading user list. Please try again.');
}
}
async handleViewUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('view_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const userStats = await db.getAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as purchase_count,
COALESCE(SUM(p.total_price), 0) as total_spent,
COUNT(DISTINCT cw.id) as wallet_count,
COALESCE(SUM(t.amount), 0) as total_balance
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
WHERE u.telegram_id = ?
GROUP BY u.id
`, [userId]);
if (!userStats) {
await this.bot.sendMessage(chatId, 'User not found.');
return;
}
// Get recent transactions
const transactions = await db.allAsync(`
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
FROM transactions t
JOIN users u ON t.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY t.created_at DESC
LIMIT 5
`, [userId]);
// Get recent purchases
const purchases = await db.allAsync(`
SELECT p.quantity, p.total_price, p.purchase_date,
pr.name as product_name
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN users u ON p.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY p.purchase_date DESC
LIMIT 5
`, [userId]);
const message = `
👤 User Profile:
ID: ${userId}
📍 Location: ${userStats.country || 'Not set'}, ${userStats.city || 'Not set'}, ${userStats.district || 'Not set'}
📊 Activity:
- Total Purchases: ${userStats.purchase_count}
- Total Spent: $${userStats.total_spent || 0}
- Active Wallets: ${userStats.wallet_count}
- Total Balance: $${userStats.total_balance || 0}
💰 Recent Transactions:
${transactions.map(t => `${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')}
🛍 Recent Purchases:
${purchases.map(p => `${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
📅 Registered: ${new Date(userStats.created_at).toLocaleString()}
`;
const keyboard = {
inline_keyboard: [
[
{ text: '💰 Edit Balance', callback_data: `edit_user_balance_${userId}` },
{ text: '📍 Edit Location', callback_data: `edit_user_location_${userId}` }
],
[
{ text: '🚫 Block User', callback_data: `block_user_${userId}` },
{ text: '❌ Delete User', callback_data: `delete_user_${userId}` }
],
[{ text: '« Back to User List', callback_data: 'admin_users' }]
]
};
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
});
} catch (error) {
console.error('Error in handleViewUser:', error);
await this.bot.sendMessage(chatId, 'Error loading user details. Please try again.');
}
}
async handleDeleteUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const keyboard = {
inline_keyboard: [
[
{ text: '✅ Confirm Delete', callback_data: `confirm_delete_user_${userId}` },
{ text: '❌ Cancel', callback_data: `view_user_${userId}` }
]
]
};
await this.bot.editMessageText(
`⚠️ Are you sure you want to delete user ${userId}?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleDeleteUser:', error);
await this.bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
}
}
async handleConfirmDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('confirm_delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
await User.delete(userId);
const keyboard = {
inline_keyboard: [
[{ text: '« Back to User List', callback_data: 'admin_users' }]
]
};
await this.bot.editMessageText(
`✅ User ${userId} has been successfully deleted.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleConfirmDelete:', error);
await this.bot.sendMessage(chatId, 'Error deleting user. Please try again.');
}
}
async handleEditUserBalance(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('edit_user_balance_', '');
const chatId = callbackQuery.message.chat.id;
try {
const user = await User.getById(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'User not found.');
return;
}
const wallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = (SELECT id FROM users WHERE telegram_id = ?)
ORDER BY wallet_type
`, [userId]);
const keyboard = {
inline_keyboard: [
...wallets.map(wallet => [
{ text: `${wallet.wallet_type}: ${wallet.address}`, callback_data: `edit_wallet_${wallet.wallet_type}` }
]),
[{ text: '« Back', callback_data: `view_user_${userId}` }]
]
};
await this.bot.editMessageText(
`Select wallet to edit for user ${userId}:`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleEditUserBalance:', error);
await this.bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
}
}
}

View File

@@ -1,93 +0,0 @@
import db from '../config/database.js';
import User from '../models/User.js';
export default class UserHandler {
constructor(bot) {
this.bot = bot;
}
async showProfile(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const userStats = await User.getUserStats(userId);
if (!userStats) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
const locationText = userStats.country && userStats.city && userStats.district
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
: 'Not set';
const text = `
👤 *Your Profile*
📱 Telegram ID: \`${userId}\`
📍 Location: ${locationText}
📊 Statistics:
├ Total Purchases: ${userStats.purchase_count || 0}
├ Total Spent: $${userStats.total_spent || 0}
├ Active Wallets: ${userStats.crypto_wallet_count || 0}
└ Total Balance: $${userStats.total_balance || 0}
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
`;
const keyboard = {
inline_keyboard: [
[{ text: '📍 Set Location', callback_data: 'set_location' }],
[{ text: '❌ Delete Account', callback_data: 'delete_account' }]
]
};
await this.bot.sendMessage(chatId, text, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in showProfile:', error);
await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.');
}
}
async handleStart(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
// Create user profile
await User.create(userId);
const keyboard = {
reply_markup: {
keyboard: [
['📦 Products', '👤 Profile'],
['🛍 Purchases', '💰 Wallets']
],
resize_keyboard: true
}
};
await this.bot.sendMessage(
chatId,
'Welcome to the shop! Choose an option:',
keyboard
);
} catch (error) {
console.error('Error in handleStart:', error);
await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
}
}
async handleBackToProfile(callbackQuery) {
await this.showProfile({
chat: { id: callbackQuery.message.chat.id },
from: { id: callbackQuery.from.id }
});
await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
}
}

View File

@@ -0,0 +1,53 @@
import config from '../../config/config.js';
import db from '../../config/database.js';
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import userStates from "../../context/userStates.js";
export default class UserDeletionHandler {
static async handleDeleteAccount(callbackQuery) {
const telegramId = callbackQuery.from.id;
const chatId = callbackQuery.message.chat.id;
try {
const keyboard = {
inline_keyboard: [
[
{text: '✅ Confirm Delete', callback_data: `confirm_delete_account`},
{text: '❌ Cancel', callback_data: `back_to_profile`}
]
]
};
await bot.editMessageText(
`⚠️ Are you sure you want to delete your account?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleDeleteUser:', error);
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
}
}
static async handleConfirmDelete(callbackQuery) {
const telegramId = callbackQuery.from.id;
const chatId = callbackQuery.message.chat.id;
try {
await UserService.updateUserStatus(telegramId, 1);
await bot.editMessageText(
'⚠Your account has been successful deleted',
{ chat_id: chatId, message_id: callbackQuery.message.message_id, }
);
} catch (error) {
console.error('Error in handleConfirmDelete:', error);
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
}
}
}

View File

@@ -0,0 +1,131 @@
// userHandler.js
import config from "../../config/config.js";
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js";
export default class UserHandler {
static async canUseBot(msg) {
const telegramId = msg.from.id;
const user = await UserService.getUserByTelegramId(telegramId);
const keyboard = {
inline_keyboard: [
[{text: "Contact support", url: config.SUPPORT_LINK}]
]
};
switch (user?.status) {
case 0:
return true;
case 1:
await bot.sendMessage(telegramId, '⚠Your account has been deleted by administrator', {reply_markup: keyboard});
return false;
case 2:
await bot.sendMessage(telegramId, '⚠Your account has been blocked by administrator', {reply_markup: keyboard});
return false;
default:
return true;
}
}
static async showProfile(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
try {
await UserService.recalculateUserBalanceByTelegramId(telegramId);
const userStats = await UserService.getDetailedUserByTelegramId(telegramId);
if (!userStats) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Получаем балансы активных и архивных кошельков
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(userStats.id);
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(userStats.id);
// Доступный баланс (bonus_balance + total_balance)
const availableBalance = userStats.bonus_balance + (userStats.total_balance || 0);
const locationText = userStats.country && userStats.city && userStats.district
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
: 'Not set';
const text = `
👤 *Your Profile*
📱 Telegram ID: \`${telegramId}\`
📍 Location: ${locationText}
📊 Statistics:
├ Total Purchases: ${userStats.purchase_count || 0}
├ Total Spent: $${userStats.total_spent || 0}
├ Active Wallets: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
├ Archived Wallets: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
├ Bonus Balance: $${userStats.bonus_balance || 0}
└ Available Balance: $${availableBalance.toFixed(2)}
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
`;
const keyboard = {
inline_keyboard: [
[{text: '📍 Set Location', callback_data: 'set_location'}],
[{text: '❌ Delete Account', callback_data: 'delete_account'}]
]
};
await bot.sendMessage(chatId, text, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in showProfile:', error);
await bot.sendMessage(chatId, 'Error loading profile. Please try again.');
}
}
static async handleStart(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
const username = msg.chat.username;
try {
// Create user profile
await UserService.createUser({
telegram_id: telegramId,
username: username
});
const keyboard = {
reply_markup: {
keyboard: [
['📦 Products', '👤 Profile'],
['🛍 Purchases', '💰 Wallets']
],
resize_keyboard: true
}
};
await bot.sendMessage(
chatId,
'Welcome to the shop! Choose an option:',
keyboard
);
} catch (error) {
console.error('Error in handleStart:', error);
await bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
}
}
static async handleBackToProfile(callbackQuery) {
await this.showProfile({
chat: {id: callbackQuery.message.chat.id},
from: {id: callbackQuery.from.id}
});
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
}
}

View File

@@ -0,0 +1,147 @@
import db from '../../config/database.js';
import LocationService from "../../services/locationService.js";
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
export default class UserLocationHandler {
static async handleSetLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
const countries = await LocationService.getCountries();
if (countries.length === 0) {
await bot.editMessageText(
'No locations available yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to Profile', callback_data: 'back_to_profile'}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...countries.map(loc => [{
text: loc.country,
callback_data: `set_country_${loc.country}`
}]),
[{text: '« Back to Profile', callback_data: 'back_to_profile'}]
]
};
await bot.editMessageText(
'🌍 Select your country:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetLocation:', error);
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
}
}
static async handleSetCountry(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('set_country_', '');
try {
const cities = await LocationService.getCitiesByCountry(country);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `set_city_${country}_${loc.city}`
}]),
[{text: '« Back to Countries', callback_data: 'set_location'}]
]
};
await bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCountry:', error);
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
static async handleSetCity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('set_city_', '').split('_');
try {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `set_district_${country}_${city}_${loc.district}`
}]),
[{text: '« Back to Cities', callback_data: `set_country_${country}`}]
]
};
await bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCity:', error);
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
static async handleSetDistrict(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const telegramId = callbackQuery.from.id;
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
try {
await db.runAsync('BEGIN TRANSACTION');
await UserService.updateUserLocation(telegramId, country, city, district);
await db.runAsync('COMMIT');
await bot.editMessageText(
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to Profile', callback_data: 'back_to_profile'}
]]
}
}
);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error in handleSetDistrict:', error);
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
}
}
}

View File

@@ -0,0 +1,735 @@
import db from '../../config/database.js';
import config from "../../config/config.js";
import LocationService from "../../services/locationService.js";
import bot from "../../context/bot.js";
import userStates from "../../context/userStates.js";
import ProductService from "../../services/productService.js";
import CategoryService from "../../services/categoryService.js";
import UserService from "../../services/userService.js";
import PurchaseService from "../../services/purchaseService.js";
export default class UserProductHandler {
static async showProducts(msg) {
const chatId = msg.chat.id;
const messageId = msg?.message_id;
try {
const countries = await LocationService.getCountries()
if (countries.length === 0) {
const message = 'No products available at the moment.';
if (messageId) {
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId
});
} else {
await bot.sendMessage(chatId, message);
}
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `shop_country_${loc.country}`
}])
};
const message = '🌍 Select your country:';
try {
if (messageId) {
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
}
} catch (error) {
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
}
} catch (error) {
console.error('Error in showProducts:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
static async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', '');
try {
const cities = await LocationService.getCitiesByCountry(country);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `shop_city_${country}_${loc.city}`
}]),
[{text: '« Back to Countries', callback_data: 'shop_start'}]
]
};
await bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
static async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('shop_city_', '').split('_');
try {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city)
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `shop_district_${country}_${city}_${loc.district}`
}]),
[{text: '« Back to Cities', callback_data: `shop_country_${country}`}]
]
};
await bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
static async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
try {
// Получаем информацию о локации
const location = await LocationService.getLocation(country, city, district);
if (!location) {
// Если локация не найдена, вернуть пользователя к предыдущему шагу
await bot.editMessageText(
'Location not found. Returning to previous menu.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: `shop_city_${country}_${city}` }
]]
}
}
);
return;
}
// Сохраняем текстовое представление локации в состоянии пользователя
userStates.set(chatId, {
location: `${country}_${city}_${district}`
});
// Получаем категории для выбранной локации
const categories = await CategoryService.getCategoriesByLocationId(location.id);
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}`
}]),
[{ text: '« Back', callback_data: `shop_city_${country}_${city}` }]
]
};
await bot.editMessageText(
'📦 Select category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
static async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
try {
// Удаляем текущее сообщение
await bot.deleteMessage(chatId, messageId);
// Получаем состояние пользователя
const state = userStates.get(chatId);
// Удаляем сообщение с фотографией, если оно существует
if (state && state.photoMessageId) {
try {
await bot.deleteMessage(chatId, state.photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
// Получаем товары для выбранной категории
const products = await ProductService.getProductsByCategoryId(categoryId);
if (products.length === 0) {
await bot.sendMessage(
chatId,
'No products available in this category.',
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: `shop_district_${state.location}` }
]]
}
}
);
return;
}
// Создаем клавиатуру с товарами
const keyboard = {
inline_keyboard: products.map(product => [
{
text: `${product.name} - $${product.price}`,
callback_data: `shop_product_${product.id}`
}
])
};
// Добавляем кнопку "Назад"
keyboard.inline_keyboard.push([
{ text: '« Back', callback_data: `shop_district_${state.location}` }
]);
// Отправляем сообщение с товарами
await bot.sendMessage(
chatId,
'Select a product:',
{
reply_markup: keyboard
}
);
// Сохраняем состояние пользователя
userStates.set(chatId, {
...state,
action: 'viewing_category',
categoryId,
location: state?.location // Сохраняем информацию о локации
});
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
static async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
try {
// Delete the photo message if it exists
if (photoMessageId) {
try {
await bot.deleteMessage(chatId, photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
const products = await ProductService.getProductsByLocationAndCategory(locationId, categoryId, subcategoryId);
const subcategory = await CategoryService.getSubcategoryById(subcategoryId);
if (products.length === 0) {
await bot.editMessageText(
'No products available in this subcategory.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{
text: '« Back to Subcategories',
callback_data: `shop_category_${locationId}_${categoryId}`
}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price}`,
callback_data: `shop_product_${prod.id}`
}]),
[{text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}`}]
]
};
await bot.editMessageText(
`📦 Products in ${subcategory.name}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
static async handleProductSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('shop_product_', '');
try {
const product = await ProductService.getDetailedProductById(productId);
if (!product) {
throw new Error('Product not found');
}
// Удаляем предыдущее сообщение
await bot.deleteMessage(chatId, messageId);
// Получаем состояние пользователя
const state = userStates.get(chatId);
// Удаляем сообщение с фотографией, если оно существует
if (state?.photoMessageId) {
try {
await bot.deleteMessage(chatId, state.photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
const message = `
📦 ${product.name}
💰 Price: $${product.price}
📝 Description: ${product.description}
📦 Available: ${product.quantity_in_stock} pcs
Category: ${product.category_name}
`;
// Отправляем фото, если оно существует
let photoMessage;
if (product.photo_url) {
try {
photoMessage = await bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
} catch (e) {
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' });
}
}
const keyboard = {
inline_keyboard: [
[{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
[
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: {} // Изначально отключено, так как количество начинается с 1
},
{ text: '1', callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: product.quantity_in_stock <= 1 ? {} : null // Отключено, если остаток 1 или меньше
}
],
[{ text: `« Back ${product.category_name}`, callback_data: `shop_category_${product.location_id}_${product.category_id}` }] // Возврат к категории
]
};
// Отправляем сообщение с кнопками
const productMessage = await bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'HTML'
});
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
userStates.set(chatId, {
action: 'buying_product',
productId,
quantity: 1,
photoMessageId: photoMessage ? photoMessage.message_id : null,
productMessageId: productMessage.message_id,
location: state?.location // Сохраняем информацию о локации
});
} catch (error) {
console.error('Error in handleProductSelection:', error);
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
static async handleIncreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('increase_quantity_', '');
const state = userStates.get(chatId);
try {
const product = await ProductService.getProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at max stock, silently ignore
if (currentQuantity >= product.quantity_in_stock) {
await bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
// Update state
userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{text: newQuantity.toString(), callback_data: 'current_quantity'},
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await bot.editMessageReplyMarkup(
{inline_keyboard: keyboard},
{
chat_id: chatId,
message_id: messageId
}
);
await bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleIncreaseQuantity:', error);
await bot.answerCallbackQuery(callbackQuery.id);
}
}
static async handleDecreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('decrease_quantity_', '');
const state = userStates.get(chatId);
try {
const product = await ProductService.getProductById(productId)
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at minimum, silently ignore
if (currentQuantity <= 1) {
await bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.max(currentQuantity - 1, 1);
// Update state
userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{text: newQuantity.toString(), callback_data: 'current_quantity'},
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await bot.editMessageReplyMarkup(
{inline_keyboard: keyboard},
{
chat_id: chatId,
message_id: messageId
}
);
await bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleDecreaseQuantity:', error);
await bot.answerCallbackQuery(callbackQuery.id);
}
}
static async handleBuyProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
const productId = callbackQuery.data.replace('buy_product_', '');
const state = userStates.get(chatId);
try {
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
throw new Error('User not found');
}
const product = await ProductService.getProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const quantity = state?.quantity || 1;
const totalPrice = product.price * quantity;
// Получение баланса пользователя
const userBalance = await UserService.getUserBalance(user.id);
// Проверка баланса пользователя
if (userBalance <= 0) {
await bot.sendMessage(
chatId,
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
{
reply_markup: {
inline_keyboard: [[
{ text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
]]
}
}
);
return;
}
if (userBalance < totalPrice) {
await bot.sendMessage(
chatId,
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
{
reply_markup: {
inline_keyboard: [[
{ text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
]]
}
}
);
return;
}
// Получение криптокошельков пользователя
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await bot.sendMessage(
chatId,
'You need to add a crypto wallet first to make purchases.',
{
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
[{ text: `Pay`, callback_data: `pay_with_main_${productId}_${quantity}` }],
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }] // Кнопка "Back"
]
};
// Отправка сообщения с кнопками
const purchaseMessage = await bot.editMessageText(
`🛒 Purchase Summary:\n\n` +
`Product: ${product.name}\n` +
`Quantity: ${quantity}\n` +
`Total: $${totalPrice}\n`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
// Сохранение ID сообщения с фотографией в состояние пользователя
userStates.set(chatId, {
...state,
photoMessageId: state?.photoMessageId || null,
purchaseMessageId: purchaseMessage.message_id
});
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
static async handlePay(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
const state = userStates.get(chatId);
try {
await UserService.recalculateUserBalanceByTelegramId(telegramId);
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
throw new Error('User not found');
}
const product = await ProductService.getProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const totalPrice = product.price * quantity;
const balance = user.total_balance + user.bonus_balance;
if (totalPrice > balance) {
userStates.delete(chatId);
await bot.editMessageText(`Not enough money`, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
});
return;
}
// Проверка наличия товара
if (product.quantity_in_stock < quantity) {
await bot.sendMessage(chatId, `❌ Not enough items in stock. Only ${product.quantity_in_stock} available.`);
return;
}
// Создаем покупку и получаем её ID
const purchaseId = await PurchaseService.createPurchase(user.id, productId, walletType, quantity, totalPrice);
// Уменьшаем количество товара в базе данных
await ProductService.decreaseProductQuantity(productId, quantity);
// Извлекаем данные о локации
const location = await LocationService.getLocationById(product.location_id);
const category = await CategoryService.getCategoryById(product.category_id);
// Удаляем сообщение с Public Photo, если оно существует
if (state?.photoMessageId) {
try {
await bot.deleteMessage(chatId, state.photoMessageId);
} catch (error) {
console.error('Error deleting Public Photo message:', error);
}
}
// Отправляем Hidden Photo
let hiddenPhotoMessage;
if (product.hidden_photo_url) {
try {
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
} catch (e) {
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
}
}
const message = `
📦 Purchase Details:
Name: ${product.name}
Quantity: ${quantity}
Total: $${totalPrice}
Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
Category: ${category?.name || 'N/A'}
🔒 Private Information:
${product.private_data || 'N/A'}
Hidden Location: ${product.hidden_description || 'N/A'}
Coordinates: ${product.hidden_coordinates || 'N/A'}
`;
const keyboard = {
inline_keyboard: [
[{ text: 'View new purchase', callback_data: `view_purchase_${purchaseId}` }], // Переход к покупке
[{ text: "Contact support", url: config.SUPPORT_LINK }] // Сохранение кнопки "Contact support"
]
};
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
userStates.set(chatId, {
action: 'viewing_purchase',
purchaseId,
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
});
} catch (error) {
console.error('Error in handlePay:', error);
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
}

View File

@@ -0,0 +1,299 @@
// userPurchaseHandler.js
import config from "../../config/config.js";
import db from '../../config/database.js';
import PurchaseService from "../../services/purchaseService.js";
import UserService from "../../services/userService.js";
import LocationService from "../../services/locationService.js";
import ProductService from "../../services/productService.js";
import CategoryService from "../../services/categoryService.js";
import bot from "../../context/bot.js";
import userStates from "../../context/userStates.js";
export default class UserPurchaseHandler {
static async viewPurchasePage(userId, page) {
try {
const limit = 10; // Количество покупок на странице
const offset = page * limit;
// Получаем покупки пользователя с учетом пагинации
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
// Получаем общее количество покупок пользователя
const totalPurchases = await PurchaseService.getTotalPurchasesByUserId(userId);
// Вычисляем общее количество страниц
const totalPages = Math.ceil(totalPurchases / limit);
// Если покупок нет, возвращаем сообщение о пустом архиве
if (totalPurchases === 0) {
return {
text: 'Your purchase history is empty.',
markup: {
inline_keyboard: [
[{ text: '🛍 Browse Products', callback_data: 'shop_start' }]
]
}
};
}
// Если покупок нет на текущей странице, но это не первая страница, переходим на предыдущую страницу
if (purchases.length === 0 && page > 0) {
return await this.viewPurchasePage(userId, page - 1);
}
const keyboard = {
inline_keyboard: [
...purchases.map(item => [{
// Добавляем иконку статуса покупки
text: `${item.status === 'received' ? '✅' : '❌'} ${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
callback_data: `view_purchase_${item.id}`
}]),
[
{
text: page > 0 ? `« Back (Page ${page})` : '« Back',
callback_data: page > 0 ? `list_purchases_${page - 1}` : 'no_action', // Если на первой странице, то "no_action"
hide: page === 0 // Скрываем кнопку "Назад", если на первой странице
},
{
text: `Page ${page + 1} of ${totalPages}`,
callback_data: 'current_page'
},
{
text: page < totalPages - 1 ? `Next » (Page ${page + 2})` : 'Next »',
callback_data: page < totalPages - 1 ? `list_purchases_${page + 1}` : 'no_action', // Если на последней странице, то "no_action"
hide: page === totalPages - 1 // Скрываем кнопку "Вперед", если на последней странице
}
]
]
};
return {
text: `📦 Select purchase to view detailed information (Page ${page + 1} of ${totalPages}):`,
markup: keyboard
};
} catch (error) {
console.error('Error in viewPurchasePage:', error);
return { text: 'Error loading purchase history. Please try again.' };
}
}
static async handlePurchaseListPage(callbackQuery) {
const telegramId = callbackQuery.from.id;
const chatId = callbackQuery.message.chat.id;
const page = parseInt(callbackQuery.data.replace('list_purchases_', ''));
try {
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
await bot.sendMessage(chatId, 'User not found.');
return;
}
// Удаляем сообщение с Hidden Photo, если оно существует
const state = userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
} catch (error) {
console.error('Error deleting Hidden Photo message:', error);
}
}
const { text, markup } = await this.viewPurchasePage(user.id, page);
await bot.editMessageText(text, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: markup,
parse_mode: 'Markdown'
});
// Удаляем состояние пользователя
userStates.delete(chatId);
} catch (e) {
console.error('Error in handlePurchaseListPage:', e);
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
}
}
static async showPurchases(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
await bot.sendMessage(chatId, 'User not found.');
return;
}
const { text, markup } = await this.viewPurchasePage(user.id, 0);
await bot.sendMessage(chatId, text, { reply_markup: markup, parse_mode: 'Markdown' });
} catch (error) {
console.error('Error in showPurchases:', error);
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
}
}
static async viewPurchase(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const purchaseId = callbackQuery.data.replace('view_purchase_', '');
try {
// Получаем данные покупки
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
await bot.sendMessage(chatId, "No such purchase");
return;
}
// Получаем данные товара по product_id
const product = await ProductService.getProductById(purchase.product_id);
if (!product) {
await bot.sendMessage(chatId, "No such product");
return;
}
// Получаем данные локации по location_id
const location = await LocationService.getLocationById(product.location_id);
// Получаем данные категории по category_id
const category = await CategoryService.getCategoryById(product.category_id);
// Удаляем старое сообщение с Hidden Photo, если оно существует
const state = userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
} catch (error) {
console.error('Error deleting Hidden Photo message:', error);
}
}
// Отправляем Hidden Photo
let hiddenPhotoMessage;
if (product.hidden_photo_url) {
try {
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
} catch (e) {
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
}
}
// Формируем сообщение с деталями покупки
const message = `
📦 Purchase Details:
Name: ${product.name || 'N/A'}
Quantity: ${purchase.quantity}
Total: $${purchase.total_price}
Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
Category: ${category?.name || 'N/A'}
🔒 Private Information:
${product.private_data || 'N/A'}
Hidden Location: ${product.hidden_description || 'N/A'}
Coordinates: ${product.hidden_coordinates || 'N/A'}
`;
// Создаем клавиатуру с кнопками
const keyboard = {
inline_keyboard: [
// Проверяем статус покупки перед добавлением кнопки "I've got it!"
...(purchase.status !== 'received' ? [[{ text: "I've got it!", callback_data: `confirm_received_${purchaseId}` }]] : []),
[{ text: "« Back to Purchase List", callback_data: `list_purchases_0` }], // Кнопка "Назад к списку покупок"
[{ text: "Contact support", url: config.SUPPORT_LINK }]
]
};
// Отправляем сообщение с деталями покупки
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
// Удаляем предыдущее сообщение
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
userStates.set(chatId, {
action: 'viewing_purchase',
purchaseId,
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
});
} catch (error) {
console.error('Error in viewPurchase:', error);
await bot.sendMessage(chatId, 'Error loading purchase details. Please try again.');
}
}
static async handleConfirmReceived(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const purchaseId = callbackQuery.data.replace('confirm_received_', '');
try {
// Получаем данные покупки
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
await bot.sendMessage(chatId, "Purchase not found.");
return;
}
// Получаем данные пользователя по user_id из покупки
const user = await UserService.getUserByUserId(purchase.user_id);
if (!user) {
await bot.sendMessage(chatId, "User not found for this purchase.");
return;
}
// Обновляем статус покупки в базе данных
await PurchaseService.updatePurchaseStatus(purchaseId, 'received');
// Добавляем запись в таблицу transactions
await db.runAsync(
`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount, created_at)
VALUES (?, ?, ?, ?, ?)`,
[
user.id, // ID пользователя
purchase.wallet_type, // Источник списания (например, "bonus_50, crypto_30")
purchase.tx_hash || 'no_hash', // Хеш транзакции (если не указан, то "no_hash")
purchase.total_price, // Сумма транзакции
new Date().toISOString() // Дата создания транзакции
]
);
// Отправляем уведомление администраторам
const adminIds = config.ADMIN_IDS; // Используем массив ADMIN_IDS
for (const adminId of adminIds) {
await bot.sendMessage(adminId, `User ${callbackQuery.from.username} has confirmed receiving purchase #${purchaseId}.`);
}
// Уведомляем пользователя
await bot.sendMessage(chatId, "Thank you! Your purchase has been marked as received.");
// Удаляем сообщение с карточкой товара
await bot.deleteMessage(chatId, messageId);
// Удаляем Hidden Photo, если оно существует
const state = userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
} catch (error) {
console.error('Error deleting Hidden Photo message:', error);
}
}
// Удаляем состояние пользователя
userStates.delete(chatId);
// Открываем список покупок для пользователя
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
} catch (error) {
console.error('Error in handleConfirmReceived:', error);
await bot.sendMessage(chatId, 'Error confirming receipt. Please try again.');
}
}
}

View File

@@ -0,0 +1,751 @@
// userWalletsHandler.js
import db from '../../config/database.js';
import WalletGenerator from '../../utils/walletGenerator.js';
import WalletUtils from '../../utils/walletUtils.js';
import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js";
import bot from "../../context/bot.js";
export default class UserWalletsHandler {
static async showBalance(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId.toString());
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Пересчитываем баланс перед отображением
await UserService.recalculateUserBalanceByTelegramId(telegramId);
// Получаем обновленные данные пользователя
const updatedUser = await UserService.getUserByTelegramId(telegramId.toString());
// Получаем активные криптокошельки
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address, balance
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [updatedUser.id]);
let message = '💰 *Your Active Wallets:*\n\n';
if (cryptoWallets.length > 0) {
const walletUtilsInstance = new WalletUtils(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address, // BTC address
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address, // LTC address
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address, // ETH address
cryptoWallets.find(w => w.wallet_type === 'USDT')?.address, // USDT address
cryptoWallets.find(w => w.wallet_type === 'USDC')?.address, // USDC address
updatedUser.id
);
const balances = await walletUtilsInstance.getAllBalancesFromDB();
let totalUsdValue = 0;
// Отображаем активные кошельки
for (const [type, balance] of Object.entries(balances)) {
const wallet = cryptoWallets.find(w => w.wallet_type === type.split(' ')[0]);
if (wallet) {
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
totalUsdValue += balance.usdValue;
}
}
// Общий баланс криптовалют
message += `📊 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`;
// Бонусный баланс
message += `🎁 *Bonus Balance:* $${updatedUser.bonus_balance.toFixed(2)}\n`;
// Доступный баланс (bonus_balance + total_balance)
const availableBalance = updatedUser.bonus_balance + (updatedUser.total_balance || 0);
message += `💰 *Available Balance:* $${availableBalance.toFixed(2)}\n`;
} else {
message = 'You don\'t have any active wallets yet.';
}
// Проверяем, есть ли архивные кошельки
const archivedCount = await WalletService.getArchivedWalletsCount(updatedUser);
const keyboard = {
inline_keyboard: [
[
{ text: ' Add Crypto Wallet', callback_data: 'add_wallet' },
{ text: '💸 Top Up', callback_data: 'top_up_wallet' }
],
[{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }]
]
};
// Добавляем кнопку архивных кошельков, если они есть
if (archivedCount > 0) {
keyboard.inline_keyboard.splice(2, 0, [
{ text: `📁 Archived Wallets (${archivedCount})`, callback_data: 'view_archived_wallets' }
]);
}
// Добавляем кнопку истории транзакций
keyboard.inline_keyboard.splice(3, 0, [
{ text: '📊 Transaction History', callback_data: 'view_transaction_history_0' }
]);
await bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'Markdown'
});
} catch (error) {
console.error('Error in showBalance:', error);
await bot.sendMessage(chatId, 'Error loading balance. Please try again.');
}
}
static async handleTransactionHistory(callbackQuery, page = 0) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId.toString());
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Fetch transactions with pagination
const limit = 10;
const offset = page * limit;
const transactions = await db.allAsync(`
SELECT amount, tx_hash, created_at, wallet_type
FROM transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [user.id, limit, offset]);
let message = '';
if (transactions.length > 0) {
message = '📊 *Transaction History:*\n\n';
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
message += `💰 Amount: ${tx.amount}\n`;
message += `🔗 TX Hash: \`${tx.tx_hash}\`\n`;
message += `🕒 Date: ${date}\n`;
message += `💼 Wallet Type: ${tx.wallet_type}\n\n`;
});
} else {
message = '📊 *Transaction History:*\n\nNo transactions found.';
}
// Create pagination buttons
const keyboard = {
inline_keyboard: [
[
{ text: '« Back', callback_data: 'back_to_balance' }
]
]
};
// Add "Previous" button if not on the first page
if (page > 0) {
keyboard.inline_keyboard.unshift([
{ text: '⬅️ Previous', callback_data: `view_transaction_history_${page - 1}` }
]);
}
// Add "Next" button if there are more transactions
const nextTransactions = await db.allAsync(`
SELECT amount, tx_hash, created_at, wallet_type
FROM transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [user.id, limit, offset + limit]);
if (nextTransactions.length > 0) {
keyboard.inline_keyboard.push([
{ text: '➡️ Next', callback_data: `view_transaction_history_${page + 1}` }
]);
}
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleTransactionHistory:', error);
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
}
}
static async handleRefreshBalance(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
// Отправляем промежуточный ответ на callback-запрос
await bot.answerCallbackQuery(callbackQuery.id, { text: '🔄 Refreshing balances...' });
const user = await UserService.getUserByTelegramId(callbackQuery.from.id.toString());
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Получаем активные кошельки пользователя из таблицы crypto_wallets
const activeWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'
`, [user.id]);
console.log('[DEBUG] Active wallets:', activeWallets); // Логируем активные кошельки
// Создаем объект для хранения адресов кошельков
const walletAddresses = {
btc: activeWallets.find(w => w.wallet_type === 'BTC')?.address || null,
ltc: activeWallets.find(w => w.wallet_type === 'LTC')?.address || null,
eth: activeWallets.find(w => w.wallet_type === 'ETH')?.address || null,
usdt: activeWallets.find(w => w.wallet_type === 'USDT')?.address || null,
usdc: activeWallets.find(w => w.wallet_type === 'USDC')?.address || null,
};
console.log('[DEBUG] Wallet addresses:', walletAddresses); // Логируем адреса кошельков
// Используем getAllBalancesExt для обновления балансов
const walletUtilsInstance = new WalletUtils(
walletAddresses.btc, // BTC address
walletAddresses.ltc, // LTC address
walletAddresses.eth, // ETH address
walletAddresses.usdt, // USDT address
walletAddresses.usdc, // USDC address
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
console.log('[DEBUG] Calling getAllBalancesExt...'); // Логируем вызов метода
const balances = await walletUtilsInstance.getAllBalancesExt();
console.log('[DEBUG] Balances:', balances); // Логируем полученные балансы
// Обновляем балансы в таблице crypto_wallets только если они изменились
for (const [type, balance] of Object.entries(balances)) {
// Определяем адрес кошелька для данного типа
let address;
switch (type) {
case 'BTC':
address = walletAddresses.btc;
break;
case 'LTC':
address = walletAddresses.ltc;
break;
case 'ETH':
address = walletAddresses.eth;
break;
case 'USDT':
address = walletAddresses.usdt;
break;
case 'USDC':
address = walletAddresses.usdc;
break;
default:
console.warn(`[DEBUG] Unknown wallet type: ${type}`);
continue;
}
if (!address) {
console.warn(`[DEBUG] Address not found for wallet type: ${type}`);
continue;
}
// Получаем текущий баланс для данного адреса
const currentBalance = await db.getAsync(`
SELECT balance
FROM crypto_wallets
WHERE user_id = ? AND address = ?
`, [user.id, address]);
// Проверяем, изменился ли баланс
if (currentBalance?.balance !== balance.amount) {
await db.runAsync(`
UPDATE crypto_wallets
SET balance = ?
WHERE user_id = ? AND address = ?
`, [balance.amount, user.id, address]); // Обновляем баланс по уникальному адресу
console.log(`Баланс для ${type} (${address}) обновлен: ${currentBalance?.balance || 0} -> ${balance.amount}`);
} else {
console.log(`Баланс для ${type} (${address}) не изменился, обновление не требуется.`);
}
}
// Пересчитываем баланс пользователя
await UserService.recalculateUserBalanceByTelegramId(callbackQuery.from.id);
// Отображаем обновленные балансы
await this.showBalance({
chat: { id: chatId },
from: { id: callbackQuery.from.id }
});
// Удаляем сообщение "Refreshing balances..."
await bot.deleteMessage(chatId, messageId);
} catch (error) {
console.error('Error in handleRefreshBalance:', error);
// Уведомляем пользователя об ошибке
await bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Error refreshing balances. Please try again.' });
// Отправляем сообщение об ошибке в чат
await bot.sendMessage(chatId, '❌ Error refreshing balances. Please try again.', {
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
}
}
static async handleAddWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const cryptoOptions = [
['BTC', 'ETH', 'LTC'],
['USDT', 'USDC']
];
const keyboard = {
inline_keyboard: [
...cryptoOptions.map(row =>
row.map(coin => ({
text: coin,
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
}))
),
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await bot.editMessageText(
'🔐 Select cryptocurrency to generate wallet:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
}
static async handleGenerateWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
try {
const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
throw new Error('User not found');
}
await db.runAsync('BEGIN TRANSACTION');
try {
// Получаем существующий кошелек этого типа
const existingWallet = await db.getAsync(
'SELECT id, address FROM crypto_wallets WHERE user_id = ? AND wallet_type = ?',
[user.id, walletType]
);
if (existingWallet) {
// Архивируем старый кошелек, добавляя суффикс с таймштампом
const timestamp = Date.now();
await db.runAsync(
'UPDATE crypto_wallets SET wallet_type = ? WHERE id = ?',
[`${walletType}_${timestamp}`, existingWallet.id]
);
}
// Создаем новый кошелек с использованием WalletService
const walletResult = await WalletService.createWallet(user.id, walletType);
if (!walletResult || !walletResult.address) {
console.error('Wallet creation failed:', {
error: walletResult,
userId: user.id,
walletType
});
throw new Error('Failed to generate wallet address');
}
// Получаем адрес для отображения
const displayAddress = walletResult.address;
const network = this.getNetworkName(walletType);
console.log('Wallet created successfully:', {
address: displayAddress,
derivationPath: walletResult.derivationPath,
userId: user.id,
walletType,
network
});
let message = `✅ New wallet generated successfully!\n\n`;
message += `Type: ${walletType}\n`;
message += `Network: ${network}\n`;
message += `Address: \`${displayAddress}\`\n\n`;
if (existingWallet) {
message += ` Your previous wallet has been archived and will remain accessible for existing funds.\n`;
}
message += `\n⚠️ Important: Your recovery phrase has been securely stored. Keep your wallet address safe!`;
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
});
await db.runAsync('COMMIT');
} catch (error) {
await db.runAsync('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error generating wallet:', error);
await bot.editMessageText(
'❌ Error generating wallet. Please try again.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
}
);
}
}
static async handleTopUpWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId);
// Get crypto wallets
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await bot.editMessageText(
'You don\'t have any wallets yet.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
let message = '💰 *Available wallets for replenishment, all you need to do is click on the wallet where you will replenish funds and it will be copied to the clipboard, then paste it on the crypto exchange as the recipient of funds.:*\n\n';
const walletUtilsInstance = new WalletUtils(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletUtilsInstance.getAllBalances();
for (const [type, balance] of Object.entries(balances)) {
if (cryptoWallets.some(w => w.wallet_type === type.split(' ')[0] ||
(type.includes('ERC-20') && w.wallet_type === 'ETH'))) {
const wallet = cryptoWallets.find(w =>
w.wallet_type === type.split(' ')[0] ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
}
}
const keyboard = {
inline_keyboard: [
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleTopUpWallet:', error);
await bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
}
}
static async handleWalletHistory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
try {
const user = UserService.getUserByTelegramId(telegramId);
const transactions = await db.allAsync(`
SELECT type, amount, tx_hash, created_at, wallet_type
FROM transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
`, [user.id]);
if (transactions.length === 0) {
await bot.editMessageText(
'No transactions found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
let message = '📊 *Recent Transactions:*\n\n';
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
const symbol = tx.type === 'deposit' ? '' : '';
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
message += `🔗 TX: \`${tx.tx_hash}\`\n`;
message += `🕒 ${date}\n\n`;
});
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleWalletHistory:', error);
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
}
}
static async handleViewArchivedWallets(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId.toString());
// Get archived wallets and validate timestamps
const archivedWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%'
ORDER BY wallet_type
`, [user.id]);
// Filter out wallets with invalid timestamps
const validArchivedWallets = archivedWallets.filter(wallet => {
const [, timestamp] = wallet.wallet_type.split('_');
const date = new Date(parseInt(timestamp));
return !isNaN(date.getTime()); // Check if date is valid
});
if (validArchivedWallets.length === 0) {
await bot.editMessageText(
'No archived wallets found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
// Group wallets by base type
const groupedWallets = {};
let totalUsdValue = 0;
for (const wallet of validArchivedWallets) {
const [baseType, timestamp] = wallet.wallet_type.split('_');
if (!groupedWallets[baseType]) {
groupedWallets[baseType] = [];
}
groupedWallets[baseType].push({
address: wallet.address,
timestamp: parseInt(timestamp)
});
}
// Create wallet service instance
const walletUtilsInstance = new WalletUtils(
groupedWallets['BTC']?.[0]?.address,
groupedWallets['LTC']?.[0]?.address,
groupedWallets['ETH']?.[0]?.address || groupedWallets['USDT']?.[0]?.address || groupedWallets['USDC']?.[0]?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
// Get all balances
const balances = await walletUtilsInstance.getAllBalances();
let message = '📁 *Archived Wallets:*\n\n';
// Process each cryptocurrency type
for (const baseType of Object.keys(groupedWallets).sort()) {
let typeTotal = 0;
let typeUsdTotal = 0;
message += `🔒 *${baseType}*\n`;
// Sort wallets by timestamp (newest first)
const sortedWallets = groupedWallets[baseType].sort((a, b) => b.timestamp - a.timestamp);
for (const wallet of sortedWallets) {
const date = new Date(wallet.timestamp);
let balance = 0;
let usdValue = 0;
// Get balance based on wallet type
switch (baseType) {
case 'BTC':
balance = balances.BTC.amount;
usdValue = balances.BTC.usdValue;
break;
case 'LTC':
balance = balances.LTC.amount;
usdValue = balances.LTC.usdValue;
break;
case 'ETH':
case 'USDT':
case 'USDC':
balance = balances[baseType]?.amount || 0;
usdValue = balances[baseType]?.usdValue || 0;
break;
}
typeTotal += balance;
typeUsdTotal += usdValue;
message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
message += `├ Value: $${usdValue.toFixed(2)}\n`;
message += `├ Address: \`${wallet.address}\`\n`;
message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
}
message += `📊 *Total ${baseType}*:\n`;
message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
totalUsdValue += typeUsdTotal;
}
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleViewArchivedWallets:', error);
await bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
}
}
static async handleBackToBalance(callbackQuery) {
await this.showBalance({
chat: { id: callbackQuery.message.chat.id },
from: { id: callbackQuery.from.id }
});
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
}
// Helper methods
static getBaseWalletType(walletType) {
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
static getWalletAddress(wallets, walletType) {
if (walletType.includes('ERC-20')) return wallets.ETH.address;
if (walletType === 'BTC') return wallets.BTC.address;
if (walletType === 'LTC') return wallets.LTC.address;
if (walletType === 'ETH') return wallets.ETH.address;
throw new Error('Invalid wallet type');
}
static getNetworkName(walletType) {
if (walletType.includes('USDT')) return 'Ethereum Network (ERC-20)';
if (walletType.includes('USDC')) return 'Ethereum Network (ERC-20)';
if (walletType === 'BTC') return 'Bitcoin Network';
if (walletType === 'LTC') return 'Litecoin Network';
if (walletType === 'ETH') return 'Ethereum Network';
return 'Unknown Network';
}
static getBaseWalletType(walletType) {
// Убираем суффиксы, такие как ERC-20, TRC-20 и т.д.
return walletType.replace(/ (ERC-20|TRC-20)$/, '');
}
}

View File

@@ -1,160 +0,0 @@
import db from '../config/database.js';
import Validators from '../utils/validators.js';
export default class UserLocationHandler {
constructor(bot) {
this.bot = bot;
}
async handleSetLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
const countries = await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
if (countries.length === 0) {
await this.bot.editMessageText(
'No locations available yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...countries.map(loc => [{
text: loc.country,
callback_data: `set_country_${loc.country}`
}]),
[{ text: '« Back to Profile', callback_data: 'back_to_profile' }]
]
};
await this.bot.editMessageText(
'🌍 Select your country:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetLocation:', error);
await this.bot.sendMessage(chatId, 'Error loading countries. Please try again.');
}
}
async handleSetCountry(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('set_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `set_city_${country}_${loc.city}`
}]),
[{ text: '« Back to Countries', callback_data: 'set_location' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCountry:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleSetCity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('set_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `set_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back to Cities', callback_data: `set_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCity:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleSetDistrict(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const userId = callbackQuery.from.id;
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
try {
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync(
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
[country, city, district, userId.toString()]
);
await db.runAsync('COMMIT');
await this.bot.editMessageText(
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
]]
}
}
);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error in handleSetDistrict:', error);
await this.bot.sendMessage(chatId, 'Error updating location. Please try again.');
}
}
}

View File

@@ -1,656 +0,0 @@
import db from '../config/database.js';
import User from '../models/User.js';
export default class UserProductHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
async showProducts(msg) {
const chatId = msg.chat.id;
const messageId = msg?.message_id;
try {
const countries = await db.allAsync(
'SELECT DISTINCT country FROM locations ORDER BY country'
);
if (countries.length === 0) {
const message = 'No products available at the moment.';
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId
});
} else {
await this.bot.sendMessage(chatId, message);
}
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `shop_country_${loc.country}`
}])
};
const message = '🌍 Select your country:';
try {
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
}
} catch (error) {
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
}
} catch (error) {
console.error('Error in showProducts:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `shop_city_${country}_${loc.city}`
}]),
[{ text: '« Back to Countries', callback_data: 'shop_start' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('shop_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `shop_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back to Cities', callback_data: `shop_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
try {
const location = await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
if (!location) {
throw new Error('Location not found');
}
const categories = await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[location.id]
);
if (categories.length === 0) {
await this.bot.editMessageText(
'No products available in this location yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}`
}]),
[{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }]
]
};
await this.bot.editMessageText(
'📦 Select category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
try {
const subcategories = await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
if (subcategories.length === 0) {
await this.bot.editMessageText(
'No products available in this category yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}`
}]),
[{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }]
]
};
await this.bot.editMessageText(
'📦 Select subcategory:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
}
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
try {
// Delete the photo message if it exists
if (photoMessageId) {
try {
await this.bot.deleteMessage(chatId, photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
const products = await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[locationId, categoryId, subcategoryId]
);
const location = await db.getAsync('SELECT * FROM locations WHERE id = ?', [locationId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
if (products.length === 0) {
await this.bot.editMessageText(
'No products available in this subcategory.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price}`,
callback_data: `shop_product_${prod.id}`
}]),
[{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }]
]
};
await this.bot.editMessageText(
`📦 Products in ${subcategory.name}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleProductSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('shop_product_', '');
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
// Delete the previous message
await this.bot.deleteMessage(chatId, messageId);
const message = `
📦 ${product.name}
💰 Price: $${product.price}
📝 Description: ${product.description}
📦 Available: ${product.quantity_in_stock} pcs
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
`;
let photoMessageId = null;
// First send the photo if it exists
if (product.photo_url) {
const photoMessage = await this.bot.sendPhoto(chatId, product.photo_url);
photoMessageId = photoMessage.message_id;
}
const keyboard = {
inline_keyboard: [
[{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
[
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: {} // Initially disabled as quantity starts at 1
},
{ text: '1', callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: product.quantity_in_stock <= 1 ? {} : null // Disabled if stock is 1 or less
}
],
[{ text: `« Back to ${product.subcategory_name}`, callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}` }]
]
};
// Then send the message with controls
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'HTML'
});
// Store the current quantity and photo message ID in user state
this.userStates.set(chatId, {
action: 'buying_product',
productId,
quantity: 1,
photoMessageId
});
} catch (error) {
console.error('Error in handleProductSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
async handleIncreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('increase_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at max stock, silently ignore
if (currentQuantity >= product.quantity_in_stock) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{ inline_keyboard: keyboard },
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleIncreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleDecreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('decrease_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at minimum, silently ignore
if (currentQuantity <= 1) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.max(currentQuantity - 1, 1);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{ inline_keyboard: keyboard },
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleDecreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleBuyProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const productId = callbackQuery.data.replace('buy_product_', '');
const state = this.userStates.get(chatId);
try {
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
const product = await db.getAsync(
'SELECT * FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const quantity = state?.quantity || 1;
const totalPrice = product.price * quantity;
// Get user's crypto wallets with balances
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await this.bot.sendMessage(
chatId,
'You need to add a crypto wallet first to make purchases.',
{
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...cryptoWallets.map(wallet => [{
text: `Pay with ${wallet.wallet_type}`,
callback_data: `pay_with_${wallet.wallet_type}_${productId}_${quantity}`
}]),
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }]
]
};
await this.bot.editMessageText(
`🛒 Purchase Summary:\n\n` +
`Product: ${product.name}\n` +
`Quantity: ${quantity}\n` +
`Total: $${totalPrice}\n\n` +
`Select payment method:`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await this.bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
async showPurchases(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await User.getById(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
const purchases = await db.allAsync(`
SELECT p.*, pr.name as product_name, pr.description,
l.country, l.city, l.district
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN locations l ON pr.location_id = l.id
WHERE p.user_id = ?
ORDER BY p.purchase_date DESC
LIMIT 10
`, [user.id]);
if (purchases.length === 0) {
await this.bot.sendMessage(
chatId,
'You haven\'t made any purchases yet.',
{
reply_markup: {
inline_keyboard: [[
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
]]
}
}
);
return;
}
let message = '🛍 *Your Recent Purchases:*\n\n';
for (const purchase of purchases) {
const date = new Date(purchase.purchase_date).toLocaleString();
message += `📦 *${purchase.product_name}*\n`;
message += `├ Quantity: ${purchase.quantity}\n`;
message += `├ Total: $${purchase.total_price}\n`;
message += `├ Location: ${purchase.country}, ${purchase.city}\n`;
message += `├ Payment: ${purchase.wallet_type}\n`;
message += `├ TX: \`${purchase.tx_hash}\`\n`;
message += `└ Date: ${date}\n\n`;
}
await this.bot.sendMessage(chatId, message, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
]]
}
});
} catch (error) {
console.error('Error in showPurchases:', error);
await this.bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
}
}
}

View File

@@ -1,567 +0,0 @@
import db from '../config/database.js';
import User from '../models/User.js';
import WalletGenerator from '../utils/walletGenerator.js';
import WalletService from '../utils/walletService.js';
export default class UserWalletsHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
async showBalance(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
if (!user) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Get active crypto wallets only
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
let message = '💰 *Your Active Wallets:*\n\n';
if (cryptoWallets.length > 0) {
const walletService = new WalletService(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
let totalUsdValue = 0;
// Show active wallets
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = cryptoWallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
if (wallet) {
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
totalUsdValue += balance.usdValue;
}
}
message += `📊 *Total Balance:* $${totalUsdValue.toFixed(2)}\n`;
} else {
message = 'You don\'t have any active wallets yet.';
}
// Check if user has archived wallets
const archivedCount = await db.getAsync(`
SELECT COUNT(*) as count
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%'
`, [user.id]);
const keyboard = {
inline_keyboard: [
[
{ text: ' Add Crypto Wallet', callback_data: 'add_wallet' },
{ text: '💸 Top Up', callback_data: 'top_up_wallet' }
],
[{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }],
[{ text: '📊 Transaction History', callback_data: 'wallet_history' }]
]
};
// Add archived wallets button if any exist
if (archivedCount.count > 0) {
keyboard.inline_keyboard.splice(2, 0, [
{ text: `📁 Archived Wallets (${archivedCount.count})`, callback_data: 'view_archived_wallets' }
]);
}
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'Markdown'
});
} catch (error) {
console.error('Error in showBalance:', error);
await this.bot.sendMessage(chatId, 'Error loading balance. Please try again.');
}
}
async handleAddWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const cryptoOptions = [
['BTC', 'ETH', 'LTC'],
['USDT TRC-20', 'USDD TRC-20'],
['USDT ERC-20', 'USDC ERC-20']
];
const keyboard = {
inline_keyboard: [
...cryptoOptions.map(row =>
row.map(coin => ({
text: coin,
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
}))
),
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await this.bot.editMessageText(
'🔐 Select cryptocurrency to generate wallet:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
}
async handleGenerateWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
try {
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
await db.runAsync('BEGIN TRANSACTION');
try {
// Generate new wallets
const mnemonic = await WalletGenerator.generateMnemonic();
const wallets = await WalletGenerator.generateWallets(mnemonic);
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, userId);
// Get the base wallet type (ETH for ERC-20, TRON for TRC-20)
const baseType = this.getBaseWalletType(walletType);
// Get existing wallet of this type
const existingWallet = await db.getAsync(
'SELECT id, address FROM crypto_wallets WHERE user_id = ? AND wallet_type = ?',
[user.id, baseType]
);
if (existingWallet) {
// Archive the old wallet by adding a suffix to its type
const timestamp = Date.now();
await db.runAsync(
'UPDATE crypto_wallets SET wallet_type = ? WHERE id = ?',
[`${baseType}_${timestamp}`, existingWallet.id]
);
}
// Store the new wallet
await db.runAsync(
`INSERT INTO crypto_wallets (
user_id, wallet_type, address, derivation_path, encrypted_mnemonic
) VALUES (?, ?, ?, ?, ?)`,
[
user.id,
baseType,
wallets[baseType].address,
wallets[baseType].path,
encryptedMnemonic
]
);
// Get the appropriate address for the requested wallet type
const displayAddress = this.getWalletAddress(wallets, walletType);
const network = this.getNetworkName(walletType);
let message = `✅ New wallet generated successfully!\n\n`;
message += `Type: ${walletType}\n`;
message += `Network: ${network}\n`;
message += `Address: \`${displayAddress}\`\n\n`;
if (existingWallet) {
message += ` Your previous wallet has been archived and will remain accessible for existing funds.\n`;
}
message += `\n⚠️ Important: Your recovery phrase has been securely stored. Keep your wallet address safe!`;
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
});
await db.runAsync('COMMIT');
} catch (error) {
await db.runAsync('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error generating wallet:', error);
await this.bot.editMessageText(
'❌ Error generating wallet. Please try again.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
}
);
}
}
async handleTopUpWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
// Get crypto wallets
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await this.bot.editMessageText(
'You don\'t have any wallets yet.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
let message = '💰 *Available Wallets:*\n\n';
const walletService = new WalletService(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
for (const [type, balance] of Object.entries(balances)) {
if (cryptoWallets.some(w => w.wallet_type === type.split(' ')[0] ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH'))) {
const wallet = cryptoWallets.find(w =>
w.wallet_type === type.split(' ')[0] ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
}
}
const keyboard = {
inline_keyboard: [
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleTopUpWallet:', error);
await this.bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
}
}
async handleWalletHistory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
const transactions = await db.allAsync(`
SELECT type, amount, tx_hash, created_at, wallet_type
FROM transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
`, [user.id]);
if (transactions.length === 0) {
await this.bot.editMessageText(
'No transactions found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
let message = '📊 *Recent Transactions:*\n\n';
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
const symbol = tx.type === 'deposit' ? '' : '';
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
message += `🔗 TX: \`${tx.tx_hash}\`\n`;
message += `🕒 ${date}\n\n`;
});
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleWalletHistory:', error);
await this.bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
}
}
async handleViewArchivedWallets(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
// Get archived wallets and validate timestamps
const archivedWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%'
ORDER BY wallet_type
`, [user.id]);
// Filter out wallets with invalid timestamps
const validArchivedWallets = archivedWallets.filter(wallet => {
const [, timestamp] = wallet.wallet_type.split('_');
const date = new Date(parseInt(timestamp));
return !isNaN(date.getTime()); // Check if date is valid
});
if (validArchivedWallets.length === 0) {
await this.bot.editMessageText(
'No archived wallets found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
// Group wallets by base type
const groupedWallets = {};
let totalUsdValue = 0;
for (const wallet of validArchivedWallets) {
const [baseType, timestamp] = wallet.wallet_type.split('_');
if (!groupedWallets[baseType]) {
groupedWallets[baseType] = [];
}
groupedWallets[baseType].push({
address: wallet.address,
timestamp: parseInt(timestamp)
});
}
// Create wallet service instance
const walletService = new WalletService(
groupedWallets['BTC']?.[0]?.address,
groupedWallets['LTC']?.[0]?.address,
groupedWallets['TRON']?.[0]?.address,
groupedWallets['ETH']?.[0]?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
// Get all balances
const balances = await walletService.getAllBalances();
let message = '📁 *Archived Wallets:*\n\n';
// Process each cryptocurrency type
for (const baseType of Object.keys(groupedWallets).sort()) {
let typeTotal = 0;
let typeUsdTotal = 0;
message += `🔒 *${baseType}*\n`;
// Sort wallets by timestamp (newest first)
const sortedWallets = groupedWallets[baseType].sort((a, b) => b.timestamp - a.timestamp);
for (const wallet of sortedWallets) {
const date = new Date(wallet.timestamp);
let balance = 0;
let usdValue = 0;
// Get balance based on wallet type
switch (baseType) {
case 'BTC':
balance = balances.BTC.amount;
usdValue = balances.BTC.usdValue;
break;
case 'LTC':
balance = balances.LTC.amount;
usdValue = balances.LTC.usdValue;
break;
case 'ETH':
balance = balances.ETH.amount;
usdValue = balances.ETH.usdValue;
break;
case 'TRON':
balance = balances['USDT TRC-20'].amount + balances['USDD TRC-20'].amount;
usdValue = balances['USDT TRC-20'].usdValue + balances['USDD TRC-20'].usdValue;
break;
}
typeTotal += balance;
typeUsdTotal += usdValue;
message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
message += `├ Value: $${usdValue.toFixed(2)}\n`;
message += `├ Address: \`${wallet.address}\`\n`;
message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
}
message += `📊 *Total ${baseType}*:\n`;
message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
totalUsdValue += typeUsdTotal;
}
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleViewArchivedWallets:', error);
await this.bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
}
}
async handleRefreshBalance(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
await this.bot.editMessageText(
'🔄 Refreshing balances...',
{
chat_id: chatId,
message_id: messageId
}
);
// Re-fetch and display updated balances
await this.showBalance({
chat: { id: chatId },
from: { id: callbackQuery.from.id }
});
// Delete the "refreshing" message
await this.bot.deleteMessage(chatId, messageId);
} catch (error) {
console.error('Error in handleRefreshBalance:', error);
await this.bot.editMessageText(
'❌ Error refreshing balances. Please try again.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
}
}
async handleBackToBalance(callbackQuery) {
await this.showBalance({
chat: { id: callbackQuery.message.chat.id },
from: { id: callbackQuery.from.id }
});
await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
}
// Helper methods
getBaseWalletType(walletType) {
if (walletType.includes('TRC-20')) return 'TRON';
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
getWalletAddress(wallets, walletType) {
if (walletType.includes('TRC-20')) return wallets.TRON.address;
if (walletType.includes('ERC-20')) return wallets.ETH.address;
if (walletType === 'BTC') return wallets.BTC.address;
if (walletType === 'LTC') return wallets.LTC.address;
if (walletType === 'ETH') return wallets.ETH.address;
throw new Error('Invalid wallet type');
}
getNetworkName(walletType) {
if (walletType.includes('TRC-20')) return 'Tron Network (TRC-20)';
if (walletType.includes('ERC-20')) return 'Ethereum Network (ERC-20)';
if (walletType === 'BTC') return 'Bitcoin Network';
if (walletType === 'LTC') return 'Litecoin Network';
if (walletType === 'ETH') return 'Ethereum Network';
return 'Unknown Network';
}
}

View File

@@ -1,278 +1,390 @@
import TelegramBot from 'node-telegram-bot-api';
import config from './config/config.js';
import UserHandler from './handlers/userHandler.js';
import UserProductHandler from './handlers/userProductHandler.js';
import UserWalletsHandler from './handlers/userWalletsHandler.js';
import UserLocationHandler from './handlers/userLocationHandler.js';
import AdminHandler from './handlers/adminHandler.js';
import AdminUserHandler from './handlers/adminUserHandler.js';
import AdminLocationHandler from './handlers/adminLocationHandler.js';
import AdminProductHandler from './handlers/adminProductHandler.js';
import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js';
import ErrorHandler from './utils/errorHandler.js';
import User from './models/User.js';
import bot from "./context/bot.js";
import userHandler from "./handlers/userHandlers/userHandler.js";
import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js";
import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js";
import userProductHandler from "./handlers/userHandlers/userProductHandler.js";
import userWalletsHandler from "./handlers/userHandlers/userWalletsHandler.js";
import userDeletionHandler from "./handlers/userHandlers/userDeletionHandler.js";
import adminHandler from "./handlers/adminHandlers/adminHandler.js";
import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js";
import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js";
import adminLocationHandler from "./handlers/adminHandlers/adminLocationHandler.js";
import adminProductHandler from "./handlers/adminHandlers/adminProductHandler.js";
import adminWalletsHandler from "./handlers/adminHandlers/adminWalletsHandler.js";
// Debug logging function
const logDebug = (action, functionName) => {
console.log(`[DEBUG] Button Press: ${action}`);
console.log(`[DEBUG] Calling Function: ${functionName}`);
console.log(`[DEBUG] Button Press: ${action}`);
console.log(`[DEBUG] Calling Function: ${functionName}`);
};
const initBot = () => {
try {
const bot = new TelegramBot(config.BOT_TOKEN, { polling: true });
console.log('Bot initialized successfully');
return bot;
} catch (error) {
console.error('Failed to initialize bot:', error);
process.exit(1);
}
};
const bot = initBot();
const userHandler = new UserHandler(bot);
const userProductHandler = new UserProductHandler(bot);
const userWalletsHandler = new UserWalletsHandler(bot);
const userLocationHandler = new UserLocationHandler(bot);
const adminHandler = new AdminHandler(bot);
const adminUserHandler = new AdminUserHandler(bot);
const adminLocationHandler = new AdminLocationHandler(bot);
const adminProductHandler = new AdminProductHandler(bot);
// Start command - Create user profile
bot.onText(/\/start/, async (msg) => {
logDebug('/start', 'handleStart');
try {
await userHandler.handleStart(msg);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'start command');
}
logDebug('/start', 'handleStart');
const canUse = await userHandler.canUseBot(msg);
if (!canUse) {
return;
}
try {
await userHandler.handleStart(msg);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'start command');
}
});
// Admin command
bot.onText(/\/admin/, async (msg) => {
logDebug('/admin', 'handleAdminCommand');
try {
await adminHandler.handleAdminCommand(msg);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'admin command');
}
logDebug('/admin', 'handleAdminCommand');
try {
await adminHandler.handleAdminCommand(msg);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'admin command');
}
});
// Handle user menu buttons
bot.on('message', async (msg) => {
if (!msg.text) return;
try {
// Check for admin location input
if (await adminLocationHandler.handleLocationInput(msg)) {
return;
if (msg.text && msg.text.toLowerCase() === '/start') {
return;
}
// Check for admin category input
if (await adminProductHandler.handleCategoryInput(msg)) {
return;
const canUse = await userHandler.canUseBot(msg);
if (!canUse) {
return;
}
// Check for admin subcategory input
if (await adminProductHandler.handleSubcategoryInput(msg)) {
return;
}
// Check for product import
if (await adminProductHandler.handleProductImport(msg)) {
return;
}
logDebug(msg.text, 'handleMessage');
switch (msg.text) {
case '📦 Products':
await userProductHandler.showProducts(msg);
break;
case '👤 Profile':
await userHandler.showProfile(msg);
break;
case '💰 Wallets':
await userWalletsHandler.showBalance(msg);
break;
case '🛍 Purchases':
await userProductHandler.showPurchases(msg);
break;
case '📦 Manage Products':
if (adminHandler.isAdmin(msg.from.id)) {
await adminProductHandler.handleProductManagement(msg);
try {
// Check for admin location input
if (await adminLocationHandler.handleLocationInput(msg)) {
return;
}
break;
case '👥 Manage Users':
if (adminHandler.isAdmin(msg.from.id)) {
await adminUserHandler.handleUserList(msg);
// Check for admin category input
if (await adminProductHandler.handleCategoryInput(msg)) {
return;
}
break;
case '📍 Manage Locations':
if (adminHandler.isAdmin(msg.from.id)) {
await adminLocationHandler.handleViewLocations(msg);
// Check for product import
if (await adminProductHandler.handleProductImport(msg)) {
return;
}
break;
// Check for product edition
if (await adminProductHandler.handleProductEditImport(msg)) {
return;
}
// Check for database dump import
if (await adminDumpHandler.handleDumpImport(msg)) {
return;
}
// Check for bonus balance input
if (await adminUserHandler.handleBonusBalanceInput(msg)) {
return;
}
// Check for category update input
if (await adminProductHandler.handleCategoryUpdate(msg)) {
return;
}
logDebug(msg.text, 'handleMessage');
switch (msg.text) {
case '📦 Products':
await userProductHandler.showProducts(msg);
break;
case '👤 Profile':
await userHandler.showProfile(msg);
break;
case '💰 Wallets':
await userWalletsHandler.showBalance(msg);
break;
case '🛍 Purchases':
await userPurchaseHandler.showPurchases(msg);
break;
case '📦 Manage Products':
if (adminHandler.isAdmin(msg.from.id)) {
await adminProductHandler.handleProductManagement(msg);
}
break;
case '👥 Manage Users':
if (adminHandler.isAdmin(msg.from.id)) {
await adminUserHandler.handleUserList(msg);
}
break;
case '📍 Manage Locations':
if (adminHandler.isAdmin(msg.from.id)) {
await adminLocationHandler.handleViewLocations(msg);
}
break;
case '💾 Database Backup':
if (adminHandler.isAdmin(msg.from.id)) {
await adminDumpHandler.handleDump(msg);
}
break;
case '💰 Manage Wallets':
if (adminHandler.isAdmin(msg.from.id)) {
await adminWalletsHandler.handleWalletManagement(msg);
}
break;
}
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
}
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
}
});
// Handle callback queries
bot.on('callback_query', async (callbackQuery) => {
const action = callbackQuery.data;
const msg = callbackQuery.message;
const action = callbackQuery.data;
const msg = callbackQuery.message;
try {
// Profile and location management
if (action === 'set_location') {
logDebug(action, 'handleSetLocation');
await userLocationHandler.handleSetLocation(callbackQuery);
} else if (action.startsWith('set_country_')) {
logDebug(action, 'handleSetCountry');
await userLocationHandler.handleSetCountry(callbackQuery);
} else if (action.startsWith('set_city_')) {
logDebug(action, 'handleSetCity');
await userLocationHandler.handleSetCity(callbackQuery);
} else if (action.startsWith('set_district_')) {
logDebug(action, 'handleSetDistrict');
await userLocationHandler.handleSetDistrict(callbackQuery);
} else if (action === 'back_to_profile') {
logDebug(action, 'handleBackToProfile');
await userHandler.handleBackToProfile(callbackQuery);
} else if (action === 'back_to_balance') {
logDebug(action, 'handleBackToBalance');
await userWalletsHandler.handleBackToBalance(callbackQuery);
const canUse = await userHandler.canUseBot(callbackQuery);
if (!canUse) {
await bot.answerCallbackQuery(callbackQuery.id);
return;
}
// Wallet management
else if (action === 'add_wallet') {
logDebug(action, 'handleAddWallet');
await userWalletsHandler.handleAddWallet(callbackQuery);
} else if (action === 'top_up_wallet') {
logDebug(action, 'handleTopUpWallet');
await userWalletsHandler.handleTopUpWallet(callbackQuery);
} else if (action === 'wallet_history') {
logDebug(action, 'handleWalletHistory');
await userWalletsHandler.handleWalletHistory(callbackQuery);
} else if (action === 'view_archived_wallets') {
logDebug(action, 'handleViewArchivedWallets');
await userWalletsHandler.handleViewArchivedWallets(callbackQuery);
} else if (action === 'refresh_balance') {
logDebug(action, 'handleRefreshBalance');
await userWalletsHandler.handleRefreshBalance(callbackQuery);
try {
// Profile and location management
if (action === 'set_location') {
logDebug(action, 'handleSetLocation');
await userLocationHandler.handleSetLocation(callbackQuery);
} else if (action.startsWith('set_country_')) {
logDebug(action, 'handleSetCountry');
await userLocationHandler.handleSetCountry(callbackQuery);
} else if (action.startsWith('set_city_')) {
logDebug(action, 'handleSetCity');
await userLocationHandler.handleSetCity(callbackQuery);
} else if (action.startsWith('set_district_')) {
logDebug(action, 'handleSetDistrict');
await userLocationHandler.handleSetDistrict(callbackQuery);
} else if (action === 'back_to_profile') {
logDebug(action, 'handleBackToProfile');
await userHandler.handleBackToProfile(callbackQuery);
} else if (action === 'back_to_balance') {
logDebug(action, 'handleBackToBalance');
await userWalletsHandler.handleBackToBalance(callbackQuery);
} else if (action === 'delete_account') {
logDebug(action, 'handleDeleteAccount');
await userDeletionHandler.handleDeleteAccount(callbackQuery);
} else if (action === 'confirm_delete_account') {
logDebug(action, 'handleConfirmDelete');
await userDeletionHandler.handleConfirmDelete(callbackQuery);
}
// Wallet management
else if (action === 'add_wallet') {
logDebug(action, 'handleAddWallet');
await userWalletsHandler.handleAddWallet(callbackQuery);
} else if (action === 'top_up_wallet') {
logDebug(action, 'handleTopUpWallet');
await userWalletsHandler.handleTopUpWallet(callbackQuery);
} else if (action === 'wallet_history') {
logDebug(action, 'handleWalletHistory');
await userWalletsHandler.handleWalletHistory(callbackQuery);
} else if (action === 'view_archived_wallets') {
logDebug(action, 'handleViewArchivedWallets');
await userWalletsHandler.handleViewArchivedWallets(callbackQuery);
} else if (action === 'refresh_balance') {
logDebug(action, 'handleRefreshBalance');
await userWalletsHandler.handleRefreshBalance(callbackQuery);
}
// Wallet generation
else if (action.startsWith('generate_wallet_')) {
logDebug(action, 'handleGenerateWallet');
await userWalletsHandler.handleGenerateWallet(callbackQuery);
}
// Shop navigation
else if (action === 'shop_start') {
logDebug(action, 'showProducts');
await userProductHandler.showProducts(msg);
} else if (action.startsWith('shop_country_')) {
logDebug(action, 'handleCountrySelection');
await userProductHandler.handleCountrySelection(callbackQuery);
} else if (action.startsWith('shop_city_')) {
logDebug(action, 'handleCitySelection');
await userProductHandler.handleCitySelection(callbackQuery);
} else if (action.startsWith('shop_district_')) {
logDebug(action, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(callbackQuery);
} else if (action.startsWith('shop_category_')) {
logDebug(action, 'handleCategorySelection');
await userProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('shop_product_')) {
logDebug(action, 'handleProductSelection');
await userProductHandler.handleProductSelection(callbackQuery);
} else if (action.startsWith('increase_quantity_')) {
logDebug(action, 'handleIncreaseQuantity');
await userProductHandler.handleIncreaseQuantity(callbackQuery);
} else if (action.startsWith('decrease_quantity_')) {
logDebug(action, 'handleDecreaseQuantity');
await userProductHandler.handleDecreaseQuantity(callbackQuery);
} else if (action.startsWith('buy_product_')) {
logDebug(action, 'handleBuyProduct');
await userProductHandler.handleBuyProduct(callbackQuery);
} else if (action.startsWith('pay_with_')) {
logDebug(action, 'handlePay');
await userProductHandler.handlePay(callbackQuery);
} else if (action.startsWith('list_purchases_')) {
logDebug(action, 'handlePurchaseListPage');
await userPurchaseHandler.handlePurchaseListPage(callbackQuery);
} else if (action.startsWith('view_purchase_')) {
logDebug(action, 'viewPurchase');
await userPurchaseHandler.viewPurchase(callbackQuery);
} else if (action.startsWith('confirm_received_')) {
logDebug(action, 'handleConfirmReceived');
await userPurchaseHandler.handleConfirmReceived(callbackQuery);
}
// Admin location management
else if (action === 'add_location') {
logDebug(action, 'handleAddLocation');
await adminLocationHandler.handleAddLocation(callbackQuery);
} else if (action === 'view_locations') {
logDebug(action, 'handleViewLocations');
await adminLocationHandler.handleViewLocations(callbackQuery);
} else if (action === 'view_ip') {
logDebug(action, 'handleViewIP');
await adminLocationHandler.handleViewIP(callbackQuery);
} else if (action === 'delete_location') {
logDebug(action, 'handleDeleteLocation');
await adminLocationHandler.handleDeleteLocation(callbackQuery);
} else if (action.startsWith('confirm_delete_location_')) {
logDebug(action, 'handleConfirmDelete');
await adminLocationHandler.handleConfirmDelete(callbackQuery);
} else if (action === 'admin_menu') {
logDebug(action, 'backToMenu');
await adminLocationHandler.backToMenu(callbackQuery);
}
// Admin product management
else if (action === 'manage_products') {
logDebug(action, 'handleProductManagement');
await adminProductHandler.handleProductManagement(callbackQuery);
} else if (action.startsWith('prod_country_')) {
logDebug(action, 'handleCountrySelection');
await adminProductHandler.handleCountrySelection(callbackQuery);
} else if (action.startsWith('prod_city_')) {
logDebug(action, 'handleCitySelection');
await adminProductHandler.handleCitySelection(callbackQuery);
} else if (action.startsWith('prod_district_')) {
logDebug(action, 'handleDistrictSelection');
await adminProductHandler.handleDistrictSelection(callbackQuery);
} else if (action.startsWith('add_category_')) {
logDebug(action, 'handleAddCategory');
await adminProductHandler.handleAddCategory(callbackQuery);
} else if (action.startsWith('edit_category_')) {
logDebug(action, 'handleEditCategory');
await adminProductHandler.handleEditCategory(callbackQuery);
} else if (action.startsWith('prod_category_')) {
logDebug(action, 'handleCategorySelection');
await adminProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('list_products_')) {
logDebug(action, 'handleProductListPage');
await adminProductHandler.handleProductListPage(callbackQuery);
} else if (action.startsWith('add_product_')) {
logDebug(action, 'handleAddProduct');
await adminProductHandler.handleAddProduct(callbackQuery);
} else if (action.startsWith('view_product_')) {
logDebug(action, 'handleViewProduct');
await adminProductHandler.handleViewProduct(callbackQuery);
} else if (action.startsWith('edit_product_')) {
logDebug(action, 'handleProductEdit');
await adminProductHandler.handleProductEdit(callbackQuery)
} else if (action.startsWith('delete_product_')) {
logDebug(action, 'handleProductDelete');
await adminProductHandler.handleProductDelete(callbackQuery);
} else if (action.startsWith('confirm_delete_product_')) {
logDebug(action, 'handleConfirmDelete');
await adminProductHandler.handleConfirmDelete(callbackQuery);
}
// Admin user management
else if (action.startsWith('view_user_')) {
logDebug(action, 'handleViewUser');
await adminUserHandler.handleViewUser(callbackQuery);
} else if (action.startsWith('list_users_')) {
logDebug(action, 'handleUserListPage');
await adminUserHandler.handleUserListPage(callbackQuery);
} else if (action.startsWith('delete_user_')) {
logDebug(action, 'handleDeleteUser');
await adminUserHandler.handleDeleteUser(callbackQuery);
} else if (action.startsWith('block_user_')) {
logDebug(action, 'handleBlockUser');
await adminUserHandler.handleBlockUser(callbackQuery);
} else if (action.startsWith('confirm_delete_user_')) {
logDebug(action, 'handleConfirmDelete');
await adminUserHandler.handleConfirmDelete(callbackQuery);
} else if (action.startsWith('confirm_block_user_')) {
logDebug(action, 'handleConfirmBlock');
await adminUserHandler.handleConfirmBlock(callbackQuery);
} else if (action.startsWith('edit_user_balance_')) {
logDebug(action, 'handleEditUserBalance');
await adminUserHandler.handleEditUserBalance(callbackQuery);
}
// Admin users location management
else if (action.startsWith('edit_user_location_')) {
logDebug(action, 'handleEditUserLocation');
await adminUserLocationHandler.handleEditUserLocation(callbackQuery);
} else if (action.startsWith('edit_user_country_')) {
logDebug(action, 'handleEditUserCountry');
await adminUserLocationHandler.handleEditUserCountry(callbackQuery);
} else if (action.startsWith('edit_user_city_')) {
logDebug(action, 'handleEditUserCity');
await adminUserLocationHandler.handleEditUserCity(callbackQuery);
} else if (action.startsWith('edit_user_district_')) {
logDebug(action, 'handleEditUserDistrict');
await adminUserLocationHandler.handleEditUserDistrict(callbackQuery)
}
// Admin Wallet management
else if (action.startsWith('wallet_type_')) { // Добавляем обработку выбора типа кошелька
logDebug(action, 'handleWalletTypeSelection');
await adminWalletsHandler.handleWalletTypeSelection(callbackQuery);
} else if (action.startsWith('check_balance_')) {
logDebug(action, 'handleCheckCommissionBalance');
await adminWalletsHandler.handleCheckCommissionBalance(callbackQuery);
} else if (action.startsWith('prev_page_') || action.startsWith('next_page_')) {
logDebug(action, 'handlePagination');
await adminWalletsHandler.handlePagination(callbackQuery);
} else if (action.startsWith('export_csv_')) {
logDebug(action, 'handleExportCSV');
await adminWalletsHandler.handleExportCSV(callbackQuery);
} else if (action === 'back_to_wallet_types') {
logDebug(action, 'handleBackToWalletTypes');
await adminWalletsHandler.handleBackToWalletTypes(callbackQuery);
}
// Dump manage
else if (action === "export_database") {
await adminDumpHandler.handleExportDatabase(callbackQuery);
return;
} else if (action === "import_database") {
await adminDumpHandler.handleImportDatabase(callbackQuery);
}
// Transaction history
else if (action.startsWith('view_transaction_history_')) {
logDebug(action, 'handleTransactionHistory');
const page = parseInt(action.split('_').pop()); // Extract page number
await userWalletsHandler.handleTransactionHistory(callbackQuery, page);
}
await bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'callback query');
}
// Wallet generation
else if (action.startsWith('generate_wallet_')) {
logDebug(action, 'handleGenerateWallet');
await userWalletsHandler.handleGenerateWallet(callbackQuery);
}
// Shop navigation
else if (action === 'shop_start') {
logDebug(action, 'showProducts');
await userProductHandler.showProducts(msg);
} else if (action.startsWith('shop_country_')) {
logDebug(action, 'handleCountrySelection');
await userProductHandler.handleCountrySelection(callbackQuery);
} else if (action.startsWith('shop_city_')) {
logDebug(action, 'handleCitySelection');
await userProductHandler.handleCitySelection(callbackQuery);
} else if (action.startsWith('shop_district_')) {
logDebug(action, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(callbackQuery);
} else if (action.startsWith('shop_category_')) {
logDebug(action, 'handleCategorySelection');
await userProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('shop_subcategory_')) {
logDebug(action, 'handleSubcategorySelection');
await userProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('shop_product_')) {
logDebug(action, 'handleProductSelection');
await userProductHandler.handleProductSelection(callbackQuery);
} else if (action.startsWith('increase_quantity_')) {
logDebug(action, 'handleIncreaseQuantity');
await userProductHandler.handleIncreaseQuantity(callbackQuery);
} else if (action.startsWith('decrease_quantity_')) {
logDebug(action, 'handleDecreaseQuantity');
await userProductHandler.handleDecreaseQuantity(callbackQuery);
} else if (action.startsWith('buy_product_')) {
logDebug(action, 'handleBuyProduct');
await userProductHandler.handleBuyProduct(callbackQuery);
}
// Admin location management
else if (action === 'add_location') {
logDebug(action, 'handleAddLocation');
await adminLocationHandler.handleAddLocation(callbackQuery);
} else if (action === 'view_locations') {
logDebug(action, 'handleViewLocations');
await adminLocationHandler.handleViewLocations(callbackQuery);
} else if (action === 'delete_location') {
logDebug(action, 'handleDeleteLocation');
await adminLocationHandler.handleDeleteLocation(callbackQuery);
} else if (action.startsWith('confirm_delete_')) {
logDebug(action, 'handleConfirmDelete');
await adminLocationHandler.handleConfirmDelete(callbackQuery);
}
// Admin product management
else if (action === 'manage_products') {
logDebug(action, 'handleProductManagement');
await adminProductHandler.handleProductManagement(callbackQuery);
} else if (action.startsWith('prod_country_')) {
logDebug(action, 'handleCountrySelection');
await adminProductHandler.handleCountrySelection(callbackQuery);
} else if (action.startsWith('prod_city_')) {
logDebug(action, 'handleCitySelection');
await adminProductHandler.handleCitySelection(callbackQuery);
} else if (action.startsWith('prod_district_')) {
logDebug(action, 'handleDistrictSelection');
await adminProductHandler.handleDistrictSelection(callbackQuery);
} else if (action.startsWith('add_category_')) {
logDebug(action, 'handleAddCategory');
await adminProductHandler.handleAddCategory(callbackQuery);
} else if (action.startsWith('edit_category_')) {
logDebug(action, 'handleEditCategory');
await adminProductHandler.handleEditCategory(callbackQuery);
} else if (action.startsWith('prod_category_')) {
logDebug(action, 'handleCategorySelection');
await adminProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('add_subcategory_')) {
logDebug(action, 'handleAddSubcategory');
await adminProductHandler.handleAddSubcategory(callbackQuery);
} else if (action.startsWith('prod_subcategory_')) {
logDebug(action, 'handleSubcategorySelection');
await adminProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('add_product_')) {
logDebug(action, 'handleAddProduct');
await adminProductHandler.handleAddProduct(callbackQuery);
} else if (action.startsWith('view_product_')) {
logDebug(action, 'handleViewProduct');
await adminProductHandler.handleViewProduct(callbackQuery);
}
// Admin user management
else if (action.startsWith('view_user_')) {
logDebug(action, 'handleViewUser');
await adminUserHandler.handleViewUser(callbackQuery);
} else if (action.startsWith('delete_user_')) {
logDebug(action, 'handleDeleteUser');
await adminUserHandler.handleDeleteUser(callbackQuery);
} else if (action.startsWith('confirm_delete_user_')) {
logDebug(action, 'handleConfirmDelete');
await adminUserHandler.handleConfirmDelete(callbackQuery);
} else if (action.startsWith('edit_user_balance_')) {
logDebug(action, 'handleEditUserBalance');
await adminUserHandler.handleEditUserBalance(callbackQuery);
}
await bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
await ErrorHandler.handleError(bot, msg.chat.id, error, 'callback query');
}
});
// Error handling
bot.on('polling_error', ErrorHandler.handlePollingError);
process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
console.error('Unhandled promise rejection:', error);
});
console.log('Bot is running...');
console.log('Bot is running...');

View File

@@ -1,66 +0,0 @@
import db from '../config/database.js';
export default class User {
static async create(telegramId) {
try {
// First check if user exists
const existingUser = await this.getById(telegramId);
if (existingUser) {
return existingUser.id;
}
// Begin transaction
await db.runAsync('BEGIN TRANSACTION');
// Create new user
const result = await db.runAsync(
'INSERT INTO users (telegram_id) VALUES (?)',
[telegramId.toString()]
);
// Commit transaction
await db.runAsync('COMMIT');
return result.lastID;
} catch (error) {
// Rollback on error
await db.runAsync('ROLLBACK');
console.error('Error creating user:', error);
throw error;
}
}
static async getById(telegramId) {
try {
return await db.getAsync(
'SELECT * FROM users WHERE telegram_id = ?',
[telegramId.toString()]
);
} catch (error) {
console.error('Error getting user:', error);
throw error;
}
}
static async getUserStats(telegramId) {
try {
return await db.getAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as purchase_count,
COALESCE(SUM(p.total_price), 0) as total_spent,
COUNT(DISTINCT cw.id) as crypto_wallet_count,
COUNT(DISTINCT cw2.id) as archived_wallet_count
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%_%'
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%_%'
WHERE u.telegram_id = ?
GROUP BY u.id
`, [telegramId.toString()]);
} catch (error) {
console.error('Error getting user stats:', error);
throw error;
}
}
}

110
src/models/Wallet.js Normal file
View File

@@ -0,0 +1,110 @@
// Wallet.js
import db from "../config/database.js";
import WalletUtils from "../utils/walletUtils.js";
export default class Wallet {
static getBaseWalletType(walletType) {
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
static async getArchivedWallets(userId) {
const archivedWallets = await db.allAsync(`
SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type LIKE '%_%'
`, [userId]);
const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address;
const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address;
const ethAddress = archivedWallets.find(w => w.wallet_type.startsWith('ETH'))?.address;
return {
btc: btcAddress,
ltc: ltcAddress,
eth: ethAddress,
wallets: archivedWallets
};
}
static async getActiveWallets(userId) {
const activeWallets = await db.allAsync(
`SELECT wallet_type, address FROM crypto_wallets WHERE user_id = ? ORDER BY wallet_type`,
[userId]
);
const btcAddress = activeWallets.find(w => w.wallet_type === 'BTC')?.address;
const ltcAddress = activeWallets.find(w => w.wallet_type === 'LTC')?.address;
const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address;
return {
btc: btcAddress,
ltc: ltcAddress,
eth: ethAddress,
wallets: activeWallets
};
}
static async getActiveWalletsBalance(userId) {
const activeWallets = await this.getActiveWallets(userId);
const walletUtilsInstance = new WalletUtils(
activeWallets.btc,
activeWallets.ltc,
activeWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletUtilsInstance.getAllBalances();
let totalUsdBalance = 0;
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = activeWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
if (!wallet) {
continue;
}
totalUsdBalance += balance.usdValue;
}
return totalUsdBalance;
}
static async getArchivedWalletsBalance(userId) {
const archiveWallets = await this.getArchivedWallets(userId);
const walletUtilsInstance = new WalletUtils(
archiveWallets.btc,
archiveWallets.ltc,
archiveWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletUtilsInstance.getAllBalances();
let totalUsdBalance = 0;
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = archiveWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('ERC-20') && w.wallet_type.startsWith('ETH'))
);
if (!wallet) {
continue;
}
totalUsdBalance += balance.usdValue;
}
return totalUsdBalance;
}
}

View File

@@ -0,0 +1,42 @@
import db from "../config/database.js";
class CategoryService {
static async getCategoriesByLocationId(locationId) {
try {
const categories = await db.allAsync(
'SELECT * FROM categories WHERE location_id = ?',
[locationId]
);
return categories;
} catch (error) {
console.error('Error fetching categories by location ID:', error);
throw new Error('Failed to fetch categories');
}
}
static async getSubcategoriesByCategoryId(categoryId) {
return await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
}
static async getCategoryById(categoryId) {
try {
const category = await db.getAsync(
'SELECT * FROM categories WHERE id = ?',
[categoryId]
);
return category;
} catch (error) {
console.error('Error fetching category by ID:', error);
throw new Error('Failed to fetch category');
}
}
static async getSubcategoryById(subcategoryId) {
return await db.getAsync('SELECT id, name FROM subcategories WHERE id = ?', [subcategoryId]);
}
}
export default CategoryService;

View File

@@ -0,0 +1,49 @@
import db from "../config/database.js";
class LocationService {
static async getCountries() {
return await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
}
static async getCitiesByCountry(country) {
return await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
}
static async getDistrictsByCountryAndCity(country, city) {
return await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
}
static async getLocation(country, city, district) {
try {
const location = await db.getAsync(
'SELECT * FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
return location;
} catch (error) {
console.error('Error fetching location:', error);
throw new Error('Failed to fetch location');
}
}
static async getLocationById(locationId) {
try {
const location = await db.getAsync(
'SELECT * FROM locations WHERE id = ?',
[locationId]
);
return location;
} catch (error) {
console.error('Error fetching location by ID:', error);
throw new Error('Failed to fetch location');
}
}
}
export default LocationService;

View File

@@ -0,0 +1,60 @@
// productService.js
import db from "../config/database.js";
class ProductService {
static async getProductById(productId) {
try {
return await db.getAsync(`SELECT * FROM products WHERE id = ?`, [productId]);
} catch (error) {
console.error('Error get product:', error);
throw error;
}
}
static async getDetailedProductById(productId) {
return await db.getAsync(
`SELECT p.*, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.id = ?`,
[productId]
);
}
static async getProductsByLocationAndCategory(locationId, categoryId) {
return await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE location_id = ? AND category_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[locationId, categoryId]
);
}
static async getProductsByCategoryId(categoryId) {
return await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE category_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[categoryId]
);
}
static async decreaseProductQuantity(productId, quantity) {
try {
await db.runAsync(
'UPDATE products SET quantity_in_stock = quantity_in_stock - ? WHERE id = ?',
[quantity, productId]
);
} catch (error) {
console.error('Error decreasing product quantity:', error);
throw new Error('Failed to update product quantity');
}
}
}
export default ProductService

View File

@@ -0,0 +1,160 @@
// purchaseService.js
import db from "../config/database.js";
import CryptoJS from "crypto";
import UserService from "../services/userService.js";
class PurchaseService {
static async getPurchasesByUserId(userId, limit, offset) {
try {
return await db.allAsync(`
SELECT
p.*,
pr.name as product_name,
pr.description,
l.country,
l.city,
l.district
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN locations l ON pr.location_id = l.id
WHERE p.user_id = ?
ORDER BY p.purchase_date DESC
LIMIT ?
OFFSET ?
`, [userId, limit, offset]);
} catch (error) {
console.error('Error get purchases:', error);
throw error;
}
}
static async getPurchaseById(purchaseId) {
try {
return await db.getAsync(
`SELECT * FROM purchases WHERE id = ?`,
[purchaseId]
);
} catch (error) {
console.error('Error getting purchase by ID:', error);
throw error;
}
}
static async createPurchase(userId, productId, walletType, quantity, totalPrice) {
try {
const user = await UserService.getUserByUserId(userId);
if (!user) {
throw new Error('User not found');
}
// Обновляем крипто-баланс пользователя перед началом покупки
await UserService.recalculateUserBalanceByTelegramId(user.telegram_id);
let remainingAmount = totalPrice;
let usedBonus = 0;
let usedCrypto = 0;
let sourceWalletType = '';
// Сначала списываем с бонусного баланса
if (user.bonus_balance > 0) {
usedBonus = Math.min(user.bonus_balance, remainingAmount);
remainingAmount -= usedBonus;
// Обновляем бонусный баланс пользователя
await db.runAsync(
'UPDATE users SET bonus_balance = bonus_balance - ? WHERE id = ?',
[usedBonus, userId]
);
// Добавляем информацию о списании с бонусного баланса
sourceWalletType += `bonus_${usedBonus}`;
}
// Если осталась сумма, списываем с крипто-баланса
if (remainingAmount > 0) {
usedCrypto = remainingAmount;
// Обновляем крипто-баланс пользователя
await db.runAsync(
'UPDATE users SET total_balance = total_balance - ? WHERE id = ?',
[usedCrypto, userId]
);
// Добавляем информацию о списании с крипто-баланса
if (sourceWalletType) {
sourceWalletType += `, crypto_${usedCrypto}`;
} else {
sourceWalletType = `crypto_${usedCrypto}`;
}
}
// Генерируем MD5-хеш для tx_hash
const txHash = CryptoJS.MD5(Date.now().toString()).toString();
// Вставляем новую покупку в базу данных
const result = await db.runAsync(
`INSERT INTO purchases (user_id, product_id, wallet_type, quantity, total_price, purchase_date, tx_hash)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[userId, productId, sourceWalletType, quantity, totalPrice, new Date().toISOString(), txHash]
);
// Возвращаем ID новой покупки
return result.lastID;
} catch (error) {
console.error('Error creating purchase:', error);
throw error;
}
}
static async updatePurchaseStatus(purchaseId, status) {
try {
const purchase = await this.getPurchaseById(purchaseId);
if (!purchase) {
throw new Error('Purchase not found');
}
if (status === 'canceled') {
const user = await UserService.getUserByUserId(purchase.user_id);
// Возвращаем средства на бонусный или крипто-баланс
if (purchase.wallet_type === 'bonus') {
await db.runAsync(
'UPDATE users SET bonus_balance = bonus_balance + ? WHERE id = ?',
[purchase.total_price, user.id]
);
} else {
await db.runAsync(
'UPDATE users SET total_balance = total_balance + ? WHERE id = ?',
[purchase.total_price, user.id]
);
}
}
// Обновляем статус покупки
await db.runAsync(
'UPDATE purchases SET status = ? WHERE id = ?',
[status, purchaseId]
);
} catch (error) {
console.error('Error updating purchase status:', error);
throw new Error('Failed to update purchase status');
}
}
static async getTotalPurchasesByUserId(userId) {
try {
const total = await db.getAsync(
`SELECT COUNT(*) AS total FROM purchases WHERE user_id = ?`,
[userId]
);
return total.total;
} catch (error) {
console.error('Error fetching total purchases by user ID:', error);
throw new Error('Failed to fetch total purchases');
}
}
}
export default PurchaseService

213
src/services/userService.js Normal file
View File

@@ -0,0 +1,213 @@
// userService.js
import db from "../config/database.js";
import Wallet from "../models/Wallet.js";
import WalletUtils from "../utils/walletUtils.js";
class UserService {
// Функция для нормализации telegram_id
static normalizeTelegramId(telegramId) {
if (typeof telegramId === 'number') {
// Если это число, преобразуем его в строку и удаляем ".0"
return telegramId.toString().replace(/\.0$/, '');
}
// Если это уже строка, возвращаем как есть
return telegramId.toString();
}
// Функция для валидации telegram_id
static validateTelegramId(telegramId) {
if (typeof telegramId !== 'string') {
throw new Error('telegram_id должен быть строкой');
}
if (telegramId.includes('.0')) {
throw new Error('telegram_id не должен содержать ".0"');
}
}
static async createUser(userData) {
try {
// Нормализуем и валидируем telegram_id
const normalizedTelegramId = this.normalizeTelegramId(userData?.telegram_id);
// console.log("Normalized telegram_id:", normalizedTelegramId); // Отладочный вывод
this.validateTelegramId(normalizedTelegramId);
// Обновляем значение telegram_id в объекте userData
userData.telegram_id = normalizedTelegramId;
// Проверяем, существует ли пользователь с таким telegram_id
const existingUser = await this.getUserByTelegramId(normalizedTelegramId);
if (existingUser) {
console.log("User already exists with telegram_id:", normalizedTelegramId);
return existingUser.id;
}
// Подготавливаем данные для вставки в базу данных
const fields = Object.keys(userData);
const values = Object.values(userData);
const marks = Array(fields.length).fill('?');
const query = `
INSERT INTO users (${fields.join(', ')})
VALUES (${marks.join(', ')})
`;
// Выполняем запрос к базе данных
await db.runAsync('BEGIN TRANSACTION');
const result = await db.runAsync(query, values);
await db.runAsync('COMMIT');
return result.lastID;
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error creating user:', error);
throw error;
}
}
static async getUserByUserId(userId) {
try {
return await db.getAsync(
'SELECT * FROM users WHERE id = ?',
[String(userId)]
);
} catch (error) {
console.error('Error getting user:', error);
throw error;
}
}
static async getUserByTelegramId(telegramId) {
try {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
return await db.getAsync(
'SELECT * FROM users WHERE telegram_id = ?',
[normalizedTelegramId]
);
} catch (error) {
console.error('Error getting user:', error);
throw error;
}
}
static async getDetailedUserByTelegramId(telegramId) {
try {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
return await db.getAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as purchase_count,
(SELECT COALESCE(SUM(p2.total_price), 0)
FROM purchases p2
WHERE p2.user_id = u.id) as total_spent,
COUNT(DISTINCT cw.id) as crypto_wallet_count,
COUNT(DISTINCT cw2.id) as archived_wallet_count
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%#_%' ESCAPE '#'
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%#_%' ESCAPE '#'
WHERE u.telegram_id = ?
GROUP BY u.id
`, [normalizedTelegramId]);
} catch (error) {
console.error('Error getting user stats:', error);
throw error;
}
}
static async updateUser(userId, newUserData) {}
static async deleteUser() {}
static async recalculateUserBalanceByTelegramId(telegramId) {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
const user = await this.getUserByTelegramId(normalizedTelegramId);
if (!user) {
return;
}
try {
// Получаем все крипто-балансы пользователя
const cryptoBalances = await db.allAsync(
`SELECT wallet_type, balance FROM crypto_wallets WHERE user_id = ?`,
[user.id]
);
// Получаем актуальные курсы криптовалют
const prices = await WalletUtils.getCryptoPrices();
// Пересчитываем балансы в доллары
let totalCryptoBalance = 0;
for (const wallet of cryptoBalances) {
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type); // Убираем суффиксы
const rate = prices[baseType.toLowerCase()] || 0; // Получаем курс для базового типа
totalCryptoBalance += wallet.balance * rate;
}
// Получаем сумму всех покупок в крипте
const cryptoPurchases = await db.getAsync(
`SELECT SUM(total_price) as total_sum FROM purchases
WHERE user_id = ? AND wallet_type LIKE 'crypto%'`,
[user.id]
);
// Вычитаем сумму покупок из общего крипто-баланса
const remainingBalance = totalCryptoBalance - (cryptoPurchases?.total_sum || 0);
// Обновляем поле total_balance в таблице users
await db.runAsync(
`UPDATE users SET total_balance = ? WHERE id = ?`,
[remainingBalance, user.id]
);
console.log(`[DEBUG] Updated total_balance for user ${user.id}: ${remainingBalance}`);
} catch (error) {
console.error('Error recalculating user balance:', error);
throw error;
}
}
static async updateUserLocation(telegramId, country, city, district) {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
await db.runAsync(
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
[country, city, district, normalizedTelegramId]
);
}
static async updateUserStatus(telegramId, status) {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
try {
await db.runAsync('BEGIN TRANSACTION');
// Update user status
await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, normalizedTelegramId]);
// Commit transaction
await db.runAsync('COMMIT');
} catch (e) {
await db.runAsync("ROLLBACK");
console.error('Error deleting user:', e);
throw e;
}
}
static async getUserBalance(userId) {
try {
const user = await this.getUserByUserId(userId);
if (!user) {
throw new Error('User not found');
}
// Возвращаем сумму доступного крипто-баланса и бонусного баланса
return user.total_balance + user.bonus_balance;
} catch (error) {
console.error('Error getting user balance:', error);
throw error;
}
}
}
export default UserService;

View File

@@ -0,0 +1,308 @@
// walletService.js
import db from "../config/database.js";
import config from "../config/config.js";
import WalletUtils from "../utils/walletUtils.js";
import WalletGenerator from "../utils/walletGenerator.js";
import crypto from 'crypto';
class WalletService {
static async getArchivedWalletsCount(user) {
try {
const archivedWallets = await db.getAsync(
`SELECT COUNT(*) AS total
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`,
[user.id]
);
return archivedWallets.total;
} catch (error) {
console.error('Error fetching archived wallets count:', error);
throw new Error('Failed to fetch archived wallets count');
}
}
static async getActiveWalletsBalance(userId) {
try {
const wallets = await db.allAsync(
`SELECT wallet_type, balance
FROM crypto_wallets
WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'`,
[userId]
);
const prices = await WalletUtils.getCryptoPrices();
let totalBalance = 0;
for (const wallet of wallets) {
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
const balance = wallet.balance || 0;
switch (baseType) {
case 'BTC':
totalBalance += balance * prices.btc;
break;
case 'LTC':
totalBalance += balance * prices.ltc;
break;
case 'ETH':
totalBalance += balance * prices.eth;
break;
case 'USDT':
totalBalance += balance; // USDT is 1:1 with USD
break;
case 'USDC':
totalBalance += balance; // USDC is 1:1 with USD
break;
}
}
return totalBalance;
} catch (error) {
console.error('Error fetching active wallets balance:', error);
throw new Error('Failed to fetch active wallets balance');
}
}
static async getArchivedWalletsBalance(userId) {
try {
const wallets = await db.allAsync(
`SELECT wallet_type, balance
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`,
[userId]
);
const prices = await WalletUtils.getCryptoPrices();
let totalBalance = 0;
for (const wallet of wallets) {
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
const balance = wallet.balance || 0;
switch (baseType) {
case 'BTC':
totalBalance += balance * prices.btc;
break;
case 'LTC':
totalBalance += balance * prices.ltc;
break;
case 'ETH':
totalBalance += balance * prices.eth;
break;
case 'USDT':
totalBalance += balance; // USDT is 1:1 with USD
break;
case 'USDC':
totalBalance += balance; // USDC is 1:1 with USD
break;
}
}
return totalBalance;
} catch (error) {
console.error('Error fetching archived wallets balance:', error);
throw new Error('Failed to fetch archived wallets balance');
}
}
// Метод для получения кошельков по типу
static async getWalletsByType(walletType) {
try {
const wallets = await db.allAsync(
`SELECT *
FROM crypto_wallets
WHERE wallet_type = ? OR wallet_type LIKE ?`,
[walletType, `${walletType}_%`]
);
return wallets;
} catch (error) {
console.error('Error fetching wallets by type:', error);
throw new Error('Failed to fetch wallets by type');
}
}
static async createWallet(userId, walletType) {
try {
// Генерация нового кошелька
const mnemonic = await WalletGenerator.generateMnemonic();
const wallets = await WalletGenerator.generateWallets(mnemonic, userId);
if (!wallets || typeof wallets !== 'object') {
throw new Error('Failed to generate wallets');
}
// Проверяем наличие базового типа кошелька
const baseType = walletType === 'USDT' || walletType === 'USDC' ? 'ETH' : walletType;
if (!wallets[baseType.toUpperCase()]) {
throw new Error(`Unsupported wallet type: ${walletType}`);
}
// Проверяем наличие ключа шифрования
if (!config.ENCRYPTION_KEY || typeof config.ENCRYPTION_KEY !== 'string') {
throw new Error('Encryption key is not configured');
}
// Проверяем и преобразуем userId
if (typeof userId !== 'number' && typeof userId !== 'string') {
throw new Error('Invalid user ID');
}
const userIdStr = userId.toString();
// Создаем ключ шифрования с использованием хэша
const baseKey = config.ENCRYPTION_KEY;
const combinedKey = baseKey + userIdStr;
// Создаем ключ и IV с использованием SHA-256
const key = crypto.createHash('sha256')
.update(config.ENCRYPTION_KEY + userIdStr)
.digest();
const iv = crypto.randomBytes(16); // Генерируем случайный IV
// Создаем шифр
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
// Шифруем мнемонику
let encrypted = cipher.update(mnemonic, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Сохраняем зашифрованные данные вместе с IV
const encryptedMnemonic = iv.toString('hex') + ':' + encrypted;
// Определяем путь деривации
let derivationPath;
if (walletType === 'USDT') {
derivationPath = "m/44'/60'/0'/0/1"; // Путь для USDT
} else if (walletType === 'USDC') {
derivationPath = "m/44'/60'/0'/0/2"; // Путь для USDC
} else {
derivationPath = wallets[walletType.toUpperCase()].path;
}
// Получаем адрес для базового типа
const walletData = wallets[baseType.toUpperCase()];
if (!walletData || !walletData.address) {
console.error('Wallet generation failed:', {
baseType,
wallets: Object.keys(wallets),
walletData
});
throw new Error('Failed to generate wallet address');
}
const address = walletData.address;
// Вставляем новый кошелек в базу данных
await db.runAsync(
`INSERT INTO crypto_wallets
(user_id, wallet_type, address, derivation_path, mnemonic)
VALUES (?, ?, ?, ?, ?)`,
[userId, walletType, address, derivationPath, encryptedMnemonic]
);
// Проверяем успешность вставки
const insertedWallet = await db.getAsync(
`SELECT * FROM crypto_wallets
WHERE user_id = ? AND wallet_type = ?`,
[userId, walletType]
);
if (!insertedWallet) {
throw new Error('Failed to verify wallet insertion');
}
// Проверяем целостность записанной мнемоники
// Разделяем IV и зашифрованные данные для проверки
const [verify_ivHex, verify_encryptedData] = insertedWallet.mnemonic.split(':');
const verify_iv = Buffer.from(verify_ivHex, 'hex');
// Создаем ключ для проверки
const verify_key = crypto.createHash('sha256')
.update(config.ENCRYPTION_KEY + userId)
.digest();
// Создаем дешифратор для проверки
const decipher = crypto.createDecipheriv('aes-256-cbc', verify_key, verify_iv);
// Дешифруем данные
let decryptedMnemonic;
try {
decryptedMnemonic = decipher.update(verify_encryptedData, 'hex', 'utf8');
decryptedMnemonic += decipher.final('utf8');
if (!decryptedMnemonic) {
throw new Error('Failed to decrypt mnemonic');
}
} catch (error) {
console.error('Decryption error:', error);
throw new Error('Failed to decrypt mnemonic: ' + error.message);
}
if (decryptedMnemonic !== mnemonic) {
console.error('Mnemonic verification failed for wallet:', walletType);
// Удаляем кошелек в случае ошибки
await db.runAsync(
`DELETE FROM crypto_wallets
WHERE user_id = ? AND wallet_type = ?`,
[userId, walletType]
);
throw new Error('Mnemonic verification failed');
}
console.log(`Successfully created and verified wallet: ${walletType}`, {
address,
derivationPath,
userId,
walletType
});
return {
address,
derivationPath,
userId,
walletType
};
} catch (error) {
console.error('Error creating wallet:', error);
throw new Error('Failed to create wallet: ' + error.message);
}
}
static async decryptMnemonic(encryptedMnemonic, userId) {
try {
// Проверяем наличие ключа шифрования
if (!config.ENCRYPTION_KEY || typeof config.ENCRYPTION_KEY !== 'string') {
throw new Error('Encryption key is not configured');
}
// Разделяем IV и зашифрованные данные
const [ivHex, encryptedData] = encryptedMnemonic.split(':');
if (!ivHex || !encryptedData) {
throw new Error('Invalid encrypted mnemonic format');
}
// Создаем ключ дешифрования
const key = crypto.createHash('sha256')
.update(config.ENCRYPTION_KEY + userId.toString())
.digest();
// Преобразуем IV из hex
const iv = Buffer.from(ivHex, 'hex');
// Создаем дешифратор
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
// Дешифруем данные
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('Error decrypting mnemonic:', error);
throw new Error('Failed to decrypt mnemonic: ' + error.message);
}
}
}
export default WalletService;

View File

@@ -1,60 +1,50 @@
// walletGenerator.js
import bip39 from 'bip39';
import HDKey from 'hdkey';
import { publicToAddress } from 'ethereumjs-util';
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';
import CryptoJS from 'crypto-js';
import CryptoJS from 'crypto';
const ECPair = ECPairFactory(ecc);
export default class WalletGenerator {
static async generateMnemonic() {
try {
return bip39.generateMnemonic(256); // 24 words for maximum security
return bip39.generateMnemonic(128); // 12 слов
} catch (error) {
console.error('Error generating mnemonic:', error);
throw new Error('Failed to generate mnemonic');
}
}
static async encryptMnemonic(mnemonic, userId) {
try {
const key = process.env.ENCRYPTION_KEY || 'default-key-12345';
return CryptoJS.AES.encrypt(mnemonic, key + userId.toString()).toString();
} catch (error) {
console.error('Error encrypting mnemonic:', error);
throw new Error('Failed to encrypt mnemonic');
}
}
static async decryptMnemonic(encryptedMnemonic, userId) {
try {
const key = process.env.ENCRYPTION_KEY || 'default-key-12345';
const bytes = CryptoJS.AES.decrypt(encryptedMnemonic, key + userId.toString());
return bytes.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('Error decrypting mnemonic:', error);
throw new Error('Failed to decrypt mnemonic');
}
}
static async generateWallets(mnemonic) {
try {
const seed = await bip39.mnemonicToSeed(mnemonic);
const hdkey = HDKey.fromMasterSeed(Buffer.from(seed));
// Generate BTC wallet (BIP84 - Native SegWit)
const btcNode = hdkey.derive("m/84'/0'/0'/0/0");
const btcKeyPair = ECPair.fromPrivateKey(btcNode.privateKey);
const btcAddress = bitcoin.payments.p2wpkh({
pubkey: btcKeyPair.publicKey
const btcAddress = bitcoin.payments.p2wpkh({
pubkey: btcKeyPair.publicKey,
}).address;
// Generate ETH wallet (BIP44)
const ethNode = hdkey.derive("m/44'/60'/0'/0/0");
const ethAddress = '0x' + publicToAddress(ethNode.publicKey, true).toString('hex');
// Generate USDT wallet (BIP44, same as ETH but different index)
const usdtNode = hdkey.derive("m/44'/60'/0'/0/1");
const usdtAddress = '0x' + publicToAddress(usdtNode.publicKey, true).toString('hex');
// Generate USDC wallet (BIP44, same as ETH but different index)
const usdcNode = hdkey.derive("m/44'/60'/0'/0/2");
const usdcAddress = '0x' + publicToAddress(usdcNode.publicKey, true).toString('hex');
// Generate LTC wallet (BIP84 - Native SegWit)
const ltcNode = hdkey.derive("m/84'/2'/0'/0/0");
const ltcKeyPair = ECPair.fromPrivateKey(ltcNode.privateKey);
@@ -65,84 +55,39 @@ export default class WalletGenerator {
bech32: 'ltc',
bip32: {
public: 0x019da462,
private: 0x019d9cfe
private: 0x019d9cfe,
},
pubKeyHash: 0x30,
scriptHash: 0x32,
wif: 0xb0
}
wif: 0xb0,
},
}).address;
// Generate TRON address (BIP44)
const tronNode = hdkey.derive("m/44'/195'/0'/0/0");
const tronAddress = this.generateTronAddress(tronNode.publicKey);
return {
BTC: {
address: btcAddress,
path: "m/84'/0'/0'/0/0"
BTC: {
address: btcAddress,
path: "m/84'/0'/0'/0/0",
},
ETH: {
address: ethAddress,
path: "m/44'/60'/0'/0/0"
ETH: {
address: ethAddress,
path: "m/44'/60'/0'/0/0",
},
LTC: {
address: ltcAddress,
path: "m/84'/2'/0'/0/0"
USDT: {
address: usdtAddress,
path: "m/44'/60'/0'/0/1",
},
USDC: {
address: usdcAddress,
path: "m/44'/60'/0'/0/2",
},
LTC: {
address: ltcAddress,
path: "m/84'/2'/0'/0/0",
},
TRON: {
address: tronAddress,
path: "m/44'/195'/0'/0/0"
}
};
} catch (error) {
console.error('Error in generateWallets:', error);
throw new Error('Failed to generate cryptocurrency wallets: ' + error.message);
}
}
static generateTronAddress(publicKey) {
try {
const addressPrefix = '41'; // TRON mainnet prefix
const pubKeyHash = CryptoJS.SHA256(
CryptoJS.lib.WordArray.create(publicKey)
).toString();
const address = addressPrefix + pubKeyHash.substring(0, 40);
return this.base58Encode(Buffer.from(address, 'hex'));
} catch (error) {
console.error('Error generating TRON address:', error);
throw new Error('Failed to generate TRON address: ' + error.message);
}
}
static base58Encode(buffer) {
try {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let digits = [0];
for (let i = 0; i < buffer.length; i++) {
let carry = buffer[i];
for (let j = 0; j < digits.length; j++) {
carry += digits[j] << 8;
digits[j] = carry % 58;
carry = (carry / 58) | 0;
}
while (carry > 0) {
digits.push(carry % 58);
carry = (carry / 58) | 0;
}
}
// Add leading zeros
for (let i = 0; buffer[i] === 0 && i < buffer.length - 1; i++) {
digits.push(0);
}
return digits.reverse().map(digit => ALPHABET[digit]).join('');
} catch (error) {
console.error('Error in base58Encode:', error);
throw new Error('Failed to encode address: ' + error.message);
}
}
}
}

View File

@@ -1,164 +0,0 @@
import axios from 'axios';
export default class WalletService {
constructor(btcAddress, ltcAddress, trxAddress, ethAddress, userId, minTimestamp) {
this.btcAddress = btcAddress;
this.ltcAddress = ltcAddress;
this.trxAddress = trxAddress;
this.ethAddress = ethAddress;
this.userId = userId;
this.minTimestamp = minTimestamp;
}
static async getCryptoPrices() {
try {
const response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,litecoin,tether,usd-coin,tron,ethereum&vs_currencies=usd');
return {
btc: response.data.bitcoin?.usd || 0,
ltc: response.data.litecoin?.usd || 0,
eth: response.data.ethereum?.usd || 0,
usdt: 1, // Stablecoin
usdc: 1, // Stablecoin
trx: response.data.tron?.usd || 0,
usdd: 1 // Stablecoin
};
} catch (error) {
console.error('Error fetching crypto prices:', error);
return {
btc: 0, ltc: 0, eth: 0, usdt: 1, usdc: 1, trx: 0, usdd: 1
};
}
}
async fetchApiRequest(url) {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
return null;
}
}
async getBtcBalance() {
if (!this.btcAddress) return 0;
try {
const url = `https://blockchain.info/balance?active=${this.btcAddress}`;
const data = await this.fetchApiRequest(url);
return data?.[this.btcAddress]?.final_balance / 100000000 || 0;
} catch (error) {
console.error('Error getting BTC balance:', error);
return 0;
}
}
async getLtcBalance() {
if (!this.ltcAddress) return 0;
try {
const url = `https://api.blockcypher.com/v1/ltc/main/addrs/${this.ltcAddress}/balance`;
const data = await this.fetchApiRequest(url);
return data?.balance / 100000000 || 0;
} catch (error) {
console.error('Error getting LTC balance:', error);
return 0;
}
}
async getEthBalance() {
if (!this.ethAddress) return 0;
try {
const url = `https://api.etherscan.io/api?module=account&action=balance&address=${this.ethAddress}&tag=latest`;
const data = await this.fetchApiRequest(url);
return data?.result ? parseFloat(data.result) / 1e18 : 0;
} catch (error) {
console.error('Error getting ETH balance:', error);
return 0;
}
}
async getUsdtErc20Balance() {
if (!this.ethAddress) return 0;
try {
const url = `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=0xdac17f958d2ee523a2206206994597c13d831ec7&address=${this.ethAddress}&tag=latest`;
const data = await this.fetchApiRequest(url);
return data?.result ? parseFloat(data.result) / 1e6 : 0;
} catch (error) {
console.error('Error getting USDT ERC20 balance:', error);
return 0;
}
}
async getUsdcErc20Balance() {
if (!this.ethAddress) return 0;
try {
const url = `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&address=${this.ethAddress}&tag=latest`;
const data = await this.fetchApiRequest(url);
return data?.result ? parseFloat(data.result) / 1e6 : 0;
} catch (error) {
console.error('Error getting USDC ERC20 balance:', error);
return 0;
}
}
async getUsdtTrc20Balance() {
if (!this.trxAddress) return 0;
try {
const url = `https://apilist.tronscan.org/api/account?address=${this.trxAddress}`;
const data = await this.fetchApiRequest(url);
const usdtToken = data?.trc20token_balances?.find(token =>
token.tokenId === 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'
);
return usdtToken ? parseFloat(usdtToken.balance) / 1e6 : 0;
} catch (error) {
console.error('Error getting USDT TRC20 balance:', error);
return 0;
}
}
async getUsddTrc20Balance() {
if (!this.trxAddress) return 0;
try {
const url = `https://apilist.tronscan.org/api/account?address=${this.trxAddress}`;
const data = await this.fetchApiRequest(url);
const usddToken = data?.trc20token_balances?.find(token =>
token.tokenId === 'TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn'
);
return usddToken ? parseFloat(usddToken.balance) / 1e18 : 0;
} catch (error) {
console.error('Error getting USDD TRC20 balance:', error);
return 0;
}
}
async getAllBalances() {
const [
btcBalance,
ltcBalance,
ethBalance,
usdtErc20Balance,
usdcErc20Balance,
usdtTrc20Balance,
usddTrc20Balance,
prices
] = await Promise.all([
this.getBtcBalance(),
this.getLtcBalance(),
this.getEthBalance(),
this.getUsdtErc20Balance(),
this.getUsdcErc20Balance(),
this.getUsdtTrc20Balance(),
this.getUsddTrc20Balance(),
WalletService.getCryptoPrices()
]);
return {
BTC: { amount: btcBalance, usdValue: btcBalance * prices.btc },
LTC: { amount: ltcBalance, usdValue: ltcBalance * prices.ltc },
ETH: { amount: ethBalance, usdValue: ethBalance * prices.eth },
'USDT ERC-20': { amount: usdtErc20Balance, usdValue: usdtErc20Balance },
'USDC ERC-20': { amount: usdcErc20Balance, usdValue: usdcErc20Balance },
'USDT TRC-20': { amount: usdtTrc20Balance, usdValue: usdtTrc20Balance },
'USDD TRC-20': { amount: usddTrc20Balance, usdValue: usddTrc20Balance }
};
}
}

364
src/utils/walletUtils.js Normal file
View File

@@ -0,0 +1,364 @@
// walletUtils.js
import axios from 'axios';
import db from '../config/database.js'; // Импортируем базу данных
// Массив публичных RPC-узлов
const rpcNodes = [
"https://rpc.ankr.com/eth",
"https://cloudflare-eth.com",
"https://nodes.mewapi.io/rpc/eth",
];
// Список популярных API для получения цен на криптовалюты
const cryptoPriceAPIs = [
{
name: 'CoinGecko',
url: 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,litecoin,ethereum&vs_currencies=usd',
parser: (data) => ({
btc: data.bitcoin?.usd || 0,
ltc: data.litecoin?.usd || 0,
eth: data.ethereum?.usd || 0,
usdt: 1, // USDT — это стейблкоин, его цена всегда 1 USD
usdc: 1 // USDC — это стейблкоин, его цена всегда 1 USD
})
},
{
name: 'Binance',
urls: {
btc: 'https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT',
ltc: 'https://api.binance.com/api/v3/ticker/price?symbol=LTCUSDT',
eth: 'https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT'
},
parser: async (urls) => {
const btcResponse = await axios.get(urls.btc);
const ltcResponse = await axios.get(urls.ltc);
const ethResponse = await axios.get(urls.eth);
return {
btc: parseFloat(btcResponse.data.price) || 0,
ltc: parseFloat(ltcResponse.data.price) || 0,
eth: parseFloat(ethResponse.data.price) || 0,
usdt: 1, // USDT — это стейблкоин, его цена всегда 1 USD
usdc: 1 // USDC — это стейблкоин, его цена всегда 1 USD
};
}
},
{
name: 'CryptoCompare',
url: 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,LTC,ETH&tsyms=USD',
parser: (data) => ({
btc: data.BTC?.USD || 0,
ltc: data.LTC?.USD || 0,
eth: data.ETH?.USD || 0,
usdt: 1, // USDT — это стейблкоин, его цена всегда 1 USD
usdc: 1 // USDC — это стейблкоин, его цена всегда 1 USD
})
}
];
// Задержка между запросами
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Кеш для хранения курсов криптовалют
let cryptoPricesCache = null;
let cacheTimestamp = 0;
const CACHE_TTL = 60 * 1000; // 1 минута
export default class WalletUtils {
constructor(btcAddress, ltcAddress, ethAddress, usdtAddress, usdcAddress, userId, minTimestamp) {
this.btcAddress = btcAddress;
this.ltcAddress = ltcAddress;
this.ethAddress = ethAddress;
this.usdtAddress = usdtAddress;
this.usdcAddress = usdcAddress;
this.userId = userId;
this.minTimestamp = minTimestamp;
}
static getBaseWalletType(walletType) {
// Убираем суффикс ERC-20, если он есть
if (walletType.includes('ERC-20')) {
return 'ETH';
}
// Убираем суффикс с таймштампом (например, USDT_1735846098129 -> USDT)
if (walletType.includes('_')) {
return walletType.split('_')[0];
}
// Возвращаем исходный тип, если это не ERC-20 и не архивный кошелек
return walletType;
}
static async getCryptoPrices() {
// Если кеш актуален, возвращаем его
if (cryptoPricesCache && Date.now() - cacheTimestamp < CACHE_TTL) {
console.log('[DEBUG] Using cached crypto prices:', cryptoPricesCache);
return cryptoPricesCache;
}
// Если кеш устарел, запрашиваем новые данные
for (const api of cryptoPriceAPIs) {
try {
console.log(`[DEBUG] Trying to fetch prices from ${api.name}...`);
let data;
if (api.name === 'Binance') {
data = await api.parser(api.urls);
} else {
const response = await axios.get(api.url);
data = api.parser(response.data);
}
console.log(`[DEBUG] Successfully fetched prices from ${api.name}:`, data);
// Обновляем кеш
cryptoPricesCache = data;
cacheTimestamp = Date.now();
return data;
} catch (error) {
if (error.response && error.response.status === 429) {
console.log(`[DEBUG] Rate limit exceeded on ${api.name}. Retrying after 2 seconds...`);
await sleep(2000);
continue; // Пробуем снова с тем же API
} else {
console.error(`[DEBUG] Error fetching prices from ${api.name}:`, error.message);
}
}
}
// Если все API не сработали, используем fallback-значения
console.error('[DEBUG] All APIs failed. Using fallback prices.');
cryptoPricesCache = {
btc: 0, ltc: 0, eth: 0, usdt: 1, usdc: 1
};
cacheTimestamp = Date.now();
return cryptoPricesCache;
}
async fetchApiRequest(url) {
try {
console.log(`[DEBUG] Fetching data from: ${url}`); // Логируем URL запроса
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
return null;
}
}
async fetchRpcRequest(method, params) {
console.log(`[DEBUG] fetchRpcRequest called with method: ${method}, params: ${JSON.stringify(params)}`); // Логируем вызов метода
const results = [];
for (const node of rpcNodes) {
try {
const response = await axios.post(node, {
jsonrpc: "2.0",
method,
params,
id: 1,
});
if (response.data && response.data.result) {
results.push(response.data.result);
console.log(`Запрос успешно выполнен на узле ${node}`); // Логируем успешный запрос
} else {
console.warn(`Некорректный ответ от узла ${node}`); // Логируем некорректный ответ
}
} catch (error) {
console.error(`Ошибка на узле ${node}: ${error.message}`); // Логируем ошибку
}
}
if (results.length === 0) {
throw new Error("Нет доступных узлов для выполнения запроса.");
}
const uniqueResults = [...new Set(results)];
if (uniqueResults.length === 1) {
console.log("Баланс совпадает на всех узлах:", uniqueResults[0]); // Логируем совпадение балансов
return uniqueResults[0];
} else {
console.warn("Результаты отличаются на некоторых узлах. Возвращаем первый результат."); // Логируем различия
return results[0];
}
}
async getBtcBalance() {
if (!this.btcAddress) {
console.log('[DEBUG] BTC address is not provided, skipping balance check.'); // Логируем отсутствие адреса
return 0;
}
try {
const url = `https://blockchain.info/balance?active=${this.btcAddress}`;
console.log(`[DEBUG] Fetching BTC balance from: ${url}`); // Логируем URL запроса
const data = await this.fetchApiRequest(url);
return data?.[this.btcAddress]?.final_balance / 100000000 || 0;
} catch (error) {
console.error('Error getting BTC balance:', error);
return 0;
}
}
async getLtcBalance() {
if (!this.ltcAddress) {
console.log('[DEBUG] LTC address is not provided, skipping balance check.'); // Логируем отсутствие адреса
return 0;
}
try {
const url = `https://api.blockcypher.com/v1/ltc/main/addrs/${this.ltcAddress}/balance`;
console.log(`[DEBUG] Fetching LTC balance from: ${url}`); // Логируем URL запроса
const data = await this.fetchApiRequest(url);
return data?.balance / 100000000 || 0;
} catch (error) {
console.error('Error getting LTC balance:', error);
return 0;
}
}
async getEthBalance() {
if (!this.ethAddress) {
console.log('[DEBUG] ETH address is not provided, skipping balance check.'); // Логируем отсутствие адреса
return 0;
}
try {
console.log(`[DEBUG] Fetching ETH balance for address: ${this.ethAddress}`); // Логируем адрес
const balanceHex = await this.fetchRpcRequest("eth_getBalance", [this.ethAddress, "latest"]);
return parseInt(balanceHex, 16) / 1e18;
} catch (error) {
console.error('Error getting ETH balance:', error);
return 0;
}
}
async getUsdtErc20Balance() {
if (!this.usdtAddress) {
console.log('[DEBUG] USDT address is not provided, skipping balance check.'); // Логируем отсутствие адреса
return 0;
}
try {
console.log(`[DEBUG] Fetching USDT ERC-20 balance for address: ${this.usdtAddress}`); // Логируем адрес
const balanceHex = await this.fetchRpcRequest("eth_call", [
{
to: "0xdac17f958d2ee523a2206206994597c13d831ec7",
data: `0x70a08231000000000000000000000000${this.usdtAddress.slice(2)}`,
},
"latest"
]);
return parseInt(balanceHex, 16) / 1e6;
} catch (error) {
console.error('Error getting USDT ERC-20 balance:', error);
return 0;
}
}
async getUsdcErc20Balance() {
if (!this.usdcAddress) {
console.log('[DEBUG] USDC address is not provided, skipping balance check.'); // Логируем отсутствие адреса
return 0;
}
try {
console.log(`[DEBUG] Fetching USDC ERC-20 balance for address: ${this.usdcAddress}`); // Логируем адрес
const balanceHex = await this.fetchRpcRequest("eth_call", [
{
to: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
data: `0x70a08231000000000000000000000000${this.usdcAddress.slice(2)}`,
},
"latest"
]);
return parseInt(balanceHex, 16) / 1e6;
} catch (error) {
console.error('Error getting USDC ERC-20 balance:', error);
return 0;
}
}
async getAllBalancesFromDB() {
const prices = await WalletUtils.getCryptoPrices();
const balances = {
BTC: { amount: 0, usdValue: 0 },
LTC: { amount: 0, usdValue: 0 },
ETH: { amount: 0, usdValue: 0 },
USDT: { amount: 0, usdValue: 0 },
USDC: { amount: 0, usdValue: 0 }
};
// Получаем балансы из таблицы crypto_wallets
const wallets = await db.allAsync(`
SELECT wallet_type, balance FROM crypto_wallets WHERE user_id = ?
`, [this.userId]);
for (const wallet of wallets) {
const [baseType] = wallet.wallet_type.split('_'); // Учитываем только базовый тип
const balance = wallet.balance || 0;
switch (baseType) {
case 'BTC':
balances.BTC.amount += balance;
balances.BTC.usdValue += balance * prices.btc;
break;
case 'LTC':
balances.LTC.amount += balance;
balances.LTC.usdValue += balance * prices.ltc;
break;
case 'ETH':
balances.ETH.amount += balance;
balances.ETH.usdValue += balance * prices.eth;
break;
case 'USDT':
balances.USDT.amount += balance;
balances.USDT.usdValue += balance;
break;
case 'USDC':
balances.USDC.amount += balance;
balances.USDC.usdValue += balance;
break;
}
}
return balances;
}
async getAllBalances() {
return await this.getAllBalancesFromDB();
}
async getAllBalancesExt() {
console.log('[DEBUG] getAllBalancesExt called'); // Логируем вызов метода
const [
btcBalance,
ltcBalance,
ethBalance,
usdtErc20Balance,
usdcErc20Balance,
prices
] = await Promise.all([
this.getBtcBalance(),
this.getLtcBalance(),
this.getEthBalance(),
this.getUsdtErc20Balance(),
this.getUsdcErc20Balance(),
WalletUtils.getCryptoPrices()
]);
console.log('[DEBUG] Balances fetched:', { // Логируем полученные балансы
btcBalance,
ltcBalance,
ethBalance,
usdtErc20Balance,
usdcErc20Balance,
prices
});
return {
BTC: { amount: btcBalance, usdValue: btcBalance * prices.btc },
LTC: { amount: ltcBalance, usdValue: ltcBalance * prices.ltc },
ETH: { amount: ethBalance, usdValue: ethBalance * prices.eth },
USDT: { amount: usdtErc20Balance, usdValue: usdtErc20Balance },
USDC: { amount: usdcErc20Balance, usdValue: usdcErc20Balance }
};
}
}

1
wg/config/resolv.conf Normal file
View File

@@ -0,0 +1 @@
nameserver 9.9.9.11

12
wg/config/wg0.conf Normal file
View File

@@ -0,0 +1,12 @@
# Autogenerated by WireGuard UI (WireAdmin)
[Interface]
PrivateKey = ePxlvZTgr+fJ7ntU6oWti13X8h2100CrjnZFOkSLUWQ=
Address = 10.8.0.4/24
DNS = 9.9.9.11
[Peer]
PublicKey = PYJSZlU38l9OzZnb7iANVk3LotbTg5MdyB2nInxhdA0=
PresharedKey = gK0SjJAvE0oFT6q9yDOQpBP6CyUOclX5yMqAm3hNa1Q=
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 0
Endpoint = 194.87.105.23:51820

191
wg/start.sh Normal file
View File

@@ -0,0 +1,191 @@
#!/bin/sh
# Функция для отображения разделителя
print_separator() {
echo "════════════════════════════════════════════════════════════════════════════════"
}
# Функция для отображения заголовка этапа
print_stage() {
echo "║ 🚀 $1"
print_separator
}
# Функция для отображения результата
print_result() {
local status=$?
local message=$1
local action=$2
if [ -n "$action" ]; then
case "$action" in
"created")
echo "║ 🆕 $message"
;;
"exists")
echo "║ ✅ $message"
;;
*)
if [ $status -eq 0 ]; then
echo "║ ✅ $message"
else
echo "║ ❌ $message"
fi
;;
esac
else
if [ $status -eq 0 ]; then
echo "║ ✅ $message"
else
echo "║ ❌ $message"
fi
fi
print_separator
}
# Проверка включения WireGuard
if [ "$WG_ENABLED" = "false" ]; then
print_stage "WireGuard is disabled"
print_result "Skipping WireGuard setup"
print_stage "Starting application"
echo "║ Application is starting..."
exec node src/index.js
exit 0
fi
# Проверка наличия /etc/resolv.conf
print_stage "Checking /etc/resolv.conf"
if [ ! -f /etc/resolv.conf ]; then
echo "║ /etc/resolv.conf not found. Creating it..."
echo "nameserver 1.1.1.1" > /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
if [ $? -eq 0 ]; then
print_result "/etc/resolv.conf created successfully." "created"
else
print_result "Failed to create /etc/resolv.conf"
exit 1
fi
else
print_result "/etc/resolv.conf already exists." "exists"
fi
# Проверка наличия конфига WireGuard
print_stage "Checking WireGuard config"
if [ ! -f /etc/wireguard/wg0.conf ]; then
echo "║ Error: WireGuard config not found!"
exit 1
else
if [ -r /etc/wireguard/wg0.conf ]; then
print_result "WireGuard config found and readable." "exists"
else
print_result "WireGuard config found but not readable!"
exit 1
fi
fi
# Проверка сети ДО включения WireGuard
print_stage "Testing connectivity BEFORE WireGuard"
echo "║ Pinging 1.1.1.1..."
ping -c 4 1.1.1.1 > /tmp/ping.log 2>&1
if [ $? -eq 0 ]; then
echo "║ Ping successful."
cat /tmp/ping.log | sed 's/^/║ /'
else
echo "║ Ping failed."
fi
print_separator
# Извлекаем DNS из конфига WireGuard
WG_DNS=$(awk -F= '/DNS/ {print $2}' /etc/wireguard/wg0.conf | xargs)
# Настройка DNS
print_stage "Configuring DNS"
if [ -n "$WG_DNS" ]; then
echo "║ Using DNS from WireGuard config: $WG_DNS"
echo "nameserver $WG_DNS" > /etc/resolv.conf
if [ $? -eq 0 ]; then
print_result "DNS configured using WireGuard settings." "created"
else
print_result "Failed to configure DNS!"
exit 1
fi
else
echo "║ Using fallback DNS: 1.1.1.1, 8.8.8.8"
echo "nameserver 1.1.1.1" > /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
if [ $? -eq 0 ]; then
print_result "DNS configured using fallback settings." "created"
else
print_result "Failed to configure DNS!"
exit 1
fi
fi
# Проверка включения WireGuard
print_stage "Checking WireGuard status"
if [ "$WG_ENABLED" = "true" ]; then
echo "║ WireGuard is enabled. Starting..."
# Запуск WireGuard
wg-quick up wg0 2>&1 | tee /tmp/wg.log
wg_status=$?
if [ $wg_status -eq 0 ]; then
echo "║ WireGuard started successfully."
print_result "WireGuard interface activated successfully."
else
echo "║ WireGuard failed to start. Logs:"
cat /tmp/wg.log | sed 's/^/║ /'
print_result "Failed to start WireGuard interface!"
exit 1
fi
# Проверка маршрутизации после запуска WireGuard
print_stage "Routing table AFTER WireGuard"
ip route | sed 's/^/║ /'
print_separator
# Проверка сети ПОСЛЕ включения WireGuard
print_stage "Testing connectivity AFTER WireGuard"
echo "║ Pinging 1.1.1.1..."
ping -c 4 1.1.1.1 > /tmp/ping.log 2>&1
if [ $? -eq 0 ]; then
echo "║ Ping successful."
cat /tmp/ping.log | sed 's/^/║ /'
else
echo "║ Ping failed."
fi
print_separator
else
echo "║ WireGuard is disabled. Skipping..."
print_result "WireGuard is disabled in configuration."
fi
# Проверка DNS
print_stage "Testing DNS"
nslookup api.ipify.org > /tmp/dns.log 2>&1
if [ $? -eq 0 ]; then
echo "║ DNS lookup successful."
cat /tmp/dns.log | sed 's/^/║ /'
else
echo "║ DNS lookup failed."
fi
print_separator
# Проверка подключения через icanhazip.com
print_stage "Testing external connectivity (icanhazip.com)"
echo "║ Fetching external IP..."
curl -s https://icanhazip.com > /tmp/curl.log 2>&1
if [ $? -eq 0 ]; then
echo "║ Connection successful."
echo "║ External IP: $(cat /tmp/curl.log)"
else
echo "║ Connection failed."
fi
print_separator
# Запуск приложения
print_stage "Starting application"
echo "║ Application is starting..."
exec node src/index.js