ваше сообщение коммита
This commit is contained in:
523
backend/services/unifiedDeploymentService.js
Normal file
523
backend/services/unifiedDeploymentService.js
Normal 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;
|
||||
Reference in New Issue
Block a user