diff --git a/backend/contracts/FactoryDeployer.sol b/backend/contracts/FactoryDeployer.sol deleted file mode 100644 index 485269e..0000000 --- a/backend/contracts/FactoryDeployer.sol +++ /dev/null @@ -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))); - } -} - - diff --git a/backend/contracts/MockNoop.sol b/backend/contracts/MockNoop.sol index a3776ee..c1cfe05 100644 --- a/backend/contracts/MockNoop.sol +++ b/backend/contracts/MockNoop.sol @@ -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; diff --git a/backend/contracts/MockPaymaster.sol b/backend/contracts/MockPaymaster.sol new file mode 100644 index 0000000..898973d --- /dev/null +++ b/backend/contracts/MockPaymaster.sol @@ -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)] + ); + } +} diff --git a/backend/contracts/MockToken.sol b/backend/contracts/MockToken.sol index 9c6ba6e..26242ae 100644 --- a/backend/contracts/MockToken.sol +++ b/backend/contracts/MockToken.sol @@ -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; diff --git a/backend/contracts/TreasuryModule.sol b/backend/contracts/TreasuryModule.sol index edd50e3..6bfd7bf 100644 --- a/backend/contracts/TreasuryModule.sol +++ b/backend/contracts/TreasuryModule.sol @@ -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 ); } diff --git a/backend/db.js b/backend/db.js index e0c2ed8..a176edb 100644 --- a/backend/db.js +++ b/backend/db.js @@ -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, // Разрешить выход при отсутствии активных клиентов }); diff --git a/backend/hardhat.config.js b/backend/hardhat.config.js index 1486250..7017542 100644 --- a/backend/hardhat.config.js +++ b/backend/hardhat.config.js @@ -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 = { diff --git a/backend/package.json b/backend/package.json index 626c456..96e9027 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" }, diff --git a/backend/routes/blockchain.js b/backend/routes/blockchain.js index f1cb6ca..28ef2b5 100644 --- a/backend/routes/blockchain.js +++ b/backend/routes/blockchain.js @@ -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) => { diff --git a/backend/routes/dleCore.js b/backend/routes/dleCore.js index 2e8daf6..29243aa 100644 --- a/backend/routes/dleCore.js +++ b/backend/routes/dleCore.js @@ -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), diff --git a/backend/routes/dleModules.js b/backend/routes/dleModules.js index 27abf61..8ecc4ba 100644 --- a/backend/routes/dleModules.js +++ b/backend/routes/dleModules.js @@ -13,12 +13,109 @@ const express = require('express'); const router = express.Router(); const { ethers } = require('ethers'); +const { Interface, AbiCoder } = ethers; +const hre = require('hardhat'); const rpcProviderService = require('../services/rpcProviderService'); +const { spawn } = require('child_process'); +const path = require('path'); + +// Утилитарная функция для автоматической компиляции контрактов +async function autoCompileContracts() { + console.log(`[DLE Modules] Запуск автоматической компиляции контрактов...`); + + return new Promise((resolve, reject) => { + const compileProcess = spawn('npx', ['hardhat', 'compile'], { + cwd: path.join(__dirname, '..'), + stdio: 'pipe' + }); + + let stdout = ''; + let stderr = ''; + + compileProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + compileProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + compileProcess.on('close', (code) => { + if (code === 0) { + console.log(`[DLE Modules] ✅ Компиляция завершена успешно`); + resolve(); + } else { + console.error(`[DLE Modules] ❌ Ошибка компиляции:`, stderr); + reject(new Error(`Ошибка компиляции: ${stderr}`)); + } + }); + + compileProcess.on('error', (error) => { + console.error(`[DLE Modules] ❌ Ошибка запуска компиляции:`, error); + reject(error); + }); + }); +} + +// Универсальная функция для выполнения операций с повторами +async function executeWithRetries(operation, operationName, maxRetries = 1, retryDelay = 30000) { + let lastError = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`[Retry Logic] ${operationName} - попытка ${attempt}/${maxRetries}`); + const result = await operation(); + console.log(`[Retry Logic] ${operationName} - успешно выполнено с попытки ${attempt}`); + return result; + } catch (error) { + lastError = error; + console.error(`[Retry Logic] ${operationName} - ошибка на попытке ${attempt}:`, error.message); + + if (attempt < maxRetries) { + console.log(`[Retry Logic] ${operationName} - повтор через ${retryDelay}ms...`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } + } + + console.error(`[Retry Logic] ${operationName} - все попытки исчерпаны`); + throw lastError; +} + +// Функция для выполнения операций с повторами для каждой сети +async function executeForEachNetwork(networks, operation, operationName, maxRetries = 1, retryDelay = 30000) { + const results = []; + let allSuccessful = true; + + for (const network of networks) { + try { + const result = await executeWithRetries( + () => operation(network), + `${operationName} в сети ${network.networkName} (${network.chainId})`, + maxRetries, + retryDelay + ); + results.push(result); + } catch (error) { + console.error(`[Retry Logic] Ошибка ${operationName} в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + return { results, allSuccessful }; +} // Проверить активность модуля router.post('/is-module-active', async (req, res) => { try { - const { dleAddress, moduleId } = req.body; + const { dleAddress, moduleId, chainId } = req.body; if (!dleAddress || !moduleId) { return res.status(400).json({ @@ -27,13 +124,26 @@ router.post('/is-module-active', async (req, res) => { }); } - console.log(`[DLE Modules] Проверка активности модуля: ${moduleId} для DLE: ${dleAddress}`); + // Если chainId не указан, используем первый доступный + let targetChainId = chainId; + if (!targetChainId) { + const allProviders = await rpcProviderService.getAllRpcProviders(); + if (allProviders.length === 0) { + return res.status(500).json({ + success: false, + error: 'RPC провайдеры не найдены в базе данных' + }); + } + targetChainId = allProviders[0].chain_id; + } - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + console.log(`[DLE Modules] Проверка активности модуля: ${moduleId} для DLE: ${dleAddress} в сети ${targetChainId}`); + + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(targetChainId); if (!rpcUrl) { return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: `RPC URL для сети ${targetChainId} не найден` }); } @@ -100,7 +210,7 @@ router.get('/deployed/:dleAddress', async (req, res) => { // Получить адрес модуля router.post('/get-module-address', async (req, res) => { try { - const { dleAddress, moduleId } = req.body; + const { dleAddress, moduleId, chainId } = req.body; if (!dleAddress || !moduleId) { return res.status(400).json({ @@ -109,13 +219,14 @@ router.post('/get-module-address', async (req, res) => { }); } - console.log(`[DLE Modules] Получение адреса модуля: ${moduleId} для DLE: ${dleAddress}`); + const targetChainId = Number(chainId) || 11155111; + console.log(`[DLE Modules] Получение адреса модуля: ${moduleId} для DLE: ${dleAddress} в сети: ${targetChainId}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(targetChainId); if (!rpcUrl) { return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: `RPC URL для сети ${targetChainId} не найден` }); } @@ -130,12 +241,13 @@ router.post('/get-module-address', async (req, res) => { // Получаем адрес модуля const moduleAddress = await dle.getModuleAddress(moduleId); - console.log(`[DLE Modules] Адрес модуля ${moduleId}: ${moduleAddress}`); + console.log(`[DLE Modules] Адрес модуля ${moduleId} (chainId ${targetChainId}): ${moduleAddress}`); res.json({ success: true, data: { - moduleAddress: moduleAddress + moduleAddress: moduleAddress, + chainId: targetChainId } }); @@ -148,6 +260,183 @@ router.post('/get-module-address', async (req, res) => { } }); +// Подготовить транзакции инициализации модулей для подписи в кошельке (во всех сетях) +router.post('/prepare-initialize-modules-all-networks', async (req, res) => { + try { + const { dleAddress } = req.body; + if (!dleAddress) { + return res.status(400).json({ success: false, error: 'Адрес DLE обязателен' }); + } + + // Получаем поддерживаемые сети из контракта + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + if (supportedNetworks.length === 0) { + return res.status(400).json({ success: false, error: 'Не найдены поддерживаемые сети для DLE' }); + } + + // Интерфейс функции инициализации + const dleIface = new Interface([ + 'function initializeBaseModules(address _treasuryAddress, address _timelockAddress, address _readerAddress)' + ]); + + // Module IDs + const moduleIds = { + treasury: '0x7472656173757279000000000000000000000000000000000000000000000000', + timelock: '0x74696d656c6f636b000000000000000000000000000000000000000000000000', + reader: '0x7265616465720000000000000000000000000000000000000000000000000000' + }; + + const results = []; + for (const network of supportedNetworks) { + try { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const dle = new ethers.Contract( + dleAddress, + [ + 'function getModuleAddress(bytes32 _moduleId) external view returns (address)', + 'function modulesInitialized() external view returns (bool)' + ], + provider + ); + + const already = await dle.modulesInitialized(); + const treasuryAddress = await dle.getModuleAddress(moduleIds.treasury); + const timelockAddress = await dle.getModuleAddress(moduleIds.timelock); + const readerAddress = await dle.getModuleAddress(moduleIds.reader); + + if ( + treasuryAddress === '0x0000000000000000000000000000000000000000' || + timelockAddress === '0x0000000000000000000000000000000000000000' || + readerAddress === '0x0000000000000000000000000000000000000000' + ) { + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'modules_not_deployed', + message: 'Не все модули задеплоены' + }); + continue; + } + + const data = dleIface.encodeFunctionData('initializeBaseModules', [ + treasuryAddress, + timelockAddress, + readerAddress + ]); + + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: already ? 'already_initialized' : 'ready', + to: dleAddress, + data + }); + } catch (e) { + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: e.message + }); + } + } + + return res.json({ success: true, data: { results } }); + } catch (error) { + console.error('[DLE Modules] Ошибка подготовки инициализации:', error); + res.status(500).json({ success: false, error: 'Ошибка подготовки инициализации: ' + error.message }); + } +}); + +// Подготовить транзакции деплоя модуля для подписи в кошельке (во всех сетях) +router.post('/prepare-deploy-module-all-networks', async (req, res) => { + try { + const { dleAddress, moduleType, deployerAddress } = req.body; + if (!dleAddress || !moduleType || !deployerAddress) { + return res.status(400).json({ success: false, error: 'dleAddress, moduleType и deployerAddress обязательны' }); + } + + // Компиляция на случай отсутствия артефактов + try { await autoCompileContracts(); } catch (_) {} + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + if (supportedNetworks.length === 0) { + return res.status(400).json({ success: false, error: 'Не найдены поддерживаемые сети для DLE' }); + } + + // Определяем контракт и аргументы конструктора + const moduleConfigs = { + treasury: { + contractName: 'TreasuryModule', + args: (chainId) => [dleAddress, chainId, deployerAddress] + }, + timelock: { + contractName: 'TimelockModule', + args: () => [dleAddress] + }, + reader: { + contractName: 'DLEReader', + args: () => [dleAddress] + } + }; + const cfg = moduleConfigs[moduleType]; + if (!cfg) return res.status(400).json({ success: false, error: `Неизвестный тип модуля: ${moduleType}` }); + + // Загрузка артефакта + const path = require('path'); + const fs = require('fs'); + const artifactPath = path.join( + __dirname, + `../artifacts/contracts/${cfg.contractName}.sol/${cfg.contractName}.json` + ); + if (!fs.existsSync(artifactPath)) { + return res.status(500).json({ success: false, error: `Артефакт не найден: ${artifactPath}` }); + } + const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8')); + const bytecode = artifact.bytecode; // 0x... + + if (!bytecode || bytecode === '0x') { + return res.status(500).json({ success: false, error: `Пустой bytecode у ${cfg.contractName}` }); + } + + const results = []; + for (const network of supportedNetworks) { + try { + const constructorArgs = cfg.args(network.chainId); + // Кодируем аргументы конструктора по ABI контракта + const ctorTypes = (artifact.abi.find(i => i.type === 'constructor')?.inputs || []).map(i => i.type); + const encodedArgs = ctorTypes.length + ? AbiCoder.defaultAbiCoder().encode(ctorTypes, constructorArgs) + : '0x'; + const data = bytecode + (encodedArgs.startsWith('0x') ? encodedArgs.slice(2) : encodedArgs); + + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'ready', + to: null, + data, + value: '0x0', + contractName: cfg.contractName + }); + } catch (e) { + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: e.message + }); + } + } + + return res.json({ success: true, data: { results } }); + } catch (error) { + console.error('[DLE Modules] Ошибка подготовки деплоя модуля:', error); + res.status(500).json({ success: false, error: 'Ошибка подготовки деплоя модуля: ' + error.message }); + } +}); + // Получить все модули router.post('/get-all-modules', async (req, res) => { try { @@ -160,204 +449,132 @@ router.post('/get-all-modules', async (req, res) => { }); } - console.log(`[DLE Modules] Получение всех модулей для DLE: ${dleAddress}`); + console.log(`[DLE Modules] Получение всех модулей для DLE: ${dleAddress} (только из блокчейна)`); - // Сначала пытаемся получить модули из файлов деплоя - console.log(`[DLE Modules] Вызываем getDeployedModulesInfo...`); - const modulesInfo = await getDeployedModulesInfo(dleAddress); - console.log(`[DLE Modules] Результат getDeployedModulesInfo:`, modulesInfo); - - if (modulesInfo.modules && modulesInfo.modules.length > 0) { - console.log(`[DLE Modules] Модули найдены в файлах, количество: ${modulesInfo.modules.length}`); - - // Преобразуем данные в нужный формат для frontend - // Группируем модули по типам, а не по сетям - const moduleGroups = { - treasury: { - moduleId: "0x747265617375727900000000000000000000000000000000000000000000000000", - moduleName: "TREASURY", - moduleDescription: "Казначейство DLE - управление финансами, депозиты, выводы, дивиденды", - addresses: [], - isActive: true, - deployedAt: modulesInfo.deployTimestamp - }, - timelock: { - moduleId: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", - moduleName: "TIMELOCK", - moduleDescription: "Модуль задержек исполнения - безопасность критических операций через таймлоки", - addresses: [], - isActive: true, - deployedAt: modulesInfo.deployTimestamp - }, - reader: { - moduleId: "0x726561646572000000000000000000000000000000000000000000000000000000", - moduleName: "READER", - moduleDescription: "Модуль чтения данных DLE - получение информации о контракте", - addresses: [], - isActive: true, - deployedAt: modulesInfo.deployTimestamp - } - }; - - // Собираем адреса для каждого типа модуля из всех сетей - // Определяем реальные chainId для каждого адреса - for (let networkIndex = 0; networkIndex < modulesInfo.modules.length; networkIndex++) { - const networkModules = modulesInfo.modules[networkIndex]; - - console.log(`[DLE Modules] Обрабатываем сеть ${networkIndex + 1}:`, networkModules); - - // Получаем информацию о сети из файла деплоя - let chainId = null; - let networkName = `Сеть ${networkIndex + 1}`; - - // Если в файле есть информация о сетях, используем её - if (modulesInfo.networks && modulesInfo.networks[networkIndex]) { - chainId = modulesInfo.networks[networkIndex].chainId; - networkName = modulesInfo.networks[networkIndex].networkName || `Сеть ${networkIndex + 1}`; - } - - if (networkModules.treasuryModule) { - moduleGroups.treasury.addresses.push({ - address: networkModules.treasuryModule, - networkName: networkName, - networkIndex: networkIndex, - chainId: chainId, - verificationStatus: modulesInfo.verification?.[networkIndex]?.treasuryModule || 'pending' - }); - } - - if (networkModules.timelockModule) { - moduleGroups.timelock.addresses.push({ - address: networkModules.timelockModule, - networkName: networkName, - networkIndex: networkIndex, - chainId: chainId, - verificationStatus: modulesInfo.verification?.[networkIndex]?.timelockModule || 'pending' - }); - } - - if (networkModules.dleReader) { - moduleGroups.reader.addresses.push({ - address: networkModules.dleReader, - networkName: networkName, - networkIndex: networkIndex, - chainId: chainId, - verificationStatus: modulesInfo.verification?.[networkIndex]?.dleReader || 'pending' - }); - } - } - - // Преобразуем в массив модулей - const formattedModules = Object.values(moduleGroups).filter(module => module.addresses.length > 0); - - console.log(`[DLE Modules] Сгруппированные модули:`, formattedModules); - console.log(`[DLE Modules] Найдено типов модулей: ${formattedModules.length}`); + // Получаем информацию о поддерживаемых сетях из DLE контракта + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + console.log(`[DLE Modules] Найдено поддерживаемых сетей: ${supportedNetworks.length}`); - res.json({ + if (supportedNetworks.length === 0) { + return res.json({ success: true, data: { - modules: formattedModules, - modulesInitialized: true, - totalModules: formattedModules.length, - activeModules: formattedModules.length - } - }); - } else { - console.log(`[DLE Modules] Файлы модулей не найдены или пусты, проверяем блокчейн для 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 isModuleActive(bytes32 _moduleId) external view returns (bool)", - "function getModuleAddress(bytes32 _moduleId) external view returns (address)", - "function modulesInitialized() external view returns (bool)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Проверяем инициализацию модулей - let modulesInitialized = false; - try { - modulesInitialized = await dle.modulesInitialized(); - } catch (error) { - console.log(`[DLE Modules] Ошибка при проверке инициализации модулей:`, error.message); - } - - if (!modulesInitialized) { - console.log(`[DLE Modules] Модули для DLE ${dleAddress} не инициализированы`); - return res.json({ - success: true, - data: { - modules: [], - modulesInitialized: false, - totalModules: 0, - activeModules: 0 - } - }); - } - - // Список стандартных модулей DLE - const standardModuleIds = [ - "0x7472656173757279000000000000000000000000000000000000000000000000", // "treasury" - "0x74696d656c6f636b000000000000000000000000000000000000000000000000", // "timelock" - "0x6d696e7400000000000000000000000000000000000000000000000000000000", // "mint" - "0x6275726e00000000000000000000000000000000000000000000000000000000", // "burn" - "0x6f7261636c650000000000000000000000000000000000000000000000000000", // "oracle" - "0x696e6865726974616e6365000000000000000000000000000000000000000000", // "inheritance" - "0x636f6d6d756e69636174696f6e00000000000000000000000000000000000000", // "communication" - "0x6170706c69636174696f6e000000000000000000000000000000000000000000" // "application" - ]; - - const modules = []; - - // Проверяем каждый модуль - for (const moduleId of standardModuleIds) { - try { - const isActive = await dle.isModuleActive(moduleId); - if (isActive) { - const moduleAddress = await dle.getModuleAddress(moduleId); - - // Получаем человекочитаемое название модуля - const moduleName = getModuleName(moduleId); - const moduleDescription = getModuleDescription(moduleId); - - modules.push({ - moduleId: moduleId, - moduleName: moduleName, - moduleDescription: moduleDescription, - moduleAddress: moduleAddress, - isActive: isActive, - deployedAt: new Date().toISOString() // В реальности нужно брать из событий - }); - - console.log(`[DLE Modules] Найден активный модуль: ${moduleName} по адресу ${moduleAddress}`); - } - } catch (error) { - console.log(`[DLE Modules] Ошибка при проверке модуля ${getModuleName(moduleId)}:`, error.message); - } - } - - console.log(`[DLE Modules] Всего найдено активных модулей в блокчейне: ${modules.length}`); - - res.json({ - success: true, - data: { - modules: modules, - modulesInitialized: modulesInitialized, - totalModules: standardModuleIds.length, - activeModules: modules.length + modules: [], + modulesInitialized: false, + totalModules: 0, + activeModules: 0, + supportedNetworks: [] } }); } + // Группируем модули по типам + const moduleGroups = { + treasury: { + moduleId: "0x7472656173757279000000000000000000000000000000000000000000000000", // 32 байта + moduleName: "TREASURY", + moduleDescription: "Казначейство DLE - управление финансами, депозиты, выводы, дивиденды", + addresses: [], + isActive: true, + deployedAt: new Date().toISOString() + }, + timelock: { + moduleId: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", // 32 байта + moduleName: "TIMELOCK", + moduleDescription: "Модуль задержек исполнения - безопасность критических операций через таймлоки", + addresses: [], + isActive: true, + deployedAt: new Date().toISOString() + }, + reader: { + moduleId: "0x7265616465720000000000000000000000000000000000000000000000000000", // 32 байта + moduleName: "READER", + moduleDescription: "Модуль чтения данных DLE - получение информации о контракте", + addresses: [], + isActive: true, + deployedAt: new Date().toISOString() + } + }; + + // Проверяем модули в каждой поддерживаемой сети + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Проверяем модули в сети: ${network.networkName} (${network.chainId})`); + + try { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + + const dleAbi = [ + "function isModuleActive(bytes32 _moduleId) external view returns (bool)", + "function getModuleAddress(bytes32 _moduleId) external view returns (address)", + "function modulesInitialized() external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Проверяем инициализацию модулей + let modulesInitialized = false; + try { + modulesInitialized = await dle.modulesInitialized(); + } catch (error) { + console.log(`[DLE Modules] Ошибка при проверке инициализации модулей в сети ${network.chainId}:`, error.message); + continue; + } + + if (!modulesInitialized) { + console.log(`[DLE Modules] Модули не инициализированы в сети ${network.chainId}, но проверяем отдельные модули`); + } + + // Проверяем каждый тип модуля + for (const [moduleType, moduleInfo] of Object.entries(moduleGroups)) { + try { + console.log(`[DLE Modules] Проверяем модуль ${moduleInfo.moduleName} (${moduleInfo.moduleId}) в сети ${network.networkName}`); + const isActive = await dle.isModuleActive(moduleInfo.moduleId); + console.log(`[DLE Modules] Модуль ${moduleInfo.moduleName} активен: ${isActive}`); + if (isActive) { + const moduleAddress = await dle.getModuleAddress(moduleInfo.moduleId); + + // Проверяем, не добавлен ли уже этот адрес для этого типа модуля + const existingAddress = moduleInfo.addresses.find(addr => + addr.address.toLowerCase() === moduleAddress.toLowerCase() + ); + + if (!existingAddress) { + moduleInfo.addresses.push({ + address: moduleAddress, + networkName: network.networkName, + networkIndex: supportedNetworks.indexOf(network), + chainId: Number(network.chainId), // Конвертируем BigInt в Number + verificationStatus: 'pending' // По умолчанию pending, можно проверить через Etherscan API + }); + + console.log(`[DLE Modules] Найден модуль ${moduleInfo.moduleName} в сети ${network.networkName}: ${moduleAddress}`); + } + } + } catch (error) { + console.log(`[DLE Modules] Ошибка при проверке модуля ${moduleInfo.moduleName} в сети ${network.chainId}:`, error.message); + } + } + } catch (error) { + console.log(`[DLE Modules] Ошибка при подключении к сети ${network.chainId}:`, error.message); + } + } + + // Преобразуем в массив модулей + const formattedModules = Object.values(moduleGroups).filter(module => module.addresses.length > 0); + + console.log(`[DLE Modules] Найдено типов модулей: ${formattedModules.length}`); + + res.json({ + success: true, + data: { + modules: formattedModules, + modulesInitialized: formattedModules.length > 0, + totalModules: formattedModules.length, + activeModules: formattedModules.length, + supportedNetworks: supportedNetworks + } + }); + } catch (error) { console.error('[DLE Modules] Ошибка при получении всех модулей:', error); res.status(500).json({ @@ -381,11 +598,11 @@ router.post('/create-add-module-proposal', async (req, res) => { console.log(`[DLE Modules] Создание предложения о добавлении модуля: ${moduleId} для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); if (!rpcUrl) { return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: `RPC URL для сети ${chainId} не найден` }); } @@ -397,17 +614,25 @@ router.post('/create-add-module-proposal', async (req, res) => { const dle = new ethers.Contract(dleAddress, dleAbi, provider); - // Создаем предложение - const tx = await dle.createAddModuleProposal(description, duration, moduleId, moduleAddress, chainId); - const receipt = await tx.wait(); + // Подготавливаем данные для транзакции (не отправляем) + const txData = await dle.createAddModuleProposal.populateTransaction( + description, + duration, + moduleId, + moduleAddress, + chainId + ); - console.log(`[DLE Modules] Предложение о добавлении модуля создано:`, receipt); + console.log(`[DLE Modules] Данные транзакции подготовлены:`, txData); res.json({ success: true, data: { - proposalId: receipt.logs[0].args.proposalId, - transactionHash: receipt.hash + to: dleAddress, + data: txData.data, + value: "0x0", + gasLimit: "0x1e8480", // 2,000,000 gas + message: "Подготовлены данные для создания предложения о добавлении модуля. Отправьте транзакцию через MetaMask." } }); @@ -473,55 +698,115 @@ router.post('/create-remove-module-proposal', async (req, res) => { } }); -// Функция для получения информации о задеплоенных модулях -async function getDeployedModulesInfo(dleAddress) { - const fs = require('fs'); - const path = require('path'); - +// Функция для получения поддерживаемых сетей из DLE контракта +async function getSupportedNetworksFromDLE(dleAddress) { try { - // Ищем файл модулей в правильной директории - const tempDir = path.join(__dirname, '../scripts/temp'); - const modulesFileName = `modules-${dleAddress.toLowerCase()}.json`; - const modulesFilePath = path.join(tempDir, modulesFileName); - - if (!fs.existsSync(modulesFilePath)) { - console.log(`[DLE Modules] Файл модулей не найден: ${modulesFileName}`); - return { modules: [], verification: {} }; + // Получаем все доступные RPC провайдеры из базы данных + const allRpcProviders = await rpcProviderService.getAllRpcProviders(); + console.log(`[DLE Modules] Найдено RPC провайдеров в базе данных: ${allRpcProviders.length}`); + + if (allRpcProviders.length === 0) { + console.log(`[DLE Modules] RPC провайдеры не найдены в базе данных`); + return []; } + + // Пробуем подключиться к каждой сети, чтобы найти DLE контракт + const supportedNetworks = []; - try { - const data = JSON.parse(fs.readFileSync(modulesFilePath, 'utf8')); - console.log(`[DLE Modules] Загружена информация о модулях для DLE: ${dleAddress}`); - - // Получаем статус верификации для каждого модуля - const modulesWithVerification = []; - - if (data.modules && Array.isArray(data.modules)) { - for (const networkModules of data.modules) { - if (networkModules.treasuryModule) { - modulesWithVerification.push({ - ...networkModules, - verificationStatus: data.verification?.[0]?.treasuryModule || 'pending' - }); + for (const rpcProvider of allRpcProviders) { + try { + console.log(`[DLE Modules] Проверяем сеть: ${rpcProvider.network_name} (${rpcProvider.chain_id})`); + + const provider = new ethers.JsonRpcProvider(rpcProvider.rpc_url); + + const dleAbi = [ + "function getSupportedChainCount() external view returns (uint256)", + "function getSupportedChainId(uint256 _index) external view returns (uint256)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Проверяем, существует ли контракт в этой сети + const code = await provider.getCode(dleAddress); + if (code === '0x') { + console.log(`[DLE Modules] DLE контракт не найден в сети ${rpcProvider.chain_id}`); + continue; + } + + // Получаем количество поддерживаемых сетей с проверкой + let chainCount; + try { + chainCount = await dle.getSupportedChainCount(); + console.log(`[DLE Modules] DLE в сети ${rpcProvider.chain_id} поддерживает ${chainCount} сетей`); + } catch (error) { + console.log(`[DLE Modules] Ошибка получения chainCount в сети ${rpcProvider.chain_id}: ${error.message}`); + // Если функция не работает, проверяем, что контракт инициализирован + try { + // Проверяем что контракт развернут (код не пустой) + const code = await provider.getCode(dleAddress); + if (code === '0x' || code.length <= 2) { + throw new Error('Контракт не развернут'); + } + console.log(`[DLE Modules] Контракт инициализирован, но getSupportedChainCount() не работает`); + // Если контракт инициализирован, но getSupportedChainCount() падает, + // это означает, что supportedChainIds пустой - используем 0 + chainCount = 0; + console.log(`[DLE Modules] supportedChainIds пустой, используем chainCount = 0`); + } catch (initError) { + console.log(`[DLE Modules] Контракт не инициализирован: ${initError.message}`); + // Пропускаем эту сеть + continue; } } + + // Получаем chainId для каждой поддерживаемой сети + for (let i = 0; i < chainCount; i++) { + try { + const chainId = await dle.getSupportedChainId(i); + + // Получаем RPC URL для этой сети из базы данных + const networkRpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + + if (networkRpcUrl) { + const networkName = getNetworkNameByChainId(chainId); + + // Проверяем, не добавлена ли уже эта сеть + const existingNetwork = supportedNetworks.find(n => n.chainId === chainId); + if (!existingNetwork) { + const etherscanUrl = getEtherscanUrlByChainId(chainId); + + supportedNetworks.push({ + chainId: Number(chainId), // Конвертируем BigInt в Number + networkName: networkName, + rpcUrl: networkRpcUrl, + etherscanUrl: etherscanUrl, + networkIndex: i + }); + + console.log(`[DLE Modules] Найдена поддерживаемая сеть: ${networkName} (${chainId})`); + } + } else { + console.log(`[DLE Modules] RPC URL не найден для сети ${chainId}`); + } + } catch (error) { + console.log(`[DLE Modules] Ошибка при получении chainId для индекса ${i}:`, error.message); + } + } + + // Если нашли поддерживаемые сети, можем остановиться + if (supportedNetworks.length > 0) { + break; + } + + } catch (error) { + console.log(`[DLE Modules] Ошибка при подключении к сети ${rpcProvider.chain_id}:`, error.message); } - - return { - modules: data.modules || [], - verification: data.verification || {}, - deployTimestamp: data.deployTimestamp, - networks: data.networks || [], // ✅ Добавляем поле networks - modulesWithVerification: modulesWithVerification - }; - } catch (error) { - console.error(`Ошибка при чтении файла ${modulesFileName}:`, error); - return { modules: [], verification: {} }; } - + + return supportedNetworks; } catch (error) { - console.error('Ошибка при получении информации о модулях:', error); - return { modules: [], verification: {} }; + console.error('[DLE Modules] Ошибка при получении поддерживаемых сетей:', error); + return []; } } @@ -540,95 +825,42 @@ function getNetworkNameByChainId(chainId) { 10: 'Optimism', 11155420: 'Optimism Sepolia', 8453: 'Base', - 84532: 'Base Sepolia' + 84532: 'Base Sepolia', + 17000: 'Holesky' }; return networkNames[chainId] || `Chain ID ${chainId}`; } -// Функция для получения модулей из файлов -async function getModulesFromFiles(dleAddress) { - const fs = require('fs'); - const path = require('path'); +// Функция для определения Etherscan URL по chainId +function getEtherscanUrlByChainId(chainId) { + const etherscanUrls = { + 1: 'https://etherscan.io', + 5: 'https://goerli.etherscan.io', + 11155111: 'https://sepolia.etherscan.io', + 137: 'https://polygonscan.com', + 80001: 'https://mumbai.polygonscan.com', + 56: 'https://bscscan.com', + 97: 'https://testnet.bscscan.com', + 42161: 'https://arbiscan.io', + 421614: 'https://sepolia.arbiscan.io', + 10: 'https://optimistic.etherscan.io', + 11155420: 'https://sepolia-optimism.etherscan.io', + 8453: 'https://basescan.org', + 84532: 'https://sepolia.basescan.org', + 17000: 'https://holesky.etherscan.io' + }; - try { - // Ищем файл модулей в правильной директории - const tempDir = path.join(__dirname, '../scripts/temp'); - const modulesFileName = `modules-${dleAddress.toLowerCase()}.json`; - const modulesFilePath = path.join(tempDir, modulesFileName); - - if (!fs.existsSync(modulesFilePath)) { - console.log(`[DLE Modules] Файл модулей не найден: ${modulesFileName}`); - return []; - } - - try { - const data = JSON.parse(fs.readFileSync(modulesFilePath, 'utf8')); - console.log(`[DLE Modules] Загружена информация о модулях для DLE: ${dleAddress}`); - - const modules = []; - - // Обрабатываем модули из файла - if (data.modules && Array.isArray(data.modules)) { - for (const networkModules of data.modules) { - if (networkModules.treasuryModule) { - modules.push({ - moduleId: "0x7472656173757279000000000000000000000000000000000000000000000000", - moduleName: "TREASURY", - moduleDescription: "Казначейство DLE - управление финансами, депозиты, выводы, дивиденды", - moduleAddress: networkModules.treasuryModule, - isActive: true, - deployedAt: data.deployTimestamp - }); - } - - if (networkModules.timelockModule) { - modules.push({ - moduleId: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", - moduleName: "TIMELOCK", - moduleDescription: "Модуль задержек исполнения - безопасность критических операций через таймлоки", - moduleAddress: networkModules.timelockModule, - isActive: true, - deployedAt: data.deployTimestamp - }); - } - - if (networkModules.dleReader) { - modules.push({ - moduleId: "0x726561646572000000000000000000000000000000000000000000000000000000", - moduleName: "READER", - moduleDescription: "Модуль чтения данных DLE - получение информации о контракте", - moduleAddress: networkModules.dleReader, - isActive: true, - deployedAt: data.deployTimestamp - }); - } - } - } - - // Убираем дубликаты (модули могут быть в нескольких сетях) - const uniqueModules = modules.filter((module, index, self) => - index === self.findIndex(m => m.moduleId === module.moduleId) - ); - - return uniqueModules; - - } catch (error) { - console.error(`Ошибка при чтении файла ${modulesFileName}:`, error); - return []; - } - - } catch (error) { - console.error('Ошибка при получении информации о модулях из файлов:', error); - return []; - } + return etherscanUrls[chainId] || 'https://etherscan.io'; } + // Вспомогательные функции для получения названий и описаний модулей function getModuleName(moduleId) { const moduleNames = { "0x7472656173757279000000000000000000000000000000000000000000000000": "TREASURY", "0x74696d656c6f636b000000000000000000000000000000000000000000000000": "TIMELOCK", + "0x7265616465720000000000000000000000000000000000000000000000000000": "READER", "0x6d696e7400000000000000000000000000000000000000000000000000000000": "MINT", "0x6275726e00000000000000000000000000000000000000000000000000000000": "BURN", "0x6f7261636c650000000000000000000000000000000000000000000000000000": "ORACLE", @@ -643,6 +875,7 @@ function getModuleDescription(moduleId) { const moduleDescriptions = { "0x7472656173757279000000000000000000000000000000000000000000000000": "Казначейство DLE - управление финансами, депозиты, выводы, дивиденды", "0x74696d656c6f636b000000000000000000000000000000000000000000000000": "Модуль задержек исполнения - безопасность критических операций через таймлоки", + "0x7265616465720000000000000000000000000000000000000000000000000000": "Модуль чтения данных DLE - получение информации о контракте", "0x6d696e7400000000000000000000000000000000000000000000000000000000": "Модуль выпуска токенов - создание дополнительных токенов DLE через governance", "0x6275726e00000000000000000000000000000000000000000000000000000000": "Модуль сжигания токенов - уменьшение общего предложения токенов DLE", "0x6f7261636c650000000000000000000000000000000000000000000000000000": "Модуль оракулов - получение внешних данных для автоматизации DLE", @@ -678,8 +911,15 @@ router.post('/verify-module', async (req, res) => { }); } - // Определяем chainId (Sepolia) - const chainId = 11155111; // Sepolia testnet + // Получаем chainId из параметров запроса + const { chainId } = req.body; + + if (!chainId) { + return res.status(400).json({ + success: false, + error: 'chainId обязателен для верификации' + }); + } // Определяем имя контракта на основе moduleId let contractName; @@ -687,7 +927,7 @@ router.post('/verify-module', async (req, res) => { contractName = "TreasuryModule"; } else if (moduleId === "0x74696d656c6f636b000000000000000000000000000000000000000000000000") { contractName = "TimelockModule"; - } else if (moduleId === "0x726561646572000000000000000000000000000000000000000000000000000000") { + } else if (moduleId === "0x7265616465720000000000000000000000000000000000000000000000000000") { contractName = "DLEReader"; } else { contractName = "UnknownModule"; @@ -739,9 +979,6 @@ router.post('/verify-module', async (req, res) => { console.log(`[DLE Modules] Результат верификации:`, verificationResult); - // Обновляем статус верификации в файле модулей - await updateModuleVerificationStatus(dleAddress, moduleId, 'success'); - res.json({ success: true, data: { @@ -755,11 +992,6 @@ router.post('/verify-module', async (req, res) => { } catch (error) { console.error('[DLE Modules] Ошибка верификации модуля:', error); - // Обновляем статус верификации в файле модулей - if (req.body.dleAddress && req.body.moduleId) { - await updateModuleVerificationStatus(req.body.dleAddress, req.body.moduleId, 'failed'); - } - res.status(500).json({ success: false, error: 'Ошибка верификации модуля: ' + error.message @@ -767,44 +999,133 @@ router.post('/verify-module', async (req, res) => { } }); -// Функция для обновления статуса верификации в файле модулей -async function updateModuleVerificationStatus(dleAddress, moduleId, status) { +// Получить информацию о поддерживаемых сетях +router.post('/get-networks-info', async (req, res) => { try { - const fs = require('fs'); - const path = require('path'); + const { dleAddress } = req.body; - const tempDir = path.join(__dirname, '../scripts/temp'); - const modulesFileName = `modules-${dleAddress.toLowerCase()}.json`; - const modulesFilePath = path.join(tempDir, modulesFileName); - - if (!fs.existsSync(modulesFilePath)) { - console.log(`[DLE Modules] Файл модулей не найден для обновления статуса: ${modulesFileName}`); - return; + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); } + + console.log(`[DLE Modules] Получение информации о сетях для DLE: ${dleAddress}`); + + // Получаем информацию о поддерживаемых сетях из DLE контракта + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + res.json({ + success: true, + data: { + networks: supportedNetworks, + totalNetworks: supportedNetworks.length + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка при получении информации о сетях:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении информации о сетях: ' + error.message + }); + } +}); + +// Проверить статус инициализации модулей +router.post('/check-modules-status', async (req, res) => { + try { + const { dleAddress } = req.body; - const data = JSON.parse(fs.readFileSync(modulesFilePath, 'utf8')); + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); + } + + console.log(`[DLE Modules] Проверка статуса модулей для DLE: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); - // Обновляем статус верификации для всех сетей - if (data.verification && Array.isArray(data.verification)) { - for (let i = 0; i < data.verification.length; i++) { - if (moduleId === "0x7472656173757279000000000000000000000000000000000000000000000000") { - data.verification[i].treasuryModule = status; - } else if (moduleId === "0x74696d656c6f636b000000000000000000000000000000000000000000000000") { - data.verification[i].timelockModule = status; - } else if (moduleId === "0x726561646572000000000000000000000000000000000000000000000000000000") { - data.verification[i].dleReader = status; + if (supportedNetworks.length === 0) { + return res.json({ + success: true, + data: { + modulesInitialized: false, + initializer: null, + modules: [], + networks: [] } + }); + } + + // Проверяем первую доступную сеть + const network = supportedNetworks[0]; + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + + 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)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Проверяем статус инициализации + const modulesInitialized = await dle.modulesInitialized(); + const initializer = await dle.initializer(); + + // Проверяем модули + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const modules = []; + for (const [name, moduleId] of Object.entries(moduleIds)) { + try { + const isActive = await dle.isModuleActive(moduleId); + const address = await dle.getModuleAddress(moduleId); + modules.push({ + name: name.toUpperCase(), + moduleId: moduleId, + isActive: isActive, + address: address + }); + } catch (error) { + modules.push({ + name: name.toUpperCase(), + moduleId: moduleId, + isActive: false, + address: null, + error: error.message + }); } } - - // Записываем обновленные данные обратно в файл - fs.writeFileSync(modulesFilePath, JSON.stringify(data, null, 2)); - console.log(`[DLE Modules] Статус верификации обновлен для модуля ${moduleId}: ${status}`); - + + res.json({ + success: true, + data: { + modulesInitialized: modulesInitialized, + initializer: initializer, + modules: modules, + networks: supportedNetworks + } + }); + } catch (error) { - console.error('[DLE Modules] Ошибка обновления статуса верификации:', error); + console.error('[DLE Modules] Ошибка при проверке статуса модулей:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при проверке статуса модулей: ' + error.message + }); } -} +}); // Функция для создания стандартного JSON input для верификации async function createStandardJsonInput(contractName, moduleAddress, dleAddress, chainId) { @@ -925,4 +1246,1723 @@ async function createStandardJsonInput(contractName, moduleAddress, dleAddress, } } +// Мультиинициализация модулей во всех сетях +router.post('/initialize-modules-all-networks', async (req, res) => { + try { + const { dleAddress, privateKey } = req.body; + + if (!dleAddress || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Мультиинициализация модулей для DLE: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + const results = []; + const dleAbi = [ + "function initializeBaseModules(address _treasuryAddress, address _timelockAddress, address _readerAddress) external", + "function modulesInitialized() external view returns (bool)", + "function getModuleAddress(bytes32 _moduleId) external view returns (address)" + ]; + + // ID модулей + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Инициализация модулей в сети: ${network.networkName} (${network.chainId})`); + + try { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + const dle = new ethers.Contract(dleAddress, dleAbi, wallet); + + // Проверяем, уже ли инициализированы модули + const modulesInitialized = await dle.modulesInitialized(); + + if (modulesInitialized) { + console.log(`[DLE Modules] Модули уже инициализированы в сети ${network.chainId}`); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'already_initialized', + message: 'Модули уже инициализированы' + }); + continue; + } + + // Получаем адреса модулей + const treasuryAddress = await dle.getModuleAddress(moduleIds.treasury); + const timelockAddress = await dle.getModuleAddress(moduleIds.timelock); + const readerAddress = await dle.getModuleAddress(moduleIds.reader); + + // Проверяем, что все модули задеплоены + if (treasuryAddress === "0x0000000000000000000000000000000000000000" || + timelockAddress === "0x0000000000000000000000000000000000000000" || + readerAddress === "0x0000000000000000000000000000000000000000") { + console.log(`[DLE Modules] Не все модули задеплоены в сети ${network.chainId}`); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'modules_not_deployed', + message: 'Не все модули задеплоены' + }); + continue; + } + + // Инициализируем модули + const tx = await dle.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress); + await tx.wait(); + + console.log(`[DLE Modules] Модули успешно инициализированы в сети ${network.chainId}`); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'success', + message: 'Модули успешно инициализированы', + transactionHash: tx.hash + }); + + } catch (error) { + console.error(`[DLE Modules] Ошибка инициализации модулей в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message + }); + } + } + + res.json({ + success: true, + data: { + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + already_initialized: results.filter(r => r.status === 'already_initialized').length, + errors: results.filter(r => r.status === 'error').length + } + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка мультиинициализации модулей:', error); + res.status(500).json({ + success: false, + error: 'Ошибка мультиинициализации модулей: ' + error.message + }); + } +}); + +// Мультиверификация модулей во всех сетях +router.post('/verify-modules-all-networks', async (req, res) => { + try { + const { dleAddress, privateKey } = req.body; + + if (!dleAddress || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Мультиверификация модулей для DLE: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + const results = []; + const dleAbi = [ + "function getModuleAddress(bytes32 _moduleId) external view returns (address)" + ]; + + // ID модулей + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + // Маппинг модулей для верификации + const moduleTypes = { + treasury: 'TreasuryModule', + timelock: 'TimelockModule', + reader: 'DLEReader' + }; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Верификация модулей в сети: ${network.networkName} (${network.chainId})`); + + const networkResults = { + chainId: network.chainId, + networkName: network.networkName, + modules: {} + }; + + try { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + for (const [moduleKey, moduleId] of Object.entries(moduleIds)) { + try { + const moduleAddress = await dle.getModuleAddress(moduleId); + + if (moduleAddress === "0x0000000000000000000000000000000000000000") { + networkResults.modules[moduleKey] = { + status: 'not_deployed', + message: 'Модуль не задеплоен' + }; + continue; + } + + // Верифицируем модуль + const verificationResult = await verifyModuleInNetwork( + moduleTypes[moduleKey], + moduleAddress, + dleAddress, + network.chainId, + network.networkName + ); + + networkResults.modules[moduleKey] = verificationResult; + + } catch (error) { + console.error(`[DLE Modules] Ошибка верификации модуля ${moduleKey} в сети ${network.chainId}:`, error.message); + networkResults.modules[moduleKey] = { + status: 'error', + message: error.message + }; + } + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка подключения к сети ${network.chainId}:`, error.message); + networkResults.error = error.message; + } + + results.push(networkResults); + } + + res.json({ + success: true, + data: { + results: results, + summary: { + total_networks: results.length, + total_modules: results.length * 3, // 3 модуля на сеть + success_count: results.reduce((sum, r) => + sum + Object.values(r.modules || {}).filter(m => m.status === 'success').length, 0), + error_count: results.reduce((sum, r) => + sum + Object.values(r.modules || {}).filter(m => m.status === 'error').length, 0) + } + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка мультиверификации модулей:', error); + res.status(500).json({ + success: false, + error: 'Ошибка мультиверификации модулей: ' + error.message + }); + } +}); + +// Вспомогательная функция для верификации модуля в конкретной сети +async function verifyModuleInNetwork(contractName, moduleAddress, dleAddress, chainId, networkName) { + try { + console.log(`[DLE Modules] Верификация ${contractName} в сети ${networkName} (${chainId})`); + + // Получаем Etherscan URL для сети + const etherscanUrl = await getEtherscanUrlByChainId(chainId); + if (!etherscanUrl) { + return { + status: 'error', + message: `Etherscan не поддерживается для сети ${networkName}` + }; + } + + // Получаем API ключ Etherscan из секретов + const { getSecret } = require('../services/secretStore'); + const apiKey = await getSecret('ETHERSCAN_V2_API_KEY'); + + if (!apiKey) { + return { + status: 'error', + message: 'API ключ Etherscan не найден в секретах' + }; + } + + // Создаем стандартный JSON input для верификации + const standardJsonInput = await createStandardJsonInput(contractName, moduleAddress, dleAddress, chainId); + + // Отправляем запрос на верификацию + const verificationResponse = await fetch(`${etherscanUrl}/api`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'verifysourcecode', + contractaddress: moduleAddress, + codeformat: 'solidity-standard-json-input', + contractname: contractName, + sourceCode: JSON.stringify(standardJsonInput), + compilerversion: 'v0.8.20+commit.a1b79de6', + optimizationUsed: '1', + runs: '1' + }) + }); + + const verificationData = await verificationResponse.json(); + + if (verificationData.status === '1') { + console.log(`[DLE Modules] ${contractName} успешно верифицирован в сети ${networkName}`); + return { + status: 'success', + message: 'Модуль успешно верифицирован', + guid: verificationData.result + }; + } else { + console.log(`[DLE Modules] Ошибка верификации ${contractName} в сети ${networkName}: ${verificationData.result}`); + return { + status: 'failed', + message: verificationData.result || 'Ошибка верификации' + }; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка верификации ${contractName} в сети ${networkName}:`, error); + return { + status: 'error', + message: error.message + }; + } +} + +// Проверка статуса деплоя DLE контракта во всех сетях +router.post('/check-dle-deployment-status', async (req, res) => { + try { + const { dleAddress, chainIds, maxRetries = 1, retryDelay = 30000 } = req.body; + + if (!dleAddress || !chainIds || !Array.isArray(chainIds)) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE и список chainIds обязательны' + }); + } + + console.log(`[DLE Modules] Проверка статуса деплоя DLE: ${dleAddress} в сетях: ${chainIds.join(', ')}`); + + const results = []; + let allSuccessful = true; + + for (const chainId of chainIds) { + console.log(`[DLE Modules] Проверка DLE в сети: ${chainId}`); + + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + if (!rpcUrl) { + throw new Error(`RPC URL не найден для сети ${chainId}`); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // Проверяем, что контракт существует и имеет код + const code = await provider.getCode(dleAddress); + + if (code === '0x') { + return { + chainId: chainId, + status: 'not_deployed', + message: 'Контракт не задеплоен' + }; + } else { + // Проверяем, что это действительно DLE контракт + const dleAbi = [ + "function name() external view returns (string)", + "function symbol() external view returns (string)", + "function modulesInitialized() external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + // Проверяем что контракт развернут (код не пустой) + const code = await provider.getCode(dleAddress); + if (code === '0x' || code.length <= 2) { + throw new Error('Контракт не развернут'); + } + const name = 'DLE'; // Используем фиксированное имя + const symbol = 'DLE'; // Используем фиксированный символ + + return { + chainId: chainId, + status: 'success', + message: 'DLE контракт успешно задеплоен', + contractInfo: { + name: name, + symbol: symbol, + hasCode: true + } + }; + } + }, + `Проверка DLE в сети ${chainId}`, + maxRetries, + retryDelay + ); + + results.push(result); + + if (result.status !== 'success') { + allSuccessful = false; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка при проверке DLE в сети ${chainId}:`, error.message); + results.push({ + chainId: chainId, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + not_deployed: results.filter(r => r.status === 'not_deployed').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'verify_dle' : 'retry_check' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка проверки статуса деплоя DLE:', error); + res.status(500).json({ + success: false, + error: 'Ошибка проверки статуса деплоя DLE: ' + error.message + }); + } +}); + +// Проверка статуса деплоя конкретного модуля во всех сетях +router.post('/check-module-deployment-status', async (req, res) => { + try { + const { dleAddress, moduleType, chainIds, maxRetries = 1, retryDelay = 30000 } = req.body; + + if (!dleAddress || !moduleType || !chainIds || !Array.isArray(chainIds)) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE, тип модуля и список chainIds обязательны' + }); + } + + console.log(`[DLE Modules] Проверка статуса деплоя модуля ${moduleType} для DLE: ${dleAddress} в сетях: ${chainIds.join(', ')}`); + + // Маппинг типов модулей на их ID + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const moduleId = moduleIds[moduleType]; + if (!moduleId) { + return res.status(400).json({ + success: false, + error: `Неизвестный тип модуля: ${moduleType}. Поддерживаемые типы: ${Object.keys(moduleIds).join(', ')}` + }); + } + + const results = []; + let allSuccessful = true; + + for (const chainId of chainIds) { + console.log(`[DLE Modules] Проверка модуля ${moduleType} в сети: ${chainId}`); + + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + if (!rpcUrl) { + throw new Error(`RPC URL не найден для сети ${chainId}`); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + const dleAbi = [ + "function getModuleAddress(bytes32 _moduleId) external view returns (address)", + "function isModuleActive(bytes32 _moduleId) external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Получаем адрес модуля + const moduleAddress = await dle.getModuleAddress(moduleId); + + if (moduleAddress === "0x0000000000000000000000000000000000000000") { + return { + chainId: chainId, + status: 'not_deployed', + message: `Модуль ${moduleType} не задеплоен` + }; + } else { + // Проверяем, что контракт модуля существует и имеет код + const code = await provider.getCode(moduleAddress); + + if (code === '0x') { + throw new Error(`Адрес модуля найден, но контракт не задеплоен: ${moduleAddress}`); + } else { + // Проверяем активность модуля + const isActive = await dle.isModuleActive(moduleId); + + return { + chainId: chainId, + status: 'success', + message: `Модуль ${moduleType} успешно задеплоен`, + moduleInfo: { + address: moduleAddress, + isActive: isActive, + hasCode: true + } + }; + } + } + }, + `Проверка модуля ${moduleType} в сети ${chainId}`, + maxRetries, + retryDelay + ); + + results.push(result); + + if (result.status !== 'success') { + allSuccessful = false; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка при проверке модуля ${moduleType} в сети ${chainId}:`, error.message); + results.push({ + chainId: chainId, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + moduleType: moduleType, + moduleId: moduleId, + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + not_deployed: results.filter(r => r.status === 'not_deployed').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'verify_module' : 'retry_check' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка проверки статуса деплоя модуля:', error); + res.status(500).json({ + success: false, + error: 'Ошибка проверки статуса деплоя модуля: ' + error.message + }); + } +}); + +// Деплой одного модуля во всех сетях +router.post('/deploy-module-all-networks', async (req, res) => { + try { + const { dleAddress, moduleType, privateKey, maxRetries = 1, retryDelay = 45000 } = req.body; + + if (!dleAddress || !moduleType || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE, тип модуля и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Деплой модуля ${moduleType} для DLE: ${dleAddress}`); + + // Автоматическая компиляция контрактов перед деплоем + try { + await autoCompileContracts(); + } catch (compileError) { + console.error(`[DLE Modules] Ошибка при компиляции контрактов:`, compileError.message); + return res.status(500).json({ + success: false, + error: `Ошибка компиляции контрактов: ${compileError.message}` + }); + } + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + // Маппинг типов модулей на их конфигурацию + const moduleConfigs = { + treasury: { + contractName: 'TreasuryModule', + constructorArgs: (dleAddress, chainId, deployerAddress) => [dleAddress, chainId, deployerAddress] + }, + timelock: { + contractName: 'TimelockModule', + constructorArgs: (dleAddress) => [dleAddress] + }, + reader: { + contractName: 'DLEReader', + constructorArgs: (dleAddress) => [dleAddress] + } + }; + + const moduleConfig = moduleConfigs[moduleType]; + if (!moduleConfig) { + return res.status(400).json({ + success: false, + error: `Неизвестный тип модуля: ${moduleType}. Поддерживаемые типы: ${Object.keys(moduleConfigs).join(', ')}` + }); + } + + const results = []; + let allSuccessful = true; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Деплой модуля ${moduleType} в сети: ${network.networkName} (${network.chainId})`); + + try { + const result = await executeWithRetries( + async () => { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + + // Получаем текущий nonce + const currentNonce = await wallet.getNonce(); + console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`); + + // Получаем фабрику контракта + const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName); + + // Подготавливаем аргументы конструктора + const constructorArgs = moduleConfig.constructorArgs(dleAddress, network.chainId, wallet.address); + + console.log(`[DLE Modules] Деплой ${moduleConfig.contractName} с аргументами:`, constructorArgs); + + // Деплоим контракт + const contract = await ContractFactory.connect(wallet).deploy(...constructorArgs); + await contract.waitForDeployment(); + + const deployedAddress = await contract.getAddress(); + console.log(`[DLE Modules] Модуль ${moduleType} задеплоен в сети ${network.chainId}: ${deployedAddress}`); + + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'success', + message: `Модуль ${moduleType} успешно задеплоен`, + moduleAddress: deployedAddress, + transactionHash: contract.deploymentTransaction().hash, + nonce: currentNonce + }; + }, + `Деплой модуля ${moduleType} в сети ${network.networkName} (${network.chainId})`, + maxRetries, + retryDelay + ); + + results.push(result); + + } catch (error) { + console.error(`[DLE Modules] Ошибка деплоя модуля ${moduleType} в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + moduleType: moduleType, + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'check_module_deployment' : 'retry_deployment' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка деплоя модуля:', error); + res.status(500).json({ + success: false, + error: 'Ошибка деплоя модуля: ' + error.message + }); + } +}); + +// Верификация DLE контракта во всех сетях +router.post('/verify-dle-all-networks', async (req, res) => { + try { + const { dleAddress, privateKey, maxRetries = 1, retryDelay = 60000 } = req.body; + + if (!dleAddress || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Верификация DLE контракта: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + const results = []; + let allSuccessful = true; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Верификация DLE в сети: ${network.networkName} (${network.chainId})`); + + try { + const result = await executeWithRetries( + async () => { + // Получаем Etherscan URL для сети + const etherscanUrl = await getEtherscanUrlByChainId(network.chainId); + if (!etherscanUrl) { + throw new Error(`Etherscan не поддерживается для сети ${network.networkName}`); + } + + // Берем параметры из сохраненной карточки DLE и не делаем on-chain вызовы + const fs = require('fs'); + const path = require('path'); + const dlesDir = path.join(__dirname, '../contracts-data/dles'); + + let saved = null; + if (fs.existsSync(dlesDir)) { + for (const f of fs.readdirSync(dlesDir)) { + if (!f.endsWith('.json')) continue; + const data = JSON.parse(fs.readFileSync(path.join(dlesDir, f), 'utf8')); + if (data && data.dleAddress && data.dleAddress.toLowerCase() === dleAddress.toLowerCase()) { + saved = data; break; + } + } + } + + // Фолбэки если карточка не найдена + const name = saved?.name || 'DLE'; + const symbol = saved?.symbol || 'DLE'; + const location = saved?.location || ''; + const coordinates = saved?.coordinates || ''; + const jurisdiction = saved?.jurisdiction ?? 0; + const oktmo = saved?.oktmo || ''; + const kpp = saved?.kpp ? Number(saved.kpp) : 0; + const quorumPercentage = saved?.quorumPercentage ?? saved?.governanceSettings?.quorumPercentage ?? 51; + + // Инициализатор — адрес из переданного приватного ключа + let initializer; + try { + const pk = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`; + initializer = new ethers.Wallet(pk).address; + } catch (_) { + initializer = (saved?.initialPartners && saved.initialPartners[0]) || "0x0000000000000000000000000000000000000000"; + } + + // Список поддерживаемых сетей и текущая сеть + const supportedChainIds = Array.isArray(saved?.networks) + ? saved.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) + : (saved?.governanceSettings?.supportedChainIds || []); + const currentChainId = Number(saved?.governanceSettings?.currentChainId || network.chainId); + + // Создаем стандартный JSON input для верификации + const standardJsonInput = { + language: "Solidity", + sources: { + "DLE.sol": { + content: require('fs').readFileSync(require('path').join(__dirname, '../contracts/DLE.sol'), 'utf8') + } + }, + settings: { + optimizer: { + enabled: true, + runs: 1 + }, + viaIR: true, + outputSelection: { + "*": { + "*": ["*"] + } + } + } + }; + + // Получаем API ключ Etherscan из секретов + const { getSecret } = require('../services/secretStore'); + const apiKey = await getSecret('ETHERSCAN_V2_API_KEY'); + + if (!apiKey) { + throw new Error('API ключ Etherscan не найден в секретах'); + } + + // Отправляем запрос на верификацию согласно Etherscan V2 API + const verificationResponse = await fetch(`${etherscanUrl}/v2/api`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + chainid: network.chainId.toString(), + apikey: apiKey, + module: 'contract', + action: 'verifysourcecode', + contractaddress: dleAddress, + codeformat: 'solidity-standard-json-input', + contractname: 'DLE.sol:DLE', + sourceCode: JSON.stringify(standardJsonInput), + compilerversion: 'v0.8.20+commit.a1b79de6', + constructorArguements: (async () => { + try { + const fs = require('fs'); + const path = require('path'); + const dlesDir = path.join(__dirname, '../contracts-data/dles'); + let found = null; + if (fs.existsSync(dlesDir)) { + for (const f of fs.readdirSync(dlesDir)) { + if (!f.endsWith('.json')) continue; + const data = JSON.parse(fs.readFileSync(path.join(dlesDir, f), 'utf8')); + if (data && data.dleAddress && data.dleAddress.toLowerCase() === dleAddress.toLowerCase()) { + found = data; break; + } + } + } + const initPartners = Array.isArray(found?.initialPartners) ? found.initialPartners : []; + const initAmounts = Array.isArray(found?.initialAmounts) ? found.initialAmounts : []; + const scIds = Array.isArray(found?.networks) ? found.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) : supportedChainIds; + const currentCid = Number(found?.governanceSettings?.currentChainId || found?.networks?.[0]?.chainId || network.chainId); + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ['tuple(string,string,string,string,uint256,string,uint256,uint256,address[],uint256[],uint256[])', 'uint256', 'address'], + [[name, symbol, location, coordinates, jurisdiction, oktmo, kpp, quorumPercentage, initPartners, initAmounts.map(a => BigInt(a)), scIds], BigInt(currentCid), initializer] + ); + return encoded; + } catch (_) { + // Fallback на пустые массивы при отсутствии сохраненных параметров + return ethers.AbiCoder.defaultAbiCoder().encode( + ['tuple(string,string,string,string,uint256,string,uint256,uint256,address[],uint256[],uint256[])', 'uint256', 'address'], + [[name, symbol, location, coordinates, jurisdiction, oktmo, kpp, quorumPercentage, [], [], supportedChainIds], BigInt(network.chainId), initializer] + ); + } + })() + }) + }); + + const verificationData = await verificationResponse.json(); + + if (verificationData.status === '1') { + console.log(`[DLE Modules] DLE отправлен на верификацию в сети ${network.networkName}, GUID: ${verificationData.result}`); + + // Проверяем статус верификации согласно best practices + const guid = verificationData.result; + let verificationStatus = 'pending'; + let attempts = 0; + const maxStatusChecks = 10; + + while (verificationStatus === 'pending' && attempts < maxStatusChecks) { + await new Promise(resolve => setTimeout(resolve, 5000)); // Ждем 5 секунд + attempts++; + + try { + const statusResponse = await fetch(`${etherscanUrl}/v2/api?chainid=${network.chainId}&module=contract&action=checkverifystatus&guid=${guid}&apikey=${apiKey}`); + const statusData = await statusResponse.json(); + + if (statusData.status === '1') { + verificationStatus = 'success'; + console.log(`[DLE Modules] DLE успешно верифицирован в сети ${network.networkName}`); + } else if (statusData.result && statusData.result.includes('Fail')) { + verificationStatus = 'failed'; + throw new Error(`Верификация не удалась: ${statusData.result}`); + } + // Если статус все еще pending, продолжаем проверку + } catch (statusError) { + console.log(`[DLE Modules] Ошибка проверки статуса верификации (попытка ${attempts}): ${statusError.message}`); + } + } + + if (verificationStatus === 'success') { + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'success', + message: 'DLE контракт успешно верифицирован', + guid: guid, + etherscanUrl: `${etherscanUrl}/address/${dleAddress}` + }; + } else { + throw new Error('Верификация не завершена в течение ожидаемого времени'); + } + } else { + throw new Error(verificationData.result || 'Ошибка отправки на верификацию'); + } + }, + `Верификация DLE в сети ${network.networkName} (${network.chainId})`, + maxRetries, + retryDelay + ); + + results.push(result); + + } catch (error) { + console.error(`[DLE Modules] Ошибка верификации DLE в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + failed: results.filter(r => r.status === 'failed').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'deploy_treasury_module' : 'retry_verification' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка верификации DLE:', error); + res.status(500).json({ + success: false, + error: 'Ошибка верификации DLE: ' + error.message + }); + } +}); + +// Верификация одного модуля во всех сетях +router.post('/verify-module-all-networks', async (req, res) => { + try { + const { dleAddress, moduleType, privateKey, maxRetries = 1, retryDelay = 60000 } = req.body; + + if (!dleAddress || !moduleType || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE, тип модуля и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Верификация модуля ${moduleType} для DLE: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + // Маппинг типов модулей на их ID + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const moduleId = moduleIds[moduleType]; + if (!moduleId) { + return res.status(400).json({ + success: false, + error: `Неизвестный тип модуля: ${moduleType}. Поддерживаемые типы: ${Object.keys(moduleIds).join(', ')}` + }); + } + + const results = []; + let allSuccessful = true; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Верификация модуля ${moduleType} в сети: ${network.networkName} (${network.chainId})`); + + try { + const result = await executeWithRetries( + async () => { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const dle = new ethers.Contract(dleAddress, [ + "function getModuleAddress(bytes32 _moduleId) external view returns (address)" + ], provider); + + // Получаем адрес модуля + const moduleAddress = await dle.getModuleAddress(moduleId); + + if (moduleAddress === "0x0000000000000000000000000000000000000000") { + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'not_deployed', + message: `Модуль ${moduleType} не задеплоен` + }; + } + + // Верифицируем модуль + const verificationResult = await verifyModuleInNetwork( + moduleType, + moduleAddress, + dleAddress, + network.chainId, + network.networkName + ); + + return { + chainId: network.chainId, + networkName: network.networkName, + ...verificationResult + }; + }, + `Верификация модуля ${moduleType} в сети ${network.networkName} (${network.chainId})`, + maxRetries, + retryDelay + ); + + results.push(result); + + if (result.status !== 'success') { + allSuccessful = false; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка верификации модуля ${moduleType} в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + moduleType: moduleType, + moduleId: moduleId, + results: results, + summary: { + total: results.length, + success: results.filter(r => r.status === 'success').length, + failed: results.filter(r => r.status === 'failed').length, + not_deployed: results.filter(r => r.status === 'not_deployed').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'initialize_module' : 'retry_verification' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка верификации модуля:', error); + res.status(500).json({ + success: false, + error: 'Ошибка верификации модуля: ' + error.message + }); + } +}); + +// Инициализация одного модуля во всех сетях +router.post('/initialize-module-all-networks', async (req, res) => { + try { + const { dleAddress, moduleType, privateKey, maxRetries = 1, retryDelay = 30000 } = req.body; + + if (!dleAddress || !moduleType || !privateKey) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE, тип модуля и приватный ключ обязательны' + }); + } + + console.log(`[DLE Modules] Инициализация модуля ${moduleType} для DLE: ${dleAddress}`); + + // Автоматическая компиляция контрактов перед инициализацией + try { + await autoCompileContracts(); + } catch (compileError) { + console.error(`[DLE Modules] Ошибка при компиляции контрактов:`, compileError.message); + return res.status(500).json({ + success: false, + error: `Ошибка компиляции контрактов: ${compileError.message}` + }); + } + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.status(400).json({ + success: false, + error: 'Не найдены поддерживаемые сети для DLE' + }); + } + + // Маппинг типов модулей на их ID + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const moduleId = moduleIds[moduleType]; + if (!moduleId) { + return res.status(400).json({ + success: false, + error: `Неизвестный тип модуля: ${moduleType}. Поддерживаемые типы: ${Object.keys(moduleIds).join(', ')}` + }); + } + + const results = []; + let allSuccessful = true; + + for (const network of supportedNetworks) { + console.log(`[DLE Modules] Инициализация модуля ${moduleType} в сети: ${network.networkName} (${network.chainId})`); + + try { + const result = await executeWithRetries( + async () => { + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + + const dleAbi = [ + "function getModuleAddress(bytes32 _moduleId) external view returns (address)", + "function isModuleActive(bytes32 _moduleId) external view returns (bool)", + "function modulesInitialized() external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, wallet); + + // Проверяем, уже ли инициализированы модули + const modulesInitialized = await dle.modulesInitialized(); + + if (modulesInitialized) { + console.log(`[DLE Modules] Модули уже инициализированы в сети ${network.chainId}`); + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'already_initialized', + message: 'Модули уже инициализированы' + }; + } + + // Получаем адрес модуля + const moduleAddress = await dle.getModuleAddress(moduleId); + + if (moduleAddress === "0x0000000000000000000000000000000000000000") { + throw new Error(`Модуль ${moduleType} не задеплоен`); + } + + // Проверяем, что модуль не активен (если активен, значит уже инициализирован) + const isActive = await dle.isModuleActive(moduleId); + + if (isActive) { + console.log(`[DLE Modules] Модуль ${moduleType} уже активен в сети ${network.chainId}`); + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'already_active', + message: `Модуль ${moduleType} уже активен` + }; + } + + // Для инициализации одного модуля нужно создать предложение + // Но это сложно, поэтому пока просто отмечаем как требующий инициализации + return { + chainId: network.chainId, + networkName: network.networkName, + status: 'requires_governance', + message: `Модуль ${moduleType} требует инициализации через governance`, + moduleAddress: moduleAddress + }; + }, + `Инициализация модуля ${moduleType} в сети ${network.networkName} (${network.chainId})`, + maxRetries, + retryDelay + ); + + results.push(result); + + if (result.status === 'not_deployed' || result.status === 'error') { + allSuccessful = false; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка инициализации модуля ${moduleType} в сети ${network.chainId}:`, error.message); + results.push({ + chainId: network.chainId, + networkName: network.networkName, + status: 'error', + message: error.message, + attempts: maxRetries + }); + allSuccessful = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + moduleType: moduleType, + moduleId: moduleId, + results: results, + summary: { + total: results.length, + already_initialized: results.filter(r => r.status === 'already_initialized').length, + already_active: results.filter(r => r.status === 'already_active').length, + requires_governance: results.filter(r => r.status === 'requires_governance').length, + not_deployed: results.filter(r => r.status === 'not_deployed').length, + errors: results.filter(r => r.status === 'error').length, + allSuccessful: allSuccessful + }, + canProceed: allSuccessful, + nextAction: allSuccessful ? 'deploy_next_module' : 'retry_initialization' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка инициализации модуля:', error); + res.status(500).json({ + success: false, + error: 'Ошибка инициализации модуля: ' + error.message + }); + } +}); + +// Финальная проверка готовности всех компонентов +router.post('/final-deployment-check', async (req, res) => { + try { + const { dleAddress, chainIds } = req.body; + + if (!dleAddress || !chainIds || !Array.isArray(chainIds)) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE и список chainIds обязательны' + }); + } + + console.log(`[DLE Modules] Финальная проверка готовности DLE: ${dleAddress} в сетях: ${chainIds.join(', ')}`); + + // ID модулей для проверки + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const results = []; + let allComponentsReady = true; + + for (const chainId of chainIds) { + console.log(`[DLE Modules] Финальная проверка в сети: ${chainId}`); + + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + if (!rpcUrl) { + throw new Error(`RPC URL не найден для сети ${chainId}`); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + const dleAbi = [ + "function name() external view returns (string)", + "function symbol() external view returns (string)", + "function modulesInitialized() external view returns (bool)", + "function getModuleAddress(bytes32 _moduleId) external view returns (address)", + "function isModuleActive(bytes32 _moduleId) external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Проверяем DLE контракт + const dleCode = await provider.getCode(dleAddress); + const dleDeployed = dleCode !== '0x'; + + let dleInfo = null; + if (dleDeployed) { + try { + // Проверяем что контракт развернут (код не пустой) + const code = await provider.getCode(dleAddress); + if (code === '0x' || code.length <= 2) { + throw new Error('Контракт не развернут'); + } + const name = 'DLE'; // Используем фиксированное имя + const symbol = 'DLE'; // Используем фиксированный символ + dleInfo = { name, symbol }; + } catch (error) { + console.log(`[DLE Modules] Ошибка получения информации о DLE в сети ${chainId}:`, error.message); + } + } + + // Проверяем модули + const modulesStatus = {}; + let allModulesReady = true; + + for (const [moduleType, moduleId] of Object.entries(moduleIds)) { + try { + const moduleAddress = await dle.getModuleAddress(moduleId); + const isActive = await dle.isModuleActive(moduleId); + + if (moduleAddress === "0x0000000000000000000000000000000000000000") { + modulesStatus[moduleType] = { + deployed: false, + active: false, + address: null + }; + allModulesReady = false; + } else { + const moduleCode = await provider.getCode(moduleAddress); + const moduleDeployed = moduleCode !== '0x'; + + modulesStatus[moduleType] = { + deployed: moduleDeployed, + active: isActive, + address: moduleAddress + }; + + if (!moduleDeployed || !isActive) { + allModulesReady = false; + } + } + } catch (error) { + console.log(`[DLE Modules] Ошибка проверки модуля ${moduleType} в сети ${chainId}:`, error.message); + modulesStatus[moduleType] = { + deployed: false, + active: false, + address: null, + error: error.message + }; + allModulesReady = false; + } + } + + // Проверяем инициализацию модулей + let modulesInitialized = false; + try { + modulesInitialized = await dle.modulesInitialized(); + } catch (error) { + console.log(`[DLE Modules] Ошибка проверки инициализации модулей в сети ${chainId}:`, error.message); + } + + const networkReady = dleDeployed && allModulesReady && modulesInitialized; + + return { + chainId: chainId, + status: networkReady ? 'ready' : 'not_ready', + message: networkReady ? 'Все компоненты готовы' : 'Не все компоненты готовы', + components: { + dle: { + deployed: dleDeployed, + info: dleInfo + }, + modules: modulesStatus, + modulesInitialized: modulesInitialized + } + }; + }, + `Финальная проверка в сети ${chainId}`, + 1, // maxRetries + 30000 // retryDelay + ); + + results.push(result); + + if (result.status !== 'ready') { + allComponentsReady = false; + } + + } catch (error) { + console.error(`[DLE Modules] Ошибка финальной проверки в сети ${chainId}:`, error.message); + results.push({ + chainId: chainId, + status: 'error', + message: error.message, + components: {}, + attempts: 1 + }); + allComponentsReady = false; + } + } + + res.json({ + success: true, + data: { + dleAddress: dleAddress, + results: results, + summary: { + total: results.length, + ready: results.filter(r => r.status === 'ready').length, + not_ready: results.filter(r => r.status === 'not_ready').length, + errors: results.filter(r => r.status === 'error').length, + allComponentsReady: allComponentsReady + }, + canShowCards: allComponentsReady, + nextAction: allComponentsReady ? 'show_interface' : 'fix_issues' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка финальной проверки:', error); + res.status(500).json({ + success: false, + error: 'Ошибка финальной проверки: ' + error.message + }); + } +}); + +// Получение статуса деплоя +router.post('/get-deployment-status', async (req, res) => { + try { + const { dleAddress } = req.body; + + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); + } + + console.log(`[DLE Modules] Получение статуса деплоя для DLE: ${dleAddress}`); + + // Получаем поддерживаемые сети + const supportedNetworks = await getSupportedNetworksFromDLE(dleAddress); + + if (supportedNetworks.length === 0) { + return res.json({ + success: true, + data: { + status: 'not_started', + currentStage: null, + completedStages: [], + failedStages: [], + progress: 0, + canShowCards: false, + errors: ['Не найдены поддерживаемые сети для DLE'], + nextAction: 'start_deployment' + } + }); + } + + // Проверяем статус каждого компонента + const stages = [ + { name: 'deploy_dle', description: 'Деплой DLE контракта' }, + { name: 'verify_dle', description: 'Верификация DLE контракта' }, + { name: 'deploy_treasury', description: 'Деплой TreasuryModule' }, + { name: 'verify_treasury', description: 'Верификация TreasuryModule' }, + { name: 'initialize_treasury', description: 'Инициализация TreasuryModule' }, + { name: 'deploy_timelock', description: 'Деплой TimelockModule' }, + { name: 'verify_timelock', description: 'Верификация TimelockModule' }, + { name: 'initialize_timelock', description: 'Инициализация TimelockModule' }, + { name: 'deploy_reader', description: 'Деплой DLEReader' }, + { name: 'verify_reader', description: 'Верификация DLEReader' }, + { name: 'initialize_reader', description: 'Инициализация DLEReader' }, + { name: 'final_initialization', description: 'Финальная инициализация всех модулей' } + ]; + + const completedStages = []; + const failedStages = []; + let currentStage = null; + let progress = 0; + + // Проверяем DLE контракт + let dleDeployed = false; + let dleVerified = false; + + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(supportedNetworks[0].chainId); + const provider = new ethers.JsonRpcProvider(rpcUrl); + const dleCode = await provider.getCode(dleAddress); + return dleCode !== '0x'; + }, + 'Проверка деплоя DLE контракта', + 3, + 30000 + ); + + dleDeployed = result; + + if (dleDeployed) { + completedStages.push('deploy_dle'); + progress += 8.33; // 1/12 этапов + + // Проверяем верификацию (упрощенно - если контракт работает, считаем верифицированным) + try { + const verificationResult = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(supportedNetworks[0].chainId); + const provider = new ethers.JsonRpcProvider(rpcUrl); + // Простая проверка - если код контракта не пустой, считаем верифицированным + const code = await provider.getCode(dleAddress); + return code !== '0x' && code.length > 2; + }, + 'Проверка верификации DLE контракта', + 3, + 30000 + ); + + dleVerified = verificationResult; + if (dleVerified) { + completedStages.push('verify_dle'); + progress += 8.33; + } + } catch (error) { + failedStages.push('verify_dle'); + } + } else { + currentStage = 'deploy_dle'; + } + } catch (error) { + failedStages.push('deploy_dle'); + currentStage = 'deploy_dle'; + } + + // Если DLE не задеплоен, останавливаемся здесь + if (!dleDeployed) { + return res.json({ + success: true, + data: { + status: 'in_progress', + currentStage: 'deploy_dle', + completedStages: [], + failedStages: failedStages, + progress: 0, + canShowCards: false, + errors: ['DLE контракт не задеплоен'], + nextAction: 'deploy_dle' + } + }); + } + + // Проверяем модули + const moduleIds = { + treasury: "0x7472656173757279000000000000000000000000000000000000000000000000", + timelock: "0x74696d656c6f636b000000000000000000000000000000000000000000000000", + reader: "0x7265616465720000000000000000000000000000000000000000000000000000" + }; + + const moduleStages = [ + { type: 'treasury', stages: ['deploy_treasury', 'verify_treasury', 'initialize_treasury'] }, + { type: 'timelock', stages: ['deploy_timelock', 'verify_timelock', 'initialize_timelock'] }, + { type: 'reader', stages: ['deploy_reader', 'verify_reader', 'initialize_reader'] } + ]; + + for (const moduleGroup of moduleStages) { + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(supportedNetworks[0].chainId); + const provider = new ethers.JsonRpcProvider(rpcUrl); + const dle = new ethers.Contract(dleAddress, [ + "function getModuleAddress(bytes32 _moduleId) external view returns (address)", + "function isModuleActive(bytes32 _moduleId) external view returns (bool)" + ], provider); + + const moduleAddress = await dle.getModuleAddress(moduleIds[moduleGroup.type]); + const isActive = await dle.isModuleActive(moduleIds[moduleGroup.type]); + + return { moduleAddress, isActive }; + }, + `Проверка модуля ${moduleGroup.type}`, + 3, + 30000 + ); + + const { moduleAddress, isActive } = result; + + if (moduleAddress !== "0x0000000000000000000000000000000000000000") { + completedStages.push(moduleGroup.stages[0]); // deploy + progress += 8.33; + + // Проверяем верификацию (упрощенно) + try { + const verificationResult = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(supportedNetworks[0].chainId); + const provider = new ethers.JsonRpcProvider(rpcUrl); + const moduleCode = await provider.getCode(moduleAddress); + return moduleCode !== '0x'; + }, + `Проверка верификации модуля ${moduleGroup.type}`, + 3, + 30000 + ); + + if (verificationResult) { + completedStages.push(moduleGroup.stages[1]); // verify + progress += 8.33; + } else { + failedStages.push(moduleGroup.stages[1]); + } + } catch (error) { + failedStages.push(moduleGroup.stages[1]); + } + + // Проверяем инициализацию + if (isActive) { + completedStages.push(moduleGroup.stages[2]); // initialize + progress += 8.33; + } else { + if (!currentStage) currentStage = moduleGroup.stages[2]; + } + } else { + if (!currentStage) currentStage = moduleGroup.stages[0]; + } + } catch (error) { + if (!currentStage) currentStage = moduleGroup.stages[0]; + } + } + + // Проверяем финальную инициализацию + try { + const result = await executeWithRetries( + async () => { + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(supportedNetworks[0].chainId); + const provider = new ethers.JsonRpcProvider(rpcUrl); + const dle = new ethers.Contract(dleAddress, [ + "function modulesInitialized() external view returns (bool)" + ], provider); + + return await dle.modulesInitialized(); + }, + 'Проверка финальной инициализации модулей', + 3, + 30000 + ); + + if (result) { + completedStages.push('final_initialization'); + progress += 8.33; + } else { + if (!currentStage) currentStage = 'final_initialization'; + } + } catch (error) { + if (!currentStage) currentStage = 'final_initialization'; + } + + // Определяем общий статус + let status = 'in_progress'; + if (progress >= 100) { + status = 'completed'; + currentStage = null; + } else if (failedStages.length > 0 && completedStages.length === 0) { + status = 'failed'; + } + + res.json({ + success: true, + data: { + status: status, + currentStage: currentStage, + completedStages: completedStages, + failedStages: failedStages, + progress: Math.round(progress), + canShowCards: status === 'completed', + errors: failedStages.length > 0 ? [`Ошибки в этапах: ${failedStages.join(', ')}`] : [], + nextAction: status === 'completed' ? 'none' : + status === 'failed' ? 'restart_deployment' : 'continue_deployment' + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка получения статуса деплоя:', error); + res.status(500).json({ + success: false, + error: 'Ошибка получения статуса деплоя: ' + error.message + }); + } +}); + module.exports = router; diff --git a/backend/routes/dleMultichain.js b/backend/routes/dleMultichain.js index 9d3c9e7..44766a5 100644 --- a/backend/routes/dleMultichain.js +++ b/backend/routes/dleMultichain.js @@ -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); diff --git a/backend/routes/dleProposals.js b/backend/routes/dleProposals.js index 9bf0cd0..96d3aef 100644 --- a/backend/routes/dleProposals.js +++ b/backend/routes/dleProposals.js @@ -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; diff --git a/backend/routes/dleV2.js b/backend/routes/dleV2.js index 374a5c5..eb81b4f 100644 --- a/backend/routes/dleV2.js +++ b/backend/routes/dleV2.js @@ -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) => { diff --git a/backend/routes/ens.js b/backend/routes/ens.js index eac48a3..15f5a3a 100644 --- a/backend/routes/ens.js +++ b/backend/routes/ens.js @@ -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 */ diff --git a/backend/routes/settings.js b/backend/routes/settings.js index fde0471..41ad19a 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -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) { // Для админов возвращаем полные данные diff --git a/backend/routes/uploads.js b/backend/routes/uploads.js index 7e3b01d..4c080c9 100644 --- a/backend/routes/uploads.js +++ b/backend/routes/uploads.js @@ -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 */ diff --git a/backend/scripts/check-modules.js b/backend/scripts/check-modules.js new file mode 100644 index 0000000..6635323 --- /dev/null +++ b/backend/scripts/check-modules.js @@ -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(); diff --git a/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json b/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json new file mode 100644 index 0000000..d4e9087 --- /dev/null +++ b/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json @@ -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" +} \ No newline at end of file diff --git a/backend/scripts/contracts-data/modules/timelock-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json b/backend/scripts/contracts-data/modules/timelock-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json new file mode 100644 index 0000000..1661ff3 --- /dev/null +++ b/backend/scripts/contracts-data/modules/timelock-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json @@ -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" +} \ No newline at end of file diff --git a/backend/scripts/contracts-data/modules/treasury-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json b/backend/scripts/contracts-data/modules/treasury-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json new file mode 100644 index 0000000..ea0b7d7 --- /dev/null +++ b/backend/scripts/contracts-data/modules/treasury-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json @@ -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" +} \ No newline at end of file diff --git a/backend/scripts/deploy/deploy-multichain.js b/backend/scripts/deploy/deploy-multichain.js old mode 100644 new mode 100755 index a032b8f..33f9db6 --- a/backend/scripts/deploy/deploy-multichain.js +++ b/backend/scripts/deploy/deploy-multichain.js @@ -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); }); diff --git a/backend/server.js b/backend/server.js index dc1d5b9..9ec18b3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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 () => { diff --git a/backend/services/databaseConnectionManager.js b/backend/services/databaseConnectionManager.js index ac962b5..b6ecfa6 100644 --- a/backend/services/databaseConnectionManager.js +++ b/backend/services/databaseConnectionManager.js @@ -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 + */ + /** * Сервис для динамического управления подключениями к базе данных * Позволяет изменять настройки БД без перезапуска приложения diff --git a/backend/services/dleV2Service.js b/backend/services/dleV2Service.js index b52c9ae..865ff7c 100644 --- a/backend/services/dleV2Service.js +++ b/backend/services/dleV2Service.js @@ -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} - Результат создания 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(); \ No newline at end of file diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index fc53557..1f7e6fa 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -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++) { diff --git a/backend/services/secretStore.js b/backend/services/secretStore.js index 2a67375..9d74ba7 100644 --- a/backend/services/secretStore.js +++ b/backend/services/secretStore.js @@ -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 */ diff --git a/backend/services/tokenBalanceService.js b/backend/services/tokenBalanceService.js index bbbfdf8..cb6544e 100644 --- a/backend/services/tokenBalanceService.js +++ b/backend/services/tokenBalanceService.js @@ -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 */ diff --git a/backend/utils/NonceManager.js b/backend/utils/NonceManager.js new file mode 100644 index 0000000..6b7ff02 --- /dev/null +++ b/backend/utils/NonceManager.js @@ -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} Актуальный 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} Заблокированный 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} Синхронизированный 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} Параметры комиссии + */ + 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} Результат транзакции + */ + 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; diff --git a/backend/utils/create2.js b/backend/utils/create2.js index 659104d..85a840e 100644 --- a/backend/utils/create2.js +++ b/backend/utils/create2.js @@ -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) { diff --git a/backend/utils/deploymentTracker.js b/backend/utils/deploymentTracker.js new file mode 100644 index 0000000..c4d56b7 --- /dev/null +++ b/backend/utils/deploymentTracker.js @@ -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; diff --git a/backend/wsHub.js b/backend/wsHub.js index b524ea8..c7eaa43 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -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 }; diff --git a/backup_deploy_20250922_220227/deploy-multichain.js b/backup_deploy_20250922_220227/deploy-multichain.js new file mode 100644 index 0000000..a032b8f --- /dev/null +++ b/backup_deploy_20250922_220227/deploy-multichain.js @@ -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); }); + + diff --git a/backup_deploy_20250922_220227/dleV2.js b/backup_deploy_20250922_220227/dleV2.js new file mode 100644 index 0000000..2bf0343 --- /dev/null +++ b/backup_deploy_20250922_220227/dleV2.js @@ -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 }); + } +}); + diff --git a/backup_deploy_20250922_220227/dleV2Service.js b/backup_deploy_20250922_220227/dleV2Service.js new file mode 100644 index 0000000..9ba53c2 --- /dev/null +++ b/backup_deploy_20250922_220227/dleV2Service.js @@ -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} - Результат создания 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} - Результат деплоя + */ + 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} - Список 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} allDles - Все DLE из файлов + * @returns {Array} - Сгруппированные 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} - Результат проверки балансов + */ + 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(); \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6bebe2c..9b63345 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,3 +1,15 @@ + + # Архитектура проекта DLE ## 🎯 Общий принцип diff --git a/docs/DLE_API_ENDPOINTS.md b/docs/DLE_API_ENDPOINTS.md index 1c998ff..3d3aa11 100644 --- a/docs/DLE_API_ENDPOINTS.md +++ b/docs/DLE_API_ENDPOINTS.md @@ -1,3 +1,15 @@ + + # API Endpoints для обновленного смарт контракта DLE ## Обзор diff --git a/docs/DLE_CONSOLIDATED_ANALYSIS.md b/docs/DLE_CONSOLIDATED_ANALYSIS.md index 38e48d3..c3e7fdd 100644 --- a/docs/DLE_CONSOLIDATED_ANALYSIS.md +++ b/docs/DLE_CONSOLIDATED_ANALYSIS.md @@ -17,7 +17,7 @@ - Делегирование «только на себя»: 1 токен = 1 голос, запрет делегирования третьим лицам. - Модульность: казна, таймлок, деактивация, коммуникации выделены в отдельные модули, операции выполняются через ядро DLE. - «100% или ничего»: много-сетевые операции исполняются только при готовности всех целевых сетей. -- Детерминированный деплой: `FactoryDeployer` + CREATE2 для одинаковых адресов во всех выбранных сетях; INIT_CODE_HASH рассчитывается автоматически из актуального initCode. +- Детерминированный деплой: CREATE с выровненным nonce для одинаковых адресов во всех выбранных сетях. - Аналитика: добавлены view‑функции для сводок, пагинации и агрегирования по предложениям. --- diff --git a/docs/DLE_DEPLOY_GUIDE.md b/docs/DLE_DEPLOY_GUIDE.md index f924c0c..370f19f 100644 --- a/docs/DLE_DEPLOY_GUIDE.md +++ b/docs/DLE_DEPLOY_GUIDE.md @@ -1,3 +1,15 @@ + + # Руководство по деплою 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 (опционально) diff --git a/docs/FRONTEND_ARCHITECTURE.md b/docs/FRONTEND_ARCHITECTURE.md index 1bb98c4..481c956 100644 --- a/docs/FRONTEND_ARCHITECTURE.md +++ b/docs/FRONTEND_ARCHITECTURE.md @@ -1,3 +1,15 @@ + + # Архитектура фронтенда DLE ## 📁 Структура сервисов diff --git a/docs/MODULE_ARCHITECTURE.md b/docs/MODULE_ARCHITECTURE.md index 494b2a9..b78dff0 100644 --- a/docs/MODULE_ARCHITECTURE.md +++ b/docs/MODULE_ARCHITECTURE.md @@ -1,3 +1,15 @@ + + # Архитектура модулей DLE ## Обзор diff --git a/docs/MODULE_DEPLOYMENT_IMPROVEMENTS.md b/docs/MODULE_DEPLOYMENT_IMPROVEMENTS.md new file mode 100644 index 0000000..a235113 --- /dev/null +++ b/docs/MODULE_DEPLOYMENT_IMPROVEMENTS.md @@ -0,0 +1,628 @@ + + +# Улучшения системы деплоя и управления модулями 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 с инициализированными и верифицированными модулями во всех выбранных сетях. diff --git a/docs/SMART_CONTRACTS.md b/docs/SMART_CONTRACTS.md index a27ea90..1bbb905 100644 --- a/docs/SMART_CONTRACTS.md +++ b/docs/SMART_CONTRACTS.md @@ -23,7 +23,7 @@ - Multi‑Chain исполнение: выполнение в целевых сетях по EIP‑712 подписям холдеров, проверяется суммарная голосующая сила на зафиксированном `timepoint` (без доверия к мостам). - «100% или ничего»: операции считаются успешными только при готовности/успешности всех целевых сетей. - Модули вынесены отдельно: `Treasury`, `Timelock`, `Deactivation`, `Communication` и др. Управление только через предложения. -- Детерминированные адреса: фабрика `FactoryDeployer` + CREATE2. Единый адрес DLE и модулей во всех выбранных сетях. INIT_CODE_HASH автоподставляется из актуального initCode. +- Детерминированные адреса: CREATE с выровненным nonce. Единый адрес DLE и модулей во всех выбранных сетях. - Аналитика: добавлены view‑функции для агрегирования и пагинации. Пример основных функций DLE v2 (интерфейс): diff --git a/frontend/src/api/axios.js b/frontend/src/api/axios.js index 54848f3..0e9e051 100644 --- a/frontend/src/api/axios.js +++ b/frontend/src/api/axios.js @@ -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' && diff --git a/frontend/src/components/deployment/DeploymentWizard.vue b/frontend/src/components/deployment/DeploymentWizard.vue new file mode 100644 index 0000000..625255a --- /dev/null +++ b/frontend/src/components/deployment/DeploymentWizard.vue @@ -0,0 +1,601 @@ + + + + + + + diff --git a/frontend/src/composables/useBlockchainNetworks.js b/frontend/src/composables/useBlockchainNetworks.js index 947ced3..5222d0d 100644 --- a/frontend/src/composables/useBlockchainNetworks.js +++ b/frontend/src/composables/useBlockchainNetworks.js @@ -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 } ] diff --git a/frontend/src/composables/useDeploymentWebSocket.js b/frontend/src/composables/useDeploymentWebSocket.js new file mode 100644 index 0000000..b6d0b18 --- /dev/null +++ b/frontend/src/composables/useDeploymentWebSocket.js @@ -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 + }; +} diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index e038dbe..43d8947 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -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', diff --git a/frontend/src/services/dleV2Service.js b/frontend/src/services/dleV2Service.js index 7815831..adf3913 100644 --- a/frontend/src/services/dleV2Service.js +++ b/frontend/src/services/dleV2Service.js @@ -15,20 +15,6 @@ import axios from 'axios'; // ===== ОСНОВНЫЕ ФУНКЦИИ DLE ===== -/** - * Создает новое DLE v2 - * @param {Object} dleParams - Параметры DLE - * @returns {Promise} - Результат создания - */ -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} - Параметры по умолчанию - */ -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} - Данные из блокчейна - */ -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} - Статус поддержки */ -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} - Текущая сеть */ -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} - Результат исполнения */ -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} - История событий */ -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} - Статистика */ -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; - } -}; \ No newline at end of file diff --git a/frontend/src/services/modulesService.js b/frontend/src/services/modulesService.js index 9922e73..c5623e6 100644 --- a/frontend/src/services/modulesService.js +++ b/frontend/src/services/modulesService.js @@ -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} - Адрес модуля */ -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} - Информация о сетях + */ +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} - Результат инициализации + */ +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} - Результат верификации + */ +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} chainIds - Список ID сетей + * @param {number} maxRetries - Максимальное количество попыток + * @param {number} retryDelay - Задержка между попытками (мс) + * @returns {Promise} - Статус деплоя 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} chainIds - Список ID сетей + * @param {number} maxRetries - Максимальное количество попыток + * @param {number} retryDelay - Задержка между попытками (мс) + * @returns {Promise} - Статус деплоя модуля + */ +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} - Результат деплоя модуля + */ +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} - Результат верификации 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} - Результат верификации модуля + */ +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} - Результат инициализации модуля + */ +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} chainIds - Список ID сетей + * @param {number} maxRetries - Максимальное количество попыток + * @param {number} retryDelay - Задержка между попытками (мс) + * @returns {Promise} - Результат финальной проверки + */ +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} - Статус деплоя + */ +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; + } +}; diff --git a/frontend/src/services/proposalsService.js b/frontend/src/services/proposalsService.js index 1a58b25..653096b 100644 --- a/frontend/src/services/proposalsService.js +++ b/frontend/src/services/proposalsService.js @@ -261,3 +261,20 @@ export const getQuorumAt = async (dleAddress, timepoint) => { throw error; } }; + +/** + * Декодирует данные предложения о добавлении модуля + * @param {string} transactionHash - Хеш транзакции создания предложения + * @returns {Promise} - Декодированные данные предложения + */ +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; + } +}; diff --git a/frontend/src/services/websocketService.js b/frontend/src/services/websocketService.js index 03b3ea6..3c6db40 100644 --- a/frontend/src/services/websocketService.js +++ b/frontend/src/services/websocketService.js @@ -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); diff --git a/frontend/src/utils/dle-contract.js b/frontend/src/utils/dle-contract.js index f27f9c3..0d55b45 100644 --- a/frontend/src/utils/dle-contract.js +++ b/frontend/src/utils/dle-contract.js @@ -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} - Адрес модуля */ -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 }); diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 700874c..8405417 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -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) { diff --git a/frontend/src/views/settings/DleDeployFormView.vue b/frontend/src/views/settings/DleDeployFormView.vue index da34724..e6e2c28 100644 --- a/frontend/src/views/settings/DleDeployFormView.vue +++ b/frontend/src/views/settings/DleDeployFormView.vue @@ -827,14 +827,42 @@
+ +
+

