diff --git a/src/config/database.js b/src/config/database.js index 6bd1071..57706ad 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -110,12 +110,23 @@ const initDb = async () => { address TEXT NOT NULL, derivation_path 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 ( diff --git a/src/handlers/adminHandlers/adminUserHandler.js b/src/handlers/adminHandlers/adminUserHandler.js index 9f11876..be6d53d 100644 --- a/src/handlers/adminHandlers/adminUserHandler.js +++ b/src/handlers/adminHandlers/adminUserHandler.js @@ -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 { @@ -146,25 +150,40 @@ export default class AdminUserHandler { // 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]); + + // Get wallet balances + const activeWalletsBalance = await WalletService.getActiveWalletsBalance(user.id); + const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(user.id); const message = ` 👤 User Profile: @@ -175,16 +194,20 @@ ID: ${telegramId} 📊 Activity: - Total Purchases: ${detailedUser.purchase_count} - Total Spent: $${detailedUser.total_spent || 0} - - Active Wallets: ${detailedUser.crypto_wallet_count} + - Active Wallets: ${detailedUser.crypto_wallet_count} ($${activeWalletsBalance.toFixed(2)}) + - Archived Wallets: ${detailedUser.archived_wallet_count} ($${archivedWalletsBalance.toFixed(2)}) - Bonus Balance: $${user.bonus_balance || 0} - - Total Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)} + - Total Balance: $${((user.total_balance || 0) + (user.bonus_balance || 0)).toFixed(2)} -💰 Recent Transactions: -${transactions.map(t => ` • ${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')} +💰 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: +🛍 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()} `; diff --git a/src/handlers/userHandlers/userHandler.js b/src/handlers/userHandlers/userHandler.js index d862aae..502dfd5 100644 --- a/src/handlers/userHandlers/userHandler.js +++ b/src/handlers/userHandlers/userHandler.js @@ -56,7 +56,7 @@ export default class UserHandler { ├ Active Wallets: ${userStats.crypto_wallet_count || 0} ├ Archived Wallets: ${userStats.archived_wallet_count || 0} ├ Bonus Balance: $${userStats.bonus_balance || 0} -└ Total Balance: $${(userStats.total_balance || 0) + (userStats.bonus_balance || 0)} +└ Available Balance: $${(userStats.total_balance || 0) + (userStats.bonus_balance || 0)} 📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()} `; diff --git a/src/handlers/userHandlers/userWalletsHandler.js b/src/handlers/userHandlers/userWalletsHandler.js index 3bf6328..a8a0f75 100644 --- a/src/handlers/userHandlers/userWalletsHandler.js +++ b/src/handlers/userHandlers/userWalletsHandler.js @@ -12,97 +12,107 @@ export default class UserWalletsHandler { const telegramId = msg.from.id; try { - const user = await UserService.getUserByTelegramId(telegramId.toString()); + const user = await UserService.getUserByTelegramId(telegramId.toString()); - if (!user) { - await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); - return; - } - - // Get active crypto wallets only - const cryptoWallets = await db.allAsync(` - SELECT wallet_type, address - FROM crypto_wallets - WHERE user_id = ? - ORDER BY wallet_type - `, [user.id]); - - let message = '💰 *Your Active Wallets:*\n\n'; - - if (cryptoWallets.length > 0) { - const 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 - user.id, - Date.now() - 30 * 24 * 60 * 60 * 1000 - ); - - const balances = await walletUtilsInstance.getAllBalances(); - let totalUsdValue = 0; - - // Show active wallets - 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; - } + if (!user) { + await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); + return; } - // Add total crypto balance - message += `📊 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`; + // Пересчитываем баланс перед отображением + await UserService.recalculateUserBalanceByTelegramId(telegramId); - // Add bonus balance - message += `🎁 *Bonus Balance:* $${user.bonus_balance.toFixed(2)}\n`; + // Получаем обновленные данные пользователя + const updatedUser = await UserService.getUserByTelegramId(telegramId.toString()); - // Add total balance - const totalBalance = totalUsdValue + user.bonus_balance; - message += `💰 *Total Balance:* $${totalBalance.toFixed(2)}\n`; - } else { - message = 'You don\'t have any active wallets yet.'; - } + // Получаем активные криптокошельки + const cryptoWallets = await db.allAsync(` + SELECT wallet_type, address + FROM crypto_wallets + WHERE user_id = ? + ORDER BY wallet_type + `, [updatedUser.id]); - // Check if user has archived wallets - const archivedCount = await WalletService.getArchivedWalletsCount(user); + let message = '💰 *Your Active Wallets:*\n\n'; - 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 (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, + Date.now() - 30 * 24 * 60 * 60 * 1000 + ); - // Add archived wallets button if any exist - if (archivedCount > 0) { - keyboard.inline_keyboard.splice(2, 0, [ - { text: `📁 Archived Wallets (${archivedCount})`, callback_data: 'view_archived_wallets' } + const balances = await walletUtilsInstance.getAllBalances(); + 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`; + + // Общий баланс (крипто + бонусы) + const totalBalance = totalUsdValue + updatedUser.bonus_balance; + message += `💰 *Total Balance:* $${totalBalance.toFixed(2)}\n`; + + // Доступный баланс (общий баланс минус расходы на покупки) + const availableBalance = updatedUser.total_balance + updatedUser.bonus_balance; + 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' } ]); - } - // 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' - }); + 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; @@ -190,36 +200,126 @@ export default class UserWalletsHandler { const messageId = callbackQuery.message.message_id; try { - await bot.editMessageText( - '🔄 Refreshing balances...', - { - chat_id: chatId, - message_id: messageId + // Отправляем промежуточный ответ на 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; } - ); - // Re-fetch and display updated balances - await this.showBalance({ - chat: { id: chatId }, - from: { id: callbackQuery.from.id } - }); + // Получаем активные кошельки пользователя из таблицы 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]); - // Delete the "refreshing" message - await bot.deleteMessage(chatId, messageId); + 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 ERC-20': + address = walletAddresses.usdt; + break; + case 'USDC ERC-20': + 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.editMessageText( - '❌ Error refreshing balances. Please try again.', - { - chat_id: chatId, - message_id: messageId, - reply_markup: { - inline_keyboard: [[ - { text: '« Back', callback_data: 'back_to_balance' } - ]] - } - } - ); + 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' } + ]] + } + }); } } @@ -627,44 +727,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 }, diff --git a/src/models/Wallet.js b/src/models/Wallet.js index 15759e9..1f8c8a6 100644 --- a/src/models/Wallet.js +++ b/src/models/Wallet.js @@ -3,7 +3,6 @@ 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,37 +13,33 @@ 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) { @@ -53,7 +48,6 @@ export default class Wallet { const walletUtilsInstance = new WalletUtils( activeWallets.btc, activeWallets.ltc, - activeWallets.tron, activeWallets.eth, userId, Date.now() - 30 * 24 * 60 * 60 * 1000 @@ -67,7 +61,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 +68,7 @@ export default class Wallet { continue; } - if (wallet) { - totalUsdBalance += balance.usdValue; - } + totalUsdBalance += balance.usdValue; } return totalUsdBalance; @@ -89,7 +80,6 @@ export default class Wallet { const walletUtilsInstance = new WalletUtils( archiveWallets.btc, archiveWallets.ltc, - archiveWallets.tron, archiveWallets.eth, userId, Date.now() - 30 * 24 * 60 * 60 * 1000 @@ -103,7 +93,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 +100,7 @@ export default class Wallet { continue; } - if (wallet) { - totalUsdBalance += balance.usdValue; - } + totalUsdBalance += balance.usdValue; } return totalUsdBalance; diff --git a/src/services/userService.js b/src/services/userService.js index 284a874..be7f7c8 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -94,7 +94,9 @@ class UserService { 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 @@ -162,19 +164,28 @@ class UserService { static async getUserBalance(userId) { try { - const user = await db.getAsync( + // Пересчитываем баланс перед получением + const user = await this.getUserByUserId(userId); + if (!user) { + throw new Error('User not found'); + } + + await this.recalculateUserBalanceByTelegramId(user.telegram_id); + + // Получаем обновленный баланс + const updatedUser = await db.getAsync( `SELECT total_balance, bonus_balance FROM users WHERE id = ?`, [userId] ); - - if (!user) { + + if (!updatedUser) { throw new Error('User not found'); } - + // Возвращаем сумму основного и бонусного баланса - return user.total_balance + user.bonus_balance; + return updatedUser.total_balance + updatedUser.bonus_balance; } catch (error) { console.error('Error getting user balance:', error); throw error; diff --git a/src/services/walletService.js b/src/services/walletService.js index e3c0185..8350548 100644 --- a/src/services/walletService.js +++ b/src/services/walletService.js @@ -1,24 +1,87 @@ +// walletService.js + import db from "../config/database.js"; +import WalletUtils from "../utils/walletUtils.js"; 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 '#'`, // Считаем только архивные кошельки + WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`, [user.id] ); - console.log('[SERVICE] Fetching archived wallets for user:', user.id, 'with telegramId:', user.telegram_id, ' and tolal arhived wallet: ', archivedWallets.total); - // Возвращаем количество архивных кошельков 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, address + FROM crypto_wallets + WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'`, + [userId] + ); + + let totalBalance = 0; + for (const wallet of wallets) { + const walletUtils = new WalletUtils( + wallet.wallet_type === 'BTC' ? wallet.address : null, + wallet.wallet_type === 'LTC' ? wallet.address : null, + wallet.wallet_type === 'ETH' ? wallet.address : null, + wallet.wallet_type === 'USDT' ? wallet.address : null, + wallet.wallet_type === 'USDC' ? wallet.address : null, + userId + ); + + const balances = await walletUtils.getAllBalances(); + totalBalance += balances[wallet.wallet_type]?.usdValue || 0; + } + + 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, address + FROM crypto_wallets + WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`, + [userId] + ); + + let totalBalance = 0; + for (const wallet of wallets) { + const walletUtils = new WalletUtils( + wallet.wallet_type === 'BTC' ? wallet.address : null, + wallet.wallet_type === 'LTC' ? wallet.address : null, + wallet.wallet_type === 'ETH' ? wallet.address : null, + wallet.wallet_type === 'USDT' ? wallet.address : null, + wallet.wallet_type === 'USDC' ? wallet.address : null, + userId + ); + + const balances = await walletUtils.getAllBalances(); + totalBalance += balances[wallet.wallet_type]?.usdValue || 0; + } + + 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( @@ -27,7 +90,7 @@ class WalletService { WHERE wallet_type = ?`, [walletType] ); - + return wallets; } catch (error) { console.error('Error fetching wallets by type:', error); diff --git a/src/utils/walletUtils.js b/src/utils/walletUtils.js index c32ab54..cc80353 100644 --- a/src/utils/walletUtils.js +++ b/src/utils/walletUtils.js @@ -1,127 +1,352 @@ 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 async getCryptoPrices() { - try { - const response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,litecoin,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, // USDT — это стейблкоин, его цена всегда 1 USD - usdc: 1 // USDC — это стейблкоин, его цена всегда 1 USD - }; - } catch (error) { - console.error('Error fetching crypto prices:', error); - return { - btc: 0, ltc: 0, eth: 0, usdt: 1, usdc: 1 - }; + 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; } - } - 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; + static getBaseWalletType(walletType) { + if (walletType.includes('ERC-20')) return 'ETH'; + return walletType; } - } - 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; + 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 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 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 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 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 getUsdtErc20Balance() { - if (!this.usdtAddress) return 0; - try { - const url = `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=0xdac17f958d2ee523a2206206994597c13d831ec7&address=${this.usdtAddress}&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 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 getUsdcErc20Balance() { - if (!this.usdcAddress) return 0; - try { - const url = `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&address=${this.usdcAddress}&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 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 getAllBalances() { - const [ - btcBalance, - ltcBalance, - ethBalance, - usdtErc20Balance, - usdcErc20Balance, - prices - ] = await Promise.all([ - this.getBtcBalance(), - this.getLtcBalance(), - this.getEthBalance(), - this.getUsdtErc20Balance(), - this.getUsdcErc20Balance(), - WalletUtils.getCryptoPrices() - ]); + 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; + } + } - 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 } - }; - } + 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 ERC-20': { amount: 0, usdValue: 0 }, + 'USDC ERC-20': { 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 = WalletUtils.getBaseWalletType(wallet.wallet_type); + 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 ERC-20'].amount += balance; + balances['USDT ERC-20'].usdValue += balance; + break; + case 'USDC': + balances['USDC ERC-20'].amount += balance; + balances['USDC ERC-20'].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 ERC-20': { amount: usdtErc20Balance, usdValue: usdtErc20Balance }, + 'USDC ERC-20': { amount: usdcErc20Balance, usdValue: usdcErc20Balance } + }; + } } \ No newline at end of file