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
This commit is contained in:
NW
2026-07-08 12:08:13 +01:00
parent 2b30bc4a91
commit f0afada884
24 changed files with 1393 additions and 268 deletions

View File

@@ -181,4 +181,101 @@ router.post('/export-seeds', async (req, res) => {
}
});
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;