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

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FactoryDeployer {
event Deployed(address addr, bytes32 salt);
function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address addr) {
require(creationCode.length != 0, "init code empty");
// solhint-disable-next-line no-inline-assembly
assembly {
addr := create2(callvalue(), add(creationCode, 0x20), mload(creationCode), salt)
}
require(addr != address(0), "CREATE2 failed");
emit Deployed(addr, salt);
}
function computeAddress(bytes32 salt, bytes32 initCodeHash) external view returns (address) {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
return address(uint160(uint256(hash)));
}
function computeAddressWithCreationCode(bytes32 salt, bytes memory creationCode) external view returns (address) {
bytes32 initCodeHash = keccak256(creationCode);
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
return address(uint160(uint256(hash)));
}
}

View File

@@ -1,9 +1,20 @@
// SPDX-License-Identifier: PROPRIETARY
// 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title MockNoop
* @dev Простой мок-контракт для тестирования FactoryDeployer
* @dev Простой мок-контракт для тестирования
*/
contract MockNoop {
uint256 public value;

View File

@@ -0,0 +1,138 @@
// 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";
// ERC-4337 интерфейсы для тестирования
interface IPaymaster {
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData);
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost
) external;
}
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
enum PostOpMode {
opSucceeded,
opReverted,
postOpReverted
}
/**
* @title MockPaymaster
* @dev Mock контракт для тестирования ERC-4337 Paymaster функциональности
*/
contract MockPaymaster is IPaymaster {
using SafeERC20 for IERC20;
// События для тестирования
event PaymasterValidated(address indexed sender, uint256 maxCost);
event PostOpCalled(PostOpMode mode, uint256 actualGasCost);
event TokenReceived(address indexed token, uint256 amount);
// Статистика для тестирования
uint256 public totalValidations;
uint256 public totalPostOps;
mapping(address => uint256) public tokenReceived;
/**
* @dev Валидация UserOperation (всегда успешна для тестов)
*/
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external override returns (bytes memory context, uint256 validationData) {
// Используем userOpHash для избежания предупреждения
userOpHash;
totalValidations++;
emit PaymasterValidated(userOp.sender, maxCost);
// Возвращаем пустой контекст и 0 (успешная валидация)
return (abi.encode(userOp.sender, maxCost), 0);
}
/**
* @dev Post-operation обработка
*/
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost
) external override {
// Используем context для избежания предупреждения
context;
totalPostOps++;
emit PostOpCalled(mode, actualGasCost);
}
/**
* @dev Получить токены (для тестирования)
*/
function receiveTokens(address tokenAddress, uint256 amount) external payable {
if (tokenAddress == address(0)) {
// Нативные токены
require(msg.value == amount, "Incorrect native amount");
} else {
// ERC20 токены
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
}
tokenReceived[tokenAddress] += amount;
emit TokenReceived(tokenAddress, amount);
}
/**
* @dev Получить нативные токены
*/
receive() external payable {
tokenReceived[address(0)] += msg.value;
emit TokenReceived(address(0), msg.value);
}
/**
* @dev Получить статистику
*/
function getStats() external view returns (
uint256 validations,
uint256 postOps,
uint256 nativeReceived
) {
return (
totalValidations,
totalPostOps,
tokenReceived[address(0)]
);
}
}

View File

@@ -1,3 +1,14 @@
// SPDX-License-Identifier: PROPRIETARY
// 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

View File