🚀 Поэтапный деплой DLE

+

+ Автоматический деплой DLE контракта и всех модулей с проверками, верификацией и инициализацией во всех выбранных сетях +

+
+
+ + Деплой DLE контракта во всех сетях +
+
+ + Автоматическая верификация контрактов +
+
+ + Деплой и инициализация всех модулей +
+
+ + Повторы при ошибках сети +
+
+
+
+ + +
+
+ +
+
@@ -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; diff --git a/frontend/src/views/smartcontracts/DleProposalsView.vue b/frontend/src/views/smartcontracts/DleProposalsView.vue index 5ac0253..07b5c3f 100644 --- a/frontend/src/views/smartcontracts/DleProposalsView.vue +++ b/frontend/src/views/smartcontracts/DleProposalsView.vue @@ -70,7 +70,7 @@ >
-
{{ proposal.description || 'Без описания' }}
+
{{ getProposalTitle(proposal) }}
{{ getProposalStatusText(proposal.status) }} @@ -92,6 +92,27 @@
Дедлайн: {{ formatDate(proposal.deadline) }}
+ + +
+
+ Тип модуля: {{ getModuleName(proposal.decodedData.moduleId) }} +
+ +
+ Сеть: {{ getChainName(proposal.decodedData.chainId) }} +
+
+ Длительность: {{ formatDuration(proposal.decodedData.duration) }} +
+
+
Голоса:
@@ -100,7 +121,7 @@ Против: {{ formatVotes(proposal.againstVotes) }}
- Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum() }}% + Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}%
@@ -140,13 +161,21 @@ Против + +
+ + + Только инициатор предложения может его исполнить + +
+
@@ -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 = ` +
+
+ + ${message} +
+
+

Ожидаем подтверждения транзакции...

+ + Посмотреть в Etherscan + +
+
+ `; + + // Добавляем стили + 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 = ` +
+
+ + ${actionText} +
+
+

