Files
telegram-shop/src/handlers/userHandlers/userWalletsHandler.js
NW 68d83807ad refactor(arch): Phase 2 — deduplicate isAdmin, convertToUsd, getBaseWalletType
- #54: Extract isAdmin() to src/middleware/auth.js, remove duplicates from 7 admin handlers
- #55: Add WalletUtils.convertToUsd(), replace 8 switch-case blocks across 4 files
- #56: Unify getBaseWalletType() — keep only WalletUtils version (most complete),
  remove duplicates from Wallet.js and userWalletsHandler.js

New file: src/middleware/auth.js
Net: -215 lines, +80 lines

Closes: #54, #55, #56
2026-06-17 22:10:34 +01:00

748 lines
27 KiB
JavaScript
Raw Blame History

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