Files
DLE/backend/services/unifiedDeploymentService.js

524 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Единый сервис для управления деплоем 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<Object>} - Результат деплоя
*/
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<Object>} - Подготовленные параметры
*/
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<Array>} - Массив 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<Object>} - Результат деплоя
*/
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<Object>} - Результат верификации
*/
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<Object>} - Результат проверки
*/
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;