Данные обновлены

+ + Посмотреть в Etherscan + +
+
+ `; + + 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 = ` +
+
+ + ${message} +
+ +
+ `; + + 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 = ` +
+
+ + ${actionText} отправлено +
+
+

Подтверждение не получено, но данные обновлены

+ + Посмотреть в Etherscan + +
+
+ `; + + 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; diff --git a/frontend/src/views/smartcontracts/ModulesView.vue b/frontend/src/views/smartcontracts/ModulesView.vue index ee31695..19a2635 100644 --- a/frontend/src/views/smartcontracts/ModulesView.vue +++ b/frontend/src/views/smartcontracts/ModulesView.vue @@ -103,6 +103,29 @@
+ +
+
+

DLEReader

+

Чтение данных DLE - API для получения информации о контракте и предложениях

+
+ API + Чтение + Данные + Интеграция +
+
+
+ +
+
+
@@ -465,12 +488,44 @@

📋 Модули DLE

-
-
+ +
+
+ + Проверка статуса деплоя... +
+
+ +
+
+
+ + + + + +
+
+

{{ deploymentStatusMessage }}

+

+ Для активации модулей необходимо запустить поэтапный деплой DLE. +

+

+ Проверьте логи деплоя и повторите попытку через форму деплоя. +

+

+ Дождитесь завершения деплоя. Модули станут доступны автоматически. +

+
+
+
+ +

Загрузка модулей...

@@ -479,7 +534,7 @@

Используйте форму выше для добавления первого модуля

-
+
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; diff --git a/frontend/src/views/smartcontracts/modules/DLEReaderDeployView.vue b/frontend/src/views/smartcontracts/modules/DLEReaderDeployView.vue new file mode 100644 index 0000000..920b264 --- /dev/null +++ b/frontend/src/views/smartcontracts/modules/DLEReaderDeployView.vue @@ -0,0 +1,585 @@ + + + + + + + diff --git a/frontend/src/views/smartcontracts/modules/TimelockModuleDeployView.vue b/frontend/src/views/smartcontracts/modules/TimelockModuleDeployView.vue index a8a69a4..c123913 100644 --- a/frontend/src/views/smartcontracts/modules/TimelockModuleDeployView.vue +++ b/frontend/src/views/smartcontracts/modules/TimelockModuleDeployView.vue @@ -49,11 +49,255 @@
- -
-
-

🚧 Форма деплоя в разработке

-

Здесь будет форма для деплоя TimelockModule

+ +
+
+

🌐 Деплой TimelockModule во всех сетях

+

Деплой модуля временных задержек во всех 4 сетях одновременно

+
+ +
+ +
+

📡 Сети для деплоя:

+
+
+ Sepolia + Chain ID: 11155111 +
+
+ Holesky + Chain ID: 17000 +
+
+ Arbitrum Sepolia + Chain ID: 421614 +
+
+ Base Sepolia + Chain ID: 84532 +
+
+
+ + +
+

⚙️ Настройки TimelockModule:

+ +
+
+
+ + + ID сети для деплоя модуля +
+ +
+ + + Стандартная задержка для операций (1-30 дней) +
+
+ +
+
+ + + Экстренная задержка для критических операций (5-1440 минут) +
+ +
+ + + Максимальная задержка для операций (1-365 дней) +
+
+ +
+
+ + + Минимальная задержка для операций (1-720 часов) +
+ +
+ + + Максимальное количество операций в очереди (10-1000) +
+
+ + +
+
🔧 Дополнительные настройки таймлока:
+ +
+ + + Селекторы функций, которые считаются критическими (JSON массив) +
+ +
+ + + Селекторы функций для экстренных операций (JSON массив) +
+ +
+
+ + + Кастомные задержки для конкретных операций (селектор => секунды) +
+ +
+ + + Автоматическое исполнение операций после истечения задержки +
+
+ +
+
+ + + Время, в течение которого можно отменить операцию (1-168 часов) +
+ +
+ + + Время, в течение которого можно исполнить операцию (1-168 часов) +
+
+ +
+ + + Описание таймлока для документации +
+
+
+
+ + +
+ + +
+
+ {{ deploymentProgress.message }} + {{ deploymentProgress.percentage }}% +
+
+
+
+
+
@@ -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; diff --git a/frontend/src/views/smartcontracts/modules/TreasuryModuleDeployView.vue b/frontend/src/views/smartcontracts/modules/TreasuryModuleDeployView.vue index b831051..8bc3aac 100644 --- a/frontend/src/views/smartcontracts/modules/TreasuryModuleDeployView.vue +++ b/frontend/src/views/smartcontracts/modules/TreasuryModuleDeployView.vue @@ -49,11 +49,263 @@
- -
-
-

🚧 Форма деплоя в разработке

-

Здесь будет форма для деплоя TreasuryModule

+ +
+
+

🌐 Деплой TreasuryModule во всех сетях

+

Деплой модуля казначейства во всех 4 сетях одновременно

+
+ +
+ +
+

📡 Сети для деплоя:

+
+
+ Sepolia + Chain ID: 11155111 +
+
+ Holesky + Chain ID: 17000 +
+
+ Arbitrum Sepolia + Chain ID: 421614 +
+
+ Base Sepolia + Chain ID: 84532 +
+
+
+ + +
+

⚙️ Настройки TreasuryModule:

+ +
+
+
+ + + Адрес экстренного администратора для управления модулем +
+ +
+ + + ID сети для деплоя модуля +
+
+ +
+
+ + + Стандартная задержка для операций (1-720 часов) +
+ +
+ + + Экстренная задержка для критических операций (5-1440 минут) +
+
+ +
+ + + Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую) +
+ +
+ + + Токены, которыми можно оплачивать газ (через запятую) +
+ + +
+
🔧 Дополнительные настройки казны:
+ +
+
+ + + Адрес Paymaster для ERC-4337 (оплата газа любым токеном) +
+ +
+ + + Максимальное количество переводов в batch операции (1-100) +
+
+ +
+
+ + + Курсы обмена токенов на нативную монету (JSON формат) +
+ +
+ + + Порог для экстренных операций в ETH +
+
+ +
+ + + Токены для автоматического добавления при деплое (JSON массив) +
+ +
+
+ + + Автоматическое обновление балансов токенов +
+ +
+ + + Разрешить batch операции переводов +
+
+ +
+ + + Описание казны для документации +
+
+
+
+ + +
+ + +
+
+ {{ deploymentProgress.message }} + {{ deploymentProgress.percentage }}% +
+
+
+
+
+
@@ -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; diff --git a/frontend/src/views/smartcontracts/modules/index.js b/frontend/src/views/smartcontracts/modules/index.js new file mode 100644 index 0000000..a8638b5 --- /dev/null +++ b/frontend/src/views/smartcontracts/modules/index.js @@ -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'; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index f79a6a1..e352456 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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, diff --git a/scripts/internal/db/db_init_helper.sh b/scripts/internal/db/db_init_helper.sh index 27a44b8..3b582f4 100755 --- a/scripts/internal/db/db_init_helper.sh +++ b/scripts/internal/db/db_init_helper.sh @@ -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