ваше сообщение коммита
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 день)
|
||||||
|
|||||||
105
backend/docs/AUTO_VERIFICATION.md
Normal file
105
backend/docs/AUTO_VERIFICATION.md
Normal 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. **Интеграция**: Единый процесс деплоя и верификации
|
||||||
@@ -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}`);
|
|
||||||
```
|
|
||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
207
backend/scripts/debug-file-monitor.js
Normal file
207
backend/scripts/debug-file-monitor.js
Normal 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 для остановки мониторинга');
|
||||||
617
backend/scripts/deploy/deploy-modules.js
Normal file
617
backend/scripts/deploy/deploy-modules.js
Normal 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); });
|
||||||
@@ -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); });
|
||||||
|
|||||||
130
backend/scripts/run-all-tests.js
Normal file
130
backend/scripts/run-all-tests.js
Normal 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);
|
||||||
|
});
|
||||||
240
backend/scripts/verify-with-hardhat-v2.js
Normal file
240
backend/scripts/verify-with-hardhat-v2.js
Normal 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 };
|
||||||
228
backend/services/deployParamsService.js
Normal file
228
backend/services/deployParamsService.js
Normal 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
@@ -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 };
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 || 'неизвестная ошибка'));
|
||||||
|
|||||||
@@ -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');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
591
frontend/src/views/smartcontracts/CreateProposalView.vue
Normal file
591
frontend/src/views/smartcontracts/CreateProposalView.vue
Normal 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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user