ваше сообщение коммита

This commit is contained in:
2025-05-20 14:32:03 +03:00
parent d79aab9352
commit 7ccd08b172
22 changed files with 677 additions and 551 deletions

View File

@@ -1,32 +0,0 @@
[
{
"name": "Ethereum Token",
"address": "0xd95a45fc46a7300e6022885afec3d618d7d3f27c",
"network": "eth",
"minBalance": "1.0"
},
{
"name": "BSC Token",
"address": "0x4B294265720B09ca39BFBA18c7E368413c0f68eB",
"network": "bsc",
"minBalance": "10.0"
},
{
"name": "Arbitrum Token",
"address": "0xdce769b847a0a697239777d0b1c7dd33b6012ba0",
"network": "arbitrum",
"minBalance": "0.5"
},
{
"name": "Custom Token",
"address": "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d",
"network": "eth",
"minBalance": "5.0"
},
{
"name": "test2",
"address": "0xef49261169B454f191678D2aFC5E91Ad2e85dfD8",
"minBalance": "1.0",
"network": "sepolia"
}
]

View File

@@ -1,27 +0,0 @@
[
{
"networkId": "bsc",
"rpcUrl": "https://bsc-dataseed1.binance.org",
"chainId": 56
},
{
"networkId": "arbitrum",
"rpcUrl": "https://arb1.arbitrum.io/rpc",
"chainId": 42161
},
{
"networkId": "polygon",
"rpcUrl": "https://polygon-rpc.com",
"chainId": 137
},
{
"networkId": "sepolia",
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"chainId": 11155111
},
{
"networkId": "ethereum",
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"chainId": 1
}
]

View File

