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

@@ -0,0 +1,617 @@
/* eslint-disable no-console */
const hre = require('hardhat');
const path = require('path');
const fs = require('fs');
// Подбираем безопасные gas/fee для разных сетей (включая L2)
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
const fee = await provider.getFeeData();
const overrides = {};
const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))());
const minFee = (await (async () => hre.ethers.parseUnits(minFeeGwei.toString(), 'gwei'))());
if (fee.maxFeePerGas) {
overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas;
overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n)
? fee.maxPriorityFeePerGas
: minPriority;
} else if (fee.gasPrice) {
overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice;
}
return overrides;
}
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit) {
const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider);
const net = await provider.getNetwork();
// DEBUG: базовая информация по сети
try {
const calcInitHash = ethers.keccak256(dleInit);
const saltLen = ethers.getBytes(salt).length;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
console.log(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`);
console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`);
console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`);
console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`);
console.log(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
} catch (e) {
console.log('[MULTI_DBG] precheck error', e?.message || e);
}
// 1) Выравнивание nonce до targetDLENonce нулевыми транзакциями (если нужно)
let current = await provider.getTransactionCount(wallet.address, 'pending');
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetDLENonce}`);
if (current > targetDLENonce) {
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
}
if (current < targetDLENonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
// Используем burn address для более надежных транзакций
const burnAddress = "0x000000000000000000000000000000000000dEaD";
while (current < targetDLENonce) {
const overrides = await getFeeOverrides(provider);
let gasLimit = 21000; // минимальный gas для обычной транзакции
let sent = false;
let lastErr = null;
for (let attempt = 0; attempt < 3 && !sent; attempt++) {
try {
const txReq = {
to: burnAddress, // отправляем на burn address вместо своего адреса
value: 0n,
nonce: current,
gasLimit,
...overrides
};
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
const txFill = await wallet.sendTransaction(txReq);
await txFill.wait();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
sent = true;
} catch (e) {
lastErr = e;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`);
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) {
gasLimit = 50000; // увеличиваем gas limit
continue;
}
if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) {
// Обновляем nonce и пробуем снова
current = await provider.getTransactionCount(wallet.address, 'pending');
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`);
continue;
}
throw e;
}
}
if (!sent) {
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
throw lastErr || new Error('filler tx failed');
}
current++;
}
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
}
// 2) Деплой DLE напрямую на согласованном nonce
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE directly with nonce=${targetDLENonce}`);
const feeOverrides = await getFeeOverrides(provider);
let gasLimit;
try {
// Оцениваем газ для деплоя DLE
const est = await wallet.estimateGas({ data: dleInit, ...feeOverrides }).catch(() => null);
// Рассчитываем доступный gasLimit из баланса
const balance = await provider.getBalance(wallet.address, 'latest');
const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
const reserve = hre.ethers.parseEther('0.005');
const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n;
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
gasLimit = est ? (est + est / 5n) : fallbackGas;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
} catch (_) {
gasLimit = 3_000_000n;
}
// Вычисляем предсказанный адрес DLE
const predictedAddress = ethers.getCreateAddress({
from: wallet.address,
nonce: targetDLENonce
});
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`);
// Проверяем, не развернут ли уже контракт
const existingCode = await provider.getCode(predictedAddress);
if (existingCode && existingCode !== '0x') {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`);
return { address: predictedAddress, chainId: Number(net.chainId) };
}
// Деплоим DLE
let tx;
try {
tx = await wallet.sendTransaction({
data: dleInit,
nonce: targetDLENonce,
gasLimit,
...feeOverrides
});
} catch (e) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`);
// Повторная попытка с обновленным nonce
const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending');
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`);
tx = await wallet.sendTransaction({
data: dleInit,
nonce: updatedNonce,
gasLimit,
...feeOverrides
});
}
const rc = await tx.wait();
const deployedAddress = rc.contractAddress || predictedAddress;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`);
return { address: deployedAddress, chainId: Number(net.chainId) };
}
// Деплой модулей в одной сети
async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) {
const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider);
const net = await provider.getNetwork();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying modules...`);
const modules = {};
// Получаем начальный nonce для всех модулей
let currentNonce = await wallet.getNonce();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce for modules: ${currentNonce}`);
// Функция для безопасного деплоя с правильным nonce
async function deployWithNonce(contractFactory, args, moduleName) {
try {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying ${moduleName} with nonce: ${currentNonce}`);
// Проверяем, что nonce актуален
const actualNonce = await wallet.getNonce();
if (actualNonce > currentNonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch, updating from ${currentNonce} to ${actualNonce}`);
currentNonce = actualNonce;
}
const contract = await contractFactory.connect(wallet).deploy(...args);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployed at: ${address}`);
currentNonce++;
return address;
} catch (error) {
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployment failed:`, error.message);
// Даже при ошибке увеличиваем nonce, чтобы не было конфликтов
currentNonce++;
return null;
}
}
// Деплой TreasuryModule
const TreasuryModule = await hre.ethers.getContractFactory('TreasuryModule');
modules.treasuryModule = await deployWithNonce(
TreasuryModule,
[dleAddress, Number(net.chainId), wallet.address], // _dleContract, _chainId, _emergencyAdmin
'TreasuryModule'
);
// Деплой TimelockModule
const TimelockModule = await hre.ethers.getContractFactory('TimelockModule');
modules.timelockModule = await deployWithNonce(
TimelockModule,
[dleAddress], // _dleContract
'TimelockModule'
);
// Деплой DLEReader
const DLEReader = await hre.ethers.getContractFactory('DLEReader');
modules.dleReader = await deployWithNonce(
DLEReader,
[dleAddress], // _dleContract
'DLEReader'
);
// Инициализация модулей в DLE
try {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing modules in DLE with nonce: ${currentNonce}`);
// Проверяем, что nonce актуален
const actualNonce = await wallet.getNonce();
if (actualNonce > currentNonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before module init, updating from ${currentNonce} to ${actualNonce}`);
currentNonce = actualNonce;
}
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
// Проверяем, что все модули задеплоены
const treasuryAddress = modules.treasuryModule;
const timelockAddress = modules.timelockModule;
const readerAddress = modules.dleReader;
if (treasuryAddress && timelockAddress && readerAddress) {
// Инициализация базовых модулей
await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`);
currentNonce++;
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`);
}
} catch (error) {
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message);
// Даже при ошибке увеличиваем nonce
currentNonce++;
}
// Инициализация logoURI
try {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI with nonce: ${currentNonce}`);
// Проверяем, что nonce актуален
const actualNonce = await wallet.getNonce();
if (actualNonce > currentNonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before logoURI init, updating from ${currentNonce} to ${actualNonce}`);
currentNonce = actualNonce;
}
// Используем логотип из параметров деплоя или fallback
const logoURL = params.logoURI || "https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE";
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
await dleContract.initializeLogoURI(logoURL);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized: ${logoURL}`);
currentNonce++;
} catch (e) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${e.message}`);
// Fallback на базовый логотип
try {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} trying fallback logoURI with nonce: ${currentNonce}`);
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
await dleContract.initializeLogoURI("https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE");
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI initialized`);
currentNonce++;
} catch (fallbackError) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI also failed: ${fallbackError.message}`);
// Даже при ошибке увеличиваем nonce
currentNonce++;
}
}
return modules;
}
// Деплой модулей во всех сетях
async function deployModulesInAllNetworks(networks, pk, dleAddress, params) {
const moduleResults = [];
for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i];
console.log(`[MULTI_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`);
try {
const modules = await deployModulesInNetwork(rpcUrl, pk, dleAddress, params);
moduleResults.push(modules);
} catch (error) {
console.error(`[MULTI_DBG] Failed to deploy modules in network ${i + 1}:`, error.message);
moduleResults.push({ error: error.message });
}
}
return moduleResults;
}
// Верификация контрактов в одной сети
async function verifyContractsInNetwork(rpcUrl, pk, dleAddress, modules, params) {
const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider);
const net = await provider.getNetwork();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting verification...`);
const verification = {};
try {
// Верификация DLE
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLE...`);
await hre.run("verify:verify", {
address: dleAddress,
constructorArguments: [
{
name: params.name || '',
symbol: params.symbol || '',
location: params.location || '',
coordinates: params.coordinates || '',
jurisdiction: params.jurisdiction || 0,
oktmo: params.oktmo || '',
okvedCodes: params.okvedCodes || [],
kpp: params.kpp ? BigInt(params.kpp) : 0n,
quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners || [],
initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)),
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
},
BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1),
params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"
],
});
verification.dle = 'success';
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification successful`);
} catch (error) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification failed: ${error.message}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification error details:`, error);
verification.dle = 'failed';
}
// Верификация модулей
if (modules && !modules.error) {
try {
// Верификация TreasuryModule
if (modules.treasuryModule) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TreasuryModule...`);
await hre.run("verify:verify", {
address: modules.treasuryModule,
constructorArguments: [
dleAddress, // _dleContract
Number(net.chainId), // _chainId
wallet.address // _emergencyAdmin
],
});
verification.treasuryModule = 'success';
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification successful`);
}
} catch (error) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification failed: ${error.message}`);
verification.treasuryModule = 'failed';
}
try {
// Верификация TimelockModule
if (modules.timelockModule) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TimelockModule...`);
await hre.run("verify:verify", {
address: modules.timelockModule,
constructorArguments: [
dleAddress // _dleContract
],
});
verification.timelockModule = 'success';
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification successful`);
}
} catch (error) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification failed: ${error.message}`);
verification.timelockModule = 'failed';
}
try {
// Верификация DLEReader
if (modules.dleReader) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLEReader...`);
await hre.run("verify:verify", {
address: modules.dleReader,
constructorArguments: [
dleAddress // _dleContract
],
});
verification.dleReader = 'success';
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification successful`);
}
} catch (error) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification failed: ${error.message}`);
verification.dleReader = 'failed';
}
}
return verification;
}
// Верификация контрактов во всех сетях
async function verifyContractsInAllNetworks(networks, pk, dleAddress, moduleResults, params) {
const verificationResults = [];
for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i];
console.log(`[MULTI_DBG] verifying contracts in network ${i + 1}/${networks.length}: ${rpcUrl}`);
try {
const verification = await verifyContractsInNetwork(rpcUrl, pk, dleAddress, moduleResults[i], params);
verificationResults.push(verification);
} catch (error) {
console.error(`[MULTI_DBG] Failed to verify contracts in network ${i + 1}:`, error.message);
verificationResults.push({ error: error.message });
}
}
return verificationResults;
}
async function main() {
const { ethers } = hre;
// Загружаем параметры из файла
const paramsPath = path.join(__dirname, './current-params.json');
if (!fs.existsSync(paramsPath)) {
throw new Error('Файл параметров не найден: ' + paramsPath);
}
const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'));
console.log('[MULTI_DBG] Загружены параметры:', {
name: params.name,
symbol: params.symbol,
supportedChainIds: params.supportedChainIds,
CREATE2_SALT: params.CREATE2_SALT
});
const pk = process.env.PRIVATE_KEY;
const salt = params.CREATE2_SALT;
const networks = params.rpcUrls || [];
if (!pk) throw new Error('Env: PRIVATE_KEY');
if (!salt) throw new Error('CREATE2_SALT not found in params');
if (networks.length === 0) throw new Error('RPC URLs not found in params');
// Prepare init code once
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name || '',
symbol: params.symbol || '',
location: params.location || '',
coordinates: params.coordinates || '',
jurisdiction: params.jurisdiction || 0,
oktmo: params.oktmo || '',
okvedCodes: params.okvedCodes || [],
kpp: params.kpp ? BigInt(params.kpp) : 0n,
quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners || [],
initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)),
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
};
const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000");
const dleInit = deployTx.data;
const initCodeHash = ethers.keccak256(dleInit);
// DEBUG: глобальные значения
try {
const saltLen = ethers.getBytes(salt).length;
console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`);
console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`);
console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
} catch (e) {
console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e);
}
// Подготовим провайдеры и вычислим общий nonce для DLE
const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u));
const wallets = providers.map(p => new hre.ethers.Wallet(pk, p));
const nonces = [];
for (let i = 0; i < providers.length; i++) {
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
nonces.push(n);
}
const targetDLENonce = Math.max(...nonces);
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
const results = [];
for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i];
console.log(`[MULTI_DBG] deploying to network ${i + 1}/${networks.length}: ${rpcUrl}`);
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
results.push({ rpcUrl, ...r });
}
// Проверяем, что все адреса одинаковые
const addresses = results.map(r => r.address);
const uniqueAddresses = [...new Set(addresses)];
if (uniqueAddresses.length > 1) {
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
throw new Error('Nonce alignment failed - addresses are different');
}
console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
// Деплой модулей во всех сетях
console.log('[MULTI_DBG] Starting module deployment...');
const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0], params);
// Верификация контрактов
console.log('[MULTI_DBG] Starting contract verification...');
const verificationResults = await verifyContractsInAllNetworks(networks, pk, uniqueAddresses[0], moduleResults, params);
// Объединяем результаты
const finalResults = results.map((result, index) => ({
...result,
modules: moduleResults[index] || {},
verification: verificationResults[index] || {}
}));
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
// Сохраняем информацию о модулях в отдельный файл для каждого DLE
// Добавляем информацию о сетях (chainId, rpcUrl)
const modulesInfo = {
dleAddress: uniqueAddresses[0],
networks: networks.map((rpcUrl, index) => ({
rpcUrl: rpcUrl,
chainId: null, // Будет заполнено ниже
networkName: null // Будет заполнено ниже
})),
modules: moduleResults,
verification: verificationResults,
deployTimestamp: new Date().toISOString()
};
// Получаем chainId для каждой сети
for (let i = 0; i < networks.length; i++) {
try {
const provider = new hre.ethers.JsonRpcProvider(networks[i]);
const network = await provider.getNetwork();
modulesInfo.networks[i].chainId = Number(network.chainId);
// Определяем название сети по chainId
const networkNames = {
1: 'Ethereum Mainnet',
5: 'Goerli',
11155111: 'Sepolia',
137: 'Polygon Mainnet',
80001: 'Mumbai',
56: 'BSC Mainnet',
97: 'BSC Testnet',
42161: 'Arbitrum One',
421614: 'Arbitrum Sepolia',
10: 'Optimism',
11155420: 'Optimism Sepolia',
8453: 'Base',
84532: 'Base Sepolia'
};
modulesInfo.networks[i].networkName = networkNames[Number(network.chainId)] || `Chain ID ${Number(network.chainId)}`;
console.log(`[MULTI_DBG] Сеть ${i + 1}: chainId=${Number(network.chainId)}, name=${modulesInfo.networks[i].networkName}`);
} catch (error) {
console.error(`[MULTI_DBG] Ошибка получения chainId для сети ${i + 1}:`, error.message);
modulesInfo.networks[i].chainId = null;
modulesInfo.networks[i].networkName = `Сеть ${i + 1}`;
}
}
// Создаем директорию temp если её нет
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const deployResultPath = path.join(tempDir, `modules-${uniqueAddresses[0].toLowerCase()}.json`);
fs.writeFileSync(deployResultPath, JSON.stringify(modulesInfo, null, 2));
console.log(`[MULTI_DBG] Modules info saved to: ${deployResultPath}`);
}
main().catch((e) => { console.error(e); process.exit(1); });

View File

@@ -0,0 +1,452 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const dleV2Service = require('../services/dleV2Service');
const logger = require('../utils/logger');
const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation
const create2 = require('../utils/create2');
const verificationStore = require('../services/verificationStore');
const etherscanV2 = require('../services/etherscanV2VerificationService');
/**
* @route POST /api/dle-v2
* @desc Создать новое DLE v2 (Digital Legal Entity)
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const dleParams = req.body;
logger.info('Получен запрос на создание DLE v2:', dleParams);
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) {
// Проверяем, есть ли в сессии адрес кошелька пользователя
if (!req.user || !req.user.walletAddress) {
return res.status(400).json({
success: false,
message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов'
});
}
// Используем адрес авторизованного пользователя
dleParams.initialPartners = [req.user.address || req.user.walletAddress];
// Если суммы не указаны, используем значение по умолчанию (100% токенов)
if (!dleParams.initialAmounts || dleParams.initialAmounts.length === 0) {
dleParams.initialAmounts = ['1000000000000000000000000']; // 1,000,000 токенов
}
}
// Создаем DLE v2
const result = await dleV2Service.createDLE(dleParams);
logger.info('DLE v2 успешно создано:', result);
res.json({
success: true,
message: 'DLE v2 успешно создано',
data: result.data
});
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при создании DLE v2'
});
}
});
/**
* @route GET /api/dle-v2
* @desc Получить список всех DLE v2
* @access Public (доступно всем пользователям)
*/
router.get('/', async (req, res, next) => {
try {
const dles = dleV2Service.getAllDLEs();
res.json({
success: true,
data: dles
});
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении списка DLE v2'
});
}
});
/**
* @route GET /api/dle-v2/default-params
* @desc Получить параметры по умолчанию для создания DLE v2
* @access Private
*/
router.get('/default-params', auth.requireAuth, async (req, res, next) => {
try {
const defaultParams = {
name: '',
symbol: '',
location: '',
coordinates: '',
jurisdiction: 1,
oktmo: 45000000000,
okvedCodes: [],
kpp: 770101001,
quorumPercentage: 51,
initialPartners: [],
initialAmounts: [],
supportedChainIds: [1, 137, 56, 42161],
currentChainId: 1
};
res.json({
success: true,
data: defaultParams
});
} catch (error) {
logger.error('Ошибка при получении параметров по умолчанию:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении параметров по умолчанию'
});
}
});
/**
* @route DELETE /api/dle-v2/:dleAddress
* @desc Удалить DLE v2 по адресу
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const { dleAddress } = req.params;
logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`);
// Проверяем существование DLE v2 в директории contracts-data/dles
const dlesDir = path.join(__dirname, '../contracts-data/dles');
const files = fs.readdirSync(dlesDir);
let fileToDelete = null;
// Находим файл, содержащий указанный адрес DLE
for (const file of files) {
if (file.includes('dle-v2-') && file.endsWith('.json')) {
const filePath = path.join(dlesDir, file);
if (fs.statSync(filePath).isFile()) {
try {
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) {
fileToDelete = filePath;
break;
}
} catch (err) {
logger.error(`Ошибка при чтении файла ${file}:`, err);
}
}
}
}
if (!fileToDelete) {
return res.status(404).json({
success: false,
message: `DLE v2 с адресом ${dleAddress} не найдено`
});
}
// Удаляем файл
fs.unlinkSync(fileToDelete);
logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`);
res.json({
success: true,
message: `DLE v2 с адресом ${dleAddress} успешно удалено`
});
} catch (error) {
logger.error('Ошибка при удалении DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при удалении DLE v2'
});
}
});
/**
* @route GET /api/dle-v2/check-admin-tokens
* @desc Проверить баланс админских токенов для адреса
* @access Public
*/
router.get('/check-admin-tokens', async (req, res, next) => {
try {
const { address } = req.query;
if (!address) {
return res.status(400).json({
success: false,
message: 'Адрес кошелька не передан'
});
}
// Проверяем баланс токенов
const { checkAdminRole } = require('../services/admin-role');
const isAdmin = await checkAdminRole(address);
res.json({
success: true,
data: {
isAdmin: isAdmin,
address: address,
message: isAdmin ? 'Админские токены найдены' : 'Админские токены не найдены'
}
});
} catch (error) {
logger.error('Ошибка при проверке админских токенов:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при проверке админских токенов'
});
}
});
/**
* @route POST /api/dle-v2/validate-private-key
* @desc Валидировать приватный ключ и получить адрес кошелька
* @access Public
*/
router.post('/validate-private-key', async (req, res, next) => {
try {
const { privateKey } = req.body;
if (!privateKey) {
return res.status(400).json({
success: false,
message: 'Приватный ключ не передан'
});
}
// Логируем входящий ключ (только для отладки)
logger.info('Получен приватный ключ для валидации:', privateKey);
logger.info('Длина входящего ключа:', privateKey.length);
logger.info('Тип входящего ключа:', typeof privateKey);
logger.info('Полный объект запроса:', JSON.stringify(req.body));
try {
// Очищаем ключ от префикса 0x если есть
const cleanKey = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
// Логируем очищенный ключ (только для отладки)
logger.info('Очищенный ключ:', cleanKey);
logger.info('Длина очищенного ключа:', cleanKey.length);
// Проверяем длину и формат (64 символа в hex)
if (cleanKey.length !== 64 || !/^[a-fA-F0-9]+$/.test(cleanKey)) {
logger.error('Некорректный формат ключа. Длина:', cleanKey.length, 'Формат:', /^[a-fA-F0-9]+$/.test(cleanKey));
return res.status(400).json({
success: false,
message: 'Некорректный формат приватного ключа'
});
}
// Генерируем адрес из приватного ключа
const wallet = new ethers.Wallet('0x' + cleanKey);
const address = wallet.address;
// Логируем сгенерированный адрес
logger.info('Сгенерированный адрес из приватного ключа:', address);
res.json({
success: true,
data: {
isValid: true,
address: address,
error: null
}
});
} catch (error) {
logger.error('Ошибка при генерации адреса из приватного ключа:', error);
res.status(400).json({
success: false,
message: 'Некорректный приватный ключ'
});
}
} catch (error) {
logger.error('Ошибка при валидации приватного ключа:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при валидации приватного ключа'
});
}
});
module.exports = router;
/**
* Дополнительные маршруты (подключаются из app.js)
*/
// Сохранить GUID верификации (если нужно отдельным вызовом)
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address, chainId, guid } = req.body || {};
if (!address || !chainId || !guid) return res.status(400).json({ success: false, message: 'address, chainId, guid обязательны' });
const data = verificationStore.updateChain(address, chainId, { guid, status: 'submitted' });
return res.json({ success: true, data });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Получить статусы верификации по адресу DLE
router.get('/verify/status/:address', auth.requireAuth, async (req, res) => {
try {
const { address } = req.params;
const data = verificationStore.read(address);
return res.json({ success: true, data });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Обновить статусы верификации, опросив Etherscan V2
router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address } = req.params;
let { etherscanApiKey } = req.body || {};
if (!etherscanApiKey) {
try {
const { getSecret } = require('../services/secretStore');
etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY');
} catch(_) {}
}
const data = verificationStore.read(address);
if (!data || !data.chains) return res.json({ success: true, data });
// Если guid отсутствует или ранее была ошибка chainid — попробуем автоматически переотправить верификацию (resubmit)
const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || ''));
if (needResubmit && etherscanApiKey) {
// Найти карточку DLE
const list = dleV2Service.getAllDLEs();
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
if (card) {
const deployParams = {
name: card.name,
symbol: card.symbol,
location: card.location,
coordinates: card.coordinates,
jurisdiction: card.jurisdiction,
oktmo: card.oktmo,
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
kpp: card.kpp,
quorumPercentage: card.quorumPercentage,
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
};
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
try {
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
} catch (_) {}
}
}
// Далее — обычный опрос по имеющимся guid
const latest = verificationStore.read(address);
const chains = Object.values(latest.chains);
for (const c of chains) {
if (!c.guid || !c.chainId) continue;
try {
const st = await etherscanV2.checkStatus(c.chainId, c.guid, etherscanApiKey);
verificationStore.updateChain(address, c.chainId, { status: st?.result || st?.message || 'unknown' });
} catch (e) {
verificationStore.updateChain(address, c.chainId, { status: `error: ${e.message}` });
}
}
const updated = verificationStore.read(address);
return res.json({ success: true, data: updated });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Повторно отправить верификацию на Etherscan V2 для уже созданного DLE
router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address } = req.params;
const { etherscanApiKey } = req.body || {};
if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) {
return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' });
}
// Найти карточку DLE по адресу
const list = dleV2Service.getAllDLEs();
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' });
// Сформировать deployParams из карточки
const deployParams = {
name: card.name,
symbol: card.symbol,
location: card.location,
coordinates: card.coordinates,
jurisdiction: card.jurisdiction,
oktmo: card.oktmo,
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
kpp: card.kpp,
quorumPercentage: card.quorumPercentage,
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
};
// Сформировать deployResult из карточки
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
const updated = verificationStore.read(address);
return res.json({ success: true, data: updated });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Предварительная проверка балансов во всех выбранных сетях
router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { supportedChainIds, privateKey } = req.body || {};
if (!privateKey) return res.status(400).json({ success: false, message: 'Приватный ключ не передан' });
if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) {
return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' });
}
const result = await dleV2Service.checkBalances(supportedChainIds, privateKey);
return res.json({ success: true, data: result });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});

View File

@@ -0,0 +1,971 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const { getRpcUrlByChainId } = require('./rpcProviderService');
const etherscanV2 = require('./etherscanV2VerificationService');
const verificationStore = require('./verificationStore');
/**
* Сервис для управления DLE v2 (Digital Legal Entity)
* Современный подход с единым контрактом
*/
class DLEV2Service {
/**
* Создает новое DLE v2 с заданными параметрами
* @param {Object} dleParams - Параметры DLE
* @returns {Promise<Object>} - Результат создания DLE
*/
async createDLE(dleParams) {
console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!");
logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ");
let paramsFile = null;
let tempParamsFile = null;
try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
// Валидация входных данных
this.validateDLEParams(dleParams);
// Подготовка параметров для деплоя
const deployParams = this.prepareDeployParams(dleParams);
// Вычисляем адрес инициализатора (инициализатором является деплоер из переданного приватного ключа)
try {
const normalizedPk = dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
const initializerAddress = new ethers.Wallet(normalizedPk).address;
deployParams.initializerAddress = initializerAddress;
} catch (e) {
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
}
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
// Сохраняем параметры во временный файл
paramsFile = this.saveParamsToFile(deployParams);
// Копируем параметры во временный файл с предсказуемым именем
tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
const deployDir = path.dirname(tempParamsFile);
if (!fs.existsSync(deployDir)) {
fs.mkdirSync(deployDir, { recursive: true });
}
fs.copyFileSync(paramsFile, tempParamsFile);
// Готовим RPC для всех выбранных сетей
const rpcUrls = [];
for (const cid of deployParams.supportedChainIds) {
logger.info(`Поиск RPC URL для chain_id: ${cid}`);
const ru = await getRpcUrlByChainId(cid);
if (!ru) {
throw new Error(`RPC URL для сети с chain_id ${cid} не найден в базе данных`);
}
rpcUrls.push(ru);
}
// Добавляем CREATE2_SALT, RPC_URLS и initializer в файл параметров
const currentParams = JSON.parse(fs.readFileSync(tempParamsFile, 'utf8'));
// Копируем все параметры из deployParams
Object.assign(currentParams, deployParams);
currentParams.CREATE2_SALT = create2Salt;
currentParams.rpcUrls = rpcUrls;
currentParams.currentChainId = deployParams.currentChainId || deployParams.supportedChainIds[0];
const { ethers } = require('ethers');
currentParams.initializer = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000";
fs.writeFileSync(tempParamsFile, JSON.stringify(currentParams, null, 2));
logger.info(`Файл параметров скопирован и обновлен с CREATE2_SALT`);
// Лёгкая проверка баланса в первой сети
{
const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider(rpcUrls[0]);
if (dleParams.privateKey) {
const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
const walletAddress = new ethers.Wallet(pk, provider).address;
const balance = await provider.getBalance(walletAddress);
const minBalance = ethers.parseEther("0.00001");
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) {
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
}
}
}
if (!dleParams.privateKey) {
throw new Error('Приватный ключ для деплоя не передан');
}
// Сначала компилируем контракты
logger.info("🔨 Компилируем контракты перед вычислением INIT_CODE_HASH...");
try {
const { spawn } = require('child_process');
await new Promise((resolve, reject) => {
const compile = spawn('npx', ['hardhat', 'compile'], {
cwd: process.cwd(),
stdio: 'inherit'
});
compile.on('close', (code) => {
if (code === 0) {
logger.info('✅ Контракты скомпилированы успешно');
resolve();
} else {
logger.warn(`⚠️ Компиляция завершилась с кодом: ${code}`);
resolve(); // Продолжаем даже при ошибке компиляции
}
});
compile.on('error', (error) => {
logger.warn('⚠️ Ошибка компиляции:', error.message);
resolve(); // Продолжаем даже при ошибке
});
});
} catch (compileError) {
logger.warn('⚠️ Ошибка компиляции:', compileError.message);
}
// INIT_CODE_HASH будет вычислен в deploy-multichain.js
// Factory больше не используется - деплой DLE напрямую
logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
// Мультисетевой деплой одним вызовом
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
});
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
logger.info("🔍 DEBUG: Запуск мультисетевого деплоя...");
// Сохраняем информацию о созданном DLE для отображения на странице управления
try {
logger.info('Результат деплоя для сохранения:', JSON.stringify(result, null, 2));
// Проверяем структуру результата
if (!result || typeof result !== 'object') {
logger.error('Неверная структура результата деплоя:', result);
throw new Error('Неверная структура результата деплоя');
}
logger.info("🔍 DEBUG: Вызываем runDeployMultichain...");
// Если результат - массив (прямой результат из скрипта), преобразуем его
let deployResult = result;
if (Array.isArray(result)) {
logger.info('Результат - массив, преобразуем в объект');
const addresses = result.map(r => r.address);
const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase());
deployResult = {
success: true,
data: {
dleAddress: addresses[0],
networks: result.map((r, index) => ({
chainId: r.chainId,
address: r.address,
success: true
})),
allSame
}
};
}
const firstNet = Array.isArray(deployResult?.data?.networks) && deployResult.data.networks.length > 0 ? deployResult.data.networks[0] : null;
const dleData = {
name: deployParams.name,
symbol: deployParams.symbol,
location: deployParams.location,
coordinates: deployParams.coordinates,
jurisdiction: deployParams.jurisdiction,
okvedCodes: deployParams.okvedCodes || [],
kpp: deployParams.kpp,
quorumPercentage: deployParams.quorumPercentage,
initialPartners: deployParams.initialPartners || [],
initialAmounts: deployParams.initialAmounts || [],
governanceSettings: {
quorumPercentage: deployParams.quorumPercentage,
supportedChainIds: deployParams.supportedChainIds,
currentChainId: deployParams.currentChainId
},
dleAddress: (deployResult?.data?.dleAddress) || (firstNet?.address) || null,
version: 'v2',
networks: deployResult?.data?.networks || [],
createdAt: new Date().toISOString()
};
// logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2)); // Убрано избыточное логирование
if (dleData.dleAddress) {
// Сохраняем данные DLE в файл
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${timestamp}.json`;
const savedPath = path.join(__dirname, '../contracts-data/dles', fileName);
// Создаем директорию, если её нет
const dlesDir = path.dirname(savedPath);
if (!fs.existsSync(dlesDir)) {
fs.mkdirSync(dlesDir, { recursive: true });
}
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
return {
success: true,
data: dleData
};
} else {
throw new Error('DLE адрес не получен после деплоя');
}
} catch (e) {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
}
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
try {
if (dleParams.etherscanApiKey) {
const { setSecret } = require('./secretStore');
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
}
} catch (_) {}
// Верификация выполняется в deploy-multichain.js
return result;
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
throw error;
} finally {
try {
if (paramsFile || tempParamsFile) {
this.cleanupTempFiles(paramsFile, tempParamsFile);
}
} catch (e) {
logger.warn('Ошибка при очистке временных файлов (finally):', e.message);
}
try {
this.pruneOldTempFiles(24 * 60 * 60 * 1000);
} catch (e) {
logger.warn('Ошибка при автоочистке старых временных файлов:', e.message);
}
}
}
/**
* Валидирует параметры DLE
* @param {Object} params - Параметры для валидации
*/
validateDLEParams(params) {
if (!params.name || params.name.trim() === '') {
throw new Error('Название DLE обязательно');
}
if (!params.symbol || params.symbol.trim() === '') {
throw new Error('Символ токена обязателен');
}
if (!params.location || params.location.trim() === '') {
throw new Error('Местонахождение DLE обязательно');
}
if (!params.initialPartners || !Array.isArray(params.initialPartners)) {
throw new Error('Партнеры должны быть массивом');
}
if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) {
throw new Error('Суммы должны быть массивом');
}
if (params.initialPartners.length !== params.initialAmounts.length) {
throw new Error('Количество партнеров должно соответствовать количеству сумм распределения');
}
if (params.initialPartners.length === 0) {
throw new Error('Должен быть указан хотя бы один партнер');
}
if (params.quorumPercentage > 100 || params.quorumPercentage < 1) {
throw new Error('Процент кворума должен быть от 1% до 100%');
}
// Проверяем адреса партнеров
for (let i = 0; i < params.initialPartners.length; i++) {
if (!ethers.isAddress || !ethers.isAddress(params.initialPartners[i])) {
throw new Error(`Неверный адрес партнера ${i + 1}: ${params.initialPartners[i]}`);
}
}
// Проверяем, что выбраны сети
if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds) || params.supportedChainIds.length === 0) {
throw new Error('Должна быть выбрана хотя бы одна сеть для деплоя');
}
// Дополнительные проверки безопасности
if (params.name.length > 100) {
throw new Error('Название DLE слишком длинное (максимум 100 символов)');
}
if (params.symbol.length > 10) {
throw new Error('Символ токена слишком длинный (максимум 10 символов)');
}
if (params.location.length > 200) {
throw new Error('Местонахождение слишком длинное (максимум 200 символов)');
}
// Проверяем суммы токенов
for (let i = 0; i < params.initialAmounts.length; i++) {
const amount = params.initialAmounts[i];
if (typeof amount !== 'string' && typeof amount !== 'number') {
throw new Error(`Неверный тип суммы для партнера ${i + 1}`);
}
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount) || numAmount <= 0) {
throw new Error(`Неверная сумма для партнера ${i + 1}: ${amount}`);
}
}
// Проверяем приватный ключ
if (!params.privateKey) {
throw new Error('Приватный ключ обязателен для деплоя');
}
const pk = params.privateKey.startsWith('0x') ? params.privateKey : `0x${params.privateKey}`;
if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) {
throw new Error('Неверный формат приватного ключа');
}
// Проверяем, что не деплоим в mainnet без подтверждения
const mainnetChains = [1, 137, 56, 42161]; // Ethereum, Polygon, BSC, Arbitrum
const hasMainnet = params.supportedChainIds.some(id => mainnetChains.includes(id));
if (hasMainnet) {
logger.warn('⚠️ ВНИМАНИЕ: Деплой включает mainnet сети! Убедитесь, что это необходимо.');
}
logger.info('✅ Валидация параметров DLE пройдена успешно');
}
/**
* Сохраняет/обновляет локальную карточку DLE для отображения в UI
* @param {Object} dleData
* @returns {string} Путь к сохраненному файлу
*/
saveDLEData(dleData) {
try {
if (!dleData || !dleData.dleAddress) {
throw new Error('Неверные данные для сохранения карточки DLE: отсутствует dleAddress');
}
const dlesDir = path.join(__dirname, '../contracts-data/dles');
if (!fs.existsSync(dlesDir)) {
fs.mkdirSync(dlesDir, { recursive: true });
}
// Если уже есть файл с таким адресом — обновим его
let targetFile = null;
try {
const files = fs.readdirSync(dlesDir);
for (const file of files) {
if (file.endsWith('.json') && file.includes('dle-v2-')) {
const fp = path.join(dlesDir, file);
try {
const existing = JSON.parse(fs.readFileSync(fp, 'utf8'));
if (existing?.dleAddress && existing.dleAddress.toLowerCase() === dleData.dleAddress.toLowerCase()) {
targetFile = fp;
// Совмещаем данные (не удаляя существующие поля сетей/верификации, если присутствуют)
dleData = { ...existing, ...dleData };
break;
}
} catch (_) {}
}
}
} catch (_) {}
if (!targetFile) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${ts}.json`;
targetFile = path.join(dlesDir, fileName);
}
fs.writeFileSync(targetFile, JSON.stringify(dleData, null, 2));
logger.info(`Карточка DLE сохранена: ${targetFile}`);
return targetFile;
} catch (e) {
logger.error('Ошибка сохранения карточки DLE:', e);
throw e;
}
}
/**
* Подготавливает параметры для деплоя
* @param {Object} params - Параметры DLE из формы
* @returns {Object} - Подготовленные параметры для скрипта деплоя
*/
prepareDeployParams(params) {
// Создаем копию объекта, чтобы не изменять исходный
const deployParams = { ...params };
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) {
deployParams.initialAmounts = deployParams.initialAmounts.map(rawAmount => {
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
try {
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
return ethers.parseUnits(rawAmount.toString(), 18).toString();
}
if (typeof rawAmount === 'string') {
const a = rawAmount.trim();
if (a.startsWith('0x')) {
// Уже base units (hex BigNumber) — оставляем как есть
return BigInt(a).toString();
}
// Десятичная строка — конвертируем в base units
return ethers.parseUnits(a, 18).toString();
}
// BigInt или иные типы — приводим к строке без изменения масштаба
return rawAmount.toString();
} catch (e) {
// Фолбэк: безопасно привести к строке
return String(rawAmount);
}
});
}
// Убеждаемся, что okvedCodes - это массив
if (!Array.isArray(deployParams.okvedCodes)) {
deployParams.okvedCodes = [];
}
// Преобразуем kpp в число
if (deployParams.kpp) {
deployParams.kpp = parseInt(deployParams.kpp) || 0;
} else {
deployParams.kpp = 0;
}
// Убеждаемся, что supportedChainIds - это массив
if (!Array.isArray(deployParams.supportedChainIds)) {
deployParams.supportedChainIds = [1]; // По умолчанию Ethereum
}
// Устанавливаем currentChainId как первую выбранную сеть
if (deployParams.supportedChainIds.length > 0) {
deployParams.currentChainId = deployParams.supportedChainIds[0];
} else {
deployParams.currentChainId = 1; // По умолчанию Ethereum
}
// Обрабатываем logoURI
if (deployParams.logoURI) {
// Если logoURI относительный путь, делаем его абсолютным
if (deployParams.logoURI.startsWith('/uploads/')) {
deployParams.logoURI = `http://localhost:8000${deployParams.logoURI}`;
}
// Если это placeholder, оставляем как есть
if (deployParams.logoURI.includes('placeholder.com')) {
// Оставляем как есть
}
}
return deployParams;
}
/**
* Сохраняет параметры во временный файл
* @param {Object} params - Параметры для сохранения
* @returns {string} - Путь к сохраненному файлу
*/
saveParamsToFile(params) {
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const fileName = `dle-v2-params-${Date.now()}.json`;
const filePath = path.join(tempDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(params, null, 2));
return filePath;
}
/**
* Запускает скрипт деплоя DLE v2
* @param {string} paramsFile - Путь к файлу с параметрами
* @returns {Promise<Object>} - Результат деплоя
*/
runDeployScript(paramsFile, extraEnv = {}) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
if (!fs.existsSync(scriptPath)) {
reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath));
return;
}
const envVars = {
...process.env,
RPC_URL: extraEnv.rpcUrl,
PRIVATE_KEY: extraEnv.privateKey
};
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
hardhatProcess.stdout.on('data', (data) => {
stdout += data.toString();
logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`);
});
hardhatProcess.stderr.on('data', (data) => {
stderr += data.toString();
logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`);
});
hardhatProcess.on('close', (code) => {
try {
const result = this.extractDeployResult(stdout);
resolve(result);
} catch (error) {
logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error);
if (code === 0) {
reject(new Error('Не удалось найти информацию о созданном DLE v2'));
} else {
reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`));
}
}
});
hardhatProcess.on('error', (error) => {
logger.error('Ошибка запуска скрипта деплоя DLE v2:', error);
reject(error);
});
});
}
// Мультисетевой деплой
runDeployMultichain(paramsFile, opts = {}) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден: ' + scriptPath));
const envVars = {
...process.env,
PRIVATE_KEY: opts.privateKey
};
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '', stderr = '';
p.stdout.on('data', (d) => {
stdout += d.toString();
logger.info(`[MULTICHAIN_DEPLOY] ${d.toString().trim()}`);
});
p.stderr.on('data', (d) => {
stderr += d.toString();
logger.error(`[MULTICHAIN_DEPLOY_ERR] ${d.toString().trim()}`);
});
p.on('close', (code) => {
try {
// Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*\])/);
if (resultMatch) {
const deployResults = JSON.parse(resultMatch[1]);
// Преобразуем результат в нужный формат
const addresses = deployResults.map(r => r.address);
const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase());
resolve({
success: true,
data: {
dleAddress: addresses[0],
networks: deployResults.map((r, index) => ({
chainId: r.chainId,
address: r.address,
success: true
})),
allSame
}
});
} else {
// Fallback: ищем адреса DLE в выводе по новому формату
const dleAddressMatches = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/g);
if (!dleAddressMatches || dleAddressMatches.length === 0) {
throw new Error('Не найдены адреса DLE в выводе');
}
const addresses = dleAddressMatches.map(match => match.match(/(0x[a-fA-F0-9]{40})/)[1]);
const addr = addresses[0];
const allSame = addresses.every(x => x.toLowerCase() === addr.toLowerCase());
if (!allSame) {
logger.warn('Адреса отличаются между сетями — продолжаем, сохраню по-сеточно', { addresses });
}
resolve({
success: true,
data: {
dleAddress: addr,
networks: addresses.map((address, index) => ({
chainId: opts.chainIds[index] || index + 1,
address,
success: true
})),
allSame
}
});
}
} catch (e) {
reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`));
}
});
p.on('error', (e) => reject(e));
});
}
/**
* Извлекает результат деплоя из stdout
* @param {string} stdout - Вывод скрипта
* @returns {Object} - Результат деплоя
*/
extractDeployResult(stdout) {
// Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*?\])/);
if (resultMatch) {
try {
const result = JSON.parse(resultMatch[1]);
return result;
} catch (e) {
logger.error('Ошибка парсинга JSON результата:', e);
}
}
// Fallback: ищем строки с адресами в выводе по новому формату
const dleAddressMatch = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/);
if (dleAddressMatch) {
return {
success: true,
data: {
dleAddress: dleAddressMatch[1],
version: 'v2'
}
};
}
// Если не нашли адрес, выводим весь stdout для отладки
console.log('Полный вывод скрипта:', stdout);
throw new Error('Не удалось извлечь адрес DLE из вывода скрипта');
}
/**
* Очищает временные файлы
* @param {string} paramsFile - Путь к файлу параметров
* @param {string} tempParamsFile - Путь к временному файлу параметров
*/
cleanupTempFiles(paramsFile, tempParamsFile) {
try {
if (fs.existsSync(paramsFile)) {
fs.unlinkSync(paramsFile);
}
if (fs.existsSync(tempParamsFile)) {
fs.unlinkSync(tempParamsFile);
}
} catch (error) {
logger.warn('Не удалось очистить временные файлы:', error);
}
}
/**
* Удаляет временные файлы параметров деплоя старше заданного возраста
* @param {number} maxAgeMs - Макс. возраст файлов в миллисекундах (по умолчанию 24ч)
*/
pruneOldTempFiles(maxAgeMs = 24 * 60 * 60 * 1000) {
const tempDir = path.join(__dirname, '../temp');
try {
if (!fs.existsSync(tempDir)) return;
const now = Date.now();
const files = fs.readdirSync(tempDir).filter(f => f.startsWith('dle-v2-params-') && f.endsWith('.json'));
for (const f of files) {
const fp = path.join(tempDir, f);
try {
const st = fs.statSync(fp);
if (now - st.mtimeMs > maxAgeMs) {
fs.unlinkSync(fp);
logger.info(`Удалён старый временный файл: ${fp}`);
}
} catch (e) {
logger.warn(`Не удалось обработать файл ${fp}: ${e.message}`);
}
}
} catch (e) {
logger.warn('Ошибка pruneOldTempFiles:', e.message);
}
}
/**
* Получает список всех созданных DLE v2
* @returns {Array<Object>} - Список DLE v2
*/
getAllDLEs() {
try {
const dlesDir = path.join(__dirname, '../contracts-data/dles');
if (!fs.existsSync(dlesDir)) {
return [];
}
const files = fs.readdirSync(dlesDir);
const allDles = files
.filter(file => file.endsWith('.json') && file.includes('dle-v2-'))
.map(file => {
try {
const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8'));
return { ...data, _fileName: file };
} catch (error) {
logger.error(`Ошибка при чтении файла ${file}:`, error);
return null;
}
})
.filter(dle => dle !== null);
// Группируем DLE по мультичейн деплоям
const groupedDles = this.groupMultichainDLEs(allDles);
return groupedDles;
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
return [];
}
}
/**
* Группирует DLE по мультичейн деплоям
* @param {Array<Object>} allDles - Все DLE из файлов
* @returns {Array<Object>} - Сгруппированные DLE
*/
groupMultichainDLEs(allDles) {
const groups = new Map();
for (const dle of allDles) {
// Создаем ключ для группировки на основе общих параметров
const groupKey = this.createGroupKey(dle);
if (!groups.has(groupKey)) {
groups.set(groupKey, {
// Основные данные из первого DLE
name: dle.name,
symbol: dle.symbol,
location: dle.location,
coordinates: dle.coordinates,
jurisdiction: dle.jurisdiction,
oktmo: dle.oktmo,
okvedCodes: dle.okvedCodes,
kpp: dle.kpp,
quorumPercentage: dle.quorumPercentage,
version: dle.version || 'v2',
deployedMultichain: true,
// Мультичейн информация
networks: [],
// Модули (одинаковые во всех сетях)
modules: dle.modules,
// Время создания (самое раннее)
creationTimestamp: dle.creationTimestamp,
creationBlock: dle.creationBlock
});
}
const group = groups.get(groupKey);
// Если у DLE есть массив networks, используем его
if (dle.networks && Array.isArray(dle.networks)) {
for (const network of dle.networks) {
group.networks.push({
chainId: network.chainId,
dleAddress: network.address || network.dleAddress,
factoryAddress: network.factoryAddress,
rpcUrl: network.rpcUrl || this.getRpcUrlForChain(network.chainId)
});
}
} else {
// Старый формат: добавляем информацию о сети из корня DLE
group.networks.push({
chainId: dle.chainId,
dleAddress: dle.dleAddress,
factoryAddress: dle.factoryAddress,
rpcUrl: dle.rpcUrl || this.getRpcUrlForChain(dle.chainId)
});
}
// Обновляем время создания на самое раннее
if (dle.creationTimestamp && (!group.creationTimestamp || dle.creationTimestamp < group.creationTimestamp)) {
group.creationTimestamp = dle.creationTimestamp;
}
}
// Преобразуем группы в массив
return Array.from(groups.values()).map(group => ({
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
// Общее количество сетей
totalNetworks: group.networks.length,
// Поддерживаемые сети
supportedChainIds: group.networks.map(n => n.chainId)
}));
}
/**
* Создает ключ для группировки DLE
* @param {Object} dle - Данные DLE
* @returns {string} - Ключ группировки
*/
createGroupKey(dle) {
// Группируем по основным параметрам DLE
const keyParts = [
dle.name,
dle.symbol,
dle.location,
dle.coordinates,
dle.jurisdiction,
dle.oktmo,
dle.kpp,
dle.quorumPercentage,
// Сортируем okvedCodes для стабильного ключа
Array.isArray(dle.okvedCodes) ? dle.okvedCodes.sort().join(',') : '',
// Сортируем supportedChainIds для стабильного ключа
Array.isArray(dle.supportedChainIds) ? dle.supportedChainIds.sort().join(',') : ''
];
return keyParts.join('|');
}
/**
* Получает RPC URL для сети
* @param {number} chainId - ID сети
* @returns {string|null} - RPC URL
*/
getRpcUrlForChain(chainId) {
try {
// Простая маппинг для основных сетей
const rpcMap = {
1: 'https://eth-mainnet.g.alchemy.com/v2/demo',
11155111: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52',
17000: 'https://ethereum-holesky.publicnode.com',
421614: 'https://sepolia-rollup.arbitrum.io/rpc',
84532: 'https://sepolia.base.org'
};
return rpcMap[chainId] || null;
} catch (error) {
return null;
}
}
/**
* Проверяет балансы в указанных сетях
* @param {number[]} chainIds - Массив chainId для проверки
* @param {string} privateKey - Приватный ключ
* @returns {Promise<Object>} - Результат проверки балансов
*/
async checkBalances(chainIds, privateKey) {
const { getRpcUrlByChainId } = require('./rpcProviderService');
const { ethers } = require('ethers');
const balances = [];
const insufficient = [];
for (const chainId of chainIds) {
try {
const rpcUrl = await getRpcUrlByChainId(chainId);
if (!rpcUrl) {
balances.push({
chainId,
balanceEth: '0',
ok: false,
error: 'RPC URL не найден'
});
insufficient.push(chainId);
continue;
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address);
const balanceEth = ethers.formatEther(balance);
const minBalance = ethers.parseEther("0.001");
const ok = balance >= minBalance;
balances.push({
chainId,
address: wallet.address,
balanceEth,
ok
});
if (!ok) {
insufficient.push(chainId);
}
} catch (error) {
balances.push({
chainId,
balanceEth: '0',
ok: false,
error: error.message
});
insufficient.push(chainId);
}
}
return {
balances,
insufficient,
allSufficient: insufficient.length === 0
};
}
}
module.exports = new DLEV2Service();