44 Commits

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
29 changed files with 9152 additions and 6973 deletions

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, с возможностью работы с криптовалютами и традиционными средствами. Проект ориентирован на пользователей, которые ценят удобство, безопасность и скорость совершения покупок. Для администраторов — это мощный инструмент для управления товаром, пользователями и финансовыми потоками магазина.

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,217546867
- 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:

11559
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@
"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",
@@ -20,7 +19,7 @@
"node-telegram-bot-api": "^0.64.0",
"sqlite3": "^5.1.6",
"tiny-secp256k1": "^2.2.3",
"tronweb": "^5.3.2"
"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';
@@ -109,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 (
@@ -165,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,
@@ -177,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 (
@@ -222,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');

View File

@@ -124,6 +124,41 @@ export default class AdminLocationHandler {
return true;
}
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;
@@ -182,7 +217,7 @@ 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' }]
]
};
@@ -227,7 +262,7 @@ 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_location_${loc.country}_${loc.city}_${loc.district}`
callback_data: `confirm_delete_location_${loc.id}` // Используем ID локации вместо строки
}])
};
@@ -254,23 +289,27 @@ export default class AdminLocationHandler {
}
const chatId = callbackQuery.message.chat.id;
const [country, city, district] = callbackQuery.data
.replace('confirm_delete_location_', '')
.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 bot.editMessageText(
`✅ Location deleted successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
`✅ Location deleted successfully!\n\nCountry: ${location.country}\nCity: ${location.city}\nDistrict: ${location.district}`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -327,4 +366,4 @@ export default class AdminLocationHandler {
userStates.delete(chatId);
}
}
}

View File

@@ -327,33 +327,31 @@ export default class AdminProductHandler {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
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 CategoryService.getSubcategoriesByCategoryId(categoryId);
const category = await CategoryService.getCategoryById(categoryId);
const location = await LocationService.getLocationById(locationId);
// Получаем товары для выбранной категории
const products = await ProductService.getProductsByCategoryId(categoryId);
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}`
...products.map(prod => [{
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
callback_data: `view_product_${prod.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}`
}]
[{ text: ' Add Product', callback_data: `add_product_${locationId}_${categoryId}` }],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
]
};
await bot.editMessageText(
`📦 Category: ${category.name}\nSelect or add subcategory:`,
`📦 Category: ${category.name}\nSelect or add product:`,
{
chat_id: chatId,
message_id: messageId,
@@ -362,7 +360,7 @@ export default class AdminProductHandler {
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
@@ -517,40 +515,43 @@ export default class AdminProductHandler {
}
}
static async handleSubcategorySelection(callbackQuery) {
static async handleCategorySelection(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
const state = userStates.get(chatId)
for (const msgId of state?.msgToDelete || []) {
try {
await bot.deleteMessage(chatId, msgId);
} catch (e) {
// ignore if can't delete
}
}
userStates.delete(chatId);
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
try {
const {text, markup} = await this.viewProductsPage(locationId, categoryId, subcategoryId, 0);
const category = await CategoryService.getCategoryById(categoryId);
const location = await LocationService.getLocationById(locationId);
// Получаем товары для выбранной категории
const products = await ProductService.getProductsByCategoryId(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: ' Add Product', callback_data: `add_product_${locationId}_${categoryId}` }],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] // Исправлено на категорию
]
};
await bot.editMessageText(
text,
`📦 Category: ${category.name}\nSelect or add product:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: markup
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
console.error('Error in handleCategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
@@ -581,11 +582,11 @@ export default class AdminProductHandler {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_');
const [locationId, categoryId] = callbackQuery.data.replace('add_product_', '').split('_');
try {
const sampleProducts = [
{
@@ -600,17 +601,16 @@ export default class AdminProductHandler {
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`;
const message = `To add product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
userStates.set(chatId, {
action: 'import_products',
locationId,
categoryId,
subcategoryId
categoryId
});
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
@@ -619,7 +619,7 @@ export default class AdminProductHandler {
inline_keyboard: [[
{
text: '❌ Cancel',
callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}`
callback_data: `prod_category_${locationId}_${categoryId}`
}
]]
}
@@ -633,40 +633,40 @@ export default class AdminProductHandler {
static async handleProductImport(msg) {
const chatId = msg.chat.id;
const state = userStates.get(chatId);
if (!state || state.action !== 'import_products') {
return false;
}
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
try {
let products;
let jsonContent;
// Handle file upload
if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) {
await bot.sendMessage(chatId, 'Please upload a .json file.');
return true;
}
const file = await bot.getFile(msg.document.file_id);
const fileContent = await bot.downloadFile(file.file_id, '.');
jsonContent = await fs.readFile(fileContent, 'utf8');
await fs.rm(fileContent);
} else if (msg.text) {
jsonContent = msg.text;
} else {
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true;
}
try {
products = JSON.parse(jsonContent);
if (!Array.isArray(products)) {
@@ -676,28 +676,28 @@ export default class AdminProductHandler {
await 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
location_id, category_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,
state.locationId, state.categoryId,
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 bot.sendMessage(
chatId,
`✅ Successfully imported ${products.length} products!`,
@@ -706,96 +706,95 @@ export default class AdminProductHandler {
inline_keyboard: [[
{
text: '« Back to Products',
callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}`
callback_data: `prod_category_${state.locationId}_${state.categoryId}`
}
]]
}
}
);
userStates.delete(chatId);
} catch (error) {
console.error('Error importing products:', error);
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
await db.runAsync('ROLLBACK');
}
return true;
}
static async handleProductEditImport(msg) {
const chatId = msg.chat.id;
const state = userStates.get(chatId);
if (!state || state.action !== 'edit_product') {
return false;
}
if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
try {
let product;
let jsonContent;
// Handle file upload
if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) {
await bot.sendMessage(chatId, 'Please upload a .json file.');
return true;
}
const file = await bot.getFile(msg.document.file_id);
const fileContent = await bot.downloadFile(file.file_id, '.');
jsonContent = await fs.readFile(fileContent, 'utf8');
await fs.rm(fileContent);
} else if (msg.text) {
jsonContent = msg.text;
} else {
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true;
}
try {
product = JSON.parse(jsonContent);
} catch (e) {
await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
return true;
}
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync(
`UPDATE products SET
location_id = ?,
category_id = ?,
subcategory_id = ?,
name = ?,
price = ?,
description = ?,
private_data = ?,
quantity_in_stock = ?,
photo_url = ?,
hidden_photo_url = ?,
hidden_coordinates = ?,
hidden_description = ?
WHERE
id = ?
`,
location_id = ?,
category_id = ?,
name = ?,
price = ?,
description = ?,
private_data = ?,
quantity_in_stock = ?,
photo_url = ?,
hidden_photo_url = ?,
hidden_coordinates = ?,
hidden_description = ?
WHERE
id = ?
`,
[
state.locationId, state.categoryId, state.subcategoryId,
state.locationId, state.categoryId,
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, state.productId
]
);
await db.runAsync('COMMIT');
await bot.sendMessage(
chatId,
`✅ Successfully edited!`,
@@ -804,20 +803,20 @@ export default class AdminProductHandler {
inline_keyboard: [[
{
text: '« Back to Products',
callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}`
callback_data: `prod_category_${state.locationId}_${state.categoryId}`
}
]]
}
}
);
userStates.delete(chatId);
} catch (error) {
console.error('Error importing products:', error);
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
await db.runAsync('ROLLBACK');
}
return true;
}
@@ -825,41 +824,40 @@ export default class AdminProductHandler {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('view_product_', '');
try {
const product = await ProductService.getDetailedProductById(productId)
const product = await ProductService.getDetailedProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const location = await LocationService.getLocationById(product.location_id);
if (!location) {
throw new Error('Location not found');
}
const message = `
📦 Product Details:
Name: ${product.name}
Price: $${product.price}
Description: ${product.description}
Stock: ${product.quantity_in_stock}
Location: ${location.country}, ${location.city}, ${location.district}
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
📦 Product Details:
Name: ${product.name}
Price: $${product.price}
Description: ${product.description}
Stock: ${product.quantity_in_stock}
Location: ${location.country}, ${location.city}, ${location.district}
Category: ${product.category_name}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
const keyboard = {
inline_keyboard: [
[
@@ -868,14 +866,14 @@ Coordinates: ${product.hidden_coordinates}
],
[{
text: '« Back',
callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}`
callback_data: `prod_category_${product.location_id}_${product.category_id}` // Исправлено на категорию
}]
]
};
let photoMessage;
let hiddenPhotoMessage;
// Send product photos
if (product.photo_url) {
try {
@@ -891,11 +889,11 @@ Coordinates: ${product.hidden_coordinates}
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
}
}
userStates.set(chatId, {
msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id]
})
await bot.deleteMessage(chatId, messageId);
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
} catch (error) {
@@ -908,45 +906,43 @@ Coordinates: ${product.hidden_coordinates}
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('edit_product_', '');
try {
const product = await ProductService.getDetailedProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const locationId = product.location_id;
const categoryId = product.category_id;
const subcategoryId = product.subcategory_id;
const sampleProduct = {
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"
}
name: product.name,
price: product.price,
description: product.description,
private_data: product.private_data,
quantity_in_stock: product.quantity_in_stock,
photo_url: product.photo_url,
hidden_photo_url: product.hidden_photo_url,
hidden_coordinates: product.hidden_coordinates,
hidden_description: product.hidden_description
};
const jsonExample = JSON.stringify(sampleProduct, null, 2);
const message = `To edit product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
userStates.set(chatId, {
action: 'edit_product',
locationId,
categoryId,
subcategoryId,
productId
});
await bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
@@ -955,13 +951,13 @@ Coordinates: ${product.hidden_coordinates}
inline_keyboard: [[
{
text: '❌ Cancel',
callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}`
callback_data: `prod_category_${locationId}_${categoryId}` // Возвращаемся к списку товаров в категории
}
]]
}
});
} catch (error) {
console.error('Error in handleViewProduct:', error);
console.error('Error in handleProductEdit:', error);
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
@@ -1012,36 +1008,36 @@ Coordinates: ${product.hidden_coordinates}
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const productId = callbackQuery.data.replace('confirm_delete_product_', '');
const chatId = callbackQuery.message.chat.id;
try {
const product = await ProductService.getDetailedProductById(productId);
if (!product) {
throw new Error('Product not found');
}
const locationId = product.location_id;
const categoryId = product.category_id;
try {
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync('DELETE FROM products WHERE id=?', [productId.toString()]);
await db.runAsync('COMMIT');
} catch (e) {
await db.runAsync("ROLLBACK");
await db.runAsync('ROLLBACK');
console.error('Error deleting product:', e);
throw e;
}
const keyboard = {
inline_keyboard: [
[{
text: '« Back',
callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}`
}]
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] // Возвращаемся к списку товаров в категории
]
};
await bot.editMessageText(
`✅ Product has been successfully deleted.`,
{

View File

@@ -1,7 +1,11 @@
// 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 {
@@ -11,90 +15,142 @@ export default class AdminUserHandler {
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
` );
// Calculate general statistics
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 totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
const bonusBalance = users.reduce((sum, u) => sum + (u.bonus_balance || 0), 0);
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
// Create statistics message
// Рассчитываем общий баланс активных и архивных кошельков
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 += `💰 Bonus Balance: $${bonusBalance.toFixed(2)}\n`;
message += `💰 Total Balance: $${(totalBalance + bonusBalance).toFixed(2)}\n`;
message += `🛍 Total Purchases: ${totalPurchases}`;
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) {
return null
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]);
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.'};
return { text: 'No users registered yet.' };
}
if ((users.length === 0) && (page > 0)) {
return await this.viewUserPage(page - 1);
}
const statistics = await this.calculateStatistics()
// 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: users.map(user => [{
text: `ID: ${user.telegram_id} | Nickname: ${user.username ? "@" + user.username : "None"} | Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)}`,
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}
{ 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.'}
return { text: 'Error loading user list. Please try again.' };
}
}
@@ -131,63 +187,79 @@ export default class AdminUserHandler {
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]);
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]);
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}
- Active Wallets: ${detailedUser.crypto_wallet_count}
- Bonus Balance: $${user.bonus_balance || 0}
- Total Balance: $${(user.total_balance || 0) + (user.bonus_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(detailedUser.created_at).toLocaleString()}
`;
👤 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: [
[
@@ -201,7 +273,7 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}
[{text: '« Back to User List', callback_data: `list_users_0`}]
]
};
await bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,

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,6 +1,9 @@
// 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) {
@@ -30,43 +33,51 @@ export default class UserHandler {
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}
├ Bonus Balance: $${userStats.bonus_balance || 0}
└ Total Balance: $${(userStats.total_balance || 0) + (userStats.bonus_balance || 0)}
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
`;
👤 *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

View File

@@ -123,42 +123,46 @@ export default class UserProductHandler {
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) {
throw new Error('Location not found');
}
const categories = await CategoryService.getCategoriesByLocationId(location.id);
if (categories.length === 0) {
// Если локация не найдена, вернуть пользователя к предыдущему шагу
await bot.editMessageText(
'No products available in this location yet.',
'Location not found. Returning to previous menu.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}
{ 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 to Districts', callback_data: `shop_city_${country}_${city}`}]
[{ text: '« Back', callback_data: `shop_city_${country}_${city}` }]
]
};
await bot.editMessageText(
'📦 Select category:',
{
@@ -177,54 +181,75 @@ export default class UserProductHandler {
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 CategoryService.getSubcategoriesByCategoryId(categoryId);
const location = await LocationService.getLocationById(locationId);
if (subcategories.length === 0) {
await bot.editMessageText(
'No products available in this category yet.',
// Удаляем текущее сообщение
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.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{
text: '« Back to Categories',
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
}
{ text: '« Back', callback_data: `shop_district_${state.location}` }
]]
}
}
);
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}`
}]
]
inline_keyboard: products.map(product => [
{
text: `${product.name} - $${product.price}`,
callback_data: `shop_product_${product.id}`
}
])
};
await bot.editMessageText(
'📦 Select subcategory:',
// Добавляем кнопку "Назад"
keyboard.inline_keyboard.push([
{ text: '« Back', callback_data: `shop_district_${state.location}` }
]);
// Отправляем сообщение с товарами
await bot.sendMessage(
chatId,
'Select a product:',
{
chat_id: chatId,
message_id: messageId,
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 subcategories. Please try again.');
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
@@ -293,75 +318,83 @@ export default class UserProductHandler {
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');
}
// Delete the previous message
// Удаляем предыдущее сообщение
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}
Subcategory: ${product.subcategory_name}
`;
let photoMessageId = null;
// First send the photo if it exists
📦 ${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'});
photoMessage = await bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
} catch (e) {
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' });
}
}
const keyboard = {
inline_keyboard: [
[{text: '🛒 Buy Now', callback_data: `buy_product_${productId}`}],
[{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
[
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: {} // Initially disabled as quantity starts at 1
callback_game: {} // Изначально отключено, так как количество начинается с 1
},
{text: '1', callback_data: 'current_quantity'},
{ 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
callback_game: product.quantity_in_stock <= 1 ? {} : null // Отключено, если остаток 1 или меньше
}
],
[{
text: `« Back to ${product.subcategory_name}`,
callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}`
}]
[{ text: `« Back ${product.category_name}`, callback_data: `shop_category_${product.location_id}_${product.category_id}` }] // Возврат к категории
]
};
// Then send the message with controls
await bot.sendMessage(chatId, message, {
// Отправляем сообщение с кнопками
const productMessage = await bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'HTML'
});
// Store the current quantity and photo message ID in user state
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
userStates.set(chatId, {
action: 'buying_product',
productId,
quantity: 1,
photoMessageId
photoMessageId: photoMessage ? photoMessage.message_id : null,
productMessageId: productMessage.message_id,
location: state?.location // Сохраняем информацию о локации
});
} catch (error) {
console.error('Error in handleProductSelection:', error);
@@ -494,30 +527,63 @@ Subcategory: ${product.subcategory_name}
const telegramId = callbackQuery.from.id;
const productId = callbackQuery.data.replace('buy_product_', '');
const state = userStates.get(chatId);
try {
const user = await UserService.getUserByTelegramId(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 quantity = state?.quantity || 1;
const totalPrice = product.price * quantity;
// Get user's crypto wallets with balances
// Получение баланса пользователя
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]);
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await bot.sendMessage(
chatId,
@@ -525,22 +591,23 @@ Subcategory: ${product.subcategory_name}
{
reply_markup: {
inline_keyboard: [[
{text: ' Add Wallet', callback_data: 'add_wallet'}
{ 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}`}]
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }] // Кнопка "Back"
]
};
await bot.editMessageText(
// Отправка сообщения с кнопками
const purchaseMessage = await bot.editMessageText(
`🛒 Purchase Summary:\n\n` +
`Product: ${product.name}\n` +
`Quantity: ${quantity}\n` +
@@ -551,6 +618,13 @@ Subcategory: ${product.subcategory_name}
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.');
@@ -562,23 +636,23 @@ Subcategory: ${product.subcategory_name}
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)
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`, {
@@ -587,46 +661,74 @@ Subcategory: ${product.subcategory_name}
});
return;
}
await PurchaseService.createPurchase(user.id, product.id, walletType, quantity, totalPrice)
// Проверка наличия товара
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'});
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'})
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
}
}
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}
`;
📦 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: "I've got it!", callback_data: "Asdasdasd"}],
[{text: "Contact support", url: config.SUPPORT_LINK}]
[{ 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.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 handleBuyProduct:', error);
console.error('Error in handlePay:', error);
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}

View File

@@ -1,160 +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 bot from "../../context/bot.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 || 0) * limit;
const previousPage = page > 0 ? page - 1 : 0;
const nextPage = page + 1;
const limit = 10; // Количество покупок на странице
const offset = page * limit;
// Получаем покупки пользователя с учетом пагинации
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
if ((purchases.length === 0) && (page == 0)) {
// Получаем общее количество покупок пользователя
const totalPurchases = await PurchaseService.getTotalPurchasesByUserId(userId);
// Вычисляем общее количество страниц
const totalPages = Math.ceil(totalPurchases / limit);
// Если покупок нет, возвращаем сообщение о пустом архиве
if (totalPurchases === 0) {
return {
text: 'You haven\'t made any purchases yet.',
markup: [[
{text: '🛍 Browse Products', callback_data: 'shop_start'}
]]
}
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, previousPage);
// Если покупок нет на текущей странице, но это не первая страница, переходим на предыдущую страницу
if (purchases.length === 0 && page > 0) {
return await this.viewPurchasePage(userId, page - 1);
}
const keyboard = {
inline_keyboard: [
...purchases.map(item => [{
text: `${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
// Добавляем иконку статуса покупки
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 // Скрываем кнопку "Вперед", если на последней странице
}
]
]
};
keyboard.inline_keyboard.push([
{text: `«`, callback_data: `list_purchases_${previousPage}`},
{text: `»`, callback_data: `list_purchases_${nextPage}`},
]);
keyboard.inline_keyboard.push([
{text: '🛍 Browse Products', callback_data: 'shop_start'}
]);
return {
text: `📦 Select purchase to view detailed information:`,
text: `📦 Select purchase to view detailed information (Page ${page + 1} of ${totalPages}):`,
markup: keyboard
}
};
} catch (error) {
console.error('Error in showPurchases:', error);
return {text: 'Error loading purchase history. Please try again.'};
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 = callbackQuery.data.replace('list_purchases_', '');
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;
}
const {text, markup} = await this.viewPurchasePage(user.id, parseInt(page));
// Удаляем сообщение с 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',
parse_mode: 'Markdown'
});
// Удаляем состояние пользователя
userStates.delete(chatId);
} catch (e) {
return;
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'});
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 handleSubcategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
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_', '');
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
await bot.sendMessage(chatId, "No such purchase");
return;
}
const product = await ProductService.getProductById(purchase.product_id)
if (!product) {
await bot.sendMessage(chatId, "No such product");
return;
}
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'})
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.');
}
}
const message = `
📦 Purchase Details:
Name: ${purchase.product_name}
Quantity: ${purchase.quantity}
Total: $${purchase.total_price}
Location: ${purchase.country}, ${purchase.city}
Payment: ${purchase.wallet_type}
Date: ${new Date(purchase.purchase_date).toLocaleString()}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
const keyboard = {
inline_keyboard: [
[{text: "I've got it!", callback_data: "Asdasdasd"}],
[{text: "Contact support", url: config.SUPPORT_LINK}]
]
};
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
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

@@ -1,100 +1,322 @@
// userWalletsHandler.js
import db from '../../config/database.js';
import WalletGenerator from '../../utils/walletGenerator.js';
import WalletService from '../../utils/walletService.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;
}
const user = await UserService.getUserByTelegramId(telegramId.toString());
// 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;
}
if (!user) {
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
message += `📊 *Total Balance:* $${totalUsdValue.toFixed(2)}\n`;
} else {
message = 'You don\'t have any active wallets yet.';
}
// Пересчитываем баланс перед отображением
await UserService.recalculateUserBalanceByTelegramId(telegramId);
// 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 updatedUser = await UserService.getUserByTelegramId(telegramId.toString());
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' }]
]
};
// Получаем активные криптокошельки
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address, balance
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [updatedUser.id]);
// 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' }
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'
});
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.');
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' }
]]
}
});
}
}
@@ -103,8 +325,7 @@ export default class UserWalletsHandler {
const cryptoOptions = [
['BTC', 'ETH', 'LTC'],
['USDT TRC-20', 'USDD TRC-20'],
['USDT ERC-20', 'USDC ERC-20']
['USDT', 'USDC']
];
const keyboard = {
@@ -133,58 +354,56 @@ export default class UserWalletsHandler {
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 {
// Generate new wallets
const mnemonic = await WalletGenerator.generateMnemonic();
const wallets = await WalletGenerator.generateWallets(mnemonic);
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, telegramId);
// 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]
[user.id, walletType]
);
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]
[`${walletType}_${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);
// Создаем новый кошелек с использованием 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`;
@@ -195,7 +414,7 @@ export default class UserWalletsHandler {
}
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,
@@ -206,7 +425,7 @@ export default class UserWalletsHandler {
]]
}
});
await db.runAsync('COMMIT');
} catch (error) {
await db.runAsync('ROLLBACK');
@@ -260,26 +479,23 @@ export default class UserWalletsHandler {
return;
}
let message = '💰 *Available Wallets:*\n\n';
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 walletService = new WalletService(
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 === 'TRON')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
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('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`;
@@ -366,7 +582,7 @@ export default class UserWalletsHandler {
static async handleViewArchivedWallets(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
try {
const user = await UserService.getUserByTelegramId(telegramId.toString());
@@ -377,14 +593,14 @@ export default class UserWalletsHandler {
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.',
@@ -400,11 +616,11 @@ export default class UserWalletsHandler {
);
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]) {
@@ -415,27 +631,26 @@ export default class UserWalletsHandler {
timestamp: parseInt(timestamp)
});
}
// Create wallet service instance
const walletService = new WalletService(
const walletUtilsInstance = new WalletUtils(
groupedWallets['BTC']?.[0]?.address,
groupedWallets['LTC']?.[0]?.address,
groupedWallets['TRON']?.[0]?.address,
groupedWallets['ETH']?.[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 walletService.getAllBalances();
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)
@@ -445,7 +660,7 @@ export default class UserWalletsHandler {
const date = new Date(wallet.timestamp);
let balance = 0;
let usdValue = 0;
// Get balance based on wallet type
switch (baseType) {
case 'BTC':
@@ -457,33 +672,31 @@ export default class UserWalletsHandler {
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;
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,
@@ -500,44 +713,6 @@ export default class UserWalletsHandler {
}
}
static async handleRefreshBalance(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
await 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 bot.deleteMessage(chatId, messageId);
} catch (error) {
console.error('Error in handleRefreshBalance:', error);
await 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' }
]]
}
}
);
}
}
static async handleBackToBalance(callbackQuery) {
await this.showBalance({
chat: { id: callbackQuery.message.chat.id },
@@ -548,13 +723,11 @@ export default class UserWalletsHandler {
// Helper methods
static getBaseWalletType(walletType) {
if (walletType.includes('TRC-20')) return 'TRON';
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
static 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;
@@ -563,11 +736,16 @@ export default class UserWalletsHandler {
}
static getNetworkName(walletType) {
if (walletType.includes('TRC-20')) return 'Tron Network (TRC-20)';
if (walletType.includes('ERC-20')) return 'Ethereum Network (ERC-20)';
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

@@ -12,6 +12,7 @@ import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocation
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) => {
@@ -69,11 +70,6 @@ bot.on('message', async (msg) => {
return;
}
// Check for admin subcategory input
if (await adminProductHandler.handleSubcategoryInput(msg)) {
return;
}
// Check for product import
if (await adminProductHandler.handleProductImport(msg)) {
return;
@@ -134,6 +130,11 @@ bot.on('message', async (msg) => {
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');
@@ -217,9 +218,6 @@ bot.on('callback_query', async (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);
@@ -241,6 +239,9 @@ bot.on('callback_query', async (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') {
@@ -249,6 +250,9 @@ bot.on('callback_query', async (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);
@@ -282,12 +286,6 @@ bot.on('callback_query', async (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('list_products_')) {
logDebug(action, 'handleProductListPage');
await adminProductHandler.handleProductListPage(callbackQuery);
@@ -344,6 +342,23 @@ bot.on('callback_query', async (callbackQuery) => {
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);
@@ -352,6 +367,13 @@ bot.on('callback_query', async (callbackQuery) => {
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');
@@ -365,4 +387,4 @@ process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
});
console.log('Bot is running...');
console.log('Bot is running...');

View File

@@ -1,9 +1,10 @@
// Wallet.js
import db from "../config/database.js";
import WalletService from "../utils/walletService.js";
import WalletUtils from "../utils/walletUtils.js";
export default class Wallet {
static getBaseWalletType(walletType) {
if (walletType.includes('TRC-20')) return 'TRON';
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
@@ -14,52 +15,47 @@ export default class Wallet {
`, [userId]);
const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address;
const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address;
const tronAddress = archivedWallets.find(w => w.wallet_type.startsWith('TRON'))?.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,
tron: tronAddress,
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 tronAddress = activeWallets.find(w => w.wallet_type === 'TRON')?.address;
const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address;
return {
btc: btcAddress,
ltc: ltcAddress,
tron: tronAddress,
eth: ethAddress,
wallets: activeWallets
}
};
}
static async getActiveWalletsBalance(userId) {
const activeWallets = await this.getActiveWallets(userId);
const walletService = new WalletService(
const walletUtilsInstance = new WalletUtils(
activeWallets.btc,
activeWallets.ltc,
activeWallets.tron,
activeWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
const balances = await walletUtilsInstance.getAllBalances();
let totalUsdBalance = 0;
@@ -67,7 +63,6 @@ export default class Wallet {
const baseType = this.getBaseWalletType(type);
const wallet = activeWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
@@ -75,9 +70,7 @@ export default class Wallet {
continue;
}
if (wallet) {
totalUsdBalance += balance.usdValue;
}
totalUsdBalance += balance.usdValue;
}
return totalUsdBalance;
@@ -86,16 +79,15 @@ export default class Wallet {
static async getArchivedWalletsBalance(userId) {
const archiveWallets = await this.getArchivedWallets(userId);
const walletService = new WalletService(
const walletUtilsInstance = new WalletUtils(
archiveWallets.btc,
archiveWallets.ltc,
archiveWallets.tron,
archiveWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
const balances = await walletUtilsInstance.getAllBalances();
let totalUsdBalance = 0;
@@ -103,7 +95,6 @@ export default class Wallet {
const baseType = this.getBaseWalletType(type);
const wallet = archiveWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type.startsWith('TRON')) ||
(type.includes('ERC-20') && w.wallet_type.startsWith('ETH'))
);
@@ -111,9 +102,7 @@ export default class Wallet {
continue;
}
if (wallet) {
totalUsdBalance += balance.usdValue;
}
totalUsdBalance += balance.usdValue;
}
return totalUsdBalance;

View File

@@ -2,10 +2,16 @@ import db from "../config/database.js";
class CategoryService {
static async getCategoriesByLocationId(locationId) {
return await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[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) {
@@ -16,7 +22,16 @@ class CategoryService {
}
static async getCategoryById(categoryId) {
return await db.getAsync('SELECT id, name FROM categories WHERE id = ?', [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) {

View File

@@ -20,17 +20,29 @@ class LocationService {
}
static async getLocation(country, city, district) {
return await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[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) {
return await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[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');
}
}
}

View File

@@ -1,3 +1,5 @@
// productService.js
import db from "../config/database.js";
class ProductService {
@@ -12,25 +14,46 @@ class ProductService {
static async getDetailedProductById(productId) {
return 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 = ?`,
`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, subcategoryId) {
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 subcategory_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[locationId, categoryId, subcategoryId]
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');
}
}
}

View File

@@ -1,4 +1,8 @@
// 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) {
@@ -28,31 +32,128 @@ class PurchaseService {
static async getPurchaseById(purchaseId) {
try {
return await db.getAsync(`
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.id = ?
`, [purchaseId]);
return await db.getAsync(
`SELECT * FROM purchases WHERE id = ?`,
[purchaseId]
);
} catch (error) {
console.error('Error get purchase:', error);
console.error('Error getting purchase by ID:', error);
throw error;
}
}
static async createPurchase(userId, productId, walletType, quantity, totalPrice) {
await db.runAsync(
'INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price) VALUES (?, ?, ?, ?, ?, ?)',
[userId, productId, walletType, "null", 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');
}
}
}

View File

@@ -1,7 +1,71 @@
// 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(
@@ -16,9 +80,10 @@ class UserService {
static async getUserByTelegramId(telegramId) {
try {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
return await db.getAsync(
'SELECT * FROM users WHERE telegram_id = ?',
[String(telegramId)]
[normalizedTelegramId]
);
} catch (error) {
console.error('Error getting user:', error);
@@ -28,104 +93,97 @@ class UserService {
static async getDetailedUserByTelegramId(telegramId) {
try {
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
return await db.getAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as purchase_count,
COALESCE(SUM(p.total_price), 0) as total_spent,
(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 '%_%'
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%_%'
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
`, [telegramId.toString()]);
`, [normalizedTelegramId]);
} catch (error) {
console.error('Error getting user stats:', error);
throw error;
}
}
static async createUser(userData) {
try {
const existingUser = await this.getUserByTelegramId(userData?.telegram_id);
if (existingUser) {
return existingUser.id;
}
const fields = Object.keys(userData);
const values = [];
for (const field of fields) {
values.push(userData[field]);
}
const marks = [];
for (let i = 0; i < fields.length; i++) {
marks.push("?");
}
const query = [
`INSERT INTO users (${fields.join(', ')})`,
`VALUES (${marks.join(', ')})`
].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 updateUser(userId, newUserData) {}
static async deleteUser() {}
static async recalculateUserBalanceByTelegramId(telegramId) {
const user = await this.getUserByTelegramId(telegramId);
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
const user = await this.getUserByTelegramId(normalizedTelegramId);
if (!user) {
return;
}
const archivedBalance = await Wallet.getArchivedWalletsBalance(user.id);
const activeBalance = await Wallet.getActiveWalletsBalance(user.id);
const purchases = await db.getAsync(
`SELECT SUM(total_price) as total_sum FROM purchases WHERE user_id = ?`,
[user.id]
);
const userTotalBalance = (activeBalance + archivedBalance) - (purchases?.total_sum || 0);
await db.runAsync(`UPDATE users SET total_balance = ? WHERE id = ?`, [userTotalBalance, user.id]);
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, telegramId.toString()]
[country, city, district, normalizedTelegramId]
);
}
static async updateUserStatus(telegramId, status) {
// statuses
// 0 - active
// 1 - deleted
// 2 - blocked
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, telegramId.toString()]);
await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, normalizedTelegramId]);
// Commit transaction
await db.runAsync('COMMIT');
@@ -135,6 +193,21 @@ class UserService {
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

@@ -1,3 +1,308 @@
class WalletService {
// 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