diff --git a/package.json b/package.json index ede318f..8f0744f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "node-telegram-bot-api": "^0.64.0", "sqlite3": "^5.1.6", "tiny-secp256k1": "^2.2.3", + "csv-writer": "^1.6.0", "tronweb": "^5.3.2" }, "devDependencies": { diff --git a/src/handlers/adminHandlers/adminProductHandler.js b/src/handlers/adminHandlers/adminProductHandler.js index e5b5e44..ad1ebaa 100644 --- a/src/handlers/adminHandlers/adminProductHandler.js +++ b/src/handlers/adminHandlers/adminProductHandler.js @@ -538,7 +538,7 @@ export default class AdminProductHandler { 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}` }] + [{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] // Исправлено на категорию ] }; @@ -824,40 +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} - -🔒 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: [ [ @@ -866,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 { @@ -889,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) { diff --git a/src/handlers/adminHandlers/adminWalletsHandler.js b/src/handlers/adminHandlers/adminWalletsHandler.js new file mode 100644 index 0000000..bd223f4 --- /dev/null +++ b/src/handlers/adminHandlers/adminWalletsHandler.js @@ -0,0 +1,184 @@ +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 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: 'Tether (USDT)', callback_data: 'wallet_type_USDT' }, + { text: 'Ethereum (ETH)', callback_data: 'wallet_type_ETH' } + ], + [ + { text: 'TRON (TRX)', callback_data: 'wallet_type_TRON' } + ] + ] + } + }; + + 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 { + // Получаем все кошельки выбранного типа + 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 = 50; + const startIndex = page * pageSize; + const endIndex = startIndex + pageSize; + const walletsPage = wallets.slice(startIndex, endIndex); + + // Формируем список кошельков с балансами + const walletList = walletsPage.map(wallet => `${wallet.address} - ${wallet.balance}`).join('\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.editMessageText( + `Total Balance for ${walletType}: $${totalBalance.toFixed(2)}\n\nWallets (${startIndex + 1}-${endIndex} of ${wallets.length}):\n${walletList}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } + + static async calculateTotalBalance(wallets) { + let totalBalance = 0; + + for (const wallet of wallets) { + const walletUtilsInstance = new WalletUtils( + wallet.wallet_type.startsWith('BTC') ? wallet.address : null, + wallet.wallet_type.startsWith('LTC') ? wallet.address : null, + wallet.wallet_type.startsWith('TRON') ? wallet.address : null, + wallet.wallet_type.startsWith('ETH') ? wallet.address : null, + null, // userId не нужен для админки + Date.now() - 30 * 24 * 60 * 60 * 1000 + ); + + const balances = await walletUtilsInstance.getAllBalances(); + const balance = balances[wallet.wallet_type] || { usdValue: 0 }; + totalBalance += balance.usdValue; + } + + return totalBalance; + } + + static async handlePagination(callbackQuery) { + const action = callbackQuery.data; + const chatId = callbackQuery.message.chat.id; + const [actionType, walletType, page] = action.split('_'); + + // Получаем все кошельки выбранного типа + const wallets = await WalletService.getWalletsByType(walletType); + + // Вычисляем суммарный баланс + const totalBalance = await this.calculateTotalBalance(wallets); + + // Отображаем страницу с учетом пагинации + await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, parseInt(page)); + } + + static async handleExportCSV(callbackQuery) { + const action = callbackQuery.data; + const chatId = callbackQuery.message.chat.id; + const walletType = action.split('_').pop(); + + try { + // Получаем все кошельки выбранного типа + const wallets = await WalletService.getWalletsByType(walletType); + + if (wallets.length === 0) { + await bot.sendMessage(chatId, `No wallets found for ${walletType}.`); + return; + } + + // Создаем CSV-файл + const csv = csvWriter.createObjectCsvWriter({ + path: `wallets_${walletType}.csv`, + header: [ + { id: 'address', title: 'Address' }, + { id: 'balance', title: 'Balance' } + ] + }); + + await csv.writeRecords(wallets); + + // Отправляем файл пользователю + await bot.sendDocument(chatId, fs.createReadStream(`wallets_${walletType}.csv`)); + + // Удаляем временный файл + fs.unlinkSync(`wallets_${walletType}.csv`); + } catch (error) { + console.error('Error exporting wallets to CSV:', error); + await bot.sendMessage(chatId, 'Failed to export wallets to CSV. Please try again later.'); + } + } + + static isAdmin(userId) { + return config.ADMIN_IDS.includes(userId.toString()); + } +} \ No newline at end of file diff --git a/src/handlers/userHandlers/userPurchaseHandler.js b/src/handlers/userHandlers/userPurchaseHandler.js index 1640887..953d802 100644 --- a/src/handlers/userHandlers/userPurchaseHandler.js +++ b/src/handlers/userHandlers/userPurchaseHandler.js @@ -1,4 +1,5 @@ import config from "../../config/config.js"; +import db from '../../config/database.js'; import PurchaseService from "../../services/purchaseService.js"; import UserService from "../../services/userService.js"; import LocationService from "../../services/locationService.js"; @@ -229,21 +230,41 @@ export default class UserPurchaseHandler { const purchaseId = callbackQuery.data.replace('confirm_received_', ''); try { - // Проверяем, подтверждена ли покупка уже + // Получаем данные покупки const purchase = await PurchaseService.getPurchaseById(purchaseId); if (!purchase) { await bot.sendMessage(chatId, "Purchase not found."); return; } - if (purchase.status === 'received') { - await bot.sendMessage(chatId, "This purchase has already been confirmed."); + // Получаем данные пользователя по user_id из покупки + const user = await UserService.getUserByUserId(purchase.user_id); + if (!user) { + await bot.sendMessage(chatId, "User not found for this purchase."); return; } + // Логируем данные для отладки + console.log('Purchase data:', purchase); + console.log('User data:', user); + // Обновляем статус покупки в базе данных 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 || 'unknown', // Тип кошелька (если не указан, то "unknown") + purchase.tx_hash || 'no_hash', // Хеш транзакции (если не указан, то "no_hash") + purchase.total_price, // Сумма транзакции + new Date().toISOString() // Дата создания транзакции + ]); + + console.log('Transaction added successfully'); // Логируем успешную вставку + // Отправляем уведомление администраторам const adminIds = config.ADMIN_IDS; // Используем массив ADMIN_IDS for (const adminId of adminIds) { @@ -255,7 +276,7 @@ export default class UserPurchaseHandler { // Удаляем сообщение с карточкой товара await bot.deleteMessage(chatId, messageId); - + // Удаляем Hidden Photo, если оно существует const state = userStates.get(chatId); if (state?.hiddenPhotoMessageId) { @@ -268,7 +289,7 @@ export default class UserPurchaseHandler { // Удаляем состояние пользователя userStates.delete(chatId); - + // Открываем список покупок для пользователя await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } }); } catch (error) { diff --git a/src/handlers/userHandlers/userWalletsHandler.js b/src/handlers/userHandlers/userWalletsHandler.js index 2ad8a76..21c0fc4 100644 --- a/src/handlers/userHandlers/userWalletsHandler.js +++ b/src/handlers/userHandlers/userWalletsHandler.js @@ -60,14 +60,14 @@ export default class UserWalletsHandler { } // Add total crypto balance - message += `💰 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`; + message += `📊 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`; // Add bonus balance message += `🎁 *Bonus Balance:* $${user.bonus_balance.toFixed(2)}\n`; // Add total balance - const totalBalance = user.bonus_balance + user.total_balance; - message += `📊 *Total Balance:* $${totalBalance.toFixed(2)}\n`; + const totalBalance = totalUsdValue + user.bonus_balance; + message += `💰 *Total Balance:* $${totalBalance.toFixed(2)}\n`; } else { message = 'You don\'t have any active wallets yet.'; } @@ -92,6 +92,11 @@ export default class UserWalletsHandler { ]); } + // Add "Transaction History" button + 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' @@ -101,6 +106,87 @@ export default class UserWalletsHandler { 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; diff --git a/src/index.js b/src/index.js index 0918fd1..5a91b35 100644 --- a/src/index.js +++ b/src/index.js @@ -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) => { @@ -129,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'); @@ -333,6 +339,20 @@ 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('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, 'handleWalletManagement'); + await adminWalletsHandler.handleWalletManagement(msg); + } // Dump manage else if (action === "export_database") { await adminDumpHandler.handleExportDatabase(callbackQuery); @@ -341,6 +361,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'); diff --git a/src/services/walletService.js b/src/services/walletService.js index 6f3f444..127afb1 100644 --- a/src/services/walletService.js +++ b/src/services/walletService.js @@ -18,6 +18,20 @@ class WalletService { throw new Error('Failed to fetch archived wallets count'); } } + // Добавляем метод для получения кошельков по типу + static async getWalletsByType(walletType) { + try { + const wallets = await db.allAsync( + `SELECT * FROM crypto_wallets WHERE wallet_type = ?`, + [walletType] + ); + + return wallets; + } catch (error) { + console.error('Error fetching wallets by type:', error); + throw new Error('Failed to fetch wallets by type'); + } +} } export default WalletService; \ No newline at end of file