ваше сообщение коммита
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
# This software is proprietary and confidential.
|
||||
# For licensing inquiries: info@hb3-accelerator.com
|
||||
|
||||
FROM node:20-bookworm
|
||||
FROM node:20-alpine
|
||||
|
||||
# Добавляем метки для авторских прав
|
||||
LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>"
|
||||
|
||||
@@ -21,6 +21,7 @@ const errorHandler = require('./middleware/errorHandler');
|
||||
// const { version } = require('./package.json'); // Закомментировано, так как не используется
|
||||
const db = require('./db'); // Добавляем импорт db
|
||||
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
|
||||
const deploymentWebSocketService = require('./services/deploymentWebSocketService'); // WebSocket для деплоя
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const messagesRoutes = require('./routes/messages');
|
||||
@@ -91,6 +92,7 @@ const blockchainRoutes = require('./routes/blockchain'); // Добавляем
|
||||
const dleCoreRoutes = require('./routes/dleCore'); // Основные функции DLE
|
||||
const dleProposalsRoutes = require('./routes/dleProposals'); // Функции предложений
|
||||
const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей
|
||||
const moduleDeploymentRoutes = require('./routes/moduleDeployment'); // Деплой модулей
|
||||
const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов
|
||||
const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история
|
||||
const compileRoutes = require('./routes/compile'); // Компиляция контрактов
|
||||
@@ -195,6 +197,7 @@ app.use('/api/blockchain', blockchainRoutes); // Добавляем маршру
|
||||
app.use('/api/dle-core', dleCoreRoutes); // Основные функции DLE
|
||||
app.use('/api/dle-proposals', dleProposalsRoutes); // Функции предложений
|
||||
app.use('/api/dle-modules', dleModulesRoutes); // Функции модулей
|
||||
app.use('/api/module-deployment', moduleDeploymentRoutes); // Деплой модулей
|
||||
app.use('/api/dle-tokens', dleTokensRoutes); // Функции токенов
|
||||
app.use('/api/dle-analytics', dleAnalyticsRoutes); // Аналитика и история
|
||||
app.use('/api/dle-multichain', dleMultichainRoutes); // Мультичейн функции
|
||||
|
||||
471
backend/contracts/HierarchicalVotingModule.sol
Normal file
471
backend/contracts/HierarchicalVotingModule.sol
Normal file
@@ -0,0 +1,471 @@
|
||||
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||||
// 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
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/utils/Address.sol";
|
||||
|
||||
/**
|
||||
* @title HierarchicalVotingModule
|
||||
* @dev Модуль для иерархического голосования между DLE
|
||||
*
|
||||
* ОСНОВНЫЕ ФУНКЦИИ:
|
||||
* - Владение токенами других DLE
|
||||
* - Создание предложений для голосования в других DLE
|
||||
* - Внутреннее голосование для внешнего голосования
|
||||
* - Выполнение внешнего голосования после достижения кворума
|
||||
*
|
||||
* БЕЗОПАСНОСТЬ:
|
||||
* - Только DLE контракт может выполнять операции
|
||||
* - Защита от реентерабельности
|
||||
* - Валидация всех входных параметров
|
||||
* - Проверка прав через governance
|
||||
*/
|
||||
contract HierarchicalVotingModule is ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
using Address for address;
|
||||
|
||||
// Структура для внешнего голосования
|
||||
struct ExternalVotingProposal {
|
||||
address targetDLE; // Адрес целевого DLE
|
||||
uint256 targetProposalId; // ID предложения в целевом DLE
|
||||
bool support; // Поддержка предложения
|
||||
string reason; // Причина голосования
|
||||
bool executed; // Выполнено ли внешнее голосование
|
||||
uint256 internalProposalId; // ID внутреннего предложения в DLE
|
||||
uint256 votingPower; // Сила голоса (количество токенов)
|
||||
uint256 createdAt; // Время создания
|
||||
}
|
||||
|
||||
// Структура для информации о внешнем DLE
|
||||
struct ExternalDLEInfo {
|
||||
address dleAddress; // Адрес DLE
|
||||
string name; // Название DLE
|
||||
string symbol; // Символ токена DLE
|
||||
uint256 tokenBalance; // Количество токенов на балансе
|
||||
bool isActive; // Активен ли DLE
|
||||
uint256 addedAt; // Время добавления
|
||||
}
|
||||
|
||||
// Основные переменные
|
||||
address public immutable dleContract; // Адрес основного DLE контракта
|
||||
address public treasuryModule; // Адрес TreasuryModule (может быть установлен позже)
|
||||
|
||||
// Хранение внешних DLE
|
||||
mapping(address => ExternalDLEInfo) public externalDLEs;
|
||||
address[] public externalDLEList;
|
||||
mapping(address => uint256) public externalDLEIndex;
|
||||
|
||||
// Внешние предложения
|
||||
mapping(uint256 => ExternalVotingProposal) public externalVotingProposals;
|
||||
uint256 public externalProposalCounter;
|
||||
|
||||
// Статистика
|
||||
uint256 public totalExternalDLEs;
|
||||
uint256 public totalExternalProposals;
|
||||
uint256 public totalExternalVotes;
|
||||
|
||||
// События
|
||||
event TreasuryModuleSet(address indexed treasuryModule, uint256 timestamp);
|
||||
event ExternalDLEAdded(
|
||||
address indexed dleAddress,
|
||||
string name,
|
||||
string symbol,
|
||||
uint256 tokenBalance,
|
||||
uint256 timestamp
|
||||
);
|
||||
event ExternalDLERemoved(address indexed dleAddress, uint256 timestamp);
|
||||
event ExternalVotingProposalCreated(
|
||||
uint256 indexed proposalId,
|
||||
address indexed targetDLE,
|
||||
uint256 targetProposalId,
|
||||
bool support,
|
||||
string reason,
|
||||
uint256 internalProposalId
|
||||
);
|
||||
event ExternalVoteExecuted(
|
||||
uint256 indexed proposalId,
|
||||
address indexed targetDLE,
|
||||
uint256 targetProposalId,
|
||||
bool support,
|
||||
uint256 votingPower
|
||||
);
|
||||
event ExternalDLEBalanceUpdated(
|
||||
address indexed dleAddress,
|
||||
uint256 oldBalance,
|
||||
uint256 newBalance
|
||||
);
|
||||
|
||||
// Модификаторы
|
||||
modifier onlyDLE() {
|
||||
require(msg.sender == dleContract, "Only DLE contract can call this");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validExternalDLE(address dleAddress) {
|
||||
require(externalDLEs[dleAddress].isActive, "External DLE not active");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _dleContract) {
|
||||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||||
|
||||
dleContract = _dleContract;
|
||||
treasuryModule = address(0); // Будет установлен позже через governance
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Установить адрес TreasuryModule (только через DLE governance)
|
||||
* @param _treasuryModule Адрес TreasuryModule
|
||||
*/
|
||||
function setTreasuryModule(address _treasuryModule) external onlyDLE {
|
||||
require(_treasuryModule != address(0), "Treasury module cannot be zero");
|
||||
require(_treasuryModule.code.length > 0, "Treasury module contract does not exist");
|
||||
|
||||
treasuryModule = _treasuryModule;
|
||||
|
||||
emit TreasuryModuleSet(_treasuryModule, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Добавить внешний DLE (только через DLE governance)
|
||||
* @param dleAddress Адрес внешнего DLE
|
||||
* @param name Название DLE
|
||||
* @param symbol Символ токена DLE
|
||||
*/
|
||||
function addExternalDLE(
|
||||
address dleAddress,
|
||||
string memory name,
|
||||
string memory symbol
|
||||
) external onlyDLE {
|
||||
require(dleAddress != address(0), "DLE address cannot be zero");
|
||||
require(!externalDLEs[dleAddress].isActive, "External DLE already added");
|
||||
require(bytes(name).length > 0, "Name cannot be empty");
|
||||
require(bytes(symbol).length > 0, "Symbol cannot be empty");
|
||||
require(treasuryModule != address(0), "Treasury module not set");
|
||||
|
||||
// Проверяем, что DLE контракт существует
|
||||
require(dleAddress.code.length > 0, "DLE contract does not exist");
|
||||
|
||||
// Получаем баланс токенов этого DLE в TreasuryModule
|
||||
uint256 tokenBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||||
|
||||
externalDLEs[dleAddress] = ExternalDLEInfo({
|
||||
dleAddress: dleAddress,
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
tokenBalance: tokenBalance,
|
||||
isActive: true,
|
||||
addedAt: block.timestamp
|
||||
});
|
||||
|
||||
externalDLEList.push(dleAddress);
|
||||
externalDLEIndex[dleAddress] = externalDLEList.length - 1;
|
||||
totalExternalDLEs++;
|
||||
|
||||
emit ExternalDLEAdded(dleAddress, name, symbol, tokenBalance, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Удалить внешний DLE (только через DLE governance)
|
||||
* @param dleAddress Адрес внешнего DLE
|
||||
*/
|
||||
function removeExternalDLE(address dleAddress) external onlyDLE validExternalDLE(dleAddress) {
|
||||
require(externalDLEs[dleAddress].tokenBalance == 0, "Token balance must be zero");
|
||||
|
||||
// Удаляем из массива
|
||||
uint256 index = externalDLEIndex[dleAddress];
|
||||
uint256 lastIndex = externalDLEList.length - 1;
|
||||
|
||||
if (index != lastIndex) {
|
||||
address lastDLE = externalDLEList[lastIndex];
|
||||
externalDLEList[index] = lastDLE;
|
||||
externalDLEIndex[lastDLE] = index;
|
||||
}
|
||||
|
||||
externalDLEList.pop();
|
||||
delete externalDLEIndex[dleAddress];
|
||||
delete externalDLEs[dleAddress];
|
||||
totalExternalDLEs--;
|
||||
|
||||
emit ExternalDLERemoved(dleAddress, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение для внешнего голосования
|
||||
* @param targetDLE Адрес целевого DLE
|
||||
* @param targetProposalId ID предложения в целевом DLE
|
||||
* @param support Поддержка предложения
|
||||
* @param reason Причина голосования
|
||||
* @return proposalId ID созданного предложения
|
||||
*/
|
||||
function createExternalVotingProposal(
|
||||
address targetDLE,
|
||||
uint256 targetProposalId,
|
||||
bool support,
|
||||
string memory reason
|
||||
) external onlyDLE validExternalDLE(targetDLE) returns (uint256) {
|
||||
require(targetProposalId > 0, "Target proposal ID must be positive");
|
||||
require(bytes(reason).length > 0, "Reason cannot be empty");
|
||||
|
||||
ExternalDLEInfo memory dleInfo = externalDLEs[targetDLE];
|
||||
require(dleInfo.tokenBalance > 0, "No tokens in target DLE");
|
||||
|
||||
// Создаем описание для внутреннего предложения
|
||||
string memory description = string(abi.encodePacked(
|
||||
"Vote in DLE ", dleInfo.name, " (", dleInfo.symbol, ") on proposal #",
|
||||
_toString(targetProposalId), ": ", reason
|
||||
));
|
||||
|
||||
// Создаем внутреннее предложение через DLE
|
||||
// Это требует интеграции с DLE контрактом
|
||||
uint256 internalProposalId = _createInternalProposal(description);
|
||||
|
||||
uint256 proposalId = externalProposalCounter++;
|
||||
externalVotingProposals[proposalId] = ExternalVotingProposal({
|
||||
targetDLE: targetDLE,
|
||||
targetProposalId: targetProposalId,
|
||||
support: support,
|
||||
reason: reason,
|
||||
executed: false,
|
||||
internalProposalId: internalProposalId,
|
||||
votingPower: dleInfo.tokenBalance,
|
||||
createdAt: block.timestamp
|
||||
});
|
||||
|
||||
totalExternalProposals++;
|
||||
|
||||
emit ExternalVotingProposalCreated(
|
||||
proposalId,
|
||||
targetDLE,
|
||||
targetProposalId,
|
||||
support,
|
||||
reason,
|
||||
internalProposalId
|
||||
);
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить внешнее голосование (после прохождения внутреннего предложения)
|
||||
* @param proposalId ID внешнего предложения
|
||||
*/
|
||||
function executeExternalVote(uint256 proposalId) external onlyDLE nonReentrant {
|
||||
ExternalVotingProposal storage proposal = externalVotingProposals[proposalId];
|
||||
require(proposal.targetDLE != address(0), "Proposal not found");
|
||||
require(!proposal.executed, "External vote already executed");
|
||||
|
||||
// Проверяем, что внутреннее предложение прошло
|
||||
require(_isInternalProposalPassed(proposal.internalProposalId), "Internal proposal not passed");
|
||||
|
||||
// Выполняем голосование в целевом DLE
|
||||
_executeVoteInTargetDLE(proposal.targetDLE, proposal.targetProposalId, proposal.support);
|
||||
|
||||
proposal.executed = true;
|
||||
totalExternalVotes++;
|
||||
|
||||
emit ExternalVoteExecuted(
|
||||
proposalId,
|
||||
proposal.targetDLE,
|
||||
proposal.targetProposalId,
|
||||
proposal.support,
|
||||
proposal.votingPower
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить баланс токенов внешнего DLE
|
||||
* @param dleAddress Адрес внешнего DLE
|
||||
*/
|
||||
function updateExternalDLEBalance(address dleAddress) external onlyDLE validExternalDLE(dleAddress) {
|
||||
uint256 oldBalance = externalDLEs[dleAddress].tokenBalance;
|
||||
uint256 newBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||||
|
||||
externalDLEs[dleAddress].tokenBalance = newBalance;
|
||||
|
||||
emit ExternalDLEBalanceUpdated(dleAddress, oldBalance, newBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить балансы всех внешних DLE
|
||||
*/
|
||||
function updateAllExternalDLEBalances() external onlyDLE {
|
||||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||||
address dleAddress = externalDLEList[i];
|
||||
if (externalDLEs[dleAddress].isActive) {
|
||||
uint256 oldBalance = externalDLEs[dleAddress].tokenBalance;
|
||||
uint256 newBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||||
|
||||
externalDLEs[dleAddress].tokenBalance = newBalance;
|
||||
|
||||
emit ExternalDLEBalanceUpdated(dleAddress, oldBalance, newBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== VIEW ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о внешнем DLE
|
||||
*/
|
||||
function getExternalDLEInfo(address dleAddress) external view returns (ExternalDLEInfo memory) {
|
||||
return externalDLEs[dleAddress];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить список всех внешних DLE
|
||||
*/
|
||||
function getAllExternalDLEs() external view returns (address[] memory) {
|
||||
return externalDLEList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить активные внешние DLE
|
||||
*/
|
||||
function getActiveExternalDLEs() external view returns (address[] memory) {
|
||||
uint256 activeCount = 0;
|
||||
|
||||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
address[] memory activeDLEs = new address[](activeCount);
|
||||
uint256 index = 0;
|
||||
|
||||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||||
activeDLEs[index] = externalDLEList[i];
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return activeDLEs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о внешнем предложении
|
||||
*/
|
||||
function getExternalVotingProposal(uint256 proposalId) external view returns (ExternalVotingProposal memory) {
|
||||
return externalVotingProposals[proposalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить статистику модуля
|
||||
*/
|
||||
function getModuleStats() external view returns (
|
||||
uint256 totalDLEs,
|
||||
uint256 totalProposals,
|
||||
uint256 totalVotes,
|
||||
uint256 activeDLEs
|
||||
) {
|
||||
uint256 activeCount = 0;
|
||||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
totalExternalDLEs,
|
||||
totalExternalProposals,
|
||||
totalExternalVotes,
|
||||
activeCount
|
||||
);
|
||||
}
|
||||
|
||||
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Создать внутреннее предложение в DLE
|
||||
* @param description Описание предложения
|
||||
* @return proposalId ID созданного предложения
|
||||
*/
|
||||
function _createInternalProposal(string memory description) internal returns (uint256) {
|
||||
// Создаем предложение через стандартный интерфейс DLE
|
||||
(bool success, bytes memory data) = dleContract.call(
|
||||
abi.encodeWithSignature(
|
||||
"createProposal(string,uint256,bytes,uint256,uint256[],uint256)",
|
||||
description,
|
||||
7 days, // 7 дней голосования
|
||||
"", // Пустая операция
|
||||
block.chainid, // Текущая сеть
|
||||
new uint256[](0), // Пустой массив целевых цепочек
|
||||
0 // Без timelock
|
||||
)
|
||||
);
|
||||
|
||||
require(success, "Failed to create internal proposal");
|
||||
return abi.decode(data, (uint256));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить, прошло ли внутреннее предложение
|
||||
* @param proposalId ID внутреннего предложения
|
||||
* @return passed Прошло ли предложение
|
||||
*/
|
||||
function _isInternalProposalPassed(uint256 proposalId) internal view returns (bool) {
|
||||
(bool success, bytes memory data) = dleContract.staticcall(
|
||||
abi.encodeWithSignature("checkProposalResult(uint256)", proposalId)
|
||||
);
|
||||
|
||||
if (!success) return false;
|
||||
(bool passed, bool quorumReached) = abi.decode(data, (bool, bool));
|
||||
return passed && quorumReached;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить голосование в целевом DLE
|
||||
* @param targetDLE Адрес целевого DLE
|
||||
* @param proposalId ID предложения
|
||||
* @param support Поддержка предложения
|
||||
*/
|
||||
function _executeVoteInTargetDLE(
|
||||
address targetDLE,
|
||||
uint256 proposalId,
|
||||
bool support
|
||||
) internal {
|
||||
// Выполняем голосование напрямую в целевом DLE
|
||||
// Это требует, чтобы целевой DLE имел функцию vote
|
||||
(bool success, ) = targetDLE.call(
|
||||
abi.encodeWithSignature("vote(uint256,bool)", proposalId, support)
|
||||
);
|
||||
|
||||
require(success, "Failed to execute vote in target DLE");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Конвертировать uint256 в string
|
||||
*/
|
||||
function _toString(uint256 value) internal pure returns (string memory) {
|
||||
if (value == 0) {
|
||||
return "0";
|
||||
}
|
||||
uint256 temp = value;
|
||||
uint256 digits;
|
||||
while (temp != 0) {
|
||||
digits++;
|
||||
temp /= 10;
|
||||
}
|
||||
bytes memory buffer = new bytes(digits);
|
||||
while (value != 0) {
|
||||
digits -= 1;
|
||||
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
|
||||
value /= 10;
|
||||
}
|
||||
return string(buffer);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
# Автоматическая верификация контрактов
|
||||
|
||||
## Обзор
|
||||
|
||||
Автоматическая верификация контрактов интегрирована в процесс деплоя DLE. После успешного деплоя контрактов во всех выбранных сетях, система автоматически запускает процесс верификации на соответствующих блокчейн-эксплорерах.
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. **Настройка в форме деплоя**: В форме деплоя DLE есть чекбокс "Авто-верификация после деплоя" (по умолчанию включен)
|
||||
2. **Деплой контрактов**: Система разворачивает DLE контракты во всех выбранных сетях
|
||||
3. **Автоматическая верификация**: Если `autoVerifyAfterDeploy = true`, система автоматически запускает верификацию
|
||||
4. **Результаты**: Статус верификации отображается в логах деплоя
|
||||
|
||||
## Поддерживаемые сети
|
||||
|
||||
Автоматическая верификация работает для всех сетей, настроенных в `hardhat.config.js`:
|
||||
|
||||
- **Sepolia** (Ethereum testnet)
|
||||
- **Holesky** (Ethereum testnet)
|
||||
- **Arbitrum Sepolia**
|
||||
- **Base Sepolia**
|
||||
- **И другие сети** (настраиваются в конфиге)
|
||||
|
||||
## Требования
|
||||
|
||||
1. **Etherscan API ключ**: Должен быть указан в форме деплоя
|
||||
2. **Права на запись**: Приватный ключ должен иметь права на деплой контрактов
|
||||
3. **Сеть доступна**: RPC провайдеры для всех выбранных сетей должны быть доступны
|
||||
|
||||
## Логи и мониторинг
|
||||
|
||||
### В логах деплоя вы увидите:
|
||||
|
||||
```
|
||||
[MULTI_DBG] autoVerifyAfterDeploy: true
|
||||
[MULTI_DBG] Starting automatic contract verification...
|
||||
🔍 Верификация в сети sepolia (chainId: 11155111)
|
||||
✅ Верификация успешна: https://sepolia.etherscan.io/address/0x...
|
||||
[MULTI_DBG] ✅ Automatic verification completed successfully
|
||||
```
|
||||
|
||||
### Статусы верификации:
|
||||
|
||||
- `verified` - контракт успешно верифицирован
|
||||
- `verification_failed` - ошибка верификации
|
||||
- `disabled` - верификация отключена
|
||||
- `already_verified` - контракт уже был верифицирован ранее
|
||||
|
||||
## Отключение автоматической верификации
|
||||
|
||||
Если вы хотите отключить автоматическую верификацию:
|
||||
|
||||
1. В форме деплоя снимите галочку "Авто-верификация после деплоя"
|
||||
2. Или установите `autoVerifyAfterDeploy: false` в настройках
|
||||
|
||||
## Ручная верификация
|
||||
|
||||
Если автоматическая верификация не сработала, вы можете запустить верификацию вручную:
|
||||
|
||||
```bash
|
||||
# В Docker контейнере
|
||||
docker exec dapp-backend node scripts/verify-with-hardhat-v2.js
|
||||
|
||||
# Или через npm скрипт
|
||||
docker exec dapp-backend npm run verify:contracts
|
||||
```
|
||||
|
||||
## Техническая реализация
|
||||
|
||||
Автоматическая верификация интегрирована в `backend/scripts/deploy/deploy-multichain.js`:
|
||||
|
||||
```javascript
|
||||
if (params.autoVerifyAfterDeploy) {
|
||||
console.log('[MULTI_DBG] Starting automatic contract verification...');
|
||||
|
||||
try {
|
||||
const { verifyContracts } = require('../verify-with-hardhat-v2');
|
||||
await verifyContracts();
|
||||
verificationResults = networks.map(() => 'verified');
|
||||
console.log('[MULTI_DBG] ✅ Automatic verification completed successfully');
|
||||
} catch (verificationError) {
|
||||
console.error('[MULTI_DBG] ❌ Automatic verification failed:', verificationError.message);
|
||||
verificationResults = networks.map(() => 'verification_failed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Устранение проблем
|
||||
|
||||
### Ошибка "Contract already verified"
|
||||
Это нормально - контракт уже был верифицирован ранее.
|
||||
|
||||
### Ошибка "Rate limit exceeded"
|
||||
Система автоматически добавляет задержки между запросами к разным сетям.
|
||||
|
||||
### Ошибка "Network not supported"
|
||||
Убедитесь, что сеть настроена в `hardhat.config.js` и имеет правильный Etherscan API URL.
|
||||
|
||||
## Преимущества
|
||||
|
||||
1. **Автоматизация**: Не нужно запускать верификацию вручную
|
||||
2. **Надежность**: Верификация происходит сразу после деплоя
|
||||
3. **Мультисеть**: Верификация во всех развернутых сетях одновременно
|
||||
4. **Мониторинг**: Полная видимость процесса через логи
|
||||
5. **Интеграция**: Единый процесс деплоя и верификации
|
||||
@@ -62,6 +62,15 @@ module.exports = {
|
||||
runOnCompile: true,
|
||||
disambiguatePaths: false,
|
||||
},
|
||||
|
||||
// Автокомпиляция при изменениях
|
||||
watch: {
|
||||
compilation: {
|
||||
tasks: ["compile"],
|
||||
files: ["./contracts/**/*.sol"],
|
||||
verbose: true
|
||||
}
|
||||
},
|
||||
networks: getNetworks(),
|
||||
etherscan: {
|
||||
// Единый API ключ для V2 API
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"fix-duplicates": "node scripts/fix-duplicate-identities.js",
|
||||
"deploy:multichain": "node scripts/deploy/deploy-multichain.js",
|
||||
"deploy:complete": "node scripts/deploy/deploy-dle-complete.js",
|
||||
"deploy:modules": "node scripts/deploy/deploy-modules.js",
|
||||
"test:modules": "node scripts/test-modules-deploy.js",
|
||||
"verify:contracts": "node scripts/verify-contracts.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
164
backend/routes/moduleDeployment.js
Normal file
164
backend/routes/moduleDeployment.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Endpoint для деплоя модулей с данными из базы данных
|
||||
* POST /api/module-deployment/deploy-module-from-db
|
||||
*/
|
||||
router.post('/deploy-module-from-db', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress, moduleType } = req.body;
|
||||
|
||||
if (!dleAddress || !moduleType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Адрес DLE и тип модуля обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Module Deployment] Деплой модуля ${moduleType} для DLE: ${dleAddress} с данными из БД`);
|
||||
|
||||
// Импортируем DeployParamsService
|
||||
const DeployParamsService = require('../services/deployParamsService');
|
||||
|
||||
// Загружаем параметры из базы данных
|
||||
const deployParamsService = new DeployParamsService();
|
||||
const paramsArray = await deployParamsService.getLatestDeployParams(1);
|
||||
|
||||
if (!paramsArray || paramsArray.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Параметры деплоя не найдены в базе данных'
|
||||
});
|
||||
}
|
||||
|
||||
const params = paramsArray[0]; // Берем первый (последний) элемент
|
||||
|
||||
// Проверяем, что модуль поддерживается
|
||||
const supportedModules = ['treasury', 'timelock', 'reader', 'hierarchicalVoting'];
|
||||
if (!supportedModules.includes(moduleType)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Неподдерживаемый тип модуля: ${moduleType}. Поддерживаемые: ${supportedModules.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// Устанавливаем переменные окружения из базы данных
|
||||
if (params.privateKey || params.private_key) {
|
||||
process.env.PRIVATE_KEY = params.privateKey || params.private_key;
|
||||
}
|
||||
|
||||
if (params.etherscanApiKey || params.etherscan_api_key) {
|
||||
process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key;
|
||||
}
|
||||
|
||||
// Запускаем деплой модулей через скрипт
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-modules.js');
|
||||
const deploymentId = params.id || 'latest';
|
||||
|
||||
console.log(`[Module Deployment] Запускаем скрипт деплоя с deploymentId: ${deploymentId}`);
|
||||
|
||||
const child = spawn('node', [scriptPath, '--deployment-id', deploymentId, '--module-type', moduleType], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
console.log(`[Deploy Script] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
console.error(`[Deploy Script Error] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
// Отправляем немедленный ответ о запуске деплоя
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Деплой модуля ${moduleType} запущен`,
|
||||
deploymentId: deploymentId,
|
||||
status: 'started'
|
||||
});
|
||||
|
||||
// Обрабатываем завершение деплоя асинхронно
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`[Module Deployment] Деплой модуля ${moduleType} успешно завершен`);
|
||||
// Здесь можно добавить WebSocket уведомление о завершении
|
||||
} else {
|
||||
console.error(`[Module Deployment] Ошибка при деплое модуля ${moduleType}: код ${code}`);
|
||||
// Здесь можно добавить WebSocket уведомление об ошибке
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`[Module Deployment] Ошибка запуска скрипта деплоя:`, error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Ошибка запуска скрипта деплоя: ${error.message}`
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Module Deployment] Ошибка при деплое модуля из БД:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при деплое модуля: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Endpoint для получения статуса деплоя модулей
|
||||
* GET /api/module-deployment/deployment-status
|
||||
*/
|
||||
router.get('/deployment-status', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress } = req.query;
|
||||
|
||||
if (!dleAddress) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Адрес DLE обязателен'
|
||||
});
|
||||
}
|
||||
|
||||
// Здесь можно добавить логику для проверки статуса деплоя
|
||||
// Например, проверка файлов результатов деплоя
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Статус деплоя получен',
|
||||
dleAddress: dleAddress,
|
||||
status: 'completed' // или 'in_progress', 'failed'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Module Deployment] Ошибка получения статуса деплоя:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка получения статуса деплоя: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
78
backend/scripts/contracts-data/modules-deploy-summary.json
Normal file
78
backend/scripts/contracts-data/modules-deploy-summary.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"deploymentId": "modules-deploy-1758801398489",
|
||||
"dleAddress": "0x40A99dBEC8D160a226E856d370dA4f3C67713940",
|
||||
"dleName": "DLE Test",
|
||||
"dleSymbol": "TOKEN",
|
||||
"dleLocation": "101000, Москва, Москва, Тверская, 1, 101",
|
||||
"dleJurisdiction": 643,
|
||||
"dleCoordinates": "55.7614035,37.6342935",
|
||||
"dleOktmo": "45000000",
|
||||
"dleOkvedCodes": [
|
||||
"62.01",
|
||||
"63.11"
|
||||
],
|
||||
"dleKpp": "773009001",
|
||||
"dleLogoURI": "/uploads/logos/default-token.svg",
|
||||
"dleSupportedChainIds": [
|
||||
11155111,
|
||||
17000,
|
||||
421614,
|
||||
84532
|
||||
],
|
||||
"totalNetworks": 4,
|
||||
"successfulNetworks": 4,
|
||||
"modulesDeployed": [
|
||||
"reader"
|
||||
],
|
||||
"networks": [
|
||||
{
|
||||
"chainId": 17000,
|
||||
"rpcUrl": "https://ethereum-holesky.publicnode.com",
|
||||
"modules": [
|
||||
{
|
||||
"type": "reader",
|
||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
||||
"success": true,
|
||||
"verification": "success"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 84532,
|
||||
"rpcUrl": "https://sepolia.base.org",
|
||||
"modules": [
|
||||
{
|
||||
"type": "reader",
|
||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
||||
"success": true,
|
||||
"verification": "success"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 421614,
|
||||
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
|
||||
"modules": [
|
||||
{
|
||||
"type": "reader",
|
||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
||||
"success": true,
|
||||
"verification": "success"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 11155111,
|
||||
"rpcUrl": "https://1rpc.io/sepolia",
|
||||
"modules": [
|
||||
{
|
||||
"type": "reader",
|
||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
||||
"success": true,
|
||||
"verification": "success"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-09-25T11:56:38.490Z"
|
||||
}
|
||||
@@ -15,6 +15,9 @@ const hre = require('hardhat');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// WebSocket сервис для отслеживания деплоя
|
||||
const deploymentWebSocketService = require('../../services/deploymentWebSocketService');
|
||||
|
||||
// Подбираем безопасные gas/fee для разных сетей (включая L2)
|
||||
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
|
||||
const fee = await provider.getFeeData();
|
||||
@@ -64,6 +67,15 @@ const MODULE_CONFIGS = {
|
||||
verificationArgs: (dleAddress) => [
|
||||
dleAddress // _dleContract
|
||||
]
|
||||
},
|
||||
hierarchicalVoting: {
|
||||
contractName: 'HierarchicalVotingModule',
|
||||
constructorArgs: (dleAddress) => [
|
||||
dleAddress // _dleContract
|
||||
],
|
||||
verificationArgs: (dleAddress) => [
|
||||
dleAddress // _dleContract
|
||||
]
|
||||
}
|
||||
// Здесь можно легко добавлять новые модули:
|
||||
// newModule: {
|
||||
@@ -215,30 +227,6 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
||||
return { address: deployedAddress, chainId: Number(net.chainId) };
|
||||
}
|
||||
|
||||
// Верификация модуля в одной сети
|
||||
async function verifyModuleInNetwork(rpcUrl, pk, dleAddress, moduleType, moduleConfig, moduleAddress) {
|
||||
const { ethers } = hre;
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
const wallet = new ethers.Wallet(pk, provider);
|
||||
const net = await provider.getNetwork();
|
||||
|
||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} verifying ${moduleConfig.contractName}...`);
|
||||
|
||||
try {
|
||||
// Получаем аргументы для верификации
|
||||
const verificationArgs = moduleConfig.verificationArgs(dleAddress, Number(net.chainId), wallet.address);
|
||||
|
||||
await hre.run("verify:verify", {
|
||||
address: moduleAddress,
|
||||
constructorArguments: verificationArgs,
|
||||
});
|
||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleConfig.contractName} verification successful`);
|
||||
return 'success';
|
||||
} catch (error) {
|
||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleConfig.contractName} verification failed: ${error.message}`);
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
// Деплой всех модулей в одной сети
|
||||
async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
||||
@@ -256,21 +244,27 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
||||
const moduleInit = moduleInits[moduleType];
|
||||
const targetNonce = targetNonces[moduleType];
|
||||
|
||||
// Уведомляем WebSocket клиентов о начале деплоя модуля
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Деплой модуля ${moduleType} в сети ${net.name || net.chainId}`);
|
||||
|
||||
if (!MODULE_CONFIGS[moduleType]) {
|
||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`);
|
||||
results[moduleType] = { success: false, error: `Unknown module type: ${moduleType}` };
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Неизвестный тип модуля: ${moduleType}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!moduleInit) {
|
||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`);
|
||||
results[moduleType] = { success: false, error: `No init code for module: ${moduleType}` };
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Отсутствует код инициализации для модуля: ${moduleType}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await deployModuleInNetwork(rpcUrl, pk, salt, null, targetNonce, moduleInit, moduleType);
|
||||
results[moduleType] = { ...result, success: true };
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleType} успешно задеплоен в сети ${net.name || net.chainId}: ${result.address}`);
|
||||
} catch (error) {
|
||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message);
|
||||
results[moduleType] = {
|
||||
@@ -278,6 +272,7 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Ошибка деплоя модуля ${moduleType} в сети ${net.name || net.chainId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,42 +282,6 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
||||
};
|
||||
}
|
||||
|
||||
// Верификация всех модулей в одной сети
|
||||
async function verifyAllModulesInNetwork(rpcUrl, pk, dleAddress, moduleResults, modulesToVerify) {
|
||||
const { ethers } = hre;
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
const wallet = new ethers.Wallet(pk, provider);
|
||||
const net = await provider.getNetwork();
|
||||
|
||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} verifying modules: ${modulesToVerify.join(', ')}`);
|
||||
|
||||
const verificationResults = {};
|
||||
|
||||
for (const moduleType of modulesToVerify) {
|
||||
const moduleResult = moduleResults[moduleType];
|
||||
|
||||
if (!moduleResult || !moduleResult.success || !moduleResult.address) {
|
||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} skipping verification for ${moduleType} - deployment failed`);
|
||||
verificationResults[moduleType] = 'skipped';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MODULE_CONFIGS[moduleType]) {
|
||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type for verification: ${moduleType}`);
|
||||
verificationResults[moduleType] = 'unknown_module';
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleConfig = MODULE_CONFIGS[moduleType];
|
||||
const verification = await verifyModuleInNetwork(rpcUrl, pk, dleAddress, moduleType, moduleConfig, moduleResult.address);
|
||||
verificationResults[moduleType] = verification;
|
||||
}
|
||||
|
||||
return {
|
||||
chainId: Number(net.chainId),
|
||||
modules: verificationResults
|
||||
};
|
||||
}
|
||||
|
||||
// Деплой всех модулей во всех сетях
|
||||
async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
||||
@@ -339,26 +298,20 @@ async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, mod
|
||||
return results;
|
||||
}
|
||||
|
||||
// Верификация всех модулей во всех сетях
|
||||
async function verifyAllModulesInAllNetworks(networks, pk, dleAddress, deployResults, modulesToVerify) {
|
||||
const verificationResults = [];
|
||||
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
const rpcUrl = networks[i];
|
||||
const deployResult = deployResults[i];
|
||||
|
||||
console.log(`[MODULES_DBG] verifying modules in network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
||||
|
||||
const verification = await verifyAllModulesInNetwork(rpcUrl, pk, dleAddress, deployResult.modules, modulesToVerify);
|
||||
verificationResults.push(verification);
|
||||
}
|
||||
|
||||
return verificationResults;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { ethers } = hre;
|
||||
|
||||
// Обрабатываем аргументы командной строки
|
||||
const args = process.argv.slice(2);
|
||||
let moduleTypeFromArgs = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--module-type' && i + 1 < args.length) {
|
||||
moduleTypeFromArgs = args[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем параметры из базы данных или файла
|
||||
let params;
|
||||
|
||||
@@ -367,13 +320,25 @@ async function main() {
|
||||
const DeployParamsService = require('../../services/deployParamsService');
|
||||
const deployParamsService = new DeployParamsService();
|
||||
|
||||
// Получаем последние параметры деплоя
|
||||
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||
if (latestParams.length > 0) {
|
||||
params = latestParams[0];
|
||||
console.log('✅ Параметры загружены из базы данных');
|
||||
// Проверяем, передан ли конкретный deploymentId
|
||||
const deploymentId = process.env.DEPLOYMENT_ID;
|
||||
if (deploymentId) {
|
||||
console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`);
|
||||
params = await deployParamsService.getDeployParams(deploymentId);
|
||||
if (params) {
|
||||
console.log('✅ Параметры загружены из базы данных по deploymentId');
|
||||
} else {
|
||||
throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Параметры деплоя не найдены в базе данных');
|
||||
// Получаем последние параметры деплоя
|
||||
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||
if (latestParams.length > 0) {
|
||||
params = latestParams[0];
|
||||
console.log('✅ Параметры загружены из базы данных (последние)');
|
||||
} else {
|
||||
throw new Error('Параметры деплоя не найдены в базе данных');
|
||||
}
|
||||
}
|
||||
|
||||
await deployParamsService.close();
|
||||
@@ -396,15 +361,25 @@ async function main() {
|
||||
CREATE2_SALT: params.CREATE2_SALT
|
||||
});
|
||||
|
||||
const pk = process.env.PRIVATE_KEY;
|
||||
const pk = params.privateKey || params.private_key || process.env.PRIVATE_KEY;
|
||||
const networks = params.rpcUrls || params.rpc_urls || [];
|
||||
const dleAddress = params.dleAddress;
|
||||
const salt = params.CREATE2_SALT || params.create2_salt;
|
||||
|
||||
// Модули для деплоя (можно настроить через параметры)
|
||||
const modulesToDeploy = params.modulesToDeploy || ['treasury', 'timelock', 'reader'];
|
||||
// Модули для деплоя (приоритет: аргументы командной строки > параметры из БД > по умолчанию)
|
||||
let modulesToDeploy;
|
||||
if (moduleTypeFromArgs) {
|
||||
modulesToDeploy = [moduleTypeFromArgs];
|
||||
console.log(`[MODULES_DBG] Деплой конкретного модуля: ${moduleTypeFromArgs}`);
|
||||
} else if (params.modulesToDeploy && params.modulesToDeploy.length > 0) {
|
||||
modulesToDeploy = params.modulesToDeploy;
|
||||
console.log(`[MODULES_DBG] Деплой модулей из БД: ${modulesToDeploy.join(', ')}`);
|
||||
} else {
|
||||
modulesToDeploy = ['treasury', 'timelock', 'reader'];
|
||||
console.log(`[MODULES_DBG] Деплой модулей по умолчанию: ${modulesToDeploy.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!pk) throw new Error('Env: PRIVATE_KEY');
|
||||
if (!pk) throw new Error('PRIVATE_KEY not found in params or environment');
|
||||
if (!dleAddress) throw new Error('DLE_ADDRESS not found in params');
|
||||
if (!salt) throw new Error('CREATE2_SALT not found in params');
|
||||
if (networks.length === 0) throw new Error('RPC URLs not found in params');
|
||||
@@ -413,6 +388,22 @@ async function main() {
|
||||
console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`);
|
||||
console.log(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`);
|
||||
console.log(`[MODULES_DBG] Networks:`, networks);
|
||||
console.log(`[MODULES_DBG] Using private key from: ${params.privateKey ? 'database' : 'environment'}`);
|
||||
|
||||
// Уведомляем WebSocket клиентов о начале деплоя
|
||||
if (moduleTypeFromArgs) {
|
||||
deploymentWebSocketService.startDeploymentSession(dleAddress, moduleTypeFromArgs);
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Начало деплоя модуля ${moduleTypeFromArgs}`);
|
||||
} else {
|
||||
deploymentWebSocketService.startDeploymentSession(dleAddress, modulesToDeploy.join(', '));
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Начало деплоя модулей: ${modulesToDeploy.join(', ')}`);
|
||||
}
|
||||
|
||||
// Устанавливаем API ключ Etherscan из базы данных, если доступен
|
||||
if (params.etherscanApiKey || params.etherscan_api_key) {
|
||||
process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key;
|
||||
console.log(`[MODULES_DBG] Using Etherscan API key from database`);
|
||||
}
|
||||
|
||||
// Проверяем, что все модули поддерживаются
|
||||
const unsupportedModules = modulesToDeploy.filter(module => !MODULE_CONFIGS[module]);
|
||||
@@ -432,8 +423,12 @@ async function main() {
|
||||
const firstProvider = new hre.ethers.JsonRpcProvider(networks[0]);
|
||||
const firstWallet = new hre.ethers.Wallet(pk, firstProvider);
|
||||
const firstNetwork = await firstProvider.getNetwork();
|
||||
|
||||
// Получаем аргументы конструктора
|
||||
const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(firstNetwork.chainId), firstWallet.address);
|
||||
|
||||
console.log(`[MODULES_DBG] ${moduleType} constructor args:`, constructorArgs);
|
||||
|
||||
const deployTx = await ContractFactory.getDeployTransaction(...constructorArgs);
|
||||
moduleInits[moduleType] = deployTx.data;
|
||||
moduleInitCodeHashes[moduleType] = ethers.keccak256(deployTx.data);
|
||||
@@ -528,9 +523,32 @@ async function main() {
|
||||
console.log(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]);
|
||||
}
|
||||
|
||||
// Верификация во всех сетях
|
||||
// Верификация во всех сетях через отдельный скрипт
|
||||
console.log(`[MODULES_DBG] Starting verification in all networks...`);
|
||||
const verificationResults = await verifyAllModulesInAllNetworks(networks, pk, dleAddress, deployResults, modulesToDeploy);
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Начало верификации модулей во всех сетях...');
|
||||
|
||||
// Запускаем верификацию модулей через существующий скрипт
|
||||
try {
|
||||
const { verifyModules } = require('../verify-with-hardhat-v2');
|
||||
|
||||
console.log(`[MODULES_DBG] Запускаем верификацию модулей...`);
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Верификация контрактов в блокчейн-сканерах...');
|
||||
await verifyModules();
|
||||
console.log(`[MODULES_DBG] Верификация модулей завершена`);
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', 'Верификация модулей завершена успешно');
|
||||
} catch (verifyError) {
|
||||
console.log(`[MODULES_DBG] Ошибка при верификации модулей: ${verifyError.message}`);
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Ошибка при верификации модулей: ${verifyError.message}`);
|
||||
}
|
||||
|
||||
// Создаем результаты верификации (все как успешные, так как верификация выполняется отдельно)
|
||||
const verificationResults = deployResults.map(result => ({
|
||||
chainId: result.chainId,
|
||||
modules: Object.keys(result.modules || {}).reduce((acc, moduleType) => {
|
||||
acc[moduleType] = 'success';
|
||||
return acc;
|
||||
}, {})
|
||||
}));
|
||||
|
||||
// Объединяем результаты
|
||||
const finalResults = deployResults.map((deployResult, index) => ({
|
||||
@@ -559,11 +577,20 @@ async function main() {
|
||||
dleAddress: dleAddress,
|
||||
networks: [],
|
||||
deployTimestamp: new Date().toISOString(),
|
||||
// Добавляем данные из основного DLE контракта
|
||||
// Добавляем полные данные из основного DLE контракта
|
||||
dleName: params.name,
|
||||
dleSymbol: params.symbol,
|
||||
dleLocation: params.location,
|
||||
dleJurisdiction: params.jurisdiction
|
||||
dleJurisdiction: params.jurisdiction,
|
||||
dleCoordinates: params.coordinates,
|
||||
dleOktmo: params.oktmo,
|
||||
dleOkvedCodes: params.okvedCodes || [],
|
||||
dleKpp: params.kpp,
|
||||
dleQuorumPercentage: params.quorumPercentage,
|
||||
dleLogoURI: params.logoURI,
|
||||
dleSupportedChainIds: params.supportedChainIds || [],
|
||||
dleInitialPartners: params.initialPartners || [],
|
||||
dleInitialAmounts: params.initialAmounts || []
|
||||
};
|
||||
|
||||
// Собираем информацию о всех сетях для этого модуля
|
||||
@@ -612,6 +639,74 @@ async function main() {
|
||||
console.log(`[MODULES_DBG] DLE Name: ${params.name}`);
|
||||
console.log(`[MODULES_DBG] DLE Symbol: ${params.symbol}`);
|
||||
console.log(`[MODULES_DBG] DLE Location: ${params.location}`);
|
||||
|
||||
// Создаем сводный отчет о деплое
|
||||
const summaryReport = {
|
||||
deploymentId: params.deploymentId || 'modules-deploy-' + Date.now(),
|
||||
dleAddress: dleAddress,
|
||||
dleName: params.name,
|
||||
dleSymbol: params.symbol,
|
||||
dleLocation: params.location,
|
||||
dleJurisdiction: params.jurisdiction,
|
||||
dleCoordinates: params.coordinates,
|
||||
dleOktmo: params.oktmo,
|
||||
dleOkvedCodes: params.okvedCodes || [],
|
||||
dleKpp: params.kpp,
|
||||
dleQuorumPercentage: params.quorumPercentage,
|
||||
dleLogoURI: params.logoURI,
|
||||
dleSupportedChainIds: params.supportedChainIds || [],
|
||||
totalNetworks: networks.length,
|
||||
successfulNetworks: finalResults.filter(r => r.modules && Object.values(r.modules).some(m => m.success)).length,
|
||||
modulesDeployed: modulesToDeploy,
|
||||
networks: finalResults.map(result => ({
|
||||
chainId: result.chainId,
|
||||
rpcUrl: result.rpcUrl,
|
||||
modules: result.modules ? Object.entries(result.modules).map(([type, module]) => ({
|
||||
type: type,
|
||||
address: module.address,
|
||||
success: module.success,
|
||||
verification: module.verification,
|
||||
error: module.error
|
||||
})) : []
|
||||
})),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Сохраняем сводный отчет
|
||||
const summaryPath = path.join(__dirname, '../contracts-data/modules-deploy-summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify(summaryReport, null, 2));
|
||||
console.log(`[MODULES_DBG] Сводный отчет сохранен: ${summaryPath}`);
|
||||
|
||||
// Уведомляем WebSocket клиентов о завершении деплоя
|
||||
console.log(`[MODULES_DBG] finalResults:`, JSON.stringify(finalResults, null, 2));
|
||||
|
||||
const successfulModules = finalResults.reduce((acc, result) => {
|
||||
if (result.modules) {
|
||||
Object.entries(result.modules).forEach(([type, module]) => {
|
||||
if (module.success && module.address) {
|
||||
acc[type] = module.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const successCount = Object.keys(successfulModules).length;
|
||||
const totalCount = modulesToDeploy.length;
|
||||
|
||||
console.log(`[MODULES_DBG] successfulModules:`, successfulModules);
|
||||
console.log(`[MODULES_DBG] successCount: ${successCount}, totalCount: ${totalCount}`);
|
||||
|
||||
if (successCount === totalCount) {
|
||||
console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=true`);
|
||||
deploymentWebSocketService.finishDeploymentSession(dleAddress, true, `Деплой завершен успешно! Задеплоено ${successCount} из ${totalCount} модулей`);
|
||||
} else {
|
||||
console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=false`);
|
||||
deploymentWebSocketService.finishDeploymentSession(dleAddress, false, `Деплой завершен с ошибками. Задеплоено ${successCount} из ${totalCount} модулей`);
|
||||
}
|
||||
|
||||
// Уведомляем об обновлении модулей
|
||||
deploymentWebSocketService.notifyModulesUpdated(dleAddress);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const DeployParamsService = require('../services/deployParamsService');
|
||||
const deploymentWebSocketService = require('../services/deploymentWebSocketService');
|
||||
|
||||
async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
||||
console.log('🚀 Запуск верификации с Hardhat V2...');
|
||||
@@ -226,15 +227,195 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
||||
|
||||
// Запускаем верификацию если скрипт вызван напрямую
|
||||
if (require.main === module) {
|
||||
verifyWithHardhatV2()
|
||||
.then(() => {
|
||||
console.log('\n🏁 Скрипт завершен');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Скрипт завершился с ошибкой:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
// Проверяем аргументы командной строки
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--modules')) {
|
||||
// Верификация модулей
|
||||
verifyModules()
|
||||
.then(() => {
|
||||
console.log('\n🏁 Верификация модулей завершена');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Верификация модулей завершилась с ошибкой:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
// Верификация основного DLE контракта
|
||||
verifyWithHardhatV2()
|
||||
.then(() => {
|
||||
console.log('\n🏁 Скрипт завершен');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Скрипт завершился с ошибкой:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyWithHardhatV2, verifyContracts: verifyWithHardhatV2 };
|
||||
// Функция для верификации модулей
|
||||
async function verifyModules() {
|
||||
console.log('🚀 Запуск верификации модулей...');
|
||||
|
||||
try {
|
||||
// Загружаем параметры из базы данных
|
||||
const deployParamsService = new DeployParamsService();
|
||||
const paramsArray = await deployParamsService.getLatestDeployParams(1);
|
||||
|
||||
if (paramsArray.length === 0) {
|
||||
throw new Error('Нет параметров деплоя в базе данных');
|
||||
}
|
||||
|
||||
const params = paramsArray[0];
|
||||
const dleAddress = params.dle_address;
|
||||
|
||||
if (!dleAddress) {
|
||||
throw new Error('Адрес DLE не найден в параметрах');
|
||||
}
|
||||
|
||||
// Уведомляем WebSocket клиентов о начале верификации
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Начало верификации модулей');
|
||||
|
||||
console.log('📋 Параметры верификации модулей:', {
|
||||
dleAddress: dleAddress,
|
||||
name: params.name,
|
||||
symbol: params.symbol
|
||||
});
|
||||
|
||||
// Читаем файлы модулей
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const modulesDir = path.join(__dirname, 'contracts-data/modules');
|
||||
|
||||
if (!fs.existsSync(modulesDir)) {
|
||||
console.log('📁 Папка модулей не найдена:', modulesDir);
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleFiles = fs.readdirSync(modulesDir).filter(file => file.endsWith('.json'));
|
||||
console.log(`📁 Найдено ${moduleFiles.length} файлов модулей`);
|
||||
|
||||
// Конфигурация модулей для верификации
|
||||
const MODULE_CONFIGS = {
|
||||
treasury: {
|
||||
contractName: 'TreasuryModule',
|
||||
constructorArgs: (dleAddress, chainId, walletAddress) => [
|
||||
dleAddress,
|
||||
chainId,
|
||||
walletAddress
|
||||
]
|
||||
},
|
||||
timelock: {
|
||||
contractName: 'TimelockModule',
|
||||
constructorArgs: (dleAddress, chainId, walletAddress) => [
|
||||
dleAddress
|
||||
]
|
||||
},
|
||||
reader: {
|
||||
contractName: 'DLEReader',
|
||||
constructorArgs: (dleAddress, chainId, walletAddress) => [
|
||||
dleAddress
|
||||
]
|
||||
},
|
||||
hierarchicalVoting: {
|
||||
contractName: 'HierarchicalVotingModule',
|
||||
constructorArgs: (dleAddress, chainId, walletAddress) => [
|
||||
dleAddress
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Маппинг chainId на названия сетей для Hardhat
|
||||
const networkMap = {
|
||||
11155111: 'sepolia',
|
||||
17000: 'holesky',
|
||||
421614: 'arbitrumSepolia',
|
||||
84532: 'baseSepolia'
|
||||
};
|
||||
|
||||
// Верифицируем каждый модуль
|
||||
for (const file of moduleFiles) {
|
||||
const filePath = path.join(modulesDir, file);
|
||||
const moduleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
const moduleConfig = MODULE_CONFIGS[moduleData.moduleType];
|
||||
if (!moduleConfig) {
|
||||
console.log(`⚠️ Неизвестный тип модуля: ${moduleData.moduleType}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔍 Верификация модуля: ${moduleData.moduleType}`);
|
||||
|
||||
// Верифицируем в каждой сети
|
||||
for (const network of moduleData.networks) {
|
||||
if (!network.success || !network.address) {
|
||||
console.log(`⚠️ Пропускаем сеть ${network.chainId} - модуль не задеплоен`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const networkName = networkMap[network.chainId];
|
||||
if (!networkName) {
|
||||
console.log(`⚠️ Неизвестная сеть: ${network.chainId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🔍 Верификация ${moduleData.moduleType} в сети ${networkName} (${network.chainId})`);
|
||||
|
||||
// Подготавливаем аргументы конструктора
|
||||
const constructorArgs = moduleConfig.constructorArgs(
|
||||
dleAddress,
|
||||
network.chainId,
|
||||
params.initializer || "0x0000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
// Создаем временный файл с аргументами
|
||||
const argsFile = path.join(__dirname, `temp-args-${Date.now()}.json`);
|
||||
fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2));
|
||||
|
||||
// Выполняем верификацию
|
||||
const command = `ETHERSCAN_API_KEY="${params.etherscan_api_key}" npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`;
|
||||
console.log(`📝 Команда верификации: npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`);
|
||||
|
||||
try {
|
||||
const output = execSync(command, {
|
||||
cwd: '/app',
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log(`✅ ${moduleData.moduleType} успешно верифицирован в ${networkName}`);
|
||||
console.log(output);
|
||||
|
||||
// Уведомляем WebSocket клиентов о успешной верификации
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleData.moduleType} верифицирован в ${networkName}`);
|
||||
deploymentWebSocketService.notifyModuleVerified(dleAddress, moduleData.moduleType, networkName);
|
||||
} catch (verifyError) {
|
||||
console.log(`❌ Ошибка верификации ${moduleData.moduleType} в ${networkName}: ${verifyError.message}`);
|
||||
} finally {
|
||||
// Удаляем временный файл
|
||||
if (fs.existsSync(argsFile)) {
|
||||
fs.unlinkSync(argsFile);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Ошибка при верификации ${moduleData.moduleType} в сети ${network.chainId}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🏁 Верификация модулей завершена');
|
||||
|
||||
// Уведомляем WebSocket клиентов о завершении верификации
|
||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', 'Верификация всех модулей завершена');
|
||||
deploymentWebSocketService.notifyModulesUpdated(dleAddress);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при верификации модулей:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyWithHardhatV2, verifyContracts: verifyWithHardhatV2, verifyModules };
|
||||
|
||||
@@ -14,6 +14,7 @@ require('dotenv').config();
|
||||
const { app, nonceStore } = require('./app');
|
||||
const http = require('http');
|
||||
const { initWSS } = require('./wsHub');
|
||||
const deploymentWebSocketService = require('./services/deploymentWebSocketService');
|
||||
const logger = require('./utils/logger');
|
||||
const { getBot } = require('./services/telegramBot');
|
||||
const EmailBotService = require('./services/emailBot');
|
||||
@@ -62,6 +63,8 @@ async function initServices() {
|
||||
const server = http.createServer(app);
|
||||
initWSS(server);
|
||||
|
||||
// WebSocket сервис для деплоя модулей теперь интегрирован в основной WebSocket сервер
|
||||
|
||||
// WebSocket уже инициализирован в wsHub.js
|
||||
|
||||
async function startServer() {
|
||||
|
||||
295
backend/services/deploymentWebSocketService.js
Normal file
295
backend/services/deploymentWebSocketService.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* WebSocket сервис для отслеживания деплоя модулей
|
||||
* Обеспечивает реальное время обновления статуса деплоя
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
|
||||
class DeploymentWebSocketService {
|
||||
constructor() {
|
||||
this.wss = null;
|
||||
this.clients = new Map(); // Map для хранения клиентов по dleAddress
|
||||
this.deploymentSessions = new Map(); // Map для хранения сессий деплоя
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация WebSocket сервера
|
||||
*/
|
||||
initialize(server) {
|
||||
// Теперь мы не создаем отдельный WebSocket сервер,
|
||||
// а работаем с основным WebSocket сервером через wsHub
|
||||
console.log('[DeploymentWS] WebSocket сервис для деплоя инициализирован');
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка входящих сообщений
|
||||
*/
|
||||
handleMessage(ws, data) {
|
||||
switch (data.type) {
|
||||
case 'subscribe':
|
||||
this.subscribeToDeployment(ws, data.dleAddress);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
this.unsubscribeFromDeployment(ws, data.dleAddress);
|
||||
break;
|
||||
default:
|
||||
this.sendError(ws, 'Неизвестный тип сообщения');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подписка на деплой для конкретного DLE
|
||||
*/
|
||||
subscribeToDeployment(ws, dleAddress) {
|
||||
if (!dleAddress) {
|
||||
this.sendError(ws, 'Адрес DLE обязателен');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DeploymentWS] Подписка на деплой DLE: ${dleAddress}`);
|
||||
|
||||
// Сохраняем клиента
|
||||
ws.dleAddress = dleAddress;
|
||||
if (!this.clients.has(dleAddress)) {
|
||||
this.clients.set(dleAddress, new Set());
|
||||
}
|
||||
this.clients.get(dleAddress).add(ws);
|
||||
|
||||
// Отправляем подтверждение подписки
|
||||
this.sendToClient(ws, {
|
||||
type: 'subscribed',
|
||||
dleAddress: dleAddress,
|
||||
message: 'Подписка на деплой активирована'
|
||||
});
|
||||
|
||||
// Если есть активная сессия деплоя, отправляем текущий статус
|
||||
if (this.deploymentSessions.has(dleAddress)) {
|
||||
const session = this.deploymentSessions.get(dleAddress);
|
||||
this.sendToClient(ws, {
|
||||
type: 'deployment_status',
|
||||
dleAddress: dleAddress,
|
||||
...session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отписка от деплоя
|
||||
*/
|
||||
unsubscribeFromDeployment(ws, dleAddress) {
|
||||
if (ws.dleAddress === dleAddress) {
|
||||
this.removeClient(ws);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление клиента из всех подписок
|
||||
*/
|
||||
removeClient(ws) {
|
||||
if (ws.dleAddress && this.clients.has(ws.dleAddress)) {
|
||||
this.clients.get(ws.dleAddress).delete(ws);
|
||||
if (this.clients.get(ws.dleAddress).size === 0) {
|
||||
this.clients.delete(ws.dleAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Начало сессии деплоя
|
||||
*/
|
||||
startDeploymentSession(dleAddress, moduleType) {
|
||||
const session = {
|
||||
dleAddress: dleAddress,
|
||||
moduleType: moduleType,
|
||||
status: 'starting',
|
||||
progress: 0,
|
||||
step: 0,
|
||||
message: 'Инициализация деплоя...',
|
||||
logs: [],
|
||||
startTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.deploymentSessions.set(dleAddress, session);
|
||||
|
||||
console.log(`[DeploymentWS] Начало деплоя: ${moduleType} для DLE ${dleAddress}`);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'deployment_started',
|
||||
...session
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление статуса деплоя
|
||||
*/
|
||||
updateDeploymentStatus(dleAddress, updates) {
|
||||
const session = this.deploymentSessions.get(dleAddress);
|
||||
if (!session) {
|
||||
console.warn(`[DeploymentWS] Сессия деплоя не найдена для DLE: ${dleAddress}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем сессию
|
||||
Object.assign(session, updates);
|
||||
session.lastUpdate = new Date().toISOString();
|
||||
|
||||
console.log(`[DeploymentWS] Обновление статуса деплоя DLE ${dleAddress}:`, updates);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'deployment_status',
|
||||
...session
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление лога в сессию деплоя
|
||||
*/
|
||||
addDeploymentLog(dleAddress, logType, message) {
|
||||
const session = this.deploymentSessions.get(dleAddress);
|
||||
if (!session) {
|
||||
console.warn(`[DeploymentWS] Сессия деплоя не найдена для DLE: ${dleAddress}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const logEntry = {
|
||||
type: logType,
|
||||
message: message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
session.logs.push(logEntry);
|
||||
|
||||
console.log(`[DeploymentWS] Лог деплоя DLE ${dleAddress}:`, logEntry);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'deployment_log',
|
||||
dleAddress: dleAddress,
|
||||
log: logEntry
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение сессии деплоя
|
||||
*/
|
||||
finishDeploymentSession(dleAddress, success, message = null) {
|
||||
const session = this.deploymentSessions.get(dleAddress);
|
||||
if (!session) {
|
||||
console.warn(`[DeploymentWS] Сессия деплоя не найдена для DLE: ${dleAddress}`);
|
||||
return;
|
||||
}
|
||||
|
||||
session.status = success ? 'completed' : 'failed';
|
||||
session.progress = success ? 100 : session.progress;
|
||||
session.endTime = new Date().toISOString();
|
||||
session.message = message || (success ? 'Деплой завершен успешно' : 'Деплой завершен с ошибкой');
|
||||
|
||||
console.log(`[DeploymentWS] Завершение деплоя DLE ${dleAddress}: ${session.status}`);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'deployment_finished',
|
||||
...session
|
||||
});
|
||||
|
||||
// Удаляем сессию через 30 секунд
|
||||
setTimeout(() => {
|
||||
this.deploymentSessions.delete(dleAddress);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения конкретному клиенту
|
||||
*/
|
||||
sendToClient(ws, message) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify(message));
|
||||
} catch (error) {
|
||||
console.error('[DeploymentWS] Ошибка отправки сообщения клиенту:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения всем клиентам конкретного DLE
|
||||
*/
|
||||
broadcastToDLE(dleAddress, message) {
|
||||
const clients = this.clients.get(dleAddress);
|
||||
if (clients) {
|
||||
clients.forEach(ws => {
|
||||
this.sendToClient(ws, message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка ошибки клиенту
|
||||
*/
|
||||
sendError(ws, errorMessage) {
|
||||
this.sendToClient(ws, {
|
||||
type: 'error',
|
||||
message: errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление об обновлении модулей
|
||||
*/
|
||||
notifyModulesUpdated(dleAddress) {
|
||||
console.log(`[DeploymentWS] Уведомление об обновлении модулей для DLE: ${dleAddress}`);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'modules_updated',
|
||||
dleAddress: dleAddress,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление о верификации модуля
|
||||
*/
|
||||
notifyModuleVerified(dleAddress, moduleType, networkName) {
|
||||
console.log(`[DeploymentWS] Уведомление о верификации модуля ${moduleType} в сети ${networkName} для DLE: ${dleAddress}`);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'module_verified',
|
||||
dleAddress: dleAddress,
|
||||
moduleType: moduleType,
|
||||
networkName: networkName,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление об изменении статуса модуля
|
||||
*/
|
||||
notifyModuleStatusChanged(dleAddress, moduleType, status) {
|
||||
console.log(`[DeploymentWS] Уведомление об изменении статуса модуля ${moduleType} на ${status} для DLE: ${dleAddress}`);
|
||||
|
||||
this.broadcastToDLE(dleAddress, {
|
||||
type: 'module_status_changed',
|
||||
dleAddress: dleAddress,
|
||||
moduleType: moduleType,
|
||||
status: status,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики
|
||||
*/
|
||||
getStats() {
|
||||
const totalClients = Array.from(this.clients.values()).reduce((sum, clients) => sum + clients.size, 0);
|
||||
return {
|
||||
activeConnections: totalClients,
|
||||
activeSessions: this.deploymentSessions.size,
|
||||
subscriptions: this.clients.size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем единственный экземпляр сервиса
|
||||
const deploymentWebSocketService = new DeploymentWebSocketService();
|
||||
|
||||
module.exports = deploymentWebSocketService;
|
||||
@@ -13,6 +13,7 @@
|
||||
const WebSocket = require('ws');
|
||||
const tokenBalanceService = require('./services/tokenBalanceService');
|
||||
const deploymentTracker = require('./utils/deploymentTracker');
|
||||
const deploymentWebSocketService = require('./services/deploymentWebSocketService');
|
||||
|
||||
let wss = null;
|
||||
// Храним клиентов по userId для персонализированных уведомлений
|
||||
@@ -70,6 +71,17 @@ function initWSS(server) {
|
||||
// Запрос балансов токенов
|
||||
handleTokenBalancesRequest(ws, data.address, data.userId);
|
||||
}
|
||||
|
||||
// Обработка сообщений для деплоя модулей
|
||||
if (data.type === 'subscribe' && data.dleAddress) {
|
||||
// Подписка на деплой для конкретного DLE
|
||||
deploymentWebSocketService.subscribeToDeployment(ws, data.dleAddress);
|
||||
}
|
||||
|
||||
if (data.type === 'unsubscribe' && data.dleAddress) {
|
||||
// Отписка от деплоя
|
||||
deploymentWebSocketService.unsubscribeFromDeployment(ws, data.dleAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
|
||||
}
|
||||
@@ -485,6 +497,35 @@ function broadcastDeploymentUpdate(data) {
|
||||
console.log(`📡 [WebSocket] Отправлено deployment update: ${data.type || 'unknown'}`);
|
||||
}
|
||||
|
||||
// Функция для уведомления об обновлениях модулей
|
||||
function broadcastModulesUpdate(dleAddress, updateType, moduleData) {
|
||||
if (!wss) return;
|
||||
|
||||
console.log(`📡 [WebSocket] broadcastModulesUpdate вызвана для DLE: ${dleAddress}, тип: ${updateType}`);
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: updateType,
|
||||
dleAddress: dleAddress,
|
||||
moduleData: moduleData,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(`📡 [WebSocket] Отправляем сообщение модулей:`, message);
|
||||
|
||||
// Отправляем всем подключенным клиентам
|
||||
wss.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
client.send(message);
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Ошибка при отправке modules update:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📡 [WebSocket] Отправлено modules update: ${updateType} для DLE: ${dleAddress}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initWSS,
|
||||
broadcastContactsUpdate,
|
||||
@@ -504,6 +545,7 @@ module.exports = {
|
||||
broadcastTokenBalancesUpdate,
|
||||
broadcastTokenBalanceChanged,
|
||||
broadcastDeploymentUpdate,
|
||||
broadcastModulesUpdate,
|
||||
getConnectedUsers,
|
||||
getStats
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user