/** * Единый сервис для управления деплоем DLE * Объединяет все операции с данными и деплоем * Copyright (c) 2024-2025 Тарабанов Александр Викторович */ const logger = require('../utils/logger'); const DeployParamsService = require('./deployParamsService'); const deploymentTracker = require('../utils/deploymentTracker'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const etherscanV2 = require('./etherscanV2VerificationService'); const { getRpcUrlByChainId } = require('./rpcProviderService'); const { ethers } = require('ethers'); // Убираем прямой импорт broadcastDeploymentUpdate - используем только deploymentTracker class UnifiedDeploymentService { constructor() { this.deployParamsService = new DeployParamsService(); } /** * Создает новый деплой DLE с полным циклом * @param {Object} dleParams - Параметры DLE из формы * @param {string} deploymentId - ID деплоя (опционально) * @returns {Promise} - Результат деплоя */ async createDLE(dleParams, deploymentId = null) { try { // 1. Генерируем ID деплоя if (!deploymentId) { deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; } logger.info(`🚀 Начало создания DLE: ${deploymentId}`); // 2. Валидируем параметры this.validateDLEParams(dleParams); // 3. Подготавливаем параметры для деплоя const deployParams = await this.prepareDeployParams(dleParams); // 4. Сохраняем в БД await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending'); logger.info(`💾 Параметры сохранены в БД: ${deploymentId}`); // 5. Запускаем деплой const result = await this.executeDeployment(deploymentId); // 6. Сохраняем результат await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result); logger.info(`✅ Деплой завершен: ${deploymentId}`); return { success: true, deploymentId, data: result }; } catch (error) { logger.error(`❌ Ошибка деплоя ${deploymentId}:`, error); // Обновляем статус на ошибку if (deploymentId) { await this.deployParamsService.updateDeploymentStatus(deploymentId, 'failed', { error: error.message }); } throw error; } } /** * Валидирует параметры DLE * @param {Object} params - Параметры для валидации */ validateDLEParams(params) { const required = ['name', 'symbol', 'privateKey', 'supportedChainIds']; const missing = required.filter(field => !params[field]); if (missing.length > 0) { throw new Error(`Отсутствуют обязательные поля: ${missing.join(', ')}`); } if (params.quorumPercentage < 1 || params.quorumPercentage > 100) { throw new Error('Кворум должен быть от 1 до 100 процентов'); } if (!params.initialPartners || params.initialPartners.length === 0) { throw new Error('Необходимо указать хотя бы одного партнера'); } if (!params.initialAmounts || params.initialAmounts.length === 0) { throw new Error('Необходимо указать начальные суммы для партнеров'); } if (params.initialPartners.length !== params.initialAmounts.length) { throw new Error('Количество партнеров должно совпадать с количеством сумм'); } if (!params.supportedChainIds || params.supportedChainIds.length === 0) { throw new Error('Необходимо указать поддерживаемые сети'); } } /** * Подготавливает параметры для деплоя * @param {Object} dleParams - Исходные параметры * @returns {Promise} - Подготовленные параметры */ async prepareDeployParams(dleParams) { // Генерируем RPC URLs на основе supportedChainIds из базы данных const rpcUrls = await this.generateRpcUrls(dleParams.supportedChainIds || []); return { name: dleParams.name, symbol: dleParams.symbol, location: dleParams.location || '', coordinates: dleParams.coordinates || '', jurisdiction: dleParams.jurisdiction || 1, oktmo: dleParams.oktmo || 45000000000, okved_codes: dleParams.okvedCodes || [], kpp: dleParams.kpp || 770101001, quorum_percentage: dleParams.quorumPercentage || 51, initial_partners: dleParams.initialPartners || [], // initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое initial_amounts: dleParams.initialAmounts || [], supported_chain_ids: dleParams.supportedChainIds || [], current_chain_id: 1, // Governance chain всегда Ethereum private_key: dleParams.privateKey, etherscan_api_key: dleParams.etherscanApiKey, logo_uri: dleParams.logoURI || '', create2_salt: dleParams.CREATE2_SALT || `0x${Math.random().toString(16).substring(2).padStart(64, '0')}`, auto_verify_after_deploy: dleParams.autoVerifyAfterDeploy || false, modules_to_deploy: dleParams.modulesToDeploy || [], rpc_urls: rpcUrls, deployment_status: 'pending' }; } /** * Генерирует RPC URLs на основе chain IDs из базы данных * @param {Array} chainIds - Массив chain IDs * @returns {Promise} - Массив RPC URLs */ async generateRpcUrls(chainIds) { const { getRpcUrlByChainId } = require('./rpcProviderService'); const rpcUrls = []; for (const chainId of chainIds) { try { const rpcUrl = await getRpcUrlByChainId(chainId); if (rpcUrl) { rpcUrls.push(rpcUrl); logger.info(`[RPC_GEN] Найден RPC для chainId ${chainId}: ${rpcUrl}`); } else { logger.warn(`[RPC_GEN] RPC не найден для chainId ${chainId}`); } } catch (error) { logger.error(`[RPC_GEN] Ошибка получения RPC для chainId ${chainId}:`, error.message); } } return rpcUrls; } /** * Выполняет деплой контрактов * @param {string} deploymentId - ID деплоя * @returns {Promise} - Результат деплоя */ async executeDeployment(deploymentId) { return new Promise((resolve, reject) => { const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js'); logger.info(`🚀 Запуск деплоя: ${scriptPath}`); const child = spawn('npx', ['hardhat', 'run', scriptPath], { cwd: path.join(__dirname, '..'), env: { ...process.env, DEPLOYMENT_ID: deploymentId }, stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { const output = data.toString(); stdout += output; logger.info(`[DEPLOY] ${output.trim()}`); // Определяем этап процесса по содержимому вывода let progress = 50; let message = 'Деплой в процессе...'; if (output.includes('Генерация ABI файла')) { progress = 10; message = 'Генерация ABI файла...'; } else if (output.includes('Генерация flattened контракта')) { progress = 20; message = 'Генерация flattened контракта...'; } else if (output.includes('Compiled') && output.includes('Solidity files')) { progress = 30; message = 'Компиляция контрактов...'; } else if (output.includes('Загружены параметры')) { progress = 40; message = 'Загрузка параметров деплоя...'; } else if (output.includes('deploying DLE directly')) { progress = 60; message = 'Деплой контрактов в сети...'; } else if (output.includes('Верификация в сети')) { progress = 80; message = 'Верификация контрактов...'; } // Отправляем WebSocket сообщение о прогрессе через deploymentTracker deploymentTracker.updateDeployment(deploymentId, { status: 'in_progress', progress: progress, message: message, output: output.trim() }); }); child.stderr.on('data', (data) => { const output = data.toString(); stderr += output; logger.error(`[DEPLOY ERROR] ${output.trim()}`); }); child.on('close', (code) => { if (code === 0) { try { const result = this.parseDeployResult(stdout); // Сохраняем результат в БД this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result) .then(() => { logger.info(`✅ Результат деплоя сохранен в БД: ${deploymentId}`); // Отправляем WebSocket сообщение о завершении через deploymentTracker deploymentTracker.completeDeployment(deploymentId, result); resolve(result); }) .catch(dbError => { logger.error(`❌ Ошибка сохранения результата в БД: ${dbError.message}`); resolve(result); // Все равно возвращаем результат }); } catch (error) { reject(new Error(`Ошибка парсинга результата: ${error.message}`)); } } else { // Логируем детали ошибки для отладки logger.error(`❌ Деплой завершился с ошибкой (код ${code})`); logger.error(`📋 stdout: ${stdout}`); logger.error(`📋 stderr: ${stderr}`); // Извлекаем конкретную ошибку из вывода const errorMessage = stderr || stdout || 'Неизвестная ошибка'; // Отправляем WebSocket сообщение об ошибке через deploymentTracker deploymentTracker.failDeployment(deploymentId, new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`)); reject(new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`)); } }); child.on('error', (error) => { reject(new Error(`Ошибка запуска деплоя: ${error.message}`)); }); }); } /** * Парсит результат деплоя из вывода скрипта * @param {string} stdout - Вывод скрипта * @returns {Object} - Структурированный результат */ parseDeployResult(stdout) { try { logger.info(`🔍 Анализируем вывод деплоя (${stdout.length} символов)`); // Ищем MULTICHAIN_DEPLOY_RESULT в выводе const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/); if (resultMatch) { const jsonStr = resultMatch[1].trim(); const deployResults = JSON.parse(jsonStr); logger.info(`📊 Результаты деплоя: ${JSON.stringify(deployResults, null, 2)}`); // Проверяем, что есть успешные деплои const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000' && !r.error); if (successfulDeploys.length > 0) { const dleAddress = successfulDeploys[0].address; logger.info(`✅ DLE адрес: ${dleAddress}`); return { success: true, data: { dleAddress: dleAddress, networks: deployResults.map(result => ({ chainId: result.chainId, address: result.address, success: result.address && result.address !== '0x0000000000000000000000000000000000000000' && !result.error, error: result.error || null, verification: result.verification || 'pending' })) }, message: `DLE успешно развернут в ${successfulDeploys.length} сетях` }; } else { // Если нет успешных деплоев, но есть результаты, возвращаем их с ошибками const failedDeploys = deployResults.filter(r => r.error); logger.warn(`⚠️ Все деплои неудачны. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}`); return { success: false, data: { networks: deployResults.map(result => ({ chainId: result.chainId, address: result.address || null, success: false, error: result.error || 'Unknown error' })) }, message: `Деплой неудачен во всех сетях. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}` }; } } // Fallback: создаем результат из текста return { success: true, message: 'Деплой выполнен успешно', output: stdout }; } catch (error) { logger.error('❌ Ошибка парсинга результата деплоя:', error); throw new Error(`Не удалось распарсить результат деплоя: ${error.message}`); } } /** * Получает статус деплоя * @param {string} deploymentId - ID деплоя * @returns {Object} - Статус деплоя */ async getDeploymentStatus(deploymentId) { return await this.deployParamsService.getDeployParams(deploymentId); } /** * Получает все деплои * @returns {Array} - Список деплоев */ async getAllDeployments() { return await this.deployParamsService.getAllDeployments(); } /** * Получает все DLE из файлов (для совместимости) * @returns {Array} - Список DLE */ getAllDLEs() { try { const dlesDir = path.join(__dirname, '../contracts-data/dles'); if (!fs.existsSync(dlesDir)) { return []; } const files = fs.readdirSync(dlesDir); const dles = []; for (const file of files) { if (file.includes('dle-v2-') && file.endsWith('.json')) { const filePath = path.join(dlesDir, file); try { const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (dleData.dleAddress) { dles.push(dleData); } } catch (err) { logger.error(`Ошибка при чтении файла ${file}:`, err); } } } return dles; } catch (error) { logger.error('Ошибка при получении списка DLE:', error); return []; } } /** * Автоматическая верификация контрактов во всех сетях * @param {Object} params - Параметры верификации * @returns {Promise} - Результат верификации */ async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) { try { logger.info('🔍 Начинаем автоматическую верификацию контрактов'); if (!deployResult?.data?.networks) { throw new Error('Нет данных о сетях для верификации'); } const verificationResults = []; for (const network of deployResult.data.networks) { try { logger.info(`🔍 Верификация в сети ${network.chainId}...`); const result = await etherscanV2.verifyContract({ contractAddress: network.dleAddress, chainId: network.chainId, deployParams, apiKey }); verificationResults.push({ chainId: network.chainId, address: network.dleAddress, success: result.success, guid: result.guid, message: result.message }); logger.info(`✅ Верификация в сети ${network.chainId} завершена`); } catch (error) { logger.error(`❌ Ошибка верификации в сети ${network.chainId}:`, error); verificationResults.push({ chainId: network.chainId, address: network.dleAddress, success: false, error: error.message }); } } return { success: true, results: verificationResults }; } catch (error) { logger.error('❌ Ошибка автоматической верификации:', error); throw error; } } /** * Проверяет балансы в указанных сетях * @param {Array} chainIds - Список ID сетей * @param {string} privateKey - Приватный ключ * @returns {Promise} - Результат проверки */ async checkBalances(chainIds, privateKey) { try { logger.info(`💰 Проверка балансов в ${chainIds.length} сетях`); const wallet = new ethers.Wallet(privateKey); const results = []; for (const chainId of chainIds) { try { const rpcUrl = await getRpcUrlByChainId(chainId); if (!rpcUrl) { results.push({ chainId, success: false, error: `RPC URL не найден для сети ${chainId}` }); continue; } // Убеждаемся, что rpcUrl - это строка const rpcUrlString = typeof rpcUrl === 'string' ? rpcUrl : rpcUrl.toString(); const provider = new ethers.JsonRpcProvider(rpcUrlString); const balance = await provider.getBalance(wallet.address); const balanceEth = ethers.formatEther(balance); results.push({ chainId, success: true, address: wallet.address, balance: balanceEth, balanceWei: balance.toString() }); logger.info(`💰 Сеть ${chainId}: ${balanceEth} ETH`); } catch (error) { logger.error(`❌ Ошибка проверки баланса в сети ${chainId}:`, error); results.push({ chainId, success: false, error: error.message }); } } return { success: true, results }; } catch (error) { logger.error('❌ Ошибка проверки балансов:', error); throw error; } } /** * Закрывает соединения */ async close() { await this.deployParamsService.close(); } } module.exports = UnifiedDeploymentService;