ваше сообщение коммита

This commit is contained in:
2025-09-30 00:23:37 +03:00
parent ca718e3178
commit 4b03951b31
77 changed files with 17161 additions and 7255 deletions

View File

@@ -0,0 +1,523 @@
/**
* Единый сервис для управления деплоем 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;