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

This commit is contained in:
2025-08-06 11:37:58 +03:00
parent c987b8f8f4
commit cde35ac576
27 changed files with 1868 additions and 2616 deletions

View File

@@ -934,12 +934,12 @@ router.get('/check-tokens/:address', async (req, res) => {
try {
const { address } = req.params;
// Получаем балансы токенов на всех сетях
const balances = await authService.getTokenBalances(address);
// Получаем балансы токенов с минимальными балансами
const balances = await authService.getUserTokenBalances(address);
res.json({
success: true,
balances,
data: balances,
});
} catch (error) {
logger.error('Error checking token balances:', error);

View File

@@ -108,4 +108,300 @@ router.post('/read-dle-info', async (req, res) => {
}
});
// Получение поддерживаемых сетей из смарт-контракта
router.post('/get-supported-chains', async (req, res) => {
try {
const { dleAddress } = req.body;
if (!dleAddress) {
return res.status(400).json({
success: false,
error: 'Адрес DLE обязателен'
});
}
console.log(`[Blockchain] Получение поддерживаемых сетей для DLE: ${dleAddress}`);
// Получаем RPC URL для Sepolia
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для проверки поддерживаемых сетей
const dleAbi = [
"function isChainSupported(uint256 _chainId) external view returns (bool)",
"function getCurrentChainId() external view returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Список всех возможных сетей для проверки
const allChains = [
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' },
{ chainId: 10, name: 'Optimism', description: 'Optimism' },
{ chainId: 8453, name: 'Base', description: 'Base' },
{ chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' },
{ chainId: 250, name: 'Fantom', description: 'Fantom Opera' },
{ chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' },
{ chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' },
{ chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' },
{ chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' }
];
const supportedChains = [];
// Проверяем каждую сеть через смарт-контракт
for (const chain of allChains) {
try {
const isSupported = await dle.isChainSupported(chain.chainId);
if (isSupported) {
supportedChains.push(chain);
}
} catch (error) {
console.log(`[Blockchain] Ошибка при проверке сети ${chain.chainId}:`, error.message);
// Продолжаем проверку других сетей
}
}
console.log(`[Blockchain] Найдено поддерживаемых сетей: ${supportedChains.length}`);
res.json({
success: true,
data: {
chains: supportedChains,
totalCount: supportedChains.length
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении поддерживаемых сетей:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении поддерживаемых сетей: ' + error.message
});
}
});
// Получение списка всех предложений
router.post('/get-proposals', async (req, res) => {
try {
const { dleAddress } = req.body;
if (!dleAddress) {
return res.status(400).json({
success: false,
error: 'Адрес DLE обязателен'
});
}
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
// Получаем RPC URL для Sepolia
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для чтения предложений (только читаемые поля)
const dleAbi = [
"function proposals(uint256) external view returns (uint256 id, string description, uint256 forVotes, uint256 againstVotes, bool executed, uint256 deadline, address initiator, bytes operation)",
"function checkProposalResult(uint256 _proposalId) external view returns (bool)",
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем события ProposalCreated для определения количества предложений
const currentBlock = await provider.getBlockNumber();
const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
console.log(`[Blockchain] Найдено событий ProposalCreated: ${events.length}`);
console.log(`[Blockchain] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
const proposals = [];
// Читаем информацию о каждом предложении
for (let i = 0; i < events.length; i++) {
try {
const proposalId = events[i].args.proposalId;
console.log(`[Blockchain] Читаем предложение ID: ${proposalId}`);
// Пробуем несколько раз для новых предложений
let proposal, isPassed;
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
proposal = await dle.proposals(proposalId);
isPassed = await dle.checkProposalResult(proposalId);
break; // Успешно прочитали
} catch (error) {
retryCount++;
console.log(`[Blockchain] Попытка ${retryCount} чтения предложения ${proposalId} не удалась:`, error.message);
if (retryCount < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Ждем 2 секунды
} else {
throw error; // Превышено количество попыток
}
}
}
// governanceChainId не сохраняется в предложении, используем текущую цепочку
const governanceChainId = 11155111; // Sepolia chain ID
console.log(`[Blockchain] Данные предложения ${proposalId}:`, {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
operation: proposal.operation,
governanceChainId: Number(governanceChainId)
});
const proposalInfo = {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
operation: proposal.operation,
governanceChainId: Number(governanceChainId),
isPassed: isPassed,
blockNumber: events[i].blockNumber
};
proposals.push(proposalInfo);
} catch (error) {
console.log(`[Blockchain] Ошибка при чтении предложения ${i}:`, error.message);
// Если это ошибка декодирования, возможно предложение еще не полностью записано
if (error.message.includes('could not decode result data')) {
console.log(`[Blockchain] Предложение ${i} еще не полностью синхронизировано, пропускаем`);
continue;
}
// Продолжаем с следующим предложением
}
}
// Сортируем по ID предложения (новые сверху)
proposals.sort((a, b) => b.id - a.id);
console.log(`[Blockchain] Найдено предложений: ${proposals.length}`);
res.json({
success: true,
data: {
proposals: proposals,
totalCount: proposals.length
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении списка предложений:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении списка предложений: ' + error.message
});
}
});
// Получение информации о предложении
router.post('/get-proposal-info', async (req, res) => {
try {
const { dleAddress, proposalId } = req.body;
if (!dleAddress || proposalId === undefined) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны: dleAddress, proposalId'
});
}
console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
// Получаем RPC URL для Sepolia
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для чтения информации о предложении
const dleAbi = [
"function proposals(uint256) external view returns (tuple(string description, uint256 duration, bytes operation, uint256 governanceChainId, uint256 startTime, bool executed, uint256 forVotes, uint256 againstVotes))",
"function checkProposalResult(uint256 _proposalId) external view returns (bool)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Читаем информацию о предложении
const proposal = await dle.proposals(proposalId);
const isPassed = await dle.checkProposalResult(proposalId);
// governanceChainId не сохраняется в предложении, используем текущую цепочку
const governanceChainId = 11155111; // Sepolia chain ID
const proposalInfo = {
description: proposal.description,
duration: Number(proposal.duration),
operation: proposal.operation,
governanceChainId: Number(proposal.governanceChainId),
startTime: Number(proposal.startTime),
executed: proposal.executed,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
isPassed: isPassed
};
console.log(`[Blockchain] Информация о предложении получена:`, proposalInfo);
res.json({
success: true,
data: proposalInfo
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении информации о предложении:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении информации о предложении: ' + error.message
});
}
});
// Импортируем WebSocket функции из wsHub
const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub');
// Экспортируем router как основной экспорт
module.exports = router;

View File

@@ -71,9 +71,9 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
/**
* @route GET /api/dle-v2
* @desc Получить список всех DLE v2
* @access Private (только для авторизованных пользователей)
* @access Public (доступно всем пользователям)
*/
router.get('/', auth.requireAuth, async (req, res, next) => {
router.get('/', async (req, res, next) => {
try {
const dles = dleV2Service.getAllDLEs();

View File

@@ -62,6 +62,8 @@ async function initServices() {
const server = http.createServer(app);
initWSS(server);
// WebSocket уже инициализирован в wsHub.js
async function startServer() {
await initDbPool(); // Дождаться пересоздания пула!
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama

View File

@@ -890,6 +890,7 @@ class AuthService {
tokenName: token.name,
symbol: token.symbol || '',
balance,
minBalance: token.min_balance,
});
}
return results;

View File

@@ -62,7 +62,14 @@ async function upsertAuthToken(token) {
}
async function deleteAuthToken(address, network) {
await encryptedDb.deleteData('auth_tokens', { address, network });
console.log(`[AuthTokenService] deleteAuthToken: address=${address}, network=${network}`);
try {
await encryptedDb.deleteData('auth_tokens', { address, network });
console.log(`[AuthTokenService] Токен успешно удален`);
} catch (error) {
console.error(`[AuthTokenService] Ошибка при удалении токена:`, error);
throw error;
}
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -323,48 +323,73 @@ class EncryptedDataService {
*/
async deleteData(tableName, conditions) {
try {
console.log(`[EncryptedDataService] deleteData: tableName=${tableName}, conditions=`, conditions);
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
console.log(`[EncryptedDataService] Columns for ${tableName}:`, columns.map(c => c.column_name));
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
// Для зашифрованных таблиц - пока используем обычный DELETE
// TODO: Добавить логику для зашифрованных условий WHERE
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.map((key, index) => {
const value = conditions[key];
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
// Ключ шифрования всегда первый параметр ($1), затем значения
return `${key}_encrypted = encrypt_text($${index + 2}, $1)`;
} else {
// Для незашифрованных колонок используем обычное сравнение
const columnName = quoteReservedWord(key);
return `${columnName} = $${index + 1}`;
}
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
// Добавляем параметры
const paramsToAdd = Object.values(conditions);
params.push(...paramsToAdd);
}
// Добавляем ключ шифрования в начало, если есть зашифрованные поля
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
if (hasEncryptedFields) {
params.unshift(this.encryptionKey);
}
console.log(`[EncryptedDataService] DELETE query: ${query}`);
console.log(`[EncryptedDataService] DELETE params:`, params);
const result = await db.getQuery()(query, params);
return result.rows;
} catch (error) {
// console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
console.error(`[EncryptedDataService] ❌ Ошибка удаления данных из ${tableName}:`, error);
throw error;
}
}

View File

@@ -313,6 +313,58 @@ function broadcastAIStatus(status) {
}
}
// Blockchain уведомления
function broadcastProposalCreated(dleAddress, proposalId, txHash) {
const message = JSON.stringify({
type: 'proposal_created',
data: {
dleAddress: dleAddress,
proposalId: proposalId,
txHash: txHash
},
timestamp: Date.now()
});
broadcastToAllClients(message);
}
function broadcastProposalVoted(dleAddress, proposalId, support, txHash) {
const message = JSON.stringify({
type: 'proposal_voted',
data: {
dleAddress: dleAddress,
proposalId: proposalId,
support: support,
txHash: txHash
},
timestamp: Date.now()
});
broadcastToAllClients(message);
}
function broadcastProposalExecuted(dleAddress, proposalId, txHash) {
const message = JSON.stringify({
type: 'proposal_executed',
data: {
dleAddress: dleAddress,
proposalId: proposalId,
txHash: txHash
},
timestamp: Date.now()
});
broadcastToAllClients(message);
}
function broadcastToAllClients(message) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
module.exports = {
initWSS,
broadcastContactsUpdate,
@@ -323,6 +375,9 @@ module.exports = {
broadcastTableRelationsUpdate,
broadcastTagsUpdate,
broadcastAIStatus,
broadcastProposalCreated,
broadcastProposalVoted,
broadcastProposalExecuted,
getConnectedUsers,
getStats
};