/** * Copyright (c) 2024-2025 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/HB3-ACCELERATOR */ const express = require('express'); const router = express.Router(); const DLEV2Service = require('../services/dleV2Service'); const dleV2Service = new DLEV2Service(); const logger = require('../utils/logger'); const auth = require('../middleware/auth'); const path = require('path'); const fs = require('fs'); const ethers = require('ethers'); // Added ethers for private key validation const deploymentTracker = require('../utils/deploymentTracker'); const create2 = require('../utils/create2'); const verificationStore = require('../services/verificationStore'); const etherscanV2 = require('../services/etherscanV2VerificationService'); /** * Асинхронная функция для выполнения деплоя в фоне */ async function executeDeploymentInBackground(deploymentId, dleParams) { try { // Отправляем уведомление о начале deploymentTracker.updateDeployment(deploymentId, { status: 'in_progress', stage: 'initializing' }); deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info'); // Выполняем деплой с передачей deploymentId для WebSocket обновлений const result = await dleV2Service.createDLE(dleParams, deploymentId); // Завершаем успешно deploymentTracker.completeDeployment(deploymentId, result.data); } catch (error) { // Завершаем с ошибкой deploymentTracker.failDeployment(deploymentId, error); } } /** * @route POST /api/dle-v2 * @desc Создать новое DLE v2 (Digital Legal Entity) * @access Private (только для авторизованных пользователей с ролью admin) */ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { try { const dleParams = req.body; logger.info('🔥 Получен запрос на асинхронный деплой DLE v2'); // Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) { // Проверяем, есть ли в сессии адрес кошелька пользователя if (!req.user || !req.user.walletAddress) { return res.status(400).json({ success: false, message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов' }); } // Используем адрес авторизованного пользователя dleParams.initialPartners = [req.user.address || req.user.walletAddress]; // Если суммы не указаны, используем значение по умолчанию (100% токенов) if (!dleParams.initialAmounts || dleParams.initialAmounts.length === 0) { dleParams.initialAmounts = ['1000000000000000000000000']; // 1,000,000 токенов } } // Используем deploymentId из запроса, если передан, иначе создаем новый const deploymentId = req.body.deploymentId || deploymentTracker.createDeployment(dleParams); // Если deploymentId был передан из запроса, создаем запись о деплое с этим ID if (req.body.deploymentId) { deploymentTracker.createDeployment(dleParams, req.body.deploymentId); } // Запускаем деплой в фоне (с await для правильной обработки ошибок!) await executeDeploymentInBackground(deploymentId, dleParams); logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`); // Сразу возвращаем ответ с ID деплоя res.json({ success: true, message: 'Деплой запущен в фоновом режиме', deploymentId: deploymentId }); } catch (error) { logger.error('❌ Ошибка при запуске асинхронного деплоя:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при запуске деплоя' }); } }); /** * @route GET /api/dle-v2 * @desc Получить список всех DLE v2 * @access Public (доступно всем пользователям) */ router.get('/', async (req, res, next) => { try { const dles = dleV2Service.getAllDLEs(); res.json({ success: true, data: dles }); } catch (error) { logger.error('Ошибка при получении списка DLE v2:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при получении списка DLE v2' }); } }); /** * @route GET /api/dle-v2/default-params * @desc Получить параметры по умолчанию для создания DLE v2 * @access Private */ router.get('/default-params', auth.requireAuth, async (req, res, next) => { try { const defaultParams = { name: '', symbol: '', location: '', coordinates: '', jurisdiction: 1, oktmo: 45000000000, okvedCodes: [], kpp: 770101001, quorumPercentage: 51, initialPartners: [], initialAmounts: [], supportedChainIds: [1, 137, 56, 42161], currentChainId: 1 }; res.json({ success: true, data: defaultParams }); } catch (error) { logger.error('Ошибка при получении параметров по умолчанию:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при получении параметров по умолчанию' }); } }); /** * @route DELETE /api/dle-v2/:dleAddress * @desc Удалить DLE v2 по адресу * @access Private (только для авторизованных пользователей с ролью admin) */ router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { try { const { dleAddress } = req.params; logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`); // Проверяем существование DLE v2 в директории contracts-data/dles const dlesDir = path.join(__dirname, '../contracts-data/dles'); const files = fs.readdirSync(dlesDir); let fileToDelete = null; // Находим файл, содержащий указанный адрес DLE for (const file of files) { if (file.includes('dle-v2-') && file.endsWith('.json')) { const filePath = path.join(dlesDir, file); if (fs.statSync(filePath).isFile()) { try { const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) { fileToDelete = filePath; break; } } catch (err) { logger.error(`Ошибка при чтении файла ${file}:`, err); } } } } if (!fileToDelete) { return res.status(404).json({ success: false, message: `DLE v2 с адресом ${dleAddress} не найдено` }); } // Удаляем файл fs.unlinkSync(fileToDelete); logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`); res.json({ success: true, message: `DLE v2 с адресом ${dleAddress} успешно удалено` }); } catch (error) { logger.error('Ошибка при удалении DLE v2:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при удалении DLE v2' }); } }); /** * @route GET /api/dle-v2/check-admin-tokens * @desc Проверить баланс админских токенов для адреса * @access Public */ router.get('/check-admin-tokens', async (req, res, next) => { try { const { address } = req.query; if (!address) { return res.status(400).json({ success: false, message: 'Адрес кошелька не передан' }); } // Проверяем баланс токенов const { checkAdminRole } = require('../services/admin-role'); const isAdmin = await checkAdminRole(address); res.json({ success: true, data: { isAdmin: isAdmin, address: address, message: isAdmin ? 'Админские токены найдены' : 'Админские токены не найдены' } }); } catch (error) { logger.error('Ошибка при проверке админских токенов:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при проверке админских токенов' }); } }); /** * @route POST /api/dle-v2/validate-private-key * @desc Валидировать приватный ключ и получить адрес кошелька * @access Public */ router.post('/validate-private-key', async (req, res, next) => { try { const { privateKey } = req.body; if (!privateKey) { return res.status(400).json({ success: false, message: 'Приватный ключ не передан' }); } // Логируем входящий ключ (только для отладки) logger.info('Получен приватный ключ для валидации:', privateKey); logger.info('Длина входящего ключа:', privateKey.length); logger.info('Тип входящего ключа:', typeof privateKey); logger.info('Полный объект запроса:', JSON.stringify(req.body)); try { // Очищаем ключ от префикса 0x если есть const cleanKey = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey; // Логируем очищенный ключ (только для отладки) logger.info('Очищенный ключ:', cleanKey); logger.info('Длина очищенного ключа:', cleanKey.length); // Проверяем длину и формат (64 символа в hex) if (cleanKey.length !== 64 || !/^[a-fA-F0-9]+$/.test(cleanKey)) { logger.error('Некорректный формат ключа. Длина:', cleanKey.length, 'Формат:', /^[a-fA-F0-9]+$/.test(cleanKey)); return res.status(400).json({ success: false, message: 'Некорректный формат приватного ключа' }); } // Генерируем адрес из приватного ключа const wallet = new ethers.Wallet('0x' + cleanKey); const address = wallet.address; // Логируем сгенерированный адрес logger.info('Сгенерированный адрес из приватного ключа:', address); res.json({ success: true, data: { isValid: true, address: address, error: null } }); } catch (error) { logger.error('Ошибка при генерации адреса из приватного ключа:', error); res.status(400).json({ success: false, message: 'Некорректный приватный ключ' }); } } catch (error) { logger.error('Ошибка при валидации приватного ключа:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при валидации приватного ключа' }); } }); /** * @route GET /api/dle-v2/deployment-status/:deploymentId * @desc Получить статус деплоя * @access Private */ router.get('/deployment-status/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { deploymentId } = req.params; const deployment = deploymentTracker.getDeployment(deploymentId); if (!deployment) { return res.status(404).json({ success: false, message: 'Деплой не найден' }); } res.json({ success: true, data: { id: deployment.id, status: deployment.status, stage: deployment.stage, progress: deployment.progress, networks: deployment.networks, startedAt: deployment.startedAt, updatedAt: deployment.updatedAt, logs: deployment.logs.slice(-50), // Последние 50 логов error: deployment.error } }); } catch (error) { logger.error('Ошибка при получении статуса деплоя:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при получении статуса' }); } }); /** * @route GET /api/dle-v2/deployment-result/:deploymentId * @desc Получить результат завершенного деплоя * @access Private */ router.get('/deployment-result/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { deploymentId } = req.params; const deployment = deploymentTracker.getDeployment(deploymentId); if (!deployment) { return res.status(404).json({ success: false, message: 'Деплой не найден' }); } if (deployment.status !== 'completed') { return res.status(400).json({ success: false, message: `Деплой не завершен. Текущий статус: ${deployment.status}`, status: deployment.status }); } res.json({ success: true, data: { result: deployment.result, completedAt: deployment.completedAt, duration: deployment.completedAt ? deployment.completedAt - deployment.startedAt : null } }); } catch (error) { logger.error('Ошибка при получении результата деплоя:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при получении результата' }); } }); /** * @route GET /api/dle-v2/deployment-stats * @desc Получить статистику деплоев * @access Private */ router.get('/deployment-stats', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const stats = deploymentTracker.getStats(); const activeDeployments = deploymentTracker.getActiveDeployments(); res.json({ success: true, data: { stats, activeDeployments: activeDeployments.map(d => ({ id: d.id, stage: d.stage, progress: d.progress, startedAt: d.startedAt })) } }); } catch (error) { logger.error('Ошибка при получении статистики деплоев:', error); res.status(500).json({ success: false, message: error.message || 'Произошла ошибка при получении статистики' }); } }); module.exports = router; /** * Дополнительные маршруты (подключаются из app.js) */ // Сохранить GUID верификации (если нужно отдельным вызовом) router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { address, chainId, guid } = req.body || {}; if (!address || !chainId || !guid) return res.status(400).json({ success: false, message: 'address, chainId, guid обязательны' }); const data = verificationStore.updateChain(address, chainId, { guid, status: 'submitted' }); return res.json({ success: true, data }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } }); // Получить статусы верификации по адресу DLE router.get('/verify/status/:address', auth.requireAuth, async (req, res) => { try { const { address } = req.params; const data = verificationStore.read(address); return res.json({ success: true, data }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } }); // Обновить статусы верификации, опросив Etherscan V2 router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { address } = req.params; let { etherscanApiKey } = req.body || {}; if (!etherscanApiKey) { try { const { getSecret } = require('../services/secretStore'); etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY'); } catch(_) {} } const data = verificationStore.read(address); if (!data || !data.chains) return res.json({ success: true, data }); // Если guid отсутствует или ранее была ошибка chainid — попробуем автоматически переотправить верификацию (resubmit) const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || '')); if (needResubmit && etherscanApiKey) { // Найти карточку DLE const list = dleV2Service.getAllDLEs(); const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); if (card) { const deployParams = { name: card.name, symbol: card.symbol, location: card.location, coordinates: card.coordinates, jurisdiction: card.jurisdiction, oktmo: card.oktmo, okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [], kpp: card.kpp, quorumPercentage: card.quorumPercentage, initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 }; const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; try { await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); } catch (_) {} } } // Далее — обычный опрос по имеющимся guid const latest = verificationStore.read(address); const chains = Object.values(latest.chains); for (const c of chains) { if (!c.guid || !c.chainId) continue; try { const st = await etherscanV2.checkStatus(c.chainId, c.guid, etherscanApiKey); verificationStore.updateChain(address, c.chainId, { status: st?.result || st?.message || 'unknown' }); } catch (e) { verificationStore.updateChain(address, c.chainId, { status: `error: ${e.message}` }); } } const updated = verificationStore.read(address); return res.json({ success: true, data: updated }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } }); // Повторно отправить верификацию на Etherscan V2 для уже созданного DLE router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { address } = req.params; const { etherscanApiKey } = req.body || {}; if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) { return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' }); } // Найти карточку DLE по адресу const list = dleV2Service.getAllDLEs(); const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' }); // Сформировать deployParams из карточки const deployParams = { name: card.name, symbol: card.symbol, location: card.location, coordinates: card.coordinates, jurisdiction: card.jurisdiction, oktmo: card.oktmo, okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [], kpp: card.kpp, quorumPercentage: card.quorumPercentage, initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 }; // Сформировать deployResult из карточки const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); const updated = verificationStore.read(address); return res.json({ success: true, data: updated }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } }); // Предварительная проверка балансов во всех выбранных сетях router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { supportedChainIds, privateKey } = req.body || {}; if (!privateKey) return res.status(400).json({ success: false, message: 'Приватный ключ не передан' }); if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) { return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' }); } const result = await dleV2Service.checkBalances(supportedChainIds, privateKey); return res.json({ success: true, data: result }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } });