ваше сообщение коммита
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -890,6 +890,7 @@ class AuthService {
|
||||
tokenName: token.name,
|
||||
symbol: token.symbol || '',
|
||||
balance,
|
||||
minBalance: token.min_balance,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user