524 lines
20 KiB
JavaScript
524 lines
20 KiB
JavaScript
/**
|
||
* Единый сервис для управления деплоем 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;
|