diff --git a/.gitignore b/.gitignore index f63583c..7d06464 100644 --- a/.gitignore +++ b/.gitignore @@ -185,6 +185,17 @@ backend/typechain-types/ # Contract data (может содержать конфиденциальную информацию) backend/contracts-data/ +# Deploy parameters (содержит приватные ключи и чувствительные данные) +backend/scripts/deploy/current-params.json +backend/scripts/deploy/*-params.json +backend/temp/dle-v2-params-*.json + +# Temporary verification files +backend/scripts/constructor-args*.json + +# Module deployment data (содержит адреса и конфигурации модулей) +backend/scripts/contracts-data/modules/* + # Temporary test files backend/test-*.js backend/test_*.js diff --git a/backend/contracts/DLE.sol b/backend/contracts/DLE.sol index d098e1a..743793a 100644 --- a/backend/contracts/DLE.sol +++ b/backend/contracts/DLE.sol @@ -144,7 +144,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta event ChainRemoved(uint256 chainId); event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); - event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId); event TokensTransferredByGovernance(address indexed recipient, uint256 amount); event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); @@ -188,7 +187,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta error ErrBadKPP(); error ErrBadQuorum(); error ErrChainAlreadySupported(); - error ErrCannotAddCurrentChain(); error ErrChainNotSupported(); error ErrCannotRemoveCurrentChain(); error ErrTransfersDisabled(); @@ -601,10 +599,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta // Операция обновления процента кворума (uint256 newQuorumPercentage) = abi.decode(data, (uint256)); _updateQuorumPercentage(newQuorumPercentage); - } else if (selector == bytes4(keccak256("_updateCurrentChainId(uint256)"))) { - // Операция обновления текущей цепочки - (uint256 newChainId) = abi.decode(data, (uint256)); - _updateCurrentChainId(newChainId); } else if (selector == bytes4(keccak256("_updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) { // Операция обновления информации DLE (string memory name, string memory symbol, string memory location, string memory coordinates, uint256 jurisdiction, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, string[], uint256)); @@ -667,19 +661,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta emit QuorumPercentageUpdated(oldQuorumPercentage, _newQuorumPercentage); } - /** - * @dev Обновить текущую цепочку - * @param _newChainId Новый ID цепочки - */ - function _updateCurrentChainId(uint256 _newChainId) internal { - if (!supportedChains[_newChainId]) revert ErrChainNotSupported(); - if (_newChainId == currentChainId) revert ErrCannotAddCurrentChain(); - - uint256 oldChainId = currentChainId; - currentChainId = _newChainId; - - emit CurrentChainIdUpdated(oldChainId, _newChainId); - } /** * @dev Перевести токены через governance (от имени DLE) diff --git a/backend/contracts/TimelockModule.sol b/backend/contracts/TimelockModule.sol index a452a7a..e365f4e 100644 --- a/backend/contracts/TimelockModule.sol +++ b/backend/contracts/TimelockModule.sol @@ -378,11 +378,9 @@ contract TimelockModule is ReentrancyGuard { // Обычные операции - стандартная задержка (2 дня) bytes4 updateDLEInfo = bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)")); - bytes4 updateChainId = bytes4(keccak256("updateCurrentChainId(uint256)")); bytes4 updateVotingDurations = bytes4(keccak256("_updateVotingDurations(uint256,uint256)")); operationDelays[updateDLEInfo] = 2 days; - operationDelays[updateChainId] = 3 days; operationDelays[updateVotingDurations] = 1 days; // Treasury операции - короткая задержка (1 день) diff --git a/backend/docs/AUTO_VERIFICATION.md b/backend/docs/AUTO_VERIFICATION.md new file mode 100644 index 0000000..0a94c6a --- /dev/null +++ b/backend/docs/AUTO_VERIFICATION.md @@ -0,0 +1,105 @@ +# Автоматическая верификация контрактов + +## Обзор + +Автоматическая верификация контрактов интегрирована в процесс деплоя DLE. После успешного деплоя контрактов во всех выбранных сетях, система автоматически запускает процесс верификации на соответствующих блокчейн-эксплорерах. + +## Как это работает + +1. **Настройка в форме деплоя**: В форме деплоя DLE есть чекбокс "Авто-верификация после деплоя" (по умолчанию включен) +2. **Деплой контрактов**: Система разворачивает DLE контракты во всех выбранных сетях +3. **Автоматическая верификация**: Если `autoVerifyAfterDeploy = true`, система автоматически запускает верификацию +4. **Результаты**: Статус верификации отображается в логах деплоя + +## Поддерживаемые сети + +Автоматическая верификация работает для всех сетей, настроенных в `hardhat.config.js`: + +- **Sepolia** (Ethereum testnet) +- **Holesky** (Ethereum testnet) +- **Arbitrum Sepolia** +- **Base Sepolia** +- **И другие сети** (настраиваются в конфиге) + +## Требования + +1. **Etherscan API ключ**: Должен быть указан в форме деплоя +2. **Права на запись**: Приватный ключ должен иметь права на деплой контрактов +3. **Сеть доступна**: RPC провайдеры для всех выбранных сетей должны быть доступны + +## Логи и мониторинг + +### В логах деплоя вы увидите: + +``` +[MULTI_DBG] autoVerifyAfterDeploy: true +[MULTI_DBG] Starting automatic contract verification... +🔍 Верификация в сети sepolia (chainId: 11155111) +✅ Верификация успешна: https://sepolia.etherscan.io/address/0x... +[MULTI_DBG] ✅ Automatic verification completed successfully +``` + +### Статусы верификации: + +- `verified` - контракт успешно верифицирован +- `verification_failed` - ошибка верификации +- `disabled` - верификация отключена +- `already_verified` - контракт уже был верифицирован ранее + +## Отключение автоматической верификации + +Если вы хотите отключить автоматическую верификацию: + +1. В форме деплоя снимите галочку "Авто-верификация после деплоя" +2. Или установите `autoVerifyAfterDeploy: false` в настройках + +## Ручная верификация + +Если автоматическая верификация не сработала, вы можете запустить верификацию вручную: + +```bash +# В Docker контейнере +docker exec dapp-backend node scripts/verify-with-hardhat-v2.js + +# Или через npm скрипт +docker exec dapp-backend npm run verify:contracts +``` + +## Техническая реализация + +Автоматическая верификация интегрирована в `backend/scripts/deploy/deploy-multichain.js`: + +```javascript +if (params.autoVerifyAfterDeploy) { + console.log('[MULTI_DBG] Starting automatic contract verification...'); + + try { + const { verifyContracts } = require('../verify-with-hardhat-v2'); + await verifyContracts(); + verificationResults = networks.map(() => 'verified'); + console.log('[MULTI_DBG] ✅ Automatic verification completed successfully'); + } catch (verificationError) { + console.error('[MULTI_DBG] ❌ Automatic verification failed:', verificationError.message); + verificationResults = networks.map(() => 'verification_failed'); + } +} +``` + +## Устранение проблем + +### Ошибка "Contract already verified" +Это нормально - контракт уже был верифицирован ранее. + +### Ошибка "Rate limit exceeded" +Система автоматически добавляет задержки между запросами к разным сетям. + +### Ошибка "Network not supported" +Убедитесь, что сеть настроена в `hardhat.config.js` и имеет правильный Etherscan API URL. + +## Преимущества + +1. **Автоматизация**: Не нужно запускать верификацию вручную +2. **Надежность**: Верификация происходит сразу после деплоя +3. **Мультисеть**: Верификация во всех развернутых сетях одновременно +4. **Мониторинг**: Полная видимость процесса через логи +5. **Интеграция**: Единый процесс деплоя и верификации diff --git a/backend/docs/MODULE_IDS.md b/backend/docs/MODULE_IDS.md deleted file mode 100644 index f415446..0000000 --- a/backend/docs/MODULE_IDS.md +++ /dev/null @@ -1,173 +0,0 @@ -# ID Модулей DLE - -## Обзор - -В системе DLE каждый модуль имеет уникальный идентификатор (ID), который используется для: -- Идентификации модуля в смарт-контракте -- Создания governance предложений -- Проверки статуса модуля - -## Формат ID - -ID модулей представляют собой 32-байтные хеши в формате: -``` -0x[32 байта в hex формате] -``` - -### Стандартные модули - -Стандартные модули используют ASCII-коды названий, дополненные нулями до 32 байт: - -| Модуль | ID | Описание | -|--------|----|---------| -| **Treasury** | `0x7472656173757279000000000000000000000000000000000000000000000000` | Модуль управления казной | -| **Timelock** | `0x74696d656c6f636b000000000000000000000000000000000000000000000000` | Модуль задержки выполнения | -| **Reader** | `0x7265616465720000000000000000000000000000000000000000000000000000` | Модуль чтения данных | - -### Дополнительные модули - -Дополнительные модули могут использовать другие форматы ID: - -| Модуль | ID | Описание | -|--------|----|---------| -| **Multisig** | `0x6d756c7469736967000000000000000000000000000000000000000000000000` | Мультиподписный модуль | -| **Deactivation** | `0x646561637469766174696f6e0000000000000000000000000000000000000000` | Модуль деактивации | -| **Analytics** | `0x616e616c79746963730000000000000000000000000000000000000000000000` | Модуль аналитики | -| **Notifications** | `0x6e6f74696669636174696f6e7300000000000000000000000000000000000000` | Модуль уведомлений | - -## Использование в коде - -### Константы - -Все ID модулей определены в файле `backend/constants/moduleIds.js`: - -```javascript -const { MODULE_IDS, MODULE_TYPE_TO_ID, MODULE_ID_TO_TYPE } = require('../constants/moduleIds'); - -// Использование -const treasuryId = MODULE_IDS.TREASURY; -const moduleType = MODULE_ID_TO_TYPE[moduleId]; -const moduleId = MODULE_TYPE_TO_ID['treasury']; -``` - -### API Endpoints - -ID модулей используются в следующих API endpoints: - -- `POST /api/dle-modules/initialize-modules` - инициализация модулей -- `POST /api/dle-modules/deploy-module` - деплой модуля -- `GET /api/dle-modules/check-module-status` - проверка статуса модуля -- `POST /api/dle-history/get-extended-history` - получение истории - -### Смарт-контракт - -В смарт-контракте DLE ID модулей используются в: - -```solidity -// Добавление модуля -function createAddModuleProposal( - string memory _description, - uint256 _duration, - bytes32 _moduleId, // <-- ID модуля - address _moduleAddress, - uint256 _chainId -) external returns (uint256); - -// Проверка модуля -function isModuleActive(bytes32 _moduleId) external view returns (bool); -function getModuleAddress(bytes32 _moduleId) external view returns (address); -``` - -## Добавление новых модулей - -### 1. Определить ID модуля - -```javascript -// В backend/constants/moduleIds.js -const MODULE_IDS = { - // ... существующие модули - NEW_MODULE: '0x6e65776d6f64756c650000000000000000000000000000000000000000000000' -}; -``` - -### 2. Обновить маппинги - -```javascript -const MODULE_TYPE_TO_ID = { - // ... существующие модули - newModule: MODULE_IDS.NEW_MODULE -}; - -const MODULE_ID_TO_TYPE = { - // ... существующие модули - [MODULE_IDS.NEW_MODULE]: 'newModule' -}; - -const MODULE_NAMES = { - // ... существующие модули - newModule: 'New Module' -}; -``` - -### 3. Обновить функцию getModuleName - -```javascript -// В backend/routes/dleHistory.js -function getModuleName(moduleId) { - if (MODULE_ID_TO_TYPE[moduleId]) { - const moduleType = MODULE_ID_TO_TYPE[moduleId]; - return MODULE_NAMES[moduleType] || moduleType; - } - - const additionalModuleNames = { - // ... существующие модули - '0x6e65776d6f64756c650000000000000000000000000000000000000000000000': 'New Module' - }; - - return additionalModuleNames[moduleId] || `Module ${moduleId}`; -} -``` - -## Безопасность - -- ID модулей должны быть уникальными -- Не используйте предсказуемые ID для критических модулей -- Все изменения ID должны проходить через governance - -## Миграция - -При изменении ID модуля: - -1. Создать governance предложение для удаления старого модуля -2. Создать governance предложение для добавления нового модуля с новым ID -3. Обновить константы в коде -4. Обновить базу данных (если необходимо) - -## Примеры - -### Создание предложения для добавления модуля - -```javascript -const moduleId = MODULE_TYPE_TO_ID['treasury']; -const moduleAddress = '0x1234567890123456789012345678901234567890'; - -// Создание предложения через governance -const proposalId = await dleContract.createAddModuleProposal( - 'Добавить Treasury модуль', - 86400, // 1 день - moduleId, - moduleAddress, - 1 // Ethereum mainnet -); -``` - -### Проверка статуса модуля - -```javascript -const moduleId = MODULE_TYPE_TO_ID['treasury']; -const isActive = await dleContract.isModuleActive(moduleId); -const moduleAddress = await dleContract.getModuleAddress(moduleId); - -console.log(`Treasury модуль: ${isActive ? 'активен' : 'неактивен'}`); -console.log(`Адрес: ${moduleAddress}`); -``` diff --git a/backend/hardhat.config.js b/backend/hardhat.config.js index 7017542..e2e3f65 100644 --- a/backend/hardhat.config.js +++ b/backend/hardhat.config.js @@ -11,13 +11,39 @@ */ require('@nomicfoundation/hardhat-toolbox'); +require('@nomicfoundation/hardhat-verify'); require('hardhat-contract-sizer'); require('dotenv').config(); function getNetworks() { - // Возвращаем пустой объект, чтобы Hardhat не зависел от переменных окружения - // Сети будут настраиваться динамически в deploy-multichain.js - return {}; + // Базовая конфигурация сетей для верификации + return { + sepolia: { + url: process.env.SEPOLIA_RPC_URL || 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', + chainId: 11155111, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + }, + holesky: { + url: process.env.HOLESKY_RPC_URL || 'https://ethereum-holesky-rpc.publicnode.com', + chainId: 17000, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + }, + mainnet: { + url: process.env.MAINNET_RPC_URL || 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', + chainId: 1, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + }, + arbitrumSepolia: { + url: process.env.ARBITRUM_SEPOLIA_RPC_URL || 'https://sepolia-rollup.arbitrum.io/rpc', + chainId: 421614, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + }, + baseSepolia: { + url: process.env.BASE_SEPOLIA_RPC_URL || 'https://sepolia.base.org', + chainId: 84532, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + } + }; } module.exports = { @@ -38,16 +64,74 @@ module.exports = { }, networks: getNetworks(), etherscan: { - apiKey: { - sepolia: process.env.ETHERSCAN_API_KEY || '', - mainnet: process.env.ETHERSCAN_API_KEY || '', - polygon: process.env.POLYGONSCAN_API_KEY || '', - arbitrumOne: process.env.ARBISCAN_API_KEY || '', - bsc: process.env.BSCSCAN_API_KEY || '', - base: process.env.BASESCAN_API_KEY || '', - baseSepolia: process.env.BASESCAN_API_KEY || '', - arbitrumSepolia: process.env.ARBISCAN_API_KEY || '', - } + // Единый API ключ для V2 API + apiKey: process.env.ETHERSCAN_API_KEY || '', + customChains: [ + { + network: "sepolia", + chainId: 11155111, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://sepolia.etherscan.io" + } + }, + { + network: "holesky", + chainId: 17000, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://holesky.etherscan.io" + } + }, + { + network: "polygon", + chainId: 137, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://polygonscan.com" + } + }, + { + network: "arbitrumOne", + chainId: 42161, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://arbiscan.io" + } + }, + { + network: "arbitrumSepolia", + chainId: 421614, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://sepolia.arbiscan.io" + } + }, + { + network: "bsc", + chainId: 56, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://bscscan.com" + } + }, + { + network: "base", + chainId: 8453, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://basescan.org" + } + }, + { + network: "baseSepolia", + chainId: 84532, + urls: { + apiURL: "https://api.etherscan.io/v2/api", + browserURL: "https://sepolia.basescan.org" + } + } + ] }, solidityCoverage: { excludeContracts: [], diff --git a/backend/package.json b/backend/package.json index 96e9027..f832615 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,8 @@ "run-migrations": "node scripts/run-migrations.js", "fix-duplicates": "node scripts/fix-duplicate-identities.js", "deploy:multichain": "node scripts/deploy/deploy-multichain.js", - "deploy:complete": "node scripts/deploy/deploy-dle-complete.js" + "deploy:complete": "node scripts/deploy/deploy-dle-complete.js", + "verify:contracts": "node scripts/verify-contracts.js" }, "dependencies": { "@anthropic-ai/sdk": "^0.51.0", diff --git a/backend/routes/dleHistory.js b/backend/routes/dleHistory.js index 82a793b..dedfbd8 100644 --- a/backend/routes/dleHistory.js +++ b/backend/routes/dleHistory.js @@ -47,7 +47,6 @@ router.post('/get-extended-history', async (req, res) => { "function listSupportedChains() external view returns (uint256[] memory)", "function getProposalsCount() external view returns (uint256)", "event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)", - "event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId)", "event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)", "event ModuleAdded(bytes32 moduleId, address moduleAddress)", "event ModuleRemoved(bytes32 moduleId)", @@ -112,24 +111,6 @@ router.post('/get-extended-history', async (req, res) => { }); } - // События изменения текущей цепочки - const chainEvents = await dle.queryFilter('CurrentChainIdUpdated', fromBlock, currentBlock); - for (let i = 0; i < chainEvents.length; i++) { - const event = chainEvents[i]; - history.push({ - id: history.length + 1, - type: 'chain_updated', - title: 'Изменена текущая цепочка', - description: `Текущая цепочка изменена с ${Number(event.args.oldChainId)} на ${Number(event.args.newChainId)}`, - timestamp: event.blockNumber * 1000, - blockNumber: event.blockNumber, - transactionHash: event.transactionHash, - details: { - oldChainId: Number(event.args.oldChainId), - newChainId: Number(event.args.newChainId) - } - }); - } // События обновления информации DLE const infoEvents = await dle.queryFilter('DLEInfoUpdated', fromBlock, currentBlock); diff --git a/backend/routes/dleV2.js b/backend/routes/dleV2.js index eb81b4f..1e0f68b 100644 --- a/backend/routes/dleV2.js +++ b/backend/routes/dleV2.js @@ -12,7 +12,8 @@ const express = require('express'); const router = express.Router(); -const dleV2Service = require('../services/dleV2Service'); +const DLEV2Service = require('../services/dleV2Service'); +const dleV2Service = new DLEV2Service(); const logger = require('../utils/logger'); const auth = require('../middleware/auth'); const path = require('path'); @@ -34,7 +35,7 @@ async function executeDeploymentInBackground(deploymentId, dleParams) { stage: 'initializing' }); - deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта и модулей', 'info'); + deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info'); // Выполняем деплой с передачей deploymentId для WebSocket обновлений const result = await dleV2Service.createDLE(dleParams, deploymentId); @@ -77,11 +78,16 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => } } - // Создаем запись о деплое - const deploymentId = deploymentTracker.createDeployment(dleParams); + // Используем deploymentId из запроса, если передан, иначе создаем новый + const deploymentId = req.body.deploymentId || deploymentTracker.createDeployment(dleParams); - // Запускаем деплой в фоне (без await!) - executeDeploymentInBackground(deploymentId, dleParams); + // Если deploymentId был передан из запроса, создаем запись о деплое с этим ID + if (req.body.deploymentId) { + deploymentTracker.createDeployment(dleParams, req.body.deploymentId); + } + + // Запускаем деплой в фоне (с await для правильной обработки ошибок!) + await executeDeploymentInBackground(deploymentId, dleParams); logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`); diff --git a/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json b/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json deleted file mode 100644 index d4e9087..0000000 --- a/backend/scripts/contracts-data/modules/reader-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index 1661ff3..0000000 --- a/backend/scripts/contracts-data/modules/timelock-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index ea0b7d7..0000000 --- a/backend/scripts/contracts-data/modules/treasury-0x4e2a2b5fca4edabb537710d9682c40c3dc3e8de2.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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/debug-file-monitor.js b/backend/scripts/debug-file-monitor.js new file mode 100644 index 0000000..dea46df --- /dev/null +++ b/backend/scripts/debug-file-monitor.js @@ -0,0 +1,207 @@ +/** + * Отладочный скрипт для мониторинга файлов в процессе деплоя + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const fs = require('fs'); +const path = require('path'); + +console.log('🔍 ОТЛАДОЧНЫЙ МОНИТОР: Отслеживание файлов current-params.json'); +console.log('=' .repeat(70)); + +class FileMonitor { + constructor() { + this.watchedFiles = new Map(); + this.isMonitoring = false; + } + + startMonitoring() { + console.log('🚀 Запуск мониторинга файлов...'); + this.isMonitoring = true; + + const deployDir = path.join(__dirname, './deploy'); + const tempDir = path.join(__dirname, '../temp'); + + // Мониторим директории + this.watchDirectory(deployDir, 'deploy'); + this.watchDirectory(tempDir, 'temp'); + + // Проверяем существующие файлы + this.checkExistingFiles(); + + console.log('✅ Мониторинг запущен. Нажмите Ctrl+C для остановки.'); + } + + watchDirectory(dirPath, label) { + if (!fs.existsSync(dirPath)) { + console.log(`📁 Директория ${label} не существует: ${dirPath}`); + return; + } + + console.log(`📁 Мониторим директорию ${label}: ${dirPath}`); + + try { + const watcher = fs.watch(dirPath, (eventType, filename) => { + if (filename && filename.includes('current-params')) { + const filePath = path.join(dirPath, filename); + const timestamp = new Date().toISOString(); + + console.log(`\n🔔 ${timestamp} - ${label.toUpperCase()}:`); + console.log(` Событие: ${eventType}`); + console.log(` Файл: ${filename}`); + console.log(` Путь: ${filePath}`); + + if (eventType === 'rename' && filename) { + // Файл создан или удален + setTimeout(() => { + const exists = fs.existsSync(filePath); + console.log(` Статус: ${exists ? 'СУЩЕСТВУЕТ' : 'УДАЛЕН'}`); + + if (exists) { + try { + const stats = fs.statSync(filePath); + console.log(` Размер: ${stats.size} байт`); + console.log(` Изменен: ${stats.mtime}`); + } catch (statError) { + console.log(` Ошибка получения статистики: ${statError.message}`); + } + } + }, 100); + } + } + }); + + this.watchedFiles.set(dirPath, watcher); + console.log(`✅ Мониторинг ${label} запущен`); + + } catch (watchError) { + console.log(`❌ Ошибка мониторинга ${label}: ${watchError.message}`); + } + } + + checkExistingFiles() { + console.log('\n🔍 Проверка существующих файлов...'); + + const pathsToCheck = [ + path.join(__dirname, './deploy/current-params.json'), + path.join(__dirname, '../temp'), + path.join(__dirname, './deploy') + ]; + + pathsToCheck.forEach(checkPath => { + try { + if (fs.existsSync(checkPath)) { + const stats = fs.statSync(checkPath); + if (stats.isFile()) { + console.log(`📄 Файл найден: ${checkPath}`); + console.log(` Размер: ${stats.size} байт`); + console.log(` Создан: ${stats.birthtime}`); + console.log(` Изменен: ${stats.mtime}`); + } else if (stats.isDirectory()) { + console.log(`📁 Директория найдена: ${checkPath}`); + const files = fs.readdirSync(checkPath); + const currentParamsFiles = files.filter(f => f.includes('current-params')); + if (currentParamsFiles.length > 0) { + console.log(` Файлы current-params: ${currentParamsFiles.join(', ')}`); + } else { + console.log(` Файлы current-params: не найдены`); + } + } + } else { + console.log(`❌ Не найден: ${checkPath}`); + } + } catch (error) { + console.log(`⚠️ Ошибка проверки ${checkPath}: ${error.message}`); + } + }); + } + + stopMonitoring() { + console.log('\n🛑 Остановка мониторинга...'); + this.isMonitoring = false; + + this.watchedFiles.forEach((watcher, path) => { + try { + watcher.close(); + console.log(`✅ Мониторинг остановлен: ${path}`); + } catch (error) { + console.log(`❌ Ошибка остановки мониторинга ${path}: ${error.message}`); + } + }); + + this.watchedFiles.clear(); + console.log('✅ Мониторинг полностью остановлен'); + } + + // Метод для периодической проверки + startPeriodicCheck(intervalMs = 5000) { + console.log(`⏰ Запуск периодической проверки (каждые ${intervalMs}ms)...`); + + const checkInterval = setInterval(() => { + if (!this.isMonitoring) { + clearInterval(checkInterval); + return; + } + + this.performPeriodicCheck(); + }, intervalMs); + + return checkInterval; + } + + performPeriodicCheck() { + const timestamp = new Date().toISOString(); + console.log(`\n⏰ ${timestamp} - Периодическая проверка:`); + + const filesToCheck = [ + path.join(__dirname, './deploy/current-params.json'), + path.join(__dirname, './deploy'), + path.join(__dirname, '../temp') + ]; + + filesToCheck.forEach(filePath => { + try { + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + if (stats.isFile()) { + console.log(` 📄 ${path.basename(filePath)}: ${stats.size} байт`); + } else if (stats.isDirectory()) { + const files = fs.readdirSync(filePath); + const currentParamsFiles = files.filter(f => f.includes('current-params')); + console.log(` 📁 ${path.basename(filePath)}: ${files.length} файлов, current-params: ${currentParamsFiles.length}`); + } + } else { + console.log(` ❌ ${path.basename(filePath)}: не существует`); + } + } catch (error) { + console.log(` ⚠️ ${path.basename(filePath)}: ошибка ${error.message}`); + } + }); + } +} + +// Создаем экземпляр монитора +const monitor = new FileMonitor(); + +// Обработка сигналов завершения +process.on('SIGINT', () => { + console.log('\n🛑 Получен сигнал SIGINT...'); + monitor.stopMonitoring(); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\n🛑 Получен сигнал SIGTERM...'); + monitor.stopMonitoring(); + process.exit(0); +}); + +// Запускаем мониторинг +monitor.startMonitoring(); +monitor.startPeriodicCheck(3000); // Проверка каждые 3 секунды + +console.log('\n💡 Инструкции:'); +console.log(' - Запустите этот скрипт в отдельном терминале'); +console.log(' - Затем запустите деплой DLE в другом терминале'); +console.log(' - Наблюдайте за изменениями файлов в реальном времени'); +console.log(' - Нажмите Ctrl+C для остановки мониторинга'); diff --git a/backend/scripts/deploy/deploy-modules.js b/backend/scripts/deploy/deploy-modules.js new file mode 100644 index 0000000..b4f129b --- /dev/null +++ b/backend/scripts/deploy/deploy-modules.js @@ -0,0 +1,617 @@ +/** + * 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'); +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; +} + +// Конфигурация модулей для деплоя +const MODULE_CONFIGS = { + treasury: { + contractName: 'TreasuryModule', + constructorArgs: (dleAddress, chainId, walletAddress) => [ + dleAddress, // _dleContract + chainId, // _chainId + walletAddress // _emergencyAdmin + ], + verificationArgs: (dleAddress, chainId, walletAddress) => [ + dleAddress, // _dleContract + chainId, // _chainId + walletAddress // _emergencyAdmin + ] + }, + timelock: { + contractName: 'TimelockModule', + constructorArgs: (dleAddress) => [ + dleAddress // _dleContract + ], + verificationArgs: (dleAddress) => [ + dleAddress // _dleContract + ] + }, + reader: { + contractName: 'DLEReader', + constructorArgs: (dleAddress) => [ + dleAddress // _dleContract + ], + verificationArgs: (dleAddress) => [ + dleAddress // _dleContract + ] + } + // Здесь можно легко добавлять новые модули: + // newModule: { + // contractName: 'NewModule', + // constructorArgs: (dleAddress, ...otherArgs) => [dleAddress, ...otherArgs], + // verificationArgs: (dleAddress, ...otherArgs) => [dleAddress, ...otherArgs] + // } +}; + +// Деплой модуля в одной сети с CREATE2 +async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce, moduleInit, moduleType) { + const { ethers } = hre; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(pk, provider); + const net = await provider.getNetwork(); + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType}...`); + + // 1) Выравнивание nonce до targetNonce нулевыми транзакциями (если нужно) + let current = await provider.getTransactionCount(wallet.address, 'pending'); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetNonce}`); + + if (current > targetNonce) { + throw new Error(`Current nonce ${current} > targetNonce ${targetNonce} on chainId=${Number(net.chainId)}`); + } + + if (current < targetNonce) { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetNonce} (${targetNonce - current} transactions needed)`); + + // Используем burn address для более надежных транзакций + const burnAddress = "0x000000000000000000000000000000000000dEaD"; + + while (current < targetNonce) { + 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, + value: 0n, + nonce: current, + gasLimit, + ...overrides + }; + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); + const txFill = await wallet.sendTransaction(txReq); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); + await txFill.wait(); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); + sent = true; + } catch (e) { + lastErr = e; + console.log(`[MODULES_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; + continue; + } + + if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) { + current = await provider.getTransactionCount(wallet.address, 'pending'); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); + continue; + } + + throw e; + } + } + + if (!sent) { + console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); + throw lastErr || new Error('filler tx failed'); + } + + current++; + } + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); + } else { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); + } + + // 2) Деплой модуля напрямую на согласованном nonce + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType} directly with nonce=${targetNonce}`); + + const feeOverrides = await getFeeOverrides(provider); + let gasLimit; + + try { + // Оцениваем газ для деплоя модуля + const est = await wallet.estimateGas({ data: moduleInit, ...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 : 1_000_000n; + const fallbackGas = maxByBalance > 2_000_000n ? 2_000_000n : (maxByBalance < 500_000n ? 500_000n : maxByBalance); + gasLimit = est ? (est + est / 5n) : fallbackGas; + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); + } catch (_) { + gasLimit = 1_000_000n; + } + + // Вычисляем предсказанный адрес модуля + const predictedAddress = ethers.getCreateAddress({ + from: wallet.address, + nonce: targetNonce + }); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} predicted ${moduleType} address=${predictedAddress}`); + + // Проверяем, не развернут ли уже контракт + const existingCode = await provider.getCode(predictedAddress); + if (existingCode && existingCode !== '0x') { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} already exists at predictedAddress, skip deploy`); + return { address: predictedAddress, chainId: Number(net.chainId) }; + } + + // Деплоим модуль + let tx; + try { + tx = await wallet.sendTransaction({ + data: moduleInit, + nonce: targetNonce, + gasLimit, + ...feeOverrides + }); + } catch (e) { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`); + // Повторная попытка с обновленным nonce + const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending'); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`); + tx = await wallet.sendTransaction({ + data: moduleInit, + nonce: updatedNonce, + gasLimit, + ...feeOverrides + }); + } + + const rc = await tx.wait(); + const deployedAddress = rc.contractAddress || predictedAddress; + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployed at=${deployedAddress}`); + return { address: deployedAddress, chainId: Number(net.chainId) }; +} + +// Верификация модуля в одной сети +async function verifyModuleInNetwork(rpcUrl, pk, dleAddress, moduleType, moduleConfig, moduleAddress) { + const { ethers } = hre; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(pk, provider); + const net = await provider.getNetwork(); + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} verifying ${moduleConfig.contractName}...`); + + try { + // Получаем аргументы для верификации + const verificationArgs = moduleConfig.verificationArgs(dleAddress, Number(net.chainId), wallet.address); + + await hre.run("verify:verify", { + address: moduleAddress, + constructorArguments: verificationArgs, + }); + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleConfig.contractName} verification successful`); + return 'success'; + } catch (error) { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleConfig.contractName} verification failed: ${error.message}`); + return 'failed'; + } +} + +// Деплой всех модулей в одной сети +async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) { + const { ethers } = hre; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(pk, provider); + const net = await provider.getNetwork(); + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying modules: ${modulesToDeploy.join(', ')}`); + + const results = {}; + + for (let i = 0; i < modulesToDeploy.length; i++) { + const moduleType = modulesToDeploy[i]; + const moduleInit = moduleInits[moduleType]; + const targetNonce = targetNonces[moduleType]; + + if (!MODULE_CONFIGS[moduleType]) { + console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`); + results[moduleType] = { success: false, error: `Unknown module type: ${moduleType}` }; + continue; + } + + if (!moduleInit) { + console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`); + results[moduleType] = { success: false, error: `No init code for module: ${moduleType}` }; + continue; + } + + try { + const result = await deployModuleInNetwork(rpcUrl, pk, salt, null, targetNonce, moduleInit, moduleType); + results[moduleType] = { ...result, success: true }; + } catch (error) { + console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message); + results[moduleType] = { + chainId: Number(net.chainId), + success: false, + error: error.message + }; + } + } + + return { + chainId: Number(net.chainId), + modules: results + }; +} + +// Верификация всех модулей в одной сети +async function verifyAllModulesInNetwork(rpcUrl, pk, dleAddress, moduleResults, modulesToVerify) { + const { ethers } = hre; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(pk, provider); + const net = await provider.getNetwork(); + + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} verifying modules: ${modulesToVerify.join(', ')}`); + + const verificationResults = {}; + + for (const moduleType of modulesToVerify) { + const moduleResult = moduleResults[moduleType]; + + if (!moduleResult || !moduleResult.success || !moduleResult.address) { + console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} skipping verification for ${moduleType} - deployment failed`); + verificationResults[moduleType] = 'skipped'; + continue; + } + + if (!MODULE_CONFIGS[moduleType]) { + console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type for verification: ${moduleType}`); + verificationResults[moduleType] = 'unknown_module'; + continue; + } + + const moduleConfig = MODULE_CONFIGS[moduleType]; + const verification = await verifyModuleInNetwork(rpcUrl, pk, dleAddress, moduleType, moduleConfig, moduleResult.address); + verificationResults[moduleType] = verification; + } + + return { + chainId: Number(net.chainId), + modules: verificationResults + }; +} + +// Деплой всех модулей во всех сетях +async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) { + const results = []; + + for (let i = 0; i < networks.length; i++) { + const rpcUrl = networks[i]; + console.log(`[MODULES_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`); + + const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces); + results.push(result); + } + + return results; +} + +// Верификация всех модулей во всех сетях +async function verifyAllModulesInAllNetworks(networks, pk, dleAddress, deployResults, modulesToVerify) { + const verificationResults = []; + + for (let i = 0; i < networks.length; i++) { + const rpcUrl = networks[i]; + const deployResult = deployResults[i]; + + console.log(`[MODULES_DBG] verifying modules in network ${i + 1}/${networks.length}: ${rpcUrl}`); + + const verification = await verifyAllModulesInNetwork(rpcUrl, pk, dleAddress, deployResult.modules, modulesToVerify); + verificationResults.push(verification); + } + + return verificationResults; +} + +async function main() { + const { ethers } = hre; + + // Загружаем параметры из базы данных или файла + let params; + + try { + // Пытаемся загрузить из базы данных + const DeployParamsService = require('../../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + + // Получаем последние параметры деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + params = latestParams[0]; + console.log('✅ Параметры загружены из базы данных'); + } else { + throw new Error('Параметры деплоя не найдены в базе данных'); + } + + await deployParamsService.close(); + } catch (dbError) { + console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message); + + // Fallback к файлу + const paramsPath = path.join(__dirname, './current-params.json'); + if (!fs.existsSync(paramsPath)) { + throw new Error('Файл параметров не найден: ' + paramsPath); + } + + params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); + console.log('✅ Параметры загружены из файла'); + } + console.log('[MODULES_DBG] Загружены параметры:', { + name: params.name, + symbol: params.symbol, + supportedChainIds: params.supportedChainIds, + CREATE2_SALT: params.CREATE2_SALT + }); + + const pk = process.env.PRIVATE_KEY; + const networks = params.rpcUrls || params.rpc_urls || []; + const dleAddress = params.dleAddress; + const salt = params.CREATE2_SALT || params.create2_salt; + + // Модули для деплоя (можно настроить через параметры) + const modulesToDeploy = params.modulesToDeploy || ['treasury', 'timelock', 'reader']; + + if (!pk) throw new Error('Env: PRIVATE_KEY'); + if (!dleAddress) throw new Error('DLE_ADDRESS not found in params'); + if (!salt) throw new Error('CREATE2_SALT not found in params'); + if (networks.length === 0) throw new Error('RPC URLs not found in params'); + + console.log(`[MODULES_DBG] Starting modules deployment to ${networks.length} networks`); + console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`); + console.log(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`); + console.log(`[MODULES_DBG] Networks:`, networks); + + // Проверяем, что все модули поддерживаются + const unsupportedModules = modulesToDeploy.filter(module => !MODULE_CONFIGS[module]); + if (unsupportedModules.length > 0) { + throw new Error(`Unsupported modules: ${unsupportedModules.join(', ')}. Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`); + } + + // Подготовим init код для каждого модуля + const moduleInits = {}; + const moduleInitCodeHashes = {}; + + for (const moduleType of modulesToDeploy) { + const moduleConfig = MODULE_CONFIGS[moduleType]; + const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName); + + // Получаем аргументы конструктора для первой сети (для расчета init кода) + const firstProvider = new hre.ethers.JsonRpcProvider(networks[0]); + const firstWallet = new hre.ethers.Wallet(pk, firstProvider); + const firstNetwork = await firstProvider.getNetwork(); + const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(firstNetwork.chainId), firstWallet.address); + + const deployTx = await ContractFactory.getDeployTransaction(...constructorArgs); + moduleInits[moduleType] = deployTx.data; + moduleInitCodeHashes[moduleType] = ethers.keccak256(deployTx.data); + + console.log(`[MODULES_DBG] ${moduleType} init code prepared, hash: ${moduleInitCodeHashes[moduleType]}`); + } + + // Подготовим провайдеры и вычислим общие nonce для каждого модуля + 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); + } + + // Вычисляем target nonce для каждого модуля + const targetNonces = {}; + let currentMaxNonce = Math.max(...nonces); + + for (const moduleType of modulesToDeploy) { + targetNonces[moduleType] = currentMaxNonce; + currentMaxNonce++; // каждый следующий модуль получает nonce +1 + } + + console.log(`[MODULES_DBG] nonces=${JSON.stringify(nonces)} targetNonces=${JSON.stringify(targetNonces)}`); + + // ПАРАЛЛЕЛЬНЫЙ деплой всех модулей во всех сетях одновременно + console.log(`[MODULES_DBG] Starting PARALLEL deployment of all modules to ${networks.length} networks`); + + const deploymentPromises = networks.map(async (rpcUrl, networkIndex) => { + console.log(`[MODULES_DBG] 🚀 Starting deployment to network ${networkIndex + 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(`[MODULES_DBG] 📡 Network ${networkIndex + 1} chainId: ${chainId}`); + + const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces); + console.log(`[MODULES_DBG] ✅ Network ${networkIndex + 1} (chainId: ${chainId}) deployment SUCCESS`); + return { rpcUrl, chainId, ...result }; + } catch (error) { + console.error(`[MODULES_DBG] ❌ Network ${networkIndex + 1} deployment FAILED:`, error.message); + return { rpcUrl, error: error.message }; + } + }); + + // Ждем завершения всех деплоев + const deployResults = await Promise.all(deploymentPromises); + console.log(`[MODULES_DBG] All ${networks.length} deployments completed`); + + // Логируем результаты деплоя для каждой сети + deployResults.forEach((result, index) => { + if (result.modules) { + console.log(`[MODULES_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS`); + Object.entries(result.modules).forEach(([moduleType, moduleResult]) => { + if (moduleResult.success) { + console.log(`[MODULES_DBG] ✅ ${moduleType}: ${moduleResult.address}`); + } else { + console.log(`[MODULES_DBG] ❌ ${moduleType}: ${moduleResult.error}`); + } + }); + } else { + console.log(`[MODULES_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`); + } + }); + + // Проверяем, что все адреса модулей одинаковые в каждой сети + for (const moduleType of modulesToDeploy) { + const addresses = deployResults + .filter(r => r.modules && r.modules[moduleType] && r.modules[moduleType].success) + .map(r => r.modules[moduleType].address); + const uniqueAddresses = [...new Set(addresses)]; + + console.log(`[MODULES_DBG] ${moduleType} addresses:`, addresses); + console.log(`[MODULES_DBG] ${moduleType} unique addresses:`, uniqueAddresses); + + if (uniqueAddresses.length > 1) { + console.error(`[MODULES_DBG] ERROR: ${moduleType} addresses are different across networks!`); + console.error(`[MODULES_DBG] addresses:`, uniqueAddresses); + throw new Error(`Nonce alignment failed for ${moduleType} - addresses are different`); + } + + if (uniqueAddresses.length === 0) { + console.error(`[MODULES_DBG] ERROR: No successful ${moduleType} deployments!`); + throw new Error(`No successful ${moduleType} deployments`); + } + + console.log(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]); + } + + // Верификация во всех сетях + console.log(`[MODULES_DBG] Starting verification in all networks...`); + const verificationResults = await verifyAllModulesInAllNetworks(networks, pk, dleAddress, deployResults, modulesToDeploy); + + // Объединяем результаты + const finalResults = deployResults.map((deployResult, index) => ({ + ...deployResult, + modules: deployResult.modules ? Object.keys(deployResult.modules).reduce((acc, moduleType) => { + acc[moduleType] = { + ...deployResult.modules[moduleType], + verification: verificationResults[index]?.modules?.[moduleType] || 'unknown' + }; + return acc; + }, {}) : {} + })); + + console.log('MODULES_DEPLOY_RESULT', JSON.stringify(finalResults)); + + // Сохраняем результаты в отдельные файлы для каждого модуля + const dleDir = path.join(__dirname, '../contracts-data/modules'); + if (!fs.existsSync(dleDir)) { + fs.mkdirSync(dleDir, { recursive: true }); + } + + // Создаем файл для каждого модуля + for (const moduleType of modulesToDeploy) { + const moduleInfo = { + moduleType: moduleType, + dleAddress: dleAddress, + networks: [], + deployTimestamp: new Date().toISOString(), + // Добавляем данные из основного DLE контракта + dleName: params.name, + dleSymbol: params.symbol, + dleLocation: params.location, + dleJurisdiction: params.jurisdiction + }; + + // Собираем информацию о всех сетях для этого модуля + for (let i = 0; i < networks.length; i++) { + const rpcUrl = networks[i]; + const deployResult = deployResults[i]; + const verificationResult = verificationResults[i]; + const moduleResult = deployResult.modules?.[moduleType]; + const verification = verificationResult?.modules?.[moduleType] || 'unknown'; + + try { + const provider = new hre.ethers.JsonRpcProvider(rpcUrl); + const network = await provider.getNetwork(); + + moduleInfo.networks.push({ + chainId: Number(network.chainId), + rpcUrl: rpcUrl, + address: moduleResult?.success ? moduleResult.address : null, + verification: verification, + success: moduleResult?.success || false, + error: moduleResult?.error || null + }); + } catch (error) { + console.error(`[MODULES_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message); + moduleInfo.networks.push({ + chainId: null, + rpcUrl: rpcUrl, + address: null, + verification: 'error', + success: false, + error: error.message + }); + } + } + + // Сохраняем файл модуля + const fileName = `${moduleType}-${dleAddress.toLowerCase()}.json`; + const filePath = path.join(dleDir, fileName); + fs.writeFileSync(filePath, JSON.stringify(moduleInfo, null, 2)); + console.log(`[MODULES_DBG] ${moduleType} info saved to: ${filePath}`); + } + + console.log('[MODULES_DBG] All modules deployment completed!'); + console.log(`[MODULES_DBG] Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`); + console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`); + console.log(`[MODULES_DBG] DLE Name: ${params.name}`); + console.log(`[MODULES_DBG] DLE Symbol: ${params.symbol}`); + console.log(`[MODULES_DBG] DLE Location: ${params.location}`); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/backend/scripts/deploy/deploy-multichain.js b/backend/scripts/deploy/deploy-multichain.js index 5ed17a1..af60427 100755 --- a/backend/scripts/deploy/deploy-multichain.js +++ b/backend/scripts/deploy/deploy-multichain.js @@ -14,6 +14,7 @@ const hre = require('hardhat'); const path = require('path'); const fs = require('fs'); +const { getRpcUrlByChainId } = require('../../services/rpcProviderService'); // Подбираем безопасные gas/fee для разных сетей (включая L2) async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { @@ -32,7 +33,7 @@ async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20 return overrides; } -async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit) { +async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params) { const { ethers } = hre; const provider = new ethers.JsonRpcProvider(rpcUrl); const wallet = new ethers.Wallet(pk, provider); @@ -162,6 +163,28 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d 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`); + + // Проверяем и инициализируем логотип для существующего контракта + if (params.logoURI && params.logoURI !== '') { + try { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`); + const DLE = await hre.ethers.getContractFactory('DLE'); + const dleContract = DLE.attach(predictedAddress); + + const currentLogo = await dleContract.logoURI(); + if (currentLogo === '' || currentLogo === '0x') { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI for existing contract: ${params.logoURI}`); + const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides); + await logoTx.wait(); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized for existing contract`); + } else { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI already set: ${currentLogo}`); + } + } catch (error) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed for existing contract: ${error.message}`); + } + } + return { address: predictedAddress, chainId: Number(net.chainId) }; } @@ -191,303 +214,74 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d const deployedAddress = rc.contractAddress || predictedAddress; console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`); + + // Инициализация логотипа если он указан + if (params.logoURI && params.logoURI !== '') { + try { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`); + const DLE = await hre.ethers.getContractFactory('DLE'); + const dleContract = DLE.attach(deployedAddress); + + const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides); + await logoTx.wait(); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized successfully`); + } catch (error) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${error.message}`); + // Не прерываем деплой из-за ошибки логотипа + } + } else { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} no logoURI specified, skipping initialization`); + } + 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) { - 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}`); - - // Модули деплоятся отдельно, инициализация через governance - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Modules deployed successfully, initialization will be done through governance proposals`); - } 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); - // Даже при ошибке увеличиваем 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; - // Загружаем параметры из файла + // Загружаем параметры из базы данных или файла + let params; + + try { + // Пытаемся загрузить из базы данных + const DeployParamsService = require('../../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + + // Проверяем, передан ли конкретный deploymentId + const deploymentId = process.env.DEPLOYMENT_ID; + if (deploymentId) { + console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`); + params = await deployParamsService.getDeployParams(deploymentId); + if (params) { + console.log('✅ Параметры загружены из базы данных по deploymentId'); + } else { + throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`); + } + } else { + // Получаем последние параметры деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + params = latestParams[0]; + console.log('✅ Параметры загружены из базы данных (последние)'); + } else { + throw new Error('Параметры деплоя не найдены в базе данных'); + } + } + + await deployParamsService.close(); + } catch (dbError) { + console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message); + + // Fallback к файлу const paramsPath = path.join(__dirname, './current-params.json'); if (!fs.existsSync(paramsPath)) { throw new Error('Файл параметров не найден: ' + paramsPath); } - const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); + params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); + console.log('✅ Параметры загружены из файла'); + } console.log('[MULTI_DBG] Загружены параметры:', { name: params.name, symbol: params.symbol, @@ -495,16 +289,16 @@ async function main() { CREATE2_SALT: params.CREATE2_SALT }); - const pk = process.env.PRIVATE_KEY; - const salt = params.CREATE2_SALT; - const networks = params.rpcUrls || []; + const pk = params.private_key || process.env.PRIVATE_KEY; + const salt = params.CREATE2_SALT || params.create2_salt; + const networks = params.rpcUrls || params.rpc_urls || []; 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 DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE'); const dleConfig = { name: params.name || '', symbol: params.symbol || '', @@ -516,7 +310,7 @@ async function main() { kpp: params.kpp ? BigInt(params.kpp) : 0n, quorumPercentage: params.quorumPercentage || 51, initialPartners: params.initialPartners || [], - initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)), + initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(10**18)), 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"); @@ -559,7 +353,7 @@ async function main() { console.log(`[MULTI_DBG] 📡 Network ${i + 1} chainId: ${chainId}`); - const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit); + const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params); console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`); return { rpcUrl, chainId, ...r }; } catch (error) { @@ -603,76 +397,51 @@ async function main() { console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]); - // Деплой модулей ОТКЛЮЧЕН - модули будут деплоиться отдельно - console.log('[MULTI_DBG] Module deployment DISABLED - modules will be deployed separately'); - const moduleResults = []; - const verificationResults = []; + // Автоматическая верификация контрактов + let verificationResults = []; + + console.log(`[MULTI_DBG] autoVerifyAfterDeploy: ${params.autoVerifyAfterDeploy}`); + + if (params.autoVerifyAfterDeploy) { + console.log('[MULTI_DBG] Starting automatic contract verification...'); + + try { + // Импортируем функцию верификации + const { verifyWithHardhatV2 } = require('../verify-with-hardhat-v2'); + + // Подготавливаем данные о развернутых сетях + const deployedNetworks = results + .filter(result => result.address && !result.error) + .map(result => ({ + chainId: result.chainId, + address: result.address + })); + + // Запускаем верификацию с данными о сетях + await verifyWithHardhatV2(params, deployedNetworks); + + // Если верификация прошла успешно, отмечаем все как верифицированные + verificationResults = networks.map(() => 'verified'); + console.log('[MULTI_DBG] ✅ Automatic verification completed successfully'); + + } catch (verificationError) { + console.error('[MULTI_DBG] ❌ Automatic verification failed:', verificationError.message); + verificationResults = networks.map(() => 'verification_failed'); + } + } else { + console.log('[MULTI_DBG] Contract verification disabled (autoVerifyAfterDeploy: false)'); + verificationResults = networks.map(() => 'disabled'); + } // Объединяем результаты const finalResults = results.map((result, index) => ({ ...result, - modules: moduleResults[index] || {}, - verification: verificationResults[index] || {} + verification: verificationResults[index] || 'failed' })); console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults)); - // Сохраняем каждый модуль в отдельный файл - const dleAddress = uniqueAddresses[0]; - const modulesDir = path.join(__dirname, '../contracts-data/modules'); - if (!fs.existsSync(modulesDir)) { - fs.mkdirSync(modulesDir, { recursive: true }); - } - - // Создаем файлы для каждого типа модуля - 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]; - - 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}`); - } - - console.log(`[MULTI_DBG] All modules saved to separate files in: ${modulesDir}`); + console.log('[MULTI_DBG] DLE deployment completed successfully!'); } main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/backend/scripts/run-all-tests.js b/backend/scripts/run-all-tests.js new file mode 100644 index 0000000..0745f31 --- /dev/null +++ b/backend/scripts/run-all-tests.js @@ -0,0 +1,130 @@ +/** + * Главный скрипт для запуска всех тестов + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const { spawn } = require('child_process'); +const path = require('path'); + +console.log('🧪 ЗАПУСК ВСЕХ ТЕСТОВ ДЛЯ ВЫЯВЛЕНИЯ ПРОБЛЕМЫ'); +console.log('=' .repeat(70)); + +const tests = [ + { + name: 'Тест создания файла', + script: './test-file-creation.js', + description: 'Проверяет базовое создание и обновление файла current-params.json' + }, + { + name: 'Тест полного потока деплоя', + script: './test-deploy-flow.js', + description: 'Имитирует полный процесс деплоя DLE с созданием файла' + }, + { + name: 'Тест сохранения файла', + script: './test-file-persistence.js', + description: 'Проверяет сохранение файла после различных операций' + }, + { + name: 'Тест обработки ошибок', + script: './test-error-handling.js', + description: 'Проверяет поведение при ошибках деплоя' + } +]; + +async function runTest(testInfo, index) { + return new Promise((resolve, reject) => { + console.log(`\n${index + 1}️⃣ ${testInfo.name}`); + console.log(`📝 ${testInfo.description}`); + console.log('─'.repeat(50)); + + const testPath = path.join(__dirname, testInfo.script); + const testProcess = spawn('node', [testPath], { + stdio: 'inherit', + cwd: __dirname + }); + + testProcess.on('close', (code) => { + if (code === 0) { + console.log(`✅ ${testInfo.name} - УСПЕШНО`); + resolve(true); + } else { + console.log(`❌ ${testInfo.name} - ОШИБКА (код: ${code})`); + resolve(false); + } + }); + + testProcess.on('error', (error) => { + console.log(`❌ ${testInfo.name} - ОШИБКА ЗАПУСКА: ${error.message}`); + resolve(false); + }); + }); +} + +async function runAllTests() { + console.log('🚀 Запуск всех тестов...\n'); + + const results = []; + + for (let i = 0; i < tests.length; i++) { + const result = await runTest(tests[i], i); + results.push({ + name: tests[i].name, + success: result + }); + + // Небольшая пауза между тестами + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + // Итоговый отчет + console.log('\n' + '='.repeat(70)); + console.log('📊 ИТОГОВЫЙ ОТЧЕТ ТЕСТОВ'); + console.log('='.repeat(70)); + + const successfulTests = results.filter(r => r.success).length; + const totalTests = results.length; + + results.forEach((result, index) => { + const status = result.success ? '✅' : '❌'; + console.log(`${index + 1}. ${status} ${result.name}`); + }); + + console.log(`\n📈 Результаты: ${successfulTests}/${totalTests} тестов прошли успешно`); + + if (successfulTests === totalTests) { + console.log('🎉 ВСЕ ТЕСТЫ ПРОШЛИ УСПЕШНО!'); + console.log('💡 Проблема НЕ в базовых операциях с файлами'); + console.log('🔍 Возможные причины проблемы:'); + console.log(' - Процесс деплоя прерывается до создания файла'); + console.log(' - Ошибка в логике dleV2Service.js'); + console.log(' - Проблема с правами доступа к файлам'); + console.log(' - Конфликт с другими процессами'); + } else { + console.log('⚠️ НЕКОТОРЫЕ ТЕСТЫ НЕ ПРОШЛИ'); + console.log('🔍 Это поможет локализовать проблему'); + } + + console.log('\n🛠️ СЛЕДУЮЩИЕ ШАГИ:'); + console.log('1. Запустите: node debug-file-monitor.js (в отдельном терминале)'); + console.log('2. Запустите деплой DLE в другом терминале'); + console.log('3. Наблюдайте за созданием/удалением файлов в реальном времени'); + console.log('4. Проверьте логи Docker: docker logs dapp-backend -f'); + + console.log('\n📋 ДОПОЛНИТЕЛЬНЫЕ КОМАНДЫ ДЛЯ ОТЛАДКИ:'); + console.log('# Проверить права доступа к директориям:'); + console.log('ls -la backend/scripts/deploy/'); + console.log('ls -la backend/temp/'); + console.log(''); + console.log('# Проверить процессы Node.js:'); + console.log('ps aux | grep node'); + console.log(''); + console.log('# Проверить использование диска:'); + console.log('df -h backend/scripts/deploy/'); +} + +// Запускаем все тесты +runAllTests().catch(error => { + console.error('❌ КРИТИЧЕСКАЯ ОШИБКА:', error.message); + process.exit(1); +}); diff --git a/backend/scripts/verify-with-hardhat-v2.js b/backend/scripts/verify-with-hardhat-v2.js new file mode 100644 index 0000000..b033be9 --- /dev/null +++ b/backend/scripts/verify-with-hardhat-v2.js @@ -0,0 +1,240 @@ +/** + * Верификация контрактов с Hardhat V2 API + */ + +const { execSync } = require('child_process'); +const DeployParamsService = require('../services/deployParamsService'); + +async function verifyWithHardhatV2(params = null, deployedNetworks = null) { + console.log('🚀 Запуск верификации с Hardhat V2...'); + + try { + // Если параметры не переданы, получаем их из базы данных + if (!params) { + const DeployParamsService = require('../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + const latestParams = await deployParamsService.getLatestDeployParams(1); + + if (latestParams.length === 0) { + throw new Error('Нет параметров деплоя в базе данных'); + } + + params = latestParams[0]; + } + + if (!params.etherscan_api_key) { + throw new Error('Etherscan API ключ не найден в параметрах'); + } + + console.log('📋 Параметры деплоя:', { + deploymentId: params.deployment_id, + name: params.name, + symbol: params.symbol + }); + + // Получаем адреса контрактов + let networks = []; + + if (deployedNetworks && Array.isArray(deployedNetworks)) { + // Используем переданные данные о сетях + networks = deployedNetworks; + console.log('📊 Используем переданные данные о развернутых сетях'); + } else if (params.deployedNetworks && Array.isArray(params.deployedNetworks)) { + networks = params.deployedNetworks; + } else if (params.dle_address && params.supportedChainIds) { + // Создаем deployedNetworks на основе dle_address и supportedChainIds + networks = params.supportedChainIds.map(chainId => ({ + chainId: chainId, + address: params.dle_address + })); + console.log('📊 Создали deployedNetworks на основе dle_address и supportedChainIds'); + } else { + throw new Error('Нет данных о развернутых сетях или адресе контракта'); + } + console.log(`🌐 Найдено ${networks.length} развернутых сетей`); + + // Маппинг chainId на названия сетей + const networkMap = { + 1: 'mainnet', + 11155111: 'sepolia', + 17000: 'holesky', + 137: 'polygon', + 42161: 'arbitrumOne', + 421614: 'arbitrumSepolia', + 56: 'bsc', + 8453: 'base', + 84532: 'baseSepolia' + }; + + // Подготавливаем аргументы конструктора + const constructorArgs = [ + { + 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 ? params.kpp : 0, + quorumPercentage: params.quorumPercentage || 51, + initialPartners: params.initialPartners || [], + initialAmounts: (params.initialAmounts || []).map(amount => (parseFloat(amount) * 10**18).toString()), + supportedChainIds: (params.supportedChainIds || []).map(id => id.toString()) + }, + (params.currentChainId || params.supportedChainIds?.[0] || 1).toString(), + params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000" + ]; + + console.log('📊 Аргументы конструктора подготовлены'); + + // Верифицируем контракт в каждой сети + const verificationResults = []; + + for (const network of networks) { + const { chainId, address } = network; + + if (!address || address === '0x0000000000000000000000000000000000000000') { + console.log(`⚠️ Пропускаем сеть ${chainId} - нет адреса контракта`); + verificationResults.push({ + success: false, + network: chainId, + error: 'No contract address' + }); + continue; + } + + const networkName = networkMap[chainId]; + if (!networkName) { + console.log(`⚠️ Неизвестная сеть ${chainId}, пропускаем верификацию`); + verificationResults.push({ + success: false, + network: chainId, + error: 'Unknown network' + }); + continue; + } + + console.log(`\n🔍 Верификация в сети ${networkName} (chainId: ${chainId})`); + console.log(`📍 Адрес: ${address}`); + + // Добавляем задержку между верификациями + if (verificationResults.length > 0) { + console.log('⏳ Ждем 5 секунд перед следующей верификацией...'); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + // Создаем временный файл с аргументами конструктора + const fs = require('fs'); + const path = require('path'); + const argsFile = path.join(__dirname, `constructor-args-${Date.now()}.json`); + + try { + fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2)); + + // Формируем команду верификации с файлом аргументов + const command = `ETHERSCAN_API_KEY="${params.etherscan_api_key}" npx hardhat verify --network ${networkName} ${address} --constructor-args ${argsFile}`; + + console.log(`💻 Выполняем команду: npx hardhat verify --network ${networkName} ${address} --constructor-args ${argsFile}`); + + const output = execSync(command, { + cwd: '/app', + encoding: 'utf8', + stdio: 'pipe' + }); + + console.log('✅ Верификация успешна:'); + console.log(output); + + verificationResults.push({ + success: true, + network: networkName, + chainId: chainId + }); + + // Удаляем временный файл + try { + fs.unlinkSync(argsFile); + } catch (e) { + console.log(`⚠️ Не удалось удалить временный файл: ${argsFile}`); + } + + } catch (error) { + // Удаляем временный файл в случае ошибки + try { + fs.unlinkSync(argsFile); + } catch (e) { + console.log(`⚠️ Не удалось удалить временный файл: ${argsFile}`); + } + + const errorOutput = error.stdout || error.stderr || error.message; + console.log('📥 Вывод команды:'); + console.log(errorOutput); + + if (errorOutput.includes('Already Verified')) { + console.log('ℹ️ Контракт уже верифицирован'); + verificationResults.push({ + success: true, + network: networkName, + chainId: chainId, + alreadyVerified: true + }); + } else if (errorOutput.includes('Successfully verified')) { + console.log('✅ Контракт успешно верифицирован!'); + verificationResults.push({ + success: true, + network: networkName, + chainId: chainId + }); + } else { + console.log('❌ Ошибка верификации'); + verificationResults.push({ + success: false, + network: networkName, + chainId: chainId, + error: errorOutput + }); + } + } + } + + // Выводим итоговые результаты + console.log('\n📊 Итоговые результаты верификации:'); + const successful = verificationResults.filter(r => r.success).length; + const failed = verificationResults.filter(r => !r.success).length; + const alreadyVerified = verificationResults.filter(r => r.alreadyVerified).length; + + console.log(`✅ Успешно верифицировано: ${successful}`); + console.log(`ℹ️ Уже было верифицировано: ${alreadyVerified}`); + console.log(`❌ Ошибки: ${failed}`); + + verificationResults.forEach(result => { + const status = result.success + ? (result.alreadyVerified ? 'ℹ️' : '✅') + : '❌'; + console.log(`${status} ${result.network} (${result.chainId}): ${result.success ? 'OK' : result.error?.substring(0, 100) + '...'}`); + }); + + console.log('\n🎉 Верификация завершена!'); + + } catch (error) { + console.error('💥 Ошибка верификации:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +// Запускаем верификацию если скрипт вызван напрямую +if (require.main === module) { + verifyWithHardhatV2() + .then(() => { + console.log('\n🏁 Скрипт завершен'); + process.exit(0); + }) + .catch((error) => { + console.error('💥 Скрипт завершился с ошибкой:', error); + process.exit(1); + }); +} + +module.exports = { verifyWithHardhatV2, verifyContracts: verifyWithHardhatV2 }; diff --git a/backend/services/deployParamsService.js b/backend/services/deployParamsService.js new file mode 100644 index 0000000..2e199f4 --- /dev/null +++ b/backend/services/deployParamsService.js @@ -0,0 +1,228 @@ +/** + * Сервис для работы с параметрами деплоя DLE + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const { Pool } = require('pg'); +const logger = require('../utils/logger'); +const encryptedDb = require('./encryptedDatabaseService'); + +class DeployParamsService { + constructor() { + this.pool = new Pool({ + user: process.env.DB_USER || 'dapp_user', + host: process.env.DB_HOST || 'dapp-postgres', + database: process.env.DB_NAME || 'dapp_db', + password: process.env.DB_PASSWORD || 'dapp_password', + port: process.env.DB_PORT || 5432, + }); + // Используем глобальный экземпляр encryptedDb + } + + /** + * Сохраняет параметры деплоя в базу данных + * @param {string} deploymentId - Идентификатор деплоя + * @param {Object} params - Параметры деплоя + * @param {string} status - Статус деплоя + * @returns {Promise} - Сохраненные параметры + */ + async saveDeployParams(deploymentId, params, status = 'pending') { + try { + logger.info(`💾 Сохранение параметров деплоя в БД: ${deploymentId}`); + + const dataToSave = { + deployment_id: deploymentId, + name: params.name, + symbol: params.symbol, + location: params.location, + coordinates: params.coordinates, + jurisdiction: params.jurisdiction, + oktmo: params.oktmo, + okved_codes: JSON.stringify(params.okvedCodes || []), + kpp: params.kpp, + quorum_percentage: params.quorumPercentage, + initial_partners: JSON.stringify(params.initialPartners || []), + initial_amounts: JSON.stringify(params.initialAmounts || []), + supported_chain_ids: JSON.stringify(params.supportedChainIds || []), + current_chain_id: params.currentChainId, + logo_uri: params.logoURI, + private_key: params.privateKey, // Будет автоматически зашифрован + etherscan_api_key: params.etherscanApiKey, + auto_verify_after_deploy: params.autoVerifyAfterDeploy || false, + create2_salt: params.CREATE2_SALT, + rpc_urls: JSON.stringify(params.rpcUrls ? (Array.isArray(params.rpcUrls) ? params.rpcUrls : Object.values(params.rpcUrls)) : []), + initializer: params.initializer, + dle_address: params.dleAddress, + modules_to_deploy: JSON.stringify(params.modulesToDeploy || []), + deployment_status: status + }; + + // Используем encryptedDb для автоматического шифрования + // Проверяем, существует ли уже запись с таким deployment_id + const existing = await this.getDeployParams(deploymentId); + const result = existing + ? await encryptedDb.saveData('deploy_params', dataToSave, { deployment_id: deploymentId }) + : await encryptedDb.saveData('deploy_params', dataToSave); + + logger.info(`✅ Параметры деплоя сохранены в БД (с шифрованием): ${deploymentId}`); + + return result; + } catch (error) { + logger.error(`❌ Ошибка при сохранении параметров деплоя: ${error.message}`); + throw error; + } + } + + /** + * Получает параметры деплоя по идентификатору + * @param {string} deploymentId - Идентификатор деплоя + * @returns {Promise} - Параметры деплоя или null + */ + async getDeployParams(deploymentId) { + try { + logger.info(`📖 Получение параметров деплоя из БД: ${deploymentId}`); + + // Используем encryptedDb для автоматического расшифрования + const result = await encryptedDb.getData('deploy_params', { + deployment_id: deploymentId + }); + + if (!result || result.length === 0) { + logger.warn(`⚠️ Параметры деплоя не найдены: ${deploymentId}`); + return null; + } + + const params = result[0]; + + // PostgreSQL автоматически преобразует JSONB в объекты JavaScript + return { + ...params, + okvedCodes: params.okved_codes || [], + initialPartners: params.initial_partners || [], + initialAmounts: params.initial_amounts || [], + supportedChainIds: params.supported_chain_ids || [], + rpcUrls: params.rpc_urls || [], + modulesToDeploy: params.modules_to_deploy || [], + CREATE2_SALT: params.create2_salt, + create2_salt: params.create2_salt, // Дублируем для совместимости + logoURI: params.logo_uri, + privateKey: params.private_key, // Автоматически расшифрован + etherscanApiKey: params.etherscan_api_key, + autoVerifyAfterDeploy: params.auto_verify_after_deploy, + dleAddress: params.dle_address, + deploymentStatus: params.deployment_status + }; + } catch (error) { + logger.error(`❌ Ошибка при получении параметров деплоя: ${error.message}`); + throw error; + } + } + + /** + * Обновляет статус деплоя + * @param {string} deploymentId - Идентификатор деплоя + * @param {string} status - Новый статус + * @param {string} dleAddress - Адрес задеплоенного контракта + * @returns {Promise} - Обновленные параметры + */ + async updateDeploymentStatus(deploymentId, status, dleAddress = null) { + try { + logger.info(`🔄 Обновление статуса деплоя: ${deploymentId} -> ${status}`); + + const query = ` + UPDATE deploy_params + SET deployment_status = $2, dle_address = $3, updated_at = CURRENT_TIMESTAMP + WHERE deployment_id = $1 + RETURNING * + `; + + const result = await this.pool.query(query, [deploymentId, status, dleAddress]); + + if (result.rows.length === 0) { + throw new Error(`Параметры деплоя не найдены: ${deploymentId}`); + } + + logger.info(`✅ Статус деплоя обновлен: ${deploymentId} -> ${status}`); + return result.rows[0]; + } catch (error) { + logger.error(`❌ Ошибка при обновлении статуса деплоя: ${error.message}`); + throw error; + } + } + + /** + * Получает последние параметры деплоя + * @param {number} limit - Количество записей + * @returns {Promise} - Список параметров деплоя + */ + async getLatestDeployParams(limit = 10) { + try { + logger.info(`📋 Получение последних параметров деплоя (лимит: ${limit})`); + + // Используем encryptedDb для автоматического расшифрования + const result = await encryptedDb.getData('deploy_params', {}, limit, 'created_at DESC'); + + // PostgreSQL автоматически преобразует JSONB в объекты JavaScript + return result.map(row => ({ + ...row, + okvedCodes: row.okved_codes || [], + initialPartners: row.initial_partners || [], + initialAmounts: row.initial_amounts || [], + supportedChainIds: row.supported_chain_ids || [], + rpcUrls: row.rpc_urls || [], + modulesToDeploy: row.modules_to_deploy || [], + CREATE2_SALT: row.create2_salt, + create2_salt: row.create2_salt, // Дублируем для совместимости + logoURI: row.logo_uri, + privateKey: row.private_key, // Автоматически расшифрован + etherscanApiKey: row.etherscan_api_key, + autoVerifyAfterDeploy: row.auto_verify_after_deploy, + dleAddress: row.dle_address, + deploymentStatus: row.deployment_status + })); + } catch (error) { + logger.error(`❌ Ошибка при получении последних параметров деплоя: ${error.message}`); + throw error; + } + } + + /** + * Удаляет параметры деплоя по deployment_id (только для отладки) + * @param {string} deploymentId - Идентификатор деплоя + * @returns {Promise} - Результат удаления + */ + async deleteDeployParams(deploymentId) { + try { + logger.info(`🗑️ Удаление параметров деплоя: ${deploymentId}`); + + const query = 'DELETE FROM deploy_params WHERE deployment_id = $1'; + const result = await this.pool.query(query, [deploymentId]); + + const deleted = result.rowCount > 0; + if (deleted) { + logger.info(`✅ Параметры деплоя удалены: ${deploymentId}`); + } else { + logger.warn(`⚠️ Параметры деплоя не найдены: ${deploymentId}`); + } + + return deleted; + } catch (error) { + logger.error(`❌ Ошибка при удалении параметров деплоя: ${error.message}`); + throw error; + } + } + + /** + * Закрывает соединение с базой данных + */ + async close() { + try { + await this.pool.end(); + logger.info('🔌 Соединение с базой данных закрыто'); + } catch (error) { + logger.error(`❌ Ошибка при закрытии соединения с БД: ${error.message}`); + } + } +} + +module.exports = DeployParamsService; diff --git a/backend/services/dleV2Service.js b/backend/services/dleV2Service.js index 865ff7c..c0d4ec5 100644 --- a/backend/services/dleV2Service.js +++ b/backend/services/dleV2Service.js @@ -18,25 +18,36 @@ const logger = require('../utils/logger'); const { getRpcUrlByChainId } = require('./rpcProviderService'); const deploymentTracker = require('../utils/deploymentTracker'); const etherscanV2 = require('./etherscanV2VerificationService'); +const DeployParamsService = require('./deployParamsService'); const verificationStore = require('./verificationStore'); /** * Сервис для управления DLE v2 (Digital Legal Entity) - * Современный подход с единым контрактом + * Современный подход с единым контрактом и базой данных */ class DLEV2Service { + constructor() { + this.deployParamsService = new DeployParamsService(); + } + /** * Создает новое DLE v2 с заданными параметрами * @param {Object} dleParams - Параметры DLE + * @param {string} deploymentId - Идентификатор деплоя (опционально) * @returns {Promise} - Результат создания DLE */ async createDLE(dleParams, deploymentId = null) { console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!"); - logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ"); - let paramsFile = null; - let tempParamsFile = null; + logger.info("🚀 Начало создания DLE v2 с параметрами:", dleParams); + try { - logger.info('Начало создания DLE v2 с параметрами:', dleParams); + // Генерируем deploymentId если не передан + if (!deploymentId) { + deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; + } + + console.log(`🆔 Deployment ID: ${deploymentId}`); + logger.info(`🆔 Deployment ID: ${deploymentId}`); // WebSocket обновление: начало процесса if (deploymentId) { @@ -47,9 +58,24 @@ class DLEV2Service { this.validateDLEParams(dleParams); // Подготовка параметров для деплоя + console.log('🔧 Подготавливаем параметры для деплоя...'); + logger.info('🔧 Подготавливаем параметры для деплоя...'); + + // Отладка: проверяем входные параметры + console.log('🔍 ОТЛАДКА - Входные параметры:'); + console.log(' supportedChainIds:', JSON.stringify(dleParams.supportedChainIds, null, 2)); + console.log(' privateKey:', dleParams.privateKey ? '[ЕСТЬ]' : '[НЕТ]'); + console.log(' name:', dleParams.name); + const deployParams = this.prepareDeployParams(dleParams); + console.log('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2)); + logger.info('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2)); + + // Сохраняем подготовленные параметры в базу данных + logger.info(`💾 Сохранение подготовленных параметров деплоя в БД: ${deploymentId}`); + await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending'); - // Вычисляем адрес инициализатора (инициализатором является деплоер из переданного приватного ключа) + // Вычисляем адрес инициализатора try { const normalizedPk = dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`; const initializerAddress = new ethers.Wallet(normalizedPk).address; @@ -63,67 +89,78 @@ class DLEV2Service { deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса'); } - // Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets + // Генерируем одноразовый CREATE2_SALT 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); + // Обновляем параметры в базе данных с CREATE2_SALT + console.log('💾 Обновляем параметры в базе данных с CREATE2_SALT...'); + logger.info('💾 Обновляем параметры в базе данных с CREATE2_SALT...'); + + const updatedParams = { + ...deployParams, + CREATE2_SALT: create2Salt + }; + + await this.deployParamsService.saveDeployParams(deploymentId, updatedParams, 'in_progress'); + logger.info(`✅ Параметры обновлены в БД с CREATE2_SALT: ${create2Salt}`); // WebSocket обновление: поиск RPC URLs if (deploymentId) { deploymentTracker.updateProgress(deploymentId, 'Поиск RPC endpoints', 15, 'Подключаемся к блокчейн сетям'); } - // Готовим 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`); + // Получаем RPC URLs для всех поддерживаемых сетей + console.log('🌐 Получаем RPC URLs для всех поддерживаемых сетей...'); + logger.info('🌐 Получаем RPC URLs для всех поддерживаемых сетей...'); + const rpcUrls = {}; + for (const chainId of deployParams.supportedChainIds) { + try { + const rpcUrl = await getRpcUrlByChainId(chainId); + if (rpcUrl) { + rpcUrls[chainId] = rpcUrl; + console.log(`✅ RPC URL для сети ${chainId}: ${rpcUrl}`); + logger.info(`✅ RPC URL для сети ${chainId}: ${rpcUrl}`); + } else { + console.log(`❌ RPC URL для сети ${chainId} не найден`); + logger.warn(`❌ RPC URL для сети ${chainId} не найден`); } + } catch (error) { + console.log(`❌ Ошибка при получении RPC URL для сети ${chainId}: ${error.message}`); + logger.error(`❌ Ошибка при получении RPC URL для сети ${chainId}: ${error.message}`); } } + + // Проверяем баланс для всех сетей + if (deploymentId) { + deploymentTracker.updateProgress(deploymentId, 'Проверка баланса', 20, 'Проверяем достаточность средств для деплоя'); + } + + console.log('💰 Проверяем баланс для деплоя...'); + logger.info('💰 Проверяем баланс для деплоя...'); + + if (dleParams.privateKey) { + try { + await this.checkBalances(deployParams.supportedChainIds, dleParams.privateKey); + console.log(`✅ Баланс достаточный для деплоя!`); + } catch (balanceError) { + logger.error(`❌ Недостаточный баланс: ${balanceError.message}`); + throw balanceError; + } + } + + // Обновляем параметры в базе данных с RPC URLs и initializer + const finalParams = { + ...updatedParams, + rpcUrls: rpcUrls, // Сохраняем как объект {chainId: url} + rpc_urls: Object.values(rpcUrls), // Также сохраняем как массив для совместимости + initializer: dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000" + }; + + await this.deployParamsService.saveDeployParams(deploymentId, finalParams, 'in_progress'); + logger.info(`✅ Параметры обновлены в БД с RPC URLs и initializer`); + if (!dleParams.privateKey) { throw new Error('Приватный ключ для деплоя не передан'); } @@ -143,143 +180,47 @@ class DLEV2Service { logger.error('🔑 Ошибка при сохранении Etherscan API Key:', e.message); } - // WebSocket обновление: компиляция произойдет автоматически в deploy-multichain.js + // WebSocket обновление: подготовка к деплою if (deploymentId) { deploymentTracker.updateProgress(deploymentId, 'Подготовка к деплою', 25, 'Подготавливаем параметры для деплоя'); } - // INIT_CODE_HASH будет вычислен в deploy-multichain.js - - // Factory больше не используется - деплой DLE напрямую - logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`); - - // WebSocket обновление: начало мульти-чейн деплоя + // Запускаем деплой через скрипт + console.log('🚀 Запускаем мультисетевой деплой...'); + logger.info('🚀 Запускаем мультисетевой деплой...'); + 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'); + deploymentTracker.updateProgress(deploymentId, 'Деплой контрактов', 30, 'Разворачиваем DLE контракты в сетях'); } - // Мультисетевой деплой одним вызовом - 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, - etherscanApiKey: dleParams.etherscanApiKey - }); + const deployResult = await this.runDeployMultichain(deploymentId); - 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'); - } - }); - } + deploymentTracker.updateProgress(deploymentId, 'Обработка результата', 80, 'Анализируем результат деплоя'); } - // Сохраняем информацию о созданном DLE для отображения на странице управления - try { - logger.info('Результат деплоя для сохранения:', JSON.stringify(result, null, 2)); - - // Проверяем структуру результата - if (!result || typeof result !== 'object') { - logger.error('Неверная структура результата деплоя:', result); - throw new Error('Неверная структура результата деплоя'); - } - logger.info("🔍 DEBUG: Вызываем runDeployMultichain..."); + // Обрабатываем результат деплоя + const result = this.extractDeployResult(deployResult.stdout, deployParams); + + if (!result || !result.success) { + throw new Error('Деплой не удался: ' + (result?.error || 'Неизвестная ошибка')); + } - // Если результат - массив (прямой результат из скрипта), преобразуем его - 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; + // Сохраняем данные DLE 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}`); // Убрано избыточное логирование + ...result.data, + deploymentId: deploymentId, + createdAt: new Date().toISOString(), + status: 'active' + }; + + this.saveDLEData(dleData); + + // Обновляем статус деплоя в базе данных + if (deploymentId && result.data.dleAddress) { + await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result.data.dleAddress); + logger.info(`✅ Статус деплоя обновлен в БД: ${deploymentId} -> completed`); + } // WebSocket обновление: финализация if (deploymentId) { @@ -289,50 +230,36 @@ class DLEV2Service { deploymentTracker.addLog(deploymentId, `💰 Общий supply: ${result.data.totalSupply || 'N/A'}`, 'info'); } - return { + const finalResult = { success: true, data: dleData }; - } else { - throw new Error('DLE адрес не получен после деплоя'); - } - } catch (e) { - logger.warn('Не удалось сохранить локальную карточку DLE:', e.message); - } - // Etherscan API Key уже сохранен в начале функции - - // Верификация выполняется в deploy-multichain.js - - // WebSocket обновление: деплой успешно завершен if (deploymentId) { - deploymentTracker.completeDeployment(deploymentId, result); + deploymentTracker.completeDeployment(deploymentId, finalResult); } - return result; + return finalResult; } catch (error) { logger.error('Ошибка при создании DLE v2:', error); + // Обновляем статус деплоя в базе данных при ошибке + if (deploymentId) { + try { + await this.deployParamsService.updateDeploymentStatus(deploymentId, 'failed'); + logger.info(`❌ Статус деплоя обновлен в БД: ${deploymentId} -> failed`); + } catch (dbError) { + logger.error(`❌ Ошибка при обновлении статуса деплоя в БД: ${dbError.message}`); + } + } + // WebSocket обновление: деплой завершился с ошибкой if (deploymentId) { deploymentTracker.failDeployment(deploymentId, 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); - } } } @@ -341,472 +268,212 @@ class DLEV2Service { * @param {Object} params - Параметры для валидации */ validateDLEParams(params) { - if (!params.name || params.name.trim() === '') { - throw new Error('Название DLE обязательно'); + const required = ['name', 'symbol', 'location', 'jurisdiction', 'quorumPercentage']; + const missing = required.filter(field => !params[field]); + + if (missing.length > 0) { + throw new Error(`Отсутствуют обязательные поля: ${missing.join(', ')}`); } - if (!params.symbol || params.symbol.trim() === '') { - throw new Error('Символ токена обязателен'); + if (params.quorumPercentage < 1 || params.quorumPercentage > 100) { + throw new Error('Кворум должен быть от 1 до 100 процентов'); } - if (!params.location || params.location.trim() === '') { - throw new Error('Местонахождение DLE обязательно'); + if (!params.initialPartners || params.initialPartners.length === 0) { + throw new Error('Необходимо указать хотя бы одного партнера'); } - if (!params.initialPartners || !Array.isArray(params.initialPartners)) { - throw new Error('Партнеры должны быть массивом'); - } - - if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) { - throw new Error('Суммы должны быть массивом'); + if (!params.initialAmounts || params.initialAmounts.length === 0) { + throw new Error('Необходимо указать начальные суммы для партнеров'); } if (params.initialPartners.length !== params.initialAmounts.length) { - throw new Error('Количество партнеров должно соответствовать количеству сумм распределения'); + throw new Error('Количество партнеров должно совпадать с количеством сумм'); } - if (params.initialPartners.length === 0) { - throw new Error('Должен быть указан хотя бы один партнер'); + if (!params.supportedChainIds || params.supportedChainIds.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} Путь к сохраненному файлу + * Сохраняет данные DLE в файловую систему + * @param {Object} dleData - Данные DLE для сохранения */ 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; + const filename = `${dleData.name}_${dleData.symbol}_${Date.now()}.json`; + const filepath = path.join(dlesDir, filename); + + fs.writeFileSync(filepath, JSON.stringify(dleData, null, 2)); + logger.info(`✅ Данные DLE сохранены: ${filepath}`); + } catch (error) { + logger.error('Ошибка при сохранении данных DLE:', error); + throw error; } } /** * Подготавливает параметры для деплоя - * @param {Object} params - Параметры DLE из формы - * @returns {Object} - Подготовленные параметры для скрипта деплоя + * @param {Object} params - Исходные параметры + * @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; + return { + name: params.name, + symbol: params.symbol, + location: params.location, + coordinates: params.coordinates, + jurisdiction: params.jurisdiction, + oktmo: params.oktmo, + okvedCodes: params.okvedCodes || [], + kpp: params.kpp, + quorumPercentage: params.quorumPercentage, + initialPartners: params.initialPartners, + initialAmounts: params.initialAmounts, + supportedChainIds: params.supportedChainIds, + currentChainId: params.currentChainId || params.supportedChainIds[0], + logoURI: params.logoURI, + privateKey: params.privateKey, + etherscanApiKey: params.etherscanApiKey, + autoVerifyAfterDeploy: params.autoVerifyAfterDeploy !== undefined ? params.autoVerifyAfterDeploy : true + }; } /** - * Сохраняет параметры во временный файл - * @param {Object} params - Параметры для сохранения - * @returns {string} - Путь к сохраненному файлу + * Запускает мультисетевой деплой через скрипт + * @param {string} deploymentId - Идентификатор деплоя + * @param {Object} opts - Дополнительные опции + * @returns {Promise} - Результат выполнения скрипта */ - 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 = {}) { + async runDeployMultichain(deploymentId, opts = {}) { 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], { + const args = []; + + console.log(`🚀 Запускаем скрипт деплоя: ${scriptPath}`); + logger.info(`🚀 Запускаем скрипт деплоя: ${scriptPath}`); + + const child = spawn('npx', ['hardhat', 'run', scriptPath], { cwd: path.join(__dirname, '..'), - env: envVars, - stdio: ['inherit', 'pipe', 'pipe'] + env: { + ...process.env, + DEPLOYMENT_ID: deploymentId, // Передаем deploymentId в скрипт + ...opts.env + }, + stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; - hardhatProcess.stdout.on('data', (data) => { - stdout += data.toString(); - logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`); + child.stdout.on('data', (data) => { + const output = data.toString(); + stdout += output; + console.log(output); + + // НЕ отправляем логи через WebSocket здесь - они уже отправляются в скрипте деплоя + // Это предотвращает дублирование логов }); - hardhatProcess.stderr.on('data', (data) => { - stderr += data.toString(); - logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`); + child.stderr.on('data', (data) => { + const output = data.toString(); + stderr += output; + console.error(output); + + // НЕ отправляем ошибки через WebSocket здесь - они уже отправляются в скрипте деплоя + // Это предотвращает дублирование логов }); - hardhatProcess.on('close', (code) => { - try { - const result = this.extractDeployResult(stdout); - resolve(result); - } catch (error) { - logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error); + child.on('close', (code) => { if (code === 0) { - reject(new Error('Не удалось найти информацию о созданном DLE v2')); + resolve({ stdout, stderr, code }); } else { - reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`)); - } + reject(new Error(`Скрипт деплоя завершился с кодом ${code}: ${stderr}`)); } }); - hardhatProcess.on('error', (error) => { - logger.error('Ошибка запуска скрипта деплоя DLE v2:', error); - reject(error); + child.on('error', (error) => { + reject(new Error(`Ошибка при запуске скрипта деплоя: ${error.message}`)); }); }); } - // Мультисетевой деплой - 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, - 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: ['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} - Результат деплоя + * @returns {Object|null} - Результат деплоя */ - extractDeployResult(stdout) { - // Ищем результат в формате MULTICHAIN_DEPLOY_RESULT - const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*?\])/); + extractDeployResult(stdout, deployParams = null) { + // Ищем MULTICHAIN_DEPLOY_RESULT в выводе + const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/); if (resultMatch) { try { - const result = JSON.parse(resultMatch[1]); - return result; + const deployResults = JSON.parse(resultMatch[1]); + // Проверяем, что есть успешные деплои + const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000'); + + if (successfulDeploys.length > 0) { + return { + success: true, + data: { + deployedNetworks: deployResults, + dleAddress: successfulDeploys[0].address, // Используем первый успешный адрес + totalNetworks: deployResults.length, + successfulNetworks: successfulDeploys.length, + // Добавляем данные из параметров деплоя + name: deployParams?.name || 'Unknown', + symbol: deployParams?.symbol || 'UNK', + location: deployParams?.location || 'Не указан', + coordinates: deployParams?.coordinates || '0,0', + jurisdiction: deployParams?.jurisdiction || 0, + quorumPercentage: deployParams?.quorumPercentage || 51, + logoURI: deployParams?.logoURI || '/uploads/logos/default-token.svg' + } + }; + } } 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 из вывода скрипта'); + return null; } /** - * Очищает временные файлы - * @param {string} paramsFile - Путь к файлу параметров - * @param {string} tempParamsFile - Путь к временному файлу параметров + * Получает параметры деплоя из базы данных + * @param {string} deploymentId - Идентификатор деплоя + * @returns {Promise} - Параметры деплоя или null */ - cleanupTempFiles(paramsFile, tempParamsFile) { + async getDeployParams(deploymentId) { try { - if (fs.existsSync(paramsFile)) { - fs.unlinkSync(paramsFile); - } - if (fs.existsSync(tempParamsFile)) { - fs.unlinkSync(tempParamsFile); - } + logger.info(`📖 Получение параметров деплоя из БД: ${deploymentId}`); + return await this.deployParamsService.getDeployParams(deploymentId); } catch (error) { - logger.warn('Не удалось очистить временные файлы:', error); + logger.error(`❌ Ошибка при получении параметров деплоя: ${error.message}`); + throw error; } } /** - * Удаляет временные файлы параметров деплоя старше заданного возраста - * @param {number} maxAgeMs - Макс. возраст файлов в миллисекундах (по умолчанию 24ч) + * Получает последние параметры деплоя + * @param {number} limit - Количество записей + * @returns {Promise} - Список параметров деплоя */ - pruneOldTempFiles(maxAgeMs = 24 * 60 * 60 * 1000) { - const tempDir = path.join(__dirname, '../temp'); + async getLatestDeployParams(limit = 10) { 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); + logger.info(`📋 Получение последних параметров деплоя (лимит: ${limit})`); + return await this.deployParamsService.getLatestDeployParams(limit); + } catch (error) { + logger.error(`❌ Ошибка при получении последних параметров деплоя: ${error.message}`); + throw error; } } @@ -823,103 +490,62 @@ class DLEV2Service { } 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); + const dles = []; - // Группируем DLE по мультичейн деплоям - const groupedDles = this.groupMultichainDLEs(allDles); - - return groupedDles; + for (const file of files) { + if (file.endsWith('.json')) { + try { + const filepath = path.join(dlesDir, file); + const content = fs.readFileSync(filepath, 'utf8'); + const dleData = JSON.parse(content); + dles.push(dleData); + } catch (error) { + logger.warn(`Ошибка при чтении файла ${file}:`, error.message); + } + } + } + + return dles.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } catch (error) { - logger.error('Ошибка при получении списка DLE v2:', error); + logger.error('Ошибка при получении списка DLE:', error); return []; } } /** - * Группирует DLE по мультичейн деплоям - * @param {Array} allDles - Все DLE из файлов - * @returns {Array} - Сгруппированные DLE + * Группирует 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, - // Мультичейн информация + createdAt: dle.createdAt, + deploymentId: dle.deploymentId, networks: [], - // Модули (одинаковые во всех сетях) - modules: dle.modules, - // Время создания (самое раннее) - creationTimestamp: dle.creationTimestamp, - creationBlock: dle.creationBlock + totalSupply: dle.totalSupply, + partnerCount: dle.partnerBalances?.length || 0 }); } - - 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({ + + groups.get(groupKey).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; - } + address: dle.address, + networkName: this.getRpcUrlForChain(dle.chainId)?.name || `Chain ${dle.chainId}`, + status: dle.status || 'active' + }); } - - // Преобразуем группы в массив - 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) - })); + + return Array.from(groups.values()); } /** @@ -928,113 +554,57 @@ class DLEV2Service { * @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('|'); + return `${dle.name}_${dle.symbol}_${dle.jurisdiction}_${dle.location}`; } /** * Получает RPC URL для сети * @param {number} chainId - ID сети - * @returns {string|null} - RPC URL + * @returns {Object|null} - Информация о RPC */ 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; - } + const rpcMappings = { + 1: { name: 'Ethereum Mainnet', url: 'https://mainnet.infura.io/v3/' }, + 11155111: { name: 'Sepolia Testnet', url: 'https://sepolia.infura.io/v3/' }, + 17000: { name: 'Holesky Testnet', url: 'https://holesky.infura.io/v3/' }, + 421614: { name: 'Arbitrum Sepolia', url: 'https://sepolia-rollup.arbitrum.io/rpc' }, + 84532: { name: 'Base Sepolia', url: 'https://sepolia.base.org' } + }; + + return rpcMappings[chainId] || null; } - - - /** - * Проверяет балансы в указанных сетях - * @param {number[]} chainIds - Массив chainId для проверки + * Проверяет баланс для деплоя в указанных сетях + * @param {Array} chainIds - Список ID сетей * @param {string} privateKey - Приватный ключ - * @returns {Promise} - Результат проверки балансов + * @returns {Promise} */ async checkBalances(chainIds, privateKey) { - const { getRpcUrlByChainId } = require('./rpcProviderService'); - const { ethers } = require('ethers'); - const balances = []; - const insufficient = []; + const wallet = new ethers.Wallet(privateKey); + const minBalance = ethers.parseEther('0.01'); // Минимум 0.01 ETH 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; + throw new Error(`RPC URL не найден для сети ${chainId}`); } 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); + console.log(`💰 Баланс в сети ${chainId}: ${ethers.formatEther(balance)} ETH`); + + if (balance < minBalance) { + throw new Error(`Недостаточный баланс в сети ${chainId}: ${ethers.formatEther(balance)} ETH (минимум: ${ethers.formatEther(minBalance)} ETH)`); } - } catch (error) { - balances.push({ - chainId, - balanceEth: '0', - ok: false, - error: error.message - }); - insufficient.push(chainId); + logger.error(`Ошибка при проверке баланса в сети ${chainId}:`, error.message); + throw error; } } - - return { - balances, - insufficient, - allSufficient: insufficient.length === 0 - }; } - - } -module.exports = new DLEV2Service(); \ No newline at end of file +module.exports = DLEV2Service; \ No newline at end of file diff --git a/backend/services/rpcProviderService.js b/backend/services/rpcProviderService.js index e406f00..2178623 100644 --- a/backend/services/rpcProviderService.js +++ b/backend/services/rpcProviderService.js @@ -88,4 +88,16 @@ async function getRpcUrlByChainId(chainId) { return providers[0]?.rpc_url || null; } -module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider, getRpcUrlByNetworkId, getRpcUrlByChainId }; \ No newline at end of file +async function getEtherscanApiUrlByChainId(chainId) { + console.log(`[RPC Service] Поиск Etherscan API URL для chain_id: ${chainId}`); + const providers = await encryptedDb.getData('rpc_providers', { chain_id: chainId }, 1); + console.log(`[RPC Service] Найдено провайдеров: ${providers.length}`); + if (providers.length > 0) { + console.log(`[RPC Service] Найден Etherscan API URL: ${providers[0].etherscan_api_url || 'НЕТ'}`); + } else { + console.log(`[RPC Service] Etherscan API URL для chain_id ${chainId} не найден`); + } + return providers[0]?.etherscan_api_url || null; +} + +module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider, getRpcUrlByNetworkId, getRpcUrlByChainId, getEtherscanApiUrlByChainId }; \ No newline at end of file diff --git a/backend/utils/deploymentTracker.js b/backend/utils/deploymentTracker.js index c4d56b7..081238e 100644 --- a/backend/utils/deploymentTracker.js +++ b/backend/utils/deploymentTracker.js @@ -21,11 +21,11 @@ class DeploymentTracker extends EventEmitter { } // Создать новый деплой - createDeployment(params) { - const deploymentId = this.generateDeploymentId(); + createDeployment(params, deploymentId = null) { + const id = deploymentId || this.generateDeploymentId(); const deployment = { - id: deploymentId, + id: id, status: 'pending', stage: 'initializing', progress: 0, @@ -38,10 +38,11 @@ class DeploymentTracker extends EventEmitter { error: null }; - this.deployments.set(deploymentId, deployment); - this.logger.info(`📝 Создан новый деплой: ${deploymentId}`); + this.deployments.set(id, deployment); + this.logger.info(`📝 Создан новый деплой: ${id}`); + console.log(`[DEPLOYMENT_TRACKER] Создан деплой: ${id}, всего деплоев: ${this.deployments.size}`); - return deploymentId; + return id; } // Получить статус деплоя @@ -75,6 +76,7 @@ class DeploymentTracker extends EventEmitter { if (!deployment) return false; const logEntry = { + id: Date.now() + Math.random(), // Уникальный ID для отслеживания дублирования timestamp: new Date(), message, type @@ -83,6 +85,11 @@ class DeploymentTracker extends EventEmitter { deployment.logs.push(logEntry); deployment.updatedAt = new Date(); + // Логируем отправку лога для отладки дублирования (только в debug режиме) + if (process.env.DEBUG_DEPLOYMENT_LOGS) { + console.log(`[DEPLOYMENT_TRACKER] Отправляем лог ID=${logEntry.id}: ${message.substring(0, 50)}...`); + } + // Отправляем только лог через WebSocket (без дублирования) this.emit('deployment_updated', { deploymentId, diff --git a/backend/wsHub.js b/backend/wsHub.js index c7eaa43..ab25f07 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -28,6 +28,7 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms function initWSS(server) { wss = new WebSocket.Server({ server, path: '/ws' }); + console.log('🔌 [WebSocket] Сервер инициализирован на пути /ws'); // Подключаем deployment tracker к WebSocket deploymentTracker.on('deployment_updated', (data) => { @@ -35,10 +36,10 @@ function initWSS(server) { }); wss.on('connection', (ws, req) => { - // console.log('🔌 [WebSocket] Новое подключение'); - // console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress); - // console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']); - // console.log('🔌 [WebSocket] Origin:', req.headers.origin); + console.log('🔌 [WebSocket] Новое подключение'); + console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress); + console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']); + console.log('🔌 [WebSocket] Origin:', req.headers.origin); // Добавляем клиента в общий список if (!wsClients.has('anonymous')) { @@ -461,11 +462,15 @@ function broadcastTokenBalanceChanged(userId, tokenAddress, newBalance, network) function broadcastDeploymentUpdate(data) { if (!wss) return; + console.log(`📡 [WebSocket] broadcastDeploymentUpdate вызвана с данными:`, JSON.stringify(data, null, 2)); + const message = JSON.stringify({ type: 'deployment_update', data: data }); + console.log(`📡 [WebSocket] Отправляем сообщение:`, message); + // Отправляем всем подключенным клиентам wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { diff --git a/docker-compose.yml b/docker-compose.yml index aace7ac..f6a2998 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -167,23 +167,23 @@ services: - '5173:5173' # Закрываем - используем nginx command: yarn run dev -- --host 0.0.0.0 ssh-tunnel-frontend: - image: alpine:latest + image: alpine:3.18 container_name: ssh-tunnel-frontend volumes: - ./id_rsa:/key:ro command: > - sh -c "apk add --no-cache openssh && ssh -i /key -o StrictHostKeyChecking=no -N -R 0.0.0.0:9000:host.docker.internal:9000 root@185.221.214.140" + sh -c "apk add --no-cache openssh-client && ssh -i /key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -N -R 0.0.0.0:9000:host.docker.internal:9000 root@185.221.214.140" restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" ssh-tunnel-backend: - image: alpine:latest + image: alpine:3.18 container_name: ssh-tunnel-backend volumes: - ./id_rsa:/key:ro command: > - sh -c "apk add --no-cache openssh && ssh -i /key -o StrictHostKeyChecking=no -N -R 0.0.0.0:8000:host.docker.internal:8000 root@185.221.214.140" + sh -c "apk add --no-cache openssh-client && ssh -i /key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -N -R 0.0.0.0:8000:host.docker.internal:8000 root@185.221.214.140" restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" diff --git a/docs/SMART_CONTRACTS.md b/docs/SMART_CONTRACTS.md index 1bbb905..22937f8 100644 --- a/docs/SMART_CONTRACTS.md +++ b/docs/SMART_CONTRACTS.md @@ -192,16 +192,11 @@ function _updateDLEInfo( function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal ``` -#### 3. Изменение текущей цепочки -```solidity -function _updateCurrentChainId(uint256 _newChainId) internal -``` -#### 4. События для отслеживания изменений +#### 3. События для отслеживания изменений ```solidity event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); -event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId); ``` ### Процесс изменения данных DLE diff --git a/frontend/src/components/deployment/DeploymentWizard.vue b/frontend/src/components/deployment/DeploymentWizard.vue index 11f68d9..4aad908 100644 --- a/frontend/src/components/deployment/DeploymentWizard.vue +++ b/frontend/src/components/deployment/DeploymentWizard.vue @@ -156,6 +156,11 @@ const props = defineProps({ type: String, required: false, default: '' + }, + autoVerifyAfterDeploy: { + type: Boolean, + required: false, + default: false } }); @@ -250,8 +255,16 @@ const startDeployment = async () => { try { addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info'); + // Генерируем deploymentId заранее, чтобы WebSocket сообщения не игнорировались + const tempDeploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; + addLog(`🆔 Временный ID деплоя: ${tempDeploymentId}`, 'info'); + + // Начинаем отслеживание сразу с временным ID + startDeploymentTracking(tempDeploymentId); + // Подготовка данных для деплоя const deployData = { + deploymentId: tempDeploymentId, // Передаем временный ID в backend name: props.dleData.name, symbol: props.dleData.tokenSymbol, location: props.dleData.addressData?.fullAddress || 'Не указан', @@ -260,7 +273,7 @@ const startDeployment = async () => { oktmo: props.dleData.selectedOktmo || '', okvedCodes: props.dleData.selectedOkved || [], kpp: props.dleData.kppCode || '', - quorumPercentage: props.dleData.governanceQuorum || 51, + quorumPercentage: props.dleData.governanceQuorum !== undefined ? props.dleData.governanceQuorum : 51, initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr), initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0), supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined), @@ -268,7 +281,7 @@ const startDeployment = async () => { logoURI: props.logoURI || '/uploads/logos/default-token.svg', privateKey: props.privateKey, etherscanApiKey: props.etherscanApiKey || '', - autoVerifyAfterDeploy: false + autoVerifyAfterDeploy: props.autoVerifyAfterDeploy !== undefined ? props.autoVerifyAfterDeploy : false }; addLog('📤 Отправляем запрос на асинхронный деплой...', 'info'); @@ -279,8 +292,11 @@ const startDeployment = async () => { if (response.data.success && response.data.deploymentId) { addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success'); - // Начинаем отслеживание через WebSocket - startDeploymentTracking(response.data.deploymentId); + // Обновляем deploymentId на реальный от сервера + if (response.data.deploymentId !== tempDeploymentId) { + addLog(`🔄 Обновляем ID деплоя: ${tempDeploymentId} → ${response.data.deploymentId}`, 'info'); + startDeploymentTracking(response.data.deploymentId); + } } else { throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка')); diff --git a/frontend/src/composables/useDeploymentWebSocket.js b/frontend/src/composables/useDeploymentWebSocket.js index b6d0b18..22bf47b 100644 --- a/frontend/src/composables/useDeploymentWebSocket.js +++ b/frontend/src/composables/useDeploymentWebSocket.js @@ -47,30 +47,19 @@ export function useDeploymentWebSocket() { // Обработчик WebSocket сообщений const handleDeploymentUpdate = (data) => { - if (data.deploymentId !== deploymentId.value) return; - console.log('🔄 [DeploymentWebSocket] Получено обновление:', data); + console.log('🔄 [DeploymentWebSocket] Текущий deploymentId:', deploymentId.value); + console.log('🔄 [DeploymentWebSocket] deploymentId из данных:', data.deploymentId); + + if (data.deploymentId !== deploymentId.value) { + console.log('🔄 [DeploymentWebSocket] Игнорируем обновление - не наш deploymentId'); + return; + } 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'); + case 'deployment_log': + if (data.log) { + addLog(data.log.message, data.log.type || 'info'); } break; @@ -87,34 +76,6 @@ export function useDeploymentWebSocket() { } 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; @@ -135,6 +96,13 @@ export function useDeploymentWebSocket() { console.warn('🤷‍♂️ [DeploymentWebSocket] Неизвестный тип события:', data.type); } }; + + // Подключаемся к WebSocket сразу при инициализации + wsClient.connect(); + if (wsClient && typeof wsClient.subscribe === 'function') { + wsClient.subscribe('deployment_update', handleDeploymentUpdate); + console.log('🔌 [DeploymentWebSocket] Подключились к WebSocket при инициализации'); + } // Начать отслеживание деплоя const startDeploymentTracking = (id) => { @@ -145,13 +113,7 @@ export function useDeploymentWebSocket() { isDeploying.value = true; clearLogs(); - // Подключаемся к WebSocket обновлениям - wsClient.connect(); - if (wsClient && typeof wsClient.subscribe === 'function') { - wsClient.subscribe('deployment_update', handleDeploymentUpdate); - } else { - console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен'); - } + // WebSocket уже подключен при инициализации addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info'); }; diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index dfce77f..0094642 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -222,6 +222,11 @@ const routes = [ name: 'management-proposals', component: () => import('../views/smartcontracts/DleProposalsView.vue') }, + { + path: '/management/create-proposal', + name: 'management-create-proposal', + component: () => import('../views/smartcontracts/CreateProposalView.vue') + }, { path: '/management/tokens', name: 'management-tokens', diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 8405417..217a18a 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -26,7 +26,12 @@ class WebSocketClient { connect() { try { - this.ws = new WebSocket('ws://localhost:8000/ws'); + // В Docker окружении используем Vite прокси для WebSocket + // Используем относительный путь, чтобы Vite прокси мог перенаправить запрос на backend + const wsUrl = window.location.protocol === 'https:' + ? 'wss://' + window.location.host + '/ws' + : 'ws://' + window.location.host + '/ws'; + this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('[WebSocket] Подключение установлено'); @@ -37,13 +42,21 @@ class WebSocketClient { this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); - console.log('[WebSocket] Получено сообщение:', data); + + // Логируем все deployment_update сообщения для отладки + if (data.type === 'deployment_update') { + console.log('[WebSocket] Получено deployment_update:', data); + console.log('[WebSocket] Данные для обработчика:', data.data); + } // Вызываем все зарегистрированные обработчики для этого события if (this.listeners.has(data.type)) { + console.log(`[WebSocket] Вызываем обработчики для типа: ${data.type}, количество: ${this.listeners.get(data.type).length}`); this.listeners.get(data.type).forEach(callback => { callback(data.data); }); + } else { + console.log(`[WebSocket] Нет обработчиков для типа: ${data.type}`); } } catch (error) { console.error('[WebSocket] Ошибка парсинга сообщения:', error); diff --git a/frontend/src/views/ManagementView.vue b/frontend/src/views/ManagementView.vue index 58d96bb..9f107dc 100644 --- a/frontend/src/views/ManagementView.vue +++ b/frontend/src/views/ManagementView.vue @@ -96,16 +96,25 @@
- Адрес контракта: - - {{ shortenAddress(dle.dleAddress) }} - - + Адреса контрактов: +
+
+ {{ getChainName(network.chainId) }}: + + {{ shortenAddress(network.address) }} + + +
+
Местоположение: {{ dle.location }} @@ -124,17 +133,13 @@ Статус: Активен
-
+
Общий объем токенов: - {{ parseFloat(dle.totalSupply).toLocaleString() }} {{ dle.symbol }} + {{ formatTokenAmount(dle.totalSupply || 0) }} {{ dle.symbol }}
-
- Логотип: - Установлен -
-
+
Дата создания: - {{ formatTimestamp(dle.creationTimestamp) }} + {{ formatTimestamp(dle.creationTimestamp || dle.createdAt) }}
@@ -345,7 +350,18 @@ function getExplorerUrl(chainId, address) { function formatTimestamp(timestamp) { if (!timestamp) return ''; - const date = new Date(timestamp * 1000); // Конвертируем из Unix timestamp + + let date; + if (typeof timestamp === 'number') { + // Unix timestamp + date = new Date(timestamp * 1000); + } else if (typeof timestamp === 'string') { + // ISO string + date = new Date(timestamp); + } else { + return ''; + } + return date.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', @@ -355,6 +371,15 @@ function formatTimestamp(timestamp) { }); } +function formatTokenAmount(amount) { + if (!amount) return '0'; + const num = parseFloat(amount); + if (num === 0) return '0'; + + // Всегда показываем полное число с разделителями тысяч + return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 }); +} + function openDleOnEtherscan(address) { window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank'); } diff --git a/frontend/src/views/settings/DleDeployFormView.vue b/frontend/src/views/settings/DleDeployFormView.vue index 7517f65..4395228 100644 --- a/frontend/src/views/settings/DleDeployFormView.vue +++ b/frontend/src/views/settings/DleDeployFormView.vue @@ -513,18 +513,6 @@
- - - -
@@ -631,7 +619,7 @@
-
💰 Требования к балансу:
+
Требования к балансу:
-
🔒 Рекомендации по безопасности:
+
Рекомендации по безопасности:
  • Используйте отдельный кошелек только для деплоя DLE
  • Убедитесь, что на кошельке достаточно средств для оплаты газа
  • @@ -697,7 +685,7 @@

    Основная информация DLE

    - 🎨 Логотип: + Логотип:
    Logo preview {{ logoFile?.name || 'ENS аватар' || 'Дефолтный логотип' }} @@ -705,11 +693,11 @@
    - 📋 Название: {{ dleSettings.name }} + Название: {{ dleSettings.name }}
    - 🪙 Токен: {{ dleSettings.tokenSymbol }} + Токен: {{ dleSettings.tokenSymbol }}
    @@ -723,7 +711,7 @@
    - 👥 Партнер {{ index + 1 }}: + Партнер {{ index + 1 }}:
    Адрес: {{ partner.address.substring(0, 10) }}...{{ partner.address.substring(partner.address.length - 8) }} @@ -736,11 +724,11 @@
    - 💰 Общий эмиссия: {{ totalTokens }} токенов + Общий эмиссия: {{ totalTokens }} токенов
    - 🗳️ Кворум подписей партнеров: {{ dleSettings.governanceQuorum }}% + Кворум подписей партнеров: {{ dleSettings.governanceQuorum }}%
    @@ -749,11 +737,11 @@

    🔗 Мульти-чейн деплой

    - 🌐 Выбранные сети: + Выбранные сети:
    • {{ network.name }} (Chain ID: {{ network.chainId }}) - ~${{ network.estimatedCost }} @@ -762,7 +750,7 @@
    - 💰 Общая стоимость: ~${{ totalDeployCost.toFixed(2) }} + Общая стоимость: ~${{ totalDeployCost.toFixed(2) }}
    @@ -775,7 +763,7 @@

    🔐 Приватный ключ

    - 🔑 Ключ: ***{{ unifiedPrivateKey.slice(-4) }} + Ключ: ***{{ unifiedPrivateKey.slice(-4) }}
    @@ -830,7 +818,7 @@
    - 📍 Координаты: {{ dleSettings.coordinates }} + 📍Координаты: {{ dleSettings.coordinates }}
    @@ -866,8 +854,8 @@ @click="deploySmartContracts" type="button" class="btn btn-primary btn-lg deploy-btn" - :disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading || showDeployProgress" - :title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`" + :disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading" + :title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}`" > Поэтапный деплой DLE @@ -877,48 +865,12 @@ @click="clearAllData" class="btn btn-danger btn-lg clear-btn" title="Очистить все данные" - :disabled="showDeployProgress" + :disabled="false" > Удалить все
    - -
    -
    -

    🚀 Деплой DLE в блокчейне

    -

    {{ deployStatus }}

    -
    - -
    -
    -
    -
    - {{ deployProgress }}% -
    - -
    -
    - - Подготовка данных -
    -
    - - Отправка на сервер -
    -
    - - Деплой в блокчейне -
    -
    - - Завершение -
    -
    -
    @@ -939,6 +891,7 @@ :dle-data="dleSettings" :logo-uri="getLogoURI()" :etherscan-api-key="etherscanApiKey" + :auto-verify-after-deploy="autoVerifyAfterDeploy" @deployment-completed="handleDeploymentCompleted" />
    @@ -1113,33 +1066,6 @@ const hasSelectedNetworks = computed(() => { return selectedNetworks.value.length > 0; }); -// Инициализация при смене выбранных сетей -// watch(selectedNetworkDetails, (nets) => { -// if (nets && nets.length > 0) predictAddresses(); -// }, { immediate: true }); - -// Предсказание адресов (упрощенно через бэкенд) - отключено -// async function predictAddresses() { -// try { -// isPredicting.value = true; -// const payload = { -// name: dleSettings.name, -// symbol: dleSettings.tokenSymbol, -// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId) -// }; -// if (resp.data && resp.data.success && resp.data.data) { -// // ожидаем вид { [chainId]: address } -// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]); -// Object.assign(predictedAddresses, resp.data.data); -// } -// } catch (e) { -// console.error('Ошибка расчета предсказанных адресов:', e); -// alert('Не удалось рассчитать предсказанные адреса'); -// } finally { -// isPredicting.value = false; -// } -// } - function copyToClipboard(text) { navigator.clipboard?.writeText(text).then(() => { // no-op @@ -1190,10 +1116,6 @@ const selectedOkvedLevel4 = ref(''); const currentSelectedOkvedCode = ref(''); const currentSelectedOkvedText = ref(''); -// Состояние процесса деплоя -const showDeployProgress = ref(false); -const deployProgress = ref(0); -const deployStatus = ref(''); // Функция определения уровня ОКВЭД кода const getOkvedLevel = (code) => { @@ -2399,10 +2321,6 @@ watch(unifiedPrivateKey, (newValue) => { // Инициализация onMounted(() => { - // Сбрасываем состояние деплоя при загрузке страницы - showDeployProgress.value = false; - deployProgress.value = 0; - deployStatus.value = ''; // Загружаем список стран loadCountries(); @@ -2544,7 +2462,6 @@ const deploySmartContracts = async () => { } catch (error) { console.error('Ошибка деплоя DLE:', error); - showDeployProgress.value = false; alert('❌ Ошибка при деплое смарт-контракта: ' + error.message); } }; @@ -2555,10 +2472,6 @@ const startStagedDeployment = async () => { // Сначала выполняем стандартный деплой DLE контракта try { - // Показываем индикатор процесса - showDeployProgress.value = true; - deployProgress.value = 10; - deployStatus.value = 'Подготовка данных для деплоя DLE...'; // Подготовка данных для деплоя console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks); @@ -2591,7 +2504,7 @@ const startStagedDeployment = async () => { privateKey: unifiedPrivateKey.value, // Верификация через Etherscan V2 etherscanApiKey: etherscanApiKey.value, - autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя + autoVerifyAfterDeploy: autoVerifyAfterDeploy.value }; // Обработка логотипа @@ -2617,8 +2530,6 @@ const startStagedDeployment = async () => { console.log('Данные для деплоя DLE:', deployData); // Предварительная проверка балансов (через приватный ключ) - deployProgress.value = 20; - deployStatus.value = 'Проверка баланса во всех выбранных сетях...'; try { const pre = await api.post('/dle-v2/precheck', { supportedChainIds: deployData.supportedChainIds, @@ -2630,7 +2541,6 @@ const startStagedDeployment = async () => { if (lacks.length > 0) { const message = `❌ Недостаточно средств в некоторых сетях!`; alert(message); - showDeployProgress.value = false; return; } console.log('✅ Проверка балансов пройдена:', preData.summary); @@ -2639,25 +2549,6 @@ const startStagedDeployment = async () => { console.warn('⚠️ Ошибка проверки балансов:', e.message); } - deployProgress.value = 30; - deployStatus.value = 'Компиляция смарт-контрактов...'; - - // Автокомпиляция контрактов - try { - const compileResponse = await api.post('/compile-contracts'); - console.log('✅ Контракты скомпилированы:', compileResponse.data); - } catch (compileError) { - console.warn('⚠️ Ошибка автокомпиляции:', compileError.message); - } - - deployProgress.value = 40; - deployStatus.value = 'Деплой DLE контракта...'; - - // Деплой будет выполнен в DeploymentWizard - // Здесь только показываем мастер деплоя - deployProgress.value = 80; - deployStatus.value = 'Запуск мастера деплоя...'; - // Показываем мастер деплоя showDeploymentWizard.value = true; @@ -2665,8 +2556,6 @@ const startStagedDeployment = async () => { return; } catch (error) { console.error('Ошибка при запуске деплоя:', error); - deployStatus.value = `❌ Ошибка: ${error.message}`; - deployProgress.value = 0; } } @@ -2697,11 +2586,10 @@ const handleDeploymentCompleted = (result) => { 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); + 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); return Boolean( validation.jurisdiction && @@ -4588,103 +4476,6 @@ async function submitDeploy() { border: 1px solid #f5c6cb; } - /* Стили для индикатора процесса деплоя */ - .deploy-progress { - margin-top: 2rem; - padding: 2rem; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 12px; - color: white; - animation: fadeIn 0.5s ease; - } - - .progress-header { - text-align: center; - margin-bottom: 2rem; - } - - .progress-header h4 { - margin: 0 0 0.5rem 0; - font-size: 1.5rem; - font-weight: 600; - } - - .progress-header p { - margin: 0; - opacity: 0.9; - font-size: 1.1rem; - } - - .progress-bar-container { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 2rem; - } - - .progress-bar { - flex: 1; - height: 12px; - background: rgba(255, 255, 255, 0.2); - border-radius: 6px; - overflow: hidden; - } - - .progress-fill { - height: 100%; - background: linear-gradient(90deg, #4ade80 0%, #22c55e 100%); - border-radius: 6px; - transition: width 0.5s ease; - } - - .progress-text { - font-weight: 600; - font-size: 1.1rem; - min-width: 50px; - } - - .progress-steps { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - } - - .step { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem; - background: rgba(255, 255, 255, 0.1); - border-radius: 8px; - opacity: 0.5; - transition: all 0.3s ease; - } - - .step.active { - opacity: 1; - background: rgba(255, 255, 255, 0.2); - } - - .step i { - font-size: 1.2rem; - color: #4ade80; - } - - .step span { - font-size: 0.9rem; - font-weight: 500; - } - - @keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } - } /* Стили для загрузки картинки токена */ .token-image-upload { diff --git a/frontend/src/views/smartcontracts/CreateProposalView.vue b/frontend/src/views/smartcontracts/CreateProposalView.vue new file mode 100644 index 0000000..e731c00 --- /dev/null +++ b/frontend/src/views/smartcontracts/CreateProposalView.vue @@ -0,0 +1,591 @@ + + + + + + + diff --git a/frontend/src/views/smartcontracts/DleBlocksManagementView.vue b/frontend/src/views/smartcontracts/DleBlocksManagementView.vue index d435587..ec0a256 100644 --- a/frontend/src/views/smartcontracts/DleBlocksManagementView.vue +++ b/frontend/src/views/smartcontracts/DleBlocksManagementView.vue @@ -34,6 +34,14 @@
    +
    +

    Создать предложение

    +

    Универсальная форма для создания новых предложений

    + +
    +

    Предложения

    Создание, подписание, выполнение

    @@ -45,16 +53,16 @@

    Балансы, трансферы, распределение

    - +
    + + +

    Кворум

    Настройки голосования

    -
    - - -
    +

    Модули DLE

    Установка, настройка, управление

    @@ -165,6 +173,14 @@ const openSettings = () => { } }; +const openCreateProposal = () => { + if (dleAddress.value) { + router.push(`/management/create-proposal?address=${dleAddress.value}`); + } else { + router.push('/management/create-proposal'); + } +}; + onMounted(() => { // Если нет адреса DLE, перенаправляем на главную страницу management if (!dleAddress.value) { @@ -279,6 +295,32 @@ onMounted(() => { transform: translateY(-1px); } +/* Стили для блока создания предложения */ +.create-proposal-block { + background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%); + border: 2px solid #28a745; +} + +.create-proposal-block:hover { + border-color: #20c997; + box-shadow: 0 4px 20px rgba(40, 167, 69, 0.15); +} + +.create-proposal-block h3 { + color: #28a745; +} + +.create-btn { + background: linear-gradient(135deg, #28a745, #20c997); + color: white; + font-weight: 700; +} + +.create-btn:hover { + background: linear-gradient(135deg, #218838, #1ea085); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + /* Адаптивность */ @media (max-width: 768px) { .blocks-row { diff --git a/frontend/src/views/smartcontracts/DleProposalsView.vue b/frontend/src/views/smartcontracts/DleProposalsView.vue index d9f6b3b..071a8df 100644 --- a/frontend/src/views/smartcontracts/DleProposalsView.vue +++ b/frontend/src/views/smartcontracts/DleProposalsView.vue @@ -195,367 +195,6 @@
    - -
    -
    -

    📝 Создание нового предложения

    - -
    - - -
    -
    - - Для создания предложений необходимо авторизоваться в приложении -

    Подключите кошелек в сайдбаре для создания новых предложений

    -
    -
    - - -
    - -
    - -
    -
    📋 Основная информация
    - -
    - - -
    - -
    - - -
    -
    - - -
    -
    ⏳ Timelock
    -
    - - -
    -
    - - -
    -
    🔗 Выбор цепочки для кворума
    -

    Выберите цепочку, в которой будет происходить сбор голосов

    - -
    -
    -
    -
    {{ chain.name }}
    - Chain ID: {{ chain.chainId }} -

    {{ chain.description }}

    -
    -
    - -
    -
    -
    -
    - - - - -
    -
    🎯 Целевые сети для исполнения
    -
    - -
    - Выберите хотя бы одну целевую сеть для исполнения операции. -
    - ⚠️ Необходимо выбрать хотя бы одну целевую сеть -
    -
    - - - - -
    -
    ⚙️ Тип операции
    - -
    -
    - - -
    - - -
    -
    - - - Введите корректный Ethereum адрес (42 символа, начинается с 0x) -
    -
    - - - Введите количество токенов для передачи -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - - Процент от общего количества токенов (1-100%) -
    -
    - - -
    -
    - - - Выберите новую цепочку для управления DLE -
    -
    -
    -
    - - -
    - - -
    - - -
    -
    👁️ Предварительный просмотр
    -
    -
    - Описание: {{ newProposal.description || 'Не указано' }} -
    -
    - Длительность: {{ newProposal.duration || 7 }} дней -
    -
    - Цепочка для кворума: - {{ getChainName(newProposal.governanceChainId) || 'Не выбрана' }} -
    -
    - Тип операции: {{ getOperationTypeName(newProposal.operationType) || 'Не выбран' }} -
    -
    - Параметры: {{ getOperationParamsPreview() }} -
    -
    -
    -
    -
    -
    @@ -564,14 +203,8 @@ import { ref, computed, onMounted, onUnmounted, watch, defineProps, defineEmits, 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, decodeProposalData } from '../../services/proposalsService.js'; +import { getProposals, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js'; import api from '../../api/axios'; -const showTargetChains = computed(() => { - // Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн) - // Можно расширить логику при появлении offchain типа - return true; -}); import wsClient from '../../utils/websocket.js'; import { ethers } from 'ethers'; @@ -916,55 +549,14 @@ const goBackToBlocks = () => { const selectedDle = ref(null); const isLoadingDle = ref(false); -// Состояние формы -// const showCreateForm = ref(false); // Больше не нужно - форма всегда видна -const isCreating = ref(false); +// Состояние фильтров const statusFilter = ref(''); -// Новое предложение -const newProposal = ref({ - description: '', - duration: 7, - governanceChainId: null, - timelockHours: 0, - targetChains: [], - operationType: '', - operationParams: { - to: '', - from: '', - amount: 0, - customData: '', - name: '', - symbol: '', - location: '', - coordinates: '', - jurisdiction: 0, - oktmo: 0, - kpp: 0, - chainId: '' - } -}); - -// Доступные цепочки (загружаются из конфигурации) -const availableChains = ref([]); - // Предложения const proposals = ref([]); -// Вычисляемые свойства -const isFormValid = computed(() => { - return ( - newProposal.value.description && - newProposal.value.duration > 0 && - newProposal.value.governanceChainId && - newProposal.value.operationType && - newProposal.value.timelockHours >= 0 && - validateOperationParams() && - validateTargetChains() - ); -}); const filteredProposals = computed(() => { console.log('[Frontend] Фильтрация предложений. Всего:', proposals.value.length); @@ -1038,11 +630,6 @@ async function loadDleData() { })); console.log('[Frontend] Итоговый список предложений:', proposals.value); - - // Загружаем поддерживаемые цепочки - const chainsResponse = await getSupportedChains(dleAddress.value); - availableChains.value = chainsResponse.data?.chains || []; - } catch (error) { console.error('Ошибка загрузки данных DLE из блокчейна:', error); @@ -1051,51 +638,9 @@ async function loadDleData() { } } -function validateOperationParams() { - const params = newProposal.value.operationParams; - - switch (newProposal.value.operationType) { - case 'transfer': - case 'mint': - return validateAddress(params.to) && params.amount > 0; - case 'burn': - return validateAddress(params.from) && params.amount > 0; - case 'custom': - return params.customData && params.customData.startsWith('0x') && params.customData.length >= 10; - case 'updateDLEInfo': - return params.name && params.symbol && params.location && params.coordinates && params.jurisdiction && params.oktmo && params.kpp; - case 'updateQuorum': - return params.quorumPercentage >= 1 && params.quorumPercentage <= 100; - case 'updateChain': - return params.chainId && params.chainId !== ''; - default: - return false; - } -} - -function validateTargetChains() { - // Если показываем целевые сети, то должна быть выбрана хотя бы одна - if (showTargetChains.value) { - return newProposal.value.targetChains.length > 0; - } - return true; -} - -function validateAddress(address) { - if (!address) return false; - // Проверяем формат Ethereum адреса - const addressRegex = /^0x[a-fA-F0-9]{40}$/; - return addressRegex.test(address); -} function getChainName(chainId) { - // Сначала ищем в availableChains - if (Array.isArray(availableChains.value)) { - const chain = availableChains.value.find(c => c.chainId === chainId); - if (chain) return chain.name; - } - - // Если не найдено, используем известные chain ID + // Используем известные chain ID const knownChains = { 1: 'Ethereum Mainnet', 11155111: 'Sepolia Testnet', @@ -1107,42 +652,6 @@ function getChainName(chainId) { return knownChains[chainId] || `Chain ID: ${chainId}`; } -function getOperationTypeName(type) { - const types = { - 'transfer': 'Передача токенов', - 'mint': 'Минтинг токенов', - 'burn': 'Сжигание токенов', - 'custom': 'Пользовательская операция', - 'updateDLEInfo': 'Обновить данные DLE', - 'updateQuorum': 'Изменить кворум', - 'updateChain': 'Изменить текущую цепочку' - }; - return types[type] || 'Неизвестный тип'; -} - -function getOperationParamsPreview() { - const params = newProposal.value.operationParams; - - switch (newProposal.value.operationType) { - case 'transfer': - return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`; - case 'mint': - return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`; - case 'burn': - return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`; - case 'custom': - return `Данные: ${params.customData.substring(0, 20)}...`; - case 'updateDLEInfo': - return `Название: ${params.name}, Символ: ${params.symbol}, Местонахождение: ${params.location}, Координаты: ${params.coordinates}, Юрисдикция: ${params.jurisdiction}, ОКТМО: ${params.oktmo}, КПП: ${params.kpp}`; - case 'updateQuorum': - return `Процент кворума: ${params.quorumPercentage}%`; - case 'updateChain': - return `Новая цепочка: ${getChainName(params.chainId) || 'Не выбрана'}`; - default: - return 'Не указаны'; - } -} - function shortenAddress(address) { if (!address) return ''; return `${address.slice(0, 6)}...${address.slice(-4)}`; @@ -1481,153 +990,6 @@ function hasVotedFor(proposalId) { -// Создание предложения -async function createProposal() { - // Проверка авторизации для создания предложений - if (!props.isAuthenticated) { - alert('❌ Для создания предложений необходимо авторизоваться в приложении'); - return; - } - - if (!isFormValid.value) { - alert('Пожалуйста, заполните все обязательные поля'); - return; - } - - isCreating.value = true; - - try { - // Подготовка данных для смарт-контракта - const operation = encodeOperation(); - - // Создаем предложение через API - const result = await createProposalAPI(dleAddress.value, { - description: newProposal.value.description, - duration: newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды - operation: operation, - governanceChainId: newProposal.value.governanceChainId, - targetChains: showTargetChains.value ? newProposal.value.targetChains : [], - timelockDelay: (newProposal.value.timelockHours || 0) * 3600 - }); - - console.log('Предложение создано:', result); - - // Отправляем WebSocket уведомление - wsClient.send('proposal_created', { - dleAddress: dleAddress.value, - proposalId: result.proposalId, - txHash: result.txHash - }); - - // Ждем немного, чтобы блокчейн обработал транзакцию - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Обновляем список предложений - await loadDleData(); - - // Отправляем WebSocket уведомление о новом предложении - wsClient.send('proposal_created', { - dleAddress: dleAddress.value, - proposalId: result.proposalId, - txHash: result.txHash - }); - - // Сбрасываем форму - resetForm(); - // showCreateForm.value = false; // Больше не нужно - - alert('✅ Предложение успешно создано!'); - - } catch (error) { - console.error('Ошибка при создании предложения:', error); - alert('❌ Ошибка при создании предложения: ' + error.message); - } finally { - isCreating.value = false; - } -} - -function encodeOperation() { - const params = newProposal.value.operationParams; - - switch (newProposal.value.operationType) { - case 'transfer': - return encodeTransferOperation(params.to, params.amount); - case 'mint': - return encodeMintOperation(params.to, params.amount); - case 'burn': - return encodeBurnOperation(params.from, params.amount); - case 'custom': - return params.customData; - case 'updateDLEInfo': - return encodeUpdateDLEInfoOperation(params.name, params.symbol, params.location, params.coordinates, params.jurisdiction, params.oktmo, params.kpp); - case 'updateQuorum': - return encodeUpdateQuorumOperation(params.quorumPercentage); - case 'updateChain': - return encodeUpdateChainOperation(params.chainId); - default: - throw new Error('Неизвестный тип операции'); - } -} - -function encodeTransferOperation(to, amount) { - // Кодировка операции передачи токенов ERC20 - const selector = '0xa9059cbb'; // transfer(address,uint256) - const paddedAddress = to.slice(2).padStart(64, '0'); - const paddedAmount = BigInt(amount).toString(16).padStart(64, '0'); - return selector + paddedAddress + paddedAmount; -} - -function encodeMintOperation(to, amount) { - // Кодировка операции минтинга токенов - const selector = '0x40c10f19'; // mint(address,uint256) - const paddedAddress = to.slice(2).padStart(64, '0'); - const paddedAmount = BigInt(amount).toString(16).padStart(64, '0'); - return selector + paddedAddress + paddedAmount; -} - -function encodeBurnOperation(from, amount) { - // Кодировка операции сжигания токенов - const selector = '0x42966c68'; // burn(address,uint256) - const paddedAddress = from.slice(2).padStart(64, '0'); - const paddedAmount = BigInt(amount).toString(16).padStart(64, '0'); - return selector + paddedAddress + paddedAmount; -} - -function encodeUpdateDLEInfoOperation(name, symbol, location, coordinates, jurisdiction, oktmo, kpp) { - // Селектор для _updateDLEInfo(string,string,string,string,uint256,string[],uint256) - const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)')).slice(0, 10); - - // Кодируем параметры - const abiCoder = new ethers.AbiCoder(); - const encodedData = abiCoder.encode( - ['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'], - [name, symbol, location, coordinates, jurisdiction, [], kpp] // okvedCodes пока пустой массив - ); - - return selector + encodedData.slice(2); -} - -function encodeUpdateQuorumOperation(quorumPercentage) { - // Селектор для _updateQuorumPercentage(uint256) - const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateQuorumPercentage(uint256)')).slice(0, 10); - - // Кодируем параметр - const abiCoder = new ethers.AbiCoder(); - const encodedData = abiCoder.encode(['uint256'], [quorumPercentage]); - - return selector + encodedData.slice(2); -} - -function encodeUpdateChainOperation(chainId) { - // Селектор для _updateCurrentChainId(uint256) - const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateCurrentChainId(uint256)')).slice(0, 10); - - // Кодируем параметр - const abiCoder = new ethers.AbiCoder(); - const encodedData = abiCoder.encode(['uint256'], [chainId]); - - return selector + encodedData.slice(2); -} // Подпись предложения async function signProposalLocal(proposalId) { @@ -2027,28 +1389,6 @@ async function executeProposalLocal(proposalId) { -function resetForm() { - newProposal.value = { - description: '', - duration: 7, - governanceChainId: null, - operationType: '', - operationParams: { - to: '', - from: '', - amount: 0, - customData: '', - name: '', - symbol: '', - location: '', - coordinates: '', - jurisdiction: 0, - oktmo: 0, - kpp: 0, - chainId: '' - } - }; -} // Проверка прав администратора function hasAdminRights() { @@ -2315,115 +1655,6 @@ onUnmounted(() => { font-style: italic; } -.create-proposal-form { - background: #f8f9fa; - border-radius: 8px; - padding: 1.5rem; - margin-top: 2rem; - border-top: 2px solid #e9ecef; -} - -.form-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.close-btn { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: #666; -} - -.form-section { - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid #eee; -} - -.form-section:last-child { - border-bottom: none; -} - -.form-section h5 { - color: #333; - margin-bottom: 1rem; -} - -.chains-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem; -} - -.chain-option { - border: 2px solid #e9ecef; - border-radius: 8px; - padding: 1rem; - cursor: pointer; - transition: all 0.3s ease; -} - -.chain-option:hover { - border-color: #007bff; -} - -.chain-option.selected { - border-color: #007bff; - background: #f8f9ff; -} - -.chain-info h6 { - margin: 0 0 0.5rem 0; - color: #333; -} - -.chain-id { - font-size: 0.9rem; - color: #666; -} - -.chain-description { - font-size: 0.9rem; - color: #888; - margin: 0.5rem 0 0 0; -} - -.chain-status { - text-align: right; - color: #007bff; -} - -.operation-types { - margin-top: 1rem; -} - -.operation-params { - margin-top: 1rem; - padding: 1rem; - background: #f8f9fa; - border-radius: 6px; -} - -.preview-card { - background: #fff; - border: 1px solid #e9ecef; - border-radius: 6px; - padding: 1rem; -} - -.preview-item { - margin-bottom: 0.5rem; -} - -.form-actions { - display: flex; - gap: 1rem; - margin-top: 1.5rem; -} .proposals-list { margin-top: 2rem; @@ -2669,42 +1900,4 @@ onUnmounted(() => { font-weight: 500; } -/* Стили для ошибок валидации */ -.form-error { - margin-top: 0.5rem; - padding: 0.5rem; - background-color: #f8d7da; - border: 1px solid #f5c6cb; - border-radius: 4px; -} - -.text-danger { - color: #dc3545 !important; -} - -.targets-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 0.5rem; - margin-bottom: 1rem; -} - -.target-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem; - border: 1px solid #e9ecef; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; -} - -.target-item:hover { - background-color: #f8f9fa; -} - -.target-item input[type="checkbox"] { - margin: 0; -} \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index e352456..edfc0ac 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -64,6 +64,17 @@ export default defineConfig({ secure: false, credentials: true, rewrite: (path) => path, + configure: (proxy, options) => { + proxy.on('error', (err, req, res) => { + console.log('WebSocket proxy error:', err.message); + }); + proxy.on('proxyReqWs', (proxyReq, req, socket) => { + console.log('WebSocket proxy request to:', req.url); + }); + proxy.on('proxyResWs', (proxyRes, req, socket) => { + console.log('WebSocket proxy response:', proxyRes.statusCode); + }); + } }, }, watch: {