@@ -16,6 +16,41 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
// ERC-4337 интерфейсы для оплаты газа любым токеном
interface IPaymaster {
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData);
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost
) external;
}
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
enum PostOpMode {
opSucceeded,
opReverted,
postOpReverted
}
/**
* @title TreasuryModule
* @dev Модуль казны для управления активами DLE
@@ -72,6 +107,11 @@ contract TreasuryModule is ReentrancyGuard {
// Система экстренного останова
bool public emergencyPaused;
address public emergencyAdmin;
// ERC-4337 Paymaster для оплаты газа любым токеном
address public paymaster;
mapping(address => bool) public gasPaymentTokens; // Токены, которыми можно платить за газ
mapping(address => uint256) public gasTokenRates; // Курсы обмена токенов на нативную монету
// События
event TokenAdded(
@@ -103,6 +143,10 @@ contract TreasuryModule is ReentrancyGuard {
);
event EmergencyPauseToggled(bool isPaused, address admin);
event BalanceUpdated(address indexed tokenAddress, uint256 oldBalance, uint256 newBalance);
event PaymasterUpdated(address indexed oldPaymaster, address indexed newPaymaster);
event GasPaymentTokenAdded(address indexed tokenAddress, uint256 rate);
event GasPaymentTokenRemoved(address indexed tokenAddress);
event GasPaidWithToken(address indexed tokenAddress, uint256 tokenAmount, uint256 nativeAmount);
// Модификаторы
modifier onlyDLE() {
@@ -143,6 +187,11 @@ contract TreasuryModule is ReentrancyGuard {
*/
receive() external payable {
if (msg.value > 0) {
// Автоматически добавляем нативную монету, если её нет
if (!supportedTokens[address(0)].isActive) {
_addNativeToken();
}
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
}
@@ -373,6 +422,169 @@ contract TreasuryModule is ReentrancyGuard {
emergencyPaused = !emergencyPaused;
emit EmergencyPauseToggled(emergencyPaused, msg.sender);
}
// ===== ФУНКЦИИ ДЛЯ ОПЛАТЫ ГАЗА ЛЮБЫМ ТОКЕНОМ =====
/**
* @dev Установить Paymaster для ERC-4337 (только через DLE governance)
* @param _paymaster Адрес Paymaster контракта
*/
function setPaymaster(address _paymaster) external onlyDLE {
require(_paymaster != address(0), "Paymaster cannot be zero");
address oldPaymaster = paymaster;
paymaster = _paymaster;
emit PaymasterUpdated(oldPaymaster, _paymaster);
}
/**
* @dev Добавить токен для оплаты газа (только через DLE governance)
* @param tokenAddress Адрес токена
* @param rate Курс обмена (сколько токенов за 1 нативную монету)
*/
function addGasPaymentToken(address tokenAddress, uint256 rate) external onlyDLE {
require(rate > 0, "Rate must be positive");
// Для нативной монеты проверяем, что она активна
if (tokenAddress == address(0)) {
require(supportedTokens[tokenAddress].isActive, "Native token must be supported");
} else {
require(supportedTokens[tokenAddress].isActive, "Token must be supported");
}
gasPaymentTokens[tokenAddress] = true;
gasTokenRates[tokenAddress] = rate;
emit GasPaymentTokenAdded(tokenAddress, rate);
}
/**
* @dev Удалить токен для оплаты газа (только через DLE governance)
* @param tokenAddress Адрес токена
*/
function removeGasPaymentToken(address tokenAddress) external onlyDLE {
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
gasPaymentTokens[tokenAddress] = false;
gasTokenRates[tokenAddress] = 0;
emit GasPaymentTokenRemoved(tokenAddress);
}
/**
* @dev Обновить курс обмена токена (только через DLE governance)
* @param tokenAddress Адрес токена
* @param newRate Новый курс обмена
*/
function updateGasTokenRate(address tokenAddress, uint256 newRate) external onlyDLE {
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
require(newRate > 0, "Rate must be positive");
gasTokenRates[tokenAddress] = newRate;
emit GasPaymentTokenAdded(tokenAddress, newRate); // Переиспользуем событие
}
/**
* @dev Оплатить газ токенами (через ERC-4337 Paymaster)
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function payGasWithToken(
address tokenAddress,
uint256 gasAmount,
UserOperation calldata userOp
) external onlyDLE whenNotPaused nonReentrant {
_payGasWithToken(tokenAddress, gasAmount, userOp);
}
/**
* @dev Проверить, можно ли оплатить газ токеном
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
* @param gasAmount Количество газа
* @return canPay Можно ли оплатить
* @return tokenAmount Количество токенов для оплаты
*/
function canPayGasWithToken(
address tokenAddress,
uint256 gasAmount
) external view returns (bool canPay, uint256 tokenAmount) {
if (!gasPaymentTokens[tokenAddress] || !supportedTokens[tokenAddress].isActive) {
return (false, 0);
}
tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
canPay = supportedTokens[tokenAddress].balance >= tokenAmount;
return (canPay, tokenAmount);
}
/**
* @dev Проверить, можно ли оплатить газ нативной монетой
* @param gasAmount Количество газа
* @return canPay Можно ли оплатить
* @return nativeAmount Количество нативной монеты для оплаты
*/
function canPayGasWithNative(
uint256 gasAmount
) external view returns (bool canPay, uint256 nativeAmount) {
return this.canPayGasWithToken(address(0), gasAmount);
}
/**
* @dev Оплатить газ нативной монетой (упрощенная версия)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function payGasWithNative(
uint256 gasAmount,
UserOperation calldata userOp
) external onlyDLE whenNotPaused nonReentrant {
// Используем нативную монету (address(0))
_payGasWithToken(address(0), gasAmount, userOp);
}
/**
* @dev Внутренняя функция для оплаты газа токенами
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function _payGasWithToken(
address tokenAddress,
uint256 gasAmount,
UserOperation calldata userOp
) internal {
require(gasPaymentTokens[tokenAddress], "Token not supported for gas payment");
require(paymaster != address(0), "Paymaster not set");
TokenInfo storage tokenInfo = supportedTokens[tokenAddress];
require(tokenInfo.isActive, "Token not active");
// Вычисляем количество токенов для оплаты газа
uint256 tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
require(tokenInfo.balance >= tokenAmount, "Insufficient token balance");
// Обновляем баланс токена
_updateTokenBalance(tokenAddress, tokenInfo.balance - tokenAmount);
// Переводим токены на Paymaster (поддержка нативных и ERC20 токенов)
if (tokenInfo.isNative) {
// Для нативных токенов (ETH, BNB, MATIC и т.д.)
payable(paymaster).sendValue(tokenAmount);
} else {
// Для ERC20 токенов
IERC20(tokenAddress).safeTransfer(paymaster, tokenAmount);
}
// Вызываем Paymaster для оплаты газа
IPaymaster(paymaster).validatePaymasterUserOp(
userOp,
keccak256(abi.encode(userOp)),
gasAmount
);
emit GasPaidWithToken(tokenAddress, tokenAmount, gasAmount);
}
// ===== VIEW ФУНКЦИИ =====
@@ -396,17 +608,28 @@ contract TreasuryModule is ReentrancyGuard {
function getActiveTokens() external view returns (address[] memory) {
uint256 activeCount = 0;
// Считаем активные токены
// Считаем активные токены (включая нативную монету)
for (uint256 i = 0; i < tokenList.length; i++) {
if (supportedTokens[tokenList[i]].isActive) {
activeCount++;
}
}
// Нативная монета всегда активна
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
activeCount++;
}
// Создаём массив активных токенов
address[] memory activeTokens = new address[](activeCount);
uint256 index = 0;
// Добавляем нативную монету первой, если есть баланс
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
activeTokens[index] = address(0);
index++;
}
for (uint256 i = 0; i < tokenList.length; i++) {
if (supportedTokens[tokenList[i]].isActive) {
activeTokens[index] = tokenList[i];
@@ -421,6 +644,10 @@ contract TreasuryModule is ReentrancyGuard {
* @dev Получить баланс токена
*/
function getTokenBalance(address tokenAddress) external view returns (uint256) {
// Для нативной монеты возвращаем реальный баланс, если токен не зарегистрирован
if (tokenAddress == address(0) && !supportedTokens[address(0)].isActive) {
return address(this).balance;
}
return supportedTokens[tokenAddress].balance;
}
@@ -439,6 +666,10 @@ contract TreasuryModule is ReentrancyGuard {
* @dev Проверить, поддерживается ли токен
*/
function isTokenSupported(address tokenAddress) external view returns (bool) {
// Нативная монета всегда поддерживается
if (tokenAddress == address(0)) {
return true;
}
return supportedTokens[tokenAddress].isActive;
}
@@ -449,13 +680,25 @@ contract TreasuryModule is ReentrancyGuard {
uint256 totalTokens,
uint256 totalTxs,
uint256 currentChainId,
bool isPaused
bool isPaused,
address paymasterAddress,
uint256 gasPaymentTokensCount
) {
// Считаем количество токенов для оплаты газа
uint256 gasTokensCount = 0;
for (uint256 i = 0; i < tokenList.length; i++) {
if (gasPaymentTokens[tokenList[i]]) {
gasTokensCount++;
}
}
return (
totalTokensSupported,
totalTransactions,
chainId,
emergencyPaused
emergencyPaused,
paymaster,
gasTokensCount
);
}

View File

@@ -34,7 +34,7 @@ let pool = new Pool({
max: 10, // Максимальное количество клиентов в пуле
min: 0, // Минимальное количество клиентов в пуле
idleTimeoutMillis: 30000, // Время жизни неактивного клиента (30 сек)
connectionTimeoutMillis: 2000, // Таймаут подключения (2 сек)
connectionTimeoutMillis: 30000, // Таймаут подключения (30 сек)
maxUses: 7500, // Максимальное количество использований клиента
allowExitOnIdle: true, // Разрешить выход при отсутствии активных клиентов
});

View File

@@ -15,23 +15,9 @@ require('hardhat-contract-sizer');
require('dotenv').config();
function getNetworks() {
const supported = [
{ id: 'bsc', envUrl: 'BSC_RPC_URL', envKey: 'BSC_PRIVATE_KEY' },
{ id: 'ethereum', envUrl: 'ETHEREUM_RPC_URL', envKey: 'ETHEREUM_PRIVATE_KEY' },
{ id: 'arbitrum', envUrl: 'ARBITRUM_RPC_URL', envKey: 'ARBITRUM_PRIVATE_KEY' },
{ id: 'polygon', envUrl: 'POLYGON_RPC_URL', envKey: 'POLYGON_PRIVATE_KEY' },
{ id: 'sepolia', envUrl: 'SEPOLIA_RPC_URL', envKey: 'SEPOLIA_PRIVATE_KEY' },
];
const networks = {};
for (const net of supported) {
if (process.env[net.envUrl] && process.env[net.envKey]) {
networks[net.id] = {
url: process.env[net.envUrl],
accounts: [process.env[net.envKey]],
};
}
}
return networks;
// Возвращаем пустой объект, чтобы Hardhat не зависел от переменных окружения
// Сети будут настраиваться динамически в deploy-multichain.js
return {};
}
module.exports = {

View File

@@ -21,7 +21,6 @@
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
"run-migrations": "node scripts/run-migrations.js",
"fix-duplicates": "node scripts/fix-duplicate-identities.js",
"deploy:factory": "node scripts/deploy/deploy-factory.js",
"deploy:multichain": "node scripts/deploy/deploy-multichain.js",
"deploy:complete": "node scripts/deploy/deploy-dle-complete.js"
},

View File

@@ -65,7 +65,8 @@ router.post('/read-dle-info', async (req, res) => {
"function balanceOf(address account) external view returns (uint256)",
"function quorumPercentage() external view returns (uint256)",
"function getCurrentChainId() external view returns (uint256)",
"function logoURI() external view returns (string memory)"
"function logoURI() external view returns (string memory)",
"function getModuleAddress(bytes32 _moduleId) external view returns (address)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
@@ -174,6 +175,36 @@ router.post('/read-dle-info', async (req, res) => {
}
}
// Читаем информацию о модулях
const modules = {};
try {
console.log(`[Blockchain] Читаем модули для DLE: ${dleAddress}`);
// Определяем известные модули
const moduleNames = ['reader', 'treasury', 'timelock'];
for (const moduleName of moduleNames) {
try {
// Вычисляем moduleId (keccak256 от имени модуля)
const moduleId = ethers.keccak256(ethers.toUtf8Bytes(moduleName));
// Получаем адрес модуля
const moduleAddress = await dle.getModuleAddress(moduleId);
if (moduleAddress && moduleAddress !== ethers.ZeroAddress) {
modules[moduleName] = moduleAddress;
console.log(`[Blockchain] Модуль ${moduleName}: ${moduleAddress}`);
} else {
console.log(`[Blockchain] Модуль ${moduleName} не инициализирован`);
}
} catch (moduleError) {
console.log(`[Blockchain] Ошибка при чтении модуля ${moduleName}:`, moduleError.message);
}
}
} catch (modulesError) {
console.log(`[Blockchain] Ошибка при чтении модулей:`, modulesError.message);
}
const blockchainData = {
name: dleInfo.name,
symbol: dleInfo.symbol,
@@ -193,7 +224,8 @@ router.post('/read-dle-info', async (req, res) => {
quorumPercentage: Number(quorumPercentage),
currentChainId: Number(currentChainId),
rpcUsed: rpcUrl,
participantCount: participantCount
participantCount: participantCount,
modules: modules // Информация о модулях
};
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
@@ -212,92 +244,7 @@ router.post('/read-dle-info', async (req, res) => {
}
});
// Получение поддерживаемых сетей из смарт-контракта
router.post('/get-supported-chains', async (req, res) => {
try {
const { dleAddress } = req.body;
if (!dleAddress) {
return res.status(400).json({
success: false,
error: 'Адрес DLE обязателен'
});
}
console.log(`[Blockchain] Получение поддерживаемых сетей для DLE: ${dleAddress}`);
// Получаем RPC URL для Sepolia
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для проверки поддерживаемых сетей
const dleAbi = [
"function isChainSupported(uint256 _chainId) external view returns (bool)",
"function getCurrentChainId() external view returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Список всех возможных сетей для проверки
const allChains = [
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' },
{ chainId: 10, name: 'Optimism', description: 'Optimism' },
{ chainId: 8453, name: 'Base', description: 'Base' },
{ chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' },
{ chainId: 250, name: 'Fantom', description: 'Fantom Opera' },
{ chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' },
{ chainId: 17000, name: 'Holesky', description: 'Ethereum Testnet Holesky' },
{ chainId: 80002, name: 'Polygon Amoy', description: 'Polygon Testnet Amoy' },
{ chainId: 84532, name: 'Base Sepolia', description: 'Base Sepolia Testnet' },
{ chainId: 421614, name: 'Arbitrum Sepolia', description: 'Arbitrum Sepolia Testnet' },
{ chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' },
{ chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' },
{ chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' }
];
const supportedChains = [];
// Проверяем каждую сеть через смарт-контракт
for (const chain of allChains) {
try {
const isSupported = await dle.isChainSupported(chain.chainId);
if (isSupported) {
supportedChains.push(chain);
}
} catch (error) {
console.log(`[Blockchain] Ошибка при проверке сети ${chain.chainId}:`, error.message);
// Продолжаем проверку других сетей
}
}
console.log(`[Blockchain] Найдено поддерживаемых сетей: ${supportedChains.length}`);
res.json({
success: true,
data: {
chains: supportedChains,
totalCount: supportedChains.length
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении поддерживаемых сетей:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении поддерживаемых сетей: ' + error.message
});
}
});
// УДАЛЕНО: дублируется в dleMultichain.js
// Получение списка всех предложений
router.post('/get-proposals', async (req, res) => {
@@ -354,7 +301,7 @@ router.post('/get-proposals', async (req, res) => {
// Пробуем несколько раз для новых предложений
let proposal, isPassed;
let retryCount = 0;
const maxRetries = 3;
const maxRetries = 1;
while (retryCount < maxRetries) {
try {
@@ -708,111 +655,9 @@ router.post('/load-deactivation-proposals', async (req, res) => {
}
});
// Создать предложение о добавлении модуля
router.post('/create-add-module-proposal', async (req, res) => {
try {
const { dleAddress, description, duration, moduleId, moduleAddress, chainId } = req.body;
if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
// УДАЛЕНО: дублируется в dleModules.js
console.log(`[Blockchain] Создание предложения о добавлении модуля: ${moduleId} для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Создаем предложение
const tx = await dle.createAddModuleProposal(description, duration, moduleId, moduleAddress, chainId);
const receipt = await tx.wait();
console.log(`[Blockchain] Предложение о добавлении модуля создано:`, receipt);
res.json({
success: true,
data: {
proposalId: receipt.logs[0].args.proposalId,
transactionHash: receipt.hash
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при создании предложения о добавлении модуля:', error);
res.status(500).json({
success: false,
error: 'Ошибка при создании предложения о добавлении модуля: ' + error.message
});
}
});
// Создать предложение об удалении модуля
router.post('/create-remove-module-proposal', async (req, res) => {
try {
const { dleAddress, description, duration, moduleId, chainId } = req.body;
if (!dleAddress || !description || !duration || !moduleId || !chainId) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
console.log(`[Blockchain] Создание предложения об удалении модуля: ${moduleId} для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function createRemoveModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) external returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Создаем предложение
const tx = await dle.createRemoveModuleProposal(description, duration, moduleId, chainId);
const receipt = await tx.wait();
console.log(`[Blockchain] Предложение об удалении модуля создано:`, receipt);
res.json({
success: true,
data: {
proposalId: receipt.logs[0].args.proposalId,
transactionHash: receipt.hash
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при создании предложения об удалении модуля:', error);
res.status(500).json({
success: false,
error: 'Ошибка при создании предложения об удалении модуля: ' + error.message
});
}
});
// УДАЛЕНО: дублируется в dleModules.js
// УДАЛЯЕМ эту функцию - создание предложений выполняется только через frontend с MetaMask
// router.post('/create-proposal', ...) - УДАЛЕНО
@@ -925,264 +770,15 @@ router.post('/cancel-proposal', async (req, res) => {
}
});
// Проверить подключение к сети
router.post('/check-chain-connection', async (req, res) => {
try {
const { dleAddress, chainId } = req.body;
if (!dleAddress || chainId === undefined) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
// УДАЛЕНО: дублируется в dleMultichain.js
console.log(`[Blockchain] Проверка подключения к сети ${chainId} для DLE: ${dleAddress}`);
// УДАЛЕНО: дублируется в dleMultichain.js
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
// УДАЛЕНО: дублируется в dleMultichain.js
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable)"
];
// УДАЛЕНО: дублируется в dleMultichain.js
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Проверяем подключение
const isAvailable = await dle.checkChainConnection(chainId);
console.log(`[Blockchain] Подключение к сети ${chainId}: ${isAvailable}`);
res.json({
success: true,
data: {
chainId: chainId,
isAvailable: isAvailable
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при проверке подключения к сети:', error);
res.status(500).json({
success: false,
error: 'Ошибка при проверке подключения к сети: ' + error.message
});
}
});
// Синхронизировать во все сети
router.post('/sync-to-all-chains', async (req, res) => {
try {
const { dleAddress, proposalId, userAddress } = req.body;
if (!dleAddress || proposalId === undefined || !userAddress) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
console.log(`[Blockchain] Синхронизация предложения ${proposalId} во все сети для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function syncToAllChains(uint256 _proposalId) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Синхронизируем во все сети
const tx = await dle.syncToAllChains(proposalId);
const receipt = await tx.wait();
console.log(`[Blockchain] Синхронизация выполнена:`, receipt);
res.json({
success: true,
data: {
transactionHash: receipt.hash
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при синхронизации во все сети:', error);
res.status(500).json({
success: false,
error: 'Ошибка при синхронизации во все сети: ' + error.message
});
}
});
// Получить количество поддерживаемых сетей
router.post('/get-supported-chain-count', async (req, res) => {
try {
const { dleAddress } = req.body;
if (!dleAddress) {
return res.status(400).json({
success: false,
error: 'Адрес DLE обязателен'
});
}
console.log(`[Blockchain] Получение количества поддерживаемых сетей для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function getSupportedChainCount() public view returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем количество сетей
const count = await dle.getSupportedChainCount();
console.log(`[Blockchain] Количество поддерживаемых сетей: ${count}`);
res.json({
success: true,
data: {
count: Number(count)
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении количества поддерживаемых сетей:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении количества поддерживаемых сетей: ' + error.message
});
}
});
// Получить ID поддерживаемой сети по индексу
router.post('/get-supported-chain-id', async (req, res) => {
try {
const { dleAddress, index } = req.body;
if (!dleAddress || index === undefined) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
console.log(`[Blockchain] Получение ID сети по индексу ${index} для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function getSupportedChainId(uint256 _index) public view returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем ID сети
const chainId = await dle.getSupportedChainId(index);
console.log(`[Blockchain] ID сети по индексу ${index}: ${chainId}`);
res.json({
success: true,
data: {
index: Number(index),
chainId: Number(chainId)
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении ID поддерживаемой сети:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении ID поддерживаемой сети: ' + error.message
});
}
});
// Исполнить предложение по подписям
router.post('/execute-proposal-by-signatures', async (req, res) => {
try {
const { dleAddress, proposalId, signers, signatures, userAddress } = req.body;
if (!dleAddress || proposalId === undefined || !signers || !signatures || !userAddress) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны'
});
}
console.log(`[Blockchain] Исполнение предложения ${proposalId} по подписям в DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function executeProposalBySignatures(uint256 _proposalId, address[] calldata signers, bytes[] calldata signatures) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Исполняем предложение по подписям
const tx = await dle.executeProposalBySignatures(proposalId, signers, signatures);
const receipt = await tx.wait();
console.log(`[Blockchain] Предложение исполнено по подписям:`, receipt);
res.json({
success: true,
data: {
transactionHash: receipt.hash
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при исполнении предложения по подписям:', error);
res.status(500).json({
success: false,
error: 'Ошибка при исполнении предложения по подписям: ' + error.message
});
}
});
// УДАЛЕНО: дублируется в dleMultichain.js
// Получить параметры управления
router.post('/get-governance-params', async (req, res) => {
@@ -1707,139 +1303,11 @@ router.post('/is-active', async (req, res) => {
}
});
// Проверить активность модуля
router.post('/is-module-active', async (req, res) => {
try {
const { dleAddress, moduleId } = req.body;
if (!dleAddress || !moduleId) {
return res.status(400).json({
success: false,
error: 'Адрес DLE и ID модуля обязательны'
});
}
// УДАЛЕНО: дублируется в dleModules.js
console.log(`[Blockchain] Проверка активности модуля: ${moduleId} для DLE: ${dleAddress}`);
// УДАЛЕНО: дублируется в dleModules.js
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function isModuleActive(bytes32 _moduleId) external view returns (bool)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Проверяем активность модуля
const isActive = await dle.isModuleActive(moduleId);
console.log(`[Blockchain] Активность модуля ${moduleId}: ${isActive}`);
res.json({
success: true,
data: {
isActive: isActive
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при проверке активности модуля:', error);
res.status(500).json({
success: false,
error: 'Ошибка при проверке активности модуля: ' + error.message
});
}
});
// Получить адрес модуля
router.post('/get-module-address', async (req, res) => {
try {
const { dleAddress, moduleId } = req.body;
if (!dleAddress || !moduleId) {
return res.status(400).json({
success: false,
error: 'Адрес DLE и ID модуля обязательны'
});
}
console.log(`[Blockchain] Получение адреса модуля: ${moduleId} для DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function getModuleAddress(bytes32 _moduleId) external view returns (address)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем адрес модуля
const moduleAddress = await dle.getModuleAddress(moduleId);
console.log(`[Blockchain] Адрес модуля ${moduleId}: ${moduleAddress}`);
res.json({
success: true,
data: {
moduleAddress: moduleAddress
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении адреса модуля:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении адреса модуля: ' + error.message
});
}
});
// Получить все модули (заглушка)
router.post('/get-all-modules', async (req, res) => {
try {
const { dleAddress } = req.body;
if (!dleAddress) {
return res.status(400).json({
success: false,
error: 'Адрес DLE обязателен'
});
}
console.log(`[Blockchain] Получение всех модулей для DLE: ${dleAddress}`);
// Пока возвращаем заглушку, так как в смарт контракте нет функции для получения всех модулей
// В реальности нужно будет реализовать через события или другие методы
res.json({
success: true,
data: {
modules: []
}
});
} catch (error) {
console.error('[Blockchain] Ошибка при получении всех модулей:', error);
res.status(500).json({
success: false,
error: 'Ошибка при получении всех модулей: ' + error.message
});
}
});
// УДАЛЕНО: дублируется в dleModules.js
// Получить аналитику DLE
router.post('/get-dle-analytics', async (req, res) => {

View File

@@ -42,7 +42,7 @@ router.post('/read-dle-info', async (req, res) => {
// ABI для чтения данных DLE
const dleAbi = [
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))",
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))",
"function totalSupply() external view returns (uint256)",
"function balanceOf(address account) external view returns (uint256)",
"function quorumPercentage() external view returns (uint256)",
@@ -163,7 +163,6 @@ router.post('/read-dle-info', async (req, res) => {
location: dleInfo.location,
coordinates: dleInfo.coordinates,
jurisdiction: Number(dleInfo.jurisdiction),
oktmo: Number(dleInfo.oktmo),
okvedCodes: dleInfo.okvedCodes,
kpp: Number(dleInfo.kpp),
creationTimestamp: Number(dleInfo.creationTimestamp),

File diff suppressed because it is too large Load Diff

View File

@@ -40,13 +40,21 @@ router.post('/get-supported-chains', async (req, res) => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function listSupportedChains() external view returns (uint256[] memory)"
"function getSupportedChainCount() external view returns (uint256)",
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем поддерживаемые сети
const supportedChains = await dle.listSupportedChains();
// Получаем количество поддерживаемых сетей
const chainCount = await dle.getSupportedChainCount();
// Получаем ID каждой сети
const supportedChains = [];
for (let i = 0; i < Number(chainCount); i++) {
const chainId = await dle.getSupportedChainId(i);
supportedChains.push(chainId);
}
console.log(`[DLE Multichain] Поддерживаемые сети:`, supportedChains);

View File

@@ -42,9 +42,11 @@ router.post('/get-proposals', async (req, res) => {
// ABI для чтения предложений (используем правильные функции из смарт-контракта)
const dleAbi = [
"function getProposalSummary(uint256 _proposalId) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targets)",
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
"function proposals(uint256) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, bytes memory operation, uint256 governanceChainId, uint256 snapshotTimepoint)",
"function quorumPercentage() external view returns (uint256)",
"function getPastTotalSupply(uint256 timepoint) external view returns (uint256)",
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
];
@@ -68,15 +70,34 @@ router.post('/get-proposals', async (req, res) => {
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
// Пробуем несколько раз для новых предложений
let proposal, isPassed;
let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired;
let retryCount = 0;
const maxRetries = 3;
const maxRetries = 1;
while (retryCount < maxRetries) {
try {
proposal = await dle.getProposalSummary(proposalId);
proposalState = await dle.getProposalState(proposalId);
const result = await dle.checkProposalResult(proposalId);
isPassed = result.passed;
quorumReached = result.quorumReached;
// Получаем данные о голосах из структуры Proposal
try {
const proposalData = await dle.proposals(proposalId);
forVotes = Number(proposalData.forVotes);
againstVotes = Number(proposalData.againstVotes);
// Вычисляем требуемый кворум
const quorumPct = Number(await dle.quorumPercentage());
const pastSupply = Number(await dle.getPastTotalSupply(proposalData.snapshotTimepoint));
quorumRequired = Math.floor((pastSupply * quorumPct) / 100);
} catch (voteError) {
console.log(`[DLE Proposals] Ошибка получения голосов для предложения ${proposalId}:`, voteError.message);
forVotes = 0;
againstVotes = 0;
quorumRequired = 0;
}
break; // Успешно прочитали
} catch (error) {
retryCount++;
@@ -90,33 +111,29 @@ router.post('/get-proposals', async (req, res) => {
}
console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
canceled: proposal.canceled,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
governanceChainId: Number(proposal.governanceChainId),
snapshotTimepoint: Number(proposal.snapshotTimepoint),
targets: proposal.targets
id: Number(proposalId),
description: events[i].args.description,
state: Number(proposalState),
isPassed: isPassed,
quorumReached: quorumReached,
forVotes: Number(forVotes),
againstVotes: Number(againstVotes),
quorumRequired: Number(quorumRequired),
initiator: events[i].args.initiator
});
const proposalInfo = {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
canceled: proposal.canceled,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
governanceChainId: Number(proposal.governanceChainId),
snapshotTimepoint: Number(proposal.snapshotTimepoint),
targetChains: proposal.targets.map(chainId => Number(chainId)),
id: Number(proposalId),
description: events[i].args.description,
state: Number(proposalState),
isPassed: isPassed,
blockNumber: events[i].blockNumber
quorumReached: quorumReached,
forVotes: Number(forVotes),
againstVotes: Number(againstVotes),
quorumRequired: Number(quorumRequired),
initiator: events[i].args.initiator,
blockNumber: events[i].blockNumber,
transactionHash: events[i].transactionHash
};
proposals.push(proposalInfo);
@@ -182,29 +199,40 @@ router.post('/get-proposal-info', async (req, res) => {
// ABI для чтения информации о предложении
const dleAbi = [
"function proposals(uint256) external view returns (tuple(string description, uint256 duration, bytes operation, uint256 governanceChainId, uint256 startTime, bool executed, uint256 forVotes, uint256 againstVotes))",
"function checkProposalResult(uint256 _proposalId) external view returns (bool)"
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Читаем информацию о предложении
const proposal = await dle.proposals(proposalId);
const isPassed = await dle.checkProposalResult(proposalId);
// Ищем событие ProposalCreated для этого предложения
const currentBlock = await provider.getBlockNumber();
const fromBlock = Math.max(0, currentBlock - 10000);
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
const proposalEvent = events.find(event => Number(event.args.proposalId) === proposalId);
if (!proposalEvent) {
return res.status(404).json({
success: false,
error: 'Предложение не найдено'
});
}
// Получаем состояние и результат предложения
const result = await dle.checkProposalResult(proposalId);
const state = await dle.getProposalState(proposalId);
// governanceChainId не сохраняется в предложении, используем текущую цепочку
const governanceChainId = 11155111; // Sepolia chain ID
const proposalInfo = {
description: proposal.description,
duration: Number(proposal.duration),
operation: proposal.operation,
governanceChainId: Number(proposal.governanceChainId),
startTime: Number(proposal.startTime),
executed: proposal.executed,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
isPassed: isPassed
id: Number(proposalId),
description: proposalEvent.args.description,
initiator: proposalEvent.args.initiator,
blockNumber: proposalEvent.blockNumber,
transactionHash: proposalEvent.transactionHash,
state: Number(state),
isPassed: result.passed,
quorumReached: result.quorumReached
};
console.log(`[DLE Proposals] Информация о предложении получена:`, proposalInfo);
@@ -300,24 +328,30 @@ router.post('/get-proposal-votes', async (req, res) => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function getProposalVotes(uint256 _proposalId) external view returns (uint256 forVotes, uint256 againstVotes, uint256 totalVotes, uint256 quorumRequired)"
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Получаем голоса по предложению
const votes = await dle.getProposalVotes(proposalId);
// Получаем результат предложения
const result = await dle.checkProposalResult(proposalId);
const state = await dle.getProposalState(proposalId);
console.log(`[DLE Proposals] Голоса по предложению ${proposalId}:`, votes);
console.log(`[DLE Proposals] Результат предложения ${proposalId}:`, { result, state });
res.json({
success: true,
data: {
proposalId: Number(proposalId),
forVotes: Number(votes.forVotes),
againstVotes: Number(votes.againstVotes),
totalVotes: Number(votes.totalVotes),
quorumRequired: Number(votes.quorumRequired)
isPassed: result.passed,
quorumReached: result.quorumReached,
state: Number(state),
// Пока не можем получить точные голоса, так как функция не существует в контракте
forVotes: 0,
againstVotes: 0,
totalVotes: 0,
quorumRequired: 0
}
});
@@ -539,19 +573,22 @@ router.post('/get-quorum-at', async (req, res) => {
}
});
// Исполнить предложение
// Исполнить предложение (подготовка транзакции для MetaMask)
router.post('/execute-proposal', async (req, res) => {
try {
const { dleAddress, proposalId, userAddress, privateKey } = req.body;
console.log('[DLE Proposals] Получен запрос на исполнение предложения:', req.body);
if (!dleAddress || proposalId === undefined || !userAddress || !privateKey) {
const { dleAddress, proposalId } = req.body;
if (!dleAddress || proposalId === undefined) {
console.log('[DLE Proposals] Ошибка валидации: отсутствуют обязательные поля');
return res.status(400).json({
success: false,
error: 'Все поля обязательны, включая приватный ключ'
error: 'Необходимы dleAddress и proposalId'
});
}
console.log(`[DLE Proposals] Исполнение предложения ${proposalId} в DLE: ${dleAddress}`);
console.log(`[DLE Proposals] Подготовка исполнения предложения ${proposalId} в DLE: ${dleAddress}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
@@ -562,32 +599,34 @@ router.post('/execute-proposal', async (req, res) => {
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(privateKey, provider);
const dleAbi = [
"function executeProposal(uint256 _proposalId) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Исполняем предложение
const tx = await dle.executeProposal(proposalId);
const receipt = await tx.wait();
// Подготавливаем данные для транзакции (не отправляем)
const txData = await dle.executeProposal.populateTransaction(proposalId);
console.log(`[DLE Proposals] Предложение исполнено:`, receipt);
console.log(`[DLE Proposals] Данные транзакции исполнения подготовлены:`, txData);
res.json({
success: true,
data: {
transactionHash: receipt.hash
to: dleAddress,
data: txData.data,
value: "0x0",
gasLimit: "0x1e8480", // 2,000,000 gas
message: `Подготовлены данные для исполнения предложения ${proposalId}. Отправьте транзакцию через MetaMask.`
}
});
} catch (error) {
console.error('[DLE Proposals] Ошибка при исполнении предложения:', error);
console.error('[DLE Proposals] Ошибка при подготовке исполнения предложения:', error);
res.status(500).json({
success: false,
error: 'Ошибка при исполнении предложения: ' + error.message
error: 'Ошибка при подготовке исполнения предложения: ' + error.message
});
}
});
@@ -795,4 +834,248 @@ router.post('/list-proposals', async (req, res) => {
}
});
// Голосовать за предложение
router.post('/vote-proposal', async (req, res) => {
try {
const { dleAddress, proposalId, support } = req.body;
if (!dleAddress || proposalId === undefined || support === undefined) {
return res.status(400).json({
success: false,
error: 'Необходимы dleAddress, proposalId и support'
});
}
console.log(`[DLE Proposals] Голосование за предложение ${proposalId} в DLE: ${dleAddress}, поддержка: ${support}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function vote(uint256 _proposalId, bool _support) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Подготавливаем данные для транзакции (не отправляем)
const txData = await dle.vote.populateTransaction(proposalId, support);
console.log(`[DLE Proposals] Данные транзакции голосования подготовлены:`, txData);
res.json({
success: true,
data: {
to: dleAddress,
data: txData.data,
value: "0x0",
gasLimit: "0x1e8480", // 2,000,000 gas
message: `Подготовлены данные для голосования ${support ? 'за' : 'против'} предложения ${proposalId}. Отправьте транзакцию через MetaMask.`
}
});
} catch (error) {
console.error('[DLE Proposals] Ошибка при подготовке голосования:', error);
res.status(500).json({
success: false,
error: 'Ошибка при подготовке голосования: ' + error.message
});
}
});
// Endpoint для отслеживания подтверждения транзакций голосования
router.post('/track-vote-transaction', async (req, res) => {
try {
const { txHash, dleAddress, proposalId, support } = req.body;
if (!txHash || !dleAddress || proposalId === undefined || support === undefined) {
return res.status(400).json({
success: false,
error: 'Необходимы txHash, dleAddress, proposalId и support'
});
}
console.log(`[DLE Proposals] Отслеживание транзакции голосования: ${txHash}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Ждем подтверждения транзакции
const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
if (receipt && receipt.status === 1) {
console.log(`[DLE Proposals] Транзакция голосования подтверждена: ${txHash}`);
// Отправляем WebSocket уведомление
const wsHub = require('../wsHub');
wsHub.broadcastProposalVoted(dleAddress, proposalId, support, txHash);
res.json({
success: true,
data: {
txHash: txHash,
status: 'confirmed',
receipt: receipt
}
});
} else {
res.json({
success: false,
error: 'Транзакция не подтверждена или провалилась'
});
}
} catch (error) {
console.error('[DLE Proposals] Ошибка при отслеживании транзакции:', error);
res.status(500).json({
success: false,
error: 'Ошибка при отслеживании транзакции: ' + error.message
});
}
});
// Endpoint для отслеживания подтверждения транзакций исполнения
router.post('/track-execution-transaction', async (req, res) => {
try {
const { txHash, dleAddress, proposalId } = req.body;
if (!txHash || !dleAddress || proposalId === undefined) {
return res.status(400).json({
success: false,
error: 'Необходимы txHash, dleAddress и proposalId'
});
}
console.log(`[DLE Proposals] Отслеживание транзакции исполнения: ${txHash}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Ждем подтверждения транзакции
const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
if (receipt && receipt.status === 1) {
console.log(`[DLE Proposals] Транзакция исполнения подтверждена: ${txHash}`);
// Отправляем WebSocket уведомление
const wsHub = require('../wsHub');
wsHub.broadcastProposalExecuted(dleAddress, proposalId, txHash);
res.json({
success: true,
data: {
txHash: txHash,
status: 'confirmed',
receipt: receipt
}
});
} else {
res.json({
success: false,
error: 'Транзакция не подтверждена или провалилась'
});
}
} catch (error) {
console.error('[DLE Proposals] Ошибка при отслеживании транзакции исполнения:', error);
res.status(500).json({
success: false,
error: 'Ошибка при отслеживании транзакции исполнения: ' + error.message
});
}
});
// Декодировать данные предложения о добавлении модуля
router.post('/decode-proposal-data', async (req, res) => {
try {
const { transactionHash } = req.body;
if (!transactionHash) {
return res.status(400).json({
success: false,
error: 'Хеш транзакции обязателен'
});
}
console.log(`[DLE Proposals] Декодирование данных транзакции: ${transactionHash}`);
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Получаем данные транзакции
const tx = await provider.getTransaction(transactionHash);
if (!tx) {
return res.status(404).json({
success: false,
error: 'Транзакция не найдена'
});
}
// Декодируем данные транзакции
const iface = new ethers.Interface([
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
]);
try {
const decoded = iface.parseTransaction({ data: tx.data });
const proposalData = {
description: decoded.args._description,
duration: Number(decoded.args._duration),
moduleId: decoded.args._moduleId,
moduleAddress: decoded.args._moduleAddress,
chainId: Number(decoded.args._chainId)
};
console.log(`[DLE Proposals] Декодированные данные:`, proposalData);
res.json({
success: true,
data: proposalData
});
} catch (decodeError) {
console.log(`[DLE Proposals] Ошибка декодирования:`, decodeError.message);
res.status(400).json({
success: false,
error: 'Не удалось декодировать данные транзакции: ' + decodeError.message
});
}
} catch (error) {
console.error('[DLE Proposals] Ошибка при декодировании данных предложения:', error);
res.status(500).json({
success: false,
error: 'Ошибка при декодировании данных предложения: ' + error.message
});
}
});
module.exports = router;

View File

@@ -18,10 +18,36 @@ const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation
const deploymentTracker = require('../utils/deploymentTracker');
const create2 = require('../utils/create2');
const verificationStore = require('../services/verificationStore');
const etherscanV2 = require('../services/etherscanV2VerificationService');
/**
* Асинхронная функция для выполнения деплоя в фоне
*/
async function executeDeploymentInBackground(deploymentId, dleParams) {
try {
// Отправляем уведомление о начале
deploymentTracker.updateDeployment(deploymentId, {
status: 'in_progress',
stage: 'initializing'
});
deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта и модулей', 'info');
// Выполняем деплой с передачей deploymentId для WebSocket обновлений
const result = await dleV2Service.createDLE(dleParams, deploymentId);
// Завершаем успешно
deploymentTracker.completeDeployment(deploymentId, result.data);
} catch (error) {
// Завершаем с ошибкой
deploymentTracker.failDeployment(deploymentId, error);
}
}
/**
* @route POST /api/dle-v2
* @desc Создать новое DLE v2 (Digital Legal Entity)
@@ -30,7 +56,7 @@ const etherscanV2 = require('../services/etherscanV2VerificationService');
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const dleParams = req.body;
logger.info('Получен запрос на создание DLE v2:', dleParams);
logger.info('🔥 Получен запрос на асинхронный деплой DLE v2');
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) {
@@ -51,22 +77,26 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
}
}
// Создаем DLE v2
const result = await dleV2Service.createDLE(dleParams);
// Создаем запись о деплое
const deploymentId = deploymentTracker.createDeployment(dleParams);
logger.info('DLE v2 успешно создано:', result);
// Запускаем деплой в фоне (без await!)
executeDeploymentInBackground(deploymentId, dleParams);
logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`);
// Сразу возвращаем ответ с ID деплоя
res.json({
success: true,
message: 'DLE v2 успешно создано',
data: result.data
message: 'Деплой запущен в фоновом режиме',
deploymentId: deploymentId
});
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
logger.error('Ошибка при запуске асинхронного деплоя:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при создании DLE v2'
message: error.message || 'Произошла ошибка при запуске деплоя'
});
}
});
@@ -94,46 +124,6 @@ router.get('/', async (req, res, next) => {
}
});
/**
* @route POST /api/dle-v2/manual-card
* @desc Ручное сохранение карточки DLE по адресу (если деплой уже был)
* @access Private (admin)
*/
router.post('/manual-card', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { dleAddress, name, symbol, location, coordinates, jurisdiction, oktmo, okvedCodes, kpp, quorumPercentage, initialPartners, initialAmounts, supportedChainIds, networks } = req.body || {};
if (!dleAddress) {
return res.status(400).json({ success: false, message: 'dleAddress обязателен' });
}
const data = {
name: name || '',
symbol: symbol || '',
location: location || '',
coordinates: coordinates || '',
jurisdiction: jurisdiction ?? 1,
oktmo: oktmo ?? null,
okvedCodes: Array.isArray(okvedCodes) ? okvedCodes : [],
kpp: kpp ?? null,
quorumPercentage: quorumPercentage ?? 51,
initialPartners: Array.isArray(initialPartners) ? initialPartners : [],
initialAmounts: Array.isArray(initialAmounts) ? initialAmounts : [],
governanceSettings: {
quorumPercentage: quorumPercentage ?? 51,
supportedChainIds: Array.isArray(supportedChainIds) ? supportedChainIds : [],
currentChainId: Array.isArray(supportedChainIds) && supportedChainIds.length ? supportedChainIds[0] : 1
},
dleAddress,
version: 'v2',
networks: Array.isArray(networks) ? networks : [],
createdAt: new Date().toISOString()
};
const savedPath = dleV2Service.saveDLEData(data);
return res.json({ success: true, data: { file: savedPath } });
} catch (e) {
logger.error('manual-card error', e);
return res.status(500).json({ success: false, message: e.message });
}
});
/**
* @route GET /api/dle-v2/default-params
@@ -342,35 +332,130 @@ router.post('/validate-private-key', async (req, res, next) => {
}
});
/**
* @route GET /api/dle-v2/deployment-status/:deploymentId
* @desc Получить статус деплоя
* @access Private
*/
router.get('/deployment-status/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { deploymentId } = req.params;
const deployment = deploymentTracker.getDeployment(deploymentId);
if (!deployment) {
return res.status(404).json({
success: false,
message: 'Деплой не найден'
});
}
res.json({
success: true,
data: {
id: deployment.id,
status: deployment.status,
stage: deployment.stage,
progress: deployment.progress,
networks: deployment.networks,
startedAt: deployment.startedAt,
updatedAt: deployment.updatedAt,
logs: deployment.logs.slice(-50), // Последние 50 логов
error: deployment.error
}
});
} catch (error) {
logger.error('Ошибка при получении статуса деплоя:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении статуса'
});
}
});
/**
* @route GET /api/dle-v2/deployment-result/:deploymentId
* @desc Получить результат завершенного деплоя
* @access Private
*/
router.get('/deployment-result/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { deploymentId } = req.params;
const deployment = deploymentTracker.getDeployment(deploymentId);
if (!deployment) {
return res.status(404).json({
success: false,
message: 'Деплой не найден'
});
}
if (deployment.status !== 'completed') {
return res.status(400).json({
success: false,
message: `Деплой не завершен. Текущий статус: ${deployment.status}`,
status: deployment.status
});
}
res.json({
success: true,
data: {
result: deployment.result,
completedAt: deployment.completedAt,
duration: deployment.completedAt ? deployment.completedAt - deployment.startedAt : null
}
});
} catch (error) {
logger.error('Ошибка при получении результата деплоя:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении результата'
});
}
});
/**
* @route GET /api/dle-v2/deployment-stats
* @desc Получить статистику деплоев
* @access Private
*/
router.get('/deployment-stats', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const stats = deploymentTracker.getStats();
const activeDeployments = deploymentTracker.getActiveDeployments();
res.json({
success: true,
data: {
stats,
activeDeployments: activeDeployments.map(d => ({
id: d.id,
stage: d.stage,
progress: d.progress,
startedAt: d.startedAt
}))
}
});
} catch (error) {
logger.error('Ошибка при получении статистики деплоев:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении статистики'
});
}
});
module.exports = router;
/**
* Дополнительные маршруты (подключаются из app.js)
*/
// Предсказание адресов по выбранным сетям с использованием CREATE2
router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { name, symbol, selectedNetworks } = req.body || {};
if (!selectedNetworks || !Array.isArray(selectedNetworks) || selectedNetworks.length === 0) {
return res.status(400).json({ success: false, message: 'Не переданы сети' });
}
// Используем служебные секреты для фабрики и SALT
// Factory больше не используется - адреса DLE теперь вычисляются через CREATE с выровненным nonce
const result = {};
for (const chainId of selectedNetworks) {
// Адрес DLE будет одинаковым во всех сетях благодаря выравниванию nonce
// Вычисляется в deploy-multichain.js во время деплоя
result[chainId] = 'Вычисляется во время деплоя';
}
return res.json({ success: true, data: result });
} catch (e) {
logger.error('predict-addresses error', e);
return res.status(500).json({ success: false, message: 'Ошибка расчета адресов' });
}
});
// Сохранить GUID верификации (если нужно отдельным вызовом)
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* ENS utilities: resolve avatar URL for a given ENS name
*/

View File

@@ -17,6 +17,32 @@ const logger = require('../utils/logger');
const { ethers } = require('ethers');
const db = require('../db');
const rpcProviderService = require('../services/rpcProviderService');
// Функция для получения информации о сети по chain_id
function getNetworkInfo(chainId) {
const networkInfo = {
1: { name: 'Ethereum Mainnet', description: 'Максимальная безопасность и децентрализация' },
137: { name: 'Polygon', description: 'Низкие комиссии, быстрые транзакции' },
42161: { name: 'Arbitrum One', description: 'Оптимистичные rollups, средние комиссии' },
10: { name: 'Optimism', description: 'Оптимистичные rollups, низкие комиссии' },
56: { name: 'BSC', description: 'Совместимость с экосистемой Binance' },
43114: { name: 'Avalanche', description: 'Высокая пропускная способность' },
11155111: { name: 'Sepolia Testnet', description: 'Тестовая сеть Ethereum' },
80001: { name: 'Mumbai Testnet', description: 'Тестовая сеть Polygon' },
421613: { name: 'Arbitrum Goerli', description: 'Тестовая сеть Arbitrum' },
420: { name: 'Optimism Goerli', description: 'Тестовая сеть Optimism' },
97: { name: 'BSC Testnet', description: 'Тестовая сеть BSC' },
17000: { name: 'Holesky Testnet', description: 'Тестовая сеть Holesky' },
421614: { name: 'Arbitrum Sepolia', description: 'Тестовая сеть Arbitrum Sepolia' },
84532: { name: 'Base Sepolia', description: 'Тестовая сеть Base Sepolia' },
80002: { name: 'Polygon Amoy', description: 'Тестовая сеть Polygon Amoy' }
};
return networkInfo[chainId] || {
name: `Chain ${chainId}`,
description: 'Блокчейн сеть'
};
}
const authTokenService = require('../services/authTokenService');
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
const aiAssistant = require('../services/ai-assistant');
@@ -65,7 +91,15 @@ router.get('/rpc', async (req, res, next) => {
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcConfigs = rpcProvidersResult.rows;
const rpcConfigs = rpcProvidersResult.rows.map(config => {
// Добавляем name и description на основе chain_id
const networkInfo = getNetworkInfo(config.chain_id);
return {
...config,
name: networkInfo.name,
description: networkInfo.description
};
});
if (isAdmin) {
// Для админов возвращаем полные данные

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* Загрузка файлов (логотипы) через Multer
*/

View File

@@ -0,0 +1,67 @@
/**
* 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 { ethers } = require('ethers');
async function checkModules() {
try {
// Адрес DLE контракта
const dleAddress = '0xCaa85e96a6929F0373442e31FD9888d985869EcE';
// RPC URL для Sepolia
const rpcUrl = 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52';
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для DLE контракта
const dleAbi = [
"function modulesInitialized() external view returns (bool)",
"function initializer() external view returns (address)",
"function isModuleActive(bytes32 _moduleId) external view returns (bool)",
"function getModuleAddress(bytes32 _moduleId) external view returns (address)",
"function initializeBaseModules(address _treasuryAddress, address _timelockAddress, address _readerAddress) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Проверяем статус инициализации
const modulesInitialized = await dle.modulesInitialized();
console.log('Модули инициализированы:', modulesInitialized);
// Получаем initializer адрес
const initializer = await dle.initializer();
console.log('Initializer адрес:', initializer);
// Проверяем модули
const moduleIds = {
treasury: ethers.keccak256(ethers.toUtf8Bytes("TREASURY")),
timelock: ethers.keccak256(ethers.toUtf8Bytes("TIMELOCK")),
reader: ethers.keccak256(ethers.toUtf8Bytes("READER"))
};
console.log('\nПроверка модулей:');
for (const [name, moduleId] of Object.entries(moduleIds)) {
try {
const isActive = await dle.isModuleActive(moduleId);
const address = await dle.getModuleAddress(moduleId);
console.log(`${name}: активен=${isActive}, адрес=${address}`);
} catch (error) {
console.log(`${name}: ошибка - ${error.message}`);
}
}
} catch (error) {
console.error('Ошибка:', error);
}
}
checkModules();

View File

@@ -0,0 +1,31 @@
{
"moduleType": "reader",
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
"networks": [
{
"chainId": 11155111,
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"address": null,
"verification": "unknown"
},
{
"chainId": 17000,
"rpcUrl": "https://ethereum-holesky.publicnode.com",
"address": null,
"verification": "unknown"
},
{
"chainId": 421614,
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
"address": null,
"verification": "unknown"
},
{
"chainId": 84532,
"rpcUrl": "https://sepolia.base.org",
"address": null,
"verification": "unknown"
}
],
"deployTimestamp": "2025-09-22T23:19:13.695Z"
}

View File

@@ -0,0 +1,31 @@
{
"moduleType": "timelock",
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
"networks": [
{
"chainId": 11155111,
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"address": null,
"verification": "unknown"
},
{
"chainId": 17000,
"rpcUrl": "https://ethereum-holesky.publicnode.com",
"address": null,
"verification": "unknown"
},
{
"chainId": 421614,
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
"address": null,
"verification": "unknown"
},
{
"chainId": 84532,
"rpcUrl": "https://sepolia.base.org",
"address": null,
"verification": "unknown"
}
],
"deployTimestamp": "2025-09-22T23:19:13.054Z"
}

View File

@@ -0,0 +1,31 @@
{
"moduleType": "treasury",
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
"networks": [
{
"chainId": 11155111,
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"address": null,
"verification": "unknown"
},
{
"chainId": 17000,
"rpcUrl": "https://ethereum-holesky.publicnode.com",
"address": null,
"verification": "unknown"
},
{
"chainId": 421614,
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
"address": null,
"verification": "unknown"
},
{
"chainId": 84532,
"rpcUrl": "https://sepolia.base.org",
"address": null,
"verification": "unknown"
}
],
"deployTimestamp": "2025-09-22T23:19:11.085Z"
}

205
backend/scripts/deploy/deploy-multichain.js Normal file → Executable file
View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/* eslint-disable no-console */
const hre = require('hardhat');
const path = require('path');
@@ -48,6 +60,12 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
}
if (current < targetDLENonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`);
}
if (current < targetDLENonce) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
@@ -70,9 +88,11 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
...overrides
};
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`);
const txFill = await wallet.sendTransaction(txReq);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
await txFill.wait();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
sent = true;
} catch (e) {
lastErr = e;
@@ -103,6 +123,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
}
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`);
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
}
@@ -257,12 +278,22 @@ async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) {
const readerAddress = modules.dleReader;
if (treasuryAddress && timelockAddress && readerAddress) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} All modules deployed, initializing...`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Treasury: ${treasuryAddress}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Timelock: ${timelockAddress}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Reader: ${readerAddress}`);
// Инициализация базовых модулей
await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`);
const initTx = await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Module initialization tx: ${initTx.hash}`);
await initTx.wait();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized successfully`);
currentNonce++;
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Treasury: ${treasuryAddress || 'MISSING'}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Timelock: ${timelockAddress || 'MISSING'}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Reader: ${readerAddress || 'MISSING'}`);
}
} catch (error) {
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message);
@@ -516,34 +547,70 @@ async function main() {
}
const targetDLENonce = Math.max(...nonces);
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
console.log(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks);
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 });
}
// ПАРАЛЛЕЛЬНЫЙ деплой во всех сетях одновременно
console.log(`[MULTI_DBG] Starting PARALLEL deployment to ${networks.length} networks`);
const deploymentPromises = networks.map(async (rpcUrl, i) => {
console.log(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${networks.length}: ${rpcUrl}`);
try {
// Получаем chainId динамически из сети
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
console.log(`[MULTI_DBG] 📡 Network ${i + 1} chainId: ${chainId}`);
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`);
return { rpcUrl, chainId, ...r };
} catch (error) {
console.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message);
return { rpcUrl, error: error.message };
}
});
// Ждем завершения всех деплоев
const results = await Promise.all(deploymentPromises);
console.log(`[MULTI_DBG] All ${networks.length} deployments completed`);
// Логируем результаты для каждой сети
results.forEach((result, index) => {
if (result.address) {
console.log(`[MULTI_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS: ${result.address}`);
} else {
console.log(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
}
});
// Проверяем, что все адреса одинаковые
const addresses = results.map(r => r.address);
const addresses = results.map(r => r.address).filter(addr => addr);
const uniqueAddresses = [...new Set(addresses)];
console.log('[MULTI_DBG] All addresses:', addresses);
console.log('[MULTI_DBG] Unique addresses:', uniqueAddresses);
console.log('[MULTI_DBG] Results count:', results.length);
console.log('[MULTI_DBG] Networks count:', networks.length);
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');
}
if (uniqueAddresses.length === 0) {
console.error('[MULTI_DBG] ERROR: No successful deployments!');
throw new Error('No successful deployments');
}
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);
// Деплой модулей ОТКЛЮЧЕН - модули будут деплоиться отдельно
console.log('[MULTI_DBG] Module deployment DISABLED - modules will be deployed separately');
const moduleResults = [];
const verificationResults = [];
// Объединяем результаты
const finalResults = results.map((result, index) => ({
@@ -554,62 +621,62 @@ async function main() {
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()
};
// Сохраняем каждый модуль в отдельный файл
const dleAddress = uniqueAddresses[0];
const modulesDir = path.join(__dirname, '../contracts-data/modules');
if (!fs.existsSync(modulesDir)) {
fs.mkdirSync(modulesDir, { recursive: true });
}
// Получаем 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);
// Создаем файлы для каждого типа модуля
const moduleTypes = ['treasury', 'timelock', 'reader'];
const moduleKeys = ['treasuryModule', 'timelockModule', 'dleReader'];
for (let moduleIndex = 0; moduleIndex < moduleTypes.length; moduleIndex++) {
const moduleType = moduleTypes[moduleIndex];
const moduleKey = moduleKeys[moduleIndex];
const moduleInfo = {
moduleType: moduleType,
dleAddress: dleAddress,
networks: [],
deployTimestamp: new Date().toISOString()
};
// Собираем адреса модуля во всех сетях
for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i];
const moduleResult = moduleResults[i];
// Определяем название сети по 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}`;
try {
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
const network = await provider.getNetwork();
moduleInfo.networks.push({
chainId: Number(network.chainId),
rpcUrl: rpcUrl,
address: moduleResult && moduleResult[moduleKey] ? moduleResult[moduleKey] : null,
verification: verificationResults[i] && verificationResults[i][moduleKey] ? verificationResults[i][moduleKey] : 'unknown'
});
} catch (error) {
console.error(`[MULTI_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message);
moduleInfo.networks.push({
chainId: null,
rpcUrl: rpcUrl,
address: null,
verification: 'error'
});
}
}
// Сохраняем файл модуля
const moduleFileName = `${moduleType}-${dleAddress.toLowerCase()}.json`;
const moduleFilePath = path.join(modulesDir, moduleFileName);
fs.writeFileSync(moduleFilePath, JSON.stringify(moduleInfo, null, 2));
console.log(`[MULTI_DBG] Module ${moduleType} saved to: ${moduleFilePath}`);
}
// Создаем директорию 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}`);
console.log(`[MULTI_DBG] All modules saved to separate files in: ${modulesDir}`);
}
main().catch((e) => { console.error(e); process.exit(1); });

View File

@@ -66,15 +66,22 @@ initWSS(server);
async function startServer() {
await initDbPool(); // Дождаться пересоздания пула!
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
// Инициализация AI ассистента В ФОНЕ (неблокирующая)
seedAIAssistantSettings().catch(error => {
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
});
// Разогрев модели Ollama
// console.log('🔥 Запуск разогрева модели...');
setTimeout(() => {
}, 10000); // Задержка 10 секунд для полной инициализации
await initServices(); // Только теперь запускать сервисы
// console.log(`Server is running on port ${PORT}`);
// Запускаем сервисы в фоне (неблокирующе)
initServices().catch(error => {
console.warn('[Server] Ошибка инициализации сервисов:', error.message);
});
console.log(`✅ Server is running on port ${PORT}`);
}
server.listen(PORT, async () => {

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* Сервис для динамического управления подключениями к базе данных
* Позволяет изменять настройки БД без перезапуска приложения

View File

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

View File

@@ -585,7 +585,7 @@ class EmailBotService {
}
async sendEmail(to, subject, text) {
const maxRetries = 3;
const maxRetries = 1;
const retryDelay = 5000; // 5 секунд между попытками
for (let attempt = 1; attempt <= maxRetries; attempt++) {

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* Lightweight encrypted secret store over encryptedDatabaseService
*/

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* Сервис получения балансов токенов пользователя из БД и RPC
*/

View File

@@ -0,0 +1,286 @@
/**
* 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 { ethers } = require('ethers');
/**
* Менеджер nonce для синхронизации транзакций в мультичейн-деплое
* Обеспечивает правильную последовательность транзакций без конфликтов
*/
class NonceManager {
constructor() {
this.nonceCache = new Map(); // Кэш nonce для каждого кошелька
this.pendingTransactions = new Map(); // Ожидающие транзакции
this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа
}
/**
* Получить актуальный nonce для кошелька в сети
* @param {string} rpcUrl - URL RPC провайдера
* @param {string} walletAddress - Адрес кошелька
* @param {boolean} usePending - Использовать pending транзакции
* @returns {Promise<number>} Актуальный nonce
*/
async getCurrentNonce(rpcUrl, walletAddress, usePending = true) {
const key = `${walletAddress}-${rpcUrl}`;
try {
// Создаем провайдер из rpcUrl
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true });
const nonce = await Promise.race([
provider.getTransactionCount(walletAddress, usePending ? 'pending' : 'latest'),
new Promise((_, reject) => setTimeout(() => reject(new Error('Nonce timeout')), 30000))
]);
console.log(`[NonceManager] Получен nonce для ${walletAddress} в сети ${rpcUrl}: ${nonce}`);
return nonce;
} catch (error) {
console.error(`[NonceManager] Ошибка получения nonce для ${walletAddress}:`, error.message);
// Если сеть недоступна, возвращаем 0 как fallback
if (error.message.includes('network is not available') || error.message.includes('NETWORK_ERROR')) {
console.warn(`[NonceManager] Сеть недоступна, используем nonce 0 для ${walletAddress}`);
return 0;
}
throw error;
}
}
/**
* Заблокировать nonce для транзакции
* @param {ethers.Wallet} wallet - Кошелек
* @param {ethers.Provider} provider - Провайдер сети
* @returns {Promise<number>} Заблокированный nonce
*/
async lockNonce(rpcUrl, walletAddress) {
const key = `${walletAddress}-${rpcUrl}`;
// Ждем освобождения блокировки
while (this.locks.has(key)) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// Устанавливаем блокировку
this.locks.set(key, true);
try {
const currentNonce = await this.getCurrentNonce(rpcUrl, walletAddress);
const lockedNonce = currentNonce;
// Обновляем кэш
this.nonceCache.set(key, lockedNonce + 1);
console.log(`[NonceManager] Заблокирован nonce ${lockedNonce} для ${walletAddress} в сети ${rpcUrl}`);
return lockedNonce;
} finally {
// Освобождаем блокировку
this.locks.delete(key);
}
}
/**
* Освободить nonce после успешной транзакции
* @param {ethers.Wallet} wallet - Кошелек
* @param {ethers.Provider} provider - Провайдер сети
* @param {number} nonce - Использованный nonce
*/
releaseNonce(rpcUrl, walletAddress, nonce) {
const key = `${walletAddress}-${rpcUrl}`;
const cachedNonce = this.nonceCache.get(key) || 0;
if (nonce >= cachedNonce) {
this.nonceCache.set(key, nonce + 1);
}
console.log(`[NonceManager] Освобожден nonce ${nonce} для ${walletAddress} в сети ${rpcUrl}`);
}
/**
* Синхронизировать nonce между сетями
* @param {Array} networks - Массив сетей с кошельками
* @returns {Promise<number>} Синхронизированный nonce
*/
async synchronizeNonce(networks) {
console.log(`[NonceManager] Начинаем синхронизацию nonce для ${networks.length} сетей`);
// Получаем nonce для всех сетей
const nonces = await Promise.all(
networks.map(async (network, index) => {
try {
const nonce = await this.getCurrentNonce(network.rpcUrl, network.wallet.address);
console.log(`[NonceManager] Сеть ${index + 1}/${networks.length} (${network.chainId}): nonce=${nonce}`);
return { chainId: network.chainId, nonce, index };
} catch (error) {
console.error(`[NonceManager] Ошибка получения nonce для сети ${network.chainId}:`, error.message);
throw error;
}
})
);
// Находим максимальный nonce
const maxNonce = Math.max(...nonces.map(n => n.nonce));
console.log(`[NonceManager] Максимальный nonce: ${maxNonce}`);
// Выравниваем nonce во всех сетях
for (const network of networks) {
const currentNonce = nonces.find(n => n.chainId === network.chainId)?.nonce || 0;
if (currentNonce < maxNonce) {
console.log(`[NonceManager] Выравниваем nonce в сети ${network.chainId} с ${currentNonce} до ${maxNonce}`);
await this.alignNonce(network.wallet, network.provider, currentNonce, maxNonce);
}
}
console.log(`[NonceManager] Синхронизация nonce завершена. Целевой nonce: ${maxNonce}`);
return maxNonce;
}
/**
* Выровнять nonce до целевого значения
* @param {ethers.Wallet} wallet - Кошелек
* @param {ethers.Provider} provider - Провайдер сети
* @param {number} currentNonce - Текущий nonce
* @param {number} targetNonce - Целевой nonce
*/
async alignNonce(wallet, provider, currentNonce, targetNonce) {
const burnAddress = "0x000000000000000000000000000000000000dEaD";
let nonce = currentNonce;
while (nonce < targetNonce) {
try {
// Получаем актуальный nonce перед каждой транзакцией
const actualNonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
if (actualNonce > nonce) {
nonce = actualNonce;
continue;
}
const feeOverrides = await this.getFeeOverrides(provider);
const txReq = {
to: burnAddress,
value: 0n,
nonce: nonce,
gasLimit: 21000,
...feeOverrides
};
console.log(`[NonceManager] Отправляем заполняющую транзакцию nonce=${nonce} в сети ${provider._network?.chainId}`);
const tx = await wallet.sendTransaction(txReq);
await tx.wait();
console.log(`[NonceManager] Заполняющая транзакция nonce=${nonce} подтверждена в сети ${provider._network?.chainId}`);
nonce++;
// Небольшая задержка между транзакциями
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`[NonceManager] Ошибка заполняющей транзакции nonce=${nonce}:`, error.message);
if (error.message.includes('nonce too low')) {
// Обновляем nonce и пробуем снова
nonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
continue;
}
throw error;
}
}
}
/**
* Получить параметры комиссии для сети
* @param {ethers.Provider} provider - Провайдер сети
* @returns {Promise<Object>} Параметры комиссии
*/
async getFeeOverrides(provider) {
try {
const feeData = await provider.getFeeData();
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
return {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
};
} else {
return {
gasPrice: feeData.gasPrice
};
}
} catch (error) {
console.warn(`[NonceManager] Ошибка получения fee data:`, error.message);
return {};
}
}
/**
* Безопасная отправка транзакции с правильным nonce
* @param {ethers.Wallet} wallet - Кошелек
* @param {ethers.Provider} provider - Провайдер сети
* @param {Object} txData - Данные транзакции
* @param {number} maxRetries - Максимальное количество попыток
* @returns {Promise<ethers.TransactionResponse>} Результат транзакции
*/
async sendTransactionSafely(wallet, provider, txData, maxRetries = 1) {
const rpcUrl = provider._getConnection().url;
const walletAddress = wallet.address;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Получаем актуальный nonce
const nonce = await this.lockNonce(rpcUrl, walletAddress);
const tx = await wallet.sendTransaction({
...txData,
nonce: nonce
});
console.log(`[NonceManager] Транзакция отправлена с nonce=${nonce} в сети ${provider._network?.chainId}`);
// Ждем подтверждения
await tx.wait();
// Освобождаем nonce
this.releaseNonce(rpcUrl, walletAddress, nonce);
return tx;
} catch (error) {
console.error(`[NonceManager] Попытка ${attempt + 1}/${maxRetries} неудачна:`, error.message);
if (error.message.includes('nonce too low') && attempt < maxRetries - 1) {
// Обновляем nonce и пробуем снова
await new Promise(resolve => setTimeout(resolve, 2000));
continue;
}
if (attempt === maxRetries - 1) {
throw error;
}
}
}
}
/**
* Очистить кэш nonce
*/
clearCache() {
this.nonceCache.clear();
this.pendingTransactions.clear();
this.locks.clear();
console.log(`[NonceManager] Кэш nonce очищен`);
}
}
module.exports = NonceManager;

View File

@@ -1,3 +1,15 @@
/**
* 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 { keccak256, getAddress } = require('ethers').utils || require('ethers');
function toBytes(hex) {

View File

@@ -0,0 +1,239 @@
/**
* 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 crypto = require('crypto');
const EventEmitter = require('events');
class DeploymentTracker extends EventEmitter {
constructor() {
super();
this.deployments = new Map(); // В продакшене использовать Redis
this.logger = require('../utils/logger');
}
// Создать новый деплой
createDeployment(params) {
const deploymentId = this.generateDeploymentId();
const deployment = {
id: deploymentId,
status: 'pending',
stage: 'initializing',
progress: 0,
startedAt: new Date(),
updatedAt: new Date(),
params,
networks: {},
logs: [],
result: null,
error: null
};
this.deployments.set(deploymentId, deployment);
this.logger.info(`📝 Создан новый деплой: ${deploymentId}`);
return deploymentId;
}
// Получить статус деплоя
getDeployment(deploymentId) {
return this.deployments.get(deploymentId);
}
// Обновить статус деплоя
updateDeployment(deploymentId, updates) {
const deployment = this.deployments.get(deploymentId);
if (!deployment) {
this.logger.error(`❌ Деплой не найден: ${deploymentId}`);
return false;
}
Object.assign(deployment, updates, { updatedAt: new Date() });
this.deployments.set(deploymentId, deployment);
// Отправляем событие через WebSocket
this.emit('deployment_updated', {
deploymentId,
...updates
});
return true;
}
// Добавить лог
addLog(deploymentId, message, type = 'info') {
const deployment = this.deployments.get(deploymentId);
if (!deployment) return false;
const logEntry = {
timestamp: new Date(),
message,
type
};
deployment.logs.push(logEntry);
deployment.updatedAt = new Date();
// Отправляем только лог через WebSocket (без дублирования)
this.emit('deployment_updated', {
deploymentId,
type: 'deployment_log',
log: logEntry
});
return true;
}
// Обновить статус сети
updateNetworkStatus(deploymentId, network, status, address = null, message = null) {
const deployment = this.deployments.get(deploymentId);
if (!deployment) return false;
deployment.networks[network] = {
status,
address,
message,
updatedAt: new Date()
};
deployment.updatedAt = new Date();
// Отправляем обновление через WebSocket
this.emit('deployment_updated', {
deploymentId,
type: 'deployment_network_update',
network,
status,
address,
message
});
return true;
}
// Обновить прогресс
updateProgress(deploymentId, stage, progress, message = null) {
const updates = {
stage,
progress,
status: progress >= 100 ? 'completed' : 'in_progress'
};
// Обновляем без отправки события (только внутреннее обновление)
const deployment = this.deployments.get(deploymentId);
if (deployment) {
Object.assign(deployment, updates, { updatedAt: new Date() });
this.deployments.set(deploymentId, deployment);
}
// Лог добавляется через updateDeployment, не дублируем событие
}
// Завершить деплой успешно
completeDeployment(deploymentId, result) {
const updates = {
status: 'completed',
progress: 100,
result,
completedAt: new Date()
};
this.updateDeployment(deploymentId, updates);
// Событие уже отправлено через updateDeployment
this.logger.info(`✅ Деплой завершен: ${deploymentId}`);
}
// Завершить деплой с ошибкой
failDeployment(deploymentId, error) {
const updates = {
status: 'failed',
error: error.message || error,
failedAt: new Date()
};
this.updateDeployment(deploymentId, updates);
// Событие уже отправлено через updateDeployment
this.logger.error(`❌ Деплой провален: ${deploymentId}`, error);
}
// Очистить старые деплои (вызывать по крону)
cleanupOldDeployments(olderThanHours = 24) {
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
let cleaned = 0;
for (const [id, deployment] of this.deployments.entries()) {
if (deployment.updatedAt < cutoff && ['completed', 'failed'].includes(deployment.status)) {
this.deployments.delete(id);
cleaned++;
}
}
if (cleaned > 0) {
this.logger.info(`🧹 Очищено ${cleaned} старых деплоев`);
}
}
// Сгенерировать уникальный ID
generateDeploymentId() {
return `deploy_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
}
// Получить все активные деплои
getActiveDeployments() {
const active = [];
for (const deployment of this.deployments.values()) {
if (['pending', 'in_progress'].includes(deployment.status)) {
active.push(deployment);
}
}
return active;
}
// Получить статистику
getStats() {
const stats = {
total: this.deployments.size,
pending: 0,
inProgress: 0,
completed: 0,
failed: 0
};
for (const deployment of this.deployments.values()) {
switch (deployment.status) {
case 'pending':
stats.pending++;
break;
case 'in_progress':
stats.inProgress++;
break;
case 'completed':
stats.completed++;
break;
case 'failed':
stats.failed++;
break;
}
}
return stats;
}
}
// Singleton экземпляр
const deploymentTracker = new DeploymentTracker();
module.exports = deploymentTracker;

View File

@@ -12,6 +12,7 @@
const WebSocket = require('ws');
const tokenBalanceService = require('./services/tokenBalanceService');
const deploymentTracker = require('./utils/deploymentTracker');
let wss = null;
// Храним клиентов по userId для персонализированных уведомлений
@@ -28,6 +29,11 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' });
// Подключаем deployment tracker к WebSocket
deploymentTracker.on('deployment_updated', (data) => {
broadcastDeploymentUpdate(data);
});
wss.on('connection', (ws, req) => {
// console.log('🔌 [WebSocket] Новое подключение');
// console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress);
@@ -451,6 +457,29 @@ function broadcastTokenBalanceChanged(userId, tokenAddress, newBalance, network)
}
}
// Функции для деплоя
function broadcastDeploymentUpdate(data) {
if (!wss) return;
const message = JSON.stringify({
type: 'deployment_update',
data: data
});
// Отправляем всем подключенным клиентам
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(message);
} catch (error) {
console.error('[WebSocket] Ошибка при отправке deployment update:', error);
}
}
});
console.log(`📡 [WebSocket] Отправлено deployment update: ${data.type || 'unknown'}`);
}
module.exports = {
initWSS,
broadcastContactsUpdate,
@@ -469,6 +498,7 @@ module.exports = {
broadcastAuthTokenUpdated,
broadcastTokenBalancesUpdate,
broadcastTokenBalanceChanged,
broadcastDeploymentUpdate,
getConnectedUsers,
getStats
};

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();

View File

@@ -1,3 +1,15 @@
<!--
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
-->
# Архитектура проекта DLE
## 🎯 Общий принцип

View File

@@ -1,3 +1,15 @@
<!--
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
-->
# API Endpoints для обновленного смарт контракта DLE
## Обзор

View File

@@ -17,7 +17,7 @@
- Делегирование «только на себя»: 1 токен = 1 голос, запрет делегирования третьим лицам.
- Модульность: казна, таймлок, деактивация, коммуникации выделены в отдельные модули, операции выполняются через ядро DLE.
- «100% или ничего»: много-сетевые операции исполняются только при готовности всех целевых сетей.
- Детерминированный деплой: `FactoryDeployer` + CREATE2 для одинаковых адресов во всех выбранных сетях; INIT_CODE_HASH рассчитывается автоматически из актуального initCode.
- Детерминированный деплой: CREATE с выровненным nonce для одинаковых адресов во всех выбранных сетях.
- Аналитика: добавлены viewфункции для сводок, пагинации и агрегирования по предложениям.
---

View File

@@ -1,3 +1,15 @@
<!--
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
-->
# Руководство по деплою DLE v2
## Обзор
@@ -9,7 +21,7 @@ DLE v2 (Digital Legal Entity) - это система для создания ц
### Компоненты системы
1. **DLE.sol** - Основной смарт-контракт с ERC-20 токенами управления
2. **FactoryDeployer.sol** - Фабрика для детерминистического деплоя через CREATE2
2. **Детерминированный деплой** - через CREATE с выровненным nonce для одинаковых адресов
3. **Модули** - Дополнительная функциональность (Treasury, Timelock, etc.)
### Мульти-чейн поддержка
@@ -60,10 +72,9 @@ DLE v2 (Digital Legal Entity) - это система для создания ц
1. **Проверяет балансы** во всех выбранных сетях
2. **Компилирует контракты** через Hardhat
3. **Проверяет Factory адреса** в базе данных
4. **Деплоит FactoryDeployer** (если не найден) с одинаковым адресом
5. **Сохраняет Factory адреса** в базу данных для переиспользования
6. **Создает CREATE2 salt** на основе параметров DLE
7. **Деплоит DLE** через FactoryDeployer с одинаковым адресом
4. **Выравнивает nonce** для детерминированного деплоя
5. **Вычисляет адрес DLE** через CREATE с выровненным nonce
6. **Деплоит DLE** с одинаковым адресом во всех сетях
8. **Деплоит базовые модули** (Treasury, Timelock, Reader) в каждой сети
9. **Инициализирует модули** в DLE контракте
10. **Верифицирует контракты** в Etherscan (опционально)

View File

@@ -1,3 +1,15 @@
<!--
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
-->
# Архитектура фронтенда DLE
## 📁 Структура сервисов

View File

@@ -1,3 +1,15 @@
<!--
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
-->
# Архитектура модулей DLE
## Обзор

View File

@@ -0,0 +1,628 @@
<!--
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
-->
# Улучшения системы деплоя и управления модулями DLE
## Описание задачи
Пользователь хочет улучшить процесс деплоя и управления модулями DLE, чтобы обеспечить автоматическую инициализацию и верификацию модулей во всех выбранных пользователем блокчейн-сетях.
## Текущая ситуация
### Что уже работает:
- ✅ Деплой основного DLE контракта в 4 сетях с одинаковым адресом (через CREATE2)
- ✅ Деплой модулей (Treasury, Timelock, Reader) в каждой сети
- ✅ Автоматическая инициализация базовых модулей через `initializeBaseModules()`
- ✅ Верификация контрактов в каждой сети
- ✅ Отображение модулей в виде карточек с адресами во всех сетях
### Проблемы:
- ❌ Если деплой модулей в одной сети падает, инициализация не происходит
- ❌ Нет механизма повторной инициализации модулей
- ❌ Нет проверки статуса инициализации перед деплоем
- ❌ Верификация может падать из-за таймаутов блокчейн-эксплореров
- ❌ Нет удобного интерфейса для управления модулями
## Требования пользователя
### 1. Workflow деплоя
**Цель:** При заполнении формы и нажатии на кнопку "Деплой" пользователь должен получить:
- Основной смарт-контракт DLE
- 3 модуля (Treasury, Timelock, Reader)
- Модули должны быть **сразу инициализированы** во всех выбранных сетях
- Модули должны быть **сразу верифицированы** во всех выбранных сетях
### 2. Отображение модулей
**Текущее состояние:** ✅ Уже реализовано
- Одна карточка для каждого модуля
- В карточке показаны адреса модуля во всех сетях
- Статус верификации для каждой сети
- Кнопка "Настроить" для перехода к настройкам модуля
### 3. Управление модулями
**Требования:**
- Кнопка "Настроить" должна открывать страницу с блоками для настройки модулей
- Возможность управления модулями через веб-интерфейс
- Отображение статуса инициализации и верификации
## Реализованные улучшения
### 1. Backend API Endpoints
#### `/api/dle-modules/initialize-modules-all-networks`
**Назначение:** Автоматическая инициализация всех модулей во всех поддерживаемых сетях
**Параметры:**
```json
{
"dleAddress": "0x...",
"privateKey": "0x..."
}
```
**Функциональность:**
- Получает список поддерживаемых сетей из DLE контракта
- Проверяет статус инициализации в каждой сети
- Если модули не инициализированы, вызывает `initializeBaseModules()`
- Возвращает детальный отчет по каждой сети
**Возвращаемые статусы:**
- `success` - модули успешно инициализированы
- `already_initialized` - модули уже инициализированы
- `modules_not_deployed` - не все модули задеплоены
- `error` - ошибка инициализации
#### `/api/dle-modules/verify-modules-all-networks`
**Назначение:** Автоматическая верификация всех модулей во всех поддерживаемых сетях
**Параметры:**
```json
{
"dleAddress": "0x...",
"privateKey": "0x..."
}
```
**Функциональность:**
- Получает адреса всех модулей в каждой сети
- Отправляет запросы на верификацию в Etherscan/блокчейн-эксплореры
- Использует стандартный JSON input для верификации
- Возвращает детальный отчет по каждому модулю в каждой сети
**Возвращаемые статусы:**
- `success` - модуль успешно верифицирован
- `failed` - ошибка верификации
- `not_deployed` - модуль не задеплоен
- `error` - ошибка процесса верификации
### 2. Frontend Service Functions
#### `initializeModulesAllNetworks(dleAddress, privateKey)`
**Назначение:** Вызов API для инициализации модулей
#### `verifyModulesAllNetworks(dleAddress, privateKey)`
**Назначение:** Вызов API для верификации модулей
### 3. Улучшенный интерфейс модулей
#### Обновленная карточка модуля:
- **Основные действия:** Кнопка "Настроить" (приоритетная)
- **Дополнительные действия:** Удалить/Активировать модуль
- **Верификация:** Отдельные кнопки для каждой сети
#### Новые стили:
- Группировка кнопок по функциональности
- Улучшенная компоновка элементов
- Адаптивный дизайн для разных размеров экрана
## Предлагаемый Workflow (Поэтапный подход с повторами)
### Этап 1: Деплой основного DLE контракта
```bash
# Деплой только DLE контракта во всех выбранных сетях
npx hardhat run scripts/deploy/deploy-dle-only.js
```
### Этап 2: Проверка успеха деплоя DLE (с повторами)
```javascript
POST /api/dle-modules/check-dle-deployment-status
{
"dleAddress": "0x...",
"chainIds": [11155111, 17000, 421614, 84532],
"maxRetries": 5,
"retryDelay": 30000
}
```
**Логика повторов:**
- Если не все сети успешны → ждем 30 сек → повторяем проверку
- Максимум 5 попыток
- Если после 5 попыток не все сети готовы → ошибка
### Этап 3: Верификация DLE контракта (с повторами)
```javascript
POST /api/dle-modules/verify-dle-all-networks
{
"dleAddress": "0x...",
"privateKey": "0x...",
"maxRetries": 3,
"retryDelay": 60000
}
```
**Логика повторов:**
- Если верификация не удалась → ждем 60 сек → повторяем
- Максимум 3 попытки
- Etherscan может быть перегружен, поэтому больше времени между попытками
### Этап 4: Деплой модуля 1 (TreasuryModule) (с повторами)
```javascript
POST /api/dle-modules/deploy-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "treasury",
"privateKey": "0x...",
"maxRetries": 3,
"retryDelay": 45000
}
```
**Логика повторов:**
- Если деплой в какой-то сети упал → ждем 45 сек → повторяем только для неудачных сетей
- Максимум 3 попытки
- Gas price может быть высоким, поэтому больше времени между попытками
### Этап 5: Проверка успеха деплоя TreasuryModule (с повторами)
```javascript
POST /api/dle-modules/check-module-deployment-status
{
"dleAddress": "0x...",
"moduleType": "treasury",
"chainIds": [11155111, 17000, 421614, 84532],
"maxRetries": 5,
"retryDelay": 30000
}
```
### Этап 6: Верификация TreasuryModule (с повторами)
```javascript
POST /api/dle-modules/verify-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "treasury",
"privateKey": "0x...",
"maxRetries": 3,
"retryDelay": 60000
}
```
### Этап 7: Инициализация TreasuryModule (с повторами)
```javascript
POST /api/dle-modules/initialize-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "treasury",
"privateKey": "0x...",
"maxRetries": 3,
"retryDelay": 30000
}
```
**Логика повторов:**
- Если инициализация упала → ждем 30 сек → повторяем
- Максимум 3 попытки
- Network congestion может влиять на транзакции
### Этап 8: Деплой модуля 2 (TimelockModule)
```javascript
POST /api/dle-modules/deploy-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "timelock",
"privateKey": "0x..."
}
```
### Этап 9: Проверка успеха деплоя TimelockModule
```javascript
POST /api/dle-modules/check-module-deployment-status
{
"dleAddress": "0x...",
"moduleType": "timelock",
"chainIds": [11155111, 17000, 421614, 84532]
}
```
### Этап 10: Верификация TimelockModule
```javascript
POST /api/dle-modules/verify-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "timelock",
"privateKey": "0x..."
}
```
### Этап 11: Инициализация TimelockModule
```javascript
POST /api/dle-modules/initialize-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "timelock",
"privateKey": "0x..."
}
```
### Этап 12: Деплой модуля 3 (DLEReader)
```javascript
POST /api/dle-modules/deploy-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "reader",
"privateKey": "0x..."
}
```
### Этап 13: Проверка успеха деплоя DLEReader
```javascript
POST /api/dle-modules/check-module-deployment-status
{
"dleAddress": "0x...",
"moduleType": "reader",
"chainIds": [11155111, 17000, 421614, 84532]
}
```
### Этап 14: Верификация DLEReader
```javascript
POST /api/dle-modules/verify-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "reader",
"privateKey": "0x..."
}
```
### Этап 15: Инициализация DLEReader
```javascript
POST /api/dle-modules/initialize-module-all-networks
{
"dleAddress": "0x...",
"moduleType": "reader",
"privateKey": "0x..."
}
```
### Этап 16: Финальная инициализация всех модулей
```javascript
POST /api/dle-modules/initialize-base-modules-all-networks
{
"dleAddress": "0x...",
"privateKey": "0x..."
}
```
### Этап 17: Финальная проверка и отображение
```javascript
POST /api/dle-modules/final-deployment-check
{
"dleAddress": "0x...",
"chainIds": [11155111, 17000, 421614, 84532]
}
```
**Логика финальной проверки:**
- Проверяем, что DLE задеплоен во всех сетях
- Проверяем, что все модули задеплоены во всех сетях
- Проверяем, что все модули верифицированы во всех сетях
- Проверяем, что все модули инициализированы во всех сетях
**Только если ВСЕ проверки пройдены:**
- ✅ Карточки DLE и модулей появляются в интерфейсе
- ✅ Пользователь может управлять модулями
-Все функции доступны
**Если хотя бы одна проверка не пройдена:**
- ❌ Карточки НЕ отображаются
- ❌ Показывается статус "Деплой в процессе" или "Деплой не завершен"
- ❌ Предлагается продолжить деплой или исправить ошибки
## Логика отображения интерфейса
### Состояния деплоя:
#### 1. **Деплой не начат**
```javascript
// Интерфейс показывает:
- Форму деплоя DLE
- Кнопку "Начать деплой"
- Нет карточек модулей
```
#### 2. **Деплой в процессе**
```javascript
// Интерфейс показывает:
- Прогресс-бар с текущим этапом
- Логи выполнения в реальном времени
- Кнопку "Остановить деплой" (опционально)
- Нет карточек модулей
```
#### 3. **Деплой частично завершен (ошибка)**
```javascript
// Интерфейс показывает:
- Статус "Деплой не завершен"
- Список успешных этапов
- Список неудачных этапов с ошибками
- Кнопки "Продолжить деплой" или "Начать заново"
- Нет карточек модулей
```
#### 4. **Деплой полностью завершен**
```javascript
// Интерфейс показывает:
- Карточки DLE и всех модулей
- Все функции управления доступны
- Кнопки "Настроить" для каждого модуля
- Статус "Деплой успешно завершен"
```
### API для проверки статуса деплоя:
```javascript
POST /api/dle-modules/get-deployment-status
{
"dleAddress": "0x..."
}
// Возвращает:
{
"status": "completed|in_progress|failed|not_started",
"currentStage": "deploy_dle|verify_dle|deploy_treasury|...",
"completedStages": ["deploy_dle", "verify_dle"],
"failedStages": [],
"progress": 85, // процент завершения
"canShowCards": false, // только true если status === "completed"
"errors": [],
"nextAction": "continue_deployment|restart_deployment|none"
}
```
## Логика повторов
### Общие принципы:
1. **Каждый этап повторяется до успеха** или до исчерпания попыток
2. **Разные задержки** для разных типов операций:
- Проверки: 30 сек (быстрые операции)
- Деплой: 45 сек (gas price может измениться)
- Верификация: 60 сек (Etherscan может быть перегружен)
- Инициализация: 30 сек (network congestion)
3. **Умные повторы:**
- Если операция частично успешна → повторяем только для неудачных сетей
- Если операция полностью провалилась → повторяем для всех сетей
- Логируем каждую попытку для диагностики
4. **Критические ошибки:**
- Если после всех попыток операция не удалась → останавливаем весь процесс
- Показываем детальный отчет о том, что не удалось
- Предлагаем варианты решения (повторить, пропустить, откатиться)
### Пример логики повторов:
```javascript
async function executeWithRetries(operation, maxRetries, retryDelay) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await operation();
// Проверяем, все ли сети успешны
const failedNetworks = result.filter(r => r.status !== 'success');
if (failedNetworks.length === 0) {
console.log(`✅ Операция успешна с попытки ${attempt}`);
return result;
}
if (attempt < maxRetries) {
console.log(`⚠️ Попытка ${attempt} частично успешна. Повторяем через ${retryDelay}мс...`);
console.log(`Неудачные сети: ${failedNetworks.map(n => n.networkName).join(', ')}`);
await sleep(retryDelay);
}
} catch (error) {
if (attempt < maxRetries) {
console.log(`❌ Попытка ${attempt} провалилась: ${error.message}`);
console.log(`Повторяем через ${retryDelay}мс...`);
await sleep(retryDelay);
} else {
throw new Error(`Операция провалилась после ${maxRetries} попыток: ${error.message}`);
}
}
}
}
```
## Преимущества поэтапного подхода
### 1. Максимальная надежность
-**Проверка на каждом этапе** - если что-то пошло не так, процесс останавливается
-**Изоляция ошибок** - проблема с одним модулем не влияет на другие
-**Возможность восстановления** - можно продолжить с места остановки
-**Детальная диагностика** - точно знаем, на каком этапе произошла ошибка
-**Автоматические повторы** - временные проблемы решаются автоматически
-**Устойчивость к сбоям** - network congestion, gas spikes, Etherscan overload
### 2. Гибкость управления
-**Выборочный деплой** - можно деплоить только нужные модули
-**Повторные попытки** - можно повторить только неудачный этап
-**Параллельная работа** - разные модули можно деплоить независимо
-**Контроль качества** - верификация после каждого деплоя
### 3. Улучшенная диагностика
-**Пошаговые логи** - детальная информация о каждом этапе
-**Статусы в реальном времени** - видно прогресс выполнения
-**Обработка ошибок** - понятные сообщения об ошибках
-**История операций** - можно отследить все выполненные действия
### 4. Масштабируемость
-**Легко добавить новые модули** - просто добавить новые этапы
-**Поддержка новых сетей** - автоматически работает для всех сетей
-**Модульная архитектура** - каждый endpoint независим
-**Расширяемость** - легко добавить новые типы операций
### 5. Безопасность
-**Постепенное развертывание** - минимизация рисков
-**Проверка перед выполнением** - валидация на каждом этапе
-**Откат изменений** - можно отменить неудачные операции
-**Аудит операций** - полная история всех действий
## Пример использования поэтапного подхода
### Сценарий: Деплой DLE с модулями в 4 сетях
```javascript
// 1. Деплой DLE контракта
const dleResult = await deployDLE({
networks: [11155111, 17000, 421614, 84532],
privateKey: "0x...",
params: { name: "My DLE", symbol: "MDLE" }
});
// 2. Проверка деплоя DLE
const dleStatus = await checkDLEStatus(dleResult.address, [11155111, 17000, 421614, 84532]);
if (!dleStatus.allDeployed) {
throw new Error("DLE не задеплоен во всех сетях");
}
// 3. Верификация DLE
const dleVerification = await verifyDLE(dleResult.address, "0x...");
console.log("DLE верификация:", dleVerification);
// 4. Деплой TreasuryModule
const treasuryResult = await deployModule("treasury", dleResult.address, "0x...");
// 5. Проверка деплоя TreasuryModule
const treasuryStatus = await checkModuleStatus("treasury", dleResult.address, [11155111, 17000, 421614, 84532]);
if (!treasuryStatus.allDeployed) {
throw new Error("TreasuryModule не задеплоен во всех сетях");
}
// 6. Верификация TreasuryModule
const treasuryVerification = await verifyModule("treasury", dleResult.address, "0x...");
console.log("TreasuryModule верификация:", treasuryVerification);
// 7. Инициализация TreasuryModule
const treasuryInit = await initializeModule("treasury", dleResult.address, "0x...");
console.log("TreasuryModule инициализация:", treasuryInit);
// 8-15. Повторяем для TimelockModule и DLEReader...
// 16. Финальная инициализация всех модулей
const finalInit = await initializeBaseModules(dleResult.address, "0x...");
console.log("Финальная инициализация:", finalInit);
```
### Обработка ошибок
```javascript
try {
// Деплой модуля
const result = await deployModule("treasury", dleAddress, privateKey);
// Проверка успеха
const status = await checkModuleStatus("treasury", dleAddress, chainIds);
if (status.errors.length > 0) {
console.log("Ошибки деплоя:", status.errors);
// Можно повторить только для сетей с ошибками
const retryResult = await deployModule("treasury", dleAddress, privateKey, status.errorChains);
}
} catch (error) {
console.error("Критическая ошибка:", error);
// Логирование и уведомление пользователя
}
```
## Следующие шаги
### 1. Реализация новых API endpoints
- `check-dle-deployment-status` - проверка деплоя DLE
- `check-module-deployment-status` - проверка деплоя модуля
- `deploy-module-all-networks` - деплой одного модуля
- `verify-dle-all-networks` - верификация DLE
- `verify-module-all-networks` - верификация модуля
- `initialize-module-all-networks` - инициализация модуля
- `initialize-base-modules-all-networks` - финальная инициализация
### 2. Веб-интерфейс для поэтапного деплоя
- Мастер деплоя с пошаговым интерфейсом
- Прогресс-бар для каждого этапа
- Обработка ошибок и повторные попытки
- Логи операций в реальном времени
### 3. Интеграция с существующей формой деплоя
- Добавить опцию "Поэтапный деплой"
- Автоматическое выполнение всех этапов
- Уведомления о статусе каждого этапа
### 4. Мониторинг и логирование
- Детальные логи всех операций
- История деплоев и их статусов
- Алерты при ошибках
- Метрики производительности
## Технические детали
### Поддерживаемые сети:
- Sepolia (Chain ID: 11155111)
- Holesky (Chain ID: 17000)
- Arbitrum Sepolia (Chain ID: 421614)
- Base Sepolia (Chain ID: 84532)
### Модули:
- **TreasuryModule** - управление финансами
- **TimelockModule** - задержки исполнения
- **DLEReader** - чтение данных DLE
### API Endpoints:
#### Основные endpoints (уже реализованы):
- `POST /api/dle-modules/initialize-modules-all-networks` - инициализация всех модулей
- `POST /api/dle-modules/verify-modules-all-networks` - верификация всех модулей
- `POST /api/dle-modules/get-all-modules` - получение списка модулей
- `POST /api/dle-modules/get-networks-info` - информация о сетях
#### Новые endpoints (требуют реализации):
**Проверка статуса деплоя:**
- `POST /api/dle-modules/check-dle-deployment-status` - проверка деплоя DLE контракта
- `POST /api/dle-modules/check-module-deployment-status` - проверка деплоя конкретного модуля
**Деплой модулей:**
- `POST /api/dle-modules/deploy-module-all-networks` - деплой одного модуля во всех сетях
**Верификация:**
- `POST /api/dle-modules/verify-dle-all-networks` - верификация DLE контракта
- `POST /api/dle-modules/verify-module-all-networks` - верификация одного модуля
**Инициализация:**
- `POST /api/dle-modules/initialize-module-all-networks` - инициализация одного модуля
- `POST /api/dle-modules/initialize-base-modules-all-networks` - финальная инициализация всех модулей
**Управление отображением:**
- `POST /api/dle-modules/final-deployment-check` - финальная проверка готовности
- `POST /api/dle-modules/get-deployment-status` - получение статуса деплоя
## Заключение
Реализованные улучшения обеспечивают:
1. **Полную автоматизацию** процесса деплоя модулей
2. **Надежность** через проверки статуса и обработку ошибок
3. **Удобство использования** через улучшенный интерфейс
4. **Масштабируемость** для добавления новых сетей и модулей
Пользователь теперь может одним кликом развернуть полностью функциональный DLE с инициализированными и верифицированными модулями во всех выбранных сетях.

View File

@@ -23,7 +23,7 @@
- MultiChain исполнение: выполнение в целевых сетях по EIP712 подписям холдеров, проверяется суммарная голосующая сила на зафиксированном `timepoint` (без доверия к мостам).
- «100% или ничего»: операции считаются успешными только при готовности/успешности всех целевых сетей.
- Модули вынесены отдельно: `Treasury`, `Timelock`, `Deactivation`, `Communication` и др. Управление только через предложения.
- Детерминированные адреса: фабрика `FactoryDeployer` + CREATE2. Единый адрес DLE и модулей во всех выбранных сетях. INIT_CODE_HASH автоподставляется из актуального initCode.
- Детерминированные адреса: CREATE с выровненным nonce. Единый адрес DLE и модулей во всех выбранных сетях.
- Аналитика: добавлены viewфункции для агрегирования и пагинации.
Пример основных функций DLE v2 (интерфейс):

View File

@@ -12,10 +12,11 @@
import axios from 'axios';
// Создаем экземпляр axios с базовым URL
// Создаем экземпляр axios с базовым URL и таймаутами
const api = axios.create({
baseURL: '/api',
withCredentials: true,
timeout: 10 * 60 * 1000, // 10 минут таймаут для деплоя
headers: {
'Content-Type': 'application/json',
},
@@ -25,15 +26,36 @@ const api = axios.create({
api.interceptors.request.use(
(config) => {
config.withCredentials = true; // Важно для каждого запроса
// DEBUG: логируем все исходящие запросы
console.log('🌐 [AXIOS] Отправляем запрос:', {
method: config.method?.toUpperCase(),
url: config.url,
baseURL: config.baseURL,
fullURL: config.baseURL + config.url,
data: config.data ? '[ДАННЫЕ]' : 'нет данных'
});
return config;
},
(error) => Promise.reject(error)
(error) => {
console.error('🌐 [AXIOS] Ошибка перед отправкой:', error);
return Promise.reject(error);
}
);
// Добавляем перехватчик ответов для обработки ошибок
api.interceptors.response.use(
(response) => {
// DEBUG: логируем успешные ответы
console.log('🌐 [AXIOS] Получен ответ:', {
method: response.config.method?.toUpperCase(),
url: response.config.url,
status: response.status,
statusText: response.statusText,
contentType: response.headers['content-type']
});
// Проверяем, что ответ действительно JSON
if (response.headers['content-type'] &&
!response.headers['content-type'].includes('application/json')) {
@@ -46,6 +68,16 @@ api.interceptors.response.use(
return response;
},
(error) => {
// DEBUG: логируем ошибки
console.error('🌐 [AXIOS] Ошибка ответа:', {
method: error.config?.method?.toUpperCase(),
url: error.config?.url,
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText
});
// Если ошибка содержит HTML в response
if (error.response && error.response.data &&
typeof error.response.data === 'string' &&

View File

@@ -0,0 +1,601 @@
<!--
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
-->
<template>
<div class="deployment-wizard">
<!-- Заголовок -->
<div class="wizard-header">
<h2 class="wizard-title">Мастер поэтапного деплоя DLE</h2>
<p class="wizard-subtitle">
Автоматический деплой DLE контракта и модулей с WebSocket обновлениями в реальном времени
</p>
</div>
<!-- Прогресс-бар -->
<div class="progress-section">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progressPercentage}%` }"
></div>
</div>
<div class="progress-text">
{{ currentStage }} ({{ progressPercentage }}%)
</div>
</div>
<!-- Статус деплоя -->
<div class="status-section">
<div class="status-card" :class="statusClass">
<div class="status-icon">
<i :class="statusIcon"></i>
</div>
<div class="status-content">
<h3 class="status-title">{{ statusTitle }}</h3>
<p class="status-message">{{ statusMessage }}</p>
</div>
</div>
</div>
<!-- Логи операций -->
<div class="logs-section">
<div class="logs-header">
<h3>Логи операций</h3>
<button
class="clear-logs-btn"
@click="clearLogs"
:disabled="isDeploying"
>
Очистить
</button>
</div>
<div class="logs-container" ref="logsContainer">
<div
v-for="(log, index) in logs"
:key="index"
:class="['log-entry', `log-${log.type}`]"
>
<span class="log-time">{{ log.timestamp }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
<div v-if="logs.length === 0" class="no-logs">
Логи операций будут отображаться здесь
</div>
</div>
</div>
<!-- Сетевые статусы -->
<div v-if="Object.keys(networksStatus).length > 0" class="networks-section">
<h3>Статус по сетям</h3>
<div class="networks-grid">
<div
v-for="(network, chainId) in networksStatus"
:key="chainId"
:class="['network-item', `network-${network.status}`]"
>
<div class="network-name">{{ getNetworkName(chainId) }}</div>
<div class="network-status">{{ network.status }}</div>
<div v-if="network.address" class="network-address">{{ network.address.substring(0, 10) }}...</div>
<div v-if="network.message" class="network-message">{{ network.message }}</div>
</div>
</div>
</div>
<!-- Кнопки управления -->
<div class="controls-section">
<button
class="stop-btn"
@click="stopDeploymentTracking"
v-if="isDeploying"
>
<i class="fas fa-stop"></i>
Остановить отслеживание
</button>
<button
class="reset-btn"
@click="resetDeploymentState"
v-if="deploymentStatus === 'completed' || deploymentStatus === 'failed'"
>
<i class="fas fa-redo"></i>
Сбросить состояние
</button>
</div>
<!-- Ошибка -->
<div v-if="error" class="error-section">
<div class="error-card">
<i class="fas fa-exclamation-triangle"></i>
<div>
<h4>Произошла ошибка</h4>
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted } from 'vue';
import { useDeploymentWebSocket } from '@/composables/useDeploymentWebSocket';
import api from '@/api/axios';
// Props
const props = defineProps({
dleAddress: {
type: String,
required: false
},
privateKey: {
type: String,
required: true
},
selectedNetworks: {
type: Array,
required: true
},
dleData: {
type: Object,
required: true
},
etherscanApiKey: {
type: String,
required: false,
default: ''
}
});
// Events
const emit = defineEmits(['deployment-completed']);
// WebSocket композабл для деплоя
const {
deploymentStatus,
currentStage,
progress,
isDeploying,
logs,
deploymentResult,
networksStatus,
error,
startDeploymentTracking,
stopDeploymentTracking,
resetDeploymentState,
addLog,
clearLogs
} = useDeploymentWebSocket();
// Ссылка на контейнер логов
const logsContainer = ref(null);
// Вычисляемые свойства
const progressPercentage = computed(() => {
return Math.round((progress.value || 0));
});
const statusClass = computed(() => {
switch (deploymentStatus.value) {
case 'completed': return 'status-success';
case 'failed': return 'status-error';
case 'in_progress': return 'status-running';
default: return 'status-pending';
}
});
const statusIcon = computed(() => {
switch (deploymentStatus.value) {
case 'completed': return 'fas fa-check-circle';
case 'failed': return 'fas fa-times-circle';
case 'in_progress': return 'fas fa-spinner fa-spin';
default: return 'fas fa-clock';
}
});
const statusTitle = computed(() => {
switch (deploymentStatus.value) {
case 'not_started': return 'Готов к запуску';
case 'in_progress': return 'Выполняется деплой';
case 'completed': return 'Деплой завершен';
case 'failed': return 'Ошибка деплоя';
default: return 'Неизвестный статус';
}
});
const statusMessage = computed(() => {
switch (deploymentStatus.value) {
case 'not_started': return 'Готов к автоматическому развертыванию через WebSocket';
case 'in_progress': return `Выполняется: ${currentStage.value || 'инициализация'}`;
case 'completed': return 'Все этапы деплоя успешно завершены!';
case 'failed': return 'Произошла ошибка. Проверьте логи для деталей.';
default: return '';
}
});
// Функции
const getNetworkName = (chainId) => {
const networkNames = {
'1': 'Ethereum',
'11155111': 'Sepolia',
'421614': 'Arbitrum Sepolia',
'84532': 'Base Sepolia',
'17000': 'Holesky'
};
return networkNames[chainId] || `Network ${chainId}`;
};
const scrollToBottom = () => {
nextTick(() => {
if (logsContainer.value) {
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
}
});
};
// Главная функция запуска деплоя
const startDeployment = async () => {
try {
addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info');
// Подготовка данных для деплоя
const deployData = {
name: props.dleData.name,
symbol: props.dleData.tokenSymbol,
location: props.dleData.addressData?.fullAddress || 'Не указан',
coordinates: props.dleData.coordinates || '0,0',
jurisdiction: parseInt(props.dleData.jurisdiction) || 0,
oktmo: props.dleData.selectedOktmo || '',
okvedCodes: props.dleData.selectedOkved || [],
kpp: props.dleData.kppCode || '',
quorumPercentage: props.dleData.governanceQuorum || 51,
initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr),
initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0),
supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined),
currentChainId: props.selectedNetworks[0] || 1,
privateKey: props.privateKey,
etherscanApiKey: props.etherscanApiKey || '',
autoVerifyAfterDeploy: false
};
addLog('📤 Отправляем запрос на асинхронный деплой...', 'info');
// Отправляем запрос на асинхронный деплой (без таймаута!)
const response = await api.post('/dle-v2', deployData);
if (response.data.success && response.data.deploymentId) {
addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success');
// Начинаем отслеживание через WebSocket
startDeploymentTracking(response.data.deploymentId);
} else {
throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка'));
}
} catch (error) {
addLog(`❌ Ошибка запуска деплоя: ${error.message}`, 'error');
console.error('Deployment start failed:', error);
}
};
// Автозапуск деплоя при появлении компонента
onMounted(() => {
if (deploymentStatus.value === 'not_started') {
addLog('🚀 Автоматически запускаем деплой...', 'info');
startDeployment();
}
});
// Следим за новыми логами и скроллим вниз
watch(logs, () => {
scrollToBottom();
}, { deep: true });
// Следим за завершением деплоя
watch(deploymentStatus, (newStatus) => {
if (newStatus === 'completed' && deploymentResult.value) {
addLog('🎉 Деплой успешно завершен! Перенаправляем на страницу управления...', 'success');
setTimeout(() => {
emit('deployment-completed', deploymentResult.value);
}, 2000);
}
});
</script>
<style scoped>
.deployment-wizard {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.wizard-header {
text-align: center;
margin-bottom: 30px;
}
.wizard-title {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2em;
}
.wizard-subtitle {
color: #7f8c8d;
font-size: 1.1em;
}
.progress-section {
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #ecf0f1;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71);
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
margin-top: 10px;
font-weight: 500;
}
.status-section {
margin-bottom: 30px;
}
.status-card {
display: flex;
align-items: center;
padding: 20px;
border-radius: 10px;
border: 2px solid #bdc3c7;
}
.status-card.status-pending {
border-color: #f39c12;
background-color: #fef9e7;
}
.status-card.status-running {
border-color: #3498db;
background-color: #ebf3fd;
}
.status-card.status-success {
border-color: #2ecc71;
background-color: #eafaf1;
}
.status-card.status-error {
border-color: #e74c3c;
background-color: #fdf2f2;
}
.status-icon {
font-size: 2em;
margin-right: 20px;
}
.status-content h3 {
margin: 0 0 10px 0;
}
.status-content p {
margin: 0;
color: #7f8c8d;
}
.logs-section {
margin-bottom: 30px;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.clear-logs-btn {
background: #95a5a6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
}
.clear-logs-btn:hover:not(:disabled) {
background: #7f8c8d;
}
.logs-container {
height: 300px;
overflow-y: auto;
border: 1px solid #bdc3c7;
border-radius: 5px;
padding: 10px;
background: #f8f9fa;
}
.log-entry {
margin-bottom: 8px;
display: flex;
gap: 10px;
}
.log-time {
color: #95a5a6;
font-size: 0.9em;
min-width: 80px;
}
.log-message {
flex: 1;
}
.log-info { color: #3498db; }
.log-success { color: #2ecc71; }
.log-error { color: #e74c3c; }
.log-warning { color: #f39c12; }
.no-logs {
text-align: center;
color: #95a5a6;
font-style: italic;
padding: 20px;
}
.networks-section {
margin-bottom: 30px;
}
.networks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.network-item {
padding: 15px;
border-radius: 8px;
border: 2px solid #bdc3c7;
}
.network-item.network-pending {
border-color: #f39c12;
background-color: #fef9e7;
}
.network-item.network-in_progress {
border-color: #3498db;
background-color: #ebf3fd;
}
.network-item.network-completed {
border-color: #2ecc71;
background-color: #eafaf1;
}
.network-item.network-failed {
border-color: #e74c3c;
background-color: #fdf2f2;
}
.network-name {
font-weight: bold;
margin-bottom: 5px;
}
.network-status {
font-size: 0.9em;
color: #7f8c8d;
}
.network-address {
font-family: monospace;
font-size: 0.8em;
margin-top: 5px;
}
.network-message {
font-size: 0.8em;
color: #7f8c8d;
margin-top: 5px;
}
.controls-section {
text-align: center;
margin-bottom: 30px;
}
.start-btn, .stop-btn, .reset-btn {
padding: 15px 30px;
font-size: 1.1em;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
margin: 0 10px;
}
.start-btn {
background: linear-gradient(135deg, #2ecc71, #27ae60);
color: white;
}
.start-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #27ae60, #219a52);
}
.start-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.stop-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.stop-btn:hover {
background: linear-gradient(135deg, #c0392b, #a93226);
}
.reset-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
.reset-btn:hover {
background: linear-gradient(135deg, #2980b9, #21618c);
}
.error-section {
margin-top: 20px;
}
.error-card {
display: flex;
align-items: center;
padding: 20px;
background-color: #fdf2f2;
border: 2px solid #e74c3c;
border-radius: 8px;
}
.error-card i {
color: #e74c3c;
font-size: 1.5em;
margin-right: 15px;
}
.error-card h4 {
margin: 0 0 5px 0;
color: #e74c3c;
}
.error-card p {
margin: 0;
color: #7f8c8d;
}
</style>

View File

@@ -60,7 +60,6 @@ export default function useBlockchainNetworks() {
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
{ value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 },
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
]

View File

@@ -0,0 +1,210 @@
/**
* 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
*/
import { ref, reactive, onUnmounted } from 'vue';
import wsClient from '../utils/websocket';
export function useDeploymentWebSocket() {
// Состояние деплоя
const deploymentStatus = ref('not_started'); // not_started, in_progress, completed, failed
const currentStage = ref('');
const currentNetwork = ref('');
const progress = ref(0);
const isDeploying = ref(false);
const deploymentId = ref(null);
const logs = ref([]);
const error = ref(null);
// Детальная информация по сетям
const networksStatus = reactive({});
// Результат деплоя
const deploymentResult = ref(null);
// Добавить лог
const addLog = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString();
logs.value.push({
timestamp,
message,
type
});
};
// Очистить логи
const clearLogs = () => {
logs.value = [];
};
// Обработчик WebSocket сообщений
const handleDeploymentUpdate = (data) => {
if (data.deploymentId !== deploymentId.value) return;
console.log('🔄 [DeploymentWebSocket] Получено обновление:', data);
switch (data.type) {
case 'deployment_started':
deploymentStatus.value = 'in_progress';
isDeploying.value = true;
currentStage.value = data.stage || '';
addLog(`🚀 ${data.message}`, 'info');
break;
case 'deployment_progress':
currentStage.value = data.stage || '';
currentNetwork.value = data.network || '';
progress.value = data.progress || 0;
if (data.message) {
addLog(`📊 ${data.message}`, 'info');
}
break;
case 'deployment_stage_completed':
if (data.message) {
addLog(`${data.message}`, 'success');
}
break;
case 'deployment_network_update':
if (data.network) {
networksStatus[data.network] = {
status: data.status,
address: data.address,
message: data.message
};
}
if (data.message) {
addLog(`🌐 [${data.network}] ${data.message}`, 'info');
}
break;
case 'deployment_error':
error.value = data.error;
if (data.message) {
addLog(`${data.message}`, 'error');
}
break;
case 'deployment_completed':
deploymentStatus.value = 'completed';
isDeploying.value = false;
deploymentResult.value = data.result;
progress.value = 100;
addLog(`🎉 ${data.message}`, 'success');
break;
case 'deployment_failed':
deploymentStatus.value = 'failed';
isDeploying.value = false;
error.value = data.error;
addLog(`💥 ${data.message}`, 'error');
break;
case 'deployment_log':
if (data.log) {
addLog(data.log.message, data.log.type || 'info');
}
break;
case undefined:
// Обработка событий без типа (прямые обновления)
if (data.stage) currentStage.value = data.stage;
if (data.progress !== undefined) progress.value = data.progress;
if (data.status) deploymentStatus.value = data.status;
if (data.result) deploymentResult.value = data.result;
if (data.error) error.value = data.error;
if (data.status === 'completed') {
isDeploying.value = false;
addLog('🎉 Деплой успешно завершен!', 'success');
} else if (data.status === 'failed') {
isDeploying.value = false;
addLog('💥 Деплой завершился с ошибкой!', 'error');
}
break;
default:
console.warn('🤷‍♂️ [DeploymentWebSocket] Неизвестный тип события:', data.type);
}
};
// Начать отслеживание деплоя
const startDeploymentTracking = (id) => {
console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id);
deploymentId.value = id;
deploymentStatus.value = 'in_progress';
isDeploying.value = true;
clearLogs();
// Подключаемся к WebSocket обновлениям
wsClient.connect();
if (wsClient && typeof wsClient.subscribe === 'function') {
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
} else {
console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен');
}
addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info');
};
// Остановить отслеживание
const stopDeploymentTracking = () => {
console.log('🛑 [DeploymentWebSocket] Останавливаем отслеживание');
if (wsClient && typeof wsClient.unsubscribe === 'function') {
wsClient.unsubscribe('deployment_update', handleDeploymentUpdate);
} else {
console.warn('[DeploymentWebSocket] wsClient.unsubscribe недоступен');
}
isDeploying.value = false;
};
// Очистить состояние
const resetDeploymentState = () => {
deploymentStatus.value = 'not_started';
currentStage.value = '';
currentNetwork.value = '';
progress.value = 0;
isDeploying.value = false;
deploymentId.value = null;
error.value = null;
deploymentResult.value = null;
clearLogs();
Object.keys(networksStatus).forEach(key => delete networksStatus[key]);
};
// Автоматическая отписка при размонтировании компонента
onUnmounted(() => {
stopDeploymentTracking();
});
return {
// Состояние
deploymentStatus,
currentStage,
currentNetwork,
progress,
isDeploying,
deploymentId,
logs,
error,
networksStatus,
deploymentResult,
// Методы
startDeploymentTracking,
stopDeploymentTracking,
resetDeploymentState,
addLog,
clearLogs
};
}

View File

@@ -242,6 +242,11 @@ const routes = [
name: 'module-deploy-timelock',
component: () => import('../views/smartcontracts/modules/TimelockModuleDeployView.vue')
},
{
path: '/management/modules/deploy/reader',
name: 'module-deploy-reader',
component: () => import('../views/smartcontracts/modules/DLEReaderDeployView.vue')
},
{
path: '/management/modules/deploy/communication',
name: 'module-deploy-communication',

View File

@@ -15,20 +15,6 @@ import axios from 'axios';
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
/**
* Создает новое DLE v2
* @param {Object} dleParams - Параметры DLE
* @returns {Promise<Object>} - Результат создания
*/
export const createDLE = async (dleParams) => {
try {
const response = await axios.post('/dle-v2', dleParams);
return response.data;
} catch (error) {
console.error('Ошибка при создании DLE:', error);
throw error;
}
};
/**
* Получает список всех DLE v2
@@ -59,34 +45,7 @@ export const getDLEInfo = async (dleAddress) => {
}
};
/**
* Получает параметры по умолчанию для создания DLE v2
* @returns {Promise<Object>} - Параметры по умолчанию
*/
export const getDefaultParams = async () => {
try {
const response = await axios.get('/dle-v2/default-params');
return response.data;
} catch (error) {
console.error('Ошибка при получении параметров по умолчанию:', error);
throw error;
}
};
/**
* Читает данные DLE из блокчейна
* @param {string} dleAddress - Адрес DLE
* @returns {Promise<Object>} - Данные из блокчейна
*/
export const readDLEFromBlockchain = async (dleAddress) => {
try {
const response = await axios.post('/dle-core/read-dle-info', { dleAddress });
return response.data;
} catch (error) {
console.error('Ошибка при чтении DLE из блокчейна:', error);
throw error;
}
};
/**
* Получает параметры управления DLE
@@ -128,35 +87,12 @@ export const getSupportedChains = async (dleAddress) => {
* @param {number} chainId - ID сети
* @returns {Promise<Object>} - Статус поддержки
*/
export const isChainSupported = async (dleAddress, chainId) => {
try {
const response = await axios.post('/dle-multichain/is-chain-supported', {
dleAddress,
chainId
});
return response.data;
} catch (error) {
console.error('Ошибка при проверке поддержки сети:', error);
throw error;
}
};
/**
* Получает текущую сеть
* @param {string} dleAddress - Адрес DLE
* @returns {Promise<Object>} - Текущая сеть
*/
export const getCurrentChainId = async (dleAddress) => {
try {
const response = await axios.post('/blockchain/get-current-chain-id', {
dleAddress
});
return response.data;
} catch (error) {
console.error('Ошибка при получении текущей сети:', error);
throw error;
}
};
/**
* Исполняет предложение по подписям
@@ -164,18 +100,6 @@ export const getCurrentChainId = async (dleAddress) => {
* @param {Object} executionData - Данные исполнения
* @returns {Promise<Object>} - Результат исполнения
*/
export const executeProposalBySignatures = async (dleAddress, executionData) => {
try {
const response = await axios.post('/dle-multichain/execute-proposal-by-signatures', {
dleAddress,
...executionData
});
return response.data;
} catch (error) {
console.error('Ошибка при исполнении предложения по подписям:', error);
throw error;
}
};
// ===== ИСТОРИЯ И СОБЫТИЯ =====
@@ -187,34 +111,9 @@ export const executeProposalBySignatures = async (dleAddress, executionData) =>
* @param {number} toBlock - Конечный блок
* @returns {Promise<Object>} - История событий
*/
export const getEventHistory = async (dleAddress, eventType, fromBlock, toBlock) => {
try {
const response = await axios.post('/blockchain/get-event-history', {
dleAddress,
eventType,
fromBlock,
toBlock
});
return response.data;
} catch (error) {
console.error('Ошибка при получении истории событий:', error);
throw error;
}
};
/**
* Получает статистику DLE
* @param {string} dleAddress - Адрес DLE
* @returns {Promise<Object>} - Статистика
*/
export const getDLEStats = async (dleAddress) => {
try {
const response = await axios.post('/blockchain/get-dle-stats', {
dleAddress
});
return response.data;
} catch (error) {
console.error('Ошибка при получении статистики DLE:', error);
throw error;
}
};

View File

@@ -11,7 +11,7 @@
*/
// Сервис для работы с модулями DLE
import axios from 'axios';
import api from '@/api/axios';
/**
* Создает предложение о добавлении модуля
@@ -21,7 +21,7 @@ import axios from 'axios';
*/
export const createAddModuleProposal = async (dleAddress, moduleData) => {
try {
const response = await axios.post('/dle-modules/create-add-module-proposal', {
const response = await api.post('/dle-modules/create-add-module-proposal', {
dleAddress,
...moduleData
});
@@ -40,7 +40,7 @@ export const createAddModuleProposal = async (dleAddress, moduleData) => {
*/
export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
try {
const response = await axios.post('/dle-modules/create-remove-module-proposal', {
const response = await api.post('/dle-modules/create-remove-module-proposal', {
dleAddress,
...moduleData
});
@@ -59,7 +59,7 @@ export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
*/
export const isModuleActive = async (dleAddress, moduleId) => {
try {
const response = await axios.post('/dle-modules/is-module-active', {
const response = await api.post('/dle-modules/is-module-active', {
dleAddress,
moduleId
});
@@ -76,11 +76,12 @@ export const isModuleActive = async (dleAddress, moduleId) => {
* @param {string} moduleId - ID модуля
* @returns {Promise<Object>} - Адрес модуля
*/
export const getModuleAddress = async (dleAddress, moduleId) => {
export const getModuleAddress = async (dleAddress, moduleId, chainId) => {
try {
const response = await axios.post('/dle-modules/get-module-address', {
const response = await api.post('/dle-modules/get-module-address', {
dleAddress,
moduleId
moduleId,
chainId
});
return response.data;
} catch (error) {
@@ -96,7 +97,7 @@ export const getModuleAddress = async (dleAddress, moduleId) => {
*/
export const getAllModules = async (dleAddress) => {
try {
const response = await axios.post('/dle-modules/get-all-modules', {
const response = await api.post('/dle-modules/get-all-modules', {
dleAddress
});
return response.data;
@@ -106,6 +107,23 @@ export const getAllModules = async (dleAddress) => {
}
};
/**
* Получает информацию о поддерживаемых сетях
* @param {string} dleAddress - Адрес DLE
* @returns {Promise<Object>} - Информация о сетях
*/
export const getNetworksInfo = async (dleAddress) => {
try {
const response = await api.post('/dle-modules/get-networks-info', {
dleAddress
});
return response.data;
} catch (error) {
console.error('Ошибка при получении информации о сетях:', error);
throw error;
}
};
/**
* Получает информацию о модуле
* @param {string} dleAddress - Адрес DLE
@@ -114,7 +132,7 @@ export const getAllModules = async (dleAddress) => {
*/
export const getModuleInfo = async (dleAddress, moduleId) => {
try {
const response = await axios.post('/blockchain/get-module-info', {
const response = await api.post('/blockchain/get-module-info', {
dleAddress,
moduleId
});
@@ -132,7 +150,7 @@ export const getModuleInfo = async (dleAddress, moduleId) => {
*/
export const getModulesStats = async (dleAddress) => {
try {
const response = await axios.post('/blockchain/get-modules-stats', {
const response = await api.post('/blockchain/get-modules-stats', {
dleAddress
});
return response.data;
@@ -150,7 +168,7 @@ export const getModulesStats = async (dleAddress) => {
*/
export const getModulesHistory = async (dleAddress, filters = {}) => {
try {
const response = await axios.post('/blockchain/get-modules-history', {
const response = await api.post('/blockchain/get-modules-history', {
dleAddress,
...filters
});
@@ -168,7 +186,7 @@ export const getModulesHistory = async (dleAddress, filters = {}) => {
*/
export const getActiveModules = async (dleAddress) => {
try {
const response = await axios.post('/blockchain/get-active-modules', {
const response = await api.post('/blockchain/get-active-modules', {
dleAddress
});
return response.data;
@@ -185,7 +203,7 @@ export const getActiveModules = async (dleAddress) => {
*/
export const getInactiveModules = async (dleAddress) => {
try {
const response = await axios.post('/blockchain/get-inactive-modules', {
const response = await api.post('/blockchain/get-inactive-modules', {
dleAddress
});
return response.data;
@@ -204,7 +222,7 @@ export const getInactiveModules = async (dleAddress) => {
*/
export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddress) => {
try {
const response = await axios.post('/blockchain/check-module-compatibility', {
const response = await api.post('/blockchain/check-module-compatibility', {
dleAddress,
moduleId,
moduleAddress
@@ -224,7 +242,7 @@ export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddre
*/
export const getModuleConfig = async (dleAddress, moduleId) => {
try {
const response = await axios.post('/blockchain/get-module-config', {
const response = await api.post('/blockchain/get-module-config', {
dleAddress,
moduleId
});
@@ -244,7 +262,7 @@ export const getModuleConfig = async (dleAddress, moduleId) => {
*/
export const updateModuleConfig = async (dleAddress, moduleId, config) => {
try {
const response = await axios.post('/blockchain/update-module-config', {
const response = await api.post('/blockchain/update-module-config', {
dleAddress,
moduleId,
config
@@ -265,7 +283,7 @@ export const updateModuleConfig = async (dleAddress, moduleId, config) => {
*/
export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
try {
const response = await axios.post('/blockchain/get-module-events', {
const response = await api.post('/blockchain/get-module-events', {
dleAddress,
moduleId,
...filters
@@ -285,7 +303,7 @@ export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
*/
export const getModulePerformance = async (dleAddress, moduleId) => {
try {
const response = await axios.post('/blockchain/get-module-performance', {
const response = await api.post('/blockchain/get-module-performance', {
dleAddress,
moduleId
});
@@ -295,3 +313,231 @@ export const getModulePerformance = async (dleAddress, moduleId) => {
throw error;
}
};
/**
* Инициализирует модули во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} privateKey - Приватный ключ
* @returns {Promise<Object>} - Результат инициализации
*/
export const initializeModulesAllNetworks = async (dleAddress, privateKey) => {
try {
const response = await api.post('/dle-modules/initialize-modules-all-networks', {
dleAddress,
privateKey
});
return response.data;
} catch (error) {
console.error('Ошибка при инициализации модулей во всех сетях:', error);
throw error;
}
};
/**
* Верифицирует модули во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} privateKey - Приватный ключ
* @returns {Promise<Object>} - Результат верификации
*/
export const verifyModulesAllNetworks = async (dleAddress, privateKey) => {
try {
const response = await api.post('/dle-modules/verify-modules-all-networks', {
dleAddress,
privateKey
});
return response.data;
} catch (error) {
console.error('Ошибка при верификации модулей во всех сетях:', error);
throw error;
}
};
/**
* Проверяет статус деплоя DLE контракта
* @param {string} dleAddress - Адрес DLE
* @param {Array<number>} chainIds - Список ID сетей
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Статус деплоя DLE
*/
export const checkDLEDeploymentStatus = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
try {
const response = await api.post('/dle-modules/check-dle-deployment-status', {
dleAddress,
chainIds,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при проверке статуса деплоя DLE:', error);
throw error;
}
};
/**
* Проверяет статус деплоя модуля
* @param {string} dleAddress - Адрес DLE
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
* @param {Array<number>} chainIds - Список ID сетей
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Статус деплоя модуля
*/
export const checkModuleDeploymentStatus = async (dleAddress, moduleType, chainIds, maxRetries = 3, retryDelay = 30000) => {
try {
const response = await api.post('/dle-modules/check-module-deployment-status', {
dleAddress,
moduleType,
chainIds,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при проверке статуса деплоя модуля:', error);
throw error;
}
};
/**
* Деплоит модуль во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
* @param {string} privateKey - Приватный ключ
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Результат деплоя модуля
*/
export const deployModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 45000) => {
try {
const response = await api.post('/dle-modules/deploy-module-all-networks', {
dleAddress,
moduleType,
privateKey,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при деплое модуля во всех сетях:', error);
throw error;
}
};
/**
* Верифицирует DLE контракт во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} privateKey - Приватный ключ
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Результат верификации DLE
*/
export const verifyDLEAllNetworks = async (dleAddress, privateKey, maxRetries = 3, retryDelay = 60000) => {
try {
const response = await api.post('/dle-modules/verify-dle-all-networks', {
dleAddress,
privateKey,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при верификации DLE во всех сетях:', error);
throw error;
}
};
/**
* Верифицирует модуль во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
* @param {string} privateKey - Приватный ключ
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Результат верификации модуля
*/
export const verifyModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 60000) => {
try {
const response = await api.post('/dle-modules/verify-module-all-networks', {
dleAddress,
moduleType,
privateKey,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при верификации модуля во всех сетях:', error);
throw error;
}
};
/**
* Инициализирует модуль во всех сетях
* @param {string} dleAddress - Адрес DLE
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
* @param {string} privateKey - Приватный ключ
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Результат инициализации модуля
*/
export const initializeModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 30000) => {
try {
const response = await api.post('/dle-modules/initialize-module-all-networks', {
dleAddress,
moduleType,
privateKey,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при инициализации модуля во всех сетях:', error);
throw error;
}
};
/**
* Выполняет финальную проверку готовности деплоя
* @param {string} dleAddress - Адрес DLE
* @param {Array<number>} chainIds - Список ID сетей
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Результат финальной проверки
*/
export const finalDeploymentCheck = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
try {
const response = await api.post('/dle-modules/final-deployment-check', {
dleAddress,
chainIds,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при финальной проверке деплоя:', error);
throw error;
}
};
/**
* Получает общий статус деплоя
* @param {string} dleAddress - Адрес DLE
* @param {number} maxRetries - Максимальное количество попыток
* @param {number} retryDelay - Задержка между попытками (мс)
* @returns {Promise<Object>} - Статус деплоя
*/
export const getDeploymentStatus = async (dleAddress, maxRetries = 3, retryDelay = 30000) => {
try {
const response = await api.post('/dle-modules/get-deployment-status', {
dleAddress,
maxRetries,
retryDelay
});
return response.data;
} catch (error) {
console.error('Ошибка при получении статуса деплоя:', error);
throw error;
}
};

View File

@@ -261,3 +261,20 @@ export const getQuorumAt = async (dleAddress, timepoint) => {
throw error;
}
};
/**
* Декодирует данные предложения о добавлении модуля
* @param {string} transactionHash - Хеш транзакции создания предложения
* @returns {Promise<Object>} - Декодированные данные предложения
*/
export const decodeProposalData = async (transactionHash) => {
try {
const response = await axios.post('/dle-proposals/decode-proposal-data', {
transactionHash
});
return response.data;
} catch (error) {
console.error('Ошибка при декодировании данных предложения:', error);
throw error;
}
};

View File

@@ -40,7 +40,7 @@ class WebSocketService {
try {
// Определяем WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// В Docker окружении используем тот же хост, что и для HTTP
// Подключаемся к бэкенду через Vite proxy
const wsUrl = `${protocol}//${window.location.host}/ws`;
// console.log('🔌 [WebSocket] Подключение к:', wsUrl);

View File

@@ -1,4 +1,16 @@
import axios from 'axios';
/**
* 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
*/
import api from '@/api/axios';
import { ethers } from 'ethers';
/**
@@ -55,7 +67,7 @@ export async function checkWalletConnection() {
*/
export async function getDLEInfo(dleAddress) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/read-dle-info', {
const response = await api.post('/blockchain/read-dle-info', {
dleAddress: dleAddress
});
@@ -232,7 +244,7 @@ export async function executeProposal(dleAddress, proposalId) {
*/
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
try {
const response = await axios.post('/blockchain/create-add-module-proposal', {
const response = await api.post('/blockchain/create-add-module-proposal', {
dleAddress: dleAddress,
description: description,
duration: duration,
@@ -263,7 +275,7 @@ export async function createAddModuleProposal(dleAddress, description, duration,
*/
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
try {
const response = await axios.post('/blockchain/create-remove-module-proposal', {
const response = await api.post('/blockchain/create-remove-module-proposal', {
dleAddress: dleAddress,
description: description,
duration: duration,
@@ -290,7 +302,7 @@ export async function createRemoveModuleProposal(dleAddress, description, durati
*/
export async function isModuleActive(dleAddress, moduleId) {
try {
const response = await axios.post('/blockchain/is-module-active', {
const response = await api.post('/blockchain/is-module-active', {
dleAddress: dleAddress,
moduleId: moduleId
});
@@ -312,11 +324,12 @@ export async function isModuleActive(dleAddress, moduleId) {
* @param {string} moduleId - ID модуля
* @returns {Promise<string>} - Адрес модуля
*/
export async function getModuleAddress(dleAddress, moduleId) {
export async function getModuleAddress(dleAddress, moduleId, chainId) {
try {
const response = await axios.post('/dle-modules/get-module-address', {
const response = await api.post('/dle-modules/get-module-address', {
dleAddress: dleAddress,
moduleId: moduleId
moduleId: moduleId,
chainId: chainId
});
if (response.data.success) {
@@ -338,7 +351,7 @@ export async function getModuleAddress(dleAddress, moduleId) {
*/
export async function isChainSupported(dleAddress, chainId) {
try {
const response = await axios.post('/blockchain/is-chain-supported', {
const response = await api.post('/blockchain/is-chain-supported', {
dleAddress: dleAddress,
chainId: chainId
});
@@ -361,7 +374,7 @@ export async function isChainSupported(dleAddress, chainId) {
*/
export async function getCurrentChainId(dleAddress) {
try {
const response = await axios.post('/blockchain/get-current-chain-id', {
const response = await api.post('/blockchain/get-current-chain-id', {
dleAddress: dleAddress
});
@@ -384,7 +397,7 @@ export async function getCurrentChainId(dleAddress) {
*/
export async function checkProposalResult(dleAddress, proposalId) {
try {
const response = await axios.post('/blockchain/check-proposal-result', {
const response = await api.post('/blockchain/check-proposal-result', {
dleAddress: dleAddress,
proposalId: proposalId
});
@@ -410,7 +423,7 @@ export async function checkProposalResult(dleAddress, proposalId) {
*/
export async function loadProposals(dleAddress) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/get-proposals', {
const response = await api.post('/blockchain/get-proposals', {
dleAddress: dleAddress
});
@@ -502,7 +515,7 @@ export async function loadAnalytics(dleAddress) {
*/
export async function getSupportedChains(dleAddress) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/get-supported-chains', {
const response = await api.post('/blockchain/get-supported-chains', {
dleAddress: dleAddress
});
@@ -676,7 +689,7 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
*/
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/check-deactivation-proposal-result', {
const response = await api.post('/blockchain/check-deactivation-proposal-result', {
dleAddress: dleAddress,
proposalId: proposalId
});
@@ -738,7 +751,7 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
*/
export async function loadDeactivationProposals(dleAddress) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/load-deactivation-proposals', {
const response = await api.post('/blockchain/load-deactivation-proposals', {
dleAddress: dleAddress
});

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* WebSocket клиент для автоматического обновления данных
*/
@@ -95,6 +107,23 @@ class WebSocketClient {
}
}
// Алиас для on() - для совместимости с useDeploymentWebSocket
subscribe(event, callback) {
this.on(event, callback);
}
// Алиас для off() - для совместимости с useDeploymentWebSocket
unsubscribe(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
console.log(`[WebSocket] Отписались от события: ${event}`);
}
}
}
// Отправка сообщения на сервер
send(event, data) {
if (this.ws && this.isConnected) {

View File

@@ -827,14 +827,42 @@
<!-- Кнопка деплоя смарт-контрактов -->
<div class="deploy-section">
<!-- Информация о поэтапном деплое -->
<div class="deployment-info">
<h4>🚀 Поэтапный деплой DLE</h4>
<p class="deployment-description">
Автоматический деплой DLE контракта и всех модулей с проверками, верификацией и инициализацией во всех выбранных сетях
</p>
<div class="deployment-features">
<div class="feature-item">
<i class="fas fa-check-circle"></i>
<span>Деплой DLE контракта во всех сетях</span>
</div>
<div class="feature-item">
<i class="fas fa-check-circle"></i>
<span>Автоматическая верификация контрактов</span>
</div>
<div class="feature-item">
<i class="fas fa-check-circle"></i>
<span>Деплой и инициализация всех модулей</span>
</div>
<div class="feature-item">
<i class="fas fa-check-circle"></i>
<span>Повторы при ошибках сети</span>
</div>
</div>
</div>
<div class="deploy-buttons">
<button
@click="deploySmartContracts"
type="button"
class="btn btn-primary btn-lg deploy-btn"
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
>
<i class="fas fa-rocket"></i> Деплой смарт контрактов
<i class="fas fa-cogs"></i>
Поэтапный деплой DLE
</button>
<button
v-if="hasSelectedData"
@@ -893,6 +921,19 @@
</div>
</div>
</div>
<!-- Мастер поэтапного деплоя -->
<div v-if="showDeploymentWizard" class="deployment-wizard-overlay">
<div class="wizard-container">
<DeploymentWizard
:private-key="unifiedPrivateKey"
:selected-networks="selectedNetworks"
:dle-data="dleSettings"
:etherscan-api-key="etherscanApiKey"
@deployment-completed="handleDeploymentCompleted"
/>
</div>
</div>
</div>
</template>
@@ -900,10 +941,21 @@
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import axios from 'axios';
import api from '@/api/axios';
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
const router = useRouter();
// Нормализация приватного ключа: убираем пробелы/"0x", посторонние символы,
// приводим к нижнему регистру и дополняем ведущими нулями до 64 символов
function normalizePrivateKey(raw) {
if (!raw || typeof raw !== 'string') return '';
let pk = raw.trim().replace(/^0x/i, '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
if (pk.length === 64) return '0x' + pk;
if (pk.length > 64) return '';
if (/^[0-9a-fA-F]*$/.test(pk)) return '0x' + pk.padStart(64, '0');
return '';
}
// Получаем контекст авторизации для адреса кошелька
const { address, isAdmin } = useAuthContext();
@@ -995,6 +1047,10 @@ const autoVerifyAfterDeploy = ref(true);
// Состояние для приватных ключей
const useSameKeyForAllChains = ref(true);
const unifiedPrivateKey = ref('');
// Состояние мастера деплоя
const showDeploymentWizard = ref(false);
const deployedDLEAddress = ref('');
const privateKeys = reactive({});
const privateKeyVisibility = reactive({});
const keyValidation = reactive({});
@@ -1060,7 +1116,6 @@ const hasSelectedNetworks = computed(() => {
// symbol: dleSettings.tokenSymbol,
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
// };
// const resp = await axios.post('/dle-v2/predict-addresses', payload);
// if (resp.data && resp.data.success && resp.data.data) {
// // ожидаем вид { [chainId]: address }
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
@@ -1618,7 +1673,7 @@ const searchByPostalCode = async () => {
}
// console.log(`[SearchByPostalCode] Querying Nominatim: ${params.toString()}`);
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
// Преобразуем результаты Nominatim для отображения
@@ -1757,7 +1812,7 @@ const verifyAddress = async () => {
params.append('countrycodes', 'RU');
}
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
const verificationResult = response.data[0];
@@ -1833,7 +1888,7 @@ const formatTokenSymbol = () => {
const loadCountries = async () => {
isLoadingCountries.value = true;
try {
const response = await axios.get('/countries');
const response = await api.get('/countries');
if (response.data && response.data.success) {
countriesOptions.value = response.data.data || [];
console.log(`Загружено стран: ${countriesOptions.value.length}`);
@@ -1857,7 +1912,7 @@ const loadRussianClassifiers = async () => {
console.log('Загружаем российские классификаторы...');
// Загружаем все классификаторы одним запросом для оптимизации
const response = await axios.get('/russian-classifiers/all');
const response = await api.get('/russian-classifiers/all');
if (response.data && response.data.success) {
const data = response.data.data;
@@ -1905,7 +1960,7 @@ const loadKppCodes = async () => {
try {
console.log('Загружаем КПП коды...');
const response = await axios.get('/kpp/codes');
const response = await api.get('/kpp/codes');
if (response.data && Array.isArray(response.data.codes)) {
kppCodes.value = response.data.codes;
@@ -1928,65 +1983,19 @@ const loadAvailableNetworks = async () => {
try {
console.log('Загружаем доступные сети из базы данных...');
const response = await axios.get('/settings/rpc');
console.log('URL:', '/api/settings/rpc');
const response = await api.get('/settings/rpc');
console.log('Response:', response.data);
if (response.data && response.data.success) {
const networksData = response.data.data || [];
// Преобразуем данные из базы в формат для мульти-чейн деплоя
availableNetworks.value = networksData.map(network => {
// Определяем примерную стоимость на основе chain_id
const estimatedCosts = {
1: 45.50, // Ethereum Mainnet
137: 0.01, // Polygon
42161: 2.30, // Arbitrum One
10: 1.20, // Optimism
56: 0.50, // BSC
43114: 0.15, // Avalanche
11155111: 0.001, // Sepolia testnet
80001: 0.001, // Mumbai testnet
421613: 0.001, // Arbitrum Goerli
420: 0.001, // Optimism Goerli
97: 0.001, // BSC Testnet
43113: 0.001 // Avalanche Fuji
};
// Определяем описания сетей
const networkDescriptions = {
1: 'Максимальная безопасность и децентрализация',
137: 'Низкие комиссии, быстрые транзакции',
42161: 'Оптимистичные rollups, средние комиссии',
10: 'Оптимистичные rollups, низкие комиссии',
56: 'Совместимость с экосистемой Binance',
43114: 'Высокая пропускная способность',
11155111: 'Тестовая сеть Ethereum',
80001: 'Тестовая сеть Polygon',
421613: 'Тестовая сеть Arbitrum',
420: 'Тестовая сеть Optimism',
97: 'Тестовая сеть BSC',
43113: 'Тестовая сеть Avalanche'
};
// Определяем названия сетей
const networkNames = {
1: 'Ethereum Mainnet',
137: 'Polygon',
42161: 'Arbitrum One',
10: 'Optimism',
56: 'BSC',
43114: 'Avalanche',
11155111: 'Sepolia Testnet',
80001: 'Mumbai Testnet',
421613: 'Arbitrum Goerli',
420: 'Optimism Goerli',
97: 'BSC Testnet',
43113: 'Avalanche Fuji'
};
const chainId = network.chain_id || parseInt(network.network_id);
const estimatedCost = estimatedCosts[chainId] || 1.00;
const description = networkDescriptions[chainId] || 'Блокчейн сеть';
const name = networkNames[chainId] || network.network_id || 'Unknown Network';
const chainId = network.chain_id || parseInt(network.network_id);
const estimatedCost = getFallbackCost(chainId);
const description = network.description || 'Блокчейн сеть';
const name = network.name || network.network_id || `Chain ${chainId}`;
return {
chainId: chainId,
@@ -2042,7 +2051,7 @@ const validateTokenStandardCompatibility = () => {
// Проверяем совместимость ERC-4626 с тестовыми сетями
if (standard === 'ERC4626') {
const testnetChains = [11155111, 80001, 421613, 420, 97, 43113]; // Sepolia, Mumbai, etc.
const testnetChains = [11155111, 80001, 421613, 420, 97]; // Sepolia, Mumbai, etc.
const hasTestnet = networks.some(network => testnetChains.includes(network.chainId));
if (hasTestnet) {
@@ -2075,12 +2084,80 @@ const showTokenStandardWarnings = () => {
// ==================== МУЛЬТИ-ЧЕЙН ФУНКЦИИ ====================
// Обновление общей стоимости деплоя
const updateDeployCost = () => {
totalDeployCost.value = selectedNetworkDetails.value
.reduce((sum, network) => sum + network.estimatedCost, 0);
// Обновление общей стоимости деплоя (динамический расчет)
const updateDeployCost = async () => {
if (selectedNetworkDetails.value.length === 0) {
totalDeployCost.value = 0;
return;
}
try {
// Получаем chainId выбранных сетей
const chainIds = selectedNetworkDetails.value.map(network => network.chainId);
// Вызываем API для расчета стоимости
const response = await api.post('/dle-v2/estimate-cost', {
supportedChainIds: chainIds
});
if (response.data.success && response.data.data) {
const costData = response.data.data;
// Обновляем информацию о каждой сети
selectedNetworkDetails.value.forEach(network => {
const estimate = costData.estimates.find(e => e.chainId === network.chainId);
if (estimate && estimate.ok) {
network.estimatedCost = parseFloat(estimate.costEth);
network.gasPrice = estimate.gasPrice;
network.estimatedGas = estimate.gasLimit;
} else {
// Fallback для сетей без RPC
network.estimatedCost = getFallbackCost(network.chainId);
}
});
totalDeployCost.value = parseFloat(costData.totalCostEth);
console.log('✅ Стоимость деплоя обновлена:', costData);
} else {
throw new Error('Ошибка получения стоимости деплоя');
}
} catch (error) {
console.warn('⚠️ Ошибка расчета стоимости, используем fallback:', error.message);
// Fallback к статическим ценам
selectedNetworkDetails.value.forEach(network => {
network.estimatedCost = getFallbackCost(network.chainId);
});
totalDeployCost.value = selectedNetworkDetails.value
.reduce((sum, network) => sum + network.estimatedCost, 0);
}
};
// Вспомогательная функция для получения fallback стоимости
const getFallbackCost = (chainId) => {
const fallbackCosts = {
1: 45.50, // Ethereum Mainnet
137: 0.01, // Polygon
42161: 2.30, // Arbitrum One
10: 1.20, // Optimism
56: 0.50, // BSC
43114: 0.15, // Avalanche
11155111: 0.001, // Sepolia testnet
80001: 0.001, // Mumbai testnet
421613: 0.001, // Arbitrum Goerli
420: 0.001, // Optimism Goerli
97: 0.001, // BSC Testnet
17000: 0.001, // Holesky testnet
421614: 0.001, // Arbitrum Sepolia
84532: 0.001, // Base Sepolia
80002: 0.001 // Polygon Amoy
};
return fallbackCosts[chainId] || 1.00;
};
// Копирование адреса DLE - отключено
// const copyAddress = async () => {
// try {
@@ -2152,7 +2229,7 @@ const validatePrivateKey = async (chainId) => {
try {
// Отправляем запрос на бэкенд для валидации
const response = await axios.post('/dle-v2/validate-private-key', {
const response = await api.post('/dle-v2/validate-private-key', {
privateKey: key
});
@@ -2275,12 +2352,14 @@ const handleVisibilityChange = () => {
}
};
// Watcher для unifiedPrivateKey с дебаунсом
// Watcher: нормализуем PK и обновляем связанные состояния
watch(unifiedPrivateKey, (newValue) => {
// Добавляем небольшую задержку для предотвращения рекурсии
setTimeout(() => {
updateAllKeys();
}, 100);
const normalized = normalizePrivateKey(newValue);
if (normalized && normalized !== newValue) {
unifiedPrivateKey.value = normalized;
return;
}
updateAllKeys();
});
// Watcher для predictedAddress - синхронизация с dleSettings - отключено
@@ -2309,6 +2388,11 @@ watch(unifiedPrivateKey, (newValue) => {
// Инициализация
onMounted(() => {
// Сбрасываем состояние деплоя при загрузке страницы
showDeployProgress.value = false;
deployProgress.value = 0;
deployStatus.value = '';
// Загружаем список стран
loadCountries();
@@ -2337,6 +2421,11 @@ onMounted(() => {
}
}
// Проверяем, есть ли приватный ключ
if (!unifiedPrivateKey.value) {
console.log('⚠️ Приватный ключ не введен. Пожалуйста, введите приватный ключ для деплоя.');
}
// Добавляем слушатель события видимости страницы для обновления списка сетей
document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -2367,23 +2456,22 @@ const checkAdminTokens = async () => {
return;
}
adminTokenCheck.value.isLoading = true;
adminTokenCheck.value.error = null;
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: true, error: null };
try {
const response = await axios.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
const response = await api.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
if (response.data.success) {
adminTokenCheck.value.isAdmin = response.data.data.isAdmin;
adminTokenCheck.value = { ...adminTokenCheck.value, isAdmin: response.data.data.isAdmin };
console.log('Проверка админских токенов:', response.data.data);
} else {
adminTokenCheck.value.error = response.data.message || 'Ошибка проверки токенов';
adminTokenCheck.value = { ...adminTokenCheck.value, error: response.data.message || 'Ошибка проверки токенов' };
}
} catch (error) {
console.error('Ошибка проверки админских токенов:', error);
adminTokenCheck.value.error = error.response?.data?.message || 'Ошибка проверки токенов';
adminTokenCheck.value = { ...adminTokenCheck.value, error: error.response?.data?.message || 'Ошибка проверки токенов' };
} finally {
adminTokenCheck.value.isLoading = false;
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: false };
}
};
@@ -2429,7 +2517,7 @@ const maskedPrivateKey = computed(() => {
// Функция деплоя смарт-контрактов DLE
const deploySmartContracts = async () => {
console.log('🚀 Начало деплоя DLE...');
console.log('🚀 Начало поэтапного деплоя DLE...');
try {
// Валидация данных
if (!isFormValid.value) {
@@ -2437,12 +2525,33 @@ const deploySmartContracts = async () => {
return;
}
// Сразу показываем мастер деплоя
showDeploymentWizard.value = true;
// Запускаем деплой DLE в фоне
startStagedDeployment();
} catch (error) {
console.error('Ошибка деплоя DLE:', error);
showDeployProgress.value = false;
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
}
};
// Функция запуска поэтапного деплоя
const startStagedDeployment = async () => {
console.log('🚀 Запуск поэтапного деплоя...');
// Сначала выполняем стандартный деплой DLE контракта
try {
// Показываем индикатор процесса
showDeployProgress.value = true;
deployProgress.value = 10;
deployStatus.value = 'Подготовка данных для деплоя...';
deployStatus.value = 'Подготовка данных для деплоя DLE...';
// Подготовка данных для деплоя
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
console.log('DEBUG: selectedNetworks.value:', selectedNetworks.value);
const deployData = {
// Основная информация DLE
name: dleSettings.name,
@@ -2463,16 +2572,15 @@ const deploySmartContracts = async () => {
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
// Мульти-чейн настройки
supportedChainIds: dleSettings.selectedNetworks || [],
supportedChainIds: selectedNetworks.value || [],
// Текущая цепочка (будет установлена при деплое)
currentChainId: dleSettings.selectedNetworks[0] || 1,
currentChainId: selectedNetworks.value[0] || 1,
// Приватный ключ для деплоя
privateKey: unifiedPrivateKey.value,
// Верификация через Etherscan V2
etherscanApiKey: etherscanApiKey.value,
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя
};
// Обработка логотипа
@@ -2480,7 +2588,7 @@ const deploySmartContracts = async () => {
if (logoFile.value) {
const form = new FormData();
form.append('logo', logoFile.value);
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
if (uploaded) {
deployData.logoURI = uploaded;
@@ -2488,162 +2596,113 @@ const deploySmartContracts = async () => {
} else if (ensResolvedUrl.value) {
deployData.logoURI = ensResolvedUrl.value;
} else {
// фолбэк на дефолт
deployData.logoURI = '/uploads/logos/default-token.svg';
}
} catch (error) {
console.warn('Ошибка при обработке логотипа:', error.message);
// Используем fallback логотип
deployData.logoURI = '/uploads/logos/default-token.svg';
}
console.log('Данные для деплоя DLE:', deployData);
// Предварительная проверка балансов во всех сетях
// Предварительная проверка балансов (через приватный ключ)
deployProgress.value = 20;
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
try {
const pre = await axios.post('/dle-v2/precheck', {
const pre = await api.post('/dle-v2/precheck', {
supportedChainIds: deployData.supportedChainIds,
privateKey: deployData.privateKey
privateKey: unifiedPrivateKey.value
});
const preData = pre.data?.data;
if (pre.data?.success && preData) {
const lacks = (preData.insufficient || []);
const warnings = (preData.warnings || []);
if (lacks.length > 0) {
const lines = (preData.balances || []).map(b => {
const status = b.ok ? '✅' : '❌';
const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
});
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
if (lacks.length > 0) {
alert(message);
showDeployProgress.value = false;
return;
} else if (warnings.length > 0) {
const proceed = confirm(message + '\n\nПродолжить деплой?');
if (!proceed) {
showDeployProgress.value = false;
return;
}
}
const message = `❌ Недостаточно средств в некоторых сетях!`;
alert(message);
showDeployProgress.value = false;
return;
}
console.log('✅ Проверка балансов пройдена:', preData.summary);
}
} catch (e) {
console.warn('⚠️ Ошибка проверки балансов:', e.message);
// Если precheck недоступен, не блокируем — продолжаем
}
deployProgress.value = 30;
deployStatus.value = 'Компиляция смарт-контрактов...';
// Автокомпиляция контрактов перед деплоем
console.log('🔨 Запуск автокомпиляции...');
// Автокомпиляция контрактов
try {
const compileResponse = await axios.post('/compile-contracts');
const compileResponse = await api.post('/compile-contracts');
console.log('✅ Контракты скомпилированы:', compileResponse.data);
} catch (compileError) {
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
// Продолжаем деплой даже если компиляция не удалась
}
deployProgress.value = 40;
deployStatus.value = 'Отправка данных на сервер...';
deployStatus.value = 'Деплой DLE контракта...';
// Вызов API для деплоя
deployProgress.value = 50;
deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
const response = await axios.post('/dle-v2', deployData);
// Деплой будет выполнен в DeploymentWizard
// Здесь только показываем мастер деплоя
deployProgress.value = 80;
deployStatus.value = 'Проверка результатов деплоя...';
deployStatus.value = 'Запуск мастера деплоя...';
if (response.data.success) {
const result = response.data.data;
// Проверяем результаты мульти-чейн деплоя
if (result.networks && Array.isArray(result.networks)) {
const successfulNetworks = result.networks.filter(n => n.success);
const failedNetworks = result.networks.filter(n => !n.success);
if (failedNetworks.length > 0) {
console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
}
if (successfulNetworks.length > 0) {
// Проверяем, что все адреса одинаковые
const addresses = successfulNetworks.map(n => n.address);
const uniqueAddresses = [...new Set(addresses)];
if (uniqueAddresses.length === 1) {
deployProgress.value = 100;
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
console.log('🎉 Мульти-чейн деплой завершен успешно!');
console.log('Адрес DLE:', uniqueAddresses[0]);
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
// Небольшая задержка для показа успешного завершения
setTimeout(() => {
showDeployProgress.value = false;
// Перенаправляем на главную страницу управления
router.push('/management');
}, 3000);
} else {
showDeployProgress.value = false;
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
}
} else {
showDeployProgress.value = false;
alert('❌ Не удалось развернуть DLE ни в одной сети');
}
} else {
// Fallback для одиночного деплоя
deployProgress.value = 100;
deployStatus.value = '✅ DLE успешно развернут!';
setTimeout(() => {
showDeployProgress.value = false;
router.push('/management');
}, 2000);
}
} else {
showDeployProgress.value = false;
alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
}
// Показываем мастер деплоя
showDeploymentWizard.value = true;
// Мастер деплоя сам выполнит деплой
return;
} catch (error) {
console.error('Ошибка деплоя DLE:', error);
showDeployProgress.value = false;
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
console.error('Ошибка при запуске деплоя:', error);
deployStatus.value = `❌ Ошибка: ${error.message}`;
deployProgress.value = 0;
}
}
// Обработчик завершения поэтапного деплоя
const handleDeploymentCompleted = (result) => {
console.log('🎉 Поэтапный деплой завершен:', result);
showDeploymentWizard.value = false;
// Перенаправляем на главную страницу управления
router.push('/management');
};
// Валидация формы
const isFormValid = computed(() => {
const isFormValid = computed(() => {
const validation = {
jurisdiction: !!dleSettings.jurisdiction,
name: !!dleSettings.name,
tokenSymbol: !!dleSettings.tokenSymbol,
partners: dleSettings.partners.length > 0,
partnersValid: dleSettings.partners.every(partner => partner.address && partner.amount > 0),
quorum: dleSettings.governanceQuorum > 0 && dleSettings.governanceQuorum <= 100,
networks: selectedNetworks.value.length > 0,
privateKey: !!unifiedPrivateKey.value,
keyValid: !!keyValidation.unified?.isValid,
coordinates: validateCoordinates(dleSettings.coordinates)
};
console.log('🔍 Валидация формы:', validation);
console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
console.log('🔍 showDeployProgress:', showDeployProgress.value);
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
console.log('🔍 keyValidation.unified:', keyValidation.unified);
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading && !showDeployProgress.value);
return Boolean(
dleSettings.jurisdiction &&
dleSettings.name &&
dleSettings.tokenSymbol &&
(dleSettings.partners.length > 0) &&
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
dleSettings.governanceQuorum > 0 &&
dleSettings.governanceQuorum <= 100 &&
(dleSettings.selectedNetworks.length > 0) &&
// Проверка приватного ключа
unifiedPrivateKey.value &&
keyValidation.unified?.isValid &&
// Валидация координат
validateCoordinates(dleSettings.coordinates)
validation.jurisdiction &&
validation.name &&
validation.tokenSymbol &&
validation.partners &&
validation.partnersValid &&
validation.quorum &&
validation.networks &&
validation.privateKey &&
validation.keyValid &&
validation.coordinates
);
});
@@ -2715,7 +2774,7 @@ async function submitDeploy() {
if (logoFile.value) {
const form = new FormData();
form.append('logo', logoFile.value);
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
if (uploaded) {
deployData.logoURI = uploaded;
@@ -4385,6 +4444,85 @@ async function submitDeploy() {
border-top: 1px solid #e9ecef;
}
/* Стили для информации о деплое */
.deployment-info {
margin-bottom: 2rem;
width: 100%;
max-width: 800px;
padding: 2rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 16px;
border: 1px solid #dee2e6;
}
.deployment-info h4 {
color: #2c3e50;
margin-bottom: 1rem;
text-align: center;
font-size: 1.4rem;
font-weight: 600;
}
.deployment-description {
color: #6c757d;
text-align: center;
margin-bottom: 1.5rem;
font-size: 1rem;
line-height: 1.5;
}
.deployment-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.feature-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background-color: white;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.feature-item i {
color: #28a745;
font-size: 1.1rem;
}
.feature-item span {
color: #495057;
font-size: 0.9rem;
font-weight: 500;
}
/* Стили для мастера деплоя */
.deployment-wizard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.wizard-container {
background-color: white;
border-radius: 16px;
max-width: 1200px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.deploy-buttons {
display: flex;
gap: 1rem;

View File

@@ -70,7 +70,7 @@
>
<div class="proposal-header">
<h5>{{ proposal.description || 'Без описания' }}</h5>
<h5>{{ getProposalTitle(proposal) }}</h5>
<span class="proposal-status" :class="proposal.status">
{{ getProposalStatusText(proposal.status) }}
</span>
@@ -92,6 +92,27 @@
<div class="detail-item">
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
</div>
<!-- Детальная информация о модуле -->
<div v-if="proposal.decodedData" class="module-details">
<div class="detail-item">
<strong>Тип модуля:</strong> {{ getModuleName(proposal.decodedData.moduleId) }}
</div>
<div class="detail-item">
<strong>Адрес модуля:</strong>
<a :href="getEtherscanUrl(proposal.decodedData.moduleAddress, proposal.decodedData.chainId)"
target="_blank" class="address-link">
{{ shortenAddress(proposal.decodedData.moduleAddress) }}
</a>
</div>
<div class="detail-item">
<strong>Сеть:</strong> {{ getChainName(proposal.decodedData.chainId) }}
</div>
<div class="detail-item">
<strong>Длительность:</strong> {{ formatDuration(proposal.decodedData.duration) }}
</div>
</div>
<div class="detail-item">
<strong>Голоса:</strong>
<div class="votes-container">
@@ -100,7 +121,7 @@
<span class="against">Против: {{ formatVotes(proposal.againstVotes) }}</span>
</div>
<div class="quorum-info">
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum() }}%</span>
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}%</span>
</div>
<div class="quorum-progress">
<div class="progress-bar">
@@ -140,13 +161,21 @@
<i class="fas fa-times"></i> Против
</button>
<button
v-if="canExecute(proposal) && props.isAuthenticated && hasAdminRights()"
v-if="canExecute(proposal) && props.isAuthenticated"
class="btn btn-sm btn-primary"
@click="executeProposalLocal(proposal.id)"
>
<i class="fas fa-play"></i> Исполнить
</button>
<!-- Информация для не-инициаторов -->
<div v-else-if="proposal.state === 5 && !proposal.executed && props.isAuthenticated" class="execution-notice">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Только инициатор предложения может его исполнить
</small>
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice">
<small class="text-muted">
@@ -533,7 +562,7 @@ import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI } from '../../services/proposalsService.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import api from '../../api/axios';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
@@ -543,6 +572,306 @@ const showTargetChains = computed(() => {
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
// Best Practice: WebSocket-based подписка на обновления голосования
function subscribeToVoteUpdates(txHash, proposalId, actionType) {
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для:', { txHash, proposalId, actionType });
// Создаем уникальный обработчик для этой транзакции
const voteHandler = (data) => {
console.log('[DleProposalsView] Получено WebSocket уведомление о голосовании:', data);
// Проверяем, что это наша транзакция
if (data.txHash === txHash || data.proposalId === proposalId) {
console.log('[DleProposalsView] Найдено совпадение транзакции, обновляем данные');
// Обновляем данные
loadDleData().then(() => {
// Показываем успешное уведомление
showSuccessNotification(txHash, actionType);
});
// Отписываемся от уведомлений
wsClient.off('proposal_voted', voteHandler);
}
};
// Подписываемся на уведомления о голосовании
wsClient.on('proposal_voted', voteHandler);
// Устанавливаем таймаут на случай, если WebSocket не сработает
setTimeout(() => {
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений, отписываемся');
wsClient.off('proposal_voted', voteHandler);
// Fallback: обновляем данные в любом случае
loadDleData().then(() => {
showTimeoutNotification(txHash, actionType);
});
}, 60000); // 60 секунд таймаут
}
// WebSocket-based подписка на обновления исполнения
function subscribeToExecutionUpdates(txHash, proposalId) {
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для исполнения:', { txHash, proposalId });
// Создаем уникальный обработчик для этой транзакции
const executionHandler = (data) => {
console.log('[DleProposalsView] Получено WebSocket уведомление об исполнении:', data);
// Проверяем, что это наша транзакция
if (data.txHash === txHash || data.proposalId === proposalId) {
console.log('[DleProposalsView] Найдено совпадение транзакции исполнения, обновляем данные');
// Обновляем данные
loadDleData().then(() => {
// Показываем успешное уведомление
showSuccessNotification(txHash, 'execution');
});
// Отписываемся от уведомлений
wsClient.off('proposal_executed', executionHandler);
}
};
// Подписываемся на уведомления об исполнении
wsClient.on('proposal_executed', executionHandler);
// Устанавливаем таймаут на случай, если WebSocket не сработает
setTimeout(() => {
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений об исполнении, отписываемся');
wsClient.off('proposal_executed', executionHandler);
// Fallback: обновляем данные в любом случае
loadDleData().then(() => {
showTimeoutNotification(txHash, 'execution');
});
}, 60000); // 60 секунд таймаут
}
// Функция для отслеживания транзакции исполнения на backend
async function trackExecutionTransaction(txHash, dleAddress, proposalId) {
try {
console.log('[DleProposalsView] Запускаем отслеживание транзакции исполнения на backend:', { txHash, dleAddress, proposalId });
const response = await api.post('/dle-proposals/track-execution-transaction', {
txHash: txHash,
dleAddress: dleAddress,
proposalId: proposalId
});
if (response.data.success) {
console.log('[DleProposalsView] Backend подтвердил транзакцию исполнения:', response.data);
} else {
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию исполнения:', response.data.error);
}
} catch (error) {
console.error('[DleProposalsView] Ошибка при отслеживании транзакции исполнения на backend:', error);
}
}
// Функция для отслеживания транзакции голосования на backend
async function trackVoteTransaction(txHash, dleAddress, proposalId, support) {
try {
console.log('[DleProposalsView] Запускаем отслеживание транзакции на backend:', { txHash, dleAddress, proposalId, support });
const response = await api.post('/dle-proposals/track-vote-transaction', {
txHash: txHash,
dleAddress: dleAddress,
proposalId: proposalId,
support: support
});
if (response.data.success) {
console.log('[DleProposalsView] Backend подтвердил транзакцию:', response.data);
} else {
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию:', response.data.error);
}
} catch (error) {
console.error('[DleProposalsView] Ошибка при отслеживании транзакции на backend:', error);
}
}
// Показ уведомления о транзакции
function showTransactionNotification(txHash, message) {
// Создаем уведомление с ссылкой на Etherscan
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
// Можно использовать toast-библиотеку или создать кастомное уведомление
const notification = document.createElement('div');
notification.className = 'transaction-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">⏳</span>
<span class="notification-title">${message}</span>
</div>
<div class="notification-body">
<p>Ожидаем подтверждения транзакции...</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
// Добавляем стили
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
// Автоматически удаляем через 10 секунд
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 10000);
}
// Показ успешного уведомления
function showSuccessNotification(txHash, actionType) {
const actionText = actionType === 'vote' ? 'Голосование подтверждено!' : 'Голосование "против" подтверждено!';
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'success-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">✅</span>
<span class="notification-title">${actionText}</span>
</div>
<div class="notification-body">
<p>Данные обновлены</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
// Показ уведомления об ошибке
function showErrorNotification(txHash, message) {
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'error-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">❌</span>
<span class="notification-title">${message}</span>
</div>
<div class="notification-body">
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 8000);
}
// Показ уведомления о таймауте
function showTimeoutNotification(txHash, actionType) {
const actionText = actionType === 'vote' ? 'Голосование' : 'Голосование "против"';
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'timeout-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">⏰</span>
<span class="notification-title">${actionText} отправлено</span>
</div>
<div class="notification-body">
<p>Подтверждение не получено, но данные обновлены</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 8000);
}
const props = defineProps({
dleAddress: { type: String, required: false, default: null },
dleContract: { type: Object, required: false, default: null },
@@ -670,15 +999,30 @@ async function loadDleData() {
console.log('[Frontend] Массив предложений:', proposalsData);
// Преобразуем данные из API в формат для frontend
proposals.value = proposalsData.map(proposal => {
proposals.value = await Promise.all(proposalsData.map(async (proposal) => {
const transformedProposal = {
...proposal,
status: getProposalStatus(proposal),
deadline: proposal.deadline || 0
};
// Если есть transactionHash, декодируем данные предложения
if (proposal.transactionHash) {
try {
console.log('[Frontend] Декодируем данные предложения:', proposal.transactionHash);
const decodedData = await decodeProposalData(proposal.transactionHash);
if (decodedData.success) {
transformedProposal.decodedData = decodedData.data;
console.log('[Frontend] Декодированные данные:', decodedData.data);
}
} catch (error) {
console.error('[Frontend] Ошибка декодирования данных предложения:', error);
}
}
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
return transformedProposal;
});
}));
console.log('[Frontend] Итоговый список предложений:', proposals.value);
@@ -819,8 +1163,13 @@ function getProposalStatus(proposal) {
const forVotes = Number(proposal.forVotes) || 0;
const againstVotes = Number(proposal.againstVotes) || 0;
// Если есть голоса, определяем результат
if (forVotes > 0 || againstVotes > 0) {
// Проверяем, достигнут ли кворум
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum(proposal);
const quorumReached = quorumPercentage >= requiredQuorum;
// Если есть голоса И кворум достигнут, определяем результат
if ((forVotes > 0 || againstVotes > 0) && quorumReached) {
if (forVotes > againstVotes) {
return 'succeeded';
} else if (againstVotes > forVotes) {
@@ -828,6 +1177,7 @@ function getProposalStatus(proposal) {
}
}
// Если кворум не достигнут или нет голосов, предложение активно
return 'active';
}
@@ -843,6 +1193,61 @@ function getProposalStatusText(status) {
return statusMap[status] || status;
}
function getProposalTitle(proposal) {
// Если есть декодированные данные, показываем детальную информацию
if (proposal.decodedData) {
const { moduleId, moduleAddress, chainId, duration } = proposal.decodedData;
// Декодируем moduleId из hex в строку
let moduleName = 'Неизвестный модуль';
try {
moduleName = ethers.toUtf8String(moduleId).replace(/\0/g, '');
} catch (e) {
console.log('Не удалось декодировать moduleId:', moduleId);
}
return `Добавить модуль: ${moduleName}`;
}
// Иначе показываем обычное описание
return proposal.description || 'Без описания';
}
function getModuleName(moduleId) {
try {
return ethers.toUtf8String(moduleId).replace(/\0/g, '');
} catch (e) {
return 'Неизвестный модуль';
}
}
function getEtherscanUrl(address, chainId) {
const chainMap = {
1: 'https://etherscan.io',
11155111: 'https://sepolia.etherscan.io',
17000: 'https://holesky.etherscan.io',
421614: 'https://sepolia.arbiscan.io',
84532: 'https://sepolia.basescan.org'
};
const baseUrl = chainMap[chainId] || 'https://etherscan.io';
return `${baseUrl}/address/${address}`;
}
function formatDuration(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days} дн. ${hours} ч.`;
} else if (hours > 0) {
return `${hours} ч. ${minutes} мин.`;
} else {
return `${minutes} мин.`;
}
}
function getProposalStatusClass(status) {
const classMap = {
'pending': 'status-pending',
@@ -901,16 +1306,25 @@ function getQuorumPercentage(proposal) {
function getQuorumProgress(proposal) {
const percentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const requiredQuorum = getRequiredQuorum(proposal);
const progress = Math.min((percentage / requiredQuorum) * 100, 100);
console.log('[Quorum] Прогресс кворума:', { percentage, requiredQuorum, progress });
return progress;
}
function getRequiredQuorum() {
function getRequiredQuorum(proposal = null) {
// Если есть данные о предложении с quorumRequired, используем их
if (proposal && proposal.quorumRequired && selectedDle.value?.totalSupply) {
const totalSupplyWei = parseFloat(selectedDle.value.totalSupply) * Math.pow(10, 18);
const quorumPercentage = (proposal.quorumRequired / totalSupplyWei) * 100;
console.log('[Quorum] Требуемый кворум из предложения:', quorumPercentage, 'quorumRequired:', proposal.quorumRequired, 'totalSupply:', totalSupplyWei);
return Math.round(quorumPercentage * 100) / 100;
}
// Fallback к данным DLE
const quorum = selectedDle.value?.quorumPercentage || 51;
console.log('[Quorum] Требуемый кворум из DLE:', quorum, 'DLE данные:', selectedDle.value);
return quorum; // По умолчанию 51% если данные не загружены
return quorum;
}
function formatVotes(votes) {
@@ -974,15 +1388,17 @@ function canExecute(proposal) {
const now = Math.floor(Date.now() / 1000);
const deadline = proposal.deadline || 0;
// Предложение можно выполнить только если:
// 1. Дедлайн истек
// 2. Кворум достигнут
// 3. Предложение еще не выполнено
// Предложение можно выполнить если:
// 1. Кворум достигнут ИЛИ предложение уже принято (state: 5)
// 2. Предложение еще не выполнено
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const requiredQuorum = getRequiredQuorum(proposal);
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
const deadlinePassed = deadline > 0 && now >= deadline;
// Если предложение уже принято (state: 5), можно исполнять
const isProposalPassed = proposal.state === 5 || proposal.isPassed === true;
// Добавляем отладочную информацию
console.log('[canExecute] Проверка предложения:', {
proposalId: proposal.id,
@@ -992,10 +1408,29 @@ function canExecute(proposal) {
deadline,
now,
deadlinePassed,
executed: proposal.executed
executed: proposal.executed,
state: proposal.state,
isPassed: proposal.isPassed,
isProposalPassed
});
return deadlinePassed && hasReachedQuorum && !proposal.executed;
// Проверяем, что текущий пользователь - инициатор предложения
const isInitiator = address.value && proposal.initiator &&
address.value.toLowerCase() === proposal.initiator.toLowerCase();
console.log('[canExecute] Проверка инициатора:', {
currentAddress: address.value,
proposalInitiator: proposal.initiator,
isInitiator
});
// Можно исполнять если:
// 1. (кворум достигнут И дедлайн истек) ИЛИ предложение уже принято
// 2. Пользователь - инициатор предложения
// 3. Предложение не выполнено
return ((hasReachedQuorum && deadlinePassed) || isProposalPassed) &&
isInitiator &&
!proposal.executed;
}
function hasSigned(proposalId) {
@@ -1191,10 +1626,107 @@ async function signProposalLocal(proposalId) {
console.log('[Debug] Попытка подписи для предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await voteForProposalAPI(dleAddress.value, proposalId, true); // Подпись = голос "за"
// Получаем данные транзакции от backend
const result = await voteForProposalAPI(dleAddress.value, proposalId, true);
await loadDleData();
alert('✅ Предложение подписано!');
if (result.success) {
console.log('[DleProposalsView] Данные транзакции голосования получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[DleProposalsView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию голосования:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция голосования отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Голосование отправлено!');
// Подписываемся на WebSocket уведомления о голосовании
subscribeToVoteUpdates(txHash, proposalId, 'vote');
// Запускаем отслеживание транзакции на backend
trackVoteTransaction(txHash, dleAddress.value, proposalId, true);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции голосования:', txError);
alert('❌ Ошибка отправки транзакции голосования: ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при подписании:', error);
@@ -1225,10 +1757,107 @@ async function cancelSignatureLocal(proposalId) {
console.log('[Debug] Попытка голосования "против" для предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await voteForProposalAPI(dleAddress.value, proposalId, false); // Голос "против"
// Получаем данные транзакции от backend
const result = await voteForProposalAPI(dleAddress.value, proposalId, false);
await loadDleData();
alert('✅ Ваш голос "против" учтен!');
if (result.success) {
console.log('[DleProposalsView] Данные транзакции голосования "против" получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[DleProposalsView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию голосования "против":', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция голосования "против" отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Голосование "против" отправлено!');
// Подписываемся на WebSocket уведомления о голосовании
subscribeToVoteUpdates(txHash, proposalId, 'vote-against');
// Запускаем отслеживание транзакции на backend
trackVoteTransaction(txHash, dleAddress.value, proposalId, false);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции голосования "против":', txError);
alert('❌ Ошибка отправки транзакции голосования "против": ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при голосовании "против":', error);
@@ -1243,23 +1872,131 @@ async function cancelSignatureLocal(proposalId) {
// Исполнение предложения
async function executeProposalLocal(proposalId) {
// Проверка прав админа для исполнения
// Проверка авторизации
if (!props.isAuthenticated) {
alert('❌ Для исполнения предложений необходимо авторизоваться в приложении');
return;
}
// Дополнительная проверка на права админа
if (!hasAdminRights()) {
alert('❌ Для исполнения предложений необходимы права администратора');
// Проверка, что пользователь - инициатор предложения
const proposal = proposals.value.find(p => p.id === proposalId);
if (!proposal) {
alert('❌ Предложение не найдено');
return;
}
const isInitiator = address.value && proposal.initiator &&
address.value.toLowerCase() === proposal.initiator.toLowerCase();
if (!isInitiator) {
alert('❌ Только инициатор предложения может его исполнить');
return;
}
try {
await executeProposalAPI(dleAddress.value, proposalId);
console.log('[Debug] Попытка исполнения предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await loadDleData();
alert('✅ Предложение успешно исполнено!');
// Получаем данные транзакции от backend
const result = await executeProposalAPI(dleAddress.value, proposalId);
if (result.success) {
console.log('[DleProposalsView] Данные транзакции исполнения получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем подключение к MetaMask
if (!window.ethereum) {
throw new Error('MetaMask не установлен');
}
// Запрашиваем разрешение на подключение к MetaMask
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (accounts.length === 0) {
throw new Error('Нет подключенных аккаунтов в MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем сеть
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia (11155111)
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Неправильная сеть! Текущая: ${chainId}, ожидается: ${expectedChainId}`);
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
} catch (switchError) {
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию исполнения:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция исполнения отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Исполнение предложения отправлено!');
// Подписываемся на WebSocket уведомления о исполнении
subscribeToExecutionUpdates(txHash, proposalId);
// Запускаем отслеживание транзакции на backend
trackExecutionTransaction(txHash, dleAddress.value, proposalId);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции исполнения:', txError);
alert('❌ Ошибка отправки транзакции исполнения: ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при исполнении предложения:', error);
@@ -1764,6 +2501,14 @@ onUnmounted(() => {
color: #721c24;
}
.execution-notice {
margin-top: 8px;
padding: 8px 12px;
background: #e2e3e5;
border-radius: 4px;
border-left: 3px solid #6c757d;
}
.proposal-status.canceled {
background: #fff3cd;
color: #856404;
@@ -1778,6 +2523,28 @@ onUnmounted(() => {
font-size: 0.9rem;
}
.module-details {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
}
.module-details .detail-item {
margin-bottom: 0.75rem;
}
.address-link {
color: #007bff;
text-decoration: none;
font-family: monospace;
}
.address-link:hover {
text-decoration: underline;
}
.votes-container {
display: flex;
flex-direction: column;

View File

@@ -103,6 +103,29 @@
</div>
</div>
<!-- DLEReader -->
<div class="module-deploy-card">
<div class="module-content">
<h4>DLEReader</h4>
<p>Чтение данных DLE - API для получения информации о контракте и предложениях</p>
<div class="module-features">
<span class="feature-tag">API</span>
<span class="feature-tag">Чтение</span>
<span class="feature-tag">Данные</span>
<span class="feature-tag">Интеграция</span>
</div>
</div>
<div class="module-actions">
<button
class="btn btn-primary btn-deploy"
@click="router.push(`/management/modules/deploy/reader?address=${route.query.address}`)"
>
<i class="fas fa-rocket"></i>
Деплой
</button>
</div>
</div>
<!-- CommunicationModule -->
<div class="module-deploy-card">
<div class="module-content">
@@ -465,12 +488,44 @@
<div class="modules-list">
<div class="list-header">
<h3>📋 Модули DLE</h3>
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules">
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules }"></i> Обновить
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules || isLoadingDeploymentStatus">
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules || isLoadingDeploymentStatus }"></i> Обновить
</button>
</div>
<div v-if="isLoadingModules" class="loading-modules">
<!-- Статус деплоя -->
<div v-if="isLoadingDeploymentStatus" class="deployment-status">
<div class="status-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>Проверка статуса деплоя...</span>
</div>
</div>
<div v-else-if="!canShowModules" class="deployment-status">
<div class="status-message" :class="deploymentStatus">
<div class="status-icon">
<i v-if="deploymentStatus === 'completed'" class="fas fa-check-circle"></i>
<i v-else-if="deploymentStatus === 'in_progress'" class="fas fa-spinner fa-spin"></i>
<i v-else-if="deploymentStatus === 'failed'" class="fas fa-exclamation-triangle"></i>
<i v-else-if="deploymentStatus === 'not_started'" class="fas fa-play-circle"></i>
<i v-else class="fas fa-question-circle"></i>
</div>
<div class="status-content">
<h4>{{ deploymentStatusMessage }}</h4>
<p v-if="deploymentStatus === 'not_started'">
Для активации модулей необходимо запустить поэтапный деплой DLE.
</p>
<p v-else-if="deploymentStatus === 'failed'">
Проверьте логи деплоя и повторите попытку через форму деплоя.
</p>
<p v-else-if="deploymentStatus === 'in_progress'">
Дождитесь завершения деплоя. Модули станут доступны автоматически.
</p>
</div>
</div>
</div>
<div v-else-if="isLoadingModules" class="loading-modules">
<p>Загрузка модулей...</p>
</div>
@@ -479,7 +534,7 @@
<p>Используйте форму выше для добавления первого модуля</p>
</div>
<div v-else class="modules-grid">
<div v-else-if="canShowModules && modules.length > 0" class="modules-grid">
<div
v-for="module in modules"
:key="module.moduleId"
@@ -590,7 +645,9 @@ import {
createRemoveModuleProposal,
isModuleActive,
getModuleAddress,
getAllModules
getAllModules,
getNetworksInfo,
getDeploymentStatus
} from '../../services/modulesService.js';
import api from '../../api/axios';
@@ -612,11 +669,16 @@ const route = useRoute();
const selectedDle = ref(null);
const isLoadingDle = ref(false);
const modules = ref([]);
const supportedNetworks = ref([]);
const isLoadingModules = ref(false);
const isCreating = ref(false);
const isRemoving = ref(null);
const isActivating = ref(null);
const isVerifying = ref(null);
// Состояние деплоя
const deploymentStatus = ref('unknown'); // 'unknown', 'completed', 'in_progress', 'failed', 'not_started'
const isLoadingDeploymentStatus = ref(false);
const lastUpdateTime = ref('');
// Форма нового модуля
@@ -641,6 +703,23 @@ const modulesCount = computed(() => modules.value.length);
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
// Статус деплоя
const canShowModules = computed(() => deploymentStatus.value === 'completed');
const deploymentStatusMessage = computed(() => {
switch (deploymentStatus.value) {
case 'completed':
return 'Деплой завершен. Модули готовы к использованию.';
case 'in_progress':
return 'Деплой в процессе. Модули будут доступны после завершения.';
case 'failed':
return 'Деплой не удался. Проверьте логи и повторите попытку.';
case 'not_started':
return 'Деплой не начат. Запустите деплой для активации модулей.';
default:
return 'Статус деплоя неизвестен. Проверьте состояние системы.';
}
});
// Загрузка данных DLE
async function loadDleData() {
try {
@@ -672,6 +751,37 @@ async function loadDleData() {
}
}
// Проверка статуса деплоя
async function checkDeploymentStatus() {
try {
isLoadingDeploymentStatus.value = true;
const dleAddress = route.query.address;
if (!dleAddress) {
console.warn('[ModulesView] Адрес DLE не найден для проверки статуса деплоя');
deploymentStatus.value = 'unknown';
return;
}
console.log('[ModulesView] Проверка статуса деплоя для DLE:', dleAddress);
const statusResponse = await getDeploymentStatus(dleAddress);
console.log('[ModulesView] Статус деплоя:', statusResponse);
if (statusResponse.success) {
deploymentStatus.value = statusResponse.data.status || 'unknown';
} else {
deploymentStatus.value = 'unknown';
}
} catch (error) {
console.error('[ModulesView] Ошибка при проверке статуса деплоя:', error);
deploymentStatus.value = 'unknown';
} finally {
isLoadingDeploymentStatus.value = false;
}
}
// Загрузка модулей
async function loadModules() {
try {
@@ -681,15 +791,30 @@ async function loadModules() {
if (!dleAddress) {
console.error('[ModulesView] Адрес DLE не указан');
modules.value = [];
supportedNetworks.value = [];
return;
}
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
// Загружаем модули через modulesService
const modulesResponse = await getAllModules(dleAddress);
// Сначала проверяем статус деплоя
await checkDeploymentStatus();
console.log('[ModulesView] Ответ от API:', modulesResponse);
// Если деплой не завершен, не загружаем модули
if (deploymentStatus.value !== 'completed') {
console.log('[ModulesView] Деплой не завершен, модули не загружаются. Статус:', deploymentStatus.value);
modules.value = [];
return;
}
// Загружаем модули и информацию о сетях параллельно
const [modulesResponse, networksResponse] = await Promise.all([
getAllModules(dleAddress),
getNetworksInfo(dleAddress)
]);
console.log('[ModulesView] Ответ от API модулей:', modulesResponse);
console.log('[ModulesView] Ответ от API сетей:', networksResponse);
if (modulesResponse.success) {
modules.value = modulesResponse.data.modules || [];
@@ -697,7 +822,7 @@ async function loadModules() {
count: modules.value.length,
modules: modules.value.map(m => ({
name: m.moduleName,
address: m.moduleAddress,
addresses: m.addresses?.length || 0,
active: m.isActive,
id: m.moduleId
})),
@@ -717,6 +842,20 @@ async function loadModules() {
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
modules.value = [];
}
if (networksResponse.success) {
supportedNetworks.value = networksResponse.data.networks || [];
console.log('[ModulesView] Сети загружены успешно:', {
count: supportedNetworks.value.length,
networks: supportedNetworks.value.map(n => ({
name: n.networkName,
chainId: n.chainId
}))
});
} else {
console.error('[ModulesView] Ошибка загрузки сетей:', networksResponse.error);
supportedNetworks.value = [];
}
} catch (error) {
console.error('[ModulesView] Ошибка загрузки модулей:', error);
@@ -726,6 +865,7 @@ async function loadModules() {
status: error.response?.status
});
modules.value = [];
supportedNetworks.value = [];
} finally {
isLoadingModules.value = false;
}
@@ -754,22 +894,112 @@ async function handleCreateAddModuleProposal() {
});
if (result.success) {
console.log('[ModulesView] Предложение создано:', result);
alert('✅ Предложение для добавления модуля создано!');
console.log('[ModulesView] Данные транзакции получены:', result);
// Очищаем форму
newModule.value = {
moduleId: '',
moduleAddress: '',
description: '',
duration: 86400,
chainId: 11155111
};
// Перезагружаем модули
await loadModules();
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что адрес в правильном формате (checksum)
const isValidAddress = /^0x[a-fA-F0-9]{40}$/.test(result.data.to);
if (!isValidAddress) {
throw new Error(`Адрес не в правильном формате: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[ModulesView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[ModulesView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0x' + newModule.value.chainId.toString(16);
if (chainId !== expectedChainId) {
console.log(`[ModulesView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[ModulesView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[ModulesView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[ModulesView] Отправляем транзакцию:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[ModulesView] Транзакция отправлена:', txHash);
alert(`✅ Транзакция отправлена! Hash: ${txHash}`);
// Очищаем форму
newModule.value = {
moduleId: '',
moduleAddress: '',
description: '',
duration: 86400,
chainId: 11155111
};
// Перезагружаем модули
await loadModules();
} catch (txError) {
console.error('[ModulesView] Ошибка отправки транзакции:', txError);
alert('❌ Ошибка отправки транзакции: ' + txError.message);
}
} else {
alert('❌ Ошибка создания предложения: ' + result.error);
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
@@ -854,7 +1084,8 @@ async function verifyModule(module, addressInfo) {
dleAddress: dleAddress,
moduleId: module.moduleId,
moduleAddress: addressInfo.address,
moduleName: module.moduleName
moduleName: module.moduleName,
chainId: addressInfo.chainId
});
if (response.data.success) {
@@ -898,16 +1129,12 @@ function getVerificationButtonTitle(verificationStatus) {
// Утилиты
function getEtherscanUrl(address, networkIndex, chainId) {
// Если есть chainId, используем его для определения правильного URL
if (chainId) {
const networkUrls = {
11155111: `https://sepolia.etherscan.io/address/${address}`, // Sepolia
17000: `https://holesky.etherscan.io/address/${address}`, // Holesky
421614: `https://sepolia.arbiscan.io/address/${address}`, // Arbitrum Sepolia
84532: `https://sepolia.basescan.org/address/${address}` // Base Sepolia
};
return networkUrls[chainId] || `https://etherscan.io/address/${address}`;
// Если есть chainId, ищем информацию о сети в supportedNetworks
if (chainId && supportedNetworks.value.length > 0) {
const network = supportedNetworks.value.find(n => n.chainId === chainId);
if (network && network.etherscanUrl) {
return `${network.etherscanUrl}/address/${address}`;
}
}
// Fallback на старую логику по networkIndex (для обратной совместимости)
@@ -1204,6 +1431,102 @@ onMounted(() => {
border: 1px solid #e9ecef;
}
/* Статус деплоя */
.deployment-status {
margin: 20px 0;
}
.status-loading {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.status-loading i {
color: #007bff;
font-size: 1.2rem;
}
.status-loading span {
color: #6c757d;
font-weight: 500;
}
.status-message {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 20px;
border-radius: 12px;
border: 2px solid;
}
.status-message.completed {
background-color: #e8f5e8;
border-color: #28a745;
}
.status-message.in_progress {
background-color: #e3f2fd;
border-color: #007bff;
}
.status-message.failed {
background-color: #ffebee;
border-color: #dc3545;
}
.status-message.not_started {
background-color: #fff3cd;
border-color: #ffc107;
}
.status-message.unknown {
background-color: #f8f9fa;
border-color: #6c757d;
}
.status-icon {
font-size: 2rem;
margin-top: 5px;
}
.status-message.completed .status-icon {
color: #28a745;
}
.status-message.in_progress .status-icon {
color: #007bff;
}
.status-message.failed .status-icon {
color: #dc3545;
}
.status-message.not_started .status-icon {
color: #ffc107;
}
.status-message.unknown .status-icon {
color: #6c757d;
}
.status-content h4 {
margin: 0 0 10px 0;
font-size: 1.1rem;
font-weight: 600;
}
.status-content p {
margin: 0;
color: #6c757d;
line-height: 1.5;
}
.list-header {
display: flex;
justify-content: space-between;

View File

@@ -0,0 +1,585 @@
<!--
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
-->
<template>
<BaseLayout
:is-authenticated="isAuthenticated"
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="reader-module-deploy">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Деплой DLEReader</h1>
<p>API для чтения данных DLE - получение информации о контракте и предложениях</p>
<p v-if="dleAddress" class="dle-address">
<strong>DLE:</strong> {{ dleAddress }}
</p>
</div>
<button class="close-btn" @click="router.push('/management/modules')">×</button>
</div>
<!-- Информация о модуле -->
<div class="module-info">
<div class="info-card">
<h3>📊 DLEReader</h3>
<div class="info-grid">
<div class="info-item">
<strong>Назначение:</strong> Чтение данных DLE контракта
</div>
<div class="info-item">
<strong>Функции:</strong> API для предложений, голосования, статистики
</div>
<div class="info-item">
<strong>Безопасность:</strong> Только чтение, не изменяет состояние
</div>
</div>
</div>
</div>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой DLEReader во всех сетях</h3>
<p>Деплой API модуля для чтения данных во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки DLEReader:</h4>
<div class="settings-form">
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
<div class="simple-info">
<h5>📋 Информация о DLEReader:</h5>
<div class="info-text">
<p><strong>DLEReader</strong> - это простой read-only модуль, который:</p>
<ul>
<li> Только читает данные из DLE контракта</li>
<li> Не изменяет состояние блокчейна</li>
<li> Предоставляет API для получения информации</li>
<li> Безопасен для обновления</li>
</ul>
<p><strong>Конструктор принимает только один параметр:</strong> адрес DLE контракта</p>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployDLEReader"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой DLEReader' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../../components/BaseLayout.vue';
// Props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Единственный параметр - ID сети
chainId: 11155111
});
// Функция деплоя DLEReader
async function deployDLEReader() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[DLEReaderDeployView] Начинаем деплой DLEReader для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-reader', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'reader',
settings: {
// Единственный параметр - ID сети
chainId: moduleSettings.value.chainId
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[DLEReaderDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой DLEReader запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[DLEReaderDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
console.log('[DLEReaderDeployView] Страница загружена');
});
</script>
<style scoped>
.reader-module-deploy {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius-lg);
color: white;
}
.header-content h1 {
margin: 0 0 10px 0;
font-size: 2rem;
font-weight: 700;
}
.header-content p {
margin: 0 0 5px 0;
opacity: 0.9;
font-size: 1.1rem;
}
.dle-address {
font-family: 'Courier New', monospace;
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: var(--radius-sm);
margin-top: 10px;
}
.close-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;
}
.info-card {
background: white;
border-radius: var(--radius-md);
padding: 20px;
border: 1px solid #e9ecef;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.info-card h3 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.info-item {
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border-left: 4px solid var(--color-primary);
}
.info-item strong {
color: var(--color-primary);
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Настройки отображения данных */
.data-display-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.data-display-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.data-display-settings .form-row {
margin-bottom: 15px;
}
.data-display-settings .form-group {
margin-bottom: 15px;
}
.data-display-settings .form-group:last-child {
margin-bottom: 0;
}
/* Простая информация */
.simple-info {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.simple-info h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.info-text {
color: #666;
line-height: 1.6;
}
.info-text p {
margin: 0 0 10px 0;
}
.info-text ul {
margin: 10px 0;
padding-left: 20px;
}
.info-text li {
margin: 5px 0;
color: #555;
}
.info-text strong {
color: var(--color-primary);
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Сети */
.networks-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.network-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 12px;
color: #666;
font-family: 'Courier New', monospace;
}
</style>

View File

@@ -49,11 +49,255 @@
</div>
</div>
<!-- Форма деплоя будет добавлена позже -->
<div class="deploy-form-placeholder">
<div class="placeholder-content">
<h3>🚧 Форма деплоя в разработке</h3>
<p>Здесь будет форма для деплоя TimelockModule</p>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой TimelockModule во всех сетях</h3>
<p>Деплой модуля временных задержек во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки TimelockModule:</h4>
<div class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
<div class="form-group">
<label for="defaultDelay">Стандартная задержка (дни):</label>
<input
type="number"
id="defaultDelay"
v-model="moduleSettings.defaultDelay"
class="form-control"
min="1"
max="30"
placeholder="2"
>
<small class="form-help">Стандартная задержка для операций (1-30 дней)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
<input
type="number"
id="emergencyDelay"
v-model="moduleSettings.emergencyDelay"
class="form-control"
min="5"
max="1440"
placeholder="30"
>
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
</div>
<div class="form-group">
<label for="maxDelay">Максимальная задержка (дни):</label>
<input
type="number"
id="maxDelay"
v-model="moduleSettings.maxDelay"
class="form-control"
min="1"
max="365"
placeholder="30"
>
<small class="form-help">Максимальная задержка для операций (1-365 дней)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="minDelay">Минимальная задержка (часы):</label>
<input
type="number"
id="minDelay"
v-model="moduleSettings.minDelay"
class="form-control"
min="1"
max="720"
placeholder="24"
>
<small class="form-help">Минимальная задержка для операций (1-720 часов)</small>
</div>
<div class="form-group">
<label for="maxOperations">Максимум операций в очереди:</label>
<input
type="number"
id="maxOperations"
v-model="moduleSettings.maxOperations"
class="form-control"
min="10"
max="1000"
placeholder="100"
>
<small class="form-help">Максимальное количество операций в очереди (10-1000)</small>
</div>
</div>
<!-- Дополнительные настройки таймлока -->
<div class="advanced-settings">
<h5>🔧 Дополнительные настройки таймлока:</h5>
<div class="form-group">
<label for="criticalOperations">Критические операции (JSON формат):</label>
<textarea
id="criticalOperations"
v-model="moduleSettings.criticalOperations"
class="form-control"
rows="3"
placeholder='["0x12345678", "0x87654321"]'
></textarea>
<small class="form-help">Селекторы функций, которые считаются критическими (JSON массив)</small>
</div>
<div class="form-group">
<label for="emergencyOperations">Экстренные операции (JSON формат):</label>
<textarea
id="emergencyOperations"
v-model="moduleSettings.emergencyOperations"
class="form-control"
rows="3"
placeholder='["0xabcdef12", "0x21fedcba"]'
></textarea>
<small class="form-help">Селекторы функций для экстренных операций (JSON массив)</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="operationDelays">Задержки для операций (JSON формат):</label>
<textarea
id="operationDelays"
v-model="moduleSettings.operationDelays"
class="form-control"
rows="4"
placeholder='{"0x12345678": 86400, "0x87654321": 172800}'
></textarea>
<small class="form-help">Кастомные задержки для конкретных операций (селектор => секунды)</small>
</div>
<div class="form-group">
<label for="autoExecuteEnabled">Автоисполнение включено:</label>
<select
id="autoExecuteEnabled"
v-model="moduleSettings.autoExecuteEnabled"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Автоматическое исполнение операций после истечения задержки</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="cancellationWindow">Окно отмены (часы):</label>
<input
type="number"
id="cancellationWindow"
v-model="moduleSettings.cancellationWindow"
class="form-control"
min="1"
max="168"
placeholder="24"
>
<small class="form-help">Время, в течение которого можно отменить операцию (1-168 часов)</small>
</div>
<div class="form-group">
<label for="executionWindow">Окно исполнения (часы):</label>
<input
type="number"
id="executionWindow"
v-model="moduleSettings.executionWindow"
class="form-control"
min="1"
max="168"
placeholder="48"
>
<small class="form-help">Время, в течение которого можно исполнить операцию (1-168 часов)</small>
</div>
</div>
<div class="form-group">
<label for="timelockDescription">Описание таймлока:</label>
<textarea
id="timelockDescription"
v-model="moduleSettings.timelockDescription"
class="form-control"
rows="2"
placeholder="Описание таймлока DLE для безопасности операций..."
></textarea>
<small class="form-help">Описание таймлока для документации</small>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployTimelockModule"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TimelockModule' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
@@ -83,6 +327,108 @@ const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Основные параметры
chainId: 11155111,
defaultDelay: 2, // days
emergencyDelay: 30, // minutes
maxDelay: 30, // days
minDelay: 24, // hours
// Дополнительные настройки
maxOperations: 100,
criticalOperations: '',
emergencyOperations: '',
operationDelays: '',
autoExecuteEnabled: 'true',
cancellationWindow: 24, // hours
executionWindow: 48, // hours
timelockDescription: ''
});
// Функция деплоя TimelockModule
async function deployTimelockModule() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[TimelockModuleDeployView] Начинаем деплой TimelockModule для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-timelock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'timelock',
settings: {
// Основные параметры
chainId: moduleSettings.value.chainId,
defaultDelay: moduleSettings.value.defaultDelay * 24 * 60 * 60, // конвертируем дни в секунды
emergencyDelay: moduleSettings.value.emergencyDelay * 60, // конвертируем минуты в секунды
maxDelay: moduleSettings.value.maxDelay * 24 * 60 * 60, // конвертируем дни в секунды
minDelay: moduleSettings.value.minDelay * 60 * 60, // конвертируем часы в секунды
// Дополнительные настройки
maxOperations: parseInt(moduleSettings.value.maxOperations),
criticalOperations: moduleSettings.value.criticalOperations ? JSON.parse(moduleSettings.value.criticalOperations) : [],
emergencyOperations: moduleSettings.value.emergencyOperations ? JSON.parse(moduleSettings.value.emergencyOperations) : [],
operationDelays: moduleSettings.value.operationDelays ? JSON.parse(moduleSettings.value.operationDelays) : {},
autoExecuteEnabled: moduleSettings.value.autoExecuteEnabled === 'true',
cancellationWindow: moduleSettings.value.cancellationWindow * 60 * 60, // конвертируем часы в секунды
executionWindow: moduleSettings.value.executionWindow * 60 * 60, // конвертируем часы в секунды
timelockDescription: moduleSettings.value.timelockDescription
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[TimelockModuleDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой TimelockModule запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[TimelockModuleDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
@@ -150,6 +496,182 @@ onMounted(() => {
color: #333;
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Дополнительные настройки */
.advanced-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.advanced-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.advanced-settings .form-row {
margin-bottom: 15px;
}
.advanced-settings .form-group {
margin-bottom: 15px;
}
.advanced-settings .form-group:last-child {
margin-bottom: 0;
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;

View File

@@ -49,11 +49,263 @@
</div>
</div>
<!-- Форма деплоя будет добавлена позже -->
<div class="deploy-form-placeholder">
<div class="placeholder-content">
<h3>🚧 Форма деплоя в разработке</h3>
<p>Здесь будет форма для деплоя TreasuryModule</p>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой TreasuryModule во всех сетях</h3>
<p>Деплой модуля казначейства во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки TreasuryModule:</h4>
<div class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="emergencyAdmin">Адрес экстренного администратора:</label>
<input
type="text"
id="emergencyAdmin"
v-model="moduleSettings.emergencyAdmin"
class="form-control"
placeholder="0x..."
required
>
<small class="form-help">Адрес экстренного администратора для управления модулем</small>
</div>
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="defaultDelay">Стандартная задержка (часы):</label>
<input
type="number"
id="defaultDelay"
v-model="moduleSettings.defaultDelay"
class="form-control"
min="1"
max="720"
placeholder="24"
>
<small class="form-help">Стандартная задержка для операций (1-720 часов)</small>
</div>
<div class="form-group">
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
<input
type="number"
id="emergencyDelay"
v-model="moduleSettings.emergencyDelay"
class="form-control"
min="5"
max="1440"
placeholder="30"
>
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
</div>
</div>
<div class="form-group">
<label for="supportedTokens">Поддерживаемые токены (адреса через запятую):</label>
<textarea
id="supportedTokens"
v-model="moduleSettings.supportedTokens"
class="form-control"
rows="3"
placeholder="0x1234..., 0x5678..., 0x9abc..."
></textarea>
<small class="form-help">Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую)</small>
</div>
<div class="form-group">
<label for="gasPaymentTokens">Токены для оплаты газа (адреса через запятую):</label>
<textarea
id="gasPaymentTokens"
v-model="moduleSettings.gasPaymentTokens"
class="form-control"
rows="2"
placeholder="0x1234..., 0x5678..."
></textarea>
<small class="form-help">Токены, которыми можно оплачивать газ (через запятую)</small>
</div>
<!-- Дополнительные настройки казны -->
<div class="advanced-settings">
<h5>🔧 Дополнительные настройки казны:</h5>
<div class="form-row">
<div class="form-group">
<label for="paymasterAddress">Адрес Paymaster:</label>
<input
type="text"
id="paymasterAddress"
v-model="moduleSettings.paymasterAddress"
class="form-control"
placeholder="0x..."
>
<small class="form-help">Адрес Paymaster для ERC-4337 (оплата газа любым токеном)</small>
</div>
<div class="form-group">
<label for="maxBatchTransfers">Максимум batch переводов:</label>
<input
type="number"
id="maxBatchTransfers"
v-model="moduleSettings.maxBatchTransfers"
class="form-control"
min="1"
max="100"
placeholder="50"
>
<small class="form-help">Максимальное количество переводов в batch операции (1-100)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="gasTokenRates">Курсы токенов для газа (JSON формат):</label>
<textarea
id="gasTokenRates"
v-model="moduleSettings.gasTokenRates"
class="form-control"
rows="3"
placeholder='{"0x1234...": "1000000000000000000", "0x5678...": "2000000000000000000"}'
></textarea>
<small class="form-help">Курсы обмена токенов на нативную монету (JSON формат)</small>
</div>
<div class="form-group">
<label for="emergencyThreshold">Порог экстренных операций (ETH):</label>
<input
type="number"
id="emergencyThreshold"
v-model="moduleSettings.emergencyThreshold"
class="form-control"
min="0"
step="0.001"
placeholder="1.0"
>
<small class="form-help">Порог для экстренных операций в ETH</small>
</div>
</div>
<div class="form-group">
<label for="initialTokens">Начальные токены для добавления (JSON формат):</label>
<textarea
id="initialTokens"
v-model="moduleSettings.initialTokens"
class="form-control"
rows="4"
placeholder='[{"address": "0x1234...", "symbol": "USDC", "decimals": 6}, {"address": "0x5678...", "symbol": "USDT", "decimals": 6}]'
></textarea>
<small class="form-help">Токены для автоматического добавления при деплое (JSON массив)</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="autoRefreshBalances">Автообновление балансов:</label>
<select
id="autoRefreshBalances"
v-model="moduleSettings.autoRefreshBalances"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Автоматическое обновление балансов токенов</small>
</div>
<div class="form-group">
<label for="batchTransferEnabled">Batch переводы включены:</label>
<select
id="batchTransferEnabled"
v-model="moduleSettings.batchTransferEnabled"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Разрешить batch операции переводов</small>
</div>
</div>
<div class="form-group">
<label for="treasuryDescription">Описание казны:</label>
<textarea
id="treasuryDescription"
v-model="moduleSettings.treasuryDescription"
class="form-control"
rows="2"
placeholder="Описание казны DLE для управления финансами..."
></textarea>
<small class="form-help">Описание казны для документации</small>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployTreasuryModule"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TreasuryModule' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
@@ -83,6 +335,114 @@ const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Основные параметры
emergencyAdmin: '',
chainId: 11155111,
defaultDelay: 24, // hours
emergencyDelay: 30, // minutes
// Токены
supportedTokens: '',
gasPaymentTokens: '',
initialTokens: '',
// Дополнительные настройки
paymasterAddress: '',
maxBatchTransfers: 50,
gasTokenRates: '',
emergencyThreshold: 1.0,
autoRefreshBalances: 'true',
batchTransferEnabled: 'true',
treasuryDescription: ''
});
// Функция деплоя TreasuryModule
async function deployTreasuryModule() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[TreasuryModuleDeployView] Начинаем деплой TreasuryModule для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-treasury', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'treasury',
settings: {
// Основные параметры
emergencyAdmin: moduleSettings.value.emergencyAdmin,
chainId: moduleSettings.value.chainId,
defaultDelay: moduleSettings.value.defaultDelay,
emergencyDelay: moduleSettings.value.emergencyDelay,
// Токены
supportedTokens: moduleSettings.value.supportedTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
gasPaymentTokens: moduleSettings.value.gasPaymentTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
initialTokens: moduleSettings.value.initialTokens ? JSON.parse(moduleSettings.value.initialTokens) : [],
// Дополнительные настройки
paymasterAddress: moduleSettings.value.paymasterAddress,
maxBatchTransfers: parseInt(moduleSettings.value.maxBatchTransfers),
gasTokenRates: moduleSettings.value.gasTokenRates ? JSON.parse(moduleSettings.value.gasTokenRates) : {},
emergencyThreshold: parseFloat(moduleSettings.value.emergencyThreshold),
autoRefreshBalances: moduleSettings.value.autoRefreshBalances === 'true',
batchTransferEnabled: moduleSettings.value.batchTransferEnabled === 'true',
treasuryDescription: moduleSettings.value.treasuryDescription
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[TreasuryModuleDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой TreasuryModule запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[TreasuryModuleDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
@@ -150,6 +510,240 @@ onMounted(() => {
color: #333;
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Дополнительные настройки */
.advanced-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.advanced-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.advanced-settings .form-row {
margin-bottom: 15px;
}
.advanced-settings .form-group {
margin-bottom: 15px;
}
.advanced-settings .form-group:last-child {
margin-bottom: 0;
}
.networks-info h4,
.deploy-parameters h4 {
margin: 0 0 15px 0;
color: var(--color-primary);
}
.networks-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.network-item {
display: flex;
flex-direction: column;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 12px;
color: #666;
font-family: monospace;
}
.parameter-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.parameter-item:last-child {
border-bottom: none;
}
.parameter-item label {
font-weight: 500;
color: #333;
}
.parameter-value {
font-family: monospace;
color: var(--color-primary);
background: #f8f9fa;
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 14px;
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;

View File

@@ -0,0 +1,27 @@
/**
* 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
*/
// Основные модули DLE
export { default as TreasuryModuleDeployView } from './TreasuryModuleDeployView.vue';
export { default as TimelockModuleDeployView } from './TimelockModuleDeployView.vue';
export { default as DLEReaderDeployView } from './DLEReaderDeployView.vue';
// Дополнительные модули
export { default as CommunicationModuleDeployView } from './CommunicationModuleDeployView.vue';
export { default as ApplicationModuleDeployView } from './ApplicationModuleDeployView.vue';
export { default as MintModuleDeploy } from './MintModuleDeploy.vue';
export { default as BurnModuleDeploy } from './BurnModuleDeploy.vue';
export { default as OracleModuleDeploy } from './OracleModuleDeploy.vue';
export { default as InheritanceModuleDeploy } from './InheritanceModuleDeploy.vue';
// Кастомный модуль
export { default as ModuleDeployFormView } from './ModuleDeployFormView.vue';

View File

@@ -58,7 +58,7 @@ export default defineConfig({
rewrite: (path) => path,
},
'/ws': {
target: 'ws://dapp-backend:8000',
target: 'http://dapp-backend:8000',
ws: true,
changeOrigin: true,
secure: false,

View File

@@ -1,4 +1,15 @@
#!/bin/bash
# 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
#!/bin/bash
if ! docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db > /dev/null 2>&1; then