ваше сообщение коммита

This commit is contained in:
2025-09-25 03:02:31 +03:00
parent 792282cd75
commit 7b2f6937c8
34 changed files with 2900 additions and 2570 deletions

11
.gitignore vendored
View File

@@ -185,6 +185,17 @@ backend/typechain-types/
# Contract data (может содержать конфиденциальную информацию) # Contract data (может содержать конфиденциальную информацию)
backend/contracts-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 # Temporary test files
backend/test-*.js backend/test-*.js
backend/test_*.js backend/test_*.js

View File

@@ -144,7 +144,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
event ChainRemoved(uint256 chainId); event ChainRemoved(uint256 chainId);
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp);
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
event TokensTransferredByGovernance(address indexed recipient, uint256 amount); event TokensTransferredByGovernance(address indexed recipient, uint256 amount);
event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); 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 ErrBadKPP();
error ErrBadQuorum(); error ErrBadQuorum();
error ErrChainAlreadySupported(); error ErrChainAlreadySupported();
error ErrCannotAddCurrentChain();
error ErrChainNotSupported(); error ErrChainNotSupported();
error ErrCannotRemoveCurrentChain(); error ErrCannotRemoveCurrentChain();
error ErrTransfersDisabled(); error ErrTransfersDisabled();
@@ -601,10 +599,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
// Операция обновления процента кворума // Операция обновления процента кворума
(uint256 newQuorumPercentage) = abi.decode(data, (uint256)); (uint256 newQuorumPercentage) = abi.decode(data, (uint256));
_updateQuorumPercentage(newQuorumPercentage); _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)"))) { } else if (selector == bytes4(keccak256("_updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) {
// Операция обновления информации DLE // Операция обновления информации 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)); (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); 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) * @dev Перевести токены через governance (от имени DLE)

View File

@@ -378,11 +378,9 @@ contract TimelockModule is ReentrancyGuard {
// Обычные операции - стандартная задержка (2 дня) // Обычные операции - стандартная задержка (2 дня)
bytes4 updateDLEInfo = bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)")); 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)")); bytes4 updateVotingDurations = bytes4(keccak256("_updateVotingDurations(uint256,uint256)"));
operationDelays[updateDLEInfo] = 2 days; operationDelays[updateDLEInfo] = 2 days;
operationDelays[updateChainId] = 3 days;
operationDelays[updateVotingDurations] = 1 days; operationDelays[updateVotingDurations] = 1 days;
// Treasury операции - короткая задержка (1 день) // Treasury операции - короткая задержка (1 день)

View File

@@ -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. **Интеграция**: Единый процесс деплоя и верификации

View File

@@ -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}`);
```

View File

@@ -11,13 +11,39 @@
*/ */
require('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-toolbox');
require('@nomicfoundation/hardhat-verify');
require('hardhat-contract-sizer'); require('hardhat-contract-sizer');
require('dotenv').config(); require('dotenv').config();
function getNetworks() { 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 = { module.exports = {
@@ -38,17 +64,75 @@ module.exports = {
}, },
networks: getNetworks(), networks: getNetworks(),
etherscan: { etherscan: {
apiKey: { // Единый API ключ для V2 API
sepolia: process.env.ETHERSCAN_API_KEY || '', apiKey: process.env.ETHERSCAN_API_KEY || '',
mainnet: process.env.ETHERSCAN_API_KEY || '', customChains: [
polygon: process.env.POLYGONSCAN_API_KEY || '', {
arbitrumOne: process.env.ARBISCAN_API_KEY || '', network: "sepolia",
bsc: process.env.BSCSCAN_API_KEY || '', chainId: 11155111,
base: process.env.BASESCAN_API_KEY || '', urls: {
baseSepolia: process.env.BASESCAN_API_KEY || '', apiURL: "https://api.etherscan.io/v2/api",
arbitrumSepolia: process.env.ARBISCAN_API_KEY || '', 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: { solidityCoverage: {
excludeContracts: [], excludeContracts: [],
skipFiles: [], skipFiles: [],

View File

@@ -22,7 +22,8 @@
"run-migrations": "node scripts/run-migrations.js", "run-migrations": "node scripts/run-migrations.js",
"fix-duplicates": "node scripts/fix-duplicate-identities.js", "fix-duplicates": "node scripts/fix-duplicate-identities.js",
"deploy:multichain": "node scripts/deploy/deploy-multichain.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": { "dependencies": {
"@anthropic-ai/sdk": "^0.51.0", "@anthropic-ai/sdk": "^0.51.0",

View File

@@ -47,7 +47,6 @@ router.post('/get-extended-history', async (req, res) => {
"function listSupportedChains() external view returns (uint256[] memory)", "function listSupportedChains() external view returns (uint256[] memory)",
"function getProposalsCount() external view returns (uint256)", "function getProposalsCount() external view returns (uint256)",
"event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)", "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 DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)",
"event ModuleAdded(bytes32 moduleId, address moduleAddress)", "event ModuleAdded(bytes32 moduleId, address moduleAddress)",
"event ModuleRemoved(bytes32 moduleId)", "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 // События обновления информации DLE
const infoEvents = await dle.queryFilter('DLEInfoUpdated', fromBlock, currentBlock); const infoEvents = await dle.queryFilter('DLEInfoUpdated', fromBlock, currentBlock);

View File

@@ -12,7 +12,8 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const dleV2Service = require('../services/dleV2Service'); const DLEV2Service = require('../services/dleV2Service');
const dleV2Service = new DLEV2Service();
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const auth = require('../middleware/auth'); const auth = require('../middleware/auth');
const path = require('path'); const path = require('path');
@@ -34,7 +35,7 @@ async function executeDeploymentInBackground(deploymentId, dleParams) {
stage: 'initializing' stage: 'initializing'
}); });
deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта и модулей', 'info'); deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info');
// Выполняем деплой с передачей deploymentId для WebSocket обновлений // Выполняем деплой с передачей deploymentId для WebSocket обновлений
const result = await dleV2Service.createDLE(dleParams, deploymentId); const result = await dleV2Service.createDLE(dleParams, deploymentId);
@@ -77,11 +78,16 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
} }
} }
// Создаем запись о деплое // Используем deploymentId из запроса, если передан, иначе создаем новый
const deploymentId = deploymentTracker.createDeployment(dleParams); const deploymentId = req.body.deploymentId || deploymentTracker.createDeployment(dleParams);
// Запускаем деплой в фоне (без await!) // Если deploymentId был передан из запроса, создаем запись о деплое с этим ID
executeDeploymentInBackground(deploymentId, dleParams); if (req.body.deploymentId) {
deploymentTracker.createDeployment(dleParams, req.body.deploymentId);
}
// Запускаем деплой в фоне (с await для правильной обработки ошибок!)
await executeDeploymentInBackground(deploymentId, dleParams);
logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`); logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`);

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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 для остановки мониторинга');

View File

@@ -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); });

View File

@@ -14,6 +14,7 @@
const hre = require('hardhat'); const hre = require('hardhat');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { getRpcUrlByChainId } = require('../../services/rpcProviderService');
// Подбираем безопасные gas/fee для разных сетей (включая L2) // Подбираем безопасные gas/fee для разных сетей (включая L2)
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
@@ -32,7 +33,7 @@ async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20
return overrides; 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 { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl); const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider); 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); const existingCode = await provider.getCode(predictedAddress);
if (existingCode && existingCode !== '0x') { if (existingCode && existingCode !== '0x') {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`); 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) }; 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; const deployedAddress = rc.contractAddress || predictedAddress;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`); 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) }; 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() { async function main() {
const { ethers } = hre; 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'); const paramsPath = path.join(__dirname, './current-params.json');
if (!fs.existsSync(paramsPath)) { if (!fs.existsSync(paramsPath)) {
throw new Error('Файл параметров не найден: ' + 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] Загружены параметры:', { console.log('[MULTI_DBG] Загружены параметры:', {
name: params.name, name: params.name,
symbol: params.symbol, symbol: params.symbol,
@@ -495,16 +289,16 @@ async function main() {
CREATE2_SALT: params.CREATE2_SALT CREATE2_SALT: params.CREATE2_SALT
}); });
const pk = process.env.PRIVATE_KEY; const pk = params.private_key || process.env.PRIVATE_KEY;
const salt = params.CREATE2_SALT; const salt = params.CREATE2_SALT || params.create2_salt;
const networks = params.rpcUrls || []; const networks = params.rpcUrls || params.rpc_urls || [];
if (!pk) throw new Error('Env: PRIVATE_KEY'); if (!pk) throw new Error('Env: PRIVATE_KEY');
if (!salt) throw new Error('CREATE2_SALT 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'); if (networks.length === 0) throw new Error('RPC URLs not found in params');
// Prepare init code once // Prepare init code once
const DLE = await hre.ethers.getContractFactory('DLE'); const DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE');
const dleConfig = { const dleConfig = {
name: params.name || '', name: params.name || '',
symbol: params.symbol || '', symbol: params.symbol || '',
@@ -516,7 +310,7 @@ async function main() {
kpp: params.kpp ? BigInt(params.kpp) : 0n, kpp: params.kpp ? BigInt(params.kpp) : 0n,
quorumPercentage: params.quorumPercentage || 51, quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners || [], 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)) supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
}; };
const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"); const 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}`); 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}`); console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`);
return { rpcUrl, chainId, ...r }; return { rpcUrl, chainId, ...r };
} catch (error) { } 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] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
// Деплой модулей ОТКЛЮЧЕН - модули будут деплоиться отдельно // Автоматическая верификация контрактов
console.log('[MULTI_DBG] Module deployment DISABLED - modules will be deployed separately'); let verificationResults = [];
const moduleResults = [];
const 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) => ({ const finalResults = results.map((result, index) => ({
...result, ...result,
modules: moduleResults[index] || {}, verification: verificationResults[index] || 'failed'
verification: verificationResults[index] || {}
})); }));
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults)); console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
// Сохраняем каждый модуль в отдельный файл console.log('[MULTI_DBG] DLE deployment completed successfully!');
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}`);
} }
main().catch((e) => { console.error(e); process.exit(1); }); main().catch((e) => { console.error(e); process.exit(1); });

View File

@@ -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);
});

View File

@@ -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 };

View File

@@ -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<Object>} - Сохраненные параметры
*/
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<Object|null>} - Параметры деплоя или 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<Object>} - Обновленные параметры
*/
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<Array>} - Список параметров деплоя
*/
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<boolean>} - Результат удаления
*/
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;

File diff suppressed because it is too large Load Diff

View File

@@ -88,4 +88,16 @@ async function getRpcUrlByChainId(chainId) {
return providers[0]?.rpc_url || null; return providers[0]?.rpc_url || null;
} }
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider, getRpcUrlByNetworkId, getRpcUrlByChainId }; 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 };

View File

@@ -21,11 +21,11 @@ class DeploymentTracker extends EventEmitter {
} }
// Создать новый деплой // Создать новый деплой
createDeployment(params) { createDeployment(params, deploymentId = null) {
const deploymentId = this.generateDeploymentId(); const id = deploymentId || this.generateDeploymentId();
const deployment = { const deployment = {
id: deploymentId, id: id,
status: 'pending', status: 'pending',
stage: 'initializing', stage: 'initializing',
progress: 0, progress: 0,
@@ -38,10 +38,11 @@ class DeploymentTracker extends EventEmitter {
error: null error: null
}; };
this.deployments.set(deploymentId, deployment); this.deployments.set(id, deployment);
this.logger.info(`📝 Создан новый деплой: ${deploymentId}`); 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; if (!deployment) return false;
const logEntry = { const logEntry = {
id: Date.now() + Math.random(), // Уникальный ID для отслеживания дублирования
timestamp: new Date(), timestamp: new Date(),
message, message,
type type
@@ -83,6 +85,11 @@ class DeploymentTracker extends EventEmitter {
deployment.logs.push(logEntry); deployment.logs.push(logEntry);
deployment.updatedAt = new Date(); deployment.updatedAt = new Date();
// Логируем отправку лога для отладки дублирования (только в debug режиме)
if (process.env.DEBUG_DEPLOYMENT_LOGS) {
console.log(`[DEPLOYMENT_TRACKER] Отправляем лог ID=${logEntry.id}: ${message.substring(0, 50)}...`);
}
// Отправляем только лог через WebSocket (без дублирования) // Отправляем только лог через WebSocket (без дублирования)
this.emit('deployment_updated', { this.emit('deployment_updated', {
deploymentId, deploymentId,

View File

@@ -28,6 +28,7 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
function initWSS(server) { function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' }); wss = new WebSocket.Server({ server, path: '/ws' });
console.log('🔌 [WebSocket] Сервер инициализирован на пути /ws');
// Подключаем deployment tracker к WebSocket // Подключаем deployment tracker к WebSocket
deploymentTracker.on('deployment_updated', (data) => { deploymentTracker.on('deployment_updated', (data) => {
@@ -35,10 +36,10 @@ function initWSS(server) {
}); });
wss.on('connection', (ws, req) => { wss.on('connection', (ws, req) => {
// console.log('🔌 [WebSocket] Новое подключение'); console.log('🔌 [WebSocket] Новое подключение');
// console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress); console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress);
// console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']); console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']);
// console.log('🔌 [WebSocket] Origin:', req.headers.origin); console.log('🔌 [WebSocket] Origin:', req.headers.origin);
// Добавляем клиента в общий список // Добавляем клиента в общий список
if (!wsClients.has('anonymous')) { if (!wsClients.has('anonymous')) {
@@ -461,11 +462,15 @@ function broadcastTokenBalanceChanged(userId, tokenAddress, newBalance, network)
function broadcastDeploymentUpdate(data) { function broadcastDeploymentUpdate(data) {
if (!wss) return; if (!wss) return;
console.log(`📡 [WebSocket] broadcastDeploymentUpdate вызвана с данными:`, JSON.stringify(data, null, 2));
const message = JSON.stringify({ const message = JSON.stringify({
type: 'deployment_update', type: 'deployment_update',
data: data data: data
}); });
console.log(`📡 [WebSocket] Отправляем сообщение:`, message);
// Отправляем всем подключенным клиентам // Отправляем всем подключенным клиентам
wss.clients.forEach(client => { wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {

View File

@@ -167,23 +167,23 @@ services:
- '5173:5173' # Закрываем - используем nginx - '5173:5173' # Закрываем - используем nginx
command: yarn run dev -- --host 0.0.0.0 command: yarn run dev -- --host 0.0.0.0
ssh-tunnel-frontend: ssh-tunnel-frontend:
image: alpine:latest image: alpine:3.18
container_name: ssh-tunnel-frontend container_name: ssh-tunnel-frontend
volumes: volumes:
- ./id_rsa:/key:ro - ./id_rsa:/key:ro
command: > 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 restart: unless-stopped
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
ssh-tunnel-backend: ssh-tunnel-backend:
image: alpine:latest image: alpine:3.18
container_name: ssh-tunnel-backend container_name: ssh-tunnel-backend
volumes: volumes:
- ./id_rsa:/key:ro - ./id_rsa:/key:ro
command: > 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 restart: unless-stopped
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"

View File

@@ -192,16 +192,11 @@ function _updateDLEInfo(
function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal
``` ```
#### 3. Изменение текущей цепочки
```solidity
function _updateCurrentChainId(uint256 _newChainId) internal
```
#### 4. События для отслеживания изменений #### 3. События для отслеживания изменений
```solidity ```solidity
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp); 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 QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
``` ```
### Процесс изменения данных DLE ### Процесс изменения данных DLE

View File

@@ -156,6 +156,11 @@ const props = defineProps({
type: String, type: String,
required: false, required: false,
default: '' default: ''
},
autoVerifyAfterDeploy: {
type: Boolean,
required: false,
default: false
} }
}); });
@@ -250,8 +255,16 @@ const startDeployment = async () => {
try { try {
addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info'); 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 = { const deployData = {
deploymentId: tempDeploymentId, // Передаем временный ID в backend
name: props.dleData.name, name: props.dleData.name,
symbol: props.dleData.tokenSymbol, symbol: props.dleData.tokenSymbol,
location: props.dleData.addressData?.fullAddress || 'Не указан', location: props.dleData.addressData?.fullAddress || 'Не указан',
@@ -260,7 +273,7 @@ const startDeployment = async () => {
oktmo: props.dleData.selectedOktmo || '', oktmo: props.dleData.selectedOktmo || '',
okvedCodes: props.dleData.selectedOkved || [], okvedCodes: props.dleData.selectedOkved || [],
kpp: props.dleData.kppCode || '', 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), initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr),
initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0), initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0),
supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined), supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined),
@@ -268,7 +281,7 @@ const startDeployment = async () => {
logoURI: props.logoURI || '/uploads/logos/default-token.svg', logoURI: props.logoURI || '/uploads/logos/default-token.svg',
privateKey: props.privateKey, privateKey: props.privateKey,
etherscanApiKey: props.etherscanApiKey || '', etherscanApiKey: props.etherscanApiKey || '',
autoVerifyAfterDeploy: false autoVerifyAfterDeploy: props.autoVerifyAfterDeploy !== undefined ? props.autoVerifyAfterDeploy : false
}; };
addLog('📤 Отправляем запрос на асинхронный деплой...', 'info'); addLog('📤 Отправляем запрос на асинхронный деплой...', 'info');
@@ -279,8 +292,11 @@ const startDeployment = async () => {
if (response.data.success && response.data.deploymentId) { if (response.data.success && response.data.deploymentId) {
addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success'); addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success');
// Начинаем отслеживание через WebSocket // Обновляем deploymentId на реальный от сервера
if (response.data.deploymentId !== tempDeploymentId) {
addLog(`🔄 Обновляем ID деплоя: ${tempDeploymentId}${response.data.deploymentId}`, 'info');
startDeploymentTracking(response.data.deploymentId); startDeploymentTracking(response.data.deploymentId);
}
} else { } else {
throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка')); throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка'));

View File

@@ -47,30 +47,19 @@ export function useDeploymentWebSocket() {
// Обработчик WebSocket сообщений // Обработчик WebSocket сообщений
const handleDeploymentUpdate = (data) => { const handleDeploymentUpdate = (data) => {
if (data.deploymentId !== deploymentId.value) return;
console.log('🔄 [DeploymentWebSocket] Получено обновление:', data); 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) { switch (data.type) {
case 'deployment_started': case 'deployment_log':
deploymentStatus.value = 'in_progress'; if (data.log) {
isDeploying.value = true; addLog(data.log.message, data.log.type || 'info');
currentStage.value = data.stage || '';
addLog(`🚀 ${data.message}`, 'info');
break;
case 'deployment_progress':
currentStage.value = data.stage || '';
currentNetwork.value = data.network || '';
progress.value = data.progress || 0;
if (data.message) {
addLog(`📊 ${data.message}`, 'info');
}
break;
case 'deployment_stage_completed':
if (data.message) {
addLog(`${data.message}`, 'success');
} }
break; break;
@@ -87,34 +76,6 @@ export function useDeploymentWebSocket() {
} }
break; 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: case undefined:
// Обработка событий без типа (прямые обновления) // Обработка событий без типа (прямые обновления)
if (data.stage) currentStage.value = data.stage; if (data.stage) currentStage.value = data.stage;
@@ -136,6 +97,13 @@ export function useDeploymentWebSocket() {
} }
}; };
// Подключаемся к WebSocket сразу при инициализации
wsClient.connect();
if (wsClient && typeof wsClient.subscribe === 'function') {
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
console.log('🔌 [DeploymentWebSocket] Подключились к WebSocket при инициализации');
}
// Начать отслеживание деплоя // Начать отслеживание деплоя
const startDeploymentTracking = (id) => { const startDeploymentTracking = (id) => {
console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id); console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id);
@@ -145,13 +113,7 @@ export function useDeploymentWebSocket() {
isDeploying.value = true; isDeploying.value = true;
clearLogs(); clearLogs();
// Подключаемся к WebSocket обновлениям // WebSocket уже подключен при инициализации
wsClient.connect();
if (wsClient && typeof wsClient.subscribe === 'function') {
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
} else {
console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен');
}
addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info'); addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info');
}; };

View File

@@ -222,6 +222,11 @@ const routes = [
name: 'management-proposals', name: 'management-proposals',
component: () => import('../views/smartcontracts/DleProposalsView.vue') component: () => import('../views/smartcontracts/DleProposalsView.vue')
}, },
{
path: '/management/create-proposal',
name: 'management-create-proposal',
component: () => import('../views/smartcontracts/CreateProposalView.vue')
},
{ {
path: '/management/tokens', path: '/management/tokens',
name: 'management-tokens', name: 'management-tokens',

View File

@@ -26,7 +26,12 @@ class WebSocketClient {
connect() { connect() {
try { 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 = () => { this.ws.onopen = () => {
console.log('[WebSocket] Подключение установлено'); console.log('[WebSocket] Подключение установлено');
@@ -37,13 +42,21 @@ class WebSocketClient {
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data); 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)) { if (this.listeners.has(data.type)) {
console.log(`[WebSocket] Вызываем обработчики для типа: ${data.type}, количество: ${this.listeners.get(data.type).length}`);
this.listeners.get(data.type).forEach(callback => { this.listeners.get(data.type).forEach(callback => {
callback(data.data); callback(data.data);
}); });
} else {
console.log(`[WebSocket] Нет обработчиков для типа: ${data.type}`);
} }
} catch (error) { } catch (error) {
console.error('[WebSocket] Ошибка парсинга сообщения:', error); console.error('[WebSocket] Ошибка парсинга сообщения:', error);

View File

@@ -96,17 +96,26 @@
</ul> </ul>
</div> </div>
<div class="detail-item" v-else> <div class="detail-item" v-else>
<strong>Адрес контракта:</strong> <strong>Адреса контрактов:</strong>
<div class="addresses-list">
<div
v-for="network in dle.deployedNetworks || [{ chainId: 11155111, address: dle.dleAddress }]"
:key="network.chainId"
class="address-item"
>
<span class="chain-name">{{ getChainName(network.chainId) }}:</span>
<a <a
:href="`https://sepolia.etherscan.io/address/${dle.dleAddress}`" :href="getExplorerUrl(network.chainId, network.address)"
target="_blank" target="_blank"
class="address-link" class="address-link"
@click.stop @click.stop
> >
{{ shortenAddress(dle.dleAddress) }} {{ shortenAddress(network.address) }}
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
</div> </div>
</div>
</div>
<div class="detail-item"> <div class="detail-item">
<strong>Местоположение:</strong> {{ dle.location }} <strong>Местоположение:</strong> {{ dle.location }}
</div> </div>
@@ -124,17 +133,13 @@
<strong>Статус:</strong> <strong>Статус:</strong>
<span class="status active">Активен</span> <span class="status active">Активен</span>
</div> </div>
<div class="detail-item" v-if="dle.totalSupply"> <div class="detail-item">
<strong>Общий объем токенов:</strong> <strong>Общий объем токенов:</strong>
<span class="token-supply">{{ parseFloat(dle.totalSupply).toLocaleString() }} {{ dle.symbol }}</span> <span class="token-supply">{{ formatTokenAmount(dle.totalSupply || 0) }} {{ dle.symbol }}</span>
</div> </div>
<div class="detail-item" v-if="dle.logoURI"> <div class="detail-item">
<strong>Логотип:</strong>
<span class="logo-info">Установлен</span>
</div>
<div class="detail-item" v-if="dle.creationTimestamp">
<strong>Дата создания:</strong> <strong>Дата создания:</strong>
<span class="creation-date">{{ formatTimestamp(dle.creationTimestamp) }}</span> <span class="creation-date">{{ formatTimestamp(dle.creationTimestamp || dle.createdAt) }}</span>
</div> </div>
</div> </div>
@@ -345,7 +350,18 @@ function getExplorerUrl(chainId, address) {
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
if (!timestamp) return ''; 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', { return date.toLocaleDateString('ru-RU', {
year: 'numeric', year: 'numeric',
month: 'long', 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) { function openDleOnEtherscan(address) {
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank'); window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
} }

View File

@@ -513,18 +513,6 @@
</div> </div>
</div> </div>
<!-- Предсказанный адрес DLE - отключено -->
<!-- <div v-if="selectedNetworks.length > 0" class="predicted-address-section">
<h5>📍 Адрес DLE во всех сетях:</h5>
<div class="address-display">
<code class="dle-address">{{ predictedAddress || 'Вычисляется...' }}</code>
<button v-if="predictedAddress" @click="copyAddress" class="copy-btn" title="Копировать адрес">
<i class="fas fa-copy"></i>
</button>
</div>
</div> -->
<!-- Кнопки управления RPC --> <!-- Кнопки управления RPC -->
<div class="rpc-settings-actions"> <div class="rpc-settings-actions">
@@ -631,7 +619,7 @@
<!-- Требования к балансу --> <!-- Требования к балансу -->
<div v-if="selectedNetworks.length > 0" class="balance-requirements"> <div v-if="selectedNetworks.length > 0" class="balance-requirements">
<h5>💰 Требования к балансу:</h5> <h5>Требования к балансу:</h5>
<div class="balance-grid"> <div class="balance-grid">
<div <div
v-for="network in selectedNetworkDetails" v-for="network in selectedNetworkDetails"
@@ -655,7 +643,7 @@
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
</div> </div>
<div class="security-content"> <div class="security-content">
<h5>🔒 Рекомендации по безопасности:</h5> <h5>Рекомендации по безопасности:</h5>
<ul> <ul>
<li>Используйте отдельный кошелек только для деплоя DLE</li> <li>Используйте отдельный кошелек только для деплоя DLE</li>
<li>Убедитесь, что на кошельке достаточно средств для оплаты газа</li> <li>Убедитесь, что на кошельке достаточно средств для оплаты газа</li>
@@ -697,7 +685,7 @@
<h4>Основная информация DLE</h4> <h4>Основная информация DLE</h4>
<div v-if="logoPreviewUrl" class="preview-item"> <div v-if="logoPreviewUrl" class="preview-item">
<strong>🎨 Логотип:</strong> <strong>Логотип:</strong>
<div style="display: flex; align-items: center; gap: 10px; margin-top: 5px;"> <div style="display: flex; align-items: center; gap: 10px; margin-top: 5px;">
<img :src="logoPreviewUrl" alt="Logo preview" style="width: 48px; height: 48px; border-radius: 6px; object-fit: contain; border: 1px solid #e9ecef;" /> <img :src="logoPreviewUrl" alt="Logo preview" style="width: 48px; height: 48px; border-radius: 6px; object-fit: contain; border: 1px solid #e9ecef;" />
<span style="color: #666; font-size: 0.9em;">{{ logoFile?.name || 'ENS аватар' || 'Дефолтный логотип' }}</span> <span style="color: #666; font-size: 0.9em;">{{ logoFile?.name || 'ENS аватар' || 'Дефолтный логотип' }}</span>
@@ -705,11 +693,11 @@
</div> </div>
<div v-if="dleSettings.name" class="preview-item"> <div v-if="dleSettings.name" class="preview-item">
<strong>📋 Название:</strong> {{ dleSettings.name }} <strong>Название:</strong> {{ dleSettings.name }}
</div> </div>
<div v-if="dleSettings.tokenSymbol" class="preview-item"> <div v-if="dleSettings.tokenSymbol" class="preview-item">
<strong>🪙 Токен:</strong> {{ dleSettings.tokenSymbol }} <strong>Токен:</strong> {{ dleSettings.tokenSymbol }}
</div> </div>
@@ -723,7 +711,7 @@
<div v-for="(partner, index) in dleSettings.partners" :key="index"> <div v-for="(partner, index) in dleSettings.partners" :key="index">
<div v-if="partner.address || partner.amount > 1" class="preview-item"> <div v-if="partner.address || partner.amount > 1" class="preview-item">
<strong>👥 Партнер {{ index + 1 }}:</strong> <strong>Партнер {{ index + 1 }}:</strong>
<div class="partner-details"> <div class="partner-details">
<div v-if="partner.address" class="partner-address"> <div v-if="partner.address" class="partner-address">
Адрес: {{ partner.address.substring(0, 10) }}...{{ partner.address.substring(partner.address.length - 8) }} Адрес: {{ partner.address.substring(0, 10) }}...{{ partner.address.substring(partner.address.length - 8) }}
@@ -736,11 +724,11 @@
</div> </div>
<div class="preview-item"> <div class="preview-item">
<strong>💰 Общий эмиссия:</strong> {{ totalTokens }} токенов <strong>Общий эмиссия:</strong> {{ totalTokens }} токенов
</div> </div>
<div class="preview-item"> <div class="preview-item">
<strong>🗳 Кворум подписей партнеров:</strong> {{ dleSettings.governanceQuorum }}% <strong>Кворум подписей партнеров:</strong> {{ dleSettings.governanceQuorum }}%
</div> </div>
</div> </div>
@@ -749,11 +737,11 @@
<h4>🔗 Мульти-чейн деплой</h4> <h4>🔗 Мульти-чейн деплой</h4>
<!-- <div class="preview-item"> <!-- <div class="preview-item">
<strong>📍 Адрес DLE:</strong> {{ predictedAddress || 'Вычисляется...' }} <strong> Адрес DLE:</strong> {{ predictedAddress || 'Вычисляется...' }}
</div> --> </div> -->
<div class="preview-item"> <div class="preview-item">
<strong>🌐 Выбранные сети:</strong> <strong>Выбранные сети:</strong>
<ul class="networks-list"> <ul class="networks-list">
<li v-for="network in selectedNetworkDetails" :key="network.chainId"> <li v-for="network in selectedNetworkDetails" :key="network.chainId">
{{ network.name }} (Chain ID: {{ network.chainId }}) - ~${{ network.estimatedCost }} {{ network.name }} (Chain ID: {{ network.chainId }}) - ~${{ network.estimatedCost }}
@@ -762,7 +750,7 @@
</div> </div>
<div class="preview-item"> <div class="preview-item">
<strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }} <strong>Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
</div> </div>
<!-- Предсказанные адреса скрыты, чтобы не создавать шум при отсутствии данных --> <!-- Предсказанные адреса скрыты, чтобы не создавать шум при отсутствии данных -->
@@ -775,7 +763,7 @@
<h4>🔐 Приватный ключ</h4> <h4>🔐 Приватный ключ</h4>
<div class="preview-item"> <div class="preview-item">
<strong>🔑 Ключ:</strong> ***{{ unifiedPrivateKey.slice(-4) }} <strong>Ключ:</strong> ***{{ unifiedPrivateKey.slice(-4) }}
</div> </div>
<div v-if="keyValidation.unified && keyValidation.unified.isValid" class="preview-item"> <div v-if="keyValidation.unified && keyValidation.unified.isValid" class="preview-item">
@@ -830,7 +818,7 @@
<!-- Координаты --> <!-- Координаты -->
<div v-if="dleSettings.coordinates" class="preview-item"> <div v-if="dleSettings.coordinates" class="preview-item">
<strong>📍 Координаты:</strong> {{ dleSettings.coordinates }} <strong>📍Координаты:</strong> {{ dleSettings.coordinates }}
</div> </div>
<!-- Кнопка деплоя смарт-контрактов --> <!-- Кнопка деплоя смарт-контрактов -->
@@ -866,8 +854,8 @@
@click="deploySmartContracts" @click="deploySmartContracts"
type="button" type="button"
class="btn btn-primary btn-lg deploy-btn" class="btn btn-primary btn-lg deploy-btn"
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading || showDeployProgress" :disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading"
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`" :title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}`"
> >
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
Поэтапный деплой DLE Поэтапный деплой DLE
@@ -877,48 +865,12 @@
@click="clearAllData" @click="clearAllData"
class="btn btn-danger btn-lg clear-btn" class="btn btn-danger btn-lg clear-btn"
title="Очистить все данные" title="Очистить все данные"
:disabled="showDeployProgress" :disabled="false"
> >
Удалить все Удалить все
</button> </button>
</div> </div>
<!-- Индикатор процесса деплоя -->
<div v-if="showDeployProgress" class="deploy-progress">
<div class="progress-header">
<h4>🚀 Деплой DLE в блокчейне</h4>
<p>{{ deployStatus }}</p>
</div>
<div class="progress-bar-container">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: deployProgress + '%' }"
></div>
</div>
<span class="progress-text">{{ deployProgress }}%</span>
</div>
<div class="progress-steps">
<div class="step" :class="{ active: deployProgress >= 10 }">
<i class="fas fa-check-circle"></i>
<span>Подготовка данных</span>
</div>
<div class="step" :class="{ active: deployProgress >= 30 }">
<i class="fas fa-check-circle"></i>
<span>Отправка на сервер</span>
</div>
<div class="step" :class="{ active: deployProgress >= 70 }">
<i class="fas fa-check-circle"></i>
<span>Деплой в блокчейне</span>
</div>
<div class="step" :class="{ active: deployProgress >= 100 }">
<i class="fas fa-check-circle"></i>
<span>Завершение</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -939,6 +891,7 @@
:dle-data="dleSettings" :dle-data="dleSettings"
:logo-uri="getLogoURI()" :logo-uri="getLogoURI()"
:etherscan-api-key="etherscanApiKey" :etherscan-api-key="etherscanApiKey"
:auto-verify-after-deploy="autoVerifyAfterDeploy"
@deployment-completed="handleDeploymentCompleted" @deployment-completed="handleDeploymentCompleted"
/> />
</div> </div>
@@ -1113,33 +1066,6 @@ const hasSelectedNetworks = computed(() => {
return selectedNetworks.value.length > 0; 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) { function copyToClipboard(text) {
navigator.clipboard?.writeText(text).then(() => { navigator.clipboard?.writeText(text).then(() => {
// no-op // no-op
@@ -1190,10 +1116,6 @@ const selectedOkvedLevel4 = ref('');
const currentSelectedOkvedCode = ref(''); const currentSelectedOkvedCode = ref('');
const currentSelectedOkvedText = ref(''); const currentSelectedOkvedText = ref('');
// Состояние процесса деплоя
const showDeployProgress = ref(false);
const deployProgress = ref(0);
const deployStatus = ref('');
// Функция определения уровня ОКВЭД кода // Функция определения уровня ОКВЭД кода
const getOkvedLevel = (code) => { const getOkvedLevel = (code) => {
@@ -2399,10 +2321,6 @@ watch(unifiedPrivateKey, (newValue) => {
// Инициализация // Инициализация
onMounted(() => { onMounted(() => {
// Сбрасываем состояние деплоя при загрузке страницы
showDeployProgress.value = false;
deployProgress.value = 0;
deployStatus.value = '';
// Загружаем список стран // Загружаем список стран
loadCountries(); loadCountries();
@@ -2544,7 +2462,6 @@ const deploySmartContracts = async () => {
} catch (error) { } catch (error) {
console.error('Ошибка деплоя DLE:', error); console.error('Ошибка деплоя DLE:', error);
showDeployProgress.value = false;
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message); alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
} }
}; };
@@ -2555,10 +2472,6 @@ const startStagedDeployment = async () => {
// Сначала выполняем стандартный деплой DLE контракта // Сначала выполняем стандартный деплой DLE контракта
try { try {
// Показываем индикатор процесса
showDeployProgress.value = true;
deployProgress.value = 10;
deployStatus.value = 'Подготовка данных для деплоя DLE...';
// Подготовка данных для деплоя // Подготовка данных для деплоя
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks); console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
@@ -2591,7 +2504,7 @@ const startStagedDeployment = async () => {
privateKey: unifiedPrivateKey.value, privateKey: unifiedPrivateKey.value,
// Верификация через Etherscan V2 // Верификация через Etherscan V2
etherscanApiKey: etherscanApiKey.value, etherscanApiKey: etherscanApiKey.value,
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
}; };
// Обработка логотипа // Обработка логотипа
@@ -2617,8 +2530,6 @@ const startStagedDeployment = async () => {
console.log('Данные для деплоя DLE:', deployData); console.log('Данные для деплоя DLE:', deployData);
// Предварительная проверка балансов (через приватный ключ) // Предварительная проверка балансов (через приватный ключ)
deployProgress.value = 20;
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
try { try {
const pre = await api.post('/dle-v2/precheck', { const pre = await api.post('/dle-v2/precheck', {
supportedChainIds: deployData.supportedChainIds, supportedChainIds: deployData.supportedChainIds,
@@ -2630,7 +2541,6 @@ const startStagedDeployment = async () => {
if (lacks.length > 0) { if (lacks.length > 0) {
const message = `❌ Недостаточно средств в некоторых сетях!`; const message = `❌ Недостаточно средств в некоторых сетях!`;
alert(message); alert(message);
showDeployProgress.value = false;
return; return;
} }
console.log('✅ Проверка балансов пройдена:', preData.summary); console.log('✅ Проверка балансов пройдена:', preData.summary);
@@ -2639,25 +2549,6 @@ const startStagedDeployment = async () => {
console.warn('⚠️ Ошибка проверки балансов:', e.message); 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; showDeploymentWizard.value = true;
@@ -2665,8 +2556,6 @@ const startStagedDeployment = async () => {
return; return;
} catch (error) { } catch (error) {
console.error('Ошибка при запуске деплоя:', error); console.error('Ошибка при запуске деплоя:', error);
deployStatus.value = `❌ Ошибка: ${error.message}`;
deployProgress.value = 0;
} }
} }
@@ -2697,11 +2586,10 @@ const handleDeploymentCompleted = (result) => {
console.log('🔍 Валидация формы:', validation); console.log('🔍 Валидация формы:', validation);
console.log('🔍 selectedNetworks.value:', selectedNetworks.value); console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
console.log('🔍 adminTokenCheck:', adminTokenCheck.value); console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
console.log('🔍 showDeployProgress:', showDeployProgress.value);
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value); console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
console.log('🔍 keyValidation.unified:', keyValidation.unified); console.log('🔍 keyValidation.unified:', keyValidation.unified);
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates); 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( return Boolean(
validation.jurisdiction && validation.jurisdiction &&
@@ -4588,103 +4476,6 @@ async function submitDeploy() {
border: 1px solid #f5c6cb; 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 { .token-image-upload {

View File

@@ -0,0 +1,591 @@
<!--
Copyright (c) 2024-2025 Тарабанов Александр Викторович
All rights reserved.
This software is proprietary and confidential.
Unauthorized copying, modification, or distribution is prohibited.
For licensing inquiries: info@hb3-accelerator.com
Website: https://hb3-accelerator.com
GitHub: https://github.com/HB3-ACCELERATOR
-->
<template>
<BaseLayout
:is-authenticated="isAuthenticated"
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="create-proposal-page">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Создание предложения</h1>
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
<p v-else-if="isLoadingDle">Загрузка...</p>
<p v-else>DLE не выбран</p>
</div>
<button class="close-btn" @click="goBackToBlocks">×</button>
</div>
<!-- Блоки операций DLE -->
<div class="operations-blocks">
<div class="blocks-header">
<h4>Типы операций DLE контракта</h4>
<p>Выберите тип операции для создания предложения</p>
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
</div>
</div>
<!-- Блоки операций -->
<div class="operations-grid">
<!-- Управление токенами -->
<div class="operation-category">
<h5>💸 Управление токенами</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">💸</div>
<h6>Передача токенов</h6>
<p>Перевод токенов DLE другому адресу через governance</p>
<button class="create-btn" @click="openTransferForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление модулями -->
<div class="operation-category">
<h5>🔧 Управление модулями</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Добавить модуль</h6>
<p>Добавление нового модуля в DLE контракт</p>
<button class="create-btn" @click="openAddModuleForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Удалить модуль</h6>
<p>Удаление существующего модуля из DLE контракта</p>
<button class="create-btn" @click="openRemoveModuleForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление сетями -->
<div class="operation-category">
<h5>🌐 Управление сетями</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Добавить сеть</h6>
<p>Добавление новой поддерживаемой блокчейн сети</p>
<button class="create-btn" @click="openAddChainForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Удалить сеть</h6>
<p>Удаление поддерживаемой блокчейн сети</p>
<button class="create-btn" @click="openRemoveChainForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление настройками DLE -->
<div class="operation-category">
<h5> Настройки DLE</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">📝</div>
<h6>Обновить данные DLE</h6>
<p>Изменение основной информации о DLE (название, символ, адрес и т.д.)</p>
<button class="create-btn" @click="openUpdateDLEInfoForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon">📊</div>
<h6>Изменить кворум</h6>
<p>Изменение процента голосов, необходимого для принятия решений</p>
<button class="create-btn" @click="openUpdateQuorumForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Изменить время голосования</h6>
<p>Настройка минимального и максимального времени голосования</p>
<button class="create-btn" @click="openUpdateVotingDurationsForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon">🖼</div>
<h6>Изменить логотип</h6>
<p>Обновление URI логотипа DLE для отображения в блокчейн-сканерах</p>
<button class="create-btn" @click="openSetLogoURIForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Оффчейн операции -->
<div class="operation-category">
<h5>📋 Оффчейн операции</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">📄</div>
<h6>Оффчейн действие</h6>
<p>Создание предложения для выполнения оффчейн операций в приложении</p>
<button class="create-btn" @click="openOffchainActionForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, computed, onMounted, defineProps, defineEmits, inject } from 'vue';
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 { createProposal as createProposalAPI } from '../../services/proposalsService.js';
import api from '../../api/axios';
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
// Можно расширить логику при появлении offchain типа
return true;
});
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
const emit = defineEmits(['auth-action-completed']);
const { address, isAuthenticated, tokenBalances, checkTokenBalances } = useAuthContext();
const router = useRouter();
const route = useRoute();
// Получаем адрес DLE из URL
const dleAddress = computed(() => {
const address = route.query.address || props.dleAddress;
console.log('DLE Address from URL:', address);
return address;
});
// Функция возврата к блокам управления
const goBackToBlocks = () => {
if (dleAddress.value) {
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
} else {
router.push('/management');
}
};
// Состояние DLE
const selectedDle = ref(null);
const isLoadingDle = ref(false);
// Доступные цепочки (загружаются из конфигурации)
const availableChains = ref([]);
// Функции для открытия отдельных форм операций
function openTransferForm() {
// TODO: Открыть форму для передачи токенов
alert('Форма передачи токенов будет реализована');
}
function openAddModuleForm() {
// TODO: Открыть форму для добавления модуля
alert('Форма добавления модуля будет реализована');
}
function openRemoveModuleForm() {
// TODO: Открыть форму для удаления модуля
alert('Форма удаления модуля будет реализована');
}
function openAddChainForm() {
// TODO: Открыть форму для добавления сети
alert('Форма добавления сети будет реализована');
}
function openRemoveChainForm() {
// TODO: Открыть форму для удаления сети
alert('Форма удаления сети будет реализована');
}
function openUpdateDLEInfoForm() {
// TODO: Открыть форму для обновления данных DLE
alert('Форма обновления данных DLE будет реализована');
}
function openUpdateQuorumForm() {
// TODO: Открыть форму для изменения кворума
alert('Форма изменения кворума будет реализована');
}
function openUpdateVotingDurationsForm() {
// TODO: Открыть форму для изменения времени голосования
alert('Форма изменения времени голосования будет реализована');
}
function openSetLogoURIForm() {
// TODO: Открыть форму для изменения логотипа
alert('Форма изменения логотипа будет реализована');
}
function openOffchainActionForm() {
// TODO: Открыть форму для оффчейн действий
alert('Форма оффчейн действий будет реализована');
}
// Функции
async function loadDleData() {
console.log('loadDleData вызвана с адресом:', dleAddress.value);
if (!dleAddress.value) {
console.warn('Адрес DLE не указан');
return;
}
isLoadingDle.value = true;
try {
// Загружаем данные DLE из блокчейна
const response = await api.post('/dle-core/read-dle-info', {
dleAddress: dleAddress.value
});
if (response.data.success) {
selectedDle.value = response.data.data;
console.log('Загружены данные DLE из блокчейна:', selectedDle.value);
} else {
console.error('Ошибка загрузки DLE:', response.data.error);
}
// Загружаем поддерживаемые цепочки
const chainsResponse = await getSupportedChains(dleAddress.value);
availableChains.value = chainsResponse.data?.chains || [];
} catch (error) {
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
} finally {
isLoadingDle.value = false;
}
}
onMounted(async () => {
// Принудительно загружаем токены, если пользователь аутентифицирован
if (isAuthenticated.value && address.value) {
console.log('[CreateProposalView] Принудительная загрузка токенов для адреса:', address.value);
await checkTokenBalances(address.value);
}
// Загрузка данных DLE
if (dleAddress.value) {
loadDleData();
}
});
</script>
<style scoped>
.create-proposal-page {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
/* Заголовок */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2rem;
margin: 0 0 5px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Стили для блоков операций */
.operations-blocks {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
padding: 2rem;
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.blocks-header {
margin-bottom: 2rem;
text-align: center;
}
.blocks-header h4 {
color: var(--color-primary);
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 600;
}
.blocks-header p {
color: #6c757d;
margin: 0;
font-size: 1rem;
}
.auth-notice {
margin-bottom: 2rem;
}
.alert {
padding: 1rem 1.5rem;
border-radius: 8px;
border: 1px solid transparent;
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.alert-info {
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}
.alert i {
margin-top: 0.25rem;
flex-shrink: 0;
}
.operations-grid {
display: flex;
flex-direction: column;
gap: 2rem;
}
.operation-category {
background: white;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.operation-category h5 {
color: var(--color-primary);
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #f0f0f0;
}
.operation-blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.operation-block {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.operation-block::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--color-primary), #20c997);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.operation-block:hover {
border-color: var(--color-primary);
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
transform: translateY(-4px);
}
.operation-block:hover::before {
transform: scaleX(1);
}
.operation-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.operation-block h6 {
color: #333;
margin: 0 0 0.75rem 0;
font-size: 1.1rem;
font-weight: 600;
}
.operation-block p {
color: #666;
margin: 0 0 1.5rem 0;
font-size: 0.9rem;
line-height: 1.5;
}
.create-btn {
background: linear-gradient(135deg, var(--color-primary), #20c997);
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
}
.create-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.create-btn:hover {
background: linear-gradient(135deg, #0056b3, #1ea085);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
}
.create-btn:hover::before {
left: 100%;
}
.create-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.create-btn:disabled::before {
display: none;
}
/* Адаптивность */
@media (max-width: 768px) {
.operations-blocks {
padding: 1rem;
}
.operation-blocks {
grid-template-columns: 1fr;
}
.operation-block {
padding: 1rem;
}
.operation-icon {
font-size: 2.5rem;
}
.blocks-header h4 {
font-size: 1.25rem;
}
.operation-category h5 {
font-size: 1.1rem;
}
}
</style>

View File

@@ -34,6 +34,14 @@
<div class="management-blocks"> <div class="management-blocks">
<!-- Первый ряд --> <!-- Первый ряд -->
<div class="blocks-row"> <div class="blocks-row">
<div class="management-block create-proposal-block">
<h3>Создать предложение</h3>
<p>Универсальная форма для создания новых предложений</p>
<button class="details-btn create-btn" @click="openCreateProposal">
Подробнее
</button>
</div>
<div class="management-block"> <div class="management-block">
<h3>Предложения</h3> <h3>Предложения</h3>
<p>Создание, подписание, выполнение</p> <p>Создание, подписание, выполнение</p>
@@ -45,16 +53,16 @@
<p>Балансы, трансферы, распределение</p> <p>Балансы, трансферы, распределение</p>
<button class="details-btn" @click="openTokens">Подробнее</button> <button class="details-btn" @click="openTokens">Подробнее</button>
</div> </div>
</div>
<!-- Второй ряд -->
<div class="blocks-row">
<div class="management-block"> <div class="management-block">
<h3>Кворум</h3> <h3>Кворум</h3>
<p>Настройки голосования</p> <p>Настройки голосования</p>
<button class="details-btn" @click="openQuorum">Подробнее</button> <button class="details-btn" @click="openQuorum">Подробнее</button>
</div> </div>
</div>
<!-- Второй ряд -->
<div class="blocks-row">
<div class="management-block"> <div class="management-block">
<h3>Модули DLE</h3> <h3>Модули DLE</h3>
<p>Установка, настройка, управление</p> <p>Установка, настройка, управление</p>
@@ -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(() => { onMounted(() => {
// Если нет адреса DLE, перенаправляем на главную страницу management // Если нет адреса DLE, перенаправляем на главную страницу management
if (!dleAddress.value) { if (!dleAddress.value) {
@@ -279,6 +295,32 @@ onMounted(() => {
transform: translateY(-1px); 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) { @media (max-width: 768px) {
.blocks-row { .blocks-row {

View File

@@ -195,367 +195,6 @@
</div> </div>
</div> </div>
<!-- Форма создания предложения (всегда внизу страницы) -->
<div class="create-proposal-form">
<div class="form-header">
<h4>📝 Создание нового предложения</h4>
<!-- Кнопка закрытия больше не нужна -->
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice-form">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
</div>
</div>
<!-- Форма только для авторизованных пользователей -->
<div v-else>
<div class="form-content">
<!-- Основная информация -->
<div class="form-section">
<h5>📋 Основная информация</h5>
<div class="form-group">
<label for="proposalDescription">Описание предложения:</label>
<textarea
id="proposalDescription"
v-model="newProposal.description"
class="form-control"
rows="3"
placeholder="Опишите, что нужно сделать..."
></textarea>
</div>
<div class="form-group">
<label for="proposalDuration">Длительность голосования (дни):</label>
<input
type="number"
id="proposalDuration"
v-model.number="newProposal.duration"
class="form-control"
min="1"
max="30"
placeholder="7"
>
</div>
</div>
<!-- Timelock -->
<div class="form-section">
<h5> Timelock</h5>
<div class="form-group-inline">
<label for="timelockHours">Задержка исполнения (часы):</label>
<input id="timelockHours" type="number" min="0" step="1" v-model.number="newProposal.timelockHours" class="form-control small" />
</div>
</div>
<!-- Выбор цепочки для кворума -->
<div class="form-section">
<h5>🔗 Выбор цепочки для кворума</h5>
<p class="form-help">Выберите цепочку, в которой будет происходить сбор голосов</p>
<div class="chains-grid">
<div
v-for="chain in availableChains"
:key="chain.chainId"
class="chain-option"
:class="{ 'selected': newProposal.governanceChainId === chain.chainId }"
@click="newProposal.governanceChainId = chain.chainId"
>
<div class="chain-info">
<h6>{{ chain.name }}</h6>
<span class="chain-id">Chain ID: {{ chain.chainId }}</span>
<p class="chain-description">{{ chain.description }}</p>
</div>
<div class="chain-status">
<i v-if="newProposal.governanceChainId === chain.chainId" class="fas fa-check"></i>
</div>
</div>
</div>
</div>
<!-- Целевые сети для исполнения (мультиселект) -->
<div class="form-section" v-if="showTargetChains">
<h5>🎯 Целевые сети для исполнения</h5>
<div class="targets-grid">
<label v-for="chain in availableChains" :key="chain.chainId" class="target-item">
<input type="checkbox" :value="chain.chainId" v-model="newProposal.targetChains" />
<span>{{ chain.name }} ({{ chain.chainId }})</span>
</label>
</div>
<small class="text-muted">Выберите хотя бы одну целевую сеть для исполнения операции.</small>
<div v-if="showTargetChains && newProposal.targetChains.length === 0" class="form-error">
<small class="text-danger"> Необходимо выбрать хотя бы одну целевую сеть</small>
</div>
</div>
<!-- Тип операции (последним блоком) -->
<div class="form-section">
<h5> Тип операции</h5>
<div class="operation-types">
<div class="form-group">
<label for="operationType">Выберите тип операции:</label>
<select id="operationType" v-model="newProposal.operationType" class="form-control">
<option value="">-- Выберите тип --</option>
<option value="transfer">Передача токенов</option>
<option value="mint">Минтинг токенов</option>
<option value="burn">Сжигание токенов</option>
<option value="updateDLEInfo">Обновить данные DLE</option>
<option value="updateQuorum">Изменить кворум</option>
<option value="updateChain">Изменить текущую цепочку</option>
<option value="custom">Пользовательская операция</option>
</select>
</div>
<!-- Параметры для передачи токенов -->
<div v-if="newProposal.operationType === 'transfer'" class="operation-params">
<div class="form-group">
<label for="transferTo">Адрес получателя:</label>
<input
type="text"
id="transferTo"
v-model="newProposal.operationParams.to"
class="form-control"
placeholder="0x1234567890abcdef1234567890abcdef12345678"
:class="{ 'is-invalid': newProposal.operationParams.to && !validateAddress(newProposal.operationParams.to) }"
>
<small class="form-text text-muted">Введите корректный Ethereum адрес (42 символа, начинается с 0x)</small>
</div>
<div class="form-group">
<label for="transferAmount">Количество токенов:</label>
<input
type="number"
id="transferAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="100"
:class="{ 'is-invalid': newProposal.operationParams.amount <= 0 }"
>
<small class="form-text text-muted">Введите количество токенов для передачи</small>
</div>
</div>
<!-- Параметры для минтинга -->
<div v-if="newProposal.operationType === 'mint'" class="operation-params">
<div class="form-group">
<label for="mintTo">Адрес получателя:</label>
<input
type="text"
id="mintTo"
v-model="newProposal.operationParams.to"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="mintAmount">Количество токенов:</label>
<input
type="number"
id="mintAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="1000"
>
</div>
</div>
<!-- Параметры для сжигания -->
<div v-if="newProposal.operationType === 'burn'" class="operation-params">
<div class="form-group">
<label for="burnFrom">Адрес владельца:</label>
<input
type="text"
id="burnFrom"
v-model="newProposal.operationParams.from"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="burnAmount">Количество токенов:</label>
<input
type="number"
id="burnAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="100"
>
</div>
</div>
<!-- Пользовательская операция -->
<div v-if="newProposal.operationType === 'custom'" class="operation-params">
<div class="form-group">
<label for="customOperation">Пользовательская операция (hex):</label>
<textarea
id="customOperation"
v-model="newProposal.operationParams.customData"
class="form-control"
rows="3"
placeholder="0x..."
></textarea>
</div>
</div>
<!-- Параметры для обновления данных DLE -->
<div v-if="newProposal.operationType === 'updateDLEInfo'" class="operation-params">
<div class="form-group">
<label for="dleName">Новое название DLE:</label>
<input
type="text"
id="dleName"
v-model="newProposal.operationParams.name"
class="form-control"
placeholder="Новое название"
>
</div>
<div class="form-group">
<label for="dleSymbol">Новый символ токена:</label>
<input
type="text"
id="dleSymbol"
v-model="newProposal.operationParams.symbol"
class="form-control"
placeholder="Новый символ"
>
</div>
<div class="form-group">
<label for="dleLocation">Новое местонахождение:</label>
<input
type="text"
id="dleLocation"
v-model="newProposal.operationParams.location"
class="form-control"
placeholder="Новое местонахождение"
>
</div>
<div class="form-group">
<label for="dleCoordinates">Новые координаты:</label>
<input
type="text"
id="dleCoordinates"
v-model="newProposal.operationParams.coordinates"
class="form-control"
placeholder="44.0422736,43.062124"
>
</div>
<div class="form-group">
<label for="dleJurisdiction">Новая юрисдикция:</label>
<input
type="number"
id="dleJurisdiction"
v-model.number="newProposal.operationParams.jurisdiction"
class="form-control"
placeholder="643"
>
</div>
<div class="form-group">
<label for="dleOktmo">Новый ОКТМО:</label>
<input
type="number"
id="dleOktmo"
v-model.number="newProposal.operationParams.oktmo"
class="form-control"
placeholder="45000000000"
>
</div>
<div class="form-group">
<label for="dleKpp">Новый КПП:</label>
<input
type="number"
id="dleKpp"
v-model.number="newProposal.operationParams.kpp"
class="form-control"
placeholder="770101001"
>
</div>
</div>
<!-- Параметры для изменения кворума -->
<div v-if="newProposal.operationType === 'updateQuorum'" class="operation-params">
<div class="form-group">
<label for="newQuorum">Новый процент кворума:</label>
<input
type="number"
id="newQuorum"
v-model.number="newProposal.operationParams.quorumPercentage"
class="form-control"
min="1"
max="100"
placeholder="51"
>
<small class="form-text text-muted">Процент от общего количества токенов (1-100%)</small>
</div>
</div>
<!-- Параметры для изменения текущей цепочки -->
<div v-if="newProposal.operationType === 'updateChain'" class="operation-params">
<div class="form-group">
<label for="newChainId">Новая текущая цепочка:</label>
<select id="newChainId" v-model="newProposal.operationParams.chainId" class="form-control">
<option value="">-- Выберите цепочку --</option>
<option v-for="chain in availableChains" :key="chain.chainId" :value="chain.chainId">
{{ chain.name }} ({{ chain.chainId }})
</option>
</select>
<small class="form-text text-muted">Выберите новую цепочку для управления DLE</small>
</div>
</div>
</div>
</div>
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="createProposal"
:disabled="!isFormValid || isCreating"
>
<i class="fas fa-paper-plane"></i>
{{ isCreating ? 'Создание...' : 'Создать предложение' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
</div>
<!-- Предварительный просмотр (в конце формы) -->
<div class="form-section">
<h5>👁 Предварительный просмотр</h5>
<div class="preview-card">
<div class="preview-item">
<strong>Описание:</strong> {{ newProposal.description || 'Не указано' }}
</div>
<div class="preview-item">
<strong>Длительность:</strong> {{ newProposal.duration || 7 }} дней
</div>
<div class="preview-item">
<strong>Цепочка для кворума:</strong>
{{ getChainName(newProposal.governanceChainId) || 'Не выбрана' }}
</div>
<div class="preview-item">
<strong>Тип операции:</strong> {{ getOperationTypeName(newProposal.operationType) || 'Не выбран' }}
</div>
<div v-if="newProposal.operationType" class="preview-item">
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
</div>
</div>
</div>
</div>
</div> <!-- Закрываем div для авторизованных пользователей -->
</div>
</BaseLayout> </BaseLayout>
</template> </template>
@@ -564,14 +203,8 @@ import { ref, computed, onMounted, onUnmounted, watch, defineProps, defineEmits,
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth'; import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue'; import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js'; import { getProposals, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import api from '../../api/axios'; import api from '../../api/axios';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
// Можно расширить логику при появлении offchain типа
return true;
});
import wsClient from '../../utils/websocket.js'; import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
@@ -916,55 +549,14 @@ const goBackToBlocks = () => {
const selectedDle = ref(null); const selectedDle = ref(null);
const isLoadingDle = ref(false); const isLoadingDle = ref(false);
// Состояние формы // Состояние фильтров
// const showCreateForm = ref(false); // Больше не нужно - форма всегда видна
const isCreating = ref(false);
const statusFilter = ref(''); 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 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(() => { const filteredProposals = computed(() => {
console.log('[Frontend] Фильтрация предложений. Всего:', proposals.value.length); console.log('[Frontend] Фильтрация предложений. Всего:', proposals.value.length);
@@ -1039,11 +631,6 @@ async function loadDleData() {
console.log('[Frontend] Итоговый список предложений:', proposals.value); console.log('[Frontend] Итоговый список предложений:', proposals.value);
// Загружаем поддерживаемые цепочки
const chainsResponse = await getSupportedChains(dleAddress.value);
availableChains.value = chainsResponse.data?.chains || [];
} catch (error) { } catch (error) {
console.error('Ошибка загрузки данных DLE из блокчейна:', error); console.error('Ошибка загрузки данных DLE из блокчейна:', error);
} finally { } finally {
@@ -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) { function getChainName(chainId) {
// Сначала ищем в availableChains // Используем известные chain ID
if (Array.isArray(availableChains.value)) {
const chain = availableChains.value.find(c => c.chainId === chainId);
if (chain) return chain.name;
}
// Если не найдено, используем известные chain ID
const knownChains = { const knownChains = {
1: 'Ethereum Mainnet', 1: 'Ethereum Mainnet',
11155111: 'Sepolia Testnet', 11155111: 'Sepolia Testnet',
@@ -1107,42 +652,6 @@ function getChainName(chainId) {
return knownChains[chainId] || `Chain ID: ${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) { function shortenAddress(address) {
if (!address) return ''; if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`; 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) { 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() { function hasAdminRights() {
@@ -2315,115 +1655,6 @@ onUnmounted(() => {
font-style: italic; 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 { .proposals-list {
margin-top: 2rem; margin-top: 2rem;
@@ -2669,42 +1900,4 @@ onUnmounted(() => {
font-weight: 500; 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;
}
</style> </style>

View File

@@ -64,6 +64,17 @@ export default defineConfig({
secure: false, secure: false,
credentials: true, credentials: true,
rewrite: (path) => path, 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: { watch: {