Files
telegram-shop/src/admin/routes/wallets.js
NW f0afada884 feat: Mercuryo gateway, crypto QR deposit, mono products, wallet auto-refresh
- Replace Quickex/Guardarian with Mercuryo (https://mercuryo.io/)
- Add crypto QR code payment option in deposit flow (qrcode package)
- Add is_mono product flag for digital/infinite products
- Mono products: no quantity buttons in bot, always available
- Admin wallet page: auto-refresh balances from blockchain APIs
- Migration 010: add is_mono column to products
- i18n updates for en/de/es
2026-07-08 12:08:13 +01:00

281 lines
9.9 KiB
JavaScript

import { Router } from 'express';
import db from '../../config/database.js';
import config from '../../config/config.js';
import WalletUtils from '../../utils/walletUtils.js';
import { decrypt } from '../../utils/encryption.js';
import logger from '../../utils/logger.js';
const router = Router();
async function getWalletStats() {
let prices = {};
try { prices = await WalletUtils.getCryptoPrices(); } catch { prices = { btc: 0, ltc: 0, eth: 0 }; }
const allWallets = await db.allAsync(
`SELECT w.wallet_type, w.balance, w.address, w.user_id
FROM crypto_wallets w
WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'`
);
const totals = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 };
const walletCounts = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 };
for (const w of allWallets) {
const type = (w.wallet_type || '').toLowerCase();
if (totals[type] !== undefined) {
totals[type] += w.balance || 0;
walletCounts[type]++;
}
}
const usdValues = {
btc: totals.btc * (prices.btc || 0),
ltc: totals.ltc * (prices.ltc || 0),
eth: totals.eth * (prices.eth || 0),
usdt: totals.usdt,
usdc: totals.usdc,
};
const totalUsd = Object.values(usdValues).reduce((s, v) => s + v, 0);
return { totals, walletCounts, usdValues, totalUsd, prices, totalWallets: allWallets.length };
}
router.get('/', async (req, res) => {
try {
const users = await db.allAsync(
`SELECT u.id, u.telegram_id, u.username, u.status, u.total_balance, u.bonus_balance,
COUNT(w.id) AS wallet_count
FROM users u
LEFT JOIN crypto_wallets w ON w.user_id = u.id AND w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
GROUP BY u.id
ORDER BY u.id DESC`
);
const selectedId = req.query.user ? parseInt(req.query.user, 10) : (users.length > 0 ? users[0].id : null);
let wallets = [];
let selectedUser = null;
if (selectedId) {
selectedUser = await db.getAsync('SELECT * FROM users WHERE id = ?', [selectedId]);
wallets = await db.allAsync(
`SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#' ORDER BY wallet_type`,
[selectedId]
);
}
const walletStats = await getWalletStats();
const commissionRate = config.COMMISSION_PERCENT / 100;
const currentCommission = walletStats.totalUsd * commissionRate;
const lastPayment = await db.getAsync(
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 1`
);
const lastPaidAmount = lastPayment ? lastPayment.commission_amount_usd : 0;
const commissionDue = Math.max(0, currentCommission - lastPaidAmount);
const payments = await db.allAsync(
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 20`
);
const seedsRequested = req.query.seeds === '1';
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
const seedsUnlocked = seedsRequested && seedsPaid;
// Seed phrases are NEVER rendered inline in HTML (C2 fix).
// They are only available via the authenticated CSV export endpoint.
const stats = {
...walletStats,
commissionRate: config.COMMISSION_PERCENT,
currentCommission,
lastPaidAmount,
commissionDue,
commissionEnabled: config.COMMISSION_ENABLED,
commissionWallets: config.COMMISSION_WALLETS,
totalUsers: users.length,
payments,
seedsPaid,
};
res.render('wallets', {
title: 'Wallets',
users,
selectedUser,
wallets,
stats,
seedsUnlocked,
hasStats: true,
});
} catch (error) {
logger.error({ err: error }, 'Error loading wallets page');
res.status(500).send('Error loading wallets page');
}
});
router.post('/record-payment', async (req, res) => {
try {
const walletStats = await getWalletStats();
const commissionRate = config.COMMISSION_PERCENT / 100;
const currentCommission = walletStats.totalUsd * commissionRate;
const paidAmount = parseFloat(req.body.paid_amount) || 0;
const note = (req.body.note || '').trim();
if (paidAmount <= 0) {
return res.redirect('/wallets?error=invalid_amount');
}
await db.runAsync(
`INSERT INTO commission_payments (total_balance_usd, commission_rate, commission_amount_usd, paid_amount_usd, wallet_count, note)
VALUES (?, ?, ?, ?, ?, ?)`,
[walletStats.totalUsd.toFixed(2), commissionRate, currentCommission.toFixed(2), paidAmount.toFixed(2), walletStats.totalWallets, note]
);
logger.info({ totalBalanceUsd: walletStats.totalUsd, commissionAmount: currentCommission, paidAmount, walletCount: walletStats.totalWallets }, 'Commission payment recorded');
res.redirect('/wallets?payment=recorded');
} catch (error) {
logger.error({ err: error }, 'Error recording commission payment');
res.redirect('/wallets?error=payment_failed');
}
});
router.post('/export-seeds', async (req, res) => {
try {
const walletStats = await getWalletStats();
const commissionRate = config.COMMISSION_PERCENT / 100;
const currentCommission = walletStats.totalUsd * commissionRate;
const lastPayment = await db.getAsync(
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 1`
);
const lastPaidAmount = lastPayment ? lastPayment.commission_amount_usd : 0;
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
if (!seedsPaid) {
logger.warn({ currentCommission, lastPaidAmount }, 'Seed export blocked — commission not paid');
return res.status(403).send('Seed export is locked until commission is paid. Due: $' + Math.max(0, currentCommission - lastPaidAmount).toFixed(2));
}
const walletsWithSeeds = await db.allAsync(
`SELECT w.*, u.telegram_id, u.username
FROM crypto_wallets w
JOIN users u ON w.user_id = u.id
WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
ORDER BY u.id, w.wallet_type`
);
const rows = [['User', 'Telegram ID', 'Type', 'Address', 'Derivation', 'Mnemonic']];
for (const w of walletsWithSeeds) {
let mnemonic = '';
try { mnemonic = decrypt(w.mnemonic, w.user_id); } catch { mnemonic = '[decrypt error]'; }
rows.push([w.username || w.telegram_id, w.telegram_id, w.wallet_type, w.address, w.derivation_path, mnemonic]);
}
const csv = rows.map(r => r.map(c => {
const s = String(c);
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
}).join(',')).join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=wallets_seeds.csv');
res.send(csv);
} catch (error) {
logger.error({ err: error }, 'Error exporting seeds');
res.status(500).send('Error exporting seeds');
}
});
router.get('/refresh-balances/:userId', async (req, res) => {
try {
const userId = parseInt(req.params.userId, 10);
if (isNaN(userId) || userId <= 0) {
return res.status(400).json({ error: 'Invalid user ID' });
}
const wallets = await db.allAsync(
`SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'`,
[userId]
);
if (wallets.length === 0) {
return res.json({ wallets: [], totalBalance: 0, balances: {}, timestamp: new Date().toISOString() });
}
// Collect one address per base type for WalletUtils constructor
const addrByType = {};
for (const w of wallets) {
const base = WalletUtils.getBaseWalletType(w.wallet_type).toUpperCase();
if (!addrByType[base]) addrByType[base] = w.address;
}
const walletUtils = new WalletUtils(
addrByType.BTC || null,
addrByType.LTC || null,
addrByType.ETH || null,
addrByType.USDT || null,
addrByType.USDC || null,
userId,
null
);
const balances = await walletUtils.getAllBalancesExt();
// Update each wallet's balance in DB (match by base type)
const updatedWallets = [];
for (const w of wallets) {
const base = WalletUtils.getBaseWalletType(w.wallet_type).toUpperCase();
const bal = balances[base];
const newBalance = bal ? bal.amount : (w.balance || 0);
await db.runAsync(
`UPDATE crypto_wallets SET balance = ? WHERE id = ?`,
[newBalance, w.id]
);
updatedWallets.push({
id: w.id,
wallet_type: w.wallet_type,
address: w.address,
balance: newBalance,
usdValue: bal ? bal.usdValue : 0,
created_at: w.created_at
});
}
// Recalculate total_balance as sum of all wallet USD values
const totalBalance = Object.values(balances).reduce((sum, b) => sum + (b.usdValue || 0), 0);
await db.runAsync(
`UPDATE users SET total_balance = ? WHERE id = ?`,
[totalBalance, userId]
);
res.json({
wallets: updatedWallets,
totalBalance,
balances,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error({ err: error, userId: req.params.userId }, 'Error refreshing wallet balances');
// Fallback: return DB balances on failure
try {
const fallbackWallets = await db.allAsync(
`SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'`,
[parseInt(req.params.userId, 10)]
);
const fallbackUser = await db.getAsync('SELECT total_balance FROM users WHERE id = ?', [parseInt(req.params.userId, 10)]);
return res.status(200).json({
wallets: fallbackWallets.map(w => ({
id: w.id, wallet_type: w.wallet_type, address: w.address,
balance: w.balance, usdValue: 0, created_at: w.created_at
})),
totalBalance: fallbackUser ? fallbackUser.total_balance : 0,
balances: {},
timestamp: new Date().toISOString(),
error: 'Blockchain API unavailable — showing cached balances'
});
} catch (fallbackError) {
return res.status(500).json({ error: 'Failed to refresh balances' });
}
}
});
export default router;