@@ -0,0 +1,23 @@
-- Миграция: создание таблиц для RPC провайдеров и токенов аутентификации
-- Таблица RPC провайдеров
CREATE TABLE IF NOT EXISTS rpc_providers (
id SERIAL PRIMARY KEY,
network_id VARCHAR(64) NOT NULL UNIQUE,
rpc_url TEXT NOT NULL,
chain_id INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Таблица токенов аутентификации
CREATE TABLE IF NOT EXISTS auth_tokens (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address VARCHAR(64) NOT NULL,
network VARCHAR(64) NOT NULL,
min_balance NUMERIC(36, 18) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT auth_tokens_address_network_unique UNIQUE (address, network)
);

View File

@@ -14,7 +14,7 @@ module.exports = {
},
networks: {
sepolia: {
url: process.env.ETHEREUM_NETWORK_URL,
url: process.env.RPC_URL_ETH,
accounts: [process.env.PRIVATE_KEY],
},
},

View File

@@ -104,7 +104,7 @@ async function requireAdmin(req, res, next) {
// Проверка через кошелек
if (req.session.address) {
const isAdmin = await authService.checkAdminToken(req.session.address);
const isAdmin = await authService.checkAdminTokens(req.session.address);
if (isAdmin) {
// Обновляем сессию
req.session.isAdmin = true;

View File

@@ -2,52 +2,17 @@ const express = require('express');
const router = express.Router();
const { requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
const fs = require('fs');
const path = require('path');
const { ethers } = require('ethers');
const rpcProviderService = require('../services/rpcProviderService');
const authTokenService = require('../services/authTokenService');
// Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
// Путь к файлу с настройками
const RPC_CONFIG_PATH = path.join(__dirname, '../config/rpc-settings.json');
const AUTH_TOKENS_PATH = path.join(__dirname, '../config/auth-tokens.json');
// Вспомогательная функция для чтения настроек из файла
const readSettingsFile = (filePath, defaultValue = []) => {
try {
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
}
return defaultValue;
} catch (error) {
logger.error(`Ошибка при чтении файла настроек ${filePath}:`, error);
return defaultValue;
}
};
// Вспомогательная функция для записи настроек в файл
const writeSettingsFile = async (filePath, data) => {
try {
// Создаем директорию, если не существует
const dirname = path.dirname(filePath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (error) {
logger.error(`Ошибка при записи файла настроек ${filePath}:`, error);
return false;
}
};
// Получение RPC настроек
router.get('/rpc', requireAdmin, async (req, res) => {
try {
const rpcConfigs = readSettingsFile(RPC_CONFIG_PATH);
const rpcConfigs = await rpcProviderService.getAllRpcProviders();
res.json({ success: true, data: rpcConfigs });
} catch (error) {
logger.error('Ошибка при получении RPC настроек:', error);
@@ -55,32 +20,47 @@ router.get('/rpc', requireAdmin, async (req, res) => {
}
});
// Сохранение RPC настроек
// Добавление/обновление одного или нескольких RPC
router.post('/rpc', requireAdmin, async (req, res) => {
try {
const { rpcConfigs } = req.body;
if (!Array.isArray(rpcConfigs)) {
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
// Если пришёл массив rpcConfigs — bulk-режим
if (Array.isArray(req.body.rpcConfigs)) {
const rpcConfigs = req.body.rpcConfigs;
if (!rpcConfigs.length) {
return res.status(400).json({ success: false, error: 'rpcConfigs не может быть пустым массивом' });
}
await rpcProviderService.saveAllRpcProviders(rpcConfigs);
return res.json({ success: true, message: 'RPC провайдеры успешно сохранены (bulk)' });
}
const success = await writeSettingsFile(RPC_CONFIG_PATH, rpcConfigs);
if (success) {
res.json({ success: true, message: 'RPC настройки успешно сохранены' });
} else {
res.status(500).json({ success: false, error: 'Ошибка при сохранении RPC настроек' });
// Иначе — одиночный режим (старый)
const { networkId, rpcUrl, chainId } = req.body;
if (!networkId || !rpcUrl) {
return res.status(400).json({ success: false, error: 'networkId и rpcUrl обязательны' });
}
await rpcProviderService.upsertRpcProvider({ networkId, rpcUrl, chainId });
res.json({ success: true, message: 'RPC провайдер сохранён' });
} catch (error) {
logger.error('Ошибка при сохранении RPC настроек:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении настроек RPC' });
logger.error('Ошибка при сохранении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении RPC' });
}
});
// Удаление одного RPC
router.delete('/rpc/:networkId', requireAdmin, async (req, res) => {
try {
const { networkId } = req.params;
await rpcProviderService.deleteRpcProvider(networkId);
res.json({ success: true, message: 'RPC провайдер удалён' });
} catch (error) {
logger.error('Ошибка при удалении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении RPC' });
}
});
// Получение токенов для аутентификации
router.get('/auth-tokens', requireAdmin, async (req, res) => {
try {
const authTokens = readSettingsFile(AUTH_TOKENS_PATH);
const authTokens = await authTokenService.getAllAuthTokens();
res.json({ success: true, data: authTokens });
} catch (error) {
logger.error('Ошибка при получении токенов аутентификации:', error);
@@ -92,24 +72,44 @@ router.get('/auth-tokens', requireAdmin, async (req, res) => {
router.post('/auth-tokens', requireAdmin, async (req, res) => {
try {
const { authTokens } = req.body;
if (!Array.isArray(authTokens)) {
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
}
const success = await writeSettingsFile(AUTH_TOKENS_PATH, authTokens);
if (success) {
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
} else {
res.status(500).json({ success: false, error: 'Ошибка при сохранении токенов аутентификации' });
}
await authTokenService.saveAllAuthTokens(authTokens);
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
} catch (error) {
logger.error('Ошибка при сохранении токенов аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' });
}
});
// Добавление/обновление одного токена
router.post('/auth-token', requireAdmin, async (req, res) => {
try {
const { name, address, network, minBalance } = req.body;
if (!name || !address || !network) {
return res.status(400).json({ success: false, error: 'name, address и network обязательны' });
}
await authTokenService.upsertAuthToken({ name, address, network, minBalance });
res.json({ success: true, message: 'Токен аутентификации сохранён' });
} catch (error) {
logger.error('Ошибка при сохранении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токена' });
}
});
// Удаление одного токена
router.delete('/auth-token/:address/:network', requireAdmin, async (req, res) => {
try {
const { address, network } = req.params;
await authTokenService.deleteAuthToken(address, network);
res.json({ success: true, message: 'Токен аутентификации удалён' });
} catch (error) {
logger.error('Ошибка при удалении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении токена' });
}
});
// Тестирование RPC соединения
router.post('/rpc-test', requireAdmin, async (req, res) => {
try {
@@ -123,7 +123,12 @@ router.post('/rpc-test', requireAdmin, async (req, res) => {
try {
// Пробуем создать провайдера и получить номер последнего блока (обновлено для ethers v6)
const provider = new ethers.JsonRpcProvider(rpcUrl);
let provider;
if (rpcUrl.startsWith('ws://') || rpcUrl.startsWith('wss://')) {
provider = new ethers.WebSocketProvider(rpcUrl);
} else {
provider = new ethers.JsonRpcProvider(rpcUrl);
}
// Устанавливаем таймаут для соединения
const timeoutPromise = new Promise((_, reject) =>

View File

@@ -1,29 +1,21 @@
const express = require('express');
const router = express.Router();
const { requireAuth } = require('../middleware/auth');
const authService = require('../services/auth-service');
const logger = require('../utils/logger');
const authService = require('../services/auth-service');
// Получение балансов токенов
router.get('/balances', requireAuth, async (req, res) => {
// Получение балансов токенов пользователя по токенам из базы
router.get('/balances', async (req, res) => {
try {
const { address } = req.session;
const address = req.query.address;
if (!address) {
return res.status(400).json({
error: 'No wallet address in session',
});
return res.status(400).json({ success: false, error: 'Не указан адрес кошелька' });
}
logger.info(`Fetching token balances for address: ${address}`);
const balances = await authService.getTokenBalances(address);
res.json(balances);
const balances = await authService.getUserTokenBalances(address);
res.json({ success: true, data: balances });
} catch (error) {
logger.error('Error fetching token balances:', error);
res.status(500).json({
error: 'Failed to fetch token balances',
});
res.status(500).json({ success: false, error: 'Failed to fetch token balances' });
}
});

View File

@@ -5,26 +5,13 @@ const crypto = require('crypto');
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
const verificationService = require('./verification-service'); // Используем сервис верификации
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
const ADMIN_CONTRACTS = [
{ address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'ethereum' },
{ address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc' },
{ address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum' },
{ address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'polygon' },
];
const authTokenService = require('./authTokenService');
const rpcProviderService = require('./rpcProviderService');
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
class AuthService {
constructor() {
// Используем существующие переменные окружения с префиксом RPC_URL_
this.providers = {
ethereum: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH), // Используем RPC_URL_ETH для ethereum
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM),
};
}
constructor() {}
// Проверка подписи
async verifySignature(message, signature, address) {
@@ -127,100 +114,86 @@ class AuthService {
*/
async checkAdminRole(address) {
if (!address) return false;
logger.info(`Checking admin role for address: ${address}`);
let foundTokens = false;
let errorCount = 0;
const balances = {};
const totalNetworks = ADMIN_CONTRACTS.length;
// Создаем массив промисов для параллельной проверки балансов
const checkPromises = ADMIN_CONTRACTS.map(async (contract) => {
// Получаем токены и RPC из базы
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
}
const checkPromises = tokens.map(async (token) => {
try {
const provider = this.providers[contract.network];
if (!provider) {
logger.error(`No provider available for network ${contract.network}`);
balances[contract.network] = 'Error: No provider';
const rpcUrl = rpcMap[token.network];
if (!rpcUrl) {
logger.error(`No RPC URL for network ${token.network}`);
balances[token.network] = 'Error: No RPC URL';
errorCount++;
return null;
}
// Проверяем доступность провайдера
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Проверяем доступность сети с таймаутом
try {
// Проверка доступности сети с таймаутом
const networkCheckPromise = provider.getNetwork();
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network check timeout')), 3000)
);
await Promise.race([networkCheckPromise, timeoutPromise]);
} catch (networkError) {
logger.error(
`Provider for ${contract.network} is not available: ${networkError.message}`
);
balances[contract.network] = 'Error: Network unavailable';
logger.error(`Provider for ${token.network} is not available: ${networkError.message}`);
balances[token.network] = 'Error: Network unavailable';
errorCount++;
return null;
}
const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
// Создаем промис с таймаутом
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 3000)
);
// Ждем первый выполненный промис
const balance = await Promise.race([balancePromise, timeoutPromise]);
const formattedBalance = ethers.formatUnits(balance, 18);
balances[contract.network] = formattedBalance;
logger.info(`Token balance on ${contract.network}:`, {
balances[token.network] = formattedBalance;
logger.info(`Token balance on ${token.network}:`, {
address,
contract: contract.address,
contract: token.address,
balance: formattedBalance,
hasTokens: balance > 0,
minBalance: token.min_balance,
hasTokens: parseFloat(formattedBalance) >= parseFloat(token.min_balance),
});
if (parseFloat(formattedBalance) > 0) {
logger.info(`Found admin tokens on ${contract.network}`);
if (parseFloat(formattedBalance) >= parseFloat(token.min_balance)) {
logger.info(`Found admin tokens on ${token.network}`);
foundTokens = true;
}
return { network: contract.network, balance: formattedBalance };
return { network: token.network, balance: formattedBalance };
} catch (error) {
logger.error(`Error checking balance in ${contract.network}:`, {
logger.error(`Error checking balance in ${token.network}:`, {
address,
contract: contract.address,
contract: token.address,
error: error.message || 'Unknown error',
});
balances[contract.network] = 'Error';
balances[token.network] = 'Error';
errorCount++;
return null;
}
});
// Ждем выполнения всех проверок
await Promise.all(checkPromises);
// Если все запросы завершились с ошибкой, считаем, что проверка не удалась
if (errorCount === totalNetworks) {
if (errorCount === tokens.length) {
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
return false;
}
if (foundTokens) {
logger.info(`Admin role summary for ${address}:`, {
networks: Object.keys(balances).filter(
(net) => balances[net] > 0 && balances[net] !== 'Error'
(net) => parseFloat(balances[net]) > 0 && balances[net] !== 'Error'
),
balances,
});
logger.info(`Admin role granted for ${address}`);
return true;
}
logger.info(`Admin role denied - no tokens found for ${address}`);
return false;
}
@@ -238,6 +211,7 @@ class AuthService {
bsc: '0',
arbitrum: '0',
polygon: '0',
sepolia: '0',
};
}
@@ -889,6 +863,46 @@ class AuthService {
throw new Error('Ошибка обработки верификации Email');
}
}
/**
* Получение балансов токенов пользователя только по токенам из базы
* @param {string} address - адрес кошелька
* @returns {Promise<Array>} - массив объектов с балансами
*/
async getUserTokenBalances(address) {
if (!address) return [];
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
}
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
const results = [];
for (const token of tokens) {
const rpcUrl = rpcMap[token.network];
if (!rpcUrl) continue;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
let balance = '0';
try {
const rawBalance = await tokenContract.balanceOf(address);
balance = ethers.formatUnits(rawBalance, 18);
if (!balance || isNaN(Number(balance))) balance = '0';
} catch (e) {
logger.error(`[getUserTokenBalances] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`, e);
balance = '0';
}
results.push({
network: token.network,
tokenAddress: token.address,
tokenName: token.name,
symbol: token.symbol || '',
balance,
});
}
return results;
}
}
// Создаем и экспортируем единственный экземпляр

View File

@@ -0,0 +1,32 @@
const db = require('../db');
async function getAllAuthTokens() {
const { rows } = await db.query('SELECT * FROM auth_tokens ORDER BY id');
return rows;
}
async function saveAllAuthTokens(authTokens) {
await db.query('DELETE FROM auth_tokens');
for (const token of authTokens) {
await db.query(
'INSERT INTO auth_tokens (name, address, network, min_balance) VALUES ($1, $2, $3, $4)',
[token.name, token.address, token.network, token.minBalance]
);
}
}
async function upsertAuthToken(token) {
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
await db.query(
`INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address, network) DO UPDATE SET name=EXCLUDED.name, min_balance=EXCLUDED.min_balance`,
[token.name, token.address, token.network, minBalance]
);
}
async function deleteAuthToken(address, network) {
await db.query('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -0,0 +1,31 @@
const db = require('../db');
async function getAllRpcProviders() {
const { rows } = await db.query('SELECT * FROM rpc_providers ORDER BY id');
return rows;
}
async function saveAllRpcProviders(rpcConfigs) {
await db.query('DELETE FROM rpc_providers');
for (const cfg of rpcConfigs) {
await db.query(
'INSERT INTO rpc_providers (network_id, rpc_url, chain_id) VALUES ($1, $2, $3)',
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
}
}
async function upsertRpcProvider(cfg) {
await db.query(
`INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES ($1, $2, $3)
ON CONFLICT (network_id) DO UPDATE SET rpc_url=EXCLUDED.rpc_url, chain_id=EXCLUDED.chain_id`,
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
}
async function deleteRpcProvider(networkId) {
await db.query('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
}
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider };