feat: Добавлены формы деплоя модулей DLE с полными настройками

- Создана форма деплоя TreasuryModule с детальными настройками казны
- Создана форма деплоя TimelockModule с настройками временных задержек
- Создана форма деплоя DLEReader с простой конфигурацией
- Добавлены маршруты и индексы для всех модулей
- Исправлены пути импорта BaseLayout
- Добавлены авторские права во все файлы
- Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
2025-09-23 02:57:59 +03:00
parent 9f94295d15
commit de0f8aecf2
63 changed files with 11631 additions and 1920 deletions

View File

@@ -16,6 +16,7 @@ const fs = require('fs');
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const { getRpcUrlByChainId } = require('./rpcProviderService');
const deploymentTracker = require('../utils/deploymentTracker');
const etherscanV2 = require('./etherscanV2VerificationService');
const verificationStore = require('./verificationStore');
@@ -29,11 +30,18 @@ class DLEV2Service {
* @param {Object} dleParams - Параметры DLE
* @returns {Promise<Object>} - Результат создания DLE
*/
async createDLE(dleParams) {
async createDLE(dleParams, deploymentId = null) {
console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!");
logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ");
let paramsFile = null;
let tempParamsFile = null;
try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
// WebSocket обновление: начало процесса
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Валидация параметров', 5, 'Проверяем входные данные');
}
// Валидация входных данных
this.validateDLEParams(dleParams);
@@ -50,6 +58,11 @@ class DLEV2Service {
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
}
// WebSocket обновление: генерация CREATE2_SALT
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса');
}
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
@@ -66,6 +79,11 @@ class DLEV2Service {
}
fs.copyFileSync(paramsFile, tempParamsFile);
// WebSocket обновление: поиск RPC URLs
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Поиск RPC endpoints', 15, 'Подключаемся к блокчейн сетям');
}
// Готовим RPC для всех выбранных сетей
const rpcUrls = [];
for (const cid of deployParams.supportedChainIds) {
@@ -99,14 +117,7 @@ class DLEV2Service {
const walletAddress = new ethers.Wallet(pk, provider).address;
const balance = await provider.getBalance(walletAddress);
if (typeof ethers.parseEther !== 'function') {
throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.00001");
if (typeof ethers.formatEther !== 'function') {
throw new Error('Метод ethers.formatEther не найден');
}
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) {
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
@@ -117,27 +128,87 @@ class DLEV2Service {
throw new Error('Приватный ключ для деплоя не передан');
}
// Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
const initCodeHash = await this.computeInitCodeHash({
...deployParams,
currentChainId: deployParams.currentChainId || deployParams.supportedChainIds[0]
});
// Сохраняем ключ Etherscan V2 ПЕРЕД деплоем
logger.info(`🔑 Etherscan API Key получен: ${dleParams.etherscanApiKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
try {
if (dleParams.etherscanApiKey) {
logger.info('🔑 Сохраняем Etherscan API Key в secretStore...');
const { setSecret } = require('./secretStore');
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
logger.info('🔑 Etherscan API Key успешно сохранен в базу данных');
} else {
logger.warn('🔑 Etherscan API Key не передан, пропускаем сохранение');
}
} catch (e) {
logger.error('🔑 Ошибка при сохранении Etherscan API Key:', e.message);
}
// WebSocket обновление: компиляция произойдет автоматически в deploy-multichain.js
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Подготовка к деплою', 25, 'Подготавливаем параметры для деплоя');
}
// INIT_CODE_HASH будет вычислен в deploy-multichain.js
// Factory больше не используется - деплой DLE напрямую
logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
// WebSocket обновление: начало мульти-чейн деплоя
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Мульти-чейн деплой', 40);
deploymentTracker.addLog(deploymentId, `🌐 Деплой в ${deployParams.supportedChainIds.length} сетях: ${deployParams.supportedChainIds.join(', ')}`, 'info');
deploymentTracker.addLog(deploymentId, `📋 Этапы: 1) DLE контракт → 2) Модули → 3) Инициализация → 4) Верификация`, 'info');
}
// Мультисетевой деплой одним вызовом
logger.info('Запуск мульти-чейн деплоя...');
logger.info("🔍 DEBUG: Подготовка к прямому деплою...");
const result = await this.runDeployMultichain(paramsFile, {
rpcUrls: rpcUrls,
chainIds: deployParams.supportedChainIds,
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
salt: create2Salt,
initCodeHash
etherscanApiKey: dleParams.etherscanApiKey
});
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
logger.info("🔍 DEBUG: Запуск мультисетевого деплоя...");
// WebSocket обновление: деплой завершен, начинаем обработку результатов
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Обработка результатов', 85, 'Деплой завершен, сохраняем результаты');
deploymentTracker.addLog(deploymentId, `✅ DLE контракт задеплоен в ${result.networks?.length || 0} сетях`, 'success');
if (result.networks) {
result.networks.forEach(network => {
deploymentTracker.addLog(deploymentId, `📍 ${network.networkName || `Chain ${network.chainId}`}: ${network.address}`, 'info');
});
}
// Логируем информацию о модулях
if (result.modules) {
deploymentTracker.addLog(deploymentId, `🔧 Модули задеплоены в ${result.modules.length} сетях`, 'info');
result.modules.forEach((moduleSet, index) => {
if (moduleSet && !moduleSet.error) {
deploymentTracker.addLog(deploymentId, `📦 Сеть ${index + 1}: Treasury=${moduleSet.treasuryModule?.substring(0, 10)}..., Timelock=${moduleSet.timelockModule?.substring(0, 10)}..., Reader=${moduleSet.dleReader?.substring(0, 10)}...`, 'info');
}
});
}
// Логируем информацию о верификации
if (result.verification) {
deploymentTracker.addLog(deploymentId, `🔍 Верификация выполнена в ${result.verification.length} сетях`, 'info');
result.verification.forEach((verification, index) => {
if (verification && !verification.error) {
const dleStatus = verification.dle === 'success' ? '✅' : '❌';
const treasuryStatus = verification.treasuryModule === 'success' ? '✅' : '❌';
const timelockStatus = verification.timelockModule === 'success' ? '✅' : '❌';
const readerStatus = verification.dleReader === 'success' ? '✅' : '❌';
deploymentTracker.addLog(deploymentId, `🔍 Сеть ${index + 1}: DLE${dleStatus} Treasury${treasuryStatus} Timelock${timelockStatus} Reader${readerStatus}`, 'info');
}
});
}
}
// Сохраняем информацию о созданном DLE для отображения на странице управления
try {
@@ -148,6 +219,7 @@ class DLEV2Service {
logger.error('Неверная структура результата деплоя:', result);
throw new Error('Неверная структура результата деплоя');
}
logger.info("🔍 DEBUG: Вызываем runDeployMultichain...");
// Если результат - массив (прямой результат из скрипта), преобразуем его
let deployResult = result;
@@ -209,6 +281,14 @@ class DLEV2Service {
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
// WebSocket обновление: финализация
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Завершение', 100, 'Деплой успешно завершен!');
deploymentTracker.addLog(deploymentId, `🎉 DLE ${result.data.name} (${result.data.symbol}) успешно создан!`, 'success');
deploymentTracker.addLog(deploymentId, `📊 Партнеров: ${result.data.partnerBalances?.length || 0}`, 'info');
deploymentTracker.addLog(deploymentId, `💰 Общий supply: ${result.data.totalSupply || 'N/A'}`, 'info');
}
return {
success: true,
data: dleData
@@ -220,31 +300,25 @@ class DLEV2Service {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
}
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
try {
if (dleParams.etherscanApiKey) {
const { setSecret } = require('./secretStore');
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
}
} catch (_) {}
// Etherscan API Key уже сохранен в начале функции
// Авто-верификация через Etherscan V2 (опционально)
if (dleParams.autoVerifyAfterDeploy) {
try {
await this.autoVerifyAcrossChains({
deployParams,
deployResult: result,
apiKey: dleParams.etherscanApiKey
});
} catch (e) {
logger.warn('Авто-верификация завершилась с ошибкой:', e.message);
}
// Верификация выполняется в deploy-multichain.js
// WebSocket обновление: деплой успешно завершен
if (deploymentId) {
deploymentTracker.completeDeployment(deploymentId, result);
}
return result;
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
// WebSocket обновление: деплой завершился с ошибкой
if (deploymentId) {
deploymentTracker.failDeployment(deploymentId, error);
}
throw error;
} finally {
try {
@@ -423,9 +497,6 @@ class DLEV2Service {
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
try {
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(rawAmount.toString(), 18).toString();
}
if (typeof rawAmount === 'string') {
@@ -435,9 +506,6 @@ class DLEV2Service {
return BigInt(a).toString();
}
// Десятичная строка — конвертируем в base units
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(a, 18).toString();
}
// BigInt или иные типы — приводим к строке без изменения масштаба
@@ -530,7 +598,7 @@ class DLEV2Service {
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '';
@@ -575,13 +643,19 @@ class DLEV2Service {
const envVars = {
...process.env,
PRIVATE_KEY: opts.privateKey
PRIVATE_KEY: opts.privateKey,
ETHERSCAN_API_KEY: opts.etherscanApiKey || ''
};
logger.info(`🔑 Передаем в deploy-multichain.js: ETHERSCAN_API_KEY=${opts.etherscanApiKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
logger.info(`🔑 Передаем в deploy-multichain.js: PRIVATE_KEY=${opts.privateKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
logger.info(`🔑 PRIVATE_KEY длина: ${opts.privateKey ? opts.privateKey.length : 0}`);
logger.info(`🔑 PRIVATE_KEY значение: ${opts.privateKey ? opts.privateKey.substring(0, 10) + '...' : 'undefined'}`);
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '', stderr = '';
@@ -838,11 +912,11 @@ class DLEV2Service {
// Преобразуем группы в массив
return Array.from(groups.values()).map(group => ({
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
// Общее количество сетей
totalNetworks: group.networks.length,
totalNetworks: group.networks.length,
// Поддерживаемые сети
supportedChainIds: group.networks.map(n => n.chainId)
}));
@@ -894,96 +968,6 @@ class DLEV2Service {
}
}
// Авто-расчёт INIT_CODE_HASH
async computeInitCodeHash(params) {
try {
// Проверяем наличие обязательных параметров
if (!params.name || !params.symbol || !params.location) {
throw new Error('Отсутствуют обязательные параметры для вычисления INIT_CODE_HASH');
}
const hre = require('hardhat');
const { ethers } = hre;
// Проверяем, что контракт DLE существует
try {
const DLE = await hre.ethers.getContractFactory('DLE');
if (!DLE) {
throw new Error('Контракт DLE не найден в Hardhat');
}
} catch (contractError) {
throw new Error(`Ошибка загрузки контракта DLE: ${contractError.message}`);
}
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates || "",
jurisdiction: params.jurisdiction || 1,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp || 0,
quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners || [],
initialAmounts: params.initialAmounts || [],
supportedChainIds: params.supportedChainIds || [1]
};
// Учитываем актуальную сигнатуру конструктора: (dleConfig, currentChainId, initializer)
const initializer = params.initializerAddress || "0x0000000000000000000000000000000000000000";
const currentChainId = params.currentChainId || 1; // Fallback на Ethereum mainnet
logger.info('Вычисление INIT_CODE_HASH с параметрами:', {
name: dleConfig.name,
symbol: dleConfig.symbol,
currentChainId,
initializer
});
// Проверяем, что метод getDeployTransaction существует
if (typeof DLE.getDeployTransaction !== 'function') {
throw new Error('Метод getDeployTransaction не найден в контракте DLE');
}
const deployTx = await DLE.getDeployTransaction(dleConfig, currentChainId, initializer);
if (!deployTx || !deployTx.data) {
throw new Error('Не удалось получить данные транзакции деплоя');
}
const initCode = deployTx.data;
// Проверяем, что метод keccak256 существует
if (typeof ethers.keccak256 !== 'function') {
throw new Error('Метод ethers.keccak256 не найден');
}
const hash = ethers.keccak256(initCode);
logger.info('INIT_CODE_HASH вычислен успешно:', hash);
return hash;
} catch (error) {
logger.error('Ошибка при вычислении INIT_CODE_HASH:', error);
// Fallback: возвращаем хеш на основе параметров
const { ethers } = require('ethers');
const fallbackData = JSON.stringify({
name: params.name,
symbol: params.symbol,
location: params.location,
jurisdiction: params.jurisdiction,
supportedChainIds: params.supportedChainIds
});
// Проверяем, что методы существуют
if (typeof ethers.toUtf8Bytes !== 'function') {
throw new Error('Метод ethers.toUtf8Bytes не найден');
}
if (typeof ethers.keccak256 !== 'function') {
throw new Error('Метод ethers.keccak256 не найден');
}
return ethers.keccak256(ethers.toUtf8Bytes(fallbackData));
}
}
@@ -1017,14 +1001,7 @@ class DLEV2Service {
const wallet = new ethers.Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address);
if (typeof ethers.formatEther !== 'function') {
throw new Error('Метод ethers.formatEther не найден');
}
const balanceEth = ethers.formatEther(balance);
if (typeof ethers.parseEther !== 'function') {
throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.001");
const ok = balance >= minBalance;
@@ -1057,155 +1034,7 @@ class DLEV2Service {
};
}
/**
* Авто-верификация контракта во всех выбранных сетях через Etherscan V2
* @param {Object} args
* @param {Object} args.deployParams
* @param {Object} args.deployResult - { success, data: { dleAddress, networks: [{rpcUrl,address}] } }
* @param {string} [args.apiKey]
*/
async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) {
if (!deployResult?.success) throw new Error('Нет результата деплоя для верификации');
// Подхватить ключ из secrets, если аргумент не передан
if (!apiKey) {
try {
const { getSecret } = require('./secretStore');
apiKey = await getSecret('ETHERSCAN_V2_API_KEY');
} catch (_) {}
}
// Получаем компилер, standard-json-input и contractName из artifacts/build-info
const { standardJson, compilerVersion, contractName, constructorArgsHex } = await this.prepareVerificationPayload(deployParams);
// Для каждой сети отправим верификацию, используя адрес из результата для соответствующего chainId
const chainIds = Array.isArray(deployParams.supportedChainIds) ? deployParams.supportedChainIds : [];
const netMap = new Map();
if (Array.isArray(deployResult.data?.networks)) {
for (const n of deployResult.data.networks) {
if (n && typeof n.chainId === 'number') netMap.set(n.chainId, n.address);
}
}
for (const cid of chainIds) {
try {
const addrForChain = netMap.get(cid);
if (!addrForChain) {
logger.warn(`[AutoVerify] Нет адреса для chainId=${cid} в результате деплоя, пропускаю`);
continue;
}
const guid = await etherscanV2.submitVerification({
chainId: cid,
contractAddress: addrForChain,
contractName,
compilerVersion,
standardJsonInput: standardJson,
constructorArgsHex,
apiKey
});
logger.info(`[AutoVerify] Отправлена верификация в chainId=${cid}, guid=${guid}`);
verificationStore.updateChain(addrForChain, cid, { guid, status: 'submitted' });
} catch (e) {
logger.warn(`[AutoVerify] Ошибка отправки верификации для chainId=${cid}: ${e.message}`);
const addrForChain = netMap.get(cid) || 'unknown';
verificationStore.updateChain(addrForChain, cid, { status: `error: ${e.message}` });
}
}
}
/**
* Формирует стандартный JSON input, compilerVersion, contractName и ABI-кодированные аргументы конструктора
*/
async prepareVerificationPayload(params) {
const hre = require('hardhat');
const path = require('path');
const fs = require('fs');
// 1) Найти самый свежий build-info
const buildInfoDir = path.join(__dirname, '..', 'artifacts', 'build-info');
let latestFile = null;
try {
const entries = fs.readdirSync(buildInfoDir).filter(f => f.endsWith('.json'));
let bestMtime = 0;
for (const f of entries) {
const fp = path.join(buildInfoDir, f);
const st = fs.statSync(fp);
if (st.mtimeMs > bestMtime) { bestMtime = st.mtimeMs; latestFile = fp; }
}
} catch (e) {
logger.warn('Артефакты build-info не найдены:', e.message);
}
let standardJson = null;
let compilerVersion = null;
let sourcePathForDLE = 'contracts/DLE.sol';
let contractName = 'contracts/DLE.sol:DLE';
if (latestFile) {
try {
const buildInfo = JSON.parse(fs.readFileSync(latestFile, 'utf8'));
// input — это стандартный JSON input для solc
standardJson = buildInfo.input || null;
// Версия компилятора
const long = buildInfo.solcLongVersion || buildInfo.solcVersion || hre.config.solidity?.version;
compilerVersion = long ? (long.startsWith('v') ? long : `v${long}`) : undefined;
// Найти путь контракта DLE
if (buildInfo.output && buildInfo.output.contracts) {
for (const [filePathKey, contractsMap] of Object.entries(buildInfo.output.contracts)) {
if (contractsMap && contractsMap['DLE']) {
sourcePathForDLE = filePathKey;
contractName = `${filePathKey}:DLE`;
break;
}
}
}
} catch (e) {
logger.warn('Не удалось прочитать build-info:', e.message);
}
}
// Если не нашли — fallback на config
if (!compilerVersion) compilerVersion = `v${hre.config.solidity.compilers?.[0]?.version || hre.config.solidity.version}`;
if (!standardJson) {
// fallback минимальная структура
standardJson = {
language: 'Solidity',
sources: { [sourcePathForDLE]: { content: '' } },
settings: { optimizer: { enabled: true, runs: 200 } }
};
}
// 2) Посчитать ABI-код аргументов конструктора через сравнение с bytecode
// Конструктор: (dleConfig, currentChainId, initializer)
const Factory = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates,
jurisdiction: params.jurisdiction,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp,
quorumPercentage: params.quorumPercentage,
initialPartners: params.initialPartners,
initialAmounts: params.initialAmounts,
supportedChainIds: params.supportedChainIds
};
const initializer = params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000";
const deployTx = await Factory.getDeployTransaction(dleConfig, params.currentChainId, initializer);
const fullData = deployTx.data; // 0x + creation bytecode + encoded args
const bytecode = Factory.bytecode; // 0x + creation bytecode
let constructorArgsHex;
try {
if (fullData && bytecode && fullData.startsWith(bytecode)) {
constructorArgsHex = '0x' + fullData.slice(bytecode.length);
}
} catch (e) {
logger.warn('Не удалось выделить constructorArguments из deployTx.data:', e.message);
}
return { standardJson, compilerVersion, contractName, constructorArgsHex };
}
}
module.exports = new DLEV2Service();