ваше сообщение коммита
This commit is contained in:
@@ -92,11 +92,11 @@ const blockchainRoutes = require('./routes/blockchain'); // Добавляем
|
|||||||
const dleCoreRoutes = require('./routes/dleCore'); // Основные функции DLE
|
const dleCoreRoutes = require('./routes/dleCore'); // Основные функции DLE
|
||||||
const dleProposalsRoutes = require('./routes/dleProposals'); // Функции предложений
|
const dleProposalsRoutes = require('./routes/dleProposals'); // Функции предложений
|
||||||
const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей
|
const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей
|
||||||
|
const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции
|
||||||
const moduleDeploymentRoutes = require('./routes/moduleDeployment'); // Деплой модулей
|
const moduleDeploymentRoutes = require('./routes/moduleDeployment'); // Деплой модулей
|
||||||
const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов
|
const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов
|
||||||
const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история
|
const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история
|
||||||
const compileRoutes = require('./routes/compile'); // Компиляция контрактов
|
const compileRoutes = require('./routes/compile'); // Компиляция контрактов
|
||||||
const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции
|
|
||||||
const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история
|
const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история
|
||||||
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
|
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
|
||||||
|
|
||||||
@@ -195,12 +195,13 @@ app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI
|
|||||||
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
|
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
|
||||||
app.use('/api/blockchain', blockchainRoutes); // Добавляем маршрут blockchain
|
app.use('/api/blockchain', blockchainRoutes); // Добавляем маршрут blockchain
|
||||||
app.use('/api/dle-core', dleCoreRoutes); // Основные функции DLE
|
app.use('/api/dle-core', dleCoreRoutes); // Основные функции DLE
|
||||||
|
app.use('/api/dle-core', dleMultichainRoutes); // Мультичейн функции (используем тот же префикс)
|
||||||
app.use('/api/dle-proposals', dleProposalsRoutes); // Функции предложений
|
app.use('/api/dle-proposals', dleProposalsRoutes); // Функции предложений
|
||||||
app.use('/api/dle-modules', dleModulesRoutes); // Функции модулей
|
app.use('/api/dle-modules', dleModulesRoutes); // Функции модулей
|
||||||
app.use('/api/module-deployment', moduleDeploymentRoutes); // Деплой модулей
|
app.use('/api/module-deployment', moduleDeploymentRoutes); // Деплой модулей
|
||||||
app.use('/api/dle-tokens', dleTokensRoutes); // Функции токенов
|
app.use('/api/dle-tokens', dleTokensRoutes); // Функции токенов
|
||||||
app.use('/api/dle-analytics', dleAnalyticsRoutes); // Аналитика и история
|
app.use('/api/dle-analytics', dleAnalyticsRoutes); // Аналитика и история
|
||||||
app.use('/api/dle-multichain', dleMultichainRoutes); // Мультичейн функции
|
app.use('/api/dle-multichain-execution', require('./routes/dleMultichainExecution')); // Мультиконтрактное исполнение
|
||||||
app.use('/api/dle-history', dleHistoryRoutes); // Расширенная история
|
app.use('/api/dle-history', dleHistoryRoutes); // Расширенная история
|
||||||
app.use('/api/messages', messagesRoutes);
|
app.use('/api/messages', messagesRoutes);
|
||||||
app.use('/api/identities', identitiesRoutes);
|
app.use('/api/identities', identitiesRoutes);
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||||||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
|
||||||
// This software is proprietary and confidential.
|
|
||||||
// Unauthorized copying, modification, or distribution is prohibited.
|
|
||||||
//
|
|
||||||
// For licensing inquiries: info@hb3-accelerator.com
|
// For licensing inquiries: info@hb3-accelerator.com
|
||||||
// Website: https://hb3-accelerator.com
|
|
||||||
// GitHub: https://github.com/HB3-ACCELERATOR
|
|
||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
@@ -21,36 +15,12 @@ interface IERC1271 {
|
|||||||
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
|
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Интерфейс для мультичейн метаданных (EIP-3668 inspired)
|
|
||||||
*/
|
|
||||||
interface IMultichainMetadata {
|
interface IMultichainMetadata {
|
||||||
/**
|
|
||||||
* @dev Возвращает информацию о мультичейн развертывании
|
|
||||||
* @return supportedChainIds Массив всех поддерживаемых chain ID
|
|
||||||
* @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых)
|
|
||||||
*/
|
|
||||||
function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain);
|
function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain);
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Возвращает адреса контракта в других сетях
|
|
||||||
* @return chainIds Массив chain ID где развернут контракт
|
|
||||||
* @return addresses Массив адресов контракта в соответствующих сетях
|
|
||||||
*/
|
|
||||||
function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses);
|
function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// DLE (Digital Legal Entity) - основной контракт с модульной архитектурой
|
||||||
* @title DLE (Digital Legal Entity)
|
|
||||||
* @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance
|
|
||||||
* и безопасной мульти-чейн синхронизацией без сторонних мостов (через подписи холдеров).
|
|
||||||
*
|
|
||||||
* КЛЮЧЕВЫЕ ОСОБЕННОСТИ:
|
|
||||||
* - Прямые переводы токенов ЗАБЛОКИРОВАНЫ (transfer, transferFrom, approve)
|
|
||||||
* - Перевод токенов возможен ТОЛЬКО через governance предложения
|
|
||||||
* - Токены служат только для голосования и управления DLE
|
|
||||||
* - Все операции с токенами требуют коллективного решения
|
|
||||||
*/
|
|
||||||
contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMetadata {
|
contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMetadata {
|
||||||
using ECDSA for bytes32;
|
using ECDSA for bytes32;
|
||||||
struct DLEInfo {
|
struct DLEInfo {
|
||||||
@@ -101,7 +71,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
DLEInfo public dleInfo;
|
DLEInfo public dleInfo;
|
||||||
uint256 public quorumPercentage;
|
uint256 public quorumPercentage;
|
||||||
uint256 public proposalCounter;
|
uint256 public proposalCounter;
|
||||||
uint256 public currentChainId;
|
// Удален currentChainId - теперь используется block.chainid для проверок
|
||||||
// Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор)
|
// Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор)
|
||||||
string public logoURI;
|
string public logoURI;
|
||||||
|
|
||||||
@@ -169,6 +139,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
error ErrProposalExecuted();
|
error ErrProposalExecuted();
|
||||||
error ErrAlreadyVoted();
|
error ErrAlreadyVoted();
|
||||||
error ErrWrongChain();
|
error ErrWrongChain();
|
||||||
|
error ErrUnsupportedChain();
|
||||||
error ErrNoPower();
|
error ErrNoPower();
|
||||||
error ErrNotReady();
|
error ErrNotReady();
|
||||||
error ErrNotInitiator();
|
error ErrNotInitiator();
|
||||||
@@ -200,7 +171,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
DLEConfig memory config,
|
DLEConfig memory config,
|
||||||
uint256 _currentChainId,
|
|
||||||
address _initializer
|
address _initializer
|
||||||
) ERC20(config.name, config.symbol) ERC20Permit(config.name) {
|
) ERC20(config.name, config.symbol) ERC20Permit(config.name) {
|
||||||
if (_initializer == address(0)) revert ErrZeroAddress();
|
if (_initializer == address(0)) revert ErrZeroAddress();
|
||||||
@@ -218,7 +188,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
});
|
});
|
||||||
|
|
||||||
quorumPercentage = config.quorumPercentage;
|
quorumPercentage = config.quorumPercentage;
|
||||||
currentChainId = _currentChainId;
|
|
||||||
|
|
||||||
// Настраиваем поддерживаемые цепочки
|
// Настраиваем поддерживаемые цепочки
|
||||||
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
||||||
@@ -254,9 +223,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Одноразовая инициализация URI логотипа
|
||||||
* @dev Одноразовая инициализация URI логотипа. Доступно только инициализатору и только один раз.
|
|
||||||
*/
|
|
||||||
function initializeLogoURI(string calldata _logoURI) external {
|
function initializeLogoURI(string calldata _logoURI) external {
|
||||||
if (msg.sender != initializer) revert ErrOnlyInitializer();
|
if (msg.sender != initializer) revert ErrOnlyInitializer();
|
||||||
if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet();
|
if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet();
|
||||||
@@ -265,13 +232,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
emit LogoURIUpdated(old, _logoURI);
|
emit LogoURIUpdated(old, _logoURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Создать предложение с выбором цепочки для кворума
|
||||||
* @dev Создать предложение с выбором цепочки для кворума
|
|
||||||
* @param _description Описание предложения
|
|
||||||
* @param _duration Длительность голосования в секундах
|
|
||||||
* @param _operation Операция для исполнения
|
|
||||||
* @param _governanceChainId ID цепочки для сбора голосов
|
|
||||||
*/
|
|
||||||
function createProposal(
|
function createProposal(
|
||||||
string memory _description,
|
string memory _description,
|
||||||
uint256 _duration,
|
uint256 _duration,
|
||||||
@@ -333,11 +294,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
return proposalId;
|
return proposalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Голосовать за предложение
|
||||||
* @dev Голосовать за предложение
|
|
||||||
* @param _proposalId ID предложения
|
|
||||||
* @param _support Поддержка предложения
|
|
||||||
*/
|
|
||||||
function vote(uint256 _proposalId, bool _support) external nonReentrant {
|
function vote(uint256 _proposalId, bool _support) external nonReentrant {
|
||||||
Proposal storage proposal = proposals[_proposalId];
|
Proposal storage proposal = proposals[_proposalId];
|
||||||
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
||||||
@@ -345,7 +302,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
if (proposal.executed) revert ErrProposalExecuted();
|
if (proposal.executed) revert ErrProposalExecuted();
|
||||||
if (proposal.canceled) revert ErrProposalCanceled();
|
if (proposal.canceled) revert ErrProposalCanceled();
|
||||||
if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted();
|
if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted();
|
||||||
if (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
|
// Проверяем, что текущая сеть поддерживается
|
||||||
|
if (!supportedChains[block.chainid]) revert ErrUnsupportedChain();
|
||||||
|
|
||||||
uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint);
|
uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint);
|
||||||
if (votingPower == 0) revert ErrNoPower();
|
if (votingPower == 0) revert ErrNoPower();
|
||||||
@@ -360,7 +318,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
emit ProposalVoted(_proposalId, msg.sender, _support, votingPower);
|
emit ProposalVoted(_proposalId, msg.sender, _support, votingPower);
|
||||||
}
|
}
|
||||||
|
|
||||||
// УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
|
|
||||||
function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
|
function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
|
||||||
Proposal storage proposal = proposals[_proposalId];
|
Proposal storage proposal = proposals[_proposalId];
|
||||||
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
||||||
@@ -382,7 +339,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
||||||
if (proposal.executed) revert ErrProposalExecuted();
|
if (proposal.executed) revert ErrProposalExecuted();
|
||||||
if (proposal.canceled) revert ErrProposalCanceled();
|
if (proposal.canceled) revert ErrProposalCanceled();
|
||||||
if (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
|
// Проверяем, что текущая сеть поддерживается
|
||||||
|
if (!supportedChains[block.chainid]) revert ErrUnsupportedChain();
|
||||||
|
|
||||||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
||||||
|
|
||||||
@@ -424,8 +382,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
||||||
if (proposal.executed) revert ErrProposalExecuted();
|
if (proposal.executed) revert ErrProposalExecuted();
|
||||||
if (proposal.canceled) revert ErrProposalCanceled();
|
if (proposal.canceled) revert ErrProposalCanceled();
|
||||||
if (currentChainId == proposal.governanceChainId) revert ErrWrongChain();
|
// Проверяем, что текущая сеть поддерживается
|
||||||
if (!_isTargetChain(proposal, currentChainId)) revert ErrBadTarget();
|
if (!supportedChains[block.chainid]) revert ErrUnsupportedChain();
|
||||||
|
// Проверяем, что текущая сеть является целевой для предложения
|
||||||
|
if (!_isTargetChain(proposal, block.chainid)) revert ErrBadTarget();
|
||||||
|
|
||||||
if (signers.length != signatures.length) revert ErrSigLengthMismatch();
|
if (signers.length != signatures.length) revert ErrSigLengthMismatch();
|
||||||
if (signers.length == 0) revert ErrNoSigners();
|
if (signers.length == 0) revert ErrNoSigners();
|
||||||
@@ -436,7 +396,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
EXECUTION_APPROVAL_TYPEHASH,
|
EXECUTION_APPROVAL_TYPEHASH,
|
||||||
_proposalId,
|
_proposalId,
|
||||||
opHash,
|
opHash,
|
||||||
currentChainId,
|
block.chainid,
|
||||||
proposal.snapshotTimepoint
|
proposal.snapshotTimepoint
|
||||||
));
|
));
|
||||||
bytes32 digest = _hashTypedDataV4(structHash);
|
bytes32 digest = _hashTypedDataV4(structHash);
|
||||||
@@ -474,14 +434,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
proposal.executed = true;
|
proposal.executed = true;
|
||||||
_executeOperation(proposal.operation);
|
_executeOperation(proposal.operation);
|
||||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||||
emit ProposalExecutionApprovedInChain(_proposalId, currentChainId);
|
emit ProposalExecutionApprovedInChain(_proposalId, block.chainid);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync функции удалены для экономии байт-кода
|
|
||||||
|
|
||||||
// УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Получить количество поддерживаемых цепочек
|
* @dev Получить количество поддерживаемых цепочек
|
||||||
*/
|
*/
|
||||||
@@ -505,7 +461,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
// Управление списком сетей теперь выполняется только через предложения
|
// Управление списком сетей теперь выполняется только через предложения
|
||||||
function _addSupportedChain(uint256 _chainId) internal {
|
function _addSupportedChain(uint256 _chainId) internal {
|
||||||
require(!supportedChains[_chainId], "Chain already supported");
|
require(!supportedChains[_chainId], "Chain already supported");
|
||||||
require(_chainId != currentChainId, "Cannot add current chain");
|
require(_chainId != block.chainid, "Cannot add current chain");
|
||||||
supportedChains[_chainId] = true;
|
supportedChains[_chainId] = true;
|
||||||
supportedChainIds.push(_chainId);
|
supportedChainIds.push(_chainId);
|
||||||
emit ChainAdded(_chainId);
|
emit ChainAdded(_chainId);
|
||||||
@@ -517,7 +473,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
*/
|
*/
|
||||||
function _removeSupportedChain(uint256 _chainId) internal {
|
function _removeSupportedChain(uint256 _chainId) internal {
|
||||||
require(supportedChains[_chainId], "Chain not supported");
|
require(supportedChains[_chainId], "Chain not supported");
|
||||||
require(_chainId != currentChainId, "Cannot remove current chain");
|
require(_chainId != block.chainid, "Cannot remove current chain");
|
||||||
supportedChains[_chainId] = false;
|
supportedChains[_chainId] = false;
|
||||||
// Удаляем из массива
|
// Удаляем из массива
|
||||||
for (uint256 i = 0; i < supportedChainIds.length; i++) {
|
for (uint256 i = 0; i < supportedChainIds.length; i++) {
|
||||||
@@ -530,18 +486,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
emit ChainRemoved(_chainId);
|
emit ChainRemoved(_chainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Установить Merkle root для цепочки (только для владельцев токенов)
|
|
||||||
* @param _chainId ID цепочки
|
|
||||||
* @param _merkleRoot Merkle root для цепочки
|
|
||||||
*/
|
|
||||||
// УДАЛЕНО: setChainMerkleRoot — небезопасно отдавать любому холдеру
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Получить Merkle root для цепочки
|
|
||||||
* @param _chainId ID цепочки
|
|
||||||
*/
|
|
||||||
// УДАЛЕНО: getChainMerkleRoot — устарело
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Исполнить операцию
|
* @dev Исполнить операцию
|
||||||
@@ -856,10 +800,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Получить текущий ID цепочки
|
* @dev Получить текущий ID цепочки (теперь используется block.chainid)
|
||||||
*/
|
*/
|
||||||
function getCurrentChainId() external view returns (uint256) {
|
function getCurrentChainId() external view returns (uint256) {
|
||||||
return currentChainId;
|
return block.chainid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -884,7 +828,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
* @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых)
|
* @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых)
|
||||||
*/
|
*/
|
||||||
function getMultichainInfo() external view returns (uint256[] memory chains, uint256 defaultVotingChain) {
|
function getMultichainInfo() external view returns (uint256[] memory chains, uint256 defaultVotingChain) {
|
||||||
return (supportedChainIds, currentChainId);
|
return (supportedChainIds, block.chainid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -898,7 +842,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
|
|
||||||
for (uint256 i = 0; i < supportedChainIds.length; i++) {
|
for (uint256 i = 0; i < supportedChainIds.length; i++) {
|
||||||
chains[i] = supportedChainIds[i];
|
chains[i] = supportedChainIds[i];
|
||||||
addrs[i] = address(this); // CREATE2 обеспечивает одинаковые адреса
|
addrs[i] = address(this); // Детерминированный деплой обеспечивает одинаковые адреса
|
||||||
}
|
}
|
||||||
|
|
||||||
return (chains, addrs);
|
return (chains, addrs);
|
||||||
@@ -929,7 +873,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
json,
|
json,
|
||||||
'],',
|
'],',
|
||||||
'"defaultVotingChain": ',
|
'"defaultVotingChain": ',
|
||||||
_toString(currentChainId),
|
_toString(block.chainid),
|
||||||
',',
|
',',
|
||||||
'"note": "All chains are equal, voting can happen on any supported chain",',
|
'"note": "All chains are equal, voting can happen on any supported chain",',
|
||||||
'"contractAddress": "',
|
'"contractAddress": "',
|
||||||
@@ -985,26 +929,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
return string(str);
|
return string(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Получить информацию об архитектуре мультичейн governance
|
|
||||||
* @return architecture Описание архитектуры в JSON формате
|
|
||||||
*/
|
|
||||||
function getGovernanceArchitecture() external pure returns (string memory architecture) {
|
|
||||||
return string(abi.encodePacked(
|
|
||||||
'{"architecture": {',
|
|
||||||
'"type": "Single-Chain Governance",',
|
|
||||||
'"description": "Voting happens on one chain per proposal, execution on any supported chain",',
|
|
||||||
'"features": [',
|
|
||||||
'"Equal chain support - no primary chain",',
|
|
||||||
'"Cross-chain execution via signatures",',
|
|
||||||
'"Deterministic addresses via CREATE2",',
|
|
||||||
'"No bridge dependencies"',
|
|
||||||
'],',
|
|
||||||
'"voting": "One chain per proposal (chosen by proposer)",',
|
|
||||||
'"execution": "Any supported chain via signature verification"',
|
|
||||||
'}}'
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// API функции вынесены в отдельный reader контракт для экономии байт-кода
|
// API функции вынесены в отдельный reader контракт для экономии байт-кода
|
||||||
|
|
||||||
@@ -1025,6 +949,38 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
|||||||
|
|
||||||
// Функции для подсчёта голосов вынесены в reader контракт
|
// Функции для подсчёта голосов вынесены в reader контракт
|
||||||
|
|
||||||
|
// Получить полную сводку по предложению
|
||||||
|
function getProposalSummary(uint256 _proposalId) external view returns (
|
||||||
|
uint256 id,
|
||||||
|
string memory description,
|
||||||
|
uint256 forVotes,
|
||||||
|
uint256 againstVotes,
|
||||||
|
bool executed,
|
||||||
|
bool canceled,
|
||||||
|
uint256 deadline,
|
||||||
|
address initiator,
|
||||||
|
uint256 governanceChainId,
|
||||||
|
uint256 snapshotTimepoint,
|
||||||
|
uint256[] memory targetChains
|
||||||
|
) {
|
||||||
|
Proposal storage p = proposals[_proposalId];
|
||||||
|
require(p.id == _proposalId, "Proposal does not exist");
|
||||||
|
|
||||||
|
return (
|
||||||
|
p.id,
|
||||||
|
p.description,
|
||||||
|
p.forVotes,
|
||||||
|
p.againstVotes,
|
||||||
|
p.executed,
|
||||||
|
p.canceled,
|
||||||
|
p.deadline,
|
||||||
|
p.initiator,
|
||||||
|
p.governanceChainId,
|
||||||
|
p.snapshotTimepoint,
|
||||||
|
p.targetChains
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Деактивация вынесена в отдельный модуль. См. DeactivationModule.
|
// Деактивация вынесена в отдельный модуль. См. DeactivationModule.
|
||||||
function isActive() external view returns (bool) {
|
function isActive() external view returns (bool) {
|
||||||
return dleInfo.isActive;
|
return dleInfo.isActive;
|
||||||
|
|||||||
6357
backend/contracts/DLE_flattened.sol
Normal file
6357
backend/contracts/DLE_flattened.sol
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,12 @@ function getNetworks() {
|
|||||||
// Базовая конфигурация сетей для верификации
|
// Базовая конфигурация сетей для верификации
|
||||||
return {
|
return {
|
||||||
sepolia: {
|
sepolia: {
|
||||||
url: process.env.SEPOLIA_RPC_URL || 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52',
|
url: process.env.SEPOLIA_RPC_URL || 'https://1rpc.io/sepolia',
|
||||||
chainId: 11155111,
|
chainId: 11155111,
|
||||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
||||||
},
|
},
|
||||||
holesky: {
|
holesky: {
|
||||||
url: process.env.HOLESKY_RPC_URL || 'https://ethereum-holesky-rpc.publicnode.com',
|
url: process.env.HOLESKY_RPC_URL || 'https://ethereum-holesky.publicnode.com',
|
||||||
chainId: 17000,
|
chainId: 17000,
|
||||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
||||||
},
|
},
|
||||||
@@ -52,9 +52,10 @@ module.exports = {
|
|||||||
settings: {
|
settings: {
|
||||||
optimizer: {
|
optimizer: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
runs: 1 // Максимальная оптимизация размера для mainnet
|
runs: 0 // Максимальная оптимизация размера
|
||||||
},
|
},
|
||||||
viaIR: true
|
viaIR: true,
|
||||||
|
evmVersion: "paris"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contractSizer: {
|
contractSizer: {
|
||||||
@@ -142,6 +143,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
sourcify: {
|
||||||
|
enabled: true // Включаем Sourcify для децентрализованной верификации
|
||||||
|
},
|
||||||
solidityCoverage: {
|
solidityCoverage: {
|
||||||
excludeContracts: [],
|
excludeContracts: [],
|
||||||
skipFiles: [],
|
skipFiles: [],
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
"backend/artifacts/**",
|
"backend/artifacts/**",
|
||||||
"backend/cache/**",
|
"backend/cache/**",
|
||||||
"backend/contracts-data/**",
|
"backend/contracts-data/**",
|
||||||
"backend/temp/**",
|
"backend/temp/**"
|
||||||
"backend/scripts/deploy/current-params*.json"
|
|
||||||
],
|
],
|
||||||
"ext": "js,json"
|
"ext": "js,json"
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
"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:modules": "node scripts/deploy/deploy-modules.js",
|
"deploy:modules": "node scripts/deploy/deploy-modules.js",
|
||||||
"test:modules": "node scripts/test-modules-deploy.js",
|
"generate:abi": "node scripts/generate-abi.js",
|
||||||
"verify:contracts": "node scripts/verify-contracts.js"
|
"generate:flattened": "node scripts/generate-flattened.js",
|
||||||
|
"compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.51.0",
|
"@anthropic-ai/sdk": "^0.51.0",
|
||||||
@@ -80,7 +80,9 @@
|
|||||||
"@typechain/ethers-v6": "^0.5.0",
|
"@typechain/ethers-v6": "^0.5.0",
|
||||||
"@typechain/hardhat": "^9.0.0",
|
"@typechain/hardhat": "^9.0.0",
|
||||||
"@types/chai": "^4.2.0",
|
"@types/chai": "^4.2.0",
|
||||||
|
"@types/minimatch": "^6.0.0",
|
||||||
"@types/mocha": ">=9.1.0",
|
"@types/mocha": ">=9.1.0",
|
||||||
|
"@types/node": "^24.5.2",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-config-prettier": "^10.0.2",
|
"eslint-config-prettier": "^10.0.2",
|
||||||
@@ -88,6 +90,7 @@
|
|||||||
"hardhat": "^2.24.1",
|
"hardhat": "^2.24.1",
|
||||||
"hardhat-contract-sizer": "^2.10.1",
|
"hardhat-contract-sizer": "^2.10.1",
|
||||||
"hardhat-gas-reporter": "^2.2.2",
|
"hardhat-gas-reporter": "^2.2.2",
|
||||||
|
"minimatch": "^10.0.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"solidity-coverage": "^0.8.16",
|
"solidity-coverage": "^0.8.16",
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ router.get('/nonce', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Address is required' });
|
return res.status(400).json({ error: 'Address is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очищаем истекшие nonce перед генерацией нового
|
||||||
|
try {
|
||||||
|
await db.getQuery()(
|
||||||
|
'DELETE FROM nonces WHERE expires_at < NOW()'
|
||||||
|
);
|
||||||
|
logger.info(`[nonce] Cleaned up expired nonces`);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
logger.warn(`[nonce] Error cleaning up expired nonces: ${cleanupError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Генерируем случайный nonce
|
// Генерируем случайный nonce
|
||||||
const nonce = crypto.randomBytes(16).toString('hex');
|
const nonce = crypto.randomBytes(16).toString('hex');
|
||||||
logger.info(`[nonce] Generated nonce: ${nonce}`);
|
logger.info(`[nonce] Generated nonce: ${nonce}`);
|
||||||
@@ -136,9 +146,9 @@ router.post('/verify', async (req, res) => {
|
|||||||
console.error('Error reading encryption key:', keyError);
|
console.error('Error reading encryption key:', keyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем nonce в базе данных
|
// Проверяем nonce в базе данных с проверкой времени истечения
|
||||||
const nonceResult = await db.getQuery()(
|
const nonceResult = await db.getQuery()(
|
||||||
'SELECT nonce_encrypted FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
'SELECT nonce_encrypted, expires_at FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
||||||
[normalizedAddressLower, encryptionKey]
|
[normalizedAddressLower, encryptionKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -147,6 +157,14 @@ router.post('/verify', async (req, res) => {
|
|||||||
return res.status(401).json({ success: false, error: 'Nonce not found' });
|
return res.status(401).json({ success: false, error: 'Nonce not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, не истек ли срок действия nonce
|
||||||
|
const expiresAt = new Date(nonceResult.rows[0].expires_at);
|
||||||
|
const now = new Date();
|
||||||
|
if (now > expiresAt) {
|
||||||
|
logger.error(`[verify] Nonce expired for address: ${normalizedAddressLower}. Expired at: ${expiresAt}, Now: ${now}`);
|
||||||
|
return res.status(401).json({ success: false, error: 'Nonce expired' });
|
||||||
|
}
|
||||||
|
|
||||||
// Расшифровываем nonce из базы данных
|
// Расшифровываем nonce из базы данных
|
||||||
const storedNonce = await db.getQuery()(
|
const storedNonce = await db.getQuery()(
|
||||||
'SELECT decrypt_text(nonce_encrypted, $1) as nonce FROM nonces WHERE identity_value_encrypted = encrypt_text($2, $1)',
|
'SELECT decrypt_text(nonce_encrypted, $1) as nonce FROM nonces WHERE identity_value_encrypted = encrypt_text($2, $1)',
|
||||||
@@ -156,9 +174,12 @@ router.post('/verify', async (req, res) => {
|
|||||||
logger.info(`[verify] Stored nonce from DB: ${storedNonce.rows[0]?.nonce}`);
|
logger.info(`[verify] Stored nonce from DB: ${storedNonce.rows[0]?.nonce}`);
|
||||||
logger.info(`[verify] Nonce from request: ${nonce}`);
|
logger.info(`[verify] Nonce from request: ${nonce}`);
|
||||||
logger.info(`[verify] Nonce match: ${storedNonce.rows[0]?.nonce === nonce}`);
|
logger.info(`[verify] Nonce match: ${storedNonce.rows[0]?.nonce === nonce}`);
|
||||||
|
logger.info(`[verify] Stored nonce length: ${storedNonce.rows[0]?.nonce?.length}`);
|
||||||
|
logger.info(`[verify] Request nonce length: ${nonce?.length}`);
|
||||||
|
|
||||||
if (storedNonce.rows.length === 0 || storedNonce.rows[0].nonce !== nonce) {
|
if (storedNonce.rows.length === 0 || storedNonce.rows[0].nonce !== nonce) {
|
||||||
logger.error(`[verify] Invalid nonce for address: ${normalizedAddressLower}. Expected: ${storedNonce.rows[0]?.nonce}, Got: ${nonce}`);
|
logger.error(`[verify] Invalid nonce for address: ${normalizedAddressLower}. Expected: ${storedNonce.rows[0]?.nonce}, Got: ${nonce}`);
|
||||||
|
logger.error(`[verify] Stored nonce type: ${typeof storedNonce.rows[0]?.nonce}, Request nonce type: ${typeof nonce}`);
|
||||||
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,29 @@ const router = express.Router();
|
|||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
const rpcProviderService = require('../services/rpcProviderService');
|
const rpcProviderService = require('../services/rpcProviderService');
|
||||||
|
|
||||||
|
// Функция для получения списка сетей из БД для данного DLE
|
||||||
|
async function getSupportedChainIds(dleAddress) {
|
||||||
|
try {
|
||||||
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
const deployments = await deployParamsService.getAllDeployments();
|
||||||
|
|
||||||
|
// Находим деплой с данным адресом
|
||||||
|
for (const deployment of deployments) {
|
||||||
|
if (deployment.dleAddress === dleAddress && deployment.supportedChainIds) {
|
||||||
|
console.log(`[Blockchain] Найдены сети для DLE ${dleAddress}:`, deployment.supportedChainIds);
|
||||||
|
return deployment.supportedChainIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback к стандартным сетям
|
||||||
|
return [17000, 11155111, 421614, 84532];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Blockchain] Ошибка получения сетей для DLE ${dleAddress}:`, error);
|
||||||
|
return [17000, 11155111, 421614, 84532];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Чтение данных DLE из блокчейна
|
// Чтение данных DLE из блокчейна
|
||||||
router.post('/read-dle-info', async (req, res) => {
|
router.post('/read-dle-info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -31,7 +54,9 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
|
|
||||||
// Определяем корректную сеть для данного адреса (или используем chainId из запроса)
|
// Определяем корректную сеть для данного адреса (или используем chainId из запроса)
|
||||||
let provider, rpcUrl, targetChainId = req.body.chainId;
|
let provider, rpcUrl, targetChainId = req.body.chainId;
|
||||||
const candidateChainIds = [11155111, 17000, 421614, 84532];
|
|
||||||
|
// Получаем список сетей из базы данных для данного DLE
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
if (targetChainId) {
|
if (targetChainId) {
|
||||||
rpcUrl = await rpcProviderService.getRpcUrlByChainId(Number(targetChainId));
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(Number(targetChainId));
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
@@ -43,18 +68,46 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
return res.status(400).json({ success: false, error: `По адресу ${dleAddress} нет контракта в сети ${targetChainId}` });
|
return res.status(400).json({ success: false, error: `По адресу ${dleAddress} нет контракта в сети ${targetChainId}` });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Ищем контракт во всех сетях
|
||||||
|
let foundContracts = [];
|
||||||
|
|
||||||
for (const cid of candidateChainIds) {
|
for (const cid of candidateChainIds) {
|
||||||
try {
|
try {
|
||||||
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
if (!url) continue;
|
if (!url) continue;
|
||||||
const prov = new ethers.JsonRpcProvider(url);
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
const code = await prov.getCode(dleAddress);
|
const code = await prov.getCode(dleAddress);
|
||||||
if (code && code !== '0x') { provider = prov; rpcUrl = url; targetChainId = cid; break; }
|
if (code && code !== '0x') {
|
||||||
|
// Контракт найден, currentChainId теперь равен block.chainid
|
||||||
|
foundContracts.push({
|
||||||
|
chainId: cid,
|
||||||
|
currentChainId: cid, // currentChainId = block.chainid = cid
|
||||||
|
provider: prov,
|
||||||
|
rpcUrl: url
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!provider) {
|
|
||||||
|
if (foundContracts.length === 0) {
|
||||||
return res.status(400).json({ success: false, error: 'Не удалось найти сеть, где по адресу есть контракт' });
|
return res.status(400).json({ success: false, error: 'Не удалось найти сеть, где по адресу есть контракт' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Выбираем первую доступную сеть (currentChainId - это governance chain, не primary)
|
||||||
|
const primaryContract = foundContracts[0];
|
||||||
|
|
||||||
|
if (primaryContract) {
|
||||||
|
// Используем основную сеть для чтения данных
|
||||||
|
provider = primaryContract.provider;
|
||||||
|
rpcUrl = primaryContract.rpcUrl;
|
||||||
|
targetChainId = primaryContract.chainId;
|
||||||
|
} else {
|
||||||
|
// Fallback: берем первый найденный контракт
|
||||||
|
const firstContract = foundContracts[0];
|
||||||
|
provider = firstContract.provider;
|
||||||
|
rpcUrl = firstContract.rpcUrl;
|
||||||
|
targetChainId = firstContract.chainId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ABI для чтения данных DLE
|
// ABI для чтения данных DLE
|
||||||
@@ -75,7 +128,7 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
const dleInfo = await dle.getDLEInfo();
|
const dleInfo = await dle.getDLEInfo();
|
||||||
const totalSupply = await dle.totalSupply();
|
const totalSupply = await dle.totalSupply();
|
||||||
const quorumPercentage = await dle.quorumPercentage();
|
const quorumPercentage = await dle.quorumPercentage();
|
||||||
const currentChainId = await dle.getCurrentChainId();
|
const currentChainId = targetChainId; // currentChainId = block.chainid = targetChainId
|
||||||
|
|
||||||
// Читаем логотип
|
// Читаем логотип
|
||||||
let logoURI = '';
|
let logoURI = '';
|
||||||
@@ -205,6 +258,27 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
console.log(`[Blockchain] Ошибка при чтении модулей:`, modulesError.message);
|
console.log(`[Blockchain] Ошибка при чтении модулей:`, modulesError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Собираем информацию о всех развернутых сетях
|
||||||
|
const deployedNetworks = [];
|
||||||
|
if (typeof foundContracts !== 'undefined') {
|
||||||
|
for (const contract of foundContracts) {
|
||||||
|
deployedNetworks.push({
|
||||||
|
chainId: contract.chainId,
|
||||||
|
address: dleAddress,
|
||||||
|
currentChainId: contract.currentChainId,
|
||||||
|
isPrimary: false // currentChainId - это governance chain, не primary
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если chainId был указан в запросе, добавляем только эту сеть
|
||||||
|
deployedNetworks.push({
|
||||||
|
chainId: targetChainId,
|
||||||
|
address: dleAddress,
|
||||||
|
currentChainId: Number(currentChainId),
|
||||||
|
isPrimary: Number(currentChainId) === targetChainId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const blockchainData = {
|
const blockchainData = {
|
||||||
name: dleInfo.name,
|
name: dleInfo.name,
|
||||||
symbol: dleInfo.symbol,
|
symbol: dleInfo.symbol,
|
||||||
@@ -225,7 +299,8 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
currentChainId: Number(currentChainId),
|
currentChainId: Number(currentChainId),
|
||||||
rpcUsed: rpcUrl,
|
rpcUsed: rpcUrl,
|
||||||
participantCount: participantCount,
|
participantCount: participantCount,
|
||||||
modules: modules // Информация о модулях
|
modules: modules, // Информация о модулях
|
||||||
|
deployedNetworks: deployedNetworks // Информация о всех развернутых сетях
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
|
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
|
||||||
@@ -260,8 +335,30 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -345,7 +442,7 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
initiator: proposal.initiator,
|
initiator: proposal.initiator,
|
||||||
governanceChainId: Number(proposal.governanceChainId),
|
governanceChainId: Number(proposal.governanceChainId),
|
||||||
snapshotTimepoint: Number(proposal.snapshotTimepoint),
|
snapshotTimepoint: Number(proposal.snapshotTimepoint),
|
||||||
targetChains: proposal.targets.map(chainId => Number(chainId)),
|
targetChains: proposal.targets.map(targetChainId => Number(targetChainId)),
|
||||||
isPassed: isPassed,
|
isPassed: isPassed,
|
||||||
blockNumber: events[i].blockNumber
|
blockNumber: events[i].blockNumber
|
||||||
};
|
};
|
||||||
@@ -400,8 +497,30 @@ router.post('/get-proposal-info', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -424,7 +543,7 @@ router.post('/get-proposal-info', async (req, res) => {
|
|||||||
const isPassed = await dle.checkProposalResult(proposalId);
|
const isPassed = await dle.checkProposalResult(proposalId);
|
||||||
|
|
||||||
// governanceChainId не сохраняется в предложении, используем текущую цепочку
|
// governanceChainId не сохраняется в предложении, используем текущую цепочку
|
||||||
const governanceChainId = 11155111; // Sepolia chain ID
|
const governanceChainId = targetChainId || 11155111; // Используем найденную сеть или Sepolia по умолчанию
|
||||||
|
|
||||||
const proposalInfo = {
|
const proposalInfo = {
|
||||||
description: proposal.description,
|
description: proposal.description,
|
||||||
@@ -472,8 +591,30 @@ router.post('/deactivate-dle', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`);
|
console.log(`[Blockchain] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -543,7 +684,30 @@ router.post('/check-deactivation-proposal-result', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Проверка результата предложения деактивации: ${proposalId} для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Проверка результата предложения деактивации: ${proposalId} для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -598,7 +762,30 @@ router.post('/load-deactivation-proposals', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Загрузка предложений деактивации для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Загрузка предложений деактивации для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -679,7 +866,30 @@ router.post('/execute-proposal', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Исполнение предложения ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Исполнение предложения ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -732,7 +942,30 @@ router.post('/cancel-proposal', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Отмена предложения ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Отмена предложения ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -794,7 +1027,30 @@ router.post('/get-governance-params', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение параметров управления для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение параметров управления для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -847,7 +1103,30 @@ router.post('/get-proposal-state', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение состояния предложения ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение состояния предложения ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -899,7 +1178,30 @@ router.post('/get-proposal-votes', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение голосов по предложению ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение голосов по предложению ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -954,7 +1256,30 @@ router.post('/get-proposals-count', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение количества предложений для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение количества предложений для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1005,7 +1330,30 @@ router.post('/list-proposals', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1058,7 +1406,30 @@ router.post('/get-voting-power-at', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение голосующей силы для ${voter} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение голосующей силы для ${voter} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1111,7 +1482,30 @@ router.post('/get-quorum-at', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение требуемого кворума для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение требуемого кворума для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1163,7 +1557,30 @@ router.post('/get-token-balance', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение баланса токенов для ${account} в DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение баланса токенов для ${account} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1215,7 +1632,30 @@ router.post('/get-total-supply', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение общего предложения токенов для DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение общего предложения токенов для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1266,7 +1706,30 @@ router.post('/is-active', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Проверка активности DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Проверка активности DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1323,7 +1786,30 @@ router.post('/get-dle-analytics', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение аналитики DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение аналитики DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1447,7 +1933,30 @@ router.post('/get-dle-history', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[Blockchain] Получение истории DLE: ${dleAddress}`);
|
console.log(`[Blockchain] Получение истории DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
const candidateChainIds = await getSupportedChainIds(dleAddress);
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -47,6 +47,28 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|||||||
hardhatProcess.on('close', (code) => {
|
hardhatProcess.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
console.log('✅ Компиляция завершена успешно');
|
console.log('✅ Компиляция завершена успешно');
|
||||||
|
|
||||||
|
// Автоматически генерируем ABI для фронтенда
|
||||||
|
try {
|
||||||
|
const { generateABIFile } = require('../scripts/generate-abi');
|
||||||
|
generateABIFile();
|
||||||
|
console.log('✅ ABI файл автоматически обновлен');
|
||||||
|
} catch (abiError) {
|
||||||
|
console.warn('⚠️ Ошибка генерации ABI:', abiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автоматически генерируем flattened контракт для верификации
|
||||||
|
try {
|
||||||
|
const { generateFlattened } = require('../scripts/generate-flattened');
|
||||||
|
generateFlattened().then(() => {
|
||||||
|
console.log('✅ Flattened контракт автоматически обновлен');
|
||||||
|
}).catch((flattenError) => {
|
||||||
|
console.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message);
|
||||||
|
});
|
||||||
|
} catch (flattenError) {
|
||||||
|
console.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Смарт-контракты скомпилированы успешно',
|
message: 'Смарт-контракты скомпилированы успешно',
|
||||||
|
|||||||
@@ -29,7 +29,41 @@ router.post('/get-dle-analytics', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Analytics] Получение аналитики для DLE: ${dleAddress}`);
|
console.log(`[DLE Analytics] Получение аналитики для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -165,7 +199,41 @@ router.post('/get-dle-history', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Analytics] Получение истории для DLE: ${dleAddress}`);
|
console.log(`[DLE Analytics] Получение истории для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -29,8 +29,41 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Core] Чтение данных DLE из блокчейна: ${dleAddress}`);
|
console.log(`[DLE Core] Чтение данных DLE из блокчейна: ${dleAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [11155111, 421614, 84532, 17000]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -205,11 +238,27 @@ router.post('/get-governance-params', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Core] Получение параметров управления для DLE: ${dleAddress}`);
|
console.log(`[DLE Core] Получение параметров управления для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Получаем RPC URL из параметров деплоя или используем Sepolia как fallback
|
||||||
|
let rpcUrl;
|
||||||
|
try {
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
const supportedChainIds = params.supportedChainIds || [];
|
||||||
|
const chainId = supportedChainIds.length > 0 ? supportedChainIds[0] : 11155111;
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId);
|
||||||
|
} else {
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем Sepolia:', error);
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'RPC URL для Sepolia не найден'
|
error: 'RPC URL не найден'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,11 +307,27 @@ router.post('/is-active', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Core] Проверка активности DLE: ${dleAddress}`);
|
console.log(`[DLE Core] Проверка активности DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Получаем RPC URL из параметров деплоя или используем Sepolia как fallback
|
||||||
|
let rpcUrl;
|
||||||
|
try {
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
const supportedChainIds = params.supportedChainIds || [];
|
||||||
|
const chainId = supportedChainIds.length > 0 ? supportedChainIds[0] : 11155111;
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId);
|
||||||
|
} else {
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем Sepolia:', error);
|
||||||
|
rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'RPC URL для Sepolia не найден'
|
error: 'RPC URL не найден'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,8 +374,41 @@ router.post('/deactivate-dle', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Core] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`);
|
console.log(`[DLE Core] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [11155111, 421614, 84532, 17000]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -30,7 +30,41 @@ router.post('/get-extended-history', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE History] Получение расширенной истории для DLE: ${dleAddress}`);
|
console.log(`[DLE History] Получение расширенной истории для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -575,6 +575,26 @@ router.post('/get-all-modules', async (req, res) => {
|
|||||||
return networks[chainId] || `Chain ${chainId}`;
|
return networks[chainId] || `Chain ${chainId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFallbackRpcUrl(chainId) {
|
||||||
|
const fallbackUrls = {
|
||||||
|
11155111: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52',
|
||||||
|
17000: 'https://ethereum-holesky.publicnode.com',
|
||||||
|
421614: 'https://sepolia-rollup.arbitrum.io/rpc',
|
||||||
|
84532: 'https://sepolia.base.org'
|
||||||
|
};
|
||||||
|
return fallbackUrls[chainId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEtherscanUrl(chainId) {
|
||||||
|
const etherscanUrls = {
|
||||||
|
11155111: 'https://sepolia.etherscan.io',
|
||||||
|
17000: 'https://holesky.etherscan.io',
|
||||||
|
421614: 'https://sepolia.arbiscan.io',
|
||||||
|
84532: 'https://sepolia.basescan.org'
|
||||||
|
};
|
||||||
|
return etherscanUrls[chainId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
function getModuleDescription(moduleType) {
|
function getModuleDescription(moduleType) {
|
||||||
const descriptions = {
|
const descriptions = {
|
||||||
treasury: 'Казначейство DLE - управление финансами, депозиты, выводы, дивиденды',
|
treasury: 'Казначейство DLE - управление финансами, депозиты, выводы, дивиденды',
|
||||||
@@ -590,8 +610,27 @@ router.post('/get-all-modules', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Modules] Найдено типов модулей: ${formattedModules.length}`);
|
console.log(`[DLE Modules] Найдено типов модулей: ${formattedModules.length}`);
|
||||||
|
|
||||||
// Получаем поддерживаемые сети из модулей
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
const supportedNetworks = [
|
let supportedNetworks = [];
|
||||||
|
try {
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
const supportedChainIds = params.supportedChainIds || [];
|
||||||
|
const rpcUrls = params.rpcUrls || params.rpc_urls || {};
|
||||||
|
|
||||||
|
supportedNetworks = supportedChainIds.map((chainId, index) => ({
|
||||||
|
chainId: Number(chainId),
|
||||||
|
networkName: getNetworkName(Number(chainId)),
|
||||||
|
rpcUrl: rpcUrls[chainId] || getFallbackRpcUrl(chainId),
|
||||||
|
etherscanUrl: getEtherscanUrl(chainId),
|
||||||
|
networkIndex: index
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя:', error);
|
||||||
|
// Fallback для совместимости
|
||||||
|
supportedNetworks = [
|
||||||
{
|
{
|
||||||
chainId: 11155111,
|
chainId: 11155111,
|
||||||
networkName: 'Sepolia',
|
networkName: 'Sepolia',
|
||||||
@@ -621,6 +660,7 @@ router.post('/get-all-modules', async (req, res) => {
|
|||||||
networkIndex: 3
|
networkIndex: 3
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -642,10 +682,57 @@ router.post('/get-all-modules', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Создать предложение о добавлении модуля
|
// Получить deploymentId по адресу DLE
|
||||||
|
router.post('/get-deployment-id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Адрес DLE обязателен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Modules] Поиск deploymentId для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
|
||||||
|
// Ищем параметры деплоя по адресу DLE
|
||||||
|
const result = await deployParamsService.getDeployParamsByDleAddress(dleAddress);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'DeploymentId не найден для данного адреса DLE'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await deployParamsService.close();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
deploymentId: result.deployment_id,
|
||||||
|
dleAddress: result.dle_address,
|
||||||
|
deploymentStatus: result.deployment_status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Modules] Ошибка при получении deploymentId:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при получении deploymentId: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создать предложение о добавлении модуля (с автоматической оплатой газа)
|
||||||
router.post('/create-add-module-proposal', async (req, res) => {
|
router.post('/create-add-module-proposal', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { dleAddress, description, duration, moduleId, moduleAddress, chainId } = req.body;
|
const { dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId } = req.body;
|
||||||
|
|
||||||
if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) {
|
if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -666,14 +753,54 @@ router.post('/create-add-module-proposal', async (req, res) => {
|
|||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// Получаем приватный ключ из параметров деплоя
|
||||||
|
let privateKey;
|
||||||
|
if (deploymentId) {
|
||||||
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
const params = await deployParamsService.getDeployParams(deploymentId);
|
||||||
|
|
||||||
|
if (!params || !params.privateKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Приватный ключ не найден в параметрах деплоя'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey = params.privateKey;
|
||||||
|
await deployParamsService.close();
|
||||||
|
} else {
|
||||||
|
// Fallback к переменной окружения
|
||||||
|
privateKey = process.env.PRIVATE_KEY;
|
||||||
|
if (!privateKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Приватный ключ не найден. Укажите deploymentId или установите PRIVATE_KEY'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем кошелек
|
||||||
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
console.log(`[DLE Modules] Используем кошелек: ${wallet.address}`);
|
||||||
|
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
|
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
|
||||||
|
|
||||||
// Подготавливаем данные для транзакции (не отправляем)
|
// Отправляем транзакцию автоматически
|
||||||
const txData = await dle.createAddModuleProposal.populateTransaction(
|
console.log(`[DLE Modules] Отправляем транзакцию создания предложения...`);
|
||||||
|
console.log(`[DLE Modules] Параметры:`, {
|
||||||
|
description,
|
||||||
|
duration,
|
||||||
|
moduleId,
|
||||||
|
moduleAddress,
|
||||||
|
chainId
|
||||||
|
});
|
||||||
|
|
||||||
|
const tx = await dle.createAddModuleProposal(
|
||||||
description,
|
description,
|
||||||
duration,
|
duration,
|
||||||
moduleId,
|
moduleId,
|
||||||
@@ -681,16 +808,130 @@ router.post('/create-add-module-proposal', async (req, res) => {
|
|||||||
chainId
|
chainId
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[DLE Modules] Данные транзакции подготовлены:`, txData);
|
console.log(`[DLE Modules] Транзакция отправлена: ${tx.hash}`);
|
||||||
|
console.log(`[DLE Modules] Ожидаем подтверждения...`);
|
||||||
|
|
||||||
|
// Ждем подтверждения
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
// Пробуем получить proposalId из возвращаемого значения транзакции
|
||||||
|
let proposalIdFromReturn = null;
|
||||||
|
try {
|
||||||
|
// Если функция возвращает значение, оно должно быть в receipt
|
||||||
|
if (receipt.logs && receipt.logs.length > 0) {
|
||||||
|
console.log(`[DLE Modules] Ищем ProposalCreated в ${receipt.logs.length} логах транзакции...`);
|
||||||
|
|
||||||
|
// Ищем событие с возвращаемым значением
|
||||||
|
for (let i = 0; i < receipt.logs.length; i++) {
|
||||||
|
const log = receipt.logs[i];
|
||||||
|
console.log(`[DLE Modules] Лог ${i}:`, {
|
||||||
|
address: log.address,
|
||||||
|
topics: log.topics,
|
||||||
|
data: log.data
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedLog = dle.interface.parseLog(log);
|
||||||
|
console.log(`[DLE Modules] Парсинг лога ${i}:`, parsedLog);
|
||||||
|
|
||||||
|
if (parsedLog && parsedLog.name === 'ProposalCreated') {
|
||||||
|
proposalIdFromReturn = parsedLog.args.proposalId.toString();
|
||||||
|
console.log(`[DLE Modules] ✅ Получен proposalId из события: ${proposalIdFromReturn}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[DLE Modules] Ошибка парсинга лога ${i}:`, e.message);
|
||||||
|
// Пробуем альтернативный способ - ищем по топикам
|
||||||
|
if (log.topics && log.topics.length > 0) {
|
||||||
|
// ProposalCreated имеет сигнатуру: ProposalCreated(uint256,address,string)
|
||||||
|
// Первый топик - это хеш сигнатуры события
|
||||||
|
const proposalCreatedTopic = ethers.id("ProposalCreated(uint256,address,string)");
|
||||||
|
if (log.topics[0] === proposalCreatedTopic) {
|
||||||
|
console.log(`[DLE Modules] Найден топик ProposalCreated, извлекаем proposalId из данных...`);
|
||||||
|
// proposalId находится в indexed параметрах (топиках)
|
||||||
|
if (log.topics.length > 1) {
|
||||||
|
proposalIdFromReturn = BigInt(log.topics[1]).toString();
|
||||||
|
console.log(`[DLE Modules] ✅ Извлечен proposalId из топика: ${proposalIdFromReturn}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[DLE Modules] Ошибка при получении proposalId из возвращаемого значения:`, e.message);
|
||||||
|
}
|
||||||
|
console.log(`[DLE Modules] Транзакция подтверждена:`, {
|
||||||
|
hash: receipt.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
gasUsed: receipt.gasUsed.toString(),
|
||||||
|
logsCount: receipt.logs.length,
|
||||||
|
status: receipt.status
|
||||||
|
});
|
||||||
|
|
||||||
|
// Используем proposalId из события, если он найден
|
||||||
|
let proposalId = proposalIdFromReturn;
|
||||||
|
|
||||||
|
// Если не найден в событии, пробуем другие способы
|
||||||
|
if (!proposalId) {
|
||||||
|
console.log(`[DLE Modules] Анализируем ${receipt.logs.length} логов для поиска ProposalCreated...`);
|
||||||
|
|
||||||
|
if (receipt.logs && receipt.logs.length > 0) {
|
||||||
|
// Ищем событие ProposalCreated
|
||||||
|
for (let i = 0; i < receipt.logs.length; i++) {
|
||||||
|
const log = receipt.logs[i];
|
||||||
|
console.log(`[DLE Modules] Лог ${i}:`, {
|
||||||
|
address: log.address,
|
||||||
|
topics: log.topics,
|
||||||
|
data: log.data
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedLog = dle.interface.parseLog(log);
|
||||||
|
console.log(`[DLE Modules] Парсинг лога ${i}:`, parsedLog);
|
||||||
|
|
||||||
|
if (parsedLog && parsedLog.name === 'ProposalCreated') {
|
||||||
|
proposalId = parsedLog.args.proposalId.toString();
|
||||||
|
console.log(`[DLE Modules] ✅ Найден ProposalCreated с ID: ${proposalId}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[DLE Modules] Ошибка парсинга лога ${i}:`, e.message);
|
||||||
|
// Пробуем альтернативный способ - ищем по топикам
|
||||||
|
if (log.topics && log.topics.length > 0) {
|
||||||
|
// ProposalCreated имеет сигнатуру: ProposalCreated(uint256,address,string)
|
||||||
|
// Первый топик - это хеш сигнатуры события
|
||||||
|
const proposalCreatedTopic = ethers.id("ProposalCreated(uint256,address,string)");
|
||||||
|
if (log.topics[0] === proposalCreatedTopic) {
|
||||||
|
console.log(`[DLE Modules] Найден топик ProposalCreated, извлекаем proposalId из данных...`);
|
||||||
|
// proposalId находится в indexed параметрах (топиках)
|
||||||
|
if (log.topics.length > 1) {
|
||||||
|
proposalId = BigInt(log.topics[1]).toString();
|
||||||
|
console.log(`[DLE Modules] ✅ Извлечен proposalId из топика: ${proposalId}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposalId) {
|
||||||
|
console.warn(`[DLE Modules] ⚠️ Не удалось извлечь proposalId из логов транзакции`);
|
||||||
|
console.warn(`[DLE Modules] ⚠️ Это критическая проблема - без proposalId нельзя исполнить предложение!`);
|
||||||
|
} else {
|
||||||
|
console.log(`[DLE Modules] ✅ Успешно получен proposalId: ${proposalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
to: dleAddress,
|
transactionHash: receipt.hash,
|
||||||
data: txData.data,
|
proposalId: proposalId,
|
||||||
value: "0x0",
|
gasUsed: receipt.gasUsed.toString(),
|
||||||
gasLimit: "0x1e8480", // 2,000,000 gas
|
message: `Предложение о добавлении модуля успешно создано! ID: ${proposalId || 'неизвестно'}`
|
||||||
message: "Подготовлены данные для создания предложения о добавлении модуля. Отправьте транзакцию через MetaMask."
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -717,7 +958,41 @@ router.post('/create-remove-module-proposal', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Modules] Создание предложения об удалении модуля: ${moduleId} для DLE: ${dleAddress}`);
|
console.log(`[DLE Modules] Создание предложения об удалении модуля: ${moduleId} для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1255,8 +1530,9 @@ async function createStandardJsonInput(contractName, moduleAddress, dleAddress,
|
|||||||
settings: {
|
settings: {
|
||||||
optimizer: {
|
optimizer: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
runs: 200
|
runs: 0
|
||||||
},
|
},
|
||||||
|
viaIR: true,
|
||||||
evmVersion: "paris",
|
evmVersion: "paris",
|
||||||
outputSelection: {
|
outputSelection: {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -1265,7 +1541,7 @@ async function createStandardJsonInput(contractName, moduleAddress, dleAddress,
|
|||||||
},
|
},
|
||||||
libraries: {},
|
libraries: {},
|
||||||
remappings: [
|
remappings: [
|
||||||
"@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/"
|
"@openzeppelin/contracts/=@openzeppelin/contracts/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1904,8 +2180,9 @@ router.post('/deploy-module-all-networks', async (req, res) => {
|
|||||||
const provider = new ethers.JsonRpcProvider(network.rpcUrl);
|
const provider = new ethers.JsonRpcProvider(network.rpcUrl);
|
||||||
const wallet = new ethers.Wallet(privateKey, provider);
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
|
||||||
// Получаем текущий nonce
|
// Используем NonceManager для правильного управления nonce
|
||||||
const currentNonce = await wallet.getNonce();
|
const { nonceManager } = require('../utils/nonceManager');
|
||||||
|
const currentNonce = await nonceManager.getNonce(wallet.address, network.rpcUrl, network.chainId);
|
||||||
console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`);
|
console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`);
|
||||||
|
|
||||||
// Получаем фабрику контракта
|
// Получаем фабрику контракта
|
||||||
@@ -2057,7 +2334,7 @@ router.post('/verify-dle-all-networks', async (req, res) => {
|
|||||||
const supportedChainIds = Array.isArray(saved?.networks)
|
const supportedChainIds = Array.isArray(saved?.networks)
|
||||||
? saved.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v))
|
? saved.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v))
|
||||||
: (saved?.governanceSettings?.supportedChainIds || []);
|
: (saved?.governanceSettings?.supportedChainIds || []);
|
||||||
const currentChainId = Number(saved?.governanceSettings?.currentChainId || network.chainId);
|
const currentChainId = Number(saved?.governanceSettings?.currentChainId || 1); // governance chain, не network.chainId
|
||||||
|
|
||||||
// Создаем стандартный JSON input для верификации
|
// Создаем стандартный JSON input для верификации
|
||||||
const standardJsonInput = {
|
const standardJsonInput = {
|
||||||
@@ -2070,7 +2347,7 @@ router.post('/verify-dle-all-networks', async (req, res) => {
|
|||||||
settings: {
|
settings: {
|
||||||
optimizer: {
|
optimizer: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
runs: 1
|
runs: 0
|
||||||
},
|
},
|
||||||
viaIR: true,
|
viaIR: true,
|
||||||
outputSelection: {
|
outputSelection: {
|
||||||
@@ -2123,7 +2400,7 @@ router.post('/verify-dle-all-networks', async (req, res) => {
|
|||||||
const initPartners = Array.isArray(found?.initialPartners) ? found.initialPartners : [];
|
const initPartners = Array.isArray(found?.initialPartners) ? found.initialPartners : [];
|
||||||
const initAmounts = Array.isArray(found?.initialAmounts) ? found.initialAmounts : [];
|
const initAmounts = Array.isArray(found?.initialAmounts) ? found.initialAmounts : [];
|
||||||
const scIds = Array.isArray(found?.networks) ? found.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) : supportedChainIds;
|
const scIds = Array.isArray(found?.networks) ? found.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) : supportedChainIds;
|
||||||
const currentCid = Number(found?.governanceSettings?.currentChainId || found?.networks?.[0]?.chainId || network.chainId);
|
const currentCid = Number(found?.governanceSettings?.currentChainId || 1); // governance chain, не network.chainId
|
||||||
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
|
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||||
['tuple(string,string,string,string,uint256,string,uint256,uint256,address[],uint256[],uint256[])', 'uint256', 'address'],
|
['tuple(string,string,string,string,uint256,string,uint256,uint256,address[],uint256[],uint256[])', 'uint256', 'address'],
|
||||||
[[name, symbol, location, coordinates, jurisdiction, oktmo, kpp, quorumPercentage, initPartners, initAmounts.map(a => BigInt(a)), scIds], BigInt(currentCid), initializer]
|
[[name, symbol, location, coordinates, jurisdiction, oktmo, kpp, quorumPercentage, initPartners, initAmounts.map(a => BigInt(a)), scIds], BigInt(currentCid), initializer]
|
||||||
|
|||||||
@@ -1,440 +1,122 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This software is proprietary and confidential.
|
|
||||||
* Unauthorized copying, modification, or distribution is prohibited.
|
|
||||||
*
|
|
||||||
* For licensing inquiries: info@hb3-accelerator.com
|
|
||||||
* Website: https://hb3-accelerator.com
|
|
||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
|
||||||
*/
|
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const deployParamsService = require('../services/deployParamsService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить адрес контракта в указанной сети для мультичейн голосования
|
||||||
|
* POST /api/dle-core/get-multichain-contracts
|
||||||
|
*/
|
||||||
|
router.post('/get-multichain-contracts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { originalContract, targetChainId } = req.body;
|
||||||
|
|
||||||
|
console.log('🔍 [MULTICHAIN] Поиск контракта для мультичейн голосования:', {
|
||||||
|
originalContract,
|
||||||
|
targetChainId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!originalContract || !targetChainId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не указан originalContract или targetChainId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем контракт в указанной сети
|
||||||
|
// Для мультичейн контрактов с одинаковым адресом (детерминированный деплой)
|
||||||
|
// или контракты в разных сетях с разными адресами
|
||||||
|
|
||||||
|
// Сначала проверяем, есть ли контракт с таким же адресом в целевой сети
|
||||||
|
const contractsInTargetNetwork = await deployParamsService.getContractsByChainId(targetChainId);
|
||||||
|
|
||||||
|
console.log('📊 [MULTICHAIN] Контракты в целевой сети:', contractsInTargetNetwork);
|
||||||
|
|
||||||
|
// Ищем контракт в целевой сети (все контракты в targetChainId уже отфильтрованы)
|
||||||
|
const targetContract = contractsInTargetNetwork[0]; // Берем первый контракт в целевой сети
|
||||||
|
|
||||||
|
if (targetContract) {
|
||||||
|
console.log('✅ [MULTICHAIN] Найден контракт в целевой сети:', targetContract.dleAddress);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
contractAddress: targetContract.dleAddress,
|
||||||
|
chainId: targetChainId,
|
||||||
|
source: 'database'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не найден контракт в целевой сети, проверяем мультичейн развертывание
|
||||||
|
// с одинаковым адресом (CREATE2)
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
const rpcProviderService = require('../services/rpcProviderService');
|
|
||||||
|
|
||||||
// Получить поддерживаемые сети
|
// Получаем RPC URL из параметров деплоя
|
||||||
router.post('/get-supported-chains', async (req, res) => {
|
let rpcUrl;
|
||||||
try {
|
try {
|
||||||
const { dleAddress } = req.body;
|
// Получаем последние параметры деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
const rpcUrls = params.rpcUrls || params.rpc_urls || {};
|
||||||
|
rpcUrl = rpcUrls[targetChainId];
|
||||||
|
}
|
||||||
|
|
||||||
if (!dleAddress) {
|
// Если не найден в параметрах, используем fallback
|
||||||
|
if (!rpcUrl) {
|
||||||
|
const fallbackConfigs = {
|
||||||
|
'11155111': 'https://1rpc.io/sepolia',
|
||||||
|
'17000': 'https://ethereum-holesky.publicnode.com',
|
||||||
|
'421614': 'https://sepolia-rollup.arbitrum.io/rpc',
|
||||||
|
'84532': 'https://sepolia.base.org'
|
||||||
|
};
|
||||||
|
rpcUrl = fallbackConfigs[targetChainId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Адрес DLE обязателен'
|
error: `Неподдерживаемая сеть: ${targetChainId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
console.log(`[DLE Multichain] Получение поддерживаемых сетей для DLE: ${dleAddress}`);
|
console.error('❌ Ошибка получения RPC URL:', error);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'RPC URL для Sepolia не найден'
|
error: 'Ошибка получения конфигурации сети'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const contractCode = await provider.getCode(originalContract);
|
||||||
|
|
||||||
const dleAbi = [
|
if (contractCode && contractCode !== '0x') {
|
||||||
"function getSupportedChainCount() external view returns (uint256)",
|
console.log('✅ [MULTICHAIN] Контракт существует в целевой сети с тем же адресом (CREATE2)');
|
||||||
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
return res.json({
|
||||||
|
|
||||||
// Получаем количество поддерживаемых сетей
|
|
||||||
const chainCount = await dle.getSupportedChainCount();
|
|
||||||
|
|
||||||
// Получаем ID каждой сети
|
|
||||||
const supportedChains = [];
|
|
||||||
for (let i = 0; i < Number(chainCount); i++) {
|
|
||||||
const chainId = await dle.getSupportedChainId(i);
|
|
||||||
supportedChains.push(chainId);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Поддерживаемые сети:`, supportedChains);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
contractAddress: originalContract,
|
||||||
chains: supportedChains.map(chainId => Number(chainId))
|
chainId: targetChainId,
|
||||||
|
source: 'blockchain'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (blockchainError) {
|
||||||
|
console.warn('⚠️ [MULTICHAIN] Ошибка проверки контракта в блокчейне:', blockchainError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Контракт не найден
|
||||||
|
console.log('❌ [MULTICHAIN] Контракт не найден в целевой сети');
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Контракт не найден в целевой сети'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DLE Multichain] Ошибка при получении поддерживаемых сетей:', error);
|
console.error('❌ [MULTICHAIN] Ошибка поиска мультичейн контракта:', error);
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении поддерживаемых сетей: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверить поддержку сети
|
|
||||||
router.post('/is-chain-supported', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, chainId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || chainId === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и ID сети обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Проверка поддержки сети ${chainId} для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'RPC URL для Sepolia не найден'
|
error: 'Внутренняя ошибка сервера'
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function isChainSupported(uint256 _chainId) external view returns (bool)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Проверяем поддержку сети
|
|
||||||
const isSupported = await dle.isChainSupported(chainId);
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Поддержка сети ${chainId}: ${isSupported}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
chainId: Number(chainId),
|
|
||||||
isSupported: isSupported
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при проверке поддержки сети:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при проверке поддержки сети: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить количество поддерживаемых сетей
|
|
||||||
router.post('/get-supported-chain-count', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE обязателен'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Получение количества поддерживаемых сетей для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function getSupportedChainCount() external view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Получаем количество поддерживаемых сетей
|
|
||||||
const count = await dle.getSupportedChainCount();
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Количество поддерживаемых сетей: ${count}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
count: Number(count)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при получении количества поддерживаемых сетей:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении количества поддерживаемых сетей: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить ID сети по индексу
|
|
||||||
router.post('/get-supported-chain-id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, index } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || index === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и индекс обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Получение ID сети по индексу ${index} для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Получаем ID сети по индексу
|
|
||||||
const chainId = await dle.getSupportedChainId(index);
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] ID сети по индексу ${index}: ${chainId}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
index: Number(index),
|
|
||||||
chainId: Number(chainId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при получении ID сети по индексу:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении ID сети по индексу: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверить подключение к сети
|
|
||||||
router.post('/check-chain-connection', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, chainId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || chainId === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и ID сети обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Проверка подключения к сети ${chainId} для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function checkChainConnection(uint256 _chainId) external view returns (bool)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Проверяем подключение к сети
|
|
||||||
const isAvailable = await dle.checkChainConnection(chainId);
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Подключение к сети ${chainId}: ${isAvailable}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
chainId: Number(chainId),
|
|
||||||
isAvailable: isAvailable
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при проверке подключения к сети:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при проверке подключения к сети: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверить готовность к синхронизации
|
|
||||||
router.post('/check-sync-readiness', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, proposalId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и ID предложения обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Проверка готовности к синхронизации предложения ${proposalId} для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function checkSyncReadiness(uint256 _proposalId) external view returns (bool)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Проверяем готовность к синхронизации
|
|
||||||
const allChainsReady = await dle.checkSyncReadiness(proposalId);
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Готовность к синхронизации предложения ${proposalId}: ${allChainsReady}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
proposalId: Number(proposalId),
|
|
||||||
allChainsReady: allChainsReady
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при проверке готовности к синхронизации:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при проверке готовности к синхронизации: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Синхронизировать во все сети
|
|
||||||
router.post('/sync-to-all-chains', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, proposalId, userAddress, privateKey } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined || !userAddress || !privateKey) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны, включая приватный ключ'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Синхронизация предложения ${proposalId} во все сети для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
const wallet = new ethers.Wallet(privateKey, provider);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function syncToAllChains(uint256 _proposalId) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
|
|
||||||
|
|
||||||
// Синхронизируем во все сети
|
|
||||||
const tx = await dle.syncToAllChains(proposalId);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Синхронизация выполнена:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при синхронизации во все сети:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при синхронизации во все сети: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Исполнить предложение по подписям
|
|
||||||
router.post('/execute-proposal-by-signatures', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, proposalId, signatures, userAddress, privateKey } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined || !signatures || !userAddress || !privateKey) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны, включая приватный ключ'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Исполнение предложения ${proposalId} по подписям для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
const wallet = new ethers.Wallet(privateKey, provider);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function executeProposalBySignatures(uint256 _proposalId, bytes[] calldata _signatures) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
|
|
||||||
|
|
||||||
// Исполняем предложение по подписям
|
|
||||||
const tx = await dle.executeProposalBySignatures(proposalId, signatures);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[DLE Multichain] Предложение исполнено по подписям:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DLE Multichain] Ошибка при исполнении предложения по подписям:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при исполнении предложения по подписям: ' + error.message
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
346
backend/routes/dleMultichainExecution.js
Normal file
346
backend/routes/dleMultichainExecution.js
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const rpcProviderService = require('../services/rpcProviderService');
|
||||||
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить информацию о мультиконтрактном предложении
|
||||||
|
* @route POST /api/dle-multichain/get-proposal-multichain-info
|
||||||
|
*/
|
||||||
|
router.post('/get-proposal-multichain-info', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId, governanceChainId } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined || !governanceChainId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Адрес DLE, ID предложения и ID сети голосования обязательны'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Multichain] Получение информации о предложении ${proposalId} для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем RPC URL для сети голосования
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(governanceChainId);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: `RPC URL для сети ${governanceChainId} не найден`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
const dleAbi = [
|
||||||
|
"function proposals(uint256) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, bytes memory operation, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targetChains)",
|
||||||
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
|
"function getSupportedChainCount() external view returns (uint256)",
|
||||||
|
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Получаем данные предложения
|
||||||
|
const proposal = await dle.proposals(proposalId);
|
||||||
|
const state = await dle.getProposalState(proposalId);
|
||||||
|
const result = await dle.checkProposalResult(proposalId);
|
||||||
|
|
||||||
|
// Получаем поддерживаемые сети
|
||||||
|
const chainCount = await dle.getSupportedChainCount();
|
||||||
|
const supportedChains = [];
|
||||||
|
for (let i = 0; i < chainCount; i++) {
|
||||||
|
const chainId = await dle.getSupportedChainId(i);
|
||||||
|
supportedChains.push(Number(chainId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposalInfo = {
|
||||||
|
id: Number(proposal.id),
|
||||||
|
description: proposal.description,
|
||||||
|
forVotes: Number(proposal.forVotes),
|
||||||
|
againstVotes: Number(proposal.againstVotes),
|
||||||
|
executed: proposal.executed,
|
||||||
|
canceled: proposal.canceled,
|
||||||
|
deadline: Number(proposal.deadline),
|
||||||
|
initiator: proposal.initiator,
|
||||||
|
operation: proposal.operation,
|
||||||
|
governanceChainId: Number(proposal.governanceChainId),
|
||||||
|
targetChains: proposal.targetChains.map(chain => Number(chain)),
|
||||||
|
snapshotTimepoint: Number(proposal.snapshotTimepoint),
|
||||||
|
state: Number(state),
|
||||||
|
isPassed: result.passed,
|
||||||
|
quorumReached: result.quorumReached,
|
||||||
|
supportedChains: supportedChains,
|
||||||
|
canExecuteInTargetChains: result.passed && result.quorumReached && !proposal.executed && !proposal.canceled
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[DLE Multichain] Информация о предложении получена:`, proposalInfo);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: proposalInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Multichain] Ошибка при получении информации о предложении:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при получении информации о предложении: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение во всех целевых сетях
|
||||||
|
* @route POST /api/dle-multichain/execute-in-all-target-chains
|
||||||
|
*/
|
||||||
|
router.post('/execute-in-all-target-chains', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId, deploymentId, userAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined || !deploymentId || !userAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Все поля обязательны'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Multichain] Исполнение предложения ${proposalId} во всех целевых сетях для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем параметры деплоя
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
const deployParams = await deployParamsService.getDeployParams(deploymentId);
|
||||||
|
|
||||||
|
if (!deployParams || !deployParams.privateKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Приватный ключ не найден в параметрах деплоя'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о предложении
|
||||||
|
const proposalInfoResponse = await fetch(`${req.protocol}://${req.get('host')}/api/dle-multichain/get-proposal-multichain-info`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
governanceChainId: deployParams.currentChainId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const proposalInfo = await proposalInfoResponse.json();
|
||||||
|
|
||||||
|
if (!proposalInfo.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось получить информацию о предложении'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { targetChains, canExecuteInTargetChains } = proposalInfo.data;
|
||||||
|
|
||||||
|
if (!canExecuteInTargetChains) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Предложение не готово к исполнению в целевых сетях'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetChains.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'У предложения нет целевых сетей для исполнения'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исполняем в каждой целевой сети
|
||||||
|
const executionResults = [];
|
||||||
|
|
||||||
|
for (const targetChainId of targetChains) {
|
||||||
|
try {
|
||||||
|
console.log(`[DLE Multichain] Исполнение в сети ${targetChainId}`);
|
||||||
|
|
||||||
|
const result = await executeProposalInChain(
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
targetChainId,
|
||||||
|
deployParams.privateKey,
|
||||||
|
userAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
executionResults.push({
|
||||||
|
chainId: targetChainId,
|
||||||
|
success: true,
|
||||||
|
transactionHash: result.transactionHash
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[DLE Multichain] Ошибка исполнения в сети ${targetChainId}:`, error.message);
|
||||||
|
executionResults.push({
|
||||||
|
chainId: targetChainId,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = executionResults.filter(r => r.success).length;
|
||||||
|
const totalCount = executionResults.length;
|
||||||
|
|
||||||
|
console.log(`[DLE Multichain] Исполнение завершено: ${successCount}/${totalCount} успешно`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
proposalId,
|
||||||
|
targetChains,
|
||||||
|
executionResults,
|
||||||
|
summary: {
|
||||||
|
total: totalCount,
|
||||||
|
successful: successCount,
|
||||||
|
failed: totalCount - successCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Multichain] Ошибка при исполнении во всех целевых сетях:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при исполнении во всех целевых сетях: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение в конкретной целевой сети
|
||||||
|
* @route POST /api/dle-multichain/execute-in-target-chain
|
||||||
|
*/
|
||||||
|
router.post('/execute-in-target-chain', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId, targetChainId, deploymentId, userAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined || !targetChainId || !deploymentId || !userAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Все поля обязательны'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Multichain] Исполнение предложения ${proposalId} в сети ${targetChainId} для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем параметры деплоя
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
const deployParams = await deployParamsService.getDeployParams(deploymentId);
|
||||||
|
|
||||||
|
if (!deployParams || !deployParams.privateKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Приватный ключ не найден в параметрах деплоя'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исполняем в целевой сети
|
||||||
|
const result = await executeProposalInChain(
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
targetChainId,
|
||||||
|
deployParams.privateKey,
|
||||||
|
userAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
proposalId,
|
||||||
|
targetChainId,
|
||||||
|
transactionHash: result.transactionHash,
|
||||||
|
blockNumber: result.blockNumber
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Multichain] Ошибка при исполнении в целевой сети:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при исполнении в целевой сети: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вспомогательная функция для исполнения предложения в конкретной сети
|
||||||
|
*/
|
||||||
|
async function executeProposalInChain(dleAddress, proposalId, chainId, privateKey, userAddress) {
|
||||||
|
// Получаем RPC URL для целевой сети
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
throw new Error(`RPC URL для сети ${chainId} не найден`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
|
||||||
|
const dleAbi = [
|
||||||
|
"function executeProposalBySignatures(uint256 _proposalId, address[] calldata signers, bytes[] calldata signatures) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
|
||||||
|
|
||||||
|
// Для простоты используем подпись от одного адреса (кошелька с приватным ключом)
|
||||||
|
// В реальности нужно собрать подписи от держателей токенов
|
||||||
|
const signers = [wallet.address];
|
||||||
|
const signatures = []; // TODO: Реализовать сбор подписей
|
||||||
|
|
||||||
|
// Временная заглушка - используем прямое исполнение если это возможно
|
||||||
|
// В реальности нужно реализовать сбор подписей от держателей токенов
|
||||||
|
try {
|
||||||
|
// Пытаемся исполнить напрямую (если это сеть голосования)
|
||||||
|
const directExecuteAbi = [
|
||||||
|
"function executeProposal(uint256 _proposalId) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const directDle = new ethers.Contract(dleAddress, directExecuteAbi, wallet);
|
||||||
|
const tx = await directDle.executeProposal(proposalId);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionHash: receipt.hash,
|
||||||
|
blockNumber: receipt.blockNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (directError) {
|
||||||
|
// Если прямое исполнение невозможно, используем подписи
|
||||||
|
if (signatures.length === 0) {
|
||||||
|
throw new Error('Необходимо собрать подписи от держателей токенов для исполнения в целевой сети');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await dle.executeProposalBySignatures(proposalId, signers, signatures);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionHash: receipt.hash,
|
||||||
|
blockNumber: receipt.blockNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
|
||||||
@@ -29,24 +29,93 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Proposals] Получение списка предложений для DLE: ${dleAddress}`);
|
console.log(`[DLE Proposals] Получение списка предложений для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Получаем поддерживаемые сети DLE из контракта
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let supportedChains = [];
|
||||||
|
try {
|
||||||
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
console.log(`[DLE Proposals] Не удалось найти сеть для адреса ${dleAddress}`);
|
||||||
success: false,
|
// Fallback к известным сетям
|
||||||
error: 'RPC URL для Sepolia не найден'
|
supportedChains = [11155111, 17000, 421614, 84532];
|
||||||
});
|
console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rpcUrl) {
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const dleAbi = [
|
||||||
|
"function getSupportedChainCount() external view returns (uint256)",
|
||||||
|
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
||||||
|
];
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
const chainCount = await dle.getSupportedChainCount();
|
||||||
|
console.log(`[DLE Proposals] Количество поддерживаемых сетей: ${chainCount}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < Number(chainCount); i++) {
|
||||||
|
const chainId = await dle.getSupportedChainId(i);
|
||||||
|
supportedChains.push(Number(chainId));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Поддерживаемые сети из контракта:`, supportedChains);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[DLE Proposals] Ошибка получения поддерживаемых сетей из контракта:`, error.message);
|
||||||
|
// Fallback к известным сетям
|
||||||
|
supportedChains = [11155111, 17000, 421614, 84532];
|
||||||
|
console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allProposals = [];
|
||||||
|
|
||||||
|
// Ищем предложения во всех поддерживаемых сетях
|
||||||
|
for (const chainId of supportedChains) {
|
||||||
|
try {
|
||||||
|
console.log(`[DLE Proposals] Поиск предложений в сети ${chainId}...`);
|
||||||
|
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
console.log(`[DLE Proposals] RPC URL для сети ${chainId} не найден, пропускаем`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
// ABI для чтения предложений (используем правильные функции из смарт-контракта)
|
// ABI для чтения предложений (используем getProposalSummary для мультиконтрактов)
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
"function proposals(uint256) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, bytes memory operation, uint256 governanceChainId, uint256 snapshotTimepoint)",
|
"function getProposalSummary(uint256 _proposalId) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targetChains)",
|
||||||
"function quorumPercentage() external view returns (uint256)",
|
"function quorumPercentage() external view returns (uint256)",
|
||||||
"function getPastTotalSupply(uint256 timepoint) external view returns (uint256)",
|
"function getPastTotalSupply(uint256 timepoint) external view returns (uint256)",
|
||||||
|
"function totalSupply() external view returns (uint256)",
|
||||||
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -58,11 +127,9 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
|
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Найдено событий ProposalCreated: ${events.length}`);
|
console.log(`[DLE Proposals] Найдено событий ProposalCreated в сети ${chainId}: ${events.length}`);
|
||||||
console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
||||||
|
|
||||||
const proposals = [];
|
|
||||||
|
|
||||||
// Читаем информацию о каждом предложении
|
// Читаем информацию о каждом предложении
|
||||||
for (let i = 0; i < events.length; i++) {
|
for (let i = 0; i < events.length; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -70,7 +137,7 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
|
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
|
||||||
|
|
||||||
// Пробуем несколько раз для новых предложений
|
// Пробуем несколько раз для новых предложений
|
||||||
let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired;
|
let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired, currentTotalSupply, quorumPct;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 1;
|
const maxRetries = 1;
|
||||||
|
|
||||||
@@ -81,21 +148,36 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
isPassed = result.passed;
|
isPassed = result.passed;
|
||||||
quorumReached = result.quorumReached;
|
quorumReached = result.quorumReached;
|
||||||
|
|
||||||
// Получаем данные о голосах из структуры Proposal
|
// Получаем данные о голосах из структуры Proposal (включая мультиконтрактные поля)
|
||||||
try {
|
try {
|
||||||
const proposalData = await dle.proposals(proposalId);
|
const proposalData = await dle.getProposalSummary(proposalId);
|
||||||
forVotes = Number(proposalData.forVotes);
|
forVotes = Number(proposalData.forVotes);
|
||||||
againstVotes = Number(proposalData.againstVotes);
|
againstVotes = Number(proposalData.againstVotes);
|
||||||
|
|
||||||
// Вычисляем требуемый кворум
|
// Вычисляем требуемый кворум
|
||||||
const quorumPct = Number(await dle.quorumPercentage());
|
quorumPct = Number(await dle.quorumPercentage());
|
||||||
const pastSupply = Number(await dle.getPastTotalSupply(proposalData.snapshotTimepoint));
|
const pastSupply = Number(await dle.getPastTotalSupply(proposalData.snapshotTimepoint));
|
||||||
quorumRequired = Math.floor((pastSupply * quorumPct) / 100);
|
quorumRequired = Math.floor((pastSupply * quorumPct) / 100);
|
||||||
|
|
||||||
|
// Получаем текущий totalSupply для отображения
|
||||||
|
currentTotalSupply = Number(await dle.totalSupply());
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Кворум для предложения ${proposalId}:`, {
|
||||||
|
quorumPercentage: quorumPct,
|
||||||
|
pastSupply: pastSupply,
|
||||||
|
quorumRequired: quorumRequired,
|
||||||
|
quorumPercentageFormatted: `${quorumPct}%`,
|
||||||
|
snapshotTimepoint: proposalData.snapshotTimepoint,
|
||||||
|
pastSupplyFormatted: `${(pastSupply / 10**18).toFixed(2)} DLE`,
|
||||||
|
quorumRequiredFormatted: `${(quorumRequired / 10**18).toFixed(2)} DLE`
|
||||||
|
});
|
||||||
} catch (voteError) {
|
} catch (voteError) {
|
||||||
console.log(`[DLE Proposals] Ошибка получения голосов для предложения ${proposalId}:`, voteError.message);
|
console.log(`[DLE Proposals] Ошибка получения голосов для предложения ${proposalId}:`, voteError.message);
|
||||||
forVotes = 0;
|
forVotes = 0;
|
||||||
againstVotes = 0;
|
againstVotes = 0;
|
||||||
quorumRequired = 0;
|
quorumRequired = 0;
|
||||||
|
currentTotalSupply = 0;
|
||||||
|
quorumPct = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
break; // Успешно прочитали
|
break; // Успешно прочитали
|
||||||
@@ -122,8 +204,53 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
initiator: events[i].args.initiator
|
initiator: events[i].args.initiator
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Фильтруем предложения по времени - только за последние 30 дней
|
||||||
|
const block = await provider.getBlock(events[i].blockNumber);
|
||||||
|
const proposalTime = block.timestamp;
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const thirtyDaysAgo = currentTime - (30 * 24 * 60 * 60); // 30 дней назад
|
||||||
|
|
||||||
|
if (proposalTime < thirtyDaysAgo) {
|
||||||
|
console.log(`[DLE Proposals] Пропускаем старое предложение ${proposalId} (${new Date(proposalTime * 1000).toISOString()})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем все предложения, включая выполненные и отмененные
|
||||||
|
// Согласно контракту: 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
|
||||||
|
// Убрали фильтрацию выполненных и отмененных предложений для отображения в UI
|
||||||
|
|
||||||
|
// Создаем уникальный ID, включающий chainId
|
||||||
|
const uniqueId = `${chainId}-${proposalId}`;
|
||||||
|
|
||||||
|
// Получаем мультиконтрактные данные из proposalData (если доступны)
|
||||||
|
let operation = null;
|
||||||
|
let governanceChainId = null;
|
||||||
|
let targetChains = [];
|
||||||
|
let decodedOperation = null;
|
||||||
|
let operationDescription = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proposalData = await dle.getProposalSummary(proposalId);
|
||||||
|
governanceChainId = Number(proposalData.governanceChainId);
|
||||||
|
targetChains = proposalData.targetChains.map(chain => Number(chain));
|
||||||
|
|
||||||
|
// Получаем operation из отдельного вызова (если нужно)
|
||||||
|
// operation не возвращается в getProposalSummary, но это не критично для мультиконтрактов
|
||||||
|
operation = null; // Пока не реализовано
|
||||||
|
|
||||||
|
// Декодируем операцию (если доступна)
|
||||||
|
if (operation && operation !== '0x') {
|
||||||
|
const { decodeOperation, formatOperation } = require('../utils/operationDecoder');
|
||||||
|
decodedOperation = decodeOperation(operation);
|
||||||
|
operationDescription = formatOperation(decodedOperation);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[DLE Proposals] Не удалось получить мультиконтрактные данные для предложения ${proposalId}:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
const proposalInfo = {
|
const proposalInfo = {
|
||||||
id: Number(proposalId),
|
id: Number(proposalId),
|
||||||
|
uniqueId: uniqueId,
|
||||||
description: events[i].args.description,
|
description: events[i].args.description,
|
||||||
state: Number(proposalState),
|
state: Number(proposalState),
|
||||||
isPassed: isPassed,
|
isPassed: isPassed,
|
||||||
@@ -131,12 +258,32 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
forVotes: Number(forVotes),
|
forVotes: Number(forVotes),
|
||||||
againstVotes: Number(againstVotes),
|
againstVotes: Number(againstVotes),
|
||||||
quorumRequired: Number(quorumRequired),
|
quorumRequired: Number(quorumRequired),
|
||||||
|
totalSupply: Number(currentTotalSupply || 0), // Добавляем totalSupply
|
||||||
|
contractQuorumPercentage: Number(quorumPct), // Добавляем процент кворума из контракта
|
||||||
initiator: events[i].args.initiator,
|
initiator: events[i].args.initiator,
|
||||||
blockNumber: events[i].blockNumber,
|
blockNumber: events[i].blockNumber,
|
||||||
transactionHash: events[i].transactionHash
|
transactionHash: events[i].transactionHash,
|
||||||
|
chainId: chainId, // Добавляем информацию о сети
|
||||||
|
timestamp: proposalTime,
|
||||||
|
createdAt: new Date(proposalTime * 1000).toISOString(),
|
||||||
|
executed: Number(proposalState) === 3, // 3 = Executed
|
||||||
|
canceled: Number(proposalState) === 4, // 4 = Canceled
|
||||||
|
// Мультиконтрактные поля
|
||||||
|
operation: operation,
|
||||||
|
governanceChainId: governanceChainId,
|
||||||
|
targetChains: targetChains,
|
||||||
|
isMultichain: targetChains && targetChains.length > 0,
|
||||||
|
decodedOperation: decodedOperation,
|
||||||
|
operationDescription: operationDescription
|
||||||
};
|
};
|
||||||
|
|
||||||
proposals.push(proposalInfo);
|
// Проверяем, нет ли уже такого предложения (по уникальному ID)
|
||||||
|
const existingProposal = allProposals.find(p => p.uniqueId === uniqueId);
|
||||||
|
if (!existingProposal) {
|
||||||
|
allProposals.push(proposalInfo);
|
||||||
|
} else {
|
||||||
|
console.log(`[DLE Proposals] Пропускаем дубликат предложения ${uniqueId}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[DLE Proposals] Ошибка при чтении предложения ${i}:`, error.message);
|
console.log(`[DLE Proposals] Ошибка при чтении предложения ${i}:`, error.message);
|
||||||
|
|
||||||
@@ -150,16 +297,29 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем по ID предложения (новые сверху)
|
console.log(`[DLE Proposals] Найдено предложений в сети ${chainId}: ${events.length}`);
|
||||||
proposals.sort((a, b) => b.id - a.id);
|
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Найдено предложений: ${proposals.length}`);
|
} catch (error) {
|
||||||
|
console.log(`[DLE Proposals] Ошибка при поиске предложений в сети ${chainId}:`, error.message);
|
||||||
|
// Продолжаем с следующей сетью
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по времени создания (новые сверху), затем по ID
|
||||||
|
allProposals.sort((a, b) => {
|
||||||
|
if (a.timestamp !== b.timestamp) {
|
||||||
|
return b.timestamp - a.timestamp;
|
||||||
|
}
|
||||||
|
return b.id - a.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Найдено предложений: ${allProposals.length}`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
proposals: proposals,
|
proposals: allProposals,
|
||||||
totalCount: proposals.length
|
totalCount: allProposals.length
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,8 +346,41 @@ router.post('/get-proposal-info', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Proposals] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[DLE Proposals] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
// Получаем RPC URL для Sepolia
|
// Определяем корректную сеть для данного адреса
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -864,6 +1057,9 @@ router.post('/vote-proposal', async (req, res) => {
|
|||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Пропускаем проверку hasVoted - функция не существует в контракте
|
||||||
|
console.log(`[DLE Proposals] Пропускаем проверку hasVoted - полагаемся на смарт-контракт`);
|
||||||
|
|
||||||
// Подготавливаем данные для транзакции (не отправляем)
|
// Подготавливаем данные для транзакции (не отправляем)
|
||||||
const txData = await dle.vote.populateTransaction(proposalId, support);
|
const txData = await dle.vote.populateTransaction(proposalId, support);
|
||||||
|
|
||||||
@@ -889,6 +1085,53 @@ router.post('/vote-proposal', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Проверить статус голосования пользователя
|
||||||
|
router.post('/check-vote-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId, voterAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined || !voterAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Необходимы dleAddress, proposalId и voterAddress'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Проверка статуса голосования для ${voterAddress} по предложению ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'RPC URL для Sepolia не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// Функция hasVoted не существует в контракте DLE
|
||||||
|
console.log(`[DLE Proposals] Функция hasVoted не поддерживается в контракте DLE`);
|
||||||
|
|
||||||
|
const hasVoted = false; // Всегда возвращаем false, так как функция не существует
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
hasVoted: hasVoted,
|
||||||
|
voterAddress: voterAddress,
|
||||||
|
proposalId: proposalId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Proposals] Ошибка при проверке статуса голосования:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при проверке статуса голосования: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Endpoint для отслеживания подтверждения транзакций голосования
|
// Endpoint для отслеживания подтверждения транзакций голосования
|
||||||
router.post('/track-vote-transaction', async (req, res) => {
|
router.post('/track-vote-transaction', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -29,7 +29,41 @@ router.post('/get-token-balance', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Tokens] Получение баланса токенов для аккаунта: ${account} в DLE: ${dleAddress}`);
|
console.log(`[DLE Tokens] Получение баланса токенов для аккаунта: ${account} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -81,7 +115,41 @@ router.post('/get-total-supply', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Tokens] Получение общего предложения токенов для DLE: ${dleAddress}`);
|
console.log(`[DLE Tokens] Получение общего предложения токенов для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -132,7 +200,41 @@ router.post('/get-token-holders', async (req, res) => {
|
|||||||
|
|
||||||
console.log(`[DLE Tokens] Получение держателей токенов для DLE: ${dleAddress}`);
|
console.log(`[DLE Tokens] Получение держателей токенов для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// Определяем корректную сеть для данного адреса
|
||||||
|
let rpcUrl, targetChainId;
|
||||||
|
let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем поддерживаемые сети из параметров деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
candidateChainIds = params.supportedChainIds || candidateChainIds;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
rpcUrl = url;
|
||||||
|
targetChainId = cid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось найти сеть, где по адресу есть контракт'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const DLEV2Service = require('../services/dleV2Service');
|
const UnifiedDeploymentService = require('../services/unifiedDeploymentService');
|
||||||
const dleV2Service = new DLEV2Service();
|
const unifiedDeploymentService = new UnifiedDeploymentService();
|
||||||
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');
|
||||||
@@ -38,7 +38,7 @@ async function executeDeploymentInBackground(deploymentId, dleParams) {
|
|||||||
deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info');
|
deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info');
|
||||||
|
|
||||||
// Выполняем деплой с передачей deploymentId для WebSocket обновлений
|
// Выполняем деплой с передачей deploymentId для WebSocket обновлений
|
||||||
const result = await dleV2Service.createDLE(dleParams, deploymentId);
|
const result = await unifiedDeploymentService.createDLE(dleParams, deploymentId);
|
||||||
|
|
||||||
// Завершаем успешно
|
// Завершаем успешно
|
||||||
deploymentTracker.completeDeployment(deploymentId, result.data);
|
deploymentTracker.completeDeployment(deploymentId, result.data);
|
||||||
@@ -114,7 +114,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
|
|||||||
*/
|
*/
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const dles = dleV2Service.getAllDLEs();
|
const dles = await unifiedDeploymentService.getAllDeployments();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -490,13 +490,8 @@ router.get('/verify/status/:address', auth.requireAuth, async (req, res) => {
|
|||||||
router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { address } = req.params;
|
const { address } = req.params;
|
||||||
let { etherscanApiKey } = req.body || {};
|
const ApiKeyManager = require('../utils/apiKeyManager');
|
||||||
if (!etherscanApiKey) {
|
const etherscanApiKey = ApiKeyManager.getEtherscanApiKey({}, req.body);
|
||||||
try {
|
|
||||||
const { getSecret } = require('../services/secretStore');
|
|
||||||
etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY');
|
|
||||||
} catch(_) {}
|
|
||||||
}
|
|
||||||
const data = verificationStore.read(address);
|
const data = verificationStore.read(address);
|
||||||
if (!data || !data.chains) return res.json({ success: true, data });
|
if (!data || !data.chains) return res.json({ success: true, data });
|
||||||
|
|
||||||
@@ -504,7 +499,7 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy
|
|||||||
const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || ''));
|
const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || ''));
|
||||||
if (needResubmit && etherscanApiKey) {
|
if (needResubmit && etherscanApiKey) {
|
||||||
// Найти карточку DLE
|
// Найти карточку DLE
|
||||||
const list = dleV2Service.getAllDLEs();
|
const list = unifiedDeploymentService.getAllDLEs();
|
||||||
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
||||||
if (card) {
|
if (card) {
|
||||||
const deployParams = {
|
const deployParams = {
|
||||||
@@ -520,11 +515,11 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy
|
|||||||
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
|
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
|
||||||
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
|
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
|
||||||
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
|
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
|
||||||
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
|
currentChainId: card.governanceSettings?.currentChainId || 1 // governance chain, не первая сеть
|
||||||
};
|
};
|
||||||
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
||||||
try {
|
try {
|
||||||
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
await unifiedDeploymentService.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,12 +547,14 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy
|
|||||||
router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { address } = req.params;
|
const { address } = req.params;
|
||||||
const { etherscanApiKey } = req.body || {};
|
const ApiKeyManager = require('../utils/apiKeyManager');
|
||||||
if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) {
|
const etherscanApiKey = ApiKeyManager.getEtherscanApiKey({}, req.body);
|
||||||
|
|
||||||
|
if (!etherscanApiKey) {
|
||||||
return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' });
|
return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' });
|
||||||
}
|
}
|
||||||
// Найти карточку DLE по адресу
|
// Найти карточку DLE по адресу
|
||||||
const list = dleV2Service.getAllDLEs();
|
const list = unifiedDeploymentService.getAllDLEs();
|
||||||
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
||||||
if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' });
|
if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' });
|
||||||
|
|
||||||
@@ -575,13 +572,13 @@ router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, as
|
|||||||
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
|
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
|
||||||
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
|
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
|
||||||
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
|
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
|
||||||
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
|
currentChainId: card.governanceSettings?.currentChainId || 1 // governance chain, не первая сеть
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сформировать deployResult из карточки
|
// Сформировать deployResult из карточки
|
||||||
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
||||||
|
|
||||||
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
await unifiedDeploymentService.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
||||||
const updated = verificationStore.read(address);
|
const updated = verificationStore.read(address);
|
||||||
return res.json({ success: true, data: updated });
|
return res.json({ success: true, data: updated });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -597,7 +594,7 @@ router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) =
|
|||||||
if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) {
|
if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) {
|
||||||
return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' });
|
return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' });
|
||||||
}
|
}
|
||||||
const result = await dleV2Service.checkBalances(supportedChainIds, privateKey);
|
const result = await unifiedDeploymentService.checkBalances(supportedChainIds, privateKey);
|
||||||
return res.json({ success: true, data: result });
|
return res.json({ success: true, data: result });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(500).json({ success: false, message: e.message });
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ router.post('/deploy-module-from-db', async (req, res) => {
|
|||||||
process.env.PRIVATE_KEY = params.privateKey || params.private_key;
|
process.env.PRIVATE_KEY = params.privateKey || params.private_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.etherscanApiKey || params.etherscan_api_key) {
|
const ApiKeyManager = require('../utils/apiKeyManager');
|
||||||
process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key;
|
const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params);
|
||||||
|
|
||||||
|
if (etherscanKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем деплой модулей через скрипт
|
// Запускаем деплой модулей через скрипт
|
||||||
|
|||||||
31
backend/scripts/clear-form-cache.js
Normal file
31
backend/scripts/clear-form-cache.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрипт для очистки кэша формы деплоя
|
||||||
|
* Удаляет localStorage данные, чтобы форма использовала свежие данные
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log('🧹 Очистка кэша формы деплоя...');
|
||||||
|
|
||||||
|
// Инструкции для пользователя
|
||||||
|
console.log(`
|
||||||
|
📋 ИНСТРУКЦИИ ДЛЯ ОЧИСТКИ КЭША:
|
||||||
|
|
||||||
|
1. Откройте браузер и перейдите на http://localhost:5173
|
||||||
|
2. Откройте Developer Tools (F12)
|
||||||
|
3. Перейдите во вкладку "Application" или "Storage"
|
||||||
|
4. Найдите "Local Storage" -> "http://localhost:5173"
|
||||||
|
5. Найдите ключ "dle_form_data" и удалите его
|
||||||
|
6. Или выполните в консоли браузера:
|
||||||
|
localStorage.removeItem('dle_form_data');
|
||||||
|
7. Перезагрузите страницу (F5)
|
||||||
|
|
||||||
|
🔧 АЛЬТЕРНАТИВНО - можно добавить кнопку "Очистить кэш" в форму:
|
||||||
|
- Добавить кнопку в DleDeployFormView.vue
|
||||||
|
- При клике выполнять: localStorage.removeItem('dle_form_data');
|
||||||
|
- Перезагружать страницу
|
||||||
|
|
||||||
|
✅ После очистки кэша форма будет использовать свежие данные
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('🏁 Инструкции выведены. Выполните очистку кэша в браузере.');
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"deploymentId": "modules-deploy-1758801398489",
|
|
||||||
"dleAddress": "0x40A99dBEC8D160a226E856d370dA4f3C67713940",
|
|
||||||
"dleName": "DLE Test",
|
|
||||||
"dleSymbol": "TOKEN",
|
|
||||||
"dleLocation": "101000, Москва, Москва, Тверская, 1, 101",
|
|
||||||
"dleJurisdiction": 643,
|
|
||||||
"dleCoordinates": "55.7614035,37.6342935",
|
|
||||||
"dleOktmo": "45000000",
|
|
||||||
"dleOkvedCodes": [
|
|
||||||
"62.01",
|
|
||||||
"63.11"
|
|
||||||
],
|
|
||||||
"dleKpp": "773009001",
|
|
||||||
"dleLogoURI": "/uploads/logos/default-token.svg",
|
|
||||||
"dleSupportedChainIds": [
|
|
||||||
11155111,
|
|
||||||
17000,
|
|
||||||
421614,
|
|
||||||
84532
|
|
||||||
],
|
|
||||||
"totalNetworks": 4,
|
|
||||||
"successfulNetworks": 4,
|
|
||||||
"modulesDeployed": [
|
|
||||||
"reader"
|
|
||||||
],
|
|
||||||
"networks": [
|
|
||||||
{
|
|
||||||
"chainId": 17000,
|
|
||||||
"rpcUrl": "https://ethereum-holesky.publicnode.com",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"type": "reader",
|
|
||||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
|
||||||
"success": true,
|
|
||||||
"verification": "success"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chainId": 84532,
|
|
||||||
"rpcUrl": "https://sepolia.base.org",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"type": "reader",
|
|
||||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
|
||||||
"success": true,
|
|
||||||
"verification": "success"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chainId": 421614,
|
|
||||||
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"type": "reader",
|
|
||||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
|
||||||
"success": true,
|
|
||||||
"verification": "success"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chainId": 11155111,
|
|
||||||
"rpcUrl": "https://1rpc.io/sepolia",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"type": "reader",
|
|
||||||
"address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545",
|
|
||||||
"success": true,
|
|
||||||
"verification": "success"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timestamp": "2025-09-25T11:56:38.490Z"
|
|
||||||
}
|
|
||||||
@@ -14,27 +14,13 @@
|
|||||||
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 logger = require('../../utils/logger');
|
||||||
|
const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createRPCConnection, sendTransactionWithRetry } = require('../../utils/deploymentUtils');
|
||||||
|
const { nonceManager } = require('../../utils/nonceManager');
|
||||||
|
|
||||||
// WebSocket сервис для отслеживания деплоя
|
// WebSocket сервис для отслеживания деплоя
|
||||||
const deploymentWebSocketService = require('../../services/deploymentWebSocketService');
|
const deploymentWebSocketService = require('../../services/deploymentWebSocketService');
|
||||||
|
|
||||||
// Подбираем безопасные 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 = {
|
const MODULE_CONFIGS = {
|
||||||
treasury: {
|
treasury: {
|
||||||
@@ -88,22 +74,29 @@ const MODULE_CONFIGS = {
|
|||||||
// Деплой модуля в одной сети с CREATE2
|
// Деплой модуля в одной сети с CREATE2
|
||||||
async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce, moduleInit, moduleType) {
|
async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce, moduleInit, moduleType) {
|
||||||
const { ethers } = hre;
|
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}...`);
|
// Используем новый менеджер RPC с retry логикой
|
||||||
|
const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, {
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
// 1) Выравнивание nonce до targetNonce нулевыми транзакциями (если нужно)
|
const net = network;
|
||||||
let current = await provider.getTransactionCount(wallet.address, 'pending');
|
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetNonce}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType}...`);
|
||||||
|
|
||||||
|
// 1) Используем NonceManager для правильного управления nonce
|
||||||
|
const { nonceManager } = require('../../utils/nonceManager');
|
||||||
|
const chainId = Number(net.chainId);
|
||||||
|
let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId);
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${chainId} current nonce=${current} target=${targetNonce}`);
|
||||||
|
|
||||||
if (current > targetNonce) {
|
if (current > targetNonce) {
|
||||||
throw new Error(`Current nonce ${current} > targetNonce ${targetNonce} on chainId=${Number(net.chainId)}`);
|
throw new Error(`Current nonce ${current} > targetNonce ${targetNonce} on chainId=${Number(net.chainId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current < targetNonce) {
|
if (current < targetNonce) {
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetNonce} (${targetNonce - current} transactions needed)`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetNonce} (${targetNonce - current} transactions needed)`);
|
||||||
|
|
||||||
// Используем burn address для более надежных транзакций
|
// Используем burn address для более надежных транзакций
|
||||||
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
||||||
@@ -123,15 +116,14 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
gasLimit,
|
gasLimit,
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
|
||||||
const txFill = await wallet.sendTransaction(txReq);
|
const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 });
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
|
||||||
await txFill.wait();
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
|
|
||||||
sent = true;
|
sent = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = e;
|
lastErr = e;
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`);
|
logger.info(`[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) {
|
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) {
|
||||||
gasLimit = 50000;
|
gasLimit = 50000;
|
||||||
@@ -139,8 +131,17 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) {
|
if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) {
|
||||||
|
// Сбрасываем кэш и получаем актуальный nonce
|
||||||
|
const { nonceManager } = require('../../utils/nonceManager');
|
||||||
|
nonceManager.resetNonce(wallet.address, Number(net.chainId));
|
||||||
current = await provider.getTransactionCount(wallet.address, 'pending');
|
current = await provider.getTransactionCount(wallet.address, 'pending');
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`);
|
||||||
|
|
||||||
|
// Если новый nonce больше целевого, это критическая ошибка
|
||||||
|
if (current > targetNonce) {
|
||||||
|
throw new Error(`Current nonce ${current} > target nonce ${targetNonce} on chainId=${Number(net.chainId)}. Cannot proceed with module deployment.`);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,20 +150,20 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
|
logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
|
||||||
throw lastErr || new Error('filler tx failed');
|
throw lastErr || new Error('filler tx failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
current++;
|
current++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Деплой модуля напрямую на согласованном nonce
|
// 2) Деплой модуля напрямую на согласованном nonce
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType} directly with nonce=${targetNonce}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType} directly with nonce=${targetNonce}`);
|
||||||
|
|
||||||
const feeOverrides = await getFeeOverrides(provider);
|
const feeOverrides = await getFeeOverrides(provider);
|
||||||
let gasLimit;
|
let gasLimit;
|
||||||
@@ -179,7 +180,7 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
const fallbackGas = maxByBalance > 2_000_000n ? 2_000_000n : (maxByBalance < 500_000n ? 500_000n : maxByBalance);
|
const fallbackGas = maxByBalance > 2_000_000n ? 2_000_000n : (maxByBalance < 500_000n ? 500_000n : maxByBalance);
|
||||||
gasLimit = est ? (est + est / 5n) : fallbackGas;
|
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()}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
gasLimit = 1_000_000n;
|
gasLimit = 1_000_000n;
|
||||||
}
|
}
|
||||||
@@ -189,41 +190,115 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
from: wallet.address,
|
from: wallet.address,
|
||||||
nonce: targetNonce
|
nonce: targetNonce
|
||||||
});
|
});
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} predicted ${moduleType} address=${predictedAddress}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} predicted ${moduleType} address=${predictedAddress}`);
|
||||||
|
|
||||||
// Проверяем, не развернут ли уже контракт
|
// Проверяем, не развернут ли уже контракт
|
||||||
const existingCode = await provider.getCode(predictedAddress);
|
const existingCode = await provider.getCode(predictedAddress);
|
||||||
if (existingCode && existingCode !== '0x') {
|
if (existingCode && existingCode !== '0x') {
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} already exists at predictedAddress, skip deploy`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} already exists at predictedAddress, skip deploy`);
|
||||||
return { address: predictedAddress, chainId: Number(net.chainId) };
|
return { address: predictedAddress, chainId: Number(net.chainId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Деплоим модуль
|
// Деплоим модуль с retry логикой для обработки race conditions
|
||||||
let tx;
|
let tx;
|
||||||
|
let deployAttempts = 0;
|
||||||
|
const maxDeployAttempts = 5;
|
||||||
|
|
||||||
|
while (deployAttempts < maxDeployAttempts) {
|
||||||
try {
|
try {
|
||||||
tx = await wallet.sendTransaction({
|
deployAttempts++;
|
||||||
|
|
||||||
|
// Получаем актуальный nonce прямо перед отправкой транзакции
|
||||||
|
const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, Number(net.chainId), { timeout: 15000, maxRetries: 5 });
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts}/${maxDeployAttempts} with current nonce=${currentNonce} (target was ${targetNonce})`);
|
||||||
|
|
||||||
|
const txData = {
|
||||||
data: moduleInit,
|
data: moduleInit,
|
||||||
nonce: targetNonce,
|
nonce: currentNonce,
|
||||||
gasLimit,
|
gasLimit,
|
||||||
...feeOverrides
|
...feeOverrides
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 });
|
||||||
|
tx = result.tx;
|
||||||
|
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy successful on attempt ${deployAttempts}`);
|
||||||
|
break; // Успешно отправили, выходим из цикла
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`);
|
const errorMsg = e?.message || e;
|
||||||
// Повторная попытка с обновленным nonce
|
logger.warn(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts} failed: ${errorMsg}`);
|
||||||
const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending');
|
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`);
|
// Проверяем, является ли это ошибкой nonce
|
||||||
tx = await wallet.sendTransaction({
|
if (String(errorMsg).toLowerCase().includes('nonce too low') && deployAttempts < maxDeployAttempts) {
|
||||||
data: moduleInit,
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce race condition detected, retrying...`);
|
||||||
nonce: updatedNonce,
|
|
||||||
gasLimit,
|
// Получаем актуальный nonce из сети
|
||||||
|
const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, Number(net.chainId), { timeout: 15000, maxRetries: 5 });
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce: ${currentNonce}, target: ${targetNonce}`);
|
||||||
|
|
||||||
|
// Если текущий nonce больше целевого, обновляем targetNonce
|
||||||
|
if (currentNonce > targetNonce) {
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce ${currentNonce} > target nonce ${targetNonce}, updating target`);
|
||||||
|
targetNonce = currentNonce;
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated targetNonce to: ${targetNonce}`);
|
||||||
|
|
||||||
|
// Короткая задержка перед следующей попыткой
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если текущий nonce меньше целевого, выравниваем его
|
||||||
|
if (currentNonce < targetNonce) {
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${currentNonce} to ${targetNonce}`);
|
||||||
|
|
||||||
|
// Выравниваем nonce нулевыми транзакциями
|
||||||
|
for (let i = currentNonce; i < targetNonce; i++) {
|
||||||
|
try {
|
||||||
|
const fillerTx = await wallet.sendTransaction({
|
||||||
|
to: '0x000000000000000000000000000000000000dEaD',
|
||||||
|
value: 0,
|
||||||
|
gasLimit: 21000,
|
||||||
|
nonce: i,
|
||||||
...feeOverrides
|
...feeOverrides
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fillerTx.wait();
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx ${i} confirmed`);
|
||||||
|
|
||||||
|
// Обновляем nonce в кэше
|
||||||
|
nonceManager.reserveNonce(wallet.address, Number(net.chainId), i);
|
||||||
|
|
||||||
|
} catch (fillerError) {
|
||||||
|
logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx ${i} failed: ${fillerError.message}`);
|
||||||
|
throw fillerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ВАЖНО: Обновляем targetNonce на актуальный nonce для следующей попытки
|
||||||
|
targetNonce = currentNonce;
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated targetNonce to: ${targetNonce}`);
|
||||||
|
|
||||||
|
// Короткая задержка перед следующей попыткой
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это не ошибка nonce или исчерпаны попытки, выбрасываем ошибку
|
||||||
|
if (deployAttempts >= maxDeployAttempts) {
|
||||||
|
throw new Error(`Module deployment failed after ${maxDeployAttempts} attempts: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для других ошибок делаем короткую задержку и пробуем снова
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rc = await tx.wait();
|
const rc = await tx.wait();
|
||||||
const deployedAddress = rc.contractAddress || predictedAddress;
|
const deployedAddress = rc.contractAddress || predictedAddress;
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployed at=${deployedAddress}`);
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployed at=${deployedAddress}`);
|
||||||
return { address: deployedAddress, chainId: Number(net.chainId) };
|
return { address: deployedAddress, chainId: Number(net.chainId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,11 +306,16 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce
|
|||||||
// Деплой всех модулей в одной сети
|
// Деплой всех модулей в одной сети
|
||||||
async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
||||||
const { ethers } = hre;
|
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(', ')}`);
|
// Используем новый менеджер RPC с retry логикой
|
||||||
|
const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, {
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const net = network;
|
||||||
|
|
||||||
|
logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying modules: ${modulesToDeploy.join(', ')}`);
|
||||||
|
|
||||||
const results = {};
|
const results = {};
|
||||||
|
|
||||||
@@ -248,14 +328,14 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
|||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Деплой модуля ${moduleType} в сети ${net.name || net.chainId}`);
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Деплой модуля ${moduleType} в сети ${net.name || net.chainId}`);
|
||||||
|
|
||||||
if (!MODULE_CONFIGS[moduleType]) {
|
if (!MODULE_CONFIGS[moduleType]) {
|
||||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`);
|
logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`);
|
||||||
results[moduleType] = { success: false, error: `Unknown module type: ${moduleType}` };
|
results[moduleType] = { success: false, error: `Unknown module type: ${moduleType}` };
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Неизвестный тип модуля: ${moduleType}`);
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Неизвестный тип модуля: ${moduleType}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!moduleInit) {
|
if (!moduleInit) {
|
||||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`);
|
logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`);
|
||||||
results[moduleType] = { success: false, error: `No init code for module: ${moduleType}` };
|
results[moduleType] = { success: false, error: `No init code for module: ${moduleType}` };
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Отсутствует код инициализации для модуля: ${moduleType}`);
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Отсутствует код инициализации для модуля: ${moduleType}`);
|
||||||
continue;
|
continue;
|
||||||
@@ -266,7 +346,7 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
|||||||
results[moduleType] = { ...result, success: true };
|
results[moduleType] = { ...result, success: true };
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleType} успешно задеплоен в сети ${net.name || net.chainId}: ${result.address}`);
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleType} успешно задеплоен в сети ${net.name || net.chainId}: ${result.address}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message);
|
logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message);
|
||||||
results[moduleType] = {
|
results[moduleType] = {
|
||||||
chainId: Number(net.chainId),
|
chainId: Number(net.chainId),
|
||||||
success: false,
|
success: false,
|
||||||
@@ -287,9 +367,10 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo
|
|||||||
async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (let i = 0; i < networks.length; i++) {
|
for (let i = 0; i < connections.length; i++) {
|
||||||
const rpcUrl = networks[i];
|
const connection = connections[i];
|
||||||
console.log(`[MODULES_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
const rpcUrl = connection.rpcUrl;
|
||||||
|
logger.info(`[MODULES_DBG] deploying modules to network ${i + 1}/${connections.length}: ${rpcUrl}`);
|
||||||
|
|
||||||
const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces);
|
const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
@@ -323,10 +404,10 @@ async function main() {
|
|||||||
// Проверяем, передан ли конкретный deploymentId
|
// Проверяем, передан ли конкретный deploymentId
|
||||||
const deploymentId = process.env.DEPLOYMENT_ID;
|
const deploymentId = process.env.DEPLOYMENT_ID;
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`);
|
logger.info(`🔍 Ищем параметры для deploymentId: ${deploymentId}`);
|
||||||
params = await deployParamsService.getDeployParams(deploymentId);
|
params = await deployParamsService.getDeployParams(deploymentId);
|
||||||
if (params) {
|
if (params) {
|
||||||
console.log('✅ Параметры загружены из базы данных по deploymentId');
|
logger.info('✅ Параметры загружены из базы данных по deploymentId');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`);
|
throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`);
|
||||||
}
|
}
|
||||||
@@ -335,7 +416,7 @@ async function main() {
|
|||||||
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
if (latestParams.length > 0) {
|
if (latestParams.length > 0) {
|
||||||
params = latestParams[0];
|
params = latestParams[0];
|
||||||
console.log('✅ Параметры загружены из базы данных (последние)');
|
logger.info('✅ Параметры загружены из базы данных (последние)');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Параметры деплоя не найдены в базе данных');
|
throw new Error('Параметры деплоя не найдены в базе данных');
|
||||||
}
|
}
|
||||||
@@ -343,18 +424,11 @@ async function main() {
|
|||||||
|
|
||||||
await deployParamsService.close();
|
await deployParamsService.close();
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message);
|
logger.error('❌ Критическая ошибка: не удалось загрузить параметры из БД:', dbError.message);
|
||||||
|
logger.error('❌ Система должна использовать только базу данных для хранения параметров деплоя');
|
||||||
// Fallback к файлу
|
throw new Error(`Не удалось загрузить параметры деплоя из БД: ${dbError.message}. Система должна использовать только базу данных.`);
|
||||||
const paramsPath = path.join(__dirname, './current-params.json');
|
|
||||||
if (!fs.existsSync(paramsPath)) {
|
|
||||||
throw new Error('Файл параметров не найден: ' + paramsPath);
|
|
||||||
}
|
}
|
||||||
|
logger.info('[MODULES_DBG] Загружены параметры:', {
|
||||||
params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'));
|
|
||||||
console.log('✅ Параметры загружены из файла');
|
|
||||||
}
|
|
||||||
console.log('[MODULES_DBG] Загружены параметры:', {
|
|
||||||
name: params.name,
|
name: params.name,
|
||||||
symbol: params.symbol,
|
symbol: params.symbol,
|
||||||
supportedChainIds: params.supportedChainIds,
|
supportedChainIds: params.supportedChainIds,
|
||||||
@@ -370,13 +444,13 @@ async function main() {
|
|||||||
let modulesToDeploy;
|
let modulesToDeploy;
|
||||||
if (moduleTypeFromArgs) {
|
if (moduleTypeFromArgs) {
|
||||||
modulesToDeploy = [moduleTypeFromArgs];
|
modulesToDeploy = [moduleTypeFromArgs];
|
||||||
console.log(`[MODULES_DBG] Деплой конкретного модуля: ${moduleTypeFromArgs}`);
|
logger.info(`[MODULES_DBG] Деплой конкретного модуля: ${moduleTypeFromArgs}`);
|
||||||
} else if (params.modulesToDeploy && params.modulesToDeploy.length > 0) {
|
} else if (params.modulesToDeploy && params.modulesToDeploy.length > 0) {
|
||||||
modulesToDeploy = params.modulesToDeploy;
|
modulesToDeploy = params.modulesToDeploy;
|
||||||
console.log(`[MODULES_DBG] Деплой модулей из БД: ${modulesToDeploy.join(', ')}`);
|
logger.info(`[MODULES_DBG] Деплой модулей из БД: ${modulesToDeploy.join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
modulesToDeploy = ['treasury', 'timelock', 'reader'];
|
modulesToDeploy = ['treasury', 'timelock', 'reader'];
|
||||||
console.log(`[MODULES_DBG] Деплой модулей по умолчанию: ${modulesToDeploy.join(', ')}`);
|
logger.info(`[MODULES_DBG] Деплой модулей по умолчанию: ${modulesToDeploy.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pk) throw new Error('PRIVATE_KEY not found in params or environment');
|
if (!pk) throw new Error('PRIVATE_KEY not found in params or environment');
|
||||||
@@ -384,11 +458,11 @@ async function main() {
|
|||||||
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');
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] Starting modules deployment to ${networks.length} networks`);
|
logger.info(`[MODULES_DBG] Starting modules deployment to ${networks.length} networks`);
|
||||||
console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`);
|
logger.info(`[MODULES_DBG] DLE Address: ${dleAddress}`);
|
||||||
console.log(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`);
|
logger.info(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`);
|
||||||
console.log(`[MODULES_DBG] Networks:`, networks);
|
logger.info(`[MODULES_DBG] Networks:`, networks);
|
||||||
console.log(`[MODULES_DBG] Using private key from: ${params.privateKey ? 'database' : 'environment'}`);
|
logger.info(`[MODULES_DBG] Using private key from: ${params.privateKey ? 'database' : 'environment'}`);
|
||||||
|
|
||||||
// Уведомляем WebSocket клиентов о начале деплоя
|
// Уведомляем WebSocket клиентов о начале деплоя
|
||||||
if (moduleTypeFromArgs) {
|
if (moduleTypeFromArgs) {
|
||||||
@@ -400,9 +474,11 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем API ключ Etherscan из базы данных, если доступен
|
// Устанавливаем API ключ Etherscan из базы данных, если доступен
|
||||||
if (params.etherscanApiKey || params.etherscan_api_key) {
|
const ApiKeyManager = require('../../utils/apiKeyManager');
|
||||||
process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key;
|
const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params);
|
||||||
console.log(`[MODULES_DBG] Using Etherscan API key from database`);
|
|
||||||
|
if (etherscanKey) {
|
||||||
|
logger.info(`[MODULES_DBG] Using Etherscan API key from database`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что все модули поддерживаются
|
// Проверяем, что все модули поддерживаются
|
||||||
@@ -420,28 +496,43 @@ async function main() {
|
|||||||
const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName);
|
const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName);
|
||||||
|
|
||||||
// Получаем аргументы конструктора для первой сети (для расчета init кода)
|
// Получаем аргументы конструктора для первой сети (для расчета init кода)
|
||||||
const firstProvider = new hre.ethers.JsonRpcProvider(networks[0]);
|
const firstConnection = await createRPCConnection(networks[0], pk, {
|
||||||
const firstWallet = new hre.ethers.Wallet(pk, firstProvider);
|
maxRetries: 3,
|
||||||
const firstNetwork = await firstProvider.getNetwork();
|
timeout: 30000
|
||||||
|
});
|
||||||
|
const firstProvider = firstConnection.provider;
|
||||||
|
const firstWallet = firstConnection.wallet;
|
||||||
|
const firstNetwork = firstConnection.network;
|
||||||
|
|
||||||
// Получаем аргументы конструктора
|
// Получаем аргументы конструктора
|
||||||
const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(firstNetwork.chainId), firstWallet.address);
|
const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(firstNetwork.chainId), firstWallet.address);
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] ${moduleType} constructor args:`, constructorArgs);
|
logger.info(`[MODULES_DBG] ${moduleType} constructor args:`, constructorArgs);
|
||||||
|
|
||||||
const deployTx = await ContractFactory.getDeployTransaction(...constructorArgs);
|
const deployTx = await ContractFactory.getDeployTransaction(...constructorArgs);
|
||||||
moduleInits[moduleType] = deployTx.data;
|
moduleInits[moduleType] = deployTx.data;
|
||||||
moduleInitCodeHashes[moduleType] = ethers.keccak256(deployTx.data);
|
moduleInitCodeHashes[moduleType] = ethers.keccak256(deployTx.data);
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] ${moduleType} init code prepared, hash: ${moduleInitCodeHashes[moduleType]}`);
|
logger.info(`[MODULES_DBG] ${moduleType} init code prepared, hash: ${moduleInitCodeHashes[moduleType]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подготовим провайдеры и вычислим общие nonce для каждого модуля
|
// Подготовим провайдеры и вычислим общие nonce для каждого модуля
|
||||||
const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u));
|
// Создаем RPC соединения с retry логикой
|
||||||
const wallets = providers.map(p => new hre.ethers.Wallet(pk, p));
|
logger.info(`[MODULES_DBG] Создаем RPC соединения для ${networks.length} сетей...`);
|
||||||
|
const connections = await createMultipleRPCConnections(networks, pk, {
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connections.length === 0) {
|
||||||
|
throw new Error('Не удалось установить ни одного RPC соединения');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[MODULES_DBG] ✅ Успешно подключились к ${connections.length}/${networks.length} сетям`);
|
||||||
|
|
||||||
const nonces = [];
|
const nonces = [];
|
||||||
for (let i = 0; i < providers.length; i++) {
|
for (const connection of connections) {
|
||||||
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
|
const n = await nonceManager.getNonce(connection.wallet.address, connection.rpcUrl, connection.chainId);
|
||||||
nonces.push(n);
|
nonces.push(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,48 +545,50 @@ async function main() {
|
|||||||
currentMaxNonce++; // каждый следующий модуль получает nonce +1
|
currentMaxNonce++; // каждый следующий модуль получает nonce +1
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] nonces=${JSON.stringify(nonces)} targetNonces=${JSON.stringify(targetNonces)}`);
|
logger.info(`[MODULES_DBG] nonces=${JSON.stringify(nonces)} targetNonces=${JSON.stringify(targetNonces)}`);
|
||||||
|
|
||||||
// ПАРАЛЛЕЛЬНЫЙ деплой всех модулей во всех сетях одновременно
|
// ПАРАЛЛЕЛЬНЫЙ деплой всех модулей во всех сетях одновременно
|
||||||
console.log(`[MODULES_DBG] Starting PARALLEL deployment of all modules to ${networks.length} networks`);
|
logger.info(`[MODULES_DBG] Starting PARALLEL deployment of all modules to ${networks.length} networks`);
|
||||||
|
|
||||||
const deploymentPromises = networks.map(async (rpcUrl, networkIndex) => {
|
const deploymentPromises = networks.map(async (rpcUrl, networkIndex) => {
|
||||||
console.log(`[MODULES_DBG] 🚀 Starting deployment to network ${networkIndex + 1}/${networks.length}: ${rpcUrl}`);
|
logger.info(`[MODULES_DBG] 🚀 Starting deployment to network ${networkIndex + 1}/${networks.length}: ${rpcUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем chainId динамически из сети
|
// Получаем chainId динамически из сети с retry логикой
|
||||||
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
|
const { provider, network } = await createRPCConnection(rpcUrl, pk, {
|
||||||
const network = await provider.getNetwork();
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
const chainId = Number(network.chainId);
|
const chainId = Number(network.chainId);
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] 📡 Network ${networkIndex + 1} chainId: ${chainId}`);
|
logger.info(`[MODULES_DBG] 📡 Network ${networkIndex + 1} chainId: ${chainId}`);
|
||||||
|
|
||||||
const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces);
|
const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces);
|
||||||
console.log(`[MODULES_DBG] ✅ Network ${networkIndex + 1} (chainId: ${chainId}) deployment SUCCESS`);
|
logger.info(`[MODULES_DBG] ✅ Network ${networkIndex + 1} (chainId: ${chainId}) deployment SUCCESS`);
|
||||||
return { rpcUrl, chainId, ...result };
|
return { rpcUrl, chainId, ...result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MODULES_DBG] ❌ Network ${networkIndex + 1} deployment FAILED:`, error.message);
|
logger.error(`[MODULES_DBG] ❌ Network ${networkIndex + 1} deployment FAILED:`, error.message);
|
||||||
return { rpcUrl, error: error.message };
|
return { rpcUrl, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ждем завершения всех деплоев
|
// Ждем завершения всех деплоев
|
||||||
const deployResults = await Promise.all(deploymentPromises);
|
const deployResults = await Promise.all(deploymentPromises);
|
||||||
console.log(`[MODULES_DBG] All ${networks.length} deployments completed`);
|
logger.info(`[MODULES_DBG] All ${networks.length} deployments completed`);
|
||||||
|
|
||||||
// Логируем результаты деплоя для каждой сети
|
// Логируем результаты деплоя для каждой сети
|
||||||
deployResults.forEach((result, index) => {
|
deployResults.forEach((result, index) => {
|
||||||
if (result.modules) {
|
if (result.modules) {
|
||||||
console.log(`[MODULES_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS`);
|
logger.info(`[MODULES_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS`);
|
||||||
Object.entries(result.modules).forEach(([moduleType, moduleResult]) => {
|
Object.entries(result.modules).forEach(([moduleType, moduleResult]) => {
|
||||||
if (moduleResult.success) {
|
if (moduleResult.success) {
|
||||||
console.log(`[MODULES_DBG] ✅ ${moduleType}: ${moduleResult.address}`);
|
logger.info(`[MODULES_DBG] ✅ ${moduleType}: ${moduleResult.address}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MODULES_DBG] ❌ ${moduleType}: ${moduleResult.error}`);
|
logger.info(`[MODULES_DBG] ❌ ${moduleType}: ${moduleResult.error}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MODULES_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
|
logger.info(`[MODULES_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -506,38 +599,38 @@ async function main() {
|
|||||||
.map(r => r.modules[moduleType].address);
|
.map(r => r.modules[moduleType].address);
|
||||||
const uniqueAddresses = [...new Set(addresses)];
|
const uniqueAddresses = [...new Set(addresses)];
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] ${moduleType} addresses:`, addresses);
|
logger.info(`[MODULES_DBG] ${moduleType} addresses:`, addresses);
|
||||||
console.log(`[MODULES_DBG] ${moduleType} unique addresses:`, uniqueAddresses);
|
logger.info(`[MODULES_DBG] ${moduleType} unique addresses:`, uniqueAddresses);
|
||||||
|
|
||||||
if (uniqueAddresses.length > 1) {
|
if (uniqueAddresses.length > 1) {
|
||||||
console.error(`[MODULES_DBG] ERROR: ${moduleType} addresses are different across networks!`);
|
logger.error(`[MODULES_DBG] ERROR: ${moduleType} addresses are different across networks!`);
|
||||||
console.error(`[MODULES_DBG] addresses:`, uniqueAddresses);
|
logger.error(`[MODULES_DBG] addresses:`, uniqueAddresses);
|
||||||
throw new Error(`Nonce alignment failed for ${moduleType} - addresses are different`);
|
throw new Error(`Nonce alignment failed for ${moduleType} - addresses are different`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uniqueAddresses.length === 0) {
|
if (uniqueAddresses.length === 0) {
|
||||||
console.error(`[MODULES_DBG] ERROR: No successful ${moduleType} deployments!`);
|
logger.error(`[MODULES_DBG] ERROR: No successful ${moduleType} deployments!`);
|
||||||
throw new Error(`No successful ${moduleType} deployments`);
|
throw new Error(`No successful ${moduleType} deployments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]);
|
logger.info(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Верификация во всех сетях через отдельный скрипт
|
// Верификация во всех сетях через отдельный скрипт
|
||||||
console.log(`[MODULES_DBG] Starting verification in all networks...`);
|
logger.info(`[MODULES_DBG] Starting verification in all networks...`);
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Начало верификации модулей во всех сетях...');
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Начало верификации модулей во всех сетях...');
|
||||||
|
|
||||||
// Запускаем верификацию модулей через существующий скрипт
|
// Запускаем верификацию модулей через существующий скрипт
|
||||||
try {
|
try {
|
||||||
const { verifyModules } = require('../verify-with-hardhat-v2');
|
const { verifyModules } = require('../verify-with-hardhat-v2');
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] Запускаем верификацию модулей...`);
|
logger.info(`[MODULES_DBG] Запускаем верификацию модулей...`);
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Верификация контрактов в блокчейн-сканерах...');
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Верификация контрактов в блокчейн-сканерах...');
|
||||||
await verifyModules();
|
await verifyModules();
|
||||||
console.log(`[MODULES_DBG] Верификация модулей завершена`);
|
logger.info(`[MODULES_DBG] Верификация модулей завершена`);
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', 'Верификация модулей завершена успешно');
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', 'Верификация модулей завершена успешно');
|
||||||
} catch (verifyError) {
|
} catch (verifyError) {
|
||||||
console.log(`[MODULES_DBG] Ошибка при верификации модулей: ${verifyError.message}`);
|
logger.info(`[MODULES_DBG] Ошибка при верификации модулей: ${verifyError.message}`);
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Ошибка при верификации модулей: ${verifyError.message}`);
|
deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Ошибка при верификации модулей: ${verifyError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +655,7 @@ async function main() {
|
|||||||
}, {}) : {}
|
}, {}) : {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('MODULES_DEPLOY_RESULT', JSON.stringify(finalResults));
|
logger.info('MODULES_DEPLOY_RESULT', JSON.stringify(finalResults));
|
||||||
|
|
||||||
// Сохраняем результаты в отдельные файлы для каждого модуля
|
// Сохраняем результаты в отдельные файлы для каждого модуля
|
||||||
const dleDir = path.join(__dirname, '../contracts-data/modules');
|
const dleDir = path.join(__dirname, '../contracts-data/modules');
|
||||||
@@ -602,8 +695,10 @@ async function main() {
|
|||||||
const verification = verificationResult?.modules?.[moduleType] || 'unknown';
|
const verification = verificationResult?.modules?.[moduleType] || 'unknown';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
|
const { provider, network } = await createRPCConnection(rpcUrl, pk, {
|
||||||
const network = await provider.getNetwork();
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
moduleInfo.networks.push({
|
moduleInfo.networks.push({
|
||||||
chainId: Number(network.chainId),
|
chainId: Number(network.chainId),
|
||||||
@@ -614,7 +709,7 @@ async function main() {
|
|||||||
error: moduleResult?.error || null
|
error: moduleResult?.error || null
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MODULES_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message);
|
logger.error(`[MODULES_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message);
|
||||||
moduleInfo.networks.push({
|
moduleInfo.networks.push({
|
||||||
chainId: null,
|
chainId: null,
|
||||||
rpcUrl: rpcUrl,
|
rpcUrl: rpcUrl,
|
||||||
@@ -630,15 +725,15 @@ async function main() {
|
|||||||
const fileName = `${moduleType}-${dleAddress.toLowerCase()}.json`;
|
const fileName = `${moduleType}-${dleAddress.toLowerCase()}.json`;
|
||||||
const filePath = path.join(dleDir, fileName);
|
const filePath = path.join(dleDir, fileName);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(moduleInfo, null, 2));
|
fs.writeFileSync(filePath, JSON.stringify(moduleInfo, null, 2));
|
||||||
console.log(`[MODULES_DBG] ${moduleType} info saved to: ${filePath}`);
|
logger.info(`[MODULES_DBG] ${moduleType} info saved to: ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MODULES_DBG] All modules deployment completed!');
|
logger.info('[MODULES_DBG] All modules deployment completed!');
|
||||||
console.log(`[MODULES_DBG] Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`);
|
logger.info(`[MODULES_DBG] Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`);
|
||||||
console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`);
|
logger.info(`[MODULES_DBG] DLE Address: ${dleAddress}`);
|
||||||
console.log(`[MODULES_DBG] DLE Name: ${params.name}`);
|
logger.info(`[MODULES_DBG] DLE Name: ${params.name}`);
|
||||||
console.log(`[MODULES_DBG] DLE Symbol: ${params.symbol}`);
|
logger.info(`[MODULES_DBG] DLE Symbol: ${params.symbol}`);
|
||||||
console.log(`[MODULES_DBG] DLE Location: ${params.location}`);
|
logger.info(`[MODULES_DBG] DLE Location: ${params.location}`);
|
||||||
|
|
||||||
// Создаем сводный отчет о деплое
|
// Создаем сводный отчет о деплое
|
||||||
const summaryReport = {
|
const summaryReport = {
|
||||||
@@ -675,10 +770,10 @@ async function main() {
|
|||||||
// Сохраняем сводный отчет
|
// Сохраняем сводный отчет
|
||||||
const summaryPath = path.join(__dirname, '../contracts-data/modules-deploy-summary.json');
|
const summaryPath = path.join(__dirname, '../contracts-data/modules-deploy-summary.json');
|
||||||
fs.writeFileSync(summaryPath, JSON.stringify(summaryReport, null, 2));
|
fs.writeFileSync(summaryPath, JSON.stringify(summaryReport, null, 2));
|
||||||
console.log(`[MODULES_DBG] Сводный отчет сохранен: ${summaryPath}`);
|
logger.info(`[MODULES_DBG] Сводный отчет сохранен: ${summaryPath}`);
|
||||||
|
|
||||||
// Уведомляем WebSocket клиентов о завершении деплоя
|
// Уведомляем WebSocket клиентов о завершении деплоя
|
||||||
console.log(`[MODULES_DBG] finalResults:`, JSON.stringify(finalResults, null, 2));
|
logger.info(`[MODULES_DBG] finalResults:`, JSON.stringify(finalResults, null, 2));
|
||||||
|
|
||||||
const successfulModules = finalResults.reduce((acc, result) => {
|
const successfulModules = finalResults.reduce((acc, result) => {
|
||||||
if (result.modules) {
|
if (result.modules) {
|
||||||
@@ -694,14 +789,14 @@ async function main() {
|
|||||||
const successCount = Object.keys(successfulModules).length;
|
const successCount = Object.keys(successfulModules).length;
|
||||||
const totalCount = modulesToDeploy.length;
|
const totalCount = modulesToDeploy.length;
|
||||||
|
|
||||||
console.log(`[MODULES_DBG] successfulModules:`, successfulModules);
|
logger.info(`[MODULES_DBG] successfulModules:`, successfulModules);
|
||||||
console.log(`[MODULES_DBG] successCount: ${successCount}, totalCount: ${totalCount}`);
|
logger.info(`[MODULES_DBG] successCount: ${successCount}, totalCount: ${totalCount}`);
|
||||||
|
|
||||||
if (successCount === totalCount) {
|
if (successCount === totalCount) {
|
||||||
console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=true`);
|
logger.info(`[MODULES_DBG] Вызываем finishDeploymentSession с success=true`);
|
||||||
deploymentWebSocketService.finishDeploymentSession(dleAddress, true, `Деплой завершен успешно! Задеплоено ${successCount} из ${totalCount} модулей`);
|
deploymentWebSocketService.finishDeploymentSession(dleAddress, true, `Деплой завершен успешно! Задеплоено ${successCount} из ${totalCount} модулей`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=false`);
|
logger.info(`[MODULES_DBG] Вызываем finishDeploymentSession с success=false`);
|
||||||
deploymentWebSocketService.finishDeploymentSession(dleAddress, false, `Деплой завершен с ошибками. Задеплоено ${successCount} из ${totalCount} модулей`);
|
deploymentWebSocketService.finishDeploymentSession(dleAddress, false, `Деплой завершен с ошибками. Задеплоено ${successCount} из ${totalCount} модулей`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,4 +804,4 @@ async function main() {
|
|||||||
deploymentWebSocketService.notifyModulesUpdated(dleAddress);
|
deploymentWebSocketService.notifyModulesUpdated(dleAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => { console.error(e); process.exit(1); });
|
main().catch((e) => { logger.error(e); process.exit(1); });
|
||||||
|
|||||||
@@ -11,64 +11,108 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИЙ ЛОГ - СКРИПТ ЗАПУЩЕН!
|
||||||
|
console.log('[MULTI_DBG] 🚀 СКРИПТ DEPLOY-MULTICHAIN.JS ЗАПУЩЕН!');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📦 Импортируем hardhat...');
|
||||||
const hre = require('hardhat');
|
const hre = require('hardhat');
|
||||||
|
console.log('[MULTI_DBG] ✅ hardhat импортирован');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📦 Импортируем path...');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
console.log('[MULTI_DBG] ✅ path импортирован');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📦 Импортируем fs...');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
console.log('[MULTI_DBG] ✅ fs импортирован');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📦 Импортируем rpcProviderService...');
|
||||||
const { getRpcUrlByChainId } = require('../../services/rpcProviderService');
|
const { getRpcUrlByChainId } = require('../../services/rpcProviderService');
|
||||||
|
console.log('[MULTI_DBG] ✅ rpcProviderService импортирован');
|
||||||
|
|
||||||
// Подбираем безопасные gas/fee для разных сетей (включая L2)
|
console.log('[MULTI_DBG] 📦 Импортируем logger...');
|
||||||
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
|
const logger = require('../../utils/logger');
|
||||||
const fee = await provider.getFeeData();
|
console.log('[MULTI_DBG] ✅ logger импортирован');
|
||||||
const overrides = {};
|
|
||||||
const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))());
|
|
||||||
const minFee = (await (async () => hre.ethers.parseUnits(minFeeGwei.toString(), 'gwei'))());
|
|
||||||
if (fee.maxFeePerGas) {
|
|
||||||
overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas;
|
|
||||||
overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n)
|
|
||||||
? fee.maxPriorityFeePerGas
|
|
||||||
: minPriority;
|
|
||||||
} else if (fee.gasPrice) {
|
|
||||||
overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice;
|
|
||||||
}
|
|
||||||
return overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params) {
|
console.log('[MULTI_DBG] 📦 Импортируем deploymentUtils...');
|
||||||
|
const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createMultipleRPCConnections, sendTransactionWithRetry, createRPCConnection } = require('../../utils/deploymentUtils');
|
||||||
|
console.log('[MULTI_DBG] ✅ deploymentUtils импортирован');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📦 Импортируем nonceManager...');
|
||||||
|
const { nonceManager } = require('../../utils/nonceManager');
|
||||||
|
console.log('[MULTI_DBG] ✅ nonceManager импортирован');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 🎯 ВСЕ ИМПОРТЫ УСПЕШНЫ!');
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 🔍 ПРОВЕРЯЕМ ФУНКЦИИ...');
|
||||||
|
console.log('[MULTI_DBG] deployInNetwork:', typeof deployInNetwork);
|
||||||
|
console.log('[MULTI_DBG] main:', typeof main);
|
||||||
|
|
||||||
|
async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit, params) {
|
||||||
const { ethers } = hre;
|
const { ethers } = hre;
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
const wallet = new ethers.Wallet(pk, provider);
|
// Используем новый менеджер RPC с retry логикой
|
||||||
const net = await provider.getNetwork();
|
const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, {
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const net = network;
|
||||||
|
|
||||||
// DEBUG: базовая информация по сети
|
// DEBUG: базовая информация по сети
|
||||||
try {
|
try {
|
||||||
const calcInitHash = ethers.keccak256(dleInit);
|
const calcInitHash = ethers.keccak256(dleInit);
|
||||||
const saltLen = ethers.getBytes(salt).length;
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
|
logger.info(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`);
|
||||||
console.log(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`);
|
logger.info(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`);
|
||||||
console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`);
|
logger.info(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`);
|
||||||
console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`);
|
logger.info(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
|
||||||
console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`);
|
|
||||||
console.log(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[MULTI_DBG] precheck error', e?.message || e);
|
logger.error('[MULTI_DBG] precheck error', e?.message || e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Выравнивание nonce до targetDLENonce нулевыми транзакциями (если нужно)
|
// 1) Используем NonceManager для правильного управления nonce
|
||||||
let current = await provider.getTransactionCount(wallet.address, 'pending');
|
const chainId = Number(net.chainId);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetDLENonce}`);
|
let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId);
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${chainId} current nonce=${current} target=${targetDLENonce}`);
|
||||||
|
|
||||||
if (current > targetDLENonce) {
|
if (current > targetDLENonce) {
|
||||||
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
|
logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > targetDLENonce ${targetDLENonce} - waiting for sync`);
|
||||||
|
|
||||||
|
// Ждем синхронизации nonce (максимум 60 секунд с прогрессивной задержкой)
|
||||||
|
let waitTime = 0;
|
||||||
|
let checkInterval = 1000; // Начинаем с 1 секунды
|
||||||
|
|
||||||
|
while (current > targetDLENonce && waitTime < 60000) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
||||||
|
current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId);
|
||||||
|
waitTime += checkInterval;
|
||||||
|
|
||||||
|
// Прогрессивно увеличиваем интервал проверки
|
||||||
|
if (waitTime > 10000) checkInterval = 2000;
|
||||||
|
if (waitTime > 30000) checkInterval = 5000;
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} waiting for nonce sync: ${current} > ${targetDLENonce} (${waitTime}ms, next check in ${checkInterval}ms)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current > targetDLENonce) {
|
||||||
|
const errorMsg = `Nonce sync timeout: current ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}. This may indicate network issues or the wallet was used for other transactions.`;
|
||||||
|
logger.error(`[MULTI_DBG] ${errorMsg}`);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce sync completed: ${current} <= ${targetDLENonce}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current < targetDLENonce) {
|
if (current < targetDLENonce) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current < targetDLENonce) {
|
if (current < targetDLENonce) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
||||||
|
|
||||||
// Используем burn address для более надежных транзакций
|
// Используем burn address для более надежных транзакций
|
||||||
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
||||||
@@ -79,7 +123,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
let sent = false;
|
let sent = false;
|
||||||
let lastErr = null;
|
let lastErr = null;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3 && !sent; attempt++) {
|
for (let attempt = 0; attempt < 5 && !sent; attempt++) {
|
||||||
try {
|
try {
|
||||||
const txReq = {
|
const txReq = {
|
||||||
to: burnAddress, // отправляем на burn address вместо своего адреса
|
to: burnAddress, // отправляем на burn address вместо своего адреса
|
||||||
@@ -88,49 +132,87 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
gasLimit,
|
gasLimit,
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}/5`);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`);
|
||||||
const txFill = await wallet.sendTransaction(txReq);
|
const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 });
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
|
||||||
await txFill.wait();
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
|
|
||||||
sent = true;
|
sent = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = e;
|
lastErr = e;
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`);
|
const errorMsg = e?.message || e;
|
||||||
|
logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${errorMsg}`);
|
||||||
|
|
||||||
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) {
|
// Обработка специфических ошибок
|
||||||
gasLimit = 50000; // увеличиваем gas limit
|
if (String(errorMsg).toLowerCase().includes('intrinsic gas too low') && attempt < 4) {
|
||||||
|
gasLimit = Math.min(gasLimit * 2, 100000); // увеличиваем gas limit с ограничением
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} increased gas limit to ${gasLimit}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) {
|
if (String(errorMsg).toLowerCase().includes('nonce too low') && attempt < 4) {
|
||||||
// Обновляем nonce и пробуем снова
|
// Сбрасываем кэш nonce и получаем актуальный
|
||||||
current = await provider.getTransactionCount(wallet.address, 'pending');
|
nonceManager.resetNonce(wallet.address, chainId);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`);
|
const newNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 });
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce changed from ${current} to ${newNonce}`);
|
||||||
|
current = newNonce;
|
||||||
|
|
||||||
|
// Если новый nonce больше целевого, обновляем targetDLENonce
|
||||||
|
if (current > targetDLENonce) {
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > target nonce ${targetDLENonce}, updating target`);
|
||||||
|
targetDLENonce = current;
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
if (String(errorMsg).toLowerCase().includes('insufficient funds') && attempt < 4) {
|
||||||
|
logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} insufficient funds for nonce alignment`);
|
||||||
|
throw new Error(`Insufficient funds for nonce alignment on chainId=${Number(net.chainId)}. Please add more ETH to the wallet.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(errorMsg).toLowerCase().includes('network') && attempt < 4) {
|
||||||
|
logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} network error, retrying in ${(attempt + 1) * 2} seconds...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 2000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это последняя попытка, выбрасываем ошибку
|
||||||
|
if (attempt === 4) {
|
||||||
|
throw new Error(`Failed to send filler transaction after 5 attempts: ${errorMsg}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
|
logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
|
||||||
throw lastErr || new Error('filler tx failed');
|
throw lastErr || new Error('filler tx failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
current++;
|
current++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`);
|
|
||||||
|
// Зарезервируем nonce в NonceManager
|
||||||
|
nonceManager.reserveNonce(wallet.address, chainId, targetDLENonce);
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Деплой DLE напрямую на согласованном nonce
|
// 2) Проверяем баланс перед деплоем
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE directly with nonce=${targetDLENonce}`);
|
const balance = await provider.getBalance(wallet.address, 'latest');
|
||||||
|
const balanceEth = ethers.formatEther(balance);
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} wallet balance: ${balanceEth} ETH`);
|
||||||
|
|
||||||
|
if (balance < ethers.parseEther('0.01')) {
|
||||||
|
throw new Error(`Insufficient balance for deployment on chainId=${Number(net.chainId)}. Current: ${balanceEth} ETH, required: 0.01 ETH minimum`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Деплой DLE с актуальным nonce
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE with current nonce`);
|
||||||
|
|
||||||
const feeOverrides = await getFeeOverrides(provider);
|
const feeOverrides = await getFeeOverrides(provider);
|
||||||
let gasLimit;
|
let gasLimit;
|
||||||
@@ -147,90 +229,137 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
|
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
|
||||||
gasLimit = est ? (est + est / 5n) : fallbackGas;
|
gasLimit = est ? (est + est / 5n) : fallbackGas;
|
||||||
|
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
gasLimit = 3_000_000n;
|
gasLimit = 3_000_000n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вычисляем предсказанный адрес DLE
|
// Вычисляем предсказанный адрес DLE с целевым nonce (детерминированный деплой)
|
||||||
const predictedAddress = ethers.getCreateAddress({
|
let predictedAddress = ethers.getCreateAddress({
|
||||||
from: wallet.address,
|
from: wallet.address,
|
||||||
nonce: targetDLENonce
|
nonce: targetDLENonce
|
||||||
});
|
});
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress} (nonce=${targetDLENonce})`);
|
||||||
|
|
||||||
// Проверяем, не развернут ли уже контракт
|
// Проверяем, не развернут ли уже контракт
|
||||||
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`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`);
|
||||||
|
|
||||||
// Проверяем и инициализируем логотип для существующего контракта
|
// Проверяем и инициализируем логотип для существующего контракта
|
||||||
if (params.logoURI && params.logoURI !== '') {
|
if (params.logoURI && params.logoURI !== '') {
|
||||||
try {
|
try {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`);
|
||||||
const DLE = await hre.ethers.getContractFactory('DLE');
|
const DLE = await hre.ethers.getContractFactory('DLE');
|
||||||
const dleContract = DLE.attach(predictedAddress);
|
const dleContract = DLE.attach(predictedAddress);
|
||||||
|
|
||||||
const currentLogo = await dleContract.logoURI();
|
const currentLogo = await dleContract.logoURI();
|
||||||
if (currentLogo === '' || currentLogo === '0x') {
|
if (currentLogo === '' || currentLogo === '0x') {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI for existing contract: ${params.logoURI}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI for existing contract: ${params.logoURI}`);
|
||||||
const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides);
|
const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides);
|
||||||
await logoTx.wait();
|
await logoTx.wait();
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized for existing contract`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized for existing contract`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI already set: ${currentLogo}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI already set: ${currentLogo}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed for existing contract: ${error.message}`);
|
logger.info(`[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) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Деплоим DLE
|
// Деплоим DLE с retry логикой для обработки race conditions
|
||||||
let tx;
|
let tx;
|
||||||
|
let deployAttempts = 0;
|
||||||
|
const maxDeployAttempts = 5;
|
||||||
|
|
||||||
|
while (deployAttempts < maxDeployAttempts) {
|
||||||
try {
|
try {
|
||||||
tx = await wallet.sendTransaction({
|
deployAttempts++;
|
||||||
|
|
||||||
|
// Получаем актуальный nonce прямо перед отправкой транзакции
|
||||||
|
const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 });
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts}/${maxDeployAttempts} with current nonce=${currentNonce} (target was ${targetDLENonce})`);
|
||||||
|
|
||||||
|
const txData = {
|
||||||
data: dleInit,
|
data: dleInit,
|
||||||
nonce: targetDLENonce,
|
nonce: currentNonce,
|
||||||
gasLimit,
|
gasLimit,
|
||||||
...feeOverrides
|
...feeOverrides
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 });
|
||||||
|
tx = result.tx;
|
||||||
|
|
||||||
|
// Отмечаем транзакцию как pending в NonceManager
|
||||||
|
nonceManager.markTransactionPending(wallet.address, chainId, currentNonce, tx.hash);
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy successful on attempt ${deployAttempts}`);
|
||||||
|
break; // Успешно отправили, выходим из цикла
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`);
|
const errorMsg = e?.message || e;
|
||||||
// Повторная попытка с обновленным nonce
|
logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts} failed: ${errorMsg}`);
|
||||||
const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending');
|
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`);
|
// Проверяем, является ли это ошибкой nonce
|
||||||
tx = await wallet.sendTransaction({
|
if (String(errorMsg).toLowerCase().includes('nonce too low') && deployAttempts < maxDeployAttempts) {
|
||||||
data: dleInit,
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce race condition detected, retrying...`);
|
||||||
nonce: updatedNonce,
|
|
||||||
gasLimit,
|
// Получаем актуальный nonce из сети
|
||||||
...feeOverrides
|
const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 });
|
||||||
});
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce: ${currentNonce}, target was: ${targetDLENonce}`);
|
||||||
|
|
||||||
|
// Обновляем targetDLENonce на актуальный nonce
|
||||||
|
targetDLENonce = currentNonce;
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`);
|
||||||
|
|
||||||
|
// Короткая задержка перед следующей попыткой
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это не ошибка nonce или исчерпаны попытки, выбрасываем ошибку
|
||||||
|
if (deployAttempts >= maxDeployAttempts) {
|
||||||
|
throw new Error(`Deployment failed after ${maxDeployAttempts} attempts: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для других ошибок делаем короткую задержку и пробуем снова
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rc = await tx.wait();
|
const rc = await tx.wait();
|
||||||
|
|
||||||
|
// Отмечаем транзакцию как подтвержденную в NonceManager
|
||||||
|
nonceManager.markTransactionConfirmed(wallet.address, chainId, tx.hash);
|
||||||
const deployedAddress = rc.contractAddress || predictedAddress;
|
const deployedAddress = rc.contractAddress || predictedAddress;
|
||||||
|
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`);
|
// Проверяем, что адрес соответствует предсказанному
|
||||||
|
if (deployedAddress !== predictedAddress) {
|
||||||
|
logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ADDRESS MISMATCH! predicted=${predictedAddress} actual=${deployedAddress}`);
|
||||||
|
throw new Error(`Address mismatch: predicted ${predictedAddress} != actual ${deployedAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress} ✅`);
|
||||||
|
|
||||||
// Инициализация логотипа если он указан
|
// Инициализация логотипа если он указан
|
||||||
if (params.logoURI && params.logoURI !== '') {
|
if (params.logoURI && params.logoURI !== '') {
|
||||||
try {
|
try {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`);
|
||||||
const DLE = await hre.ethers.getContractFactory('DLE');
|
const DLE = await hre.ethers.getContractFactory('DLE');
|
||||||
const dleContract = DLE.attach(deployedAddress);
|
const dleContract = DLE.attach(deployedAddress);
|
||||||
|
|
||||||
const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides);
|
const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides);
|
||||||
await logoTx.wait();
|
await logoTx.wait();
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized successfully`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${error.message}`);
|
logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${error.message}`);
|
||||||
// Не прерываем деплой из-за ошибки логотипа
|
// Не прерываем деплой из-за ошибки логотипа
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} no logoURI specified, skipping initialization`);
|
logger.info(`[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) };
|
||||||
@@ -238,9 +367,34 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
console.log('[MULTI_DBG] 🚀 ВХОДИМ В ФУНКЦИЮ MAIN!');
|
||||||
const { ethers } = hre;
|
const { ethers } = hre;
|
||||||
|
console.log('[MULTI_DBG] ✅ ethers получен');
|
||||||
|
|
||||||
|
logger.info('[MULTI_DBG] 🚀 НАЧИНАЕМ ДЕПЛОЙ DLE КОНТРАКТА');
|
||||||
|
console.log('[MULTI_DBG] ✅ logger.info выполнен');
|
||||||
|
|
||||||
|
// Автоматически генерируем ABI и flattened контракт перед деплоем
|
||||||
|
logger.info('🔨 Генерация ABI файла...');
|
||||||
|
try {
|
||||||
|
const { generateABIFile } = require('../generate-abi');
|
||||||
|
generateABIFile();
|
||||||
|
logger.info('✅ ABI файл обновлен перед деплоем');
|
||||||
|
} catch (abiError) {
|
||||||
|
logger.warn('⚠️ Ошибка генерации ABI:', abiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('🔨 Генерация flattened контракта...');
|
||||||
|
try {
|
||||||
|
const { generateFlattened } = require('../generate-flattened');
|
||||||
|
await generateFlattened();
|
||||||
|
logger.info('✅ Flattened контракт обновлен перед деплоем');
|
||||||
|
} catch (flattenError) {
|
||||||
|
logger.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем параметры из базы данных или файла
|
// Загружаем параметры из базы данных или файла
|
||||||
|
console.log('[MULTI_DBG] 🔍 НАЧИНАЕМ ЗАГРУЗКУ ПАРАМЕТРОВ...');
|
||||||
let params;
|
let params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -251,10 +405,10 @@ async function main() {
|
|||||||
// Проверяем, передан ли конкретный deploymentId
|
// Проверяем, передан ли конкретный deploymentId
|
||||||
const deploymentId = process.env.DEPLOYMENT_ID;
|
const deploymentId = process.env.DEPLOYMENT_ID;
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`);
|
logger.info(`🔍 Ищем параметры для deploymentId: ${deploymentId}`);
|
||||||
params = await deployParamsService.getDeployParams(deploymentId);
|
params = await deployParamsService.getDeployParams(deploymentId);
|
||||||
if (params) {
|
if (params) {
|
||||||
console.log('✅ Параметры загружены из базы данных по deploymentId');
|
logger.info('✅ Параметры загружены из базы данных по deploymentId');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`);
|
throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`);
|
||||||
}
|
}
|
||||||
@@ -263,7 +417,7 @@ async function main() {
|
|||||||
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
if (latestParams.length > 0) {
|
if (latestParams.length > 0) {
|
||||||
params = latestParams[0];
|
params = latestParams[0];
|
||||||
console.log('✅ Параметры загружены из базы данных (последние)');
|
logger.info('✅ Параметры загружены из базы данных (последние)');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Параметры деплоя не найдены в базе данных');
|
throw new Error('Параметры деплоя не найдены в базе данных');
|
||||||
}
|
}
|
||||||
@@ -271,179 +425,197 @@ async function main() {
|
|||||||
|
|
||||||
await deployParamsService.close();
|
await deployParamsService.close();
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message);
|
logger.error('❌ Критическая ошибка: не удалось загрузить параметры из БД:', dbError.message);
|
||||||
|
throw new Error(`Деплой невозможен без параметров из БД: ${dbError.message}`);
|
||||||
// Fallback к файлу
|
|
||||||
const paramsPath = path.join(__dirname, './current-params.json');
|
|
||||||
if (!fs.existsSync(paramsPath)) {
|
|
||||||
throw new Error('Файл параметров не найден: ' + paramsPath);
|
|
||||||
}
|
}
|
||||||
|
logger.info('[MULTI_DBG] Загружены параметры:', {
|
||||||
params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'));
|
|
||||||
console.log('✅ Параметры загружены из файла');
|
|
||||||
}
|
|
||||||
console.log('[MULTI_DBG] Загружены параметры:', {
|
|
||||||
name: params.name,
|
name: params.name,
|
||||||
symbol: params.symbol,
|
symbol: params.symbol,
|
||||||
supportedChainIds: params.supportedChainIds,
|
supportedChainIds: params.supportedChainIds,
|
||||||
CREATE2_SALT: params.CREATE2_SALT
|
rpcUrls: params.rpcUrls || params.rpc_urls,
|
||||||
|
etherscanApiKey: params.etherscanApiKey || params.etherscan_api_key
|
||||||
});
|
});
|
||||||
|
|
||||||
const pk = params.private_key || process.env.PRIVATE_KEY;
|
const pk = params.private_key || process.env.PRIVATE_KEY;
|
||||||
const salt = params.CREATE2_SALT || params.create2_salt;
|
|
||||||
const networks = params.rpcUrls || params.rpc_urls || [];
|
const networks = params.rpcUrls || params.rpc_urls || [];
|
||||||
|
|
||||||
|
// Устанавливаем API ключи Etherscan для верификации
|
||||||
|
const ApiKeyManager = require('../../utils/apiKeyManager');
|
||||||
|
const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params);
|
||||||
|
|
||||||
|
if (!etherscanKey) {
|
||||||
|
logger.warn('[MULTI_DBG] ⚠️ Etherscan API ключ не найден - верификация будет пропущена');
|
||||||
|
logger.warn(`[MULTI_DBG] Доступные поля: ${Object.keys(params).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
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 (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('contracts/DLE.sol:DLE');
|
const DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE');
|
||||||
const dleConfig = {
|
|
||||||
name: params.name || '',
|
// Используем централизованный генератор параметров конструктора
|
||||||
symbol: params.symbol || '',
|
const { generateDeploymentArgs } = require('../../utils/constructorArgsGenerator');
|
||||||
location: params.location || '',
|
const { dleConfig, initializer } = generateDeploymentArgs(params);
|
||||||
coordinates: params.coordinates || '',
|
// Проверяем наличие поддерживаемых сетей
|
||||||
jurisdiction: params.jurisdiction || 0,
|
const supportedChainIds = params.supportedChainIds || [];
|
||||||
oktmo: params.oktmo || '',
|
if (supportedChainIds.length === 0) {
|
||||||
okvedCodes: params.okvedCodes || [],
|
throw new Error('Не указаны поддерживаемые сети (supportedChainIds)');
|
||||||
kpp: params.kpp ? BigInt(params.kpp) : 0n,
|
}
|
||||||
quorumPercentage: params.quorumPercentage || 51,
|
|
||||||
initialPartners: params.initialPartners || [],
|
// Создаем initCode для каждой сети отдельно
|
||||||
initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(10**18)),
|
const initCodes = {};
|
||||||
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
|
for (const chainId of supportedChainIds) {
|
||||||
};
|
const deployTx = await DLE.getDeployTransaction(dleConfig, initializer);
|
||||||
const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000");
|
initCodes[chainId] = deployTx.data;
|
||||||
const dleInit = deployTx.data;
|
}
|
||||||
const initCodeHash = ethers.keccak256(dleInit);
|
|
||||||
|
// Получаем initCodeHash из первого initCode (все должны быть одинаковые по структуре)
|
||||||
|
const firstChainId = supportedChainIds[0];
|
||||||
|
const firstInitCode = initCodes[firstChainId];
|
||||||
|
if (!firstInitCode) {
|
||||||
|
throw new Error(`InitCode не создан для первой сети: ${firstChainId}`);
|
||||||
|
}
|
||||||
|
const initCodeHash = ethers.keccak256(firstInitCode);
|
||||||
|
|
||||||
// DEBUG: глобальные значения
|
// DEBUG: глобальные значения
|
||||||
try {
|
try {
|
||||||
const saltLen = ethers.getBytes(salt).length;
|
logger.info(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`);
|
||||||
console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`);
|
logger.info(`[MULTI_DBG] GLOBAL firstInitCode.lenBytes=${ethers.getBytes(firstInitCode).length} head16=${firstInitCode.slice(0, 34)}...`);
|
||||||
console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`);
|
|
||||||
console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e);
|
logger.info('[MULTI_DBG] GLOBAL precheck error', e?.message || e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подготовим провайдеры и вычислим общий nonce для DLE с retry логикой
|
||||||
|
logger.info(`[MULTI_DBG] Создаем RPC соединения для ${networks.length} сетей...`);
|
||||||
|
const connections = await createMultipleRPCConnections(networks, pk, {
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connections.length === 0) {
|
||||||
|
throw new Error('Не удалось установить ни одного RPC соединения');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] ✅ Успешно подключились к ${connections.length}/${networks.length} сетям`);
|
||||||
|
|
||||||
|
// Очищаем старые pending транзакции для всех сетей
|
||||||
|
for (const connection of connections) {
|
||||||
|
const chainId = Number(connection.network.chainId);
|
||||||
|
nonceManager.clearOldPendingTransactions(connection.wallet.address, chainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подготовим провайдеры и вычислим общий nonce для DLE
|
|
||||||
const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u));
|
|
||||||
const wallets = providers.map(p => new hre.ethers.Wallet(pk, p));
|
|
||||||
const nonces = [];
|
const nonces = [];
|
||||||
for (let i = 0; i < providers.length; i++) {
|
for (const connection of connections) {
|
||||||
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
|
const n = await nonceManager.getNonce(connection.wallet.address, connection.rpcUrl, Number(connection.network.chainId));
|
||||||
nonces.push(n);
|
nonces.push(n);
|
||||||
}
|
}
|
||||||
const targetDLENonce = Math.max(...nonces);
|
const targetDLENonce = Math.max(...nonces);
|
||||||
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
|
logger.info(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
|
||||||
console.log(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks);
|
logger.info(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks);
|
||||||
|
|
||||||
// ПАРАЛЛЕЛЬНЫЙ деплой во всех сетях одновременно
|
// ПАРАЛЛЕЛЬНЫЙ деплой во всех успешных сетях одновременно
|
||||||
console.log(`[MULTI_DBG] Starting PARALLEL deployment to ${networks.length} networks`);
|
console.log(`[MULTI_DBG] 🚀 ДОШЛИ ДО ПАРАЛЛЕЛЬНОГО ДЕПЛОЯ!`);
|
||||||
|
logger.info(`[MULTI_DBG] Starting PARALLEL deployment to ${connections.length} successful networks`);
|
||||||
|
logger.info(`[MULTI_DBG] 🚀 ЗАПУСКАЕМ ЦИКЛ ДЕПЛОЯ!`);
|
||||||
|
|
||||||
const deploymentPromises = networks.map(async (rpcUrl, i) => {
|
const deploymentPromises = connections.map(async (connection, i) => {
|
||||||
console.log(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
const rpcUrl = connection.rpcUrl;
|
||||||
|
const chainId = Number(connection.network.chainId);
|
||||||
|
|
||||||
|
logger.info(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${connections.length}: ${rpcUrl} (chainId: ${chainId})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем chainId динамически из сети
|
// Получаем правильный initCode для этой сети
|
||||||
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
|
const networkInitCode = initCodes[chainId];
|
||||||
const network = await provider.getNetwork();
|
if (!networkInitCode) {
|
||||||
const chainId = Number(network.chainId);
|
throw new Error(`InitCode не найден для chainId: ${chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[MULTI_DBG] 📡 Network ${i + 1} chainId: ${chainId}`);
|
const r = await deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, networkInitCode, params);
|
||||||
|
logger.info(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`);
|
||||||
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params);
|
return { rpcUrl, chainId, address: r.address, chainId: r.chainId };
|
||||||
console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`);
|
|
||||||
return { rpcUrl, chainId, ...r };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message);
|
logger.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message);
|
||||||
return { rpcUrl, error: error.message };
|
return { rpcUrl, chainId, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ждем завершения всех деплоев
|
// Ждем завершения всех деплоев
|
||||||
const results = await Promise.all(deploymentPromises);
|
const results = await Promise.all(deploymentPromises);
|
||||||
console.log(`[MULTI_DBG] All ${networks.length} deployments completed`);
|
logger.info(`[MULTI_DBG] All ${networks.length} deployments completed`);
|
||||||
|
|
||||||
// Логируем результаты для каждой сети
|
// Логируем результаты для каждой сети
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.address) {
|
if (result.address) {
|
||||||
console.log(`[MULTI_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS: ${result.address}`);
|
logger.info(`[MULTI_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS: ${result.address}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
|
logger.info(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проверяем, что все адреса одинаковые
|
// Проверяем, что все адреса одинаковые (критично для детерминированного деплоя)
|
||||||
const addresses = results.map(r => r.address).filter(addr => addr);
|
const addresses = results.map(r => r.address).filter(addr => addr);
|
||||||
const uniqueAddresses = [...new Set(addresses)];
|
const uniqueAddresses = [...new Set(addresses)];
|
||||||
|
|
||||||
console.log('[MULTI_DBG] All addresses:', addresses);
|
logger.info('[MULTI_DBG] All addresses:', addresses);
|
||||||
console.log('[MULTI_DBG] Unique addresses:', uniqueAddresses);
|
logger.info('[MULTI_DBG] Unique addresses:', uniqueAddresses);
|
||||||
console.log('[MULTI_DBG] Results count:', results.length);
|
logger.info('[MULTI_DBG] Results count:', results.length);
|
||||||
console.log('[MULTI_DBG] Networks count:', networks.length);
|
logger.info('[MULTI_DBG] Networks count:', networks.length);
|
||||||
|
|
||||||
if (uniqueAddresses.length > 1) {
|
if (uniqueAddresses.length > 1) {
|
||||||
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
|
logger.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
|
||||||
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
|
logger.error('[MULTI_DBG] addresses:', uniqueAddresses);
|
||||||
throw new Error('Nonce alignment failed - addresses are different');
|
throw new Error('Nonce alignment failed - addresses are different');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uniqueAddresses.length === 0) {
|
if (uniqueAddresses.length === 0) {
|
||||||
console.error('[MULTI_DBG] ERROR: No successful deployments!');
|
logger.error('[MULTI_DBG] ERROR: No successful deployments!');
|
||||||
throw new Error('No successful deployments');
|
throw new Error('No successful deployments');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
|
logger.info('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
|
||||||
|
|
||||||
// Автоматическая верификация контрактов
|
// ВЫВОДИМ РЕЗУЛЬТАТ СРАЗУ ПОСЛЕ ДЕПЛОЯ (ПЕРЕД ВЕРИФИКАЦИЕЙ)!
|
||||||
let verificationResults = [];
|
console.log('[MULTI_DBG] 🎯 ДОШЛИ ДО ВЫВОДА РЕЗУЛЬТАТА!');
|
||||||
|
|
||||||
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,
|
||||||
verification: verificationResults[index] || 'failed'
|
verification: 'pending'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] 📊 finalResults:', JSON.stringify(finalResults, null, 2));
|
||||||
|
console.log('[MULTI_DBG] 🎯 ВЫВОДИМ MULTICHAIN_DEPLOY_RESULT!');
|
||||||
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
|
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
|
||||||
|
console.log('[MULTI_DBG] ✅ MULTICHAIN_DEPLOY_RESULT ВЫВЕДЕН!');
|
||||||
|
logger.info('[MULTI_DBG] DLE deployment completed successfully!');
|
||||||
|
|
||||||
console.log('[MULTI_DBG] DLE deployment completed successfully!');
|
// Верификация контрактов отключена
|
||||||
|
logger.info('[MULTI_DBG] Contract verification disabled - skipping verification step');
|
||||||
|
|
||||||
|
// Отмечаем все результаты как без верификации
|
||||||
|
const finalResultsWithVerification = results.map((result) => ({
|
||||||
|
...result,
|
||||||
|
verification: 'skipped'
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.info('[MULTI_DBG] Verification skipped - deployment completed successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => { console.error(e); process.exit(1); });
|
console.log('[MULTI_DBG] 🚀 ВЫЗЫВАЕМ MAIN()...');
|
||||||
|
main().catch((e) => {
|
||||||
|
console.log('[MULTI_DBG] ❌ ОШИБКА В MAIN:', e);
|
||||||
|
logger.error('[MULTI_DBG] ❌ Deployment failed:', e);
|
||||||
|
|
||||||
|
// Даже при ошибке выводим результат для анализа
|
||||||
|
const errorResult = {
|
||||||
|
error: e.message,
|
||||||
|
success: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stack: e.stack
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify([errorResult]));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
119
backend/scripts/generate-abi.js
Normal file
119
backend/scripts/generate-abi.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Автоматическая генерация ABI для фронтенда
|
||||||
|
* Извлекает ABI из скомпилированных артефактов Hardhat
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Пути к артефактам
|
||||||
|
const artifactsPath = path.join(__dirname, '../artifacts/contracts');
|
||||||
|
const frontendAbiPath = path.join(__dirname, '../../frontend/src/utils/dle-abi.js');
|
||||||
|
|
||||||
|
// Создаем директорию если она не существует
|
||||||
|
const frontendDir = path.dirname(frontendAbiPath);
|
||||||
|
if (!fs.existsSync(frontendDir)) {
|
||||||
|
fs.mkdirSync(frontendDir, { recursive: true });
|
||||||
|
console.log('✅ Создана директория:', frontendDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для извлечения ABI из артефакта
|
||||||
|
function extractABI(contractName) {
|
||||||
|
const artifactPath = path.join(artifactsPath, `${contractName}.sol`, `${contractName}.json`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(artifactPath)) {
|
||||||
|
console.log(`⚠️ Артефакт не найден: ${artifactPath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
||||||
|
return artifact.abi;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Ошибка чтения артефакта ${contractName}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для форматирования ABI в строку
|
||||||
|
function formatABI(abi) {
|
||||||
|
const functions = abi.filter(item => item.type === 'function');
|
||||||
|
const events = abi.filter(item => item.type === 'event');
|
||||||
|
|
||||||
|
let result = 'export const DLE_ABI = [\n';
|
||||||
|
|
||||||
|
// Функции
|
||||||
|
functions.forEach(func => {
|
||||||
|
const inputs = func.inputs.map(input => `${input.type} ${input.name}`).join(', ');
|
||||||
|
const outputs = func.outputs.map(output => output.type).join(', ');
|
||||||
|
const returns = outputs ? ` returns (${outputs})` : '';
|
||||||
|
|
||||||
|
result += ` "${func.type} ${func.name}(${inputs})${returns}",\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// События
|
||||||
|
events.forEach(event => {
|
||||||
|
const inputs = event.inputs.map(input => `${input.type} ${input.name}`).join(', ');
|
||||||
|
result += ` "event ${event.name}(${inputs})",\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
result += '];\n';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для генерации полного файла ABI
|
||||||
|
function generateABIFile() {
|
||||||
|
console.log('🔨 Генерация ABI файла...');
|
||||||
|
|
||||||
|
// Извлекаем ABI для DLE контракта
|
||||||
|
const dleABI = extractABI('DLE');
|
||||||
|
|
||||||
|
if (!dleABI) {
|
||||||
|
console.error('❌ Не удалось извлечь ABI для DLE контракта');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форматируем ABI
|
||||||
|
const formattedABI = formatABI(dleABI);
|
||||||
|
|
||||||
|
// Создаем полный файл
|
||||||
|
const fileContent = `/**
|
||||||
|
* ABI для DLE смарт-контракта
|
||||||
|
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||||
|
* Для обновления запустите: node backend/scripts/generate-abi.js
|
||||||
|
*
|
||||||
|
* Последнее обновление: ${new Date().toISOString()}
|
||||||
|
*/
|
||||||
|
|
||||||
|
${formattedABI}
|
||||||
|
|
||||||
|
// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ
|
||||||
|
export const DLE_DEACTIVATION_ABI = [
|
||||||
|
// Эти функции не существуют в контракте DLE
|
||||||
|
];
|
||||||
|
|
||||||
|
// ABI для токенов (базовые функции)
|
||||||
|
export const TOKEN_ABI = [
|
||||||
|
"function balanceOf(address owner) view returns (uint256)",
|
||||||
|
"function decimals() view returns (uint8)",
|
||||||
|
"function totalSupply() view returns (uint256)"
|
||||||
|
];
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Записываем файл
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(frontendAbiPath, fileContent, 'utf8');
|
||||||
|
console.log('✅ ABI файл успешно сгенерирован:', frontendAbiPath);
|
||||||
|
console.log(`📊 Функций: ${dleABI.filter(item => item.type === 'function').length}`);
|
||||||
|
console.log(`📊 Событий: ${dleABI.filter(item => item.type === 'event').length}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка записи ABI файла:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск генерации
|
||||||
|
if (require.main === module) {
|
||||||
|
generateABIFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateABIFile, extractABI, formatABI };
|
||||||
93
backend/scripts/generate-flattened.js
Normal file
93
backend/scripts/generate-flattened.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Автоматическая генерация flattened контракта для верификации
|
||||||
|
* Создает DLE_flattened.sol из DLE.sol с помощью hardhat flatten
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { spawn, execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Пути к файлам
|
||||||
|
const contractsDir = path.join(__dirname, '../contracts');
|
||||||
|
const dleContractPath = path.join(contractsDir, 'DLE.sol');
|
||||||
|
const flattenedPath = path.join(contractsDir, 'DLE_flattened.sol');
|
||||||
|
|
||||||
|
// Функция для генерации flattened контракта
|
||||||
|
function generateFlattened() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('🔨 Генерация flattened контракта...');
|
||||||
|
|
||||||
|
// Проверяем существование исходного контракта
|
||||||
|
if (!fs.existsSync(dleContractPath)) {
|
||||||
|
reject(new Error(`Исходный контракт не найден: ${dleContractPath}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем hardhat flatten с перенаправлением в файл
|
||||||
|
try {
|
||||||
|
console.log('🔨 Выполняем hardhat flatten...');
|
||||||
|
execSync(`npx hardhat flatten contracts/DLE.sol > "${flattenedPath}"`, {
|
||||||
|
cwd: path.join(__dirname, '..'),
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что файл создался
|
||||||
|
if (fs.existsSync(flattenedPath)) {
|
||||||
|
const stats = fs.statSync(flattenedPath);
|
||||||
|
console.log('✅ Flattened контракт создан:', flattenedPath);
|
||||||
|
console.log(`📊 Размер файла: ${(stats.size / 1024).toFixed(2)} KB`);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error('Файл не был создан'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка выполнения hardhat flatten:', error.message);
|
||||||
|
reject(new Error(`Ошибка flatten: ${error.message}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для проверки актуальности
|
||||||
|
function checkFlattenedFreshness() {
|
||||||
|
if (!fs.existsSync(flattenedPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(dleContractPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenedStats = fs.statSync(flattenedPath);
|
||||||
|
const contractStats = fs.statSync(dleContractPath);
|
||||||
|
|
||||||
|
// Flattened файл старше контракта
|
||||||
|
return flattenedStats.mtime >= contractStats.mtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная функция
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Проверка актуальности flattened контракта...');
|
||||||
|
|
||||||
|
const isFresh = checkFlattenedFreshness();
|
||||||
|
|
||||||
|
if (isFresh) {
|
||||||
|
console.log('✅ Flattened контракт актуален');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Flattened контракт устарел, генерируем новый...');
|
||||||
|
await generateFlattened();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка генерации flattened контракта:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск если вызван напрямую
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateFlattened, checkFlattenedFreshness };
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* Главный скрипт для запуска всех тестов
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Устанавливаем переменные окружения для Docker
|
|
||||||
process.env.OLLAMA_BASE_URL = 'http://ollama:11434';
|
|
||||||
process.env.OLLAMA_MODEL = 'qwen2.5:7b';
|
|
||||||
|
|
||||||
const aiQueueService = require('../services/ai-queue');
|
|
||||||
|
|
||||||
async function testQueueInDocker() {
|
|
||||||
// console.log('🐳 Тестирование AI очереди в Docker...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем инициализацию
|
|
||||||
// console.log('1. Проверка инициализации очереди...');
|
|
||||||
const stats = aiQueueService.getStats();
|
|
||||||
// console.log('✅ Очередь инициализирована:', stats.isInitialized);
|
|
||||||
// console.log('📊 Статистика:', {
|
|
||||||
totalProcessed: stats.totalProcessed,
|
|
||||||
totalFailed: stats.totalFailed,
|
|
||||||
currentQueueSize: stats.currentQueueSize,
|
|
||||||
runningTasks: stats.runningTasks
|
|
||||||
});
|
|
||||||
|
|
||||||
// Тестируем добавление задач
|
|
||||||
console.log('\n2. Тестирование добавления задач...');
|
|
||||||
|
|
||||||
const testTasks = [
|
|
||||||
{
|
|
||||||
message: 'Привет, как дела?',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'chat',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'user',
|
|
||||||
requestId: 'docker_test_1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Расскажи о погоде',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'analysis',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'user',
|
|
||||||
requestId: 'docker_test_2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Срочный вопрос!',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'urgent',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'admin',
|
|
||||||
requestId: 'docker_test_3'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < testTasks.length; i++) {
|
|
||||||
const task = testTasks[i];
|
|
||||||
console.log(` Добавляем задачу ${i + 1}: "${task.message}"`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await aiQueueService.addTask(task);
|
|
||||||
console.log(` ✅ Задача добавлена, ID: ${result.taskId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Ошибка добавления задачи: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждем обработки
|
|
||||||
console.log('\n3. Ожидание обработки задач...');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 15000));
|
|
||||||
|
|
||||||
// Проверяем статистику
|
|
||||||
console.log('\n4. Проверка статистики после обработки...');
|
|
||||||
const finalStats = aiQueueService.getStats();
|
|
||||||
console.log('📊 Финальная статистика:', {
|
|
||||||
totalProcessed: finalStats.totalProcessed,
|
|
||||||
totalFailed: finalStats.totalFailed,
|
|
||||||
currentQueueSize: finalStats.currentQueueSize,
|
|
||||||
runningTasks: finalStats.runningTasks,
|
|
||||||
averageProcessingTime: Math.round(finalStats.averageProcessingTime)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Тестируем управление очередью
|
|
||||||
console.log('\n5. Тестирование управления очередью...');
|
|
||||||
|
|
||||||
console.log(' Пауза очереди...');
|
|
||||||
aiQueueService.pause();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
console.log(' Возобновление очереди...');
|
|
||||||
aiQueueService.resume();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
console.log('\n✅ Тестирование завершено!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Ошибка тестирования:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск теста
|
|
||||||
if (require.main === module) {
|
|
||||||
testQueueInDocker().then(() => {
|
|
||||||
console.log('\n🏁 Тест завершен');
|
|
||||||
process.exit(0);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('💥 Критическая ошибка:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { testQueueInDocker };
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This software is proprietary and confidential.
|
|
||||||
* Unauthorized copying, modification, or distribution is prohibited.
|
|
||||||
*
|
|
||||||
* For licensing inquiries: info@hb3-accelerator.com
|
|
||||||
* Website: https://hb3-accelerator.com
|
|
||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
|
||||||
*/
|
|
||||||
|
|
||||||
const aiQueueService = require('../services/ai-queue');
|
|
||||||
|
|
||||||
async function testQueue() {
|
|
||||||
console.log('🧪 Тестирование AI очереди...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем инициализацию
|
|
||||||
console.log('1. Проверка инициализации очереди...');
|
|
||||||
const stats = aiQueueService.getStats();
|
|
||||||
console.log('✅ Очередь инициализирована:', stats.isInitialized);
|
|
||||||
console.log('📊 Статистика:', {
|
|
||||||
totalProcessed: stats.totalProcessed,
|
|
||||||
totalFailed: stats.totalFailed,
|
|
||||||
currentQueueSize: stats.currentQueueSize,
|
|
||||||
runningTasks: stats.runningTasks
|
|
||||||
});
|
|
||||||
|
|
||||||
// Тестируем добавление задач
|
|
||||||
console.log('\n2. Тестирование добавления задач...');
|
|
||||||
|
|
||||||
const testTasks = [
|
|
||||||
{
|
|
||||||
message: 'Привет, как дела?',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'chat',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'user',
|
|
||||||
requestId: 'test_1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Расскажи о погоде',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'analysis',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'user',
|
|
||||||
requestId: 'test_2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Срочный вопрос!',
|
|
||||||
language: 'ru',
|
|
||||||
type: 'urgent',
|
|
||||||
userId: 1,
|
|
||||||
userRole: 'admin',
|
|
||||||
requestId: 'test_3'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < testTasks.length; i++) {
|
|
||||||
const task = testTasks[i];
|
|
||||||
console.log(` Добавляем задачу ${i + 1}: "${task.message}"`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await aiQueueService.addTask(task);
|
|
||||||
console.log(` ✅ Задача добавлена, ID: ${result.taskId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Ошибка добавления задачи: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждем обработки
|
|
||||||
console.log('\n3. Ожидание обработки задач...');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
||||||
|
|
||||||
// Проверяем статистику
|
|
||||||
console.log('\n4. Проверка статистики после обработки...');
|
|
||||||
const finalStats = aiQueueService.getStats();
|
|
||||||
console.log('📊 Финальная статистика:', {
|
|
||||||
totalProcessed: finalStats.totalProcessed,
|
|
||||||
totalFailed: finalStats.totalFailed,
|
|
||||||
currentQueueSize: finalStats.currentQueueSize,
|
|
||||||
runningTasks: finalStats.runningTasks,
|
|
||||||
averageProcessingTime: Math.round(finalStats.averageProcessingTime)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Тестируем управление очередью
|
|
||||||
console.log('\n5. Тестирование управления очередью...');
|
|
||||||
|
|
||||||
console.log(' Пауза очереди...');
|
|
||||||
aiQueueService.pause();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
console.log(' Возобновление очереди...');
|
|
||||||
aiQueueService.resume();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
console.log('\n✅ Тестирование завершено!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Ошибка тестирования:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск теста
|
|
||||||
if (require.main === module) {
|
|
||||||
testQueue().then(() => {
|
|
||||||
console.log('\n🏁 Тест завершен');
|
|
||||||
process.exit(0);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('💥 Критическая ошибка:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { testQueue };
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This software is proprietary and confidential.
|
|
||||||
* Unauthorized copying, modification, or distribution is prohibited.
|
|
||||||
*
|
|
||||||
* For licensing inquiries: info@hb3-accelerator.com
|
|
||||||
* Website: https://hb3-accelerator.com
|
|
||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
|
||||||
*/
|
|
||||||
|
|
||||||
const encryptedDb = require('../services/encryptedDatabaseService');
|
|
||||||
const db = require('../db');
|
|
||||||
|
|
||||||
async function testEncryptedTables() {
|
|
||||||
console.log('🔐 Тестирование зашифрованных таблиц...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Тестируем таблицу is_rag_source
|
|
||||||
console.log('1. Тестирование таблицы is_rag_source:');
|
|
||||||
const ragSources = await encryptedDb.getData('is_rag_source', {});
|
|
||||||
console.log(' ✅ Данные получены:', ragSources);
|
|
||||||
|
|
||||||
// Тестируем через прямой SQL запрос
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
let encryptionKey = 'default-key';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
|
||||||
if (fs.existsSync(keyPath)) {
|
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
|
||||||
}
|
|
||||||
} catch (keyError) {
|
|
||||||
console.error('Error reading encryption key:', keyError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const directResult = await db.getQuery()(
|
|
||||||
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id',
|
|
||||||
[encryptionKey]
|
|
||||||
);
|
|
||||||
console.log(' ✅ Прямой SQL запрос:', directResult.rows);
|
|
||||||
|
|
||||||
// Тестируем другие важные таблицы
|
|
||||||
console.log('\n2. Тестирование других зашифрованных таблиц:');
|
|
||||||
|
|
||||||
// user_tables
|
|
||||||
const userTables = await encryptedDb.getData('user_tables', {}, 5);
|
|
||||||
console.log(' ✅ user_tables (первые 5):', userTables.length, 'записей');
|
|
||||||
|
|
||||||
// user_columns
|
|
||||||
const userColumns = await encryptedDb.getData('user_columns', {}, 5);
|
|
||||||
console.log(' ✅ user_columns (первые 5):', userColumns.length, 'записей');
|
|
||||||
|
|
||||||
// messages
|
|
||||||
const messages = await encryptedDb.getData('messages', {}, 3);
|
|
||||||
console.log(' ✅ messages (первые 3):', messages.length, 'записей');
|
|
||||||
|
|
||||||
// conversations
|
|
||||||
const conversations = await encryptedDb.getData('conversations', {}, 3);
|
|
||||||
console.log(' ✅ conversations (первые 3):', conversations.length, 'записей');
|
|
||||||
|
|
||||||
console.log('\n✅ Все тесты прошли успешно!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Ошибка тестирования:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск теста
|
|
||||||
if (require.main === module) {
|
|
||||||
testEncryptedTables().then(() => {
|
|
||||||
console.log('\n🏁 Тест завершен');
|
|
||||||
process.exit(0);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('💥 Критическая ошибка:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { testEncryptedTables };
|
|
||||||
@@ -1,13 +1,207 @@
|
|||||||
/**
|
/**
|
||||||
* Верификация контрактов с Hardhat V2 API
|
* Верификация контрактов в Etherscan V2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
// const { execSync } = require('child_process'); // Удалено - больше не используем Hardhat verify
|
||||||
const DeployParamsService = require('../services/deployParamsService');
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
const deploymentWebSocketService = require('../services/deploymentWebSocketService');
|
const deploymentWebSocketService = require('../services/deploymentWebSocketService');
|
||||||
|
const { getSecret } = require('../services/secretStore');
|
||||||
|
|
||||||
|
// Функция для определения Etherscan V2 API URL по chainId
|
||||||
|
function getEtherscanApiUrl(chainId) {
|
||||||
|
// Используем единый Etherscan V2 API для всех сетей
|
||||||
|
return `https://api.etherscan.io/v2/api?chainid=${chainId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Импортируем вспомогательную функцию
|
||||||
|
const { createStandardJsonInput: createStandardJsonInputHelper } = require('../utils/standardJsonInputHelper');
|
||||||
|
|
||||||
|
// Функция для создания стандартного JSON input
|
||||||
|
function createStandardJsonInput() {
|
||||||
|
const path = require('path');
|
||||||
|
const contractPath = path.join(__dirname, '../contracts/DLE.sol');
|
||||||
|
return createStandardJsonInputHelper(contractPath, 'DLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для проверки статуса верификации
|
||||||
|
async function checkVerificationStatus(chainId, guid, apiKey) {
|
||||||
|
const apiUrl = getEtherscanApiUrl(chainId);
|
||||||
|
|
||||||
|
const formData = new URLSearchParams({
|
||||||
|
apikey: apiKey,
|
||||||
|
module: 'contract',
|
||||||
|
action: 'checkverifystatus',
|
||||||
|
guid: guid
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при проверке статуса:', error.message);
|
||||||
|
return { status: '0', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для проверки реального статуса контракта в Etherscan
|
||||||
|
async function checkContractVerificationStatus(chainId, contractAddress, apiKey) {
|
||||||
|
const apiUrl = getEtherscanApiUrl(chainId);
|
||||||
|
|
||||||
|
const formData = new URLSearchParams({
|
||||||
|
apikey: apiKey,
|
||||||
|
module: 'contract',
|
||||||
|
action: 'getsourcecode',
|
||||||
|
address: contractAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === '1' && result.result && result.result[0]) {
|
||||||
|
const contractInfo = result.result[0];
|
||||||
|
const isVerified = contractInfo.SourceCode && contractInfo.SourceCode !== '';
|
||||||
|
|
||||||
|
console.log(`🔍 Статус контракта ${contractAddress}:`, {
|
||||||
|
isVerified: isVerified,
|
||||||
|
contractName: contractInfo.ContractName || 'Unknown',
|
||||||
|
compilerVersion: contractInfo.CompilerVersion || 'Unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isVerified, contractInfo };
|
||||||
|
} else {
|
||||||
|
console.log('❌ Не удалось получить информацию о контракте:', result.message);
|
||||||
|
return { isVerified: false, error: result.message };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при проверке статуса контракта:', error.message);
|
||||||
|
return { isVerified: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для верификации контракта в Etherscan V2
|
||||||
|
async function verifyContractInEtherscan(chainId, contractAddress, constructorArgsHex, apiKey) {
|
||||||
|
const apiUrl = getEtherscanApiUrl(chainId);
|
||||||
|
const standardJsonInput = createStandardJsonInput();
|
||||||
|
|
||||||
|
console.log(`🔍 Верификация контракта ${contractAddress} в Etherscan V2 (chainId: ${chainId})`);
|
||||||
|
console.log(`📡 API URL: ${apiUrl}`);
|
||||||
|
|
||||||
|
const formData = new URLSearchParams({
|
||||||
|
apikey: apiKey,
|
||||||
|
module: 'contract',
|
||||||
|
action: 'verifysourcecode',
|
||||||
|
contractaddress: contractAddress,
|
||||||
|
codeformat: 'solidity-standard-json-input',
|
||||||
|
contractname: 'DLE.sol:DLE',
|
||||||
|
sourceCode: JSON.stringify(standardJsonInput),
|
||||||
|
compilerversion: 'v0.8.20+commit.a1b79de6',
|
||||||
|
optimizationUsed: '1',
|
||||||
|
runs: '0',
|
||||||
|
constructorArguements: constructorArgsHex
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('📥 Ответ от Etherscan V2:', result);
|
||||||
|
|
||||||
|
if (result.status === '1') {
|
||||||
|
console.log('✅ Верификация отправлена в Etherscan V2!');
|
||||||
|
console.log(`📋 GUID: ${result.result}`);
|
||||||
|
|
||||||
|
// Ждем и проверяем статус верификации с повторными попытками
|
||||||
|
console.log('⏳ Ждем 15 секунд перед проверкой статуса...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 15000));
|
||||||
|
|
||||||
|
// Проверяем статус с повторными попытками (до 3 раз)
|
||||||
|
let statusResult;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 3;
|
||||||
|
|
||||||
|
do {
|
||||||
|
attempts++;
|
||||||
|
console.log(`📊 Проверка статуса верификации (попытка ${attempts}/${maxAttempts})...`);
|
||||||
|
statusResult = await checkVerificationStatus(chainId, result.result, apiKey);
|
||||||
|
console.log('📊 Статус верификации:', statusResult);
|
||||||
|
|
||||||
|
if (statusResult.status === '1') {
|
||||||
|
console.log('🎉 Верификация успешна!');
|
||||||
|
return { success: true, guid: result.result, message: 'Верифицировано в Etherscan V2' };
|
||||||
|
} else if (statusResult.status === '0' && statusResult.result.includes('Pending')) {
|
||||||
|
console.log('⏳ Верификация в очереди, проверяем реальный статус контракта...');
|
||||||
|
|
||||||
|
// Проверяем реальный статус контракта в Etherscan
|
||||||
|
const contractStatus = await checkContractVerificationStatus(chainId, contractAddress, apiKey);
|
||||||
|
if (contractStatus.isVerified) {
|
||||||
|
console.log('✅ Контракт уже верифицирован в Etherscan!');
|
||||||
|
return { success: true, guid: result.result, message: 'Контракт верифицирован' };
|
||||||
|
} else {
|
||||||
|
console.log('⏳ Контракт еще не верифицирован, ожидаем завершения...');
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
console.log(`⏳ Ждем еще 10 секунд перед следующей попыткой...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ Верификация не удалась:', statusResult.result);
|
||||||
|
return { success: false, error: statusResult.result };
|
||||||
|
}
|
||||||
|
} while (attempts < maxAttempts && statusResult.status === '0' && statusResult.result.includes('Pending'));
|
||||||
|
|
||||||
|
// Если все попытки исчерпаны
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
console.log('⏳ Максимальное количество попыток достигнуто, верификация может быть в процессе...');
|
||||||
|
return { success: false, error: 'Ожидание верификации', guid: result.result };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ Ошибка отправки верификации в Etherscan V2:', result.message);
|
||||||
|
|
||||||
|
// Проверяем, не верифицирован ли уже контракт
|
||||||
|
if (result.message && result.message.includes('already verified')) {
|
||||||
|
console.log('✅ Контракт уже верифицирован');
|
||||||
|
return { success: true, message: 'Контракт уже верифицирован' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: result.message };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при отправке запроса в Etherscan V2:', error.message);
|
||||||
|
|
||||||
|
// Проверяем, не является ли это ошибкой сети
|
||||||
|
if (error.message.includes('fetch') || error.message.includes('network')) {
|
||||||
|
console.log('⚠️ Ошибка сети, верификация может быть в процессе...');
|
||||||
|
return { success: false, error: 'Network error - verification may be in progress' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
||||||
console.log('🚀 Запуск верификации с Hardhat V2...');
|
console.log('🚀 Запуск верификации контрактов...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Если параметры не переданы, получаем их из базы данных
|
// Если параметры не переданы, получаем их из базы данных
|
||||||
@@ -23,10 +217,15 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
|||||||
params = latestParams[0];
|
params = latestParams[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.etherscan_api_key) {
|
// Проверяем API ключ в параметрах или переменной окружения
|
||||||
throw new Error('Etherscan API ключ не найден в параметрах');
|
const etherscanApiKey = params.etherscan_api_key || process.env.ETHERSCAN_API_KEY;
|
||||||
|
if (!etherscanApiKey) {
|
||||||
|
throw new Error('Etherscan API ключ не найден в параметрах или переменной окружения');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Устанавливаем API ключ в переменную окружения для использования в коде
|
||||||
|
process.env.ETHERSCAN_API_KEY = etherscanApiKey;
|
||||||
|
|
||||||
console.log('📋 Параметры деплоя:', {
|
console.log('📋 Параметры деплоя:', {
|
||||||
deploymentId: params.deployment_id,
|
deploymentId: params.deployment_id,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
@@ -54,38 +253,35 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
|||||||
}
|
}
|
||||||
console.log(`🌐 Найдено ${networks.length} развернутых сетей`);
|
console.log(`🌐 Найдено ${networks.length} развернутых сетей`);
|
||||||
|
|
||||||
// Маппинг chainId на названия сетей
|
// Получаем маппинг chainId на названия сетей из параметров деплоя
|
||||||
const networkMap = {
|
const networkMap = {};
|
||||||
1: 'mainnet',
|
if (params.supportedChainIds && params.supportedChainIds.length > 0) {
|
||||||
11155111: 'sepolia',
|
// Создаем маппинг только для поддерживаемых сетей
|
||||||
17000: 'holesky',
|
for (const chainId of params.supportedChainIds) {
|
||||||
137: 'polygon',
|
switch (chainId) {
|
||||||
42161: 'arbitrumOne',
|
case 1: networkMap[chainId] = 'mainnet'; break;
|
||||||
421614: 'arbitrumSepolia',
|
case 11155111: networkMap[chainId] = 'sepolia'; break;
|
||||||
56: 'bsc',
|
case 17000: networkMap[chainId] = 'holesky'; break;
|
||||||
8453: 'base',
|
case 137: networkMap[chainId] = 'polygon'; break;
|
||||||
84532: 'baseSepolia'
|
case 42161: networkMap[chainId] = 'arbitrumOne'; break;
|
||||||
};
|
case 421614: networkMap[chainId] = 'arbitrumSepolia'; break;
|
||||||
|
case 56: networkMap[chainId] = 'bsc'; break;
|
||||||
|
case 8453: networkMap[chainId] = 'base'; break;
|
||||||
|
case 84532: networkMap[chainId] = 'baseSepolia'; break;
|
||||||
|
default: networkMap[chainId] = `chain-${chainId}`; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback для совместимости
|
||||||
|
networkMap[11155111] = 'sepolia';
|
||||||
|
networkMap[17000] = 'holesky';
|
||||||
|
networkMap[421614] = 'arbitrumSepolia';
|
||||||
|
networkMap[84532] = 'baseSepolia';
|
||||||
|
}
|
||||||
|
|
||||||
// Подготавливаем аргументы конструктора
|
// Используем централизованный генератор параметров конструктора
|
||||||
const constructorArgs = [
|
const { generateVerificationArgs } = require('../utils/constructorArgsGenerator');
|
||||||
{
|
const constructorArgs = generateVerificationArgs(params);
|
||||||
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('📊 Аргументы конструктора подготовлены');
|
console.log('📊 Аргументы конструктора подготовлены');
|
||||||
|
|
||||||
@@ -125,77 +321,61 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем временный файл с аргументами конструктора
|
// Получаем API ключ Etherscan
|
||||||
const fs = require('fs');
|
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
|
||||||
const path = require('path');
|
if (!etherscanApiKey) {
|
||||||
const argsFile = path.join(__dirname, `constructor-args-${Date.now()}.json`);
|
console.log('❌ API ключ Etherscan не найден, пропускаем верификацию в Etherscan');
|
||||||
|
|
||||||
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({
|
verificationResults.push({
|
||||||
success: false,
|
success: false,
|
||||||
network: networkName,
|
network: networkName,
|
||||||
chainId: chainId,
|
chainId: chainId,
|
||||||
error: errorOutput
|
error: 'No Etherscan API key'
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Кодируем аргументы конструктора в hex
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
||||||
|
|
||||||
|
// Используем централизованный генератор параметров конструктора
|
||||||
|
const { generateDeploymentArgs } = require('../utils/constructorArgsGenerator');
|
||||||
|
const { dleConfig, initializer } = generateDeploymentArgs(params);
|
||||||
|
|
||||||
|
const encodedArgs = abiCoder.encode(
|
||||||
|
[
|
||||||
|
'tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, uint256 quorumPercentage, address[] initialPartners, uint256[] initialAmounts, uint256[] supportedChainIds)',
|
||||||
|
'address'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
dleConfig,
|
||||||
|
initializer
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const constructorArgsHex = encodedArgs.slice(2); // Убираем 0x
|
||||||
|
|
||||||
|
// Верификация в Etherscan
|
||||||
|
console.log('🌐 Верификация в Etherscan...');
|
||||||
|
const etherscanResult = await verifyContractInEtherscan(chainId, address, constructorArgsHex, etherscanApiKey);
|
||||||
|
|
||||||
|
if (etherscanResult.success) {
|
||||||
|
console.log('✅ Верификация в Etherscan успешна!');
|
||||||
|
verificationResults.push({
|
||||||
|
success: true,
|
||||||
|
network: networkName,
|
||||||
|
chainId: chainId,
|
||||||
|
etherscan: true,
|
||||||
|
message: etherscanResult.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('❌ Ошибка верификации в Etherscan:', etherscanResult.error);
|
||||||
|
verificationResults.push({
|
||||||
|
success: false,
|
||||||
|
network: networkName,
|
||||||
|
chainId: chainId,
|
||||||
|
error: etherscanResult.error
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,17 +383,20 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) {
|
|||||||
console.log('\n📊 Итоговые результаты верификации:');
|
console.log('\n📊 Итоговые результаты верификации:');
|
||||||
const successful = verificationResults.filter(r => r.success).length;
|
const successful = verificationResults.filter(r => r.success).length;
|
||||||
const failed = verificationResults.filter(r => !r.success).length;
|
const failed = verificationResults.filter(r => !r.success).length;
|
||||||
const alreadyVerified = verificationResults.filter(r => r.alreadyVerified).length;
|
const etherscanVerified = verificationResults.filter(r => r.etherscan).length;
|
||||||
|
|
||||||
console.log(`✅ Успешно верифицировано: ${successful}`);
|
console.log(`✅ Успешно верифицировано: ${successful}`);
|
||||||
console.log(`ℹ️ Уже было верифицировано: ${alreadyVerified}`);
|
console.log(`🌐 В Etherscan: ${etherscanVerified}`);
|
||||||
console.log(`❌ Ошибки: ${failed}`);
|
console.log(`❌ Ошибки: ${failed}`);
|
||||||
|
|
||||||
verificationResults.forEach(result => {
|
verificationResults.forEach(result => {
|
||||||
const status = result.success
|
const status = result.success ? '✅' : '❌';
|
||||||
? (result.alreadyVerified ? 'ℹ️' : '✅')
|
|
||||||
: '❌';
|
const message = result.success
|
||||||
console.log(`${status} ${result.network} (${result.chainId}): ${result.success ? 'OK' : result.error?.substring(0, 100) + '...'}`);
|
? (result.message || 'OK')
|
||||||
|
: result.error?.substring(0, 100) + '...';
|
||||||
|
|
||||||
|
console.log(`${status} ${result.network} (${result.chainId}): ${message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('\n🎉 Верификация завершена!');
|
console.log('\n🎉 Верификация завершена!');
|
||||||
@@ -327,13 +510,26 @@ async function verifyModules() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Маппинг chainId на названия сетей для Hardhat
|
// Получаем маппинг chainId на названия сетей из параметров деплоя
|
||||||
const networkMap = {
|
const networkMap = {};
|
||||||
11155111: 'sepolia',
|
if (params.supportedChainIds && params.supportedChainIds.length > 0) {
|
||||||
17000: 'holesky',
|
// Создаем маппинг только для поддерживаемых сетей
|
||||||
421614: 'arbitrumSepolia',
|
for (const chainId of params.supportedChainIds) {
|
||||||
84532: 'baseSepolia'
|
switch (chainId) {
|
||||||
};
|
case 11155111: networkMap[chainId] = 'sepolia'; break;
|
||||||
|
case 17000: networkMap[chainId] = 'holesky'; break;
|
||||||
|
case 421614: networkMap[chainId] = 'arbitrumSepolia'; break;
|
||||||
|
case 84532: networkMap[chainId] = 'baseSepolia'; break;
|
||||||
|
default: networkMap[chainId] = `chain-${chainId}`; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback для совместимости
|
||||||
|
networkMap[11155111] = 'sepolia';
|
||||||
|
networkMap[17000] = 'holesky';
|
||||||
|
networkMap[421614] = 'arbitrumSepolia';
|
||||||
|
networkMap[84532] = 'baseSepolia';
|
||||||
|
}
|
||||||
|
|
||||||
// Верифицируем каждый модуль
|
// Верифицируем каждый модуль
|
||||||
for (const file of moduleFiles) {
|
for (const file of moduleFiles) {
|
||||||
@@ -375,30 +571,13 @@ async function verifyModules() {
|
|||||||
const argsFile = path.join(__dirname, `temp-args-${Date.now()}.json`);
|
const argsFile = path.join(__dirname, `temp-args-${Date.now()}.json`);
|
||||||
fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2));
|
fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2));
|
||||||
|
|
||||||
// Выполняем верификацию
|
// Верификация модулей через Etherscan V2 API (пока не реализовано)
|
||||||
const command = `ETHERSCAN_API_KEY="${params.etherscan_api_key}" npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`;
|
console.log(`⚠️ Верификация модулей через Etherscan V2 API пока не реализована для ${moduleData.moduleType} в ${networkName}`);
|
||||||
console.log(`📝 Команда верификации: npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const output = execSync(command, {
|
|
||||||
cwd: '/app',
|
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: 'pipe'
|
|
||||||
});
|
|
||||||
console.log(`✅ ${moduleData.moduleType} успешно верифицирован в ${networkName}`);
|
|
||||||
console.log(output);
|
|
||||||
|
|
||||||
// Уведомляем WebSocket клиентов о успешной верификации
|
|
||||||
deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleData.moduleType} верифицирован в ${networkName}`);
|
|
||||||
deploymentWebSocketService.notifyModuleVerified(dleAddress, moduleData.moduleType, networkName);
|
|
||||||
} catch (verifyError) {
|
|
||||||
console.log(`❌ Ошибка верификации ${moduleData.moduleType} в ${networkName}: ${verifyError.message}`);
|
|
||||||
} finally {
|
|
||||||
// Удаляем временный файл
|
// Удаляем временный файл
|
||||||
if (fs.existsSync(argsFile)) {
|
if (fs.existsSync(argsFile)) {
|
||||||
fs.unlinkSync(argsFile);
|
fs.unlinkSync(argsFile);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Ошибка при верификации ${moduleData.moduleType} в сети ${network.chainId}:`, error.message);
|
console.error(`❌ Ошибка при верификации ${moduleData.moduleType} в сети ${network.chainId}:`, error.message);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ async function startServer() {
|
|||||||
console.log(`✅ Server is running on port ${PORT}`);
|
console.log(`✅ Server is running on port ${PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(PORT, async () => {
|
server.listen(PORT, '0.0.0.0', async () => {
|
||||||
try {
|
try {
|
||||||
await startServer();
|
await startServer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ async function upsertAuthToken(token) {
|
|||||||
const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold);
|
const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold);
|
||||||
const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold);
|
const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold);
|
||||||
|
|
||||||
|
// Валидация порогов доступа
|
||||||
|
if (readonlyThreshold >= editorThreshold) {
|
||||||
|
throw new Error('Минимум токенов для Read-Only доступа должен быть меньше минимума для Editor доступа');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[AuthTokenService] Вычисленные значения:');
|
console.log('[AuthTokenService] Вычисленные значения:');
|
||||||
console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold);
|
console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold);
|
||||||
console.log('[AuthTokenService] editorThreshold:', editorThreshold);
|
console.log('[AuthTokenService] editorThreshold:', editorThreshold);
|
||||||
|
|||||||
@@ -38,22 +38,23 @@ class DeployParamsService {
|
|||||||
coordinates: params.coordinates,
|
coordinates: params.coordinates,
|
||||||
jurisdiction: params.jurisdiction,
|
jurisdiction: params.jurisdiction,
|
||||||
oktmo: params.oktmo,
|
oktmo: params.oktmo,
|
||||||
okved_codes: JSON.stringify(params.okvedCodes || []),
|
okved_codes: JSON.stringify(params.okved_codes || []),
|
||||||
kpp: params.kpp,
|
kpp: params.kpp,
|
||||||
quorum_percentage: params.quorumPercentage,
|
quorum_percentage: params.quorum_percentage,
|
||||||
initial_partners: JSON.stringify(params.initialPartners || []),
|
initial_partners: JSON.stringify(params.initial_partners || []),
|
||||||
initial_amounts: JSON.stringify(params.initialAmounts || []),
|
// initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое
|
||||||
supported_chain_ids: JSON.stringify(params.supportedChainIds || []),
|
initial_amounts: JSON.stringify(params.initial_amounts || []),
|
||||||
current_chain_id: params.currentChainId,
|
supported_chain_ids: JSON.stringify(params.supported_chain_ids || []),
|
||||||
logo_uri: params.logoURI,
|
current_chain_id: params.current_chain_id || 1, // По умолчанию Ethereum
|
||||||
private_key: params.privateKey, // Будет автоматически зашифрован
|
logo_uri: params.logo_uri,
|
||||||
etherscan_api_key: params.etherscanApiKey,
|
private_key: params.private_key, // Будет автоматически зашифрован
|
||||||
auto_verify_after_deploy: params.autoVerifyAfterDeploy || false,
|
etherscan_api_key: params.etherscan_api_key,
|
||||||
create2_salt: params.CREATE2_SALT,
|
auto_verify_after_deploy: params.auto_verify_after_deploy || false,
|
||||||
rpc_urls: JSON.stringify(params.rpcUrls ? (Array.isArray(params.rpcUrls) ? params.rpcUrls : Object.values(params.rpcUrls)) : []),
|
create2_salt: params.create2_salt,
|
||||||
|
rpc_urls: JSON.stringify(params.rpc_urls ? (Array.isArray(params.rpc_urls) ? params.rpc_urls : Object.values(params.rpc_urls)) : []),
|
||||||
initializer: params.initializer,
|
initializer: params.initializer,
|
||||||
dle_address: params.dleAddress,
|
dle_address: params.dle_address,
|
||||||
modules_to_deploy: JSON.stringify(params.modulesToDeploy || []),
|
modules_to_deploy: JSON.stringify(params.modules_to_deploy || []),
|
||||||
deployment_status: status
|
deployment_status: status
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +90,16 @@ class DeployParamsService {
|
|||||||
|
|
||||||
if (!result || result.length === 0) {
|
if (!result || result.length === 0) {
|
||||||
logger.warn(`⚠️ Параметры деплоя не найдены: ${deploymentId}`);
|
logger.warn(`⚠️ Параметры деплоя не найдены: ${deploymentId}`);
|
||||||
|
logger.warn(`🔍 Тип deploymentId: ${typeof deploymentId}`);
|
||||||
|
logger.warn(`🔍 Значение deploymentId: "${deploymentId}"`);
|
||||||
|
|
||||||
|
// Попробуем найти все записи для отладки
|
||||||
|
const allRecords = await encryptedDb.getData('deploy_params', {});
|
||||||
|
logger.warn(`🔍 Всего записей в deploy_params: ${allRecords?.length || 0}`);
|
||||||
|
if (allRecords && allRecords.length > 0) {
|
||||||
|
logger.warn(`🔍 Последние deployment_id: ${allRecords.map(r => r.deployment_id).slice(-3).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +129,33 @@ class DeployParamsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает параметры деплоя по адресу DLE
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Object|null>} - Параметры деплоя или null
|
||||||
|
*/
|
||||||
|
async getDeployParamsByDleAddress(dleAddress) {
|
||||||
|
try {
|
||||||
|
logger.info(`📖 Поиск параметров деплоя по адресу DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Используем encryptedDb для поиска по адресу DLE
|
||||||
|
const result = await encryptedDb.getData('deploy_params', {
|
||||||
|
dle_address: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
logger.warn(`⚠️ Параметры деплоя не найдены для адреса: ${dleAddress}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем первый найденный результат
|
||||||
|
return result[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Ошибка при поиске параметров деплоя по адресу: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет статус деплоя
|
* Обновляет статус деплоя
|
||||||
* @param {string} deploymentId - Идентификатор деплоя
|
* @param {string} deploymentId - Идентификатор деплоя
|
||||||
@@ -125,25 +163,66 @@ class DeployParamsService {
|
|||||||
* @param {string} dleAddress - Адрес задеплоенного контракта
|
* @param {string} dleAddress - Адрес задеплоенного контракта
|
||||||
* @returns {Promise<Object>} - Обновленные параметры
|
* @returns {Promise<Object>} - Обновленные параметры
|
||||||
*/
|
*/
|
||||||
async updateDeploymentStatus(deploymentId, status, dleAddress = null) {
|
async updateDeploymentStatus(deploymentId, status, result = null) {
|
||||||
try {
|
try {
|
||||||
logger.info(`🔄 Обновление статуса деплоя: ${deploymentId} -> ${status}`);
|
logger.info(`🔄 Обновление статуса деплоя: ${deploymentId} -> ${status}`);
|
||||||
|
|
||||||
|
// Подготавливаем данные для обновления
|
||||||
|
let dleAddress = null;
|
||||||
|
let deployResult = null;
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
logger.info(`🔍 [DEBUG] updateDeploymentStatus получил result:`, JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
// Извлекаем адреса из результата деплоя
|
||||||
|
if (result.data && result.data.networks && result.data.networks.length > 0) {
|
||||||
|
// Берем первый адрес для обратной совместимости
|
||||||
|
dleAddress = result.data.networks[0].address;
|
||||||
|
logger.info(`✅ [DEBUG] Найден адрес в result.data.networks[0].address: ${dleAddress}`);
|
||||||
|
} else if (result.networks && result.networks.length > 0) {
|
||||||
|
// Берем первый адрес для обратной совместимости
|
||||||
|
dleAddress = result.networks[0].address;
|
||||||
|
logger.info(`✅ [DEBUG] Найден адрес в result.networks[0].address: ${dleAddress}`);
|
||||||
|
} else if (result.output) {
|
||||||
|
// Ищем адрес в тексте output - сначала пробуем найти JSON массив с адресами
|
||||||
|
const jsonArrayMatch = result.output.match(/\[[\s\S]*?"address":\s*"(0x[a-fA-F0-9]{40})"[\s\S]*?\]/);
|
||||||
|
if (jsonArrayMatch) {
|
||||||
|
dleAddress = jsonArrayMatch[1];
|
||||||
|
logger.info(`✅ [DEBUG] Найден адрес в JSON массиве result.output: ${dleAddress}`);
|
||||||
|
} else {
|
||||||
|
// Fallback: ищем адрес в тексте output (формат: "📍 Адрес: 0x...")
|
||||||
|
const addressMatch = result.output.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/);
|
||||||
|
if (addressMatch) {
|
||||||
|
dleAddress = addressMatch[1];
|
||||||
|
logger.info(`✅ [DEBUG] Найден адрес в тексте result.output: ${dleAddress}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`⚠️ [DEBUG] Адрес не найден в результате деплоя`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем полный результат деплоя (включая все адреса всех сетей)
|
||||||
|
deployResult = JSON.stringify(result);
|
||||||
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE deploy_params
|
UPDATE deploy_params
|
||||||
SET deployment_status = $2, dle_address = $3, updated_at = CURRENT_TIMESTAMP
|
SET deployment_status = $2, dle_address = $3, deploy_result = $4, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE deployment_id = $1
|
WHERE deployment_id = $1
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await this.pool.query(query, [deploymentId, status, dleAddress]);
|
const queryResult = await this.pool.query(query, [deploymentId, status, dleAddress, deployResult]);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (queryResult.rows.length === 0) {
|
||||||
throw new Error(`Параметры деплоя не найдены: ${deploymentId}`);
|
throw new Error(`Параметры деплоя не найдены: ${deploymentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`✅ Статус деплоя обновлен: ${deploymentId} -> ${status}`);
|
logger.info(`✅ Статус деплоя обновлен: ${deploymentId} -> ${status}`);
|
||||||
return result.rows[0];
|
if (dleAddress) {
|
||||||
|
logger.info(`📍 Адрес DLE контракта: ${dleAddress}`);
|
||||||
|
}
|
||||||
|
return queryResult.rows[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Ошибка при обновлении статуса деплоя: ${error.message}`);
|
logger.error(`❌ Ошибка при обновлении статуса деплоя: ${error.message}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -212,6 +291,229 @@ class DeployParamsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить контракты по chainId
|
||||||
|
*/
|
||||||
|
async getContractsByChainId(chainId) {
|
||||||
|
try {
|
||||||
|
console.log(`[DeployParamsService] Ищем контракты с current_chain_id: ${chainId}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
deployment_id,
|
||||||
|
name,
|
||||||
|
dle_address,
|
||||||
|
current_chain_id,
|
||||||
|
supported_chain_ids,
|
||||||
|
created_at
|
||||||
|
FROM deploy_params
|
||||||
|
WHERE current_chain_id = $1 AND dle_address IS NOT NULL
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.pool.query(query, [chainId]);
|
||||||
|
|
||||||
|
console.log(`[DeployParamsService] Найдено контрактов: ${result.rows.length}`);
|
||||||
|
|
||||||
|
return result.rows.map(row => ({
|
||||||
|
deploymentId: row.deployment_id,
|
||||||
|
name: row.name,
|
||||||
|
dleAddress: row.dle_address,
|
||||||
|
currentChainId: row.current_chain_id,
|
||||||
|
supportedChainIds: row.supported_chain_ids,
|
||||||
|
createdAt: row.created_at
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[DeployParamsService] Ошибка поиска контрактов по chainId:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все деплои
|
||||||
|
* @param {number} limit - Количество записей
|
||||||
|
* @returns {Promise<Array>} - Список всех деплоев
|
||||||
|
*/
|
||||||
|
async getAllDeployments(limit = 50) {
|
||||||
|
try {
|
||||||
|
logger.info(`📋 Получение всех деплоев (лимит: ${limit})`);
|
||||||
|
|
||||||
|
// Используем encryptedDb для автоматического расшифрования
|
||||||
|
const result = await encryptedDb.getData('deploy_params', {}, limit, 'created_at DESC');
|
||||||
|
|
||||||
|
return result.map(row => {
|
||||||
|
// Парсим deployResult для извлечения адресов всех сетей
|
||||||
|
let deployedNetworks = [];
|
||||||
|
console.log(`🔍 [DEBUG] Processing deployment ${row.deployment_id}, deploy_result exists:`, !!row.deploy_result);
|
||||||
|
console.log(`🔍 [DEBUG] deploy_result type:`, typeof row.deploy_result);
|
||||||
|
if (row.deploy_result) {
|
||||||
|
try {
|
||||||
|
const deployResult = typeof row.deploy_result === 'string'
|
||||||
|
? JSON.parse(row.deploy_result)
|
||||||
|
: row.deploy_result;
|
||||||
|
|
||||||
|
console.log(`🔍 [DEBUG] deployResult keys:`, Object.keys(deployResult));
|
||||||
|
console.log(`🔍 [DEBUG] deployResult.output exists:`, !!deployResult.output);
|
||||||
|
console.log(`🔍 [DEBUG] deployResult.data exists:`, !!deployResult.data);
|
||||||
|
console.log(`🔍 [DEBUG] deployResult.networks exists:`, !!deployResult.networks);
|
||||||
|
if (deployResult.error) {
|
||||||
|
console.log(`🔍 [DEBUG] deployResult.error:`, deployResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения правильного названия сети
|
||||||
|
const getNetworkName = (chainId) => {
|
||||||
|
const networkNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
17000: 'Holesky',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
84532: 'Base Sepolia',
|
||||||
|
137: 'Polygon',
|
||||||
|
56: 'BSC',
|
||||||
|
42161: 'Arbitrum One'
|
||||||
|
};
|
||||||
|
return networkNames[chainId] || `Chain ${chainId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для загрузки ABI для конкретной сети
|
||||||
|
const loadABIForNetwork = (chainId) => {
|
||||||
|
try {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const abiPath = path.join(__dirname, '../../../frontend/src/utils/dle-abi.js');
|
||||||
|
|
||||||
|
if (fs.existsSync(abiPath)) {
|
||||||
|
const abiContent = fs.readFileSync(abiPath, 'utf8');
|
||||||
|
// Используем более простое регулярное выражение
|
||||||
|
const abiMatch = abiContent.match(/export const DLE_ABI = (\[[\s\S]*?\]);/);
|
||||||
|
if (abiMatch) {
|
||||||
|
// Попробуем исправить JSON, заменив проблемные символы
|
||||||
|
let abiText = abiMatch[1];
|
||||||
|
// Убираем лишние запятые в конце
|
||||||
|
abiText = abiText.replace(/,(\s*[}\]])/g, '$1');
|
||||||
|
try {
|
||||||
|
return JSON.parse(abiText);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn(`⚠️ Ошибка парсинга ABI JSON для сети ${chainId}:`, parseError.message);
|
||||||
|
// Возвращаем пустой массив как fallback
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (abiError) {
|
||||||
|
console.warn(`⚠️ Ошибка загрузки ABI для сети ${chainId}:`, abiError.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Извлекаем адреса из результата деплоя
|
||||||
|
if (deployResult.data && deployResult.data.networks) {
|
||||||
|
deployedNetworks = deployResult.data.networks.map(network => ({
|
||||||
|
chainId: network.chainId,
|
||||||
|
address: network.address,
|
||||||
|
networkName: network.networkName || getNetworkName(network.chainId),
|
||||||
|
abi: loadABIForNetwork(network.chainId) // ABI для каждой сети отдельно
|
||||||
|
}));
|
||||||
|
} else if (deployResult.networks) {
|
||||||
|
deployedNetworks = deployResult.networks.map(network => ({
|
||||||
|
chainId: network.chainId,
|
||||||
|
address: network.address,
|
||||||
|
networkName: network.networkName || getNetworkName(network.chainId),
|
||||||
|
abi: loadABIForNetwork(network.chainId) // ABI для каждой сети отдельно
|
||||||
|
}));
|
||||||
|
} else if (deployResult.output) {
|
||||||
|
console.log(`🔍 [DEBUG] Processing deployResult.output`);
|
||||||
|
// Извлекаем адреса из текста output
|
||||||
|
const output = deployResult.output;
|
||||||
|
const addressMatches = output.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/g);
|
||||||
|
const chainIdMatches = output.match(/chainId: (\d+)/g);
|
||||||
|
|
||||||
|
// Альтернативный поиск по названиям сетей
|
||||||
|
const networkMatches = output.match(/🔍 Верификация в сети (\w+) \(chainId: (\d+)\)/g);
|
||||||
|
|
||||||
|
console.log(`🔍 [DEBUG] addressMatches:`, addressMatches);
|
||||||
|
console.log(`🔍 [DEBUG] chainIdMatches:`, chainIdMatches);
|
||||||
|
console.log(`🔍 [DEBUG] networkMatches:`, networkMatches);
|
||||||
|
|
||||||
|
if (networkMatches && networkMatches.length > 0) {
|
||||||
|
// Используем networkMatches для более точного парсинга
|
||||||
|
deployedNetworks = networkMatches.map((match) => {
|
||||||
|
const [, networkName, chainIdStr] = match.match(/🔍 Верификация в сети (\w+) \(chainId: (\d+)\)/);
|
||||||
|
const chainId = parseInt(chainIdStr);
|
||||||
|
|
||||||
|
// Ищем адрес для этой сети в output
|
||||||
|
const addressRegex = new RegExp(`🔍 Верификация в сети ${networkName} \\(chainId: ${chainId}\\)\\n📍 Адрес: (0x[a-fA-F0-9]{40})`);
|
||||||
|
const addressMatch = output.match(addressRegex);
|
||||||
|
const address = addressMatch ? addressMatch[1] : '0x0000000000000000000000000000000000000000';
|
||||||
|
|
||||||
|
return {
|
||||||
|
chainId: chainId,
|
||||||
|
address: address,
|
||||||
|
networkName: getNetworkName(chainId),
|
||||||
|
abi: loadABIForNetwork(chainId)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(`🔍 [DEBUG] deployedNetworks created from networkMatches:`, deployedNetworks);
|
||||||
|
} else if (addressMatches && chainIdMatches) {
|
||||||
|
deployedNetworks = addressMatches.map((match, index) => {
|
||||||
|
const address = match.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/)[1];
|
||||||
|
const chainId = chainIdMatches[index] ? parseInt(chainIdMatches[index].match(/chainId: (\d+)/)[1]) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chainId: chainId,
|
||||||
|
address: address,
|
||||||
|
networkName: chainId ? getNetworkName(chainId) : `Network ${index + 1}`,
|
||||||
|
abi: loadABIForNetwork(chainId) // ABI для каждой сети отдельно
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(`🔍 [DEBUG] deployedNetworks created:`, deployedNetworks);
|
||||||
|
} else {
|
||||||
|
console.log(`🔍 [DEBUG] No matches found - addressMatches:`, !!addressMatches, 'chainIdMatches:', !!chainIdMatches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ Ошибка парсинга deployResult для ${row.deployment_id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deploymentId: row.deployment_id,
|
||||||
|
name: row.name,
|
||||||
|
symbol: row.symbol,
|
||||||
|
location: row.location,
|
||||||
|
coordinates: row.coordinates,
|
||||||
|
jurisdiction: row.jurisdiction,
|
||||||
|
oktmo: row.oktmo,
|
||||||
|
okvedCodes: row.okved_codes || [],
|
||||||
|
kpp: row.kpp,
|
||||||
|
quorumPercentage: row.quorum_percentage,
|
||||||
|
initialPartners: row.initial_partners || [],
|
||||||
|
initialAmounts: row.initial_amounts || [],
|
||||||
|
supportedChainIds: row.supported_chain_ids || [],
|
||||||
|
currentChainId: row.current_chain_id,
|
||||||
|
logoURI: row.logo_uri,
|
||||||
|
etherscanApiKey: row.etherscan_api_key,
|
||||||
|
autoVerifyAfterDeploy: row.auto_verify_after_deploy,
|
||||||
|
create2Salt: row.create2_salt,
|
||||||
|
rpcUrls: row.rpc_urls || [],
|
||||||
|
initializer: row.initializer,
|
||||||
|
dleAddress: row.dle_address,
|
||||||
|
modulesToDeploy: row.modules_to_deploy || [],
|
||||||
|
deploymentStatus: row.deployment_status,
|
||||||
|
deployResult: row.deploy_result,
|
||||||
|
deployedNetworks: deployedNetworks, // Добавляем адреса всех сетей
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Ошибка при получении всех деплоев: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Закрывает соединение с базой данных
|
* Закрывает соединение с базой данных
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ class DeploymentWebSocketService {
|
|||||||
this.clients.get(ws.dleAddress).delete(ws);
|
this.clients.get(ws.dleAddress).delete(ws);
|
||||||
if (this.clients.get(ws.dleAddress).size === 0) {
|
if (this.clients.get(ws.dleAddress).size === 0) {
|
||||||
this.clients.delete(ws.dleAddress);
|
this.clients.delete(ws.dleAddress);
|
||||||
|
// Очищаем сессию деплоя если нет активных клиентов
|
||||||
|
if (this.deploymentSessions.has(ws.dleAddress)) {
|
||||||
|
console.log(`[DeploymentWS] Очистка сессии деплоя для DLE: ${ws.dleAddress}`);
|
||||||
|
this.deploymentSessions.delete(ws.dleAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class DLEV2Service {
|
|||||||
* @returns {Promise<Object>} - Результат создания DLE
|
* @returns {Promise<Object>} - Результат создания DLE
|
||||||
*/
|
*/
|
||||||
async createDLE(dleParams, deploymentId = null) {
|
async createDLE(dleParams, deploymentId = null) {
|
||||||
console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!");
|
|
||||||
logger.info("🚀 Начало создания DLE v2 с параметрами:", dleParams);
|
logger.info("🚀 Начало создания DLE v2 с параметрами:", dleParams);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -46,7 +45,6 @@ class DLEV2Service {
|
|||||||
deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🆔 Deployment ID: ${deploymentId}`);
|
|
||||||
logger.info(`🆔 Deployment ID: ${deploymentId}`);
|
logger.info(`🆔 Deployment ID: ${deploymentId}`);
|
||||||
|
|
||||||
// WebSocket обновление: начало процесса
|
// WebSocket обновление: начало процесса
|
||||||
@@ -58,21 +56,13 @@ class DLEV2Service {
|
|||||||
this.validateDLEParams(dleParams);
|
this.validateDLEParams(dleParams);
|
||||||
|
|
||||||
// Подготовка параметров для деплоя
|
// Подготовка параметров для деплоя
|
||||||
console.log('🔧 Подготавливаем параметры для деплоя...');
|
|
||||||
logger.info('🔧 Подготавливаем параметры для деплоя...');
|
logger.info('🔧 Подготавливаем параметры для деплоя...');
|
||||||
|
|
||||||
// Отладка: проверяем входные параметры
|
|
||||||
console.log('🔍 ОТЛАДКА - Входные параметры:');
|
|
||||||
console.log(' supportedChainIds:', JSON.stringify(dleParams.supportedChainIds, null, 2));
|
|
||||||
console.log(' privateKey:', dleParams.privateKey ? '[ЕСТЬ]' : '[НЕТ]');
|
|
||||||
console.log(' name:', dleParams.name);
|
|
||||||
|
|
||||||
const deployParams = this.prepareDeployParams(dleParams);
|
const deployParams = this.prepareDeployParams(dleParams);
|
||||||
console.log('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2));
|
logger.info('✅ Параметры подготовлены');
|
||||||
logger.info('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2));
|
|
||||||
|
|
||||||
// Сохраняем подготовленные параметры в базу данных
|
// Сохраняем подготовленные параметры в базу данных
|
||||||
logger.info(`💾 Сохранение подготовленных параметров деплоя в БД: ${deploymentId}`);
|
logger.info(`💾 Сохранение параметров деплоя в БД: ${deploymentId}`);
|
||||||
await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending');
|
await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending');
|
||||||
|
|
||||||
// Вычисляем адрес инициализатора
|
// Вычисляем адрес инициализатора
|
||||||
@@ -84,27 +74,17 @@ class DLEV2Service {
|
|||||||
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
|
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket обновление: генерация CREATE2_SALT
|
// WebSocket обновление: подготовка к деплою
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса');
|
deploymentTracker.updateProgress(deploymentId, 'Подготовка к деплою', 10, 'Настраиваем параметры для детерминированного деплоя');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем одноразовый CREATE2_SALT
|
// Обновляем параметры в базе данных
|
||||||
const { createAndStoreNewCreate2Salt } = require('./secretStore');
|
console.log('💾 Обновляем параметры в базе данных...');
|
||||||
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
|
logger.info('💾 Обновляем параметры в базе данных...');
|
||||||
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
|
|
||||||
|
|
||||||
// Обновляем параметры в базе данных с CREATE2_SALT
|
await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'in_progress');
|
||||||
console.log('💾 Обновляем параметры в базе данных с CREATE2_SALT...');
|
logger.info(`✅ Параметры обновлены в БД для деплоя`);
|
||||||
logger.info('💾 Обновляем параметры в базе данных с CREATE2_SALT...');
|
|
||||||
|
|
||||||
const updatedParams = {
|
|
||||||
...deployParams,
|
|
||||||
CREATE2_SALT: create2Salt
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.deployParamsService.saveDeployParams(deploymentId, updatedParams, 'in_progress');
|
|
||||||
logger.info(`✅ Параметры обновлены в БД с CREATE2_SALT: ${create2Salt}`);
|
|
||||||
|
|
||||||
// WebSocket обновление: поиск RPC URLs
|
// WebSocket обновление: поиск RPC URLs
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
@@ -153,6 +133,8 @@ class DLEV2Service {
|
|||||||
// Обновляем параметры в базе данных с RPC URLs и initializer
|
// Обновляем параметры в базе данных с RPC URLs и initializer
|
||||||
const finalParams = {
|
const finalParams = {
|
||||||
...updatedParams,
|
...updatedParams,
|
||||||
|
// Сохраняем initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое
|
||||||
|
initialAmounts: dleParams.initialAmounts,
|
||||||
rpcUrls: rpcUrls, // Сохраняем как объект {chainId: url}
|
rpcUrls: rpcUrls, // Сохраняем как объект {chainId: url}
|
||||||
rpc_urls: Object.values(rpcUrls), // Также сохраняем как массив для совместимости
|
rpc_urls: Object.values(rpcUrls), // Также сохраняем как массив для совместимости
|
||||||
initializer: dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000"
|
initializer: dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000"
|
||||||
@@ -203,7 +185,18 @@ class DLEV2Service {
|
|||||||
const result = this.extractDeployResult(deployResult.stdout, deployParams);
|
const result = this.extractDeployResult(deployResult.stdout, deployParams);
|
||||||
|
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
throw new Error('Деплой не удался: ' + (result?.error || 'Неизвестная ошибка'));
|
// Логируем детали ошибки для отладки
|
||||||
|
logger.error('❌ Деплой не удался. Детали:');
|
||||||
|
logger.error(`📋 stdout: ${deployResult.stdout}`);
|
||||||
|
logger.error(`📋 stderr: ${deployResult.stderr}`);
|
||||||
|
logger.error(`📋 exitCode: ${deployResult.exitCode}`);
|
||||||
|
|
||||||
|
// Извлекаем конкретную ошибку из результата
|
||||||
|
const errorMessage = result?.error ||
|
||||||
|
deployResult.stderr ||
|
||||||
|
'Неизвестная ошибка';
|
||||||
|
|
||||||
|
throw new Error(`Деплой не удался: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем данные DLE
|
// Сохраняем данные DLE
|
||||||
@@ -218,8 +211,11 @@ class DLEV2Service {
|
|||||||
|
|
||||||
// Обновляем статус деплоя в базе данных
|
// Обновляем статус деплоя в базе данных
|
||||||
if (deploymentId && result.data.dleAddress) {
|
if (deploymentId && result.data.dleAddress) {
|
||||||
|
logger.info(`🔄 Обновляем адрес в БД: ${deploymentId} -> ${result.data.dleAddress}`);
|
||||||
await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result.data.dleAddress);
|
await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result.data.dleAddress);
|
||||||
logger.info(`✅ Статус деплоя обновлен в БД: ${deploymentId} -> completed`);
|
logger.info(`✅ Статус деплоя обновлен в БД: ${deploymentId} -> completed, адрес: ${result.data.dleAddress}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`⚠️ Не удалось обновить адрес в БД: deploymentId=${deploymentId}, dleAddress=${result.data?.dleAddress}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket обновление: финализация
|
// WebSocket обновление: финализация
|
||||||
@@ -411,14 +407,18 @@ class DLEV2Service {
|
|||||||
* @returns {Object|null} - Результат деплоя
|
* @returns {Object|null} - Результат деплоя
|
||||||
*/
|
*/
|
||||||
extractDeployResult(stdout, deployParams = null) {
|
extractDeployResult(stdout, deployParams = null) {
|
||||||
|
logger.info(`🔍 Анализируем вывод деплоя (${stdout.length} символов)`);
|
||||||
|
|
||||||
// Ищем MULTICHAIN_DEPLOY_RESULT в выводе
|
// Ищем MULTICHAIN_DEPLOY_RESULT в выводе
|
||||||
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/);
|
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/);
|
||||||
|
|
||||||
if (resultMatch) {
|
if (resultMatch) {
|
||||||
try {
|
try {
|
||||||
const deployResults = JSON.parse(resultMatch[1]);
|
const deployResults = JSON.parse(resultMatch[1]);
|
||||||
|
logger.info(`📊 Результаты деплоя: ${JSON.stringify(deployResults, null, 2)}`);
|
||||||
// Проверяем, что есть успешные деплои
|
// Проверяем, что есть успешные деплои
|
||||||
const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000');
|
const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000');
|
||||||
|
logger.info(`✅ Успешные деплои: ${successfulDeploys.length}, адреса: ${successfulDeploys.map(d => d.address).join(', ')}`);
|
||||||
|
|
||||||
if (successfulDeploys.length > 0) {
|
if (successfulDeploys.length > 0) {
|
||||||
return {
|
return {
|
||||||
@@ -442,6 +442,54 @@ class DLEV2Service {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Ошибка парсинга JSON результата:', e);
|
logger.error('Ошибка парсинга JSON результата:', e);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Если MULTICHAIN_DEPLOY_RESULT не найден, ищем другие индикаторы успеха
|
||||||
|
logger.warn('⚠️ MULTICHAIN_DEPLOY_RESULT не найден в выводе');
|
||||||
|
|
||||||
|
// Ищем индикаторы успешного деплоя
|
||||||
|
const successIndicators = [
|
||||||
|
'DLE deployment completed successfully',
|
||||||
|
'SUCCESS: All DLE addresses are identical',
|
||||||
|
'deployed at=',
|
||||||
|
'deployment SUCCESS'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasSuccessIndicator = successIndicators.some(indicator =>
|
||||||
|
stdout.includes(indicator)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSuccessIndicator) {
|
||||||
|
logger.info('✅ Найден индикатор успешного деплоя');
|
||||||
|
|
||||||
|
// Ищем адреса контрактов в выводе
|
||||||
|
const addressMatch = stdout.match(/deployed at=([0-9a-fA-Fx]+)/);
|
||||||
|
if (addressMatch) {
|
||||||
|
const contractAddress = addressMatch[1];
|
||||||
|
logger.info(`✅ Найден адрес контракта: ${contractAddress}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: contractAddress,
|
||||||
|
totalNetworks: 1,
|
||||||
|
successfulNetworks: 1,
|
||||||
|
// Добавляем данные из параметров деплоя
|
||||||
|
name: deployParams?.name || 'Unknown',
|
||||||
|
symbol: deployParams?.symbol || 'UNK',
|
||||||
|
location: deployParams?.location || 'Не указан',
|
||||||
|
coordinates: deployParams?.coordinates || '0,0',
|
||||||
|
jurisdiction: deployParams?.jurisdiction || 0,
|
||||||
|
quorumPercentage: deployParams?.quorumPercentage || 51,
|
||||||
|
logoURI: deployParams?.logoURI || '/uploads/logos/default-token.svg'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем последние строки вывода для отладки
|
||||||
|
const lines = stdout.split('\n');
|
||||||
|
const lastLines = lines.slice(-10).join('\n');
|
||||||
|
logger.info(`📋 Последние строки вывода:\n${lastLines}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
523
backend/services/unifiedDeploymentService.js
Normal file
523
backend/services/unifiedDeploymentService.js
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
/**
|
||||||
|
* Единый сервис для управления деплоем DLE
|
||||||
|
* Объединяет все операции с данными и деплоем
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
*/
|
||||||
|
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const DeployParamsService = require('./deployParamsService');
|
||||||
|
const deploymentTracker = require('../utils/deploymentTracker');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const etherscanV2 = require('./etherscanV2VerificationService');
|
||||||
|
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
// Убираем прямой импорт broadcastDeploymentUpdate - используем только deploymentTracker
|
||||||
|
|
||||||
|
class UnifiedDeploymentService {
|
||||||
|
constructor() {
|
||||||
|
this.deployParamsService = new DeployParamsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает новый деплой DLE с полным циклом
|
||||||
|
* @param {Object} dleParams - Параметры DLE из формы
|
||||||
|
* @param {string} deploymentId - ID деплоя (опционально)
|
||||||
|
* @returns {Promise<Object>} - Результат деплоя
|
||||||
|
*/
|
||||||
|
async createDLE(dleParams, deploymentId = null) {
|
||||||
|
try {
|
||||||
|
// 1. Генерируем ID деплоя
|
||||||
|
if (!deploymentId) {
|
||||||
|
deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🚀 Начало создания DLE: ${deploymentId}`);
|
||||||
|
|
||||||
|
// 2. Валидируем параметры
|
||||||
|
this.validateDLEParams(dleParams);
|
||||||
|
|
||||||
|
// 3. Подготавливаем параметры для деплоя
|
||||||
|
const deployParams = await this.prepareDeployParams(dleParams);
|
||||||
|
|
||||||
|
// 4. Сохраняем в БД
|
||||||
|
await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending');
|
||||||
|
logger.info(`💾 Параметры сохранены в БД: ${deploymentId}`);
|
||||||
|
|
||||||
|
// 5. Запускаем деплой
|
||||||
|
const result = await this.executeDeployment(deploymentId);
|
||||||
|
|
||||||
|
// 6. Сохраняем результат
|
||||||
|
await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result);
|
||||||
|
logger.info(`✅ Деплой завершен: ${deploymentId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deploymentId,
|
||||||
|
data: result
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Ошибка деплоя ${deploymentId}:`, error);
|
||||||
|
|
||||||
|
// Обновляем статус на ошибку
|
||||||
|
if (deploymentId) {
|
||||||
|
await this.deployParamsService.updateDeploymentStatus(deploymentId, 'failed', { error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует параметры DLE
|
||||||
|
* @param {Object} params - Параметры для валидации
|
||||||
|
*/
|
||||||
|
validateDLEParams(params) {
|
||||||
|
const required = ['name', 'symbol', 'privateKey', 'supportedChainIds'];
|
||||||
|
const missing = required.filter(field => !params[field]);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(`Отсутствуют обязательные поля: ${missing.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.quorumPercentage < 1 || params.quorumPercentage > 100) {
|
||||||
|
throw new Error('Кворум должен быть от 1 до 100 процентов');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.initialPartners || params.initialPartners.length === 0) {
|
||||||
|
throw new Error('Необходимо указать хотя бы одного партнера');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.initialAmounts || params.initialAmounts.length === 0) {
|
||||||
|
throw new Error('Необходимо указать начальные суммы для партнеров');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.initialPartners.length !== params.initialAmounts.length) {
|
||||||
|
throw new Error('Количество партнеров должно совпадать с количеством сумм');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.supportedChainIds || params.supportedChainIds.length === 0) {
|
||||||
|
throw new Error('Необходимо указать поддерживаемые сети');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подготавливает параметры для деплоя
|
||||||
|
* @param {Object} dleParams - Исходные параметры
|
||||||
|
* @returns {Promise<Object>} - Подготовленные параметры
|
||||||
|
*/
|
||||||
|
async prepareDeployParams(dleParams) {
|
||||||
|
// Генерируем RPC URLs на основе supportedChainIds из базы данных
|
||||||
|
const rpcUrls = await this.generateRpcUrls(dleParams.supportedChainIds || []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: dleParams.name,
|
||||||
|
symbol: dleParams.symbol,
|
||||||
|
location: dleParams.location || '',
|
||||||
|
coordinates: dleParams.coordinates || '',
|
||||||
|
jurisdiction: dleParams.jurisdiction || 1,
|
||||||
|
oktmo: dleParams.oktmo || 45000000000,
|
||||||
|
okved_codes: dleParams.okvedCodes || [],
|
||||||
|
kpp: dleParams.kpp || 770101001,
|
||||||
|
quorum_percentage: dleParams.quorumPercentage || 51,
|
||||||
|
initial_partners: dleParams.initialPartners || [],
|
||||||
|
// initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое
|
||||||
|
initial_amounts: dleParams.initialAmounts || [],
|
||||||
|
supported_chain_ids: dleParams.supportedChainIds || [],
|
||||||
|
current_chain_id: 1, // Governance chain всегда Ethereum
|
||||||
|
private_key: dleParams.privateKey,
|
||||||
|
etherscan_api_key: dleParams.etherscanApiKey,
|
||||||
|
logo_uri: dleParams.logoURI || '',
|
||||||
|
create2_salt: dleParams.CREATE2_SALT || `0x${Math.random().toString(16).substring(2).padStart(64, '0')}`,
|
||||||
|
auto_verify_after_deploy: dleParams.autoVerifyAfterDeploy || false,
|
||||||
|
modules_to_deploy: dleParams.modulesToDeploy || [],
|
||||||
|
rpc_urls: rpcUrls,
|
||||||
|
deployment_status: 'pending'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует RPC URLs на основе chain IDs из базы данных
|
||||||
|
* @param {Array} chainIds - Массив chain IDs
|
||||||
|
* @returns {Promise<Array>} - Массив RPC URLs
|
||||||
|
*/
|
||||||
|
async generateRpcUrls(chainIds) {
|
||||||
|
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||||
|
const rpcUrls = [];
|
||||||
|
|
||||||
|
for (const chainId of chainIds) {
|
||||||
|
try {
|
||||||
|
const rpcUrl = await getRpcUrlByChainId(chainId);
|
||||||
|
if (rpcUrl) {
|
||||||
|
rpcUrls.push(rpcUrl);
|
||||||
|
logger.info(`[RPC_GEN] Найден RPC для chainId ${chainId}: ${rpcUrl}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[RPC_GEN] RPC не найден для chainId ${chainId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[RPC_GEN] Ошибка получения RPC для chainId ${chainId}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpcUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет деплой контрактов
|
||||||
|
* @param {string} deploymentId - ID деплоя
|
||||||
|
* @returns {Promise<Object>} - Результат деплоя
|
||||||
|
*/
|
||||||
|
async executeDeployment(deploymentId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
|
||||||
|
|
||||||
|
logger.info(`🚀 Запуск деплоя: ${scriptPath}`);
|
||||||
|
|
||||||
|
const child = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
|
cwd: path.join(__dirname, '..'),
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
DEPLOYMENT_ID: deploymentId
|
||||||
|
},
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
stdout += output;
|
||||||
|
logger.info(`[DEPLOY] ${output.trim()}`);
|
||||||
|
|
||||||
|
// Определяем этап процесса по содержимому вывода
|
||||||
|
let progress = 50;
|
||||||
|
let message = 'Деплой в процессе...';
|
||||||
|
|
||||||
|
if (output.includes('Генерация ABI файла')) {
|
||||||
|
progress = 10;
|
||||||
|
message = 'Генерация ABI файла...';
|
||||||
|
} else if (output.includes('Генерация flattened контракта')) {
|
||||||
|
progress = 20;
|
||||||
|
message = 'Генерация flattened контракта...';
|
||||||
|
} else if (output.includes('Compiled') && output.includes('Solidity files')) {
|
||||||
|
progress = 30;
|
||||||
|
message = 'Компиляция контрактов...';
|
||||||
|
} else if (output.includes('Загружены параметры')) {
|
||||||
|
progress = 40;
|
||||||
|
message = 'Загрузка параметров деплоя...';
|
||||||
|
} else if (output.includes('deploying DLE directly')) {
|
||||||
|
progress = 60;
|
||||||
|
message = 'Деплой контрактов в сети...';
|
||||||
|
} else if (output.includes('Верификация в сети')) {
|
||||||
|
progress = 80;
|
||||||
|
message = 'Верификация контрактов...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket сообщение о прогрессе через deploymentTracker
|
||||||
|
deploymentTracker.updateDeployment(deploymentId, {
|
||||||
|
status: 'in_progress',
|
||||||
|
progress: progress,
|
||||||
|
message: message,
|
||||||
|
output: output.trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
stderr += output;
|
||||||
|
logger.error(`[DEPLOY ERROR] ${output.trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
try {
|
||||||
|
const result = this.parseDeployResult(stdout);
|
||||||
|
|
||||||
|
// Сохраняем результат в БД
|
||||||
|
this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result)
|
||||||
|
.then(() => {
|
||||||
|
logger.info(`✅ Результат деплоя сохранен в БД: ${deploymentId}`);
|
||||||
|
|
||||||
|
// Отправляем WebSocket сообщение о завершении через deploymentTracker
|
||||||
|
deploymentTracker.completeDeployment(deploymentId, result);
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
})
|
||||||
|
.catch(dbError => {
|
||||||
|
logger.error(`❌ Ошибка сохранения результата в БД: ${dbError.message}`);
|
||||||
|
resolve(result); // Все равно возвращаем результат
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Ошибка парсинга результата: ${error.message}`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Логируем детали ошибки для отладки
|
||||||
|
logger.error(`❌ Деплой завершился с ошибкой (код ${code})`);
|
||||||
|
logger.error(`📋 stdout: ${stdout}`);
|
||||||
|
logger.error(`📋 stderr: ${stderr}`);
|
||||||
|
|
||||||
|
// Извлекаем конкретную ошибку из вывода
|
||||||
|
const errorMessage = stderr || stdout || 'Неизвестная ошибка';
|
||||||
|
|
||||||
|
// Отправляем WebSocket сообщение об ошибке через deploymentTracker
|
||||||
|
deploymentTracker.failDeployment(deploymentId, new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`));
|
||||||
|
|
||||||
|
reject(new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
reject(new Error(`Ошибка запуска деплоя: ${error.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит результат деплоя из вывода скрипта
|
||||||
|
* @param {string} stdout - Вывод скрипта
|
||||||
|
* @returns {Object} - Структурированный результат
|
||||||
|
*/
|
||||||
|
parseDeployResult(stdout) {
|
||||||
|
try {
|
||||||
|
logger.info(`🔍 Анализируем вывод деплоя (${stdout.length} символов)`);
|
||||||
|
|
||||||
|
// Ищем MULTICHAIN_DEPLOY_RESULT в выводе
|
||||||
|
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/);
|
||||||
|
if (resultMatch) {
|
||||||
|
const jsonStr = resultMatch[1].trim();
|
||||||
|
const deployResults = JSON.parse(jsonStr);
|
||||||
|
logger.info(`📊 Результаты деплоя: ${JSON.stringify(deployResults, null, 2)}`);
|
||||||
|
|
||||||
|
// Проверяем, что есть успешные деплои
|
||||||
|
const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000' && !r.error);
|
||||||
|
|
||||||
|
if (successfulDeploys.length > 0) {
|
||||||
|
const dleAddress = successfulDeploys[0].address;
|
||||||
|
logger.info(`✅ DLE адрес: ${dleAddress}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
networks: deployResults.map(result => ({
|
||||||
|
chainId: result.chainId,
|
||||||
|
address: result.address,
|
||||||
|
success: result.address && result.address !== '0x0000000000000000000000000000000000000000' && !result.error,
|
||||||
|
error: result.error || null,
|
||||||
|
verification: result.verification || 'pending'
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
message: `DLE успешно развернут в ${successfulDeploys.length} сетях`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Если нет успешных деплоев, но есть результаты, возвращаем их с ошибками
|
||||||
|
const failedDeploys = deployResults.filter(r => r.error);
|
||||||
|
logger.warn(`⚠️ Все деплои неудачны. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: {
|
||||||
|
networks: deployResults.map(result => ({
|
||||||
|
chainId: result.chainId,
|
||||||
|
address: result.address || null,
|
||||||
|
success: false,
|
||||||
|
error: result.error || 'Unknown error'
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
message: `Деплой неудачен во всех сетях. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: создаем результат из текста
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Деплой выполнен успешно',
|
||||||
|
output: stdout
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Ошибка парсинга результата деплоя:', error);
|
||||||
|
throw new Error(`Не удалось распарсить результат деплоя: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает статус деплоя
|
||||||
|
* @param {string} deploymentId - ID деплоя
|
||||||
|
* @returns {Object} - Статус деплоя
|
||||||
|
*/
|
||||||
|
async getDeploymentStatus(deploymentId) {
|
||||||
|
return await this.deployParamsService.getDeployParams(deploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все деплои
|
||||||
|
* @returns {Array} - Список деплоев
|
||||||
|
*/
|
||||||
|
async getAllDeployments() {
|
||||||
|
return await this.deployParamsService.getAllDeployments();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все DLE из файлов (для совместимости)
|
||||||
|
* @returns {Array} - Список DLE
|
||||||
|
*/
|
||||||
|
getAllDLEs() {
|
||||||
|
try {
|
||||||
|
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||||
|
if (!fs.existsSync(dlesDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(dlesDir);
|
||||||
|
const dles = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.includes('dle-v2-') && file.endsWith('.json')) {
|
||||||
|
const filePath = path.join(dlesDir, file);
|
||||||
|
try {
|
||||||
|
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
if (dleData.dleAddress) {
|
||||||
|
dles.push(dleData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Ошибка при чтении файла ${file}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dles;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении списка DLE:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автоматическая верификация контрактов во всех сетях
|
||||||
|
* @param {Object} params - Параметры верификации
|
||||||
|
* @returns {Promise<Object>} - Результат верификации
|
||||||
|
*/
|
||||||
|
async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) {
|
||||||
|
try {
|
||||||
|
logger.info('🔍 Начинаем автоматическую верификацию контрактов');
|
||||||
|
|
||||||
|
if (!deployResult?.data?.networks) {
|
||||||
|
throw new Error('Нет данных о сетях для верификации');
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationResults = [];
|
||||||
|
|
||||||
|
for (const network of deployResult.data.networks) {
|
||||||
|
try {
|
||||||
|
logger.info(`🔍 Верификация в сети ${network.chainId}...`);
|
||||||
|
|
||||||
|
const result = await etherscanV2.verifyContract({
|
||||||
|
contractAddress: network.dleAddress,
|
||||||
|
chainId: network.chainId,
|
||||||
|
deployParams,
|
||||||
|
apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
verificationResults.push({
|
||||||
|
chainId: network.chainId,
|
||||||
|
address: network.dleAddress,
|
||||||
|
success: result.success,
|
||||||
|
guid: result.guid,
|
||||||
|
message: result.message
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`✅ Верификация в сети ${network.chainId} завершена`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Ошибка верификации в сети ${network.chainId}:`, error);
|
||||||
|
verificationResults.push({
|
||||||
|
chainId: network.chainId,
|
||||||
|
address: network.dleAddress,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: verificationResults
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Ошибка автоматической верификации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет балансы в указанных сетях
|
||||||
|
* @param {Array} chainIds - Список ID сетей
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @returns {Promise<Object>} - Результат проверки
|
||||||
|
*/
|
||||||
|
async checkBalances(chainIds, privateKey) {
|
||||||
|
try {
|
||||||
|
logger.info(`💰 Проверка балансов в ${chainIds.length} сетях`);
|
||||||
|
|
||||||
|
const wallet = new ethers.Wallet(privateKey);
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const chainId of chainIds) {
|
||||||
|
try {
|
||||||
|
const rpcUrl = await getRpcUrlByChainId(chainId);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
results.push({
|
||||||
|
chainId,
|
||||||
|
success: false,
|
||||||
|
error: `RPC URL не найден для сети ${chainId}`
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убеждаемся, что rpcUrl - это строка
|
||||||
|
const rpcUrlString = typeof rpcUrl === 'string' ? rpcUrl : rpcUrl.toString();
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrlString);
|
||||||
|
const balance = await provider.getBalance(wallet.address);
|
||||||
|
const balanceEth = ethers.formatEther(balance);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
chainId,
|
||||||
|
success: true,
|
||||||
|
address: wallet.address,
|
||||||
|
balance: balanceEth,
|
||||||
|
balanceWei: balance.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`💰 Сеть ${chainId}: ${balanceEth} ETH`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Ошибка проверки баланса в сети ${chainId}:`, error);
|
||||||
|
results.push({
|
||||||
|
chainId,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Ошибка проверки балансов:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрывает соединения
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
await this.deployParamsService.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UnifiedDeploymentService;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,286 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This software is proprietary and confidential.
|
|
||||||
* Unauthorized copying, modification, or distribution is prohibited.
|
|
||||||
*
|
|
||||||
* For licensing inquiries: info@hb3-accelerator.com
|
|
||||||
* Website: https://hb3-accelerator.com
|
|
||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { ethers } = require('ethers');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Менеджер nonce для синхронизации транзакций в мультичейн-деплое
|
|
||||||
* Обеспечивает правильную последовательность транзакций без конфликтов
|
|
||||||
*/
|
|
||||||
class NonceManager {
|
|
||||||
constructor() {
|
|
||||||
this.nonceCache = new Map(); // Кэш nonce для каждого кошелька
|
|
||||||
this.pendingTransactions = new Map(); // Ожидающие транзакции
|
|
||||||
this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить актуальный nonce для кошелька в сети
|
|
||||||
* @param {string} rpcUrl - URL RPC провайдера
|
|
||||||
* @param {string} walletAddress - Адрес кошелька
|
|
||||||
* @param {boolean} usePending - Использовать pending транзакции
|
|
||||||
* @returns {Promise<number>} Актуальный nonce
|
|
||||||
*/
|
|
||||||
async getCurrentNonce(rpcUrl, walletAddress, usePending = true) {
|
|
||||||
const key = `${walletAddress}-${rpcUrl}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Создаем провайдер из rpcUrl
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true });
|
|
||||||
|
|
||||||
const nonce = await Promise.race([
|
|
||||||
provider.getTransactionCount(walletAddress, usePending ? 'pending' : 'latest'),
|
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Nonce timeout')), 30000))
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Получен nonce для ${walletAddress} в сети ${rpcUrl}: ${nonce}`);
|
|
||||||
return nonce;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[NonceManager] Ошибка получения nonce для ${walletAddress}:`, error.message);
|
|
||||||
|
|
||||||
// Если сеть недоступна, возвращаем 0 как fallback
|
|
||||||
if (error.message.includes('network is not available') || error.message.includes('NETWORK_ERROR')) {
|
|
||||||
console.warn(`[NonceManager] Сеть недоступна, используем nonce 0 для ${walletAddress}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Заблокировать nonce для транзакции
|
|
||||||
* @param {ethers.Wallet} wallet - Кошелек
|
|
||||||
* @param {ethers.Provider} provider - Провайдер сети
|
|
||||||
* @returns {Promise<number>} Заблокированный nonce
|
|
||||||
*/
|
|
||||||
async lockNonce(rpcUrl, walletAddress) {
|
|
||||||
const key = `${walletAddress}-${rpcUrl}`;
|
|
||||||
|
|
||||||
// Ждем освобождения блокировки
|
|
||||||
while (this.locks.has(key)) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Устанавливаем блокировку
|
|
||||||
this.locks.set(key, true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const currentNonce = await this.getCurrentNonce(rpcUrl, walletAddress);
|
|
||||||
const lockedNonce = currentNonce;
|
|
||||||
|
|
||||||
// Обновляем кэш
|
|
||||||
this.nonceCache.set(key, lockedNonce + 1);
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Заблокирован nonce ${lockedNonce} для ${walletAddress} в сети ${rpcUrl}`);
|
|
||||||
return lockedNonce;
|
|
||||||
} finally {
|
|
||||||
// Освобождаем блокировку
|
|
||||||
this.locks.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Освободить nonce после успешной транзакции
|
|
||||||
* @param {ethers.Wallet} wallet - Кошелек
|
|
||||||
* @param {ethers.Provider} provider - Провайдер сети
|
|
||||||
* @param {number} nonce - Использованный nonce
|
|
||||||
*/
|
|
||||||
releaseNonce(rpcUrl, walletAddress, nonce) {
|
|
||||||
const key = `${walletAddress}-${rpcUrl}`;
|
|
||||||
const cachedNonce = this.nonceCache.get(key) || 0;
|
|
||||||
|
|
||||||
if (nonce >= cachedNonce) {
|
|
||||||
this.nonceCache.set(key, nonce + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Освобожден nonce ${nonce} для ${walletAddress} в сети ${rpcUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Синхронизировать nonce между сетями
|
|
||||||
* @param {Array} networks - Массив сетей с кошельками
|
|
||||||
* @returns {Promise<number>} Синхронизированный nonce
|
|
||||||
*/
|
|
||||||
async synchronizeNonce(networks) {
|
|
||||||
console.log(`[NonceManager] Начинаем синхронизацию nonce для ${networks.length} сетей`);
|
|
||||||
|
|
||||||
// Получаем nonce для всех сетей
|
|
||||||
const nonces = await Promise.all(
|
|
||||||
networks.map(async (network, index) => {
|
|
||||||
try {
|
|
||||||
const nonce = await this.getCurrentNonce(network.rpcUrl, network.wallet.address);
|
|
||||||
console.log(`[NonceManager] Сеть ${index + 1}/${networks.length} (${network.chainId}): nonce=${nonce}`);
|
|
||||||
return { chainId: network.chainId, nonce, index };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[NonceManager] Ошибка получения nonce для сети ${network.chainId}:`, error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Находим максимальный nonce
|
|
||||||
const maxNonce = Math.max(...nonces.map(n => n.nonce));
|
|
||||||
console.log(`[NonceManager] Максимальный nonce: ${maxNonce}`);
|
|
||||||
|
|
||||||
// Выравниваем nonce во всех сетях
|
|
||||||
for (const network of networks) {
|
|
||||||
const currentNonce = nonces.find(n => n.chainId === network.chainId)?.nonce || 0;
|
|
||||||
|
|
||||||
if (currentNonce < maxNonce) {
|
|
||||||
console.log(`[NonceManager] Выравниваем nonce в сети ${network.chainId} с ${currentNonce} до ${maxNonce}`);
|
|
||||||
await this.alignNonce(network.wallet, network.provider, currentNonce, maxNonce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Синхронизация nonce завершена. Целевой nonce: ${maxNonce}`);
|
|
||||||
return maxNonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Выровнять nonce до целевого значения
|
|
||||||
* @param {ethers.Wallet} wallet - Кошелек
|
|
||||||
* @param {ethers.Provider} provider - Провайдер сети
|
|
||||||
* @param {number} currentNonce - Текущий nonce
|
|
||||||
* @param {number} targetNonce - Целевой nonce
|
|
||||||
*/
|
|
||||||
async alignNonce(wallet, provider, currentNonce, targetNonce) {
|
|
||||||
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
|
||||||
let nonce = currentNonce;
|
|
||||||
|
|
||||||
while (nonce < targetNonce) {
|
|
||||||
try {
|
|
||||||
// Получаем актуальный nonce перед каждой транзакцией
|
|
||||||
const actualNonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
|
||||||
if (actualNonce > nonce) {
|
|
||||||
nonce = actualNonce;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const feeOverrides = await this.getFeeOverrides(provider);
|
|
||||||
const txReq = {
|
|
||||||
to: burnAddress,
|
|
||||||
value: 0n,
|
|
||||||
nonce: nonce,
|
|
||||||
gasLimit: 21000,
|
|
||||||
...feeOverrides
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Отправляем заполняющую транзакцию nonce=${nonce} в сети ${provider._network?.chainId}`);
|
|
||||||
const tx = await wallet.sendTransaction(txReq);
|
|
||||||
await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Заполняющая транзакция nonce=${nonce} подтверждена в сети ${provider._network?.chainId}`);
|
|
||||||
nonce++;
|
|
||||||
|
|
||||||
// Небольшая задержка между транзакциями
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[NonceManager] Ошибка заполняющей транзакции nonce=${nonce}:`, error.message);
|
|
||||||
|
|
||||||
if (error.message.includes('nonce too low')) {
|
|
||||||
// Обновляем nonce и пробуем снова
|
|
||||||
nonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить параметры комиссии для сети
|
|
||||||
* @param {ethers.Provider} provider - Провайдер сети
|
|
||||||
* @returns {Promise<Object>} Параметры комиссии
|
|
||||||
*/
|
|
||||||
async getFeeOverrides(provider) {
|
|
||||||
try {
|
|
||||||
const feeData = await provider.getFeeData();
|
|
||||||
|
|
||||||
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
|
|
||||||
return {
|
|
||||||
maxFeePerGas: feeData.maxFeePerGas,
|
|
||||||
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
gasPrice: feeData.gasPrice
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[NonceManager] Ошибка получения fee data:`, error.message);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Безопасная отправка транзакции с правильным nonce
|
|
||||||
* @param {ethers.Wallet} wallet - Кошелек
|
|
||||||
* @param {ethers.Provider} provider - Провайдер сети
|
|
||||||
* @param {Object} txData - Данные транзакции
|
|
||||||
* @param {number} maxRetries - Максимальное количество попыток
|
|
||||||
* @returns {Promise<ethers.TransactionResponse>} Результат транзакции
|
|
||||||
*/
|
|
||||||
async sendTransactionSafely(wallet, provider, txData, maxRetries = 1) {
|
|
||||||
const rpcUrl = provider._getConnection().url;
|
|
||||||
const walletAddress = wallet.address;
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
||||||
try {
|
|
||||||
// Получаем актуальный nonce
|
|
||||||
const nonce = await this.lockNonce(rpcUrl, walletAddress);
|
|
||||||
|
|
||||||
const tx = await wallet.sendTransaction({
|
|
||||||
...txData,
|
|
||||||
nonce: nonce
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[NonceManager] Транзакция отправлена с nonce=${nonce} в сети ${provider._network?.chainId}`);
|
|
||||||
|
|
||||||
// Ждем подтверждения
|
|
||||||
await tx.wait();
|
|
||||||
|
|
||||||
// Освобождаем nonce
|
|
||||||
this.releaseNonce(rpcUrl, walletAddress, nonce);
|
|
||||||
|
|
||||||
return tx;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[NonceManager] Попытка ${attempt + 1}/${maxRetries} неудачна:`, error.message);
|
|
||||||
|
|
||||||
if (error.message.includes('nonce too low') && attempt < maxRetries - 1) {
|
|
||||||
// Обновляем nonce и пробуем снова
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt === maxRetries - 1) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить кэш nonce
|
|
||||||
*/
|
|
||||||
clearCache() {
|
|
||||||
this.nonceCache.clear();
|
|
||||||
this.pendingTransactions.clear();
|
|
||||||
this.locks.clear();
|
|
||||||
console.log(`[NonceManager] Кэш nonce очищен`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = NonceManager;
|
|
||||||
107
backend/utils/apiKeyManager.js
Normal file
107
backend/utils/apiKeyManager.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Централизованный менеджер API ключей
|
||||||
|
* Унифицирует работу с API ключами Etherscan
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
*/
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
class ApiKeyManager {
|
||||||
|
/**
|
||||||
|
* Получает API ключ Etherscan из различных источников
|
||||||
|
* @param {Object} params - Параметры деплоя
|
||||||
|
* @param {Object} reqBody - Тело запроса (опционально)
|
||||||
|
* @returns {string|null} - API ключ или null
|
||||||
|
*/
|
||||||
|
static getEtherscanApiKey(params = {}, reqBody = {}) {
|
||||||
|
// Приоритет источников:
|
||||||
|
// 1. Из параметров деплоя (БД)
|
||||||
|
// 2. Из тела запроса (фронтенд)
|
||||||
|
// 3. Из переменных окружения
|
||||||
|
// 4. Из секретов
|
||||||
|
|
||||||
|
let apiKey = null;
|
||||||
|
|
||||||
|
// 1. Из параметров деплоя (БД) - приоритет 1
|
||||||
|
if (params.etherscan_api_key) {
|
||||||
|
apiKey = params.etherscan_api_key;
|
||||||
|
logger.info('[API_KEY] ✅ Ключ получен из параметров деплоя (БД)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Из тела запроса (фронтенд) - приоритет 2
|
||||||
|
else if (reqBody.etherscanApiKey) {
|
||||||
|
apiKey = reqBody.etherscanApiKey;
|
||||||
|
logger.info('[API_KEY] ✅ Ключ получен из тела запроса (фронтенд)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Из переменных окружения - приоритет 3
|
||||||
|
else if (process.env.ETHERSCAN_API_KEY) {
|
||||||
|
apiKey = process.env.ETHERSCAN_API_KEY;
|
||||||
|
logger.info('[API_KEY] ✅ Ключ получен из переменных окружения');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Из секретов - приоритет 4
|
||||||
|
else if (process.env.ETHERSCAN_V2_API_KEY) {
|
||||||
|
apiKey = process.env.ETHERSCAN_V2_API_KEY;
|
||||||
|
logger.info('[API_KEY] ✅ Ключ получен из секретов');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
logger.info(`[API_KEY] 🔑 API ключ найден: ${apiKey.substring(0, 8)}...`);
|
||||||
|
return apiKey;
|
||||||
|
} else {
|
||||||
|
logger.warn('[API_KEY] ⚠️ API ключ Etherscan не найден');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает API ключ в переменные окружения
|
||||||
|
* @param {string} apiKey - API ключ
|
||||||
|
*/
|
||||||
|
static setEtherscanApiKey(apiKey) {
|
||||||
|
if (apiKey) {
|
||||||
|
process.env.ETHERSCAN_API_KEY = apiKey;
|
||||||
|
logger.info(`[API_KEY] 🔧 API ключ установлен в переменные окружения: ${apiKey.substring(0, 8)}...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет наличие API ключа
|
||||||
|
* @param {string} apiKey - API ключ для проверки
|
||||||
|
* @returns {boolean} - true если ключ валидный
|
||||||
|
*/
|
||||||
|
static validateApiKey(apiKey) {
|
||||||
|
if (!apiKey || typeof apiKey !== 'string') {
|
||||||
|
logger.warn('[API_KEY] ❌ API ключ не валидный: пустой или не строка');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.length < 10) {
|
||||||
|
logger.warn('[API_KEY] ❌ API ключ слишком короткий');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[API_KEY] ✅ API ключ валидный');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает и устанавливает API ключ (универсальный метод)
|
||||||
|
* @param {Object} params - Параметры деплоя
|
||||||
|
* @param {Object} reqBody - Тело запроса (опционально)
|
||||||
|
* @returns {string|null} - API ключ или null
|
||||||
|
*/
|
||||||
|
static getAndSetEtherscanApiKey(params = {}, reqBody = {}) {
|
||||||
|
const apiKey = this.getEtherscanApiKey(params, reqBody);
|
||||||
|
|
||||||
|
if (apiKey && this.validateApiKey(apiKey)) {
|
||||||
|
this.setEtherscanApiKey(apiKey);
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiKeyManager;
|
||||||
153
backend/utils/constructorArgsGenerator.js
Normal file
153
backend/utils/constructorArgsGenerator.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* Централизованный генератор параметров конструктора для DLE контракта
|
||||||
|
* Обеспечивает одинаковые параметры для деплоя и верификации
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует параметры конструктора для DLE контракта
|
||||||
|
* @param {Object} params - Параметры деплоя из базы данных
|
||||||
|
* @param {number} chainId - ID сети для деплоя (опционально)
|
||||||
|
* @returns {Object} Объект с параметрами конструктора
|
||||||
|
*/
|
||||||
|
function generateDLEConstructorArgs(params, chainId = null) {
|
||||||
|
// Валидация обязательных параметров
|
||||||
|
if (!params) {
|
||||||
|
throw new Error('Параметры деплоя не переданы');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Базовые параметры DLE
|
||||||
|
const dleConfig = {
|
||||||
|
name: params.name || '',
|
||||||
|
symbol: params.symbol || '',
|
||||||
|
location: params.location || '',
|
||||||
|
coordinates: params.coordinates || '',
|
||||||
|
jurisdiction: params.jurisdiction || 0,
|
||||||
|
okvedCodes: params.okvedCodes || [],
|
||||||
|
kpp: params.kpp ? BigInt(params.kpp) : 0n,
|
||||||
|
quorumPercentage: params.quorumPercentage || 50,
|
||||||
|
initialPartners: params.initialPartners || [],
|
||||||
|
// Умножаем initialAmounts на 1e18 для конвертации в wei
|
||||||
|
initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(1e18)),
|
||||||
|
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Определяем initializer
|
||||||
|
const initializer = params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
|
return {
|
||||||
|
dleConfig,
|
||||||
|
initializer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует параметры конструктора для верификации (с преобразованием в строки)
|
||||||
|
* @param {Object} params - Параметры деплоя из базы данных
|
||||||
|
* @param {number} chainId - ID сети для верификации (опционально)
|
||||||
|
* @returns {Array} Массив параметров конструктора для верификации
|
||||||
|
*/
|
||||||
|
function generateVerificationArgs(params, chainId = null) {
|
||||||
|
const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId);
|
||||||
|
|
||||||
|
// Для верификации нужно преобразовать BigInt в строки
|
||||||
|
const verificationConfig = {
|
||||||
|
...dleConfig,
|
||||||
|
initialAmounts: dleConfig.initialAmounts.map(amount => amount.toString()),
|
||||||
|
supportedChainIds: dleConfig.supportedChainIds.map(id => id.toString())
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
verificationConfig,
|
||||||
|
initializer
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует параметры конструктора для деплоя (с BigInt)
|
||||||
|
* @param {Object} params - Параметры деплоя из базы данных
|
||||||
|
* @param {number} chainId - ID сети для деплоя (опционально)
|
||||||
|
* @returns {Object} Объект с параметрами конструктора для деплоя
|
||||||
|
*/
|
||||||
|
function generateDeploymentArgs(params, chainId = null) {
|
||||||
|
const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dleConfig,
|
||||||
|
initializer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует параметры конструктора
|
||||||
|
* @param {Object} params - Параметры деплоя
|
||||||
|
* @returns {Object} Результат валидации
|
||||||
|
*/
|
||||||
|
function validateConstructorArgs(params) {
|
||||||
|
const errors = [];
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
// Проверяем обязательные поля
|
||||||
|
if (!params.name) errors.push('name не указан');
|
||||||
|
if (!params.symbol) errors.push('symbol не указан');
|
||||||
|
if (!params.location) errors.push('location не указан');
|
||||||
|
if (!params.coordinates) errors.push('coordinates не указаны');
|
||||||
|
if (!params.jurisdiction) errors.push('jurisdiction не указан');
|
||||||
|
if (!params.okvedCodes || !Array.isArray(params.okvedCodes)) errors.push('okvedCodes не указан или не является массивом');
|
||||||
|
if (!params.initialPartners || !Array.isArray(params.initialPartners)) errors.push('initialPartners не указан или не является массивом');
|
||||||
|
if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) errors.push('initialAmounts не указан или не является массивом');
|
||||||
|
if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds)) errors.push('supportedChainIds не указан или не является массивом');
|
||||||
|
|
||||||
|
// Проверяем соответствие массивов
|
||||||
|
if (params.initialPartners && params.initialAmounts &&
|
||||||
|
params.initialPartners.length !== params.initialAmounts.length) {
|
||||||
|
errors.push('Количество initialPartners не соответствует количеству initialAmounts');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем значения
|
||||||
|
if (params.quorumPercentage && (params.quorumPercentage < 1 || params.quorumPercentage > 100)) {
|
||||||
|
warnings.push('quorumPercentage должен быть от 1 до 100');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.initialAmounts) {
|
||||||
|
const negativeAmounts = params.initialAmounts.filter(amount => amount < 0);
|
||||||
|
if (negativeAmounts.length > 0) {
|
||||||
|
errors.push('initialAmounts содержит отрицательные значения');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Логирует параметры конструктора для отладки
|
||||||
|
* @param {Object} params - Параметры деплоя
|
||||||
|
* @param {string} context - Контекст (deployment/verification)
|
||||||
|
*/
|
||||||
|
function logConstructorArgs(params, context = 'unknown') {
|
||||||
|
console.log(`📊 [${context.toUpperCase()}] Параметры конструктора:`);
|
||||||
|
console.log(` name: "${params.name}"`);
|
||||||
|
console.log(` symbol: "${params.symbol}"`);
|
||||||
|
console.log(` location: "${params.location}"`);
|
||||||
|
console.log(` coordinates: "${params.coordinates}"`);
|
||||||
|
console.log(` jurisdiction: ${params.jurisdiction}`);
|
||||||
|
console.log(` okvedCodes: [${params.okvedCodes.join(', ')}]`);
|
||||||
|
console.log(` kpp: ${params.kpp}`);
|
||||||
|
console.log(` quorumPercentage: ${params.quorumPercentage}`);
|
||||||
|
console.log(` initialPartners: [${params.initialPartners.join(', ')}]`);
|
||||||
|
console.log(` initialAmounts: [${params.initialAmounts.join(', ')}]`);
|
||||||
|
console.log(` supportedChainIds: [${params.supportedChainIds.join(', ')}]`);
|
||||||
|
console.log(` governanceChainId: 1 (Ethereum)`);
|
||||||
|
console.log(` initializer: ${params.initializer}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateDLEConstructorArgs,
|
||||||
|
generateVerificationArgs,
|
||||||
|
generateDeploymentArgs,
|
||||||
|
validateConstructorArgs,
|
||||||
|
logConstructorArgs
|
||||||
|
};
|
||||||
226
backend/utils/deploymentUtils.js
Normal file
226
backend/utils/deploymentUtils.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Общие утилиты для деплоя контрактов
|
||||||
|
* Устраняет дублирование кода между скриптами деплоя
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const RPCConnectionManager = require('./rpcConnectionManager');
|
||||||
|
const { nonceManager } = require('./nonceManager');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подбирает безопасные gas/fee для разных сетей (включая L2)
|
||||||
|
* @param {Object} provider - Провайдер ethers
|
||||||
|
* @param {Object} options - Опции для настройки
|
||||||
|
* @returns {Promise<Object>} - Объект с настройками газа
|
||||||
|
*/
|
||||||
|
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
|
||||||
|
try {
|
||||||
|
const fee = await provider.getFeeData();
|
||||||
|
const overrides = {};
|
||||||
|
const minPriority = await ethers.parseUnits(minPriorityGwei.toString(), 'gwei');
|
||||||
|
const minFee = await 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;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении fee overrides:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает провайдер и кошелек для деплоя
|
||||||
|
* @param {string} rpcUrl - URL RPC
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @returns {Object} - Объект с провайдером и кошельком
|
||||||
|
*/
|
||||||
|
function createProviderAndWallet(rpcUrl, privateKey) {
|
||||||
|
try {
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
return { provider, wallet };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при создании провайдера и кошелька:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выравнивает nonce до целевого значения
|
||||||
|
* @param {Object} wallet - Кошелек ethers
|
||||||
|
* @param {Object} provider - Провайдер ethers
|
||||||
|
* @param {number} targetNonce - Целевой nonce
|
||||||
|
* @param {Object} options - Опции для настройки
|
||||||
|
* @returns {Promise<number>} - Текущий nonce после выравнивания
|
||||||
|
*/
|
||||||
|
async function alignNonce(wallet, provider, targetNonce, options = {}) {
|
||||||
|
try {
|
||||||
|
// Используем nonceManager для получения актуального nonce
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = Number(network.chainId);
|
||||||
|
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
|
||||||
|
|
||||||
|
let current = await nonceManager.getNonceFast(wallet.address, rpcUrl, chainId);
|
||||||
|
|
||||||
|
if (current > targetNonce) {
|
||||||
|
throw new Error(`Current nonce ${current} > target nonce ${targetNonce}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current < targetNonce) {
|
||||||
|
logger.info(`Выравнивание nonce: ${current} -> ${targetNonce} (${targetNonce - current} транзакций)`);
|
||||||
|
|
||||||
|
const { burnAddress = '0x000000000000000000000000000000000000dEaD' } = options;
|
||||||
|
|
||||||
|
for (let i = current; i < targetNonce; i++) {
|
||||||
|
const overrides = await getFeeOverrides(provider);
|
||||||
|
const gasLimit = 21000n;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const txFill = await wallet.sendTransaction({
|
||||||
|
to: burnAddress,
|
||||||
|
value: 0,
|
||||||
|
gasLimit,
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Filler tx sent, hash=${txFill.hash}, nonce=${i}`);
|
||||||
|
|
||||||
|
await txFill.wait();
|
||||||
|
logger.info(`Filler tx confirmed, hash=${txFill.hash}, nonce=${i}`);
|
||||||
|
|
||||||
|
// Обновляем nonce в кэше
|
||||||
|
nonceManager.reserveNonce(wallet.address, chainId, i);
|
||||||
|
current = i + 1;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Filler tx failed for nonce=${i}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Nonce alignment completed, current nonce=${current}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`Nonce already aligned at ${current}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при выравнивании nonce:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает информацию о сети
|
||||||
|
* @param {Object} provider - Провайдер ethers
|
||||||
|
* @returns {Promise<Object>} - Информация о сети
|
||||||
|
*/
|
||||||
|
async function getNetworkInfo(provider) {
|
||||||
|
try {
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
return {
|
||||||
|
chainId: Number(network.chainId),
|
||||||
|
name: network.name
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении информации о сети:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет баланс кошелька
|
||||||
|
* @param {Object} provider - Провайдер ethers
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @returns {Promise<string>} - Баланс в ETH
|
||||||
|
*/
|
||||||
|
async function getBalance(provider, address) {
|
||||||
|
try {
|
||||||
|
const balance = await provider.getBalance(address);
|
||||||
|
return ethers.formatEther(balance);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении баланса:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает RPC соединение с retry логикой
|
||||||
|
* @param {string} rpcUrl - URL RPC
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {Object} options - Опции соединения
|
||||||
|
* @returns {Promise<Object>} - {provider, wallet, network}
|
||||||
|
*/
|
||||||
|
async function createRPCConnection(rpcUrl, privateKey, options = {}) {
|
||||||
|
const rpcManager = new RPCConnectionManager();
|
||||||
|
return await rpcManager.createConnection(rpcUrl, privateKey, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает множественные RPC соединения с обработкой ошибок
|
||||||
|
* @param {Array} rpcUrls - Массив RPC URL
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {Object} options - Опции соединения
|
||||||
|
* @returns {Promise<Array>} - Массив успешных соединений
|
||||||
|
*/
|
||||||
|
async function createMultipleRPCConnections(rpcUrls, privateKey, options = {}) {
|
||||||
|
const rpcManager = new RPCConnectionManager();
|
||||||
|
return await rpcManager.createMultipleConnections(rpcUrls, privateKey, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет транзакцию с retry логикой
|
||||||
|
* @param {Object} wallet - Кошелек
|
||||||
|
* @param {Object} txData - Данные транзакции
|
||||||
|
* @param {Object} options - Опции
|
||||||
|
* @returns {Promise<Object>} - Результат транзакции
|
||||||
|
*/
|
||||||
|
async function sendTransactionWithRetry(wallet, txData, options = {}) {
|
||||||
|
const rpcManager = new RPCConnectionManager();
|
||||||
|
return await rpcManager.sendTransactionWithRetry(wallet, txData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает nonce с retry логикой
|
||||||
|
* @param {Object} provider - Провайдер
|
||||||
|
* @param {string} address - Адрес
|
||||||
|
* @param {Object} options - Опции
|
||||||
|
* @returns {Promise<number>} - Nonce
|
||||||
|
*/
|
||||||
|
async function getNonceWithRetry(provider, address, options = {}) {
|
||||||
|
// Используем быстрый метод по умолчанию
|
||||||
|
if (options.fast !== false) {
|
||||||
|
try {
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = Number(network.chainId);
|
||||||
|
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
|
||||||
|
return await nonceManager.getNonceFast(address, rpcUrl, chainId);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[deploymentUtils] Быстрый nonce failed, используем retry: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback на retry метод
|
||||||
|
return await nonceManager.getNonceWithRetry(provider, address, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getFeeOverrides,
|
||||||
|
createProviderAndWallet,
|
||||||
|
alignNonce,
|
||||||
|
getNetworkInfo,
|
||||||
|
getBalance,
|
||||||
|
createRPCConnection,
|
||||||
|
createMultipleRPCConnections,
|
||||||
|
sendTransactionWithRetry,
|
||||||
|
getNonceWithRetry
|
||||||
|
};
|
||||||
353
backend/utils/nonceManager.js
Normal file
353
backend/utils/nonceManager.js
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* Менеджер nonce для управления транзакциями в разных сетях
|
||||||
|
* Решает проблему "nonce too low" при деплое в нескольких сетях
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
|
||||||
|
class NonceManager {
|
||||||
|
constructor() {
|
||||||
|
this.nonceCache = new Map(); // Кэш nonce для каждого адреса и сети
|
||||||
|
this.pendingTransactions = new Map(); // Отслеживание pending транзакций
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить актуальный nonce для адреса в сети с таймаутом и retry логикой
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {string} rpcUrl - RPC URL сети
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {Object} options - Опции (timeout, maxRetries)
|
||||||
|
* @returns {Promise<number>} - Актуальный nonce
|
||||||
|
*/
|
||||||
|
async getNonce(address, rpcUrl, chainId, options = {}) {
|
||||||
|
const { timeout = 10000, maxRetries = 3 } = options; // Увеличиваем таймаут и попытки
|
||||||
|
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// Получаем nonce из сети с таймаутом
|
||||||
|
const networkNonce = await Promise.race([
|
||||||
|
provider.getTransactionCount(address, 'pending'),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Nonce timeout')), timeout)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ВАЖНО: Не используем кэш для критических операций деплоя
|
||||||
|
// Всегда получаем актуальный nonce из сети
|
||||||
|
this.nonceCache.set(cacheKey, networkNonce);
|
||||||
|
|
||||||
|
console.log(`[NonceManager] ${address}:${chainId} nonce=${networkNonce} (попытка ${attempt})`);
|
||||||
|
|
||||||
|
return networkNonce;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Ошибка ${address}:${chainId} (${attempt}):`, error.message);
|
||||||
|
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
// В случае критической ошибки, сбрасываем кэш и пробуем еще раз
|
||||||
|
this.nonceCache.delete(cacheKey);
|
||||||
|
throw new Error(`Не удалось получить nonce после ${maxRetries} попыток: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Увеличиваем задержку между попытками
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Зарезервировать nonce для транзакции
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {number} nonce - Nonce для резервирования
|
||||||
|
*/
|
||||||
|
reserveNonce(address, chainId, nonce) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
const currentNonce = this.nonceCache.get(cacheKey) || 0;
|
||||||
|
|
||||||
|
if (nonce >= currentNonce) {
|
||||||
|
this.nonceCache.set(cacheKey, nonce + 1);
|
||||||
|
console.log(`[NonceManager] Зарезервирован nonce ${nonce} для ${address} в сети ${chainId}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[NonceManager] Попытка использовать nonce ${nonce} меньше текущего ${currentNonce} для ${address} в сети ${chainId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отметить транзакцию как pending
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {number} nonce - Nonce транзакции
|
||||||
|
* @param {string} txHash - Хэш транзакции
|
||||||
|
*/
|
||||||
|
markTransactionPending(address, chainId, nonce, txHash) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
|
||||||
|
|
||||||
|
pendingTxs.push({
|
||||||
|
nonce,
|
||||||
|
txHash,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingTransactions.set(cacheKey, pendingTxs);
|
||||||
|
console.log(`[NonceManager] Отмечена pending транзакция ${txHash} с nonce ${nonce} для ${address} в сети ${chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отметить транзакцию как подтвержденную
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {string} txHash - Хэш транзакции
|
||||||
|
*/
|
||||||
|
markTransactionConfirmed(address, chainId, txHash) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
|
||||||
|
|
||||||
|
const txIndex = pendingTxs.findIndex(tx => tx.txHash === txHash);
|
||||||
|
if (txIndex !== -1) {
|
||||||
|
const tx = pendingTxs[txIndex];
|
||||||
|
pendingTxs.splice(txIndex, 1);
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Транзакция ${txHash} подтверждена для ${address} в сети ${chainId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить старые pending транзакции
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {number} maxAge - Максимальный возраст в миллисекундах (по умолчанию 5 минут)
|
||||||
|
*/
|
||||||
|
clearOldPendingTransactions(address, chainId, maxAge = 5 * 60 * 1000) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const validTxs = pendingTxs.filter(tx => (now - tx.timestamp) < maxAge);
|
||||||
|
|
||||||
|
if (validTxs.length !== pendingTxs.length) {
|
||||||
|
this.pendingTransactions.set(cacheKey, validTxs);
|
||||||
|
console.log(`[NonceManager] Очищено ${pendingTxs.length - validTxs.length} старых pending транзакций для ${address} в сети ${chainId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить информацию о pending транзакциях
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {Array} - Массив pending транзакций
|
||||||
|
*/
|
||||||
|
getPendingTransactions(address, chainId) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
return this.pendingTransactions.get(cacheKey) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбросить кэш nonce для адреса и сети
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
*/
|
||||||
|
resetNonce(address, chainId) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
this.nonceCache.delete(cacheKey);
|
||||||
|
this.pendingTransactions.delete(cacheKey);
|
||||||
|
console.log(`[NonceManager] Сброшен кэш nonce для ${address} в сети ${chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статистику по nonce
|
||||||
|
* @returns {Object} - Статистика
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
nonceCache: Object.fromEntries(this.nonceCache),
|
||||||
|
pendingTransactions: Object.fromEntries(this.pendingTransactions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Быстрое получение nonce без retry (для критичных по времени операций)
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {string} rpcUrl - RPC URL сети
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {Promise<number>} - Nonce
|
||||||
|
*/
|
||||||
|
async getNonceFast(address, rpcUrl, chainId) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
const cachedNonce = this.nonceCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedNonce !== undefined) {
|
||||||
|
console.log(`[NonceManager] Быстрый nonce из кэша: ${cachedNonce} для ${address}:${chainId}`);
|
||||||
|
return cachedNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем RPC URLs из базы данных с fallback
|
||||||
|
const rpcUrls = await this.getRpcUrlsFromDatabase(chainId, rpcUrl);
|
||||||
|
|
||||||
|
for (const currentRpc of rpcUrls) {
|
||||||
|
try {
|
||||||
|
console.log(`[NonceManager] Пробуем RPC: ${currentRpc}`);
|
||||||
|
const provider = new ethers.JsonRpcProvider(currentRpc);
|
||||||
|
|
||||||
|
const networkNonce = await Promise.race([
|
||||||
|
provider.getTransactionCount(address, 'pending'),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Fast nonce timeout')), 3000)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.nonceCache.set(cacheKey, networkNonce);
|
||||||
|
console.log(`[NonceManager] ✅ Nonce получен: ${networkNonce} для ${address}:${chainId} с RPC: ${currentRpc}`);
|
||||||
|
return networkNonce;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[NonceManager] RPC failed: ${currentRpc} - ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если все RPC недоступны, возвращаем 0
|
||||||
|
console.warn(`[NonceManager] Все RPC недоступны для ${address}:${chainId}, возвращаем 0`);
|
||||||
|
this.nonceCache.set(cacheKey, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить RPC URLs из базы данных с fallback
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @param {string} primaryRpcUrl - Основной RPC URL (опциональный)
|
||||||
|
* @returns {Promise<Array>} - Массив RPC URL
|
||||||
|
*/
|
||||||
|
async getRpcUrlsFromDatabase(chainId, primaryRpcUrl = null) {
|
||||||
|
const rpcUrls = [];
|
||||||
|
|
||||||
|
// Добавляем основной RPC URL если указан
|
||||||
|
if (primaryRpcUrl) {
|
||||||
|
rpcUrls.push(primaryRpcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем RPC из deploy_params (как в deploy-multichain.js)
|
||||||
|
const DeployParamsService = require('../services/deployParamsService');
|
||||||
|
const deployParamsService = new DeployParamsService();
|
||||||
|
|
||||||
|
// Получаем последние параметры деплоя
|
||||||
|
const latestParams = await deployParamsService.getLatestDeployParams(1);
|
||||||
|
if (latestParams.length > 0) {
|
||||||
|
const params = latestParams[0];
|
||||||
|
const supportedChainIds = params.supported_chain_ids || [];
|
||||||
|
const rpcUrlsFromParams = params.rpc_urls || [];
|
||||||
|
|
||||||
|
// Находим RPC для нужного chainId
|
||||||
|
const chainIndex = supportedChainIds.indexOf(chainId);
|
||||||
|
if (chainIndex !== -1 && rpcUrlsFromParams[chainIndex]) {
|
||||||
|
const deployRpcUrl = rpcUrlsFromParams[chainIndex];
|
||||||
|
if (!rpcUrls.includes(deployRpcUrl)) {
|
||||||
|
rpcUrls.push(deployRpcUrl);
|
||||||
|
console.log(`[NonceManager] ✅ RPC из deploy_params для chainId ${chainId}: ${deployRpcUrl}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await deployParamsService.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[NonceManager] deploy_params недоступны для chainId ${chainId}, используем fallback: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда добавляем fallback RPC для надежности
|
||||||
|
const fallbackRPCs = this.getFallbackRPCs(chainId);
|
||||||
|
for (const fallbackRpc of fallbackRPCs) {
|
||||||
|
if (!rpcUrls.includes(fallbackRpc)) {
|
||||||
|
rpcUrls.push(fallbackRpc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[NonceManager] RPC URLs для chainId ${chainId}:`, rpcUrls);
|
||||||
|
return rpcUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список fallback RPC для сети
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {Array} - Массив RPC URL
|
||||||
|
*/
|
||||||
|
getFallbackRPCs(chainId) {
|
||||||
|
const fallbackRPCs = {
|
||||||
|
1: [ // Mainnet
|
||||||
|
'https://eth.llamarpc.com',
|
||||||
|
'https://rpc.ankr.com/eth',
|
||||||
|
'https://ethereum.publicnode.com'
|
||||||
|
],
|
||||||
|
11155111: [ // Sepolia
|
||||||
|
'https://rpc.sepolia.org',
|
||||||
|
'https://sepolia.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'
|
||||||
|
],
|
||||||
|
17000: [ // Holesky
|
||||||
|
'https://ethereum-holesky.publicnode.com',
|
||||||
|
'https://holesky.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'
|
||||||
|
],
|
||||||
|
421614: [ // Arbitrum Sepolia
|
||||||
|
'https://sepolia-rollup.arbitrum.io/rpc'
|
||||||
|
],
|
||||||
|
84532: [ // Base Sepolia
|
||||||
|
'https://sepolia.base.org'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbackRPCs[chainId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интеграция с существующими системами - замена для rpcConnectionManager
|
||||||
|
* @param {Object} provider - Провайдер ethers
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {Object} options - Опции
|
||||||
|
* @returns {Promise<number>} - Nonce
|
||||||
|
*/
|
||||||
|
async getNonceWithRetry(provider, address, options = {}) {
|
||||||
|
// Извлекаем chainId из провайдера
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = Number(network.chainId);
|
||||||
|
|
||||||
|
// Получаем RPC URL из провайдера (если возможно)
|
||||||
|
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
|
||||||
|
|
||||||
|
return await this.getNonce(address, rpcUrl, chainId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно обновляет nonce из сети (для обработки race conditions)
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @param {string} rpcUrl - RPC URL сети
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {Promise<number>} - Актуальный nonce
|
||||||
|
*/
|
||||||
|
async forceRefreshNonce(address, rpcUrl, chainId) {
|
||||||
|
const cacheKey = `${address}-${chainId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const networkNonce = await provider.getTransactionCount(address, 'pending');
|
||||||
|
|
||||||
|
// Принудительно обновляем кэш
|
||||||
|
this.nonceCache.set(cacheKey, networkNonce);
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Force refreshed nonce for ${address}:${chainId} = ${networkNonce}`);
|
||||||
|
return networkNonce;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Force refresh failed for ${address}:${chainId}:`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем глобальный экземпляр
|
||||||
|
const nonceManager = new NonceManager();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
NonceManager,
|
||||||
|
nonceManager
|
||||||
|
};
|
||||||
281
backend/utils/operationDecoder.js
Normal file
281
backend/utils/operationDecoder.js
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Декодирует операцию из формата abi.encodeWithSelector
|
||||||
|
* @param {string} operation - Закодированная операция (hex string)
|
||||||
|
* @returns {Object} - Декодированная операция
|
||||||
|
*/
|
||||||
|
function decodeOperation(operation) {
|
||||||
|
try {
|
||||||
|
if (!operation || operation.length < 4) {
|
||||||
|
return {
|
||||||
|
type: 'unknown',
|
||||||
|
selector: null,
|
||||||
|
data: null,
|
||||||
|
decoded: null,
|
||||||
|
error: 'Invalid operation format'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем селектор (первые 4 байта)
|
||||||
|
const selector = operation.slice(0, 10); // 0x + 4 байта
|
||||||
|
const data = operation.slice(10); // Остальные данные
|
||||||
|
|
||||||
|
// Определяем тип операции по селектору
|
||||||
|
const operationType = getOperationType(selector);
|
||||||
|
|
||||||
|
if (operationType === 'unknown') {
|
||||||
|
return {
|
||||||
|
type: 'unknown',
|
||||||
|
selector: selector,
|
||||||
|
data: data,
|
||||||
|
decoded: null,
|
||||||
|
error: 'Unknown operation selector'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Декодируем данные в зависимости от типа операции
|
||||||
|
let decoded = null;
|
||||||
|
try {
|
||||||
|
decoded = decodeOperationData(operationType, data);
|
||||||
|
} catch (decodeError) {
|
||||||
|
return {
|
||||||
|
type: operationType,
|
||||||
|
selector: selector,
|
||||||
|
data: data,
|
||||||
|
decoded: null,
|
||||||
|
error: `Failed to decode ${operationType}: ${decodeError.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: operationType,
|
||||||
|
selector: selector,
|
||||||
|
data: data,
|
||||||
|
decoded: decoded,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
selector: null,
|
||||||
|
data: null,
|
||||||
|
decoded: null,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет тип операции по селектору
|
||||||
|
* @param {string} selector - Селектор функции (0x + 4 байта)
|
||||||
|
* @returns {string} - Тип операции
|
||||||
|
*/
|
||||||
|
function getOperationType(selector) {
|
||||||
|
const selectors = {
|
||||||
|
'0x12345678': '_addModule', // Пример селектора
|
||||||
|
'0x87654321': '_removeModule', // Пример селектора
|
||||||
|
'0xabcdef12': '_addSupportedChain', // Пример селектора
|
||||||
|
'0x21fedcba': '_removeSupportedChain', // Пример селектора
|
||||||
|
'0x1234abcd': '_transferTokens', // Пример селектора
|
||||||
|
'0xabcd1234': '_updateVotingDurations', // Пример селектора
|
||||||
|
'0x5678efgh': '_setLogoURI', // Пример селектора
|
||||||
|
'0xefgh5678': '_updateQuorumPercentage', // Пример селектора
|
||||||
|
'0x9abc1234': '_updateDLEInfo', // Пример селектора
|
||||||
|
'0x12349abc': 'offchainAction' // Пример селектора
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вычисляем реальные селекторы
|
||||||
|
const realSelectors = {
|
||||||
|
[ethers.id('_addModule(bytes32,address)').slice(0, 10)]: '_addModule',
|
||||||
|
[ethers.id('_removeModule(bytes32)').slice(0, 10)]: '_removeModule',
|
||||||
|
[ethers.id('_addSupportedChain(uint256)').slice(0, 10)]: '_addSupportedChain',
|
||||||
|
[ethers.id('_removeSupportedChain(uint256)').slice(0, 10)]: '_removeSupportedChain',
|
||||||
|
[ethers.id('_transferTokens(address,uint256)').slice(0, 10)]: '_transferTokens',
|
||||||
|
[ethers.id('_updateVotingDurations(uint256,uint256)').slice(0, 10)]: '_updateVotingDurations',
|
||||||
|
[ethers.id('_setLogoURI(string)').slice(0, 10)]: '_setLogoURI',
|
||||||
|
[ethers.id('_updateQuorumPercentage(uint256)').slice(0, 10)]: '_updateQuorumPercentage',
|
||||||
|
[ethers.id('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)').slice(0, 10)]: '_updateDLEInfo',
|
||||||
|
[ethers.id('offchainAction(bytes32,string,bytes32)').slice(0, 10)]: 'offchainAction'
|
||||||
|
};
|
||||||
|
|
||||||
|
return realSelectors[selector] || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Декодирует данные операции в зависимости от типа
|
||||||
|
* @param {string} operationType - Тип операции
|
||||||
|
* @param {string} data - Закодированные данные
|
||||||
|
* @returns {Object} - Декодированные данные
|
||||||
|
*/
|
||||||
|
function decodeOperationData(operationType, data) {
|
||||||
|
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
||||||
|
|
||||||
|
switch (operationType) {
|
||||||
|
case '_addModule':
|
||||||
|
const [moduleId, moduleAddress] = abiCoder.decode(['bytes32', 'address'], '0x' + data);
|
||||||
|
return {
|
||||||
|
moduleId: moduleId,
|
||||||
|
moduleAddress: moduleAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_removeModule':
|
||||||
|
const [moduleIdToRemove] = abiCoder.decode(['bytes32'], '0x' + data);
|
||||||
|
return {
|
||||||
|
moduleId: moduleIdToRemove
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_addSupportedChain':
|
||||||
|
const [chainIdToAdd] = abiCoder.decode(['uint256'], '0x' + data);
|
||||||
|
return {
|
||||||
|
chainId: Number(chainIdToAdd)
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_removeSupportedChain':
|
||||||
|
const [chainIdToRemove] = abiCoder.decode(['uint256'], '0x' + data);
|
||||||
|
return {
|
||||||
|
chainId: Number(chainIdToRemove)
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_transferTokens':
|
||||||
|
const [recipient, amount] = abiCoder.decode(['address', 'uint256'], '0x' + data);
|
||||||
|
return {
|
||||||
|
recipient: recipient,
|
||||||
|
amount: amount.toString(),
|
||||||
|
amountFormatted: ethers.formatEther(amount)
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_updateVotingDurations':
|
||||||
|
const [minDuration, maxDuration] = abiCoder.decode(['uint256', 'uint256'], '0x' + data);
|
||||||
|
return {
|
||||||
|
minDuration: Number(minDuration),
|
||||||
|
maxDuration: Number(maxDuration)
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_setLogoURI':
|
||||||
|
const [logoURI] = abiCoder.decode(['string'], '0x' + data);
|
||||||
|
return {
|
||||||
|
logoURI: logoURI
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_updateQuorumPercentage':
|
||||||
|
const [quorumPercentage] = abiCoder.decode(['uint256'], '0x' + data);
|
||||||
|
return {
|
||||||
|
quorumPercentage: Number(quorumPercentage)
|
||||||
|
};
|
||||||
|
|
||||||
|
case '_updateDLEInfo':
|
||||||
|
const [name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp] = abiCoder.decode(
|
||||||
|
['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'],
|
||||||
|
'0x' + data
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
symbol: symbol,
|
||||||
|
location: location,
|
||||||
|
coordinates: coordinates,
|
||||||
|
jurisdiction: Number(jurisdiction),
|
||||||
|
okvedCodes: okvedCodes,
|
||||||
|
kpp: Number(kpp)
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'offchainAction':
|
||||||
|
const [actionId, kind, payloadHash] = abiCoder.decode(['bytes32', 'string', 'bytes32'], '0x' + data);
|
||||||
|
return {
|
||||||
|
actionId: actionId,
|
||||||
|
kind: kind,
|
||||||
|
payloadHash: payloadHash
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown operation type: ${operationType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует декодированную операцию для отображения
|
||||||
|
* @param {Object} decodedOperation - Декодированная операция
|
||||||
|
* @returns {string} - Отформатированное описание
|
||||||
|
*/
|
||||||
|
function formatOperation(decodedOperation) {
|
||||||
|
if (decodedOperation.error) {
|
||||||
|
return `Ошибка: ${decodedOperation.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, decoded } = decodedOperation;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case '_addModule':
|
||||||
|
return `Добавить модуль: ${decoded.moduleId} (${decoded.moduleAddress})`;
|
||||||
|
|
||||||
|
case '_removeModule':
|
||||||
|
return `Удалить модуль: ${decoded.moduleId}`;
|
||||||
|
|
||||||
|
case '_addSupportedChain':
|
||||||
|
return `Добавить поддерживаемую сеть: ${decoded.chainId}`;
|
||||||
|
|
||||||
|
case '_removeSupportedChain':
|
||||||
|
return `Удалить поддерживаемую сеть: ${decoded.chainId}`;
|
||||||
|
|
||||||
|
case '_transferTokens':
|
||||||
|
return `Перевести токены: ${decoded.amountFormatted} DLE на адрес ${decoded.recipient}`;
|
||||||
|
|
||||||
|
case '_updateVotingDurations':
|
||||||
|
return `Обновить длительность голосования: ${decoded.minDuration}-${decoded.maxDuration} секунд`;
|
||||||
|
|
||||||
|
case '_setLogoURI':
|
||||||
|
return `Обновить логотип: ${decoded.logoURI}`;
|
||||||
|
|
||||||
|
case '_updateQuorumPercentage':
|
||||||
|
return `Обновить процент кворума: ${decoded.quorumPercentage}%`;
|
||||||
|
|
||||||
|
case '_updateDLEInfo':
|
||||||
|
return `Обновить информацию DLE: ${decoded.name} (${decoded.symbol})`;
|
||||||
|
|
||||||
|
case 'offchainAction':
|
||||||
|
return `Оффчейн действие: ${decoded.kind} (${decoded.actionId})`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `Неизвестная операция: ${type}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает название сети по ID
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {string} - Название сети
|
||||||
|
*/
|
||||||
|
function getChainName(chainId) {
|
||||||
|
const chainNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
17000: 'Holesky',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
84532: 'Base Sepolia',
|
||||||
|
137: 'Polygon',
|
||||||
|
80001: 'Polygon Mumbai',
|
||||||
|
56: 'BSC',
|
||||||
|
97: 'BSC Testnet'
|
||||||
|
};
|
||||||
|
|
||||||
|
return chainNames[chainId] || `Chain ${chainId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
decodeOperation,
|
||||||
|
formatOperation,
|
||||||
|
getChainName
|
||||||
|
};
|
||||||
250
backend/utils/rpcConnectionManager.js
Normal file
250
backend/utils/rpcConnectionManager.js
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Менеджер RPC соединений с retry логикой и обработкой ошибок
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
class RPCConnectionManager {
|
||||||
|
constructor() {
|
||||||
|
this.connections = new Map(); // Кэш соединений
|
||||||
|
this.retryConfig = {
|
||||||
|
maxRetries: 3,
|
||||||
|
baseDelay: 1000, // 1 секунда
|
||||||
|
maxDelay: 10000, // 10 секунд
|
||||||
|
timeout: 30000 // 30 секунд
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает RPC соединение с retry логикой
|
||||||
|
* @param {string} rpcUrl - URL RPC
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {Object} options - Опции соединения
|
||||||
|
* @returns {Promise<Object>} - {provider, wallet, network}
|
||||||
|
*/
|
||||||
|
async createConnection(rpcUrl, privateKey, options = {}) {
|
||||||
|
const config = { ...this.retryConfig, ...options };
|
||||||
|
const connectionKey = `${rpcUrl}_${privateKey}`;
|
||||||
|
|
||||||
|
// Проверяем кэш
|
||||||
|
if (this.connections.has(connectionKey)) {
|
||||||
|
const cached = this.connections.get(connectionKey);
|
||||||
|
if (Date.now() - cached.timestamp < 60000) { // 1 минута кэш
|
||||||
|
logger.info(`[RPC_MANAGER] Используем кэшированное соединение: ${rpcUrl}`);
|
||||||
|
return cached.connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] Создаем новое RPC соединение: ${rpcUrl}`);
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
|
||||||
|
polling: false,
|
||||||
|
staticNetwork: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем соединение с timeout
|
||||||
|
const network = await Promise.race([
|
||||||
|
provider.getNetwork(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('RPC timeout')), config.timeout)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
|
||||||
|
const connection = { provider, wallet, network };
|
||||||
|
|
||||||
|
// Кэшируем соединение
|
||||||
|
this.connections.set(connectionKey, {
|
||||||
|
connection,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] ✅ RPC соединение установлено: ${rpcUrl} (chainId: ${network.chainId})`);
|
||||||
|
return connection;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[RPC_MANAGER] ❌ Попытка ${attempt}/${config.maxRetries} failed: ${error.message}`);
|
||||||
|
|
||||||
|
if (attempt === config.maxRetries) {
|
||||||
|
throw new Error(`RPC соединение не удалось установить после ${config.maxRetries} попыток: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспоненциальная задержка
|
||||||
|
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
|
||||||
|
logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает множественные RPC соединения с обработкой ошибок
|
||||||
|
* @param {Array} rpcUrls - Массив RPC URL
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {Object} options - Опции соединения
|
||||||
|
* @returns {Promise<Array>} - Массив успешных соединений
|
||||||
|
*/
|
||||||
|
async createMultipleConnections(rpcUrls, privateKey, options = {}) {
|
||||||
|
logger.info(`[RPC_MANAGER] Создаем ${rpcUrls.length} RPC соединений...`);
|
||||||
|
|
||||||
|
const connectionPromises = rpcUrls.map(async (rpcUrl, index) => {
|
||||||
|
try {
|
||||||
|
const connection = await this.createConnection(rpcUrl, privateKey, options);
|
||||||
|
return { index, rpcUrl, ...connection, success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[RPC_MANAGER] ❌ Соединение ${index + 1} failed: ${rpcUrl} - ${error.message}`);
|
||||||
|
return { index, rpcUrl, error: error.message, success: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(connectionPromises);
|
||||||
|
const successful = results.filter(r => r.success);
|
||||||
|
const failed = results.filter(r => !r.success);
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] ✅ Успешных соединений: ${successful.length}/${rpcUrls.length}`);
|
||||||
|
if (failed.length > 0) {
|
||||||
|
logger.warn(`[RPC_MANAGER] ⚠️ Неудачных соединений: ${failed.length}`);
|
||||||
|
failed.forEach(f => logger.warn(`[RPC_MANAGER] - ${f.rpcUrl}: ${f.error}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successful.length === 0) {
|
||||||
|
throw new Error('Не удалось установить ни одного RPC соединения');
|
||||||
|
}
|
||||||
|
|
||||||
|
return successful;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет транзакцию с retry логикой
|
||||||
|
* @param {Object} wallet - Кошелек
|
||||||
|
* @param {Object} txData - Данные транзакции
|
||||||
|
* @param {Object} options - Опции
|
||||||
|
* @returns {Promise<Object>} - Результат транзакции
|
||||||
|
*/
|
||||||
|
async sendTransactionWithRetry(wallet, txData, options = {}) {
|
||||||
|
const config = { ...this.retryConfig, ...options };
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
logger.info(`[RPC_MANAGER] Отправка транзакции (попытка ${attempt}/${config.maxRetries})`);
|
||||||
|
|
||||||
|
const tx = await wallet.sendTransaction({
|
||||||
|
...txData,
|
||||||
|
timeout: config.timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] ✅ Транзакция отправлена: ${tx.hash}`);
|
||||||
|
|
||||||
|
// Ждем подтверждения с timeout
|
||||||
|
const receipt = await Promise.race([
|
||||||
|
tx.wait(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Transaction timeout')), config.timeout)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] ✅ Транзакция подтверждена: ${tx.hash}`);
|
||||||
|
return { tx, receipt, success: true };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[RPC_MANAGER] ❌ Транзакция failed (попытка ${attempt}): ${error.message}`);
|
||||||
|
|
||||||
|
if (attempt === config.maxRetries) {
|
||||||
|
throw new Error(`Транзакция не удалась после ${config.maxRetries} попыток: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, стоит ли повторять
|
||||||
|
if (this.shouldRetry(error)) {
|
||||||
|
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
|
||||||
|
logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет, стоит ли повторять операцию
|
||||||
|
* @param {Error} error - Ошибка
|
||||||
|
* @returns {boolean} - Стоит ли повторять
|
||||||
|
*/
|
||||||
|
shouldRetry(error) {
|
||||||
|
const retryableErrors = [
|
||||||
|
'NETWORK_ERROR',
|
||||||
|
'TIMEOUT',
|
||||||
|
'ECONNRESET',
|
||||||
|
'ENOTFOUND',
|
||||||
|
'ETIMEDOUT',
|
||||||
|
'RPC timeout',
|
||||||
|
'Transaction timeout'
|
||||||
|
];
|
||||||
|
|
||||||
|
const errorMessage = error.message.toLowerCase();
|
||||||
|
return retryableErrors.some(retryableError =>
|
||||||
|
errorMessage.includes(retryableError.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает nonce с retry логикой
|
||||||
|
* @param {Object} provider - Провайдер
|
||||||
|
* @param {string} address - Адрес
|
||||||
|
* @param {Object} options - Опции
|
||||||
|
* @returns {Promise<number>} - Nonce
|
||||||
|
*/
|
||||||
|
async getNonceWithRetry(provider, address, options = {}) {
|
||||||
|
const config = { ...this.retryConfig, ...options };
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const nonce = await Promise.race([
|
||||||
|
provider.getTransactionCount(address, 'pending'),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Nonce timeout')), config.timeout)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.info(`[RPC_MANAGER] ✅ Nonce получен: ${nonce} (попытка ${attempt})`);
|
||||||
|
return nonce;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[RPC_MANAGER] ❌ Nonce failed (попытка ${attempt}): ${error.message}`);
|
||||||
|
|
||||||
|
if (attempt === config.maxRetries) {
|
||||||
|
throw new Error(`Не удалось получить nonce после ${config.maxRetries} попыток: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает кэш соединений
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.connections.clear();
|
||||||
|
logger.info('[RPC_MANAGER] Кэш соединений очищен');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает статистику соединений
|
||||||
|
* @returns {Object} - Статистика
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
cachedConnections: this.connections.size,
|
||||||
|
retryConfig: this.retryConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RPCConnectionManager;
|
||||||
@@ -96,6 +96,9 @@ function initWSS(server) {
|
|||||||
wsClients.delete(userId);
|
wsClients.delete(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем клиента из deploymentWebSocketService
|
||||||
|
deploymentWebSocketService.removeClient(ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('error', (error) => {
|
ws.on('error', (error) => {
|
||||||
@@ -494,7 +497,7 @@ function broadcastDeploymentUpdate(data) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📡 [WebSocket] Отправлено deployment update: ${data.type || 'unknown'}`);
|
console.log(`📡 [WebSocket] Отправлено deployment update: deployment_update`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для уведомления об обновлениях модулей
|
// Функция для уведомления об обновлениях модулей
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,8 @@ services:
|
|||||||
POSTGRES_DB: ${DB_NAME:-dapp_db}
|
POSTGRES_DB: ${DB_NAME:-dapp_db}
|
||||||
POSTGRES_USER: ${DB_USER:-dapp_user}
|
POSTGRES_USER: ${DB_USER:-dapp_user}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password}
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password}
|
||||||
# ports:
|
ports:
|
||||||
# - '5432:5432' # Закрываем доступ к базе данных извне
|
- '5432:5432' # Открываем доступ к базе данных извне для разработки
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD-SHELL
|
- CMD-SHELL
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|
||||||
DLE v2 (Digital Legal Entity) - это система для создания цифровых юридических лиц с мульти-чейн поддержкой. Основная особенность - использование CREATE2 для обеспечения одинакового адреса смарт-контракта во всех поддерживаемых сетях.
|
DLE v2 (Digital Legal Entity) - это система для создания цифровых юридических лиц с мульти-чейн поддержкой. Основная особенность - использование CREATE с выровненным nonce для обеспечения одинакового адреса смарт-контракта во всех поддерживаемых сетях.
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ DLE v2 (Digital Legal Entity) - это система для создания ц
|
|||||||
|
|
||||||
### Мульти-чейн поддержка
|
### Мульти-чейн поддержка
|
||||||
|
|
||||||
- **CREATE2** - Одинаковый адрес во всех EVM-совместимых сетях
|
- **CREATE с выровненным nonce** - Одинаковый адрес во всех EVM-совместимых сетях
|
||||||
- **Single-Chain Governance** - Голосование происходит в одной сети
|
- **Single-Chain Governance** - Голосование происходит в одной сети
|
||||||
- **Multi-Chain Execution** - Исполнение в целевых сетях по подписям
|
- **Multi-Chain Execution** - Исполнение в целевых сетях по подписям
|
||||||
|
|
||||||
@@ -146,33 +146,42 @@ CREATE TABLE factory_addresses (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### CREATE2 Механизм
|
### CREATE Механизм
|
||||||
|
|
||||||
Система использует двухуровневый CREATE2 для обеспечения одинаковых адресов:
|
Система использует CREATE с выровненным nonce для обеспечения одинаковых адресов:
|
||||||
|
|
||||||
#### 1. Factory Deployer
|
#### 1. Выравнивание nonce
|
||||||
```solidity
|
```javascript
|
||||||
// Предсказуемый адрес Factory через CREATE
|
// Выравнивание nonce до целевого значения
|
||||||
address factoryAddress = getCreateAddress(
|
while (currentNonce < targetNonce) {
|
||||||
from: deployerAddress,
|
await sendTransaction({
|
||||||
nonce: deployerNonce
|
to: burnAddress,
|
||||||
);
|
value: 0,
|
||||||
|
nonce: currentNonce
|
||||||
|
});
|
||||||
|
currentNonce++;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. DLE Contract
|
#### 2. Деплой DLE
|
||||||
```solidity
|
```javascript
|
||||||
// Вычисление адреса DLE через CREATE2
|
// Вычисление адреса DLE через CREATE
|
||||||
address predictedAddress = factoryDeployer.computeAddress(
|
const predictedAddress = ethers.getCreateAddress({
|
||||||
salt,
|
from: wallet.address,
|
||||||
keccak256(creationCode)
|
nonce: targetNonce
|
||||||
);
|
});
|
||||||
|
|
||||||
// Деплой DLE с одинаковым адресом
|
// Деплой DLE с предсказанным адресом
|
||||||
factoryDeployer.deploy(salt, creationCode);
|
await wallet.sendTransaction({
|
||||||
|
data: dleInitCode,
|
||||||
|
nonce: targetNonce
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Ключевые принципы:
|
#### Ключевые принципы:
|
||||||
- **Factory Deployer** деплоится с одинаковым адресом во всех сетях
|
- **Выровненный nonce** обеспечивает одинаковые адреса во всех сетях
|
||||||
|
- **Burn address** используется для выравнивания nonce без потери средств
|
||||||
|
- **Проверка баланса** перед деплоем предотвращает неудачи
|
||||||
- **DLE Contract** деплоится через Factory с одинаковым salt
|
- **DLE Contract** деплоится через Factory с одинаковым salt
|
||||||
- **Результат**: Одинаковый адрес DLE во всех EVM-совместимых сетях
|
- **Результат**: Одинаковый адрес DLE во всех EVM-совместимых сетях
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits, provi
|
|||||||
import { useAuthContext } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||||
import { useNotifications } from '../composables/useNotifications';
|
import { useNotifications } from '../composables/useNotifications';
|
||||||
|
import { useTokenBalancesWebSocket } from '../composables/useTokenBalancesWebSocket';
|
||||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||||
import { connectWithWallet } from '../services/wallet';
|
import { connectWithWallet } from '../services/wallet';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
@@ -68,7 +69,10 @@ import NotificationDisplay from './NotificationDisplay.vue';
|
|||||||
const auth = useAuthContext();
|
const auth = useAuthContext();
|
||||||
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
||||||
|
|
||||||
// Определяем props, которые будут приходить от родительского View
|
// Используем useTokenBalancesWebSocket для получения актуального состояния загрузки
|
||||||
|
const { isLoadingTokens: wsIsLoadingTokens } = useTokenBalancesWebSocket();
|
||||||
|
|
||||||
|
// Определяем props, которые будут приходить от родительского View (оставляем для совместимости)
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isAuthenticated: Boolean,
|
isAuthenticated: Boolean,
|
||||||
identities: Array,
|
identities: Array,
|
||||||
@@ -79,17 +83,26 @@ const props = defineProps({
|
|||||||
// Определяем emits
|
// Определяем emits
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
|
// Используем useAuth напрямую для получения актуальных данных
|
||||||
|
const isAuthenticated = computed(() => auth.isAuthenticated.value);
|
||||||
|
const identities = computed(() => auth.identities.value);
|
||||||
|
const tokenBalances = computed(() => auth.tokenBalances.value);
|
||||||
|
const isLoadingTokens = computed(() => {
|
||||||
|
// Приоритет: WebSocket состояние > пропс > false
|
||||||
|
return wsIsLoadingTokens.value || (props.isLoadingTokens !== undefined ? props.isLoadingTokens : false);
|
||||||
|
});
|
||||||
|
|
||||||
// Предоставляем данные дочерним компонентам через provide/inject
|
// Предоставляем данные дочерним компонентам через provide/inject
|
||||||
provide('isAuthenticated', computed(() => props.isAuthenticated));
|
provide('isAuthenticated', isAuthenticated);
|
||||||
provide('identities', computed(() => props.identities));
|
provide('identities', identities);
|
||||||
provide('tokenBalances', computed(() => props.tokenBalances));
|
provide('tokenBalances', tokenBalances);
|
||||||
provide('isLoadingTokens', computed(() => props.isLoadingTokens));
|
provide('isLoadingTokens', isLoadingTokens);
|
||||||
|
|
||||||
// Отладочная информация
|
// Отладочная информация
|
||||||
console.log('[BaseLayout] Props received:', {
|
console.log('[BaseLayout] Auth state:', {
|
||||||
isAuthenticated: props.isAuthenticated,
|
isAuthenticated: isAuthenticated.value,
|
||||||
tokenBalances: props.tokenBalances,
|
tokenBalances: tokenBalances.value,
|
||||||
isLoadingTokens: props.isLoadingTokens
|
isLoadingTokens: isLoadingTokens.value
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback после успешной аутентификации/привязки через Email/Telegram
|
// Callback после успешной аутентификации/привязки через Email/Telegram
|
||||||
@@ -168,6 +181,12 @@ const handleWalletAuth = async () => {
|
|||||||
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
|
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
|
||||||
} else if (error.message && error.message.includes('Браузерный кошелек не установлен')) {
|
} else if (error.message && error.message.includes('Браузерный кошелек не установлен')) {
|
||||||
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
|
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
|
||||||
|
} else if (error.message && error.message.includes('Не удалось получить nonce')) {
|
||||||
|
errorMessage = 'Ошибка получения nonce. Попробуйте обновить страницу и повторить попытку.';
|
||||||
|
} else if (error.message && error.message.includes('Invalid nonce')) {
|
||||||
|
errorMessage = 'Ошибка аутентификации. Попробуйте обновить страницу и повторить попытку.';
|
||||||
|
} else if (error.message && error.message.includes('Nonce expired')) {
|
||||||
|
errorMessage = 'Время сессии истекло. Попробуйте обновить страницу и повторить попытку.';
|
||||||
} else if (error.message) {
|
} else if (error.message) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
|||||||
223
frontend/src/components/NetworkSwitchNotification.vue
Normal file
223
frontend/src/components/NetworkSwitchNotification.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<!--
|
||||||
|
Network Switch Notification Component
|
||||||
|
Компонент для уведомления о необходимости переключения сети
|
||||||
|
|
||||||
|
Author: HB3 Accelerator
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="showNotification" class="network-notification">
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-icon">⚠️</div>
|
||||||
|
<div class="notification-text">
|
||||||
|
<h4>Требуется переключение сети</h4>
|
||||||
|
<p>Для голосования по этому предложению необходимо переключиться на сеть <strong>{{ targetNetworkName }}</strong></p>
|
||||||
|
<p>Текущая сеть: <strong>{{ currentNetworkName }}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="notification-actions">
|
||||||
|
<button @click="switchNetwork" class="btn btn-primary" :disabled="isSwitching">
|
||||||
|
{{ isSwitching ? 'Переключение...' : 'Переключить сеть' }}
|
||||||
|
</button>
|
||||||
|
<button @click="dismiss" class="btn btn-secondary">Позже</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { switchNetwork, getCurrentNetwork } from '@/utils/networkSwitcher';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NetworkSwitchNotification',
|
||||||
|
props: {
|
||||||
|
targetChainId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentChainId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['network-switched', 'dismissed'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const isSwitching = ref(false);
|
||||||
|
const showNotification = computed(() => props.visible && props.targetChainId !== props.currentChainId);
|
||||||
|
|
||||||
|
const targetNetworkName = computed(() => {
|
||||||
|
const networkNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
17000: 'Holesky',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
84532: 'Base Sepolia',
|
||||||
|
8453: 'Base'
|
||||||
|
};
|
||||||
|
return networkNames[props.targetChainId] || `Сеть ${props.targetChainId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentNetworkName = computed(() => {
|
||||||
|
const networkNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
17000: 'Holesky',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
84532: 'Base Sepolia',
|
||||||
|
8453: 'Base'
|
||||||
|
};
|
||||||
|
return networkNames[props.currentChainId] || `Сеть ${props.currentChainId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchNetworkHandler = async () => {
|
||||||
|
try {
|
||||||
|
isSwitching.value = true;
|
||||||
|
console.log(`🔄 [Network Switch] Переключаемся на сеть ${props.targetChainId}...`);
|
||||||
|
|
||||||
|
const result = await switchNetwork(props.targetChainId);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('✅ [Network Switch] Сеть переключена успешно');
|
||||||
|
emit('network-switched', result);
|
||||||
|
} else {
|
||||||
|
console.error('❌ [Network Switch] Ошибка переключения:', result.error);
|
||||||
|
alert(`Ошибка переключения сети: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Network Switch] Ошибка:', error);
|
||||||
|
alert(`Ошибка переключения сети: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
isSwitching.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
emit('dismissed');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showNotification,
|
||||||
|
targetNetworkName,
|
||||||
|
currentNetworkName,
|
||||||
|
isSwitching,
|
||||||
|
switchNetwork: switchNetworkHandler,
|
||||||
|
dismiss
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.network-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 400px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.network-notification {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -192,5 +192,275 @@ const formatTime = (timestamp) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Ваши стили для формы */
|
/* Статус подключения */
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.active {
|
||||||
|
background-color: #28a745;
|
||||||
|
box-shadow: 0 0 8px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.inactive {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Форма */
|
||||||
|
.tunnel-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:disabled,
|
||||||
|
.form-group textarea:disabled {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дополнительные настройки */
|
||||||
|
.advanced-section {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопки */
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-btn {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary), #20c997);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #0056b3, #1ea085);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-btn:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover:not(:disabled) {
|
||||||
|
background: #5a6268;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:disabled {
|
||||||
|
background: #adb5bd;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Лог операций */
|
||||||
|
.operation-log {
|
||||||
|
margin-top: 2rem;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.success .log-message {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.error .log-message {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.info .log-message {
|
||||||
|
color: #17a2b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-btn,
|
||||||
|
.reset-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -30,6 +30,12 @@ const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false });
|
|||||||
const updateIdentities = async () => {
|
const updateIdentities = async () => {
|
||||||
if (!isAuthenticated.value || !userId.value) return;
|
if (!isAuthenticated.value || !userId.value) return;
|
||||||
|
|
||||||
|
// Проверяем, что identities ref существует
|
||||||
|
if (!identities || typeof identities.value === 'undefined') {
|
||||||
|
console.warn('Identities ref is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/auth/identities');
|
const response = await axios.get('/auth/identities');
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -46,14 +52,26 @@ const updateIdentities = async () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Сравниваем новый отфильтрованный список с текущим значением
|
// Сравниваем новый отфильтрованный список с текущим значением
|
||||||
const currentProviders = identities.value.map(id => id.provider).sort();
|
const currentProviders = (identities.value || []).map(id => id?.provider || '').sort();
|
||||||
const newProviders = filteredIdentities.map(id => id.provider).sort();
|
const newProviders = (filteredIdentities || []).map(id => id?.provider || '').sort();
|
||||||
|
|
||||||
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
||||||
|
|
||||||
// Обновляем реактивное значение
|
// Обновляем реактивное значение с проверкой
|
||||||
|
try {
|
||||||
|
if (identities && identities.value !== undefined) {
|
||||||
identities.value = filteredIdentities;
|
identities.value = filteredIdentities;
|
||||||
console.log('User identities updated:', identities.value);
|
console.log('User identities updated:', identities.value);
|
||||||
|
} else {
|
||||||
|
console.warn('Identities ref is not available or not initialized');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating identities:', error);
|
||||||
|
// Если произошла ошибка, пытаемся инициализировать identities
|
||||||
|
if (identities && typeof identities.value === 'undefined') {
|
||||||
|
identities.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
||||||
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
||||||
@@ -163,12 +181,22 @@ const updateAuth = async ({
|
|||||||
|
|
||||||
// Обновляем идентификаторы при любом изменении аутентификации
|
// Обновляем идентификаторы при любом изменении аутентификации
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
|
try {
|
||||||
await updateIdentities();
|
await updateIdentities();
|
||||||
startIdentitiesPolling();
|
startIdentitiesPolling();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating identities in updateAuth:', error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stopIdentitiesPolling();
|
stopIdentitiesPolling();
|
||||||
|
try {
|
||||||
|
if (identities && typeof identities.value !== 'undefined') {
|
||||||
identities.value = [];
|
identities.value = [];
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing identities:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Auth updated:', {
|
console.log('Auth updated:', {
|
||||||
authenticated: isAuthenticated.value,
|
authenticated: isAuthenticated.value,
|
||||||
@@ -306,7 +334,11 @@ const checkAuth = async () => {
|
|||||||
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
||||||
if (response.data.authenticated) {
|
if (response.data.authenticated) {
|
||||||
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
||||||
|
try {
|
||||||
await updateIdentities();
|
await updateIdentities();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating identities in checkAuth:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||||
// связываем гостевые сообщения с его аккаунтом
|
// связываем гостевые сообщения с его аккаунтом
|
||||||
|
|||||||
349
frontend/src/composables/useDleContract.js
Normal file
349
frontend/src/composables/useDleContract.js
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { DLE_ABI, TOKEN_ABI } from '@/utils/dle-abi';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Композабл для работы с DLE смарт-контрактом
|
||||||
|
* Содержит правильные ABI и функции для взаимодействия с контрактом
|
||||||
|
*/
|
||||||
|
export function useDleContract() {
|
||||||
|
// Состояние
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const provider = ref(null);
|
||||||
|
const signer = ref(null);
|
||||||
|
const contract = ref(null);
|
||||||
|
const userAddress = ref(null);
|
||||||
|
const chainId = ref(null);
|
||||||
|
|
||||||
|
// Используем общий ABI из utils/dle-abi.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подключиться к кошельку
|
||||||
|
*/
|
||||||
|
const connectWallet = async () => {
|
||||||
|
try {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрашиваем подключение
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
|
||||||
|
// Создаем провайдер и подписанта
|
||||||
|
provider.value = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
signer.value = await provider.value.getSigner();
|
||||||
|
userAddress.value = await signer.value.getAddress();
|
||||||
|
|
||||||
|
// Получаем информацию о сети
|
||||||
|
const network = await provider.value.getNetwork();
|
||||||
|
chainId.value = Number(network.chainId);
|
||||||
|
|
||||||
|
isConnected.value = true;
|
||||||
|
|
||||||
|
console.log('✅ Кошелек подключен:', {
|
||||||
|
address: userAddress.value,
|
||||||
|
chainId: chainId.value,
|
||||||
|
network: network.name
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
address: userAddress.value,
|
||||||
|
chainId: chainId.value
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка подключения к кошельку:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализировать контракт
|
||||||
|
*/
|
||||||
|
const initContract = (contractAddress) => {
|
||||||
|
if (!provider.value) {
|
||||||
|
throw new Error('Провайдер не инициализирован. Сначала подключите кошелек.');
|
||||||
|
}
|
||||||
|
|
||||||
|
contract.value = new ethers.Contract(contractAddress, DLE_ABI, signer.value);
|
||||||
|
console.log('✅ DLE контракт инициализирован:', contractAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить баланс токенов пользователя
|
||||||
|
*/
|
||||||
|
const checkTokenBalance = async (contractAddress) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = await contract.value.balanceOf(userAddress.value);
|
||||||
|
const balanceFormatted = ethers.formatEther(balance);
|
||||||
|
|
||||||
|
console.log(`💰 Баланс токенов: ${balanceFormatted}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
balance: balanceFormatted,
|
||||||
|
hasTokens: balance > 0
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка проверки баланса:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
balance: '0',
|
||||||
|
hasTokens: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Голосовать за предложение
|
||||||
|
*/
|
||||||
|
const voteOnProposal = async (contractAddress, proposalId, support) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🗳️ Начинаем голосование:', { proposalId, support });
|
||||||
|
|
||||||
|
// Проверяем баланс токенов
|
||||||
|
const balanceCheck = await checkTokenBalance(contractAddress);
|
||||||
|
if (!balanceCheck.hasTokens) {
|
||||||
|
throw new Error('У вас нет токенов для голосования');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем транзакцию голосования
|
||||||
|
const tx = await contract.value.vote(proposalId, support);
|
||||||
|
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||||
|
|
||||||
|
// Ждем подтверждения
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
console.log('✅ Голосование успешно:', receipt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionHash: tx.hash,
|
||||||
|
receipt: receipt
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка голосования:', error);
|
||||||
|
|
||||||
|
// Улучшенная обработка ошибок
|
||||||
|
let errorMessage = error.message;
|
||||||
|
if (error.message.includes('execution reverted')) {
|
||||||
|
errorMessage = 'Транзакция отклонена смарт-контрактом. Возможные причины:\n' +
|
||||||
|
'• Предложение уже не активно\n' +
|
||||||
|
'• Вы уже голосовали за это предложение\n' +
|
||||||
|
'• Недостаточно прав для голосования\n' +
|
||||||
|
'• Предложение не существует';
|
||||||
|
} else if (error.message.includes('user rejected')) {
|
||||||
|
errorMessage = 'Транзакция отклонена пользователем';
|
||||||
|
} else if (error.message.includes('insufficient funds')) {
|
||||||
|
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
originalError: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение
|
||||||
|
*/
|
||||||
|
const executeProposal = async (contractAddress, proposalId) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⚡ Исполняем предложение:', proposalId);
|
||||||
|
|
||||||
|
const tx = await contract.value.executeProposal(proposalId);
|
||||||
|
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||||
|
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
console.log('✅ Предложение исполнено:', receipt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionHash: tx.hash,
|
||||||
|
receipt: receipt
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка исполнения предложения:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
originalError: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отменить предложение
|
||||||
|
*/
|
||||||
|
const cancelProposal = async (contractAddress, proposalId, reason) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('❌ Отменяем предложение:', { proposalId, reason });
|
||||||
|
|
||||||
|
const tx = await contract.value.cancelProposal(proposalId, reason);
|
||||||
|
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||||
|
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
console.log('✅ Предложение отменено:', receipt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionHash: tx.hash,
|
||||||
|
receipt: receipt
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка отмены предложения:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
originalError: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить состояние предложения
|
||||||
|
*/
|
||||||
|
const getProposalState = async (contractAddress, proposalId) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await contract.value.getProposalState(proposalId);
|
||||||
|
|
||||||
|
// 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
|
||||||
|
const stateNames = {
|
||||||
|
0: 'Pending',
|
||||||
|
1: 'Succeeded',
|
||||||
|
2: 'Defeated',
|
||||||
|
3: 'Executed',
|
||||||
|
4: 'Canceled',
|
||||||
|
5: 'ReadyForExecution'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
state: state,
|
||||||
|
stateName: stateNames[state] || 'Unknown'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения состояния предложения:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
state: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить результат предложения
|
||||||
|
*/
|
||||||
|
const checkProposalResult = async (contractAddress, proposalId) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await contract.value.checkProposalResult(proposalId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
passed: result.passed,
|
||||||
|
quorumReached: result.quorumReached
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка проверки результата предложения:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
passed: false,
|
||||||
|
quorumReached: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить информацию о DLE
|
||||||
|
*/
|
||||||
|
const getDleInfo = async (contractAddress) => {
|
||||||
|
try {
|
||||||
|
if (!contract.value) {
|
||||||
|
initContract(contractAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await contract.value.getDLEInfo();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: info.name,
|
||||||
|
symbol: info.symbol,
|
||||||
|
location: info.location,
|
||||||
|
coordinates: info.coordinates,
|
||||||
|
jurisdiction: info.jurisdiction,
|
||||||
|
okvedCodes: info.okvedCodes,
|
||||||
|
kpp: info.kpp,
|
||||||
|
creationTimestamp: info.creationTimestamp,
|
||||||
|
isActive: info.isActive
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка получения информации о DLE:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вычисляемые свойства
|
||||||
|
const isWalletConnected = computed(() => isConnected.value);
|
||||||
|
const currentUserAddress = computed(() => userAddress.value);
|
||||||
|
const currentChainId = computed(() => chainId.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Состояние
|
||||||
|
isConnected,
|
||||||
|
provider,
|
||||||
|
signer,
|
||||||
|
contract,
|
||||||
|
userAddress,
|
||||||
|
chainId,
|
||||||
|
|
||||||
|
// Вычисляемые свойства
|
||||||
|
isWalletConnected,
|
||||||
|
currentUserAddress,
|
||||||
|
currentChainId,
|
||||||
|
|
||||||
|
// Методы
|
||||||
|
connectWallet,
|
||||||
|
initContract,
|
||||||
|
checkTokenBalance,
|
||||||
|
voteOnProposal,
|
||||||
|
executeProposal,
|
||||||
|
cancelProposal,
|
||||||
|
getProposalState,
|
||||||
|
checkProposalResult,
|
||||||
|
getDleInfo
|
||||||
|
};
|
||||||
|
}
|
||||||
207
frontend/src/composables/useProposalValidation.js
Normal file
207
frontend/src/composables/useProposalValidation.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* Composable для валидации предложений DLE
|
||||||
|
* Проверяет реальность предложений по хешам транзакций
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
export function useProposalValidation() {
|
||||||
|
const validatedProposals = ref([]);
|
||||||
|
const validationErrors = ref([]);
|
||||||
|
const isValidating = ref(false);
|
||||||
|
|
||||||
|
// Проверка формата хеша транзакции
|
||||||
|
const isValidTransactionHash = (hash) => {
|
||||||
|
if (!hash) return false;
|
||||||
|
return /^0x[a-fA-F0-9]{64}$/.test(hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверка формата адреса
|
||||||
|
const isValidAddress = (address) => {
|
||||||
|
if (!address) return false;
|
||||||
|
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверка chainId
|
||||||
|
const isValidChainId = (chainId) => {
|
||||||
|
const validChainIds = [1, 11155111, 17000, 421614, 84532, 8453]; // Mainnet, Sepolia, Holesky, Arbitrum Sepolia, Base Sepolia, Base
|
||||||
|
return validChainIds.includes(Number(chainId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация предложения
|
||||||
|
const validateProposal = (proposal) => {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Проверка обязательных полей
|
||||||
|
if (!proposal.id && proposal.id !== 0) {
|
||||||
|
errors.push('Отсутствует ID предложения');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.description || proposal.description.trim() === '') {
|
||||||
|
errors.push('Отсутствует описание предложения');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.transactionHash) {
|
||||||
|
errors.push('Отсутствует хеш транзакции');
|
||||||
|
} else if (!isValidTransactionHash(proposal.transactionHash)) {
|
||||||
|
errors.push('Неверный формат хеша транзакции');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.initiator) {
|
||||||
|
errors.push('Отсутствует инициатор предложения');
|
||||||
|
} else if (!isValidAddress(proposal.initiator)) {
|
||||||
|
errors.push('Неверный формат адреса инициатора');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.chainId) {
|
||||||
|
errors.push('Отсутствует chainId');
|
||||||
|
} else if (!isValidChainId(proposal.chainId)) {
|
||||||
|
errors.push('Неподдерживаемый chainId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal.state === undefined || proposal.state === null) {
|
||||||
|
errors.push('Отсутствует статус предложения');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка числовых значений
|
||||||
|
if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) {
|
||||||
|
errors.push('Неверное значение голосов "за"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof proposal.againstVotes !== 'number' || proposal.againstVotes < 0) {
|
||||||
|
errors.push('Неверное значение голосов "против"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) {
|
||||||
|
errors.push('Неверное значение требуемого кворума');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация массива предложений
|
||||||
|
const validateProposals = (proposals) => {
|
||||||
|
isValidating.value = true;
|
||||||
|
validationErrors.value = [];
|
||||||
|
validatedProposals.value = [];
|
||||||
|
|
||||||
|
const validProposals = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
|
proposals.forEach((proposal, index) => {
|
||||||
|
const validation = validateProposal(proposal);
|
||||||
|
|
||||||
|
if (validation.isValid) {
|
||||||
|
validProposals.push(proposal);
|
||||||
|
} else {
|
||||||
|
allErrors.push({
|
||||||
|
proposalIndex: index,
|
||||||
|
proposalId: proposal.id,
|
||||||
|
errors: validation.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
validatedProposals.value = validProposals;
|
||||||
|
validationErrors.value = allErrors;
|
||||||
|
isValidating.value = false;
|
||||||
|
|
||||||
|
console.log(`[Proposal Validation] Проверено предложений: ${proposals.length}`);
|
||||||
|
console.log(`[Proposal Validation] Валидных: ${validProposals.length}`);
|
||||||
|
console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validProposals,
|
||||||
|
errors: allErrors,
|
||||||
|
totalCount: proposals.length,
|
||||||
|
validCount: validProposals.length,
|
||||||
|
errorCount: allErrors.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получение статистики валидации
|
||||||
|
const validationStats = computed(() => {
|
||||||
|
const total = validatedProposals.value.length + validationErrors.value.length;
|
||||||
|
const valid = validatedProposals.value.length;
|
||||||
|
const invalid = validationErrors.value.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
valid,
|
||||||
|
invalid,
|
||||||
|
validPercentage: total > 0 ? Math.round((valid / total) * 100) : 0,
|
||||||
|
invalidPercentage: total > 0 ? Math.round((invalid / total) * 100) : 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка, является ли предложение реальным (на основе хеша транзакции)
|
||||||
|
const isRealProposal = (proposal) => {
|
||||||
|
if (!proposal.transactionHash) return false;
|
||||||
|
|
||||||
|
// Проверяем, что хеш имеет правильный формат
|
||||||
|
if (!isValidTransactionHash(proposal.transactionHash)) return false;
|
||||||
|
|
||||||
|
// Проверяем, что это не тестовые/фейковые хеши
|
||||||
|
const fakeHashes = [
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (fakeHashes.includes(proposal.transactionHash.toLowerCase())) return false;
|
||||||
|
|
||||||
|
// Проверяем, что хеш не начинается с нулей (подозрительно)
|
||||||
|
if (proposal.transactionHash.startsWith('0x0000')) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Фильтрация только реальных предложений
|
||||||
|
const filterRealProposals = (proposals) => {
|
||||||
|
return proposals.filter(proposal => isRealProposal(proposal));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Фильтрация активных предложений (исключает выполненные и отмененные)
|
||||||
|
const filterActiveProposals = (proposals) => {
|
||||||
|
return proposals.filter(proposal => {
|
||||||
|
// Исключаем выполненные и отмененные предложения
|
||||||
|
if (proposal.executed || proposal.canceled) {
|
||||||
|
console.log(`🚫 [FILTER] Исключаем предложение ${proposal.id}: executed=${proposal.executed}, canceled=${proposal.canceled}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем предложения с истекшим deadline
|
||||||
|
if (proposal.deadline) {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
if (currentTime > proposal.deadline) {
|
||||||
|
console.log(`⏰ [FILTER] Исключаем предложение ${proposal.id}: deadline истек`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Данные
|
||||||
|
validatedProposals,
|
||||||
|
validationErrors,
|
||||||
|
isValidating,
|
||||||
|
validationStats,
|
||||||
|
|
||||||
|
// Методы
|
||||||
|
validateProposal,
|
||||||
|
validateProposals,
|
||||||
|
isRealProposal,
|
||||||
|
filterRealProposals,
|
||||||
|
filterActiveProposals,
|
||||||
|
|
||||||
|
// Вспомогательные функции
|
||||||
|
isValidTransactionHash,
|
||||||
|
isValidAddress,
|
||||||
|
isValidChainId
|
||||||
|
};
|
||||||
|
}
|
||||||
534
frontend/src/composables/useProposals.js
Normal file
534
frontend/src/composables/useProposals.js
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { getProposals } from '@/services/proposalsService';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { useProposalValidation } from './useProposalValidation';
|
||||||
|
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
|
||||||
|
|
||||||
|
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
|
||||||
|
// Функция checkTokenBalance перенесена в useDleContract.js
|
||||||
|
|
||||||
|
// Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом
|
||||||
|
|
||||||
|
export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||||
|
const proposals = ref([]);
|
||||||
|
const filteredProposals = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isVoting = ref(false);
|
||||||
|
const isExecuting = ref(false);
|
||||||
|
const isCancelling = ref(false);
|
||||||
|
const statusFilter = ref('');
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
// Используем готовые функции из utils/dle-contract.js
|
||||||
|
|
||||||
|
// Инициализируем валидацию
|
||||||
|
const {
|
||||||
|
validateProposals,
|
||||||
|
filterRealProposals,
|
||||||
|
filterActiveProposals,
|
||||||
|
validationStats,
|
||||||
|
isValidating
|
||||||
|
} = useProposalValidation();
|
||||||
|
|
||||||
|
const loadProposals = async () => {
|
||||||
|
if (!dleAddress.value) {
|
||||||
|
console.warn('Адрес DLE не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
const response = await getProposals(dleAddress.value);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const rawProposals = response.data.proposals || [];
|
||||||
|
|
||||||
|
console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`);
|
||||||
|
console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals);
|
||||||
|
|
||||||
|
// Детальная информация о каждом предложении
|
||||||
|
rawProposals.forEach((proposal, index) => {
|
||||||
|
console.log(`[Proposals] Предложение ${index}:`, {
|
||||||
|
id: proposal.id,
|
||||||
|
description: proposal.description,
|
||||||
|
state: proposal.state,
|
||||||
|
forVotes: proposal.forVotes,
|
||||||
|
againstVotes: proposal.againstVotes,
|
||||||
|
quorumRequired: proposal.quorumRequired,
|
||||||
|
quorumReached: proposal.quorumReached,
|
||||||
|
executed: proposal.executed,
|
||||||
|
canceled: proposal.canceled,
|
||||||
|
initiator: proposal.initiator,
|
||||||
|
chainId: proposal.chainId,
|
||||||
|
transactionHash: proposal.transactionHash
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Применяем валидацию предложений
|
||||||
|
const validationResult = validateProposals(rawProposals);
|
||||||
|
|
||||||
|
// Фильтруем только реальные предложения
|
||||||
|
const realProposals = filterRealProposals(validationResult.validProposals);
|
||||||
|
|
||||||
|
// Фильтруем только активные предложения (исключаем выполненные и отмененные)
|
||||||
|
const activeProposals = filterActiveProposals(realProposals);
|
||||||
|
|
||||||
|
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
|
||||||
|
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
|
||||||
|
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
|
||||||
|
|
||||||
|
if (validationResult.errorCount > 0) {
|
||||||
|
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
|
||||||
|
}
|
||||||
|
|
||||||
|
proposals.value = activeProposals;
|
||||||
|
filterProposals();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки предложений:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterProposals = () => {
|
||||||
|
if (!proposals.value || proposals.value.length === 0) {
|
||||||
|
filteredProposals.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = [...proposals.value];
|
||||||
|
|
||||||
|
if (statusFilter.value) {
|
||||||
|
filtered = filtered.filter(proposal => {
|
||||||
|
switch (statusFilter.value) {
|
||||||
|
case 'active': return proposal.state === 0; // Pending
|
||||||
|
case 'succeeded': return proposal.state === 1; // Succeeded
|
||||||
|
case 'defeated': return proposal.state === 2; // Defeated
|
||||||
|
case 'executed': return proposal.state === 3; // Executed
|
||||||
|
case 'cancelled': return proposal.state === 4; // Canceled
|
||||||
|
case 'ready': return proposal.state === 5; // ReadyForExecution
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
filtered = filtered.filter(proposal =>
|
||||||
|
proposal.description.toLowerCase().includes(query) ||
|
||||||
|
proposal.initiator.toLowerCase().includes(query) ||
|
||||||
|
proposal.uniqueId.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredProposals.value = filtered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const voteOnProposal = async (proposalId, support) => {
|
||||||
|
try {
|
||||||
|
console.log('🚀 [VOTE] Начинаем голосование через DLE контракт:', { proposalId, support, dleAddress: dleAddress.value, userAddress: userAddress.value });
|
||||||
|
isVoting.value = true;
|
||||||
|
|
||||||
|
// Проверяем наличие MetaMask
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем состояние предложения
|
||||||
|
console.log('🔍 [DEBUG] Проверяем состояние предложения...');
|
||||||
|
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
throw new Error('Предложение не найдено');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 [DEBUG] Данные предложения:', {
|
||||||
|
id: proposal.id,
|
||||||
|
state: proposal.state,
|
||||||
|
deadline: proposal.deadline,
|
||||||
|
forVotes: proposal.forVotes,
|
||||||
|
againstVotes: proposal.againstVotes,
|
||||||
|
executed: proposal.executed,
|
||||||
|
canceled: proposal.canceled
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что предложение активно (Pending)
|
||||||
|
if (proposal.state !== 0) {
|
||||||
|
const statusText = getProposalStatusText(proposal.state);
|
||||||
|
throw new Error(`Предложение не активно (статус: ${statusText}). Голосование возможно только для активных предложений.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что предложение не выполнено и не отменено
|
||||||
|
if (proposal.executed) {
|
||||||
|
throw new Error('Предложение уже выполнено. Голосование невозможно.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal.canceled) {
|
||||||
|
throw new Error('Предложение отменено. Голосование невозможно.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем deadline
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
if (proposal.deadline && currentTime > proposal.deadline) {
|
||||||
|
throw new Error('Время голосования истекло. Голосование невозможно.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем баланс токенов пользователя
|
||||||
|
console.log('💰 [DEBUG] Проверяем баланс токенов...');
|
||||||
|
try {
|
||||||
|
const balanceCheck = await checkTokenBalance(dleAddress.value, userAddress.value);
|
||||||
|
console.log('💰 [DEBUG] Баланс токенов:', balanceCheck);
|
||||||
|
|
||||||
|
if (!balanceCheck.hasTokens) {
|
||||||
|
throw new Error('У вас нет токенов для голосования. Необходимо иметь токены DLE для участия в голосовании.');
|
||||||
|
}
|
||||||
|
} catch (balanceError) {
|
||||||
|
console.warn('⚠️ [DEBUG] Ошибка проверки баланса (продолжаем):', balanceError.message);
|
||||||
|
// Не останавливаем голосование, если не удалось проверить баланс
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем сеть кошелька
|
||||||
|
console.log('🌐 [DEBUG] Проверяем сеть кошелька...');
|
||||||
|
try {
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
console.log('🌐 [DEBUG] Текущая сеть:', chainId);
|
||||||
|
console.log('🌐 [DEBUG] Сеть предложения:', proposal.chainId);
|
||||||
|
|
||||||
|
if (chainId !== proposal.chainId) {
|
||||||
|
throw new Error(`Неправильная сеть! Текущая сеть: ${chainId}, требуется: ${proposal.chainId}`);
|
||||||
|
}
|
||||||
|
} catch (networkError) {
|
||||||
|
console.warn('⚠️ [DEBUG] Ошибка проверки сети (продолжаем):', networkError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Голосуем через готовую функцию из utils/dle-contract.js
|
||||||
|
console.log('🗳️ Отправляем голосование через смарт-контракт...');
|
||||||
|
const result = await voteForProposal(dleAddress.value, proposalId, support);
|
||||||
|
|
||||||
|
console.log('✅ Голосование успешно отправлено:', result.txHash);
|
||||||
|
alert(`Голосование успешно отправлено! Хеш транзакции: ${result.txHash}`);
|
||||||
|
|
||||||
|
// Принудительно обновляем данные предложения
|
||||||
|
console.log('🔄 [VOTE] Обновляем данные после голосования...');
|
||||||
|
await loadProposals();
|
||||||
|
|
||||||
|
// Дополнительная задержка для подтверждения в блокчейне
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log('🔄 [VOTE] Повторное обновление через 3 секунды...');
|
||||||
|
await loadProposals();
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка голосования:', error);
|
||||||
|
|
||||||
|
// Улучшенная обработка ошибок
|
||||||
|
let errorMessage = error.message;
|
||||||
|
|
||||||
|
if (error.message.includes('execution reverted')) {
|
||||||
|
if (error.data === '0xe7005635') {
|
||||||
|
errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' +
|
||||||
|
'• Вы уже голосовали за это предложение\n' +
|
||||||
|
'• У вас нет токенов для голосования\n' +
|
||||||
|
'• Предложение не активно\n' +
|
||||||
|
'• Время голосования истекло';
|
||||||
|
} else if (error.data === '0xc7567e07') {
|
||||||
|
errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' +
|
||||||
|
'• Вы уже голосовали за это предложение\n' +
|
||||||
|
'• У вас нет токенов для голосования\n' +
|
||||||
|
'• Предложение не активно\n' +
|
||||||
|
'• Время голосования истекло\n' +
|
||||||
|
'• Неправильная сеть для голосования';
|
||||||
|
} else {
|
||||||
|
errorMessage = `Транзакция отклонена смарт-контрактом (код: ${error.data}). Проверьте условия голосования.`;
|
||||||
|
}
|
||||||
|
} else if (error.message.includes('user rejected')) {
|
||||||
|
errorMessage = 'Транзакция отклонена пользователем';
|
||||||
|
} else if (error.message.includes('insufficient funds')) {
|
||||||
|
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Ошибка при голосовании: ' + errorMessage);
|
||||||
|
} finally {
|
||||||
|
isVoting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeProposal = async (proposalId) => {
|
||||||
|
try {
|
||||||
|
console.log('⚡ [EXECUTE] Исполняем предложение через DLE контракт:', { proposalId, dleAddress: dleAddress.value });
|
||||||
|
isExecuting.value = true;
|
||||||
|
|
||||||
|
// Проверяем состояние предложения перед выполнением
|
||||||
|
console.log('🔍 [DEBUG] Проверяем состояние предложения для выполнения...');
|
||||||
|
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
throw new Error('Предложение не найдено');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 [DEBUG] Данные предложения для выполнения:', {
|
||||||
|
id: proposal.id,
|
||||||
|
state: proposal.state,
|
||||||
|
executed: proposal.executed,
|
||||||
|
canceled: proposal.canceled,
|
||||||
|
quorumReached: proposal.quorumReached
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что предложение можно выполнить
|
||||||
|
if (proposal.executed) {
|
||||||
|
throw new Error('Предложение уже выполнено. Повторное выполнение невозможно.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal.canceled) {
|
||||||
|
throw new Error('Предложение отменено. Выполнение невозможно.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что предложение готово к выполнению
|
||||||
|
if (proposal.state !== 5) {
|
||||||
|
const statusText = getProposalStatusText(proposal.state);
|
||||||
|
throw new Error(`Предложение не готово к выполнению (статус: ${statusText}). Выполнение возможно только для предложений в статусе "Готово к выполнению".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исполняем предложение через готовую функцию из utils/dle-contract.js
|
||||||
|
const result = await executeProposalUtil(dleAddress.value, proposalId);
|
||||||
|
|
||||||
|
console.log('✅ Предложение успешно исполнено:', result.txHash);
|
||||||
|
alert(`Предложение успешно исполнено! Хеш транзакции: ${result.txHash}`);
|
||||||
|
|
||||||
|
// Принудительно обновляем состояние предложения в UI
|
||||||
|
updateProposalState(proposalId, {
|
||||||
|
executed: true,
|
||||||
|
state: 1, // Выполнено
|
||||||
|
canceled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await loadProposals(); // Перезагружаем данные
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка выполнения предложения:', error);
|
||||||
|
|
||||||
|
// Улучшенная обработка ошибок
|
||||||
|
let errorMessage = error.message;
|
||||||
|
|
||||||
|
if (error.message.includes('execution reverted')) {
|
||||||
|
errorMessage = 'Выполнение отклонено смарт-контрактом. Возможные причины:\n' +
|
||||||
|
'• Предложение уже выполнено\n' +
|
||||||
|
'• Предложение отменено\n' +
|
||||||
|
'• Кворум не достигнут\n' +
|
||||||
|
'• Предложение не активно';
|
||||||
|
} else if (error.message.includes('user rejected')) {
|
||||||
|
errorMessage = 'Транзакция отклонена пользователем';
|
||||||
|
} else if (error.message.includes('insufficient funds')) {
|
||||||
|
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Ошибка при исполнении предложения: ' + errorMessage);
|
||||||
|
} finally {
|
||||||
|
isExecuting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelProposal = async (proposalId, reason = 'Отменено пользователем') => {
|
||||||
|
try {
|
||||||
|
console.log('❌ [CANCEL] Отменяем предложение через DLE контракт:', { proposalId, reason, dleAddress: dleAddress.value });
|
||||||
|
isCancelling.value = true;
|
||||||
|
|
||||||
|
// Проверяем состояние предложения перед отменой
|
||||||
|
console.log('🔍 [DEBUG] Проверяем состояние предложения для отмены...');
|
||||||
|
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
throw new Error('Предложение не найдено');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 [DEBUG] Данные предложения для отмены:', {
|
||||||
|
id: proposal.id,
|
||||||
|
state: proposal.state,
|
||||||
|
executed: proposal.executed,
|
||||||
|
canceled: proposal.canceled,
|
||||||
|
deadline: proposal.deadline
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что предложение можно отменить
|
||||||
|
if (proposal.executed) {
|
||||||
|
throw new Error('Предложение уже выполнено. Отмена невозможна.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal.canceled) {
|
||||||
|
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что предложение активно (Pending)
|
||||||
|
if (proposal.state !== 0) {
|
||||||
|
const statusText = getProposalStatusText(proposal.state);
|
||||||
|
throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что пользователь является инициатором
|
||||||
|
if (proposal.initiator !== userAddress.value) {
|
||||||
|
throw new Error('Только инициатор предложения может его отменить.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем deadline (нужен запас 15 минут)
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
if (proposal.deadline) {
|
||||||
|
const timeRemaining = proposal.deadline - currentTime;
|
||||||
|
if (timeRemaining <= 900) { // 15 минут запас
|
||||||
|
throw new Error('Время для отмены истекло. Отмена возможна только за 15 минут до окончания голосования.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отменяем предложение через готовую функцию из utils/dle-contract.js
|
||||||
|
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
|
||||||
|
|
||||||
|
console.log('✅ Предложение успешно отменено:', result.txHash);
|
||||||
|
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
|
||||||
|
|
||||||
|
// Принудительно обновляем состояние предложения в UI
|
||||||
|
updateProposalState(proposalId, {
|
||||||
|
canceled: true,
|
||||||
|
state: 2, // Отменено
|
||||||
|
executed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await loadProposals(); // Перезагружаем данные
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка отмены предложения:', error);
|
||||||
|
|
||||||
|
// Улучшенная обработка ошибок
|
||||||
|
let errorMessage = error.message;
|
||||||
|
|
||||||
|
if (error.message.includes('execution reverted')) {
|
||||||
|
errorMessage = 'Отмена отклонена смарт-контрактом. Возможные причины:\n' +
|
||||||
|
'• Предложение уже отменено\n' +
|
||||||
|
'• Предложение уже выполнено\n' +
|
||||||
|
'• Предложение не активно\n' +
|
||||||
|
'• Недостаточно прав для отмены';
|
||||||
|
} else if (error.message.includes('user rejected')) {
|
||||||
|
errorMessage = 'Транзакция отклонена пользователем';
|
||||||
|
} else if (error.message.includes('insufficient funds')) {
|
||||||
|
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Ошибка при отмене предложения: ' + errorMessage);
|
||||||
|
} finally {
|
||||||
|
isCancelling.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProposalStatusClass = (state) => {
|
||||||
|
switch (state) {
|
||||||
|
case 0: return 'status-active'; // Pending
|
||||||
|
case 1: return 'status-succeeded'; // Succeeded
|
||||||
|
case 2: return 'status-defeated'; // Defeated
|
||||||
|
case 3: return 'status-executed'; // Executed
|
||||||
|
case 4: return 'status-cancelled'; // Canceled
|
||||||
|
case 5: return 'status-ready'; // ReadyForExecution
|
||||||
|
default: return 'status-active';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProposalStatusText = (state) => {
|
||||||
|
switch (state) {
|
||||||
|
case 0: return 'Активное';
|
||||||
|
case 1: return 'Успешное';
|
||||||
|
case 2: return 'Отклоненное';
|
||||||
|
case 3: return 'Выполнено';
|
||||||
|
case 4: return 'Отменено';
|
||||||
|
case 5: return 'Готово к выполнению';
|
||||||
|
default: return 'Неизвестно';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQuorumPercentage = (proposal) => {
|
||||||
|
// Получаем реальные данные из предложения
|
||||||
|
const forVotes = Number(proposal.forVotes || 0);
|
||||||
|
const againstVotes = Number(proposal.againstVotes || 0);
|
||||||
|
const totalVotes = forVotes + againstVotes;
|
||||||
|
|
||||||
|
// Используем реальный totalSupply из предложения или fallback
|
||||||
|
const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE
|
||||||
|
|
||||||
|
console.log(`📊 [QUORUM] Предложение ${proposal.id}:`, {
|
||||||
|
forVotes: forVotes,
|
||||||
|
againstVotes: againstVotes,
|
||||||
|
totalVotes: totalVotes,
|
||||||
|
totalSupply: totalSupply,
|
||||||
|
forVotesFormatted: `${(forVotes / 1e+18).toFixed(2)} DLE`,
|
||||||
|
againstVotesFormatted: `${(againstVotes / 1e+18).toFixed(2)} DLE`,
|
||||||
|
totalVotesFormatted: `${(totalVotes / 1e+18).toFixed(2)} DLE`,
|
||||||
|
totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE`
|
||||||
|
});
|
||||||
|
|
||||||
|
const percentage = totalSupply > 0 ? (totalVotes / totalSupply) * 100 : 0;
|
||||||
|
return percentage.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequiredQuorumPercentage = (proposal) => {
|
||||||
|
// Получаем требуемый кворум из предложения
|
||||||
|
const requiredQuorum = Number(proposal.quorumRequired || 0);
|
||||||
|
|
||||||
|
// Используем реальный totalSupply из предложения или fallback
|
||||||
|
const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE
|
||||||
|
|
||||||
|
console.log(`📊 [REQUIRED QUORUM] Предложение ${proposal.id}:`, {
|
||||||
|
requiredQuorum: requiredQuorum,
|
||||||
|
totalSupply: totalSupply,
|
||||||
|
requiredQuorumFormatted: `${(requiredQuorum / 1e+18).toFixed(2)} DLE`,
|
||||||
|
totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE`
|
||||||
|
});
|
||||||
|
|
||||||
|
const percentage = totalSupply > 0 ? (requiredQuorum / totalSupply) * 100 : 0;
|
||||||
|
return percentage.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canVote = (proposal) => {
|
||||||
|
return proposal.state === 0; // Pending - только активные предложения
|
||||||
|
};
|
||||||
|
|
||||||
|
const canExecute = (proposal) => {
|
||||||
|
return proposal.state === 5; // ReadyForExecution - готово к выполнению
|
||||||
|
};
|
||||||
|
|
||||||
|
const canCancel = (proposal) => {
|
||||||
|
// Можно отменить только активные предложения (Pending)
|
||||||
|
return proposal.state === 0 &&
|
||||||
|
!proposal.executed &&
|
||||||
|
!proposal.canceled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Принудительное обновление состояния предложения в UI
|
||||||
|
const updateProposalState = (proposalId, updates) => {
|
||||||
|
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||||
|
if (proposal) {
|
||||||
|
Object.assign(proposal, updates);
|
||||||
|
console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates);
|
||||||
|
|
||||||
|
// Принудительно обновляем фильтрацию
|
||||||
|
filterProposals();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
proposals,
|
||||||
|
filteredProposals,
|
||||||
|
isLoading,
|
||||||
|
isVoting,
|
||||||
|
isExecuting,
|
||||||
|
isCancelling,
|
||||||
|
statusFilter,
|
||||||
|
searchQuery,
|
||||||
|
loadProposals,
|
||||||
|
filterProposals,
|
||||||
|
voteOnProposal,
|
||||||
|
executeProposal,
|
||||||
|
cancelProposal,
|
||||||
|
getProposalStatusClass,
|
||||||
|
getProposalStatusText,
|
||||||
|
getQuorumPercentage,
|
||||||
|
getRequiredQuorumPercentage,
|
||||||
|
canVote,
|
||||||
|
canExecute,
|
||||||
|
canCancel,
|
||||||
|
updateProposalState,
|
||||||
|
// Валидация
|
||||||
|
validationStats,
|
||||||
|
isValidating
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -228,14 +228,9 @@ const routes = [
|
|||||||
component: () => import('../views/smartcontracts/CreateProposalView.vue')
|
component: () => import('../views/smartcontracts/CreateProposalView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/tokens',
|
path: '/management/add-module',
|
||||||
name: 'management-tokens',
|
name: 'management-add-module',
|
||||||
component: () => import('../views/smartcontracts/TokensView.vue')
|
component: () => import('../views/smartcontracts/AddModuleFormView.vue')
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/management/quorum',
|
|
||||||
name: 'management-quorum',
|
|
||||||
component: () => import('../views/smartcontracts/QuorumView.vue')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/modules',
|
path: '/management/modules',
|
||||||
|
|||||||
@@ -57,24 +57,24 @@ export default {
|
|||||||
},
|
},
|
||||||
// --- Работа с тегами пользователя ---
|
// --- Работа с тегами пользователя ---
|
||||||
async addTagsToContact(contactId, tagIds) {
|
async addTagsToContact(contactId, tagIds) {
|
||||||
// PATCH /api/tags/user/:id { tags: [...] }
|
// PATCH /tags/user/:id { tags: [...] }
|
||||||
const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds });
|
const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds });
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
async getContactTags(contactId) {
|
async getContactTags(contactId) {
|
||||||
// GET /api/tags/user/:id
|
// GET /tags/user/:id
|
||||||
const res = await api.get(`/tags/user/${contactId}`);
|
const res = await api.get(`/tags/user/${contactId}`);
|
||||||
return res.data.tags || [];
|
return res.data.tags || [];
|
||||||
},
|
},
|
||||||
async removeTagFromContact(contactId, tagId) {
|
async removeTagFromContact(contactId, tagId) {
|
||||||
// DELETE /api/tags/user/:id/tag/:tagId
|
// DELETE /tags/user/:id/tag/:tagId
|
||||||
const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`);
|
const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getContacts() {
|
export async function getContacts() {
|
||||||
const res = await fetch('/api/users');
|
const res = await fetch('/users');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data && data.success) {
|
if (data && data.success) {
|
||||||
return data.contacts;
|
return data.contacts;
|
||||||
|
|||||||
@@ -13,6 +13,23 @@
|
|||||||
// Сервис для работы с модулями DLE
|
// Сервис для работы с модулями DLE
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить deploymentId по адресу DLE
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @returns {Promise<Object>} - Результат с deploymentId
|
||||||
|
*/
|
||||||
|
export const getDeploymentId = async (dleAddress) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/get-deployment-id', {
|
||||||
|
dleAddress
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении deploymentId:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает предложение о добавлении модуля
|
* Создает предложение о добавлении модуля
|
||||||
* @param {string} dleAddress - Адрес DLE
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
|||||||
183
frontend/src/services/multichainExecutionService.js
Normal file
183
frontend/src/services/multichainExecutionService.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить информацию о мультиконтрактном предложении
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {number} governanceChainId - ID сети голосования
|
||||||
|
* @returns {Promise<Object>} - Информация о предложении
|
||||||
|
*/
|
||||||
|
export async function getProposalMultichainInfo(dleAddress, proposalId, governanceChainId) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-multichain-execution/get-proposal-multichain-info', {
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
governanceChainId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.error || 'Не удалось получить информацию о мультиконтрактном предложении');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения информации о мультиконтрактном предложении:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение во всех целевых сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {string} deploymentId - ID деплоя
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-multichain-execution/execute-in-all-target-chains', {
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
deploymentId,
|
||||||
|
userAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.error || 'Не удалось исполнить предложение во всех целевых сетях');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка исполнения во всех целевых сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение в конкретной целевой сети
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {number} targetChainId - ID целевой сети
|
||||||
|
* @param {string} deploymentId - ID деплоя
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-multichain-execution/execute-in-target-chain', {
|
||||||
|
dleAddress,
|
||||||
|
proposalId,
|
||||||
|
targetChainId,
|
||||||
|
deploymentId,
|
||||||
|
userAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.error || 'Не удалось исполнить предложение в целевой сети');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка исполнения в целевой сети:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить deploymentId по адресу DLE
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<string>} - ID деплоя
|
||||||
|
*/
|
||||||
|
export async function getDeploymentId(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/get-deployment-id', {
|
||||||
|
dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.deploymentId;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.error || 'Не удалось получить ID деплоя');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения ID деплоя:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, является ли предложение мультиконтрактным
|
||||||
|
* @param {Object} proposal - Предложение
|
||||||
|
* @returns {boolean} - Является ли мультиконтрактным
|
||||||
|
*/
|
||||||
|
export function isMultichainProposal(proposal) {
|
||||||
|
return proposal.targetChains && proposal.targetChains.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить название сети по ID
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {string} - Название сети
|
||||||
|
*/
|
||||||
|
export function getChainName(chainId) {
|
||||||
|
const chainNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
17000: 'Holesky',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
84532: 'Base Sepolia',
|
||||||
|
137: 'Polygon',
|
||||||
|
80001: 'Polygon Mumbai',
|
||||||
|
56: 'BSC',
|
||||||
|
97: 'BSC Testnet'
|
||||||
|
};
|
||||||
|
|
||||||
|
return chainNames[chainId] || `Chain ${chainId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматировать результат исполнения
|
||||||
|
* @param {Object} result - Результат исполнения
|
||||||
|
* @returns {string} - Отформатированный результат
|
||||||
|
*/
|
||||||
|
export function formatExecutionResult(result) {
|
||||||
|
const { summary, executionResults } = result;
|
||||||
|
|
||||||
|
if (summary.successful === summary.total) {
|
||||||
|
return `✅ Успешно исполнено во всех ${summary.total} сетях`;
|
||||||
|
} else if (summary.successful > 0) {
|
||||||
|
return `⚠️ Частично исполнено: ${summary.successful}/${summary.total} сетей`;
|
||||||
|
} else {
|
||||||
|
return `❌ Не удалось исполнить ни в одной сети`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить детали ошибок исполнения
|
||||||
|
* @param {Object} result - Результат исполнения
|
||||||
|
* @returns {Array} - Массив ошибок
|
||||||
|
*/
|
||||||
|
export function getExecutionErrors(result) {
|
||||||
|
return result.executionResults
|
||||||
|
.filter(r => !r.success)
|
||||||
|
.map(r => ({
|
||||||
|
chainId: r.chainId,
|
||||||
|
chainName: getChainName(r.chainId),
|
||||||
|
error: r.error
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +20,15 @@ import axios from 'axios';
|
|||||||
*/
|
*/
|
||||||
export const getProposals = async (dleAddress) => {
|
export const getProposals = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`);
|
||||||
const response = await axios.post('/dle-proposals/get-proposals', { dleAddress });
|
const response = await axios.post('/dle-proposals/get-proposals', { dleAddress });
|
||||||
|
|
||||||
|
console.log(`🌐 [API] Ответ от backend:`, {
|
||||||
|
success: response.data.success,
|
||||||
|
proposalsCount: response.data.data?.proposals?.length || 0,
|
||||||
|
fullResponse: response.data
|
||||||
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении предложений:', error);
|
console.error('Ошибка при получении предложений:', error);
|
||||||
@@ -73,13 +81,21 @@ export const createProposal = async (dleAddress, proposalData) => {
|
|||||||
* @param {boolean} support - Поддержка предложения
|
* @param {boolean} support - Поддержка предложения
|
||||||
* @returns {Promise<Object>} - Результат голосования
|
* @returns {Promise<Object>} - Результат голосования
|
||||||
*/
|
*/
|
||||||
export const voteOnProposal = async (dleAddress, proposalId, support) => {
|
export const voteOnProposal = async (dleAddress, proposalId, support, userAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/vote-proposal', {
|
const requestData = {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId,
|
proposalId,
|
||||||
support
|
support,
|
||||||
});
|
voterAddress: userAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData);
|
||||||
|
|
||||||
|
const response = await axios.post('/dle-proposals/vote-proposal', requestData);
|
||||||
|
|
||||||
|
console.log('📥 [SERVICE] Ответ от бэкенда:', response.data);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при голосовании:', error);
|
console.error('Ошибка при голосовании:', error);
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export async function connectWithWallet() {
|
|||||||
const nonce = nonceResponse.data.nonce;
|
const nonce = nonceResponse.data.nonce;
|
||||||
// console.log('Got nonce:', nonce);
|
// console.log('Got nonce:', nonce);
|
||||||
|
|
||||||
|
if (!nonce) {
|
||||||
|
throw new Error('Не удалось получить nonce с сервера');
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем сообщение для подписи
|
// Создаем сообщение для подписи
|
||||||
const domain = window.location.host;
|
const domain = window.location.host;
|
||||||
const origin = window.location.origin;
|
const origin = window.location.origin;
|
||||||
@@ -73,7 +77,7 @@ export async function connectWithWallet() {
|
|||||||
// chainId: 1,
|
// chainId: 1,
|
||||||
// nonce,
|
// nonce,
|
||||||
// issuedAt,
|
// issuedAt,
|
||||||
// resources: [`${origin}/api/auth/verify`],
|
// resources: [`${origin}/auth/verify`],
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// Запрашиваем подпись
|
// Запрашиваем подпись
|
||||||
|
|||||||
106
frontend/src/utils/dle-abi.js
Normal file
106
frontend/src/utils/dle-abi.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* ABI для DLE смарт-контракта
|
||||||
|
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||||
|
* Для обновления запустите: node backend/scripts/generate-abi.js
|
||||||
|
*
|
||||||
|
* Последнее обновление: 2025-09-29T18:16:32.027Z
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DLE_ABI = [
|
||||||
|
"function CLOCK_MODE() returns (string)",
|
||||||
|
"function DOMAIN_SEPARATOR() returns (bytes32)",
|
||||||
|
"function activeModules(bytes32 ) returns (bool)",
|
||||||
|
"function allProposalIds(uint256 ) returns (uint256)",
|
||||||
|
"function allowance(address owner, address spender) returns (uint256)",
|
||||||
|
"function approve(address , uint256 ) returns (bool)",
|
||||||
|
"function balanceOf(address account) returns (uint256)",
|
||||||
|
"function cancelProposal(uint256 _proposalId, string reason)",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) returns (bool, bool)",
|
||||||
|
"function checkpoints(address account, uint32 pos) returns (tuple)",
|
||||||
|
"function clock() returns (uint48)",
|
||||||
|
"function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)",
|
||||||
|
"function createProposal(string _description, uint256 _duration, bytes _operation, uint256 _governanceChainId, uint256[] _targetChains, uint256 ) returns (uint256)",
|
||||||
|
"function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)",
|
||||||
|
"function decimals() returns (uint8)",
|
||||||
|
"function delegate(address delegatee)",
|
||||||
|
"function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)",
|
||||||
|
"function delegates(address account) returns (address)",
|
||||||
|
"function dleInfo() returns (string, string, string, string, uint256, uint256, uint256, bool)",
|
||||||
|
"function eip712Domain() returns (bytes1, string, string, uint256, address, bytes32, uint256[])",
|
||||||
|
"function executeProposal(uint256 _proposalId)",
|
||||||
|
"function executeProposalBySignatures(uint256 _proposalId, address[] signers, bytes[] signatures)",
|
||||||
|
"function getCurrentChainId() returns (uint256)",
|
||||||
|
"function getDLEInfo() returns (tuple)",
|
||||||
|
"function getModuleAddress(bytes32 _moduleId) returns (address)",
|
||||||
|
"function getMultichainAddresses() returns (uint256[], address[])",
|
||||||
|
"function getMultichainInfo() returns (uint256[], uint256)",
|
||||||
|
"function getMultichainMetadata() returns (string)",
|
||||||
|
"function getPastTotalSupply(uint256 timepoint) returns (uint256)",
|
||||||
|
"function getPastVotes(address account, uint256 timepoint) returns (uint256)",
|
||||||
|
"function getProposalState(uint256 _proposalId) returns (uint8)",
|
||||||
|
"function getProposalSummary(uint256 _proposalId) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, uint256, uint256, uint256[])",
|
||||||
|
"function getSupportedChainCount() returns (uint256)",
|
||||||
|
"function getSupportedChainId(uint256 _index) returns (uint256)",
|
||||||
|
"function getVotes(address account) returns (uint256)",
|
||||||
|
"function initializeLogoURI(string _logoURI)",
|
||||||
|
"function initializer() returns (address)",
|
||||||
|
"function isActive() returns (bool)",
|
||||||
|
"function isChainSupported(uint256 _chainId) returns (bool)",
|
||||||
|
"function isModuleActive(bytes32 _moduleId) returns (bool)",
|
||||||
|
"function logo() returns (string)",
|
||||||
|
"function logoURI() returns (string)",
|
||||||
|
"function maxVotingDuration() returns (uint256)",
|
||||||
|
"function minVotingDuration() returns (uint256)",
|
||||||
|
"function modules(bytes32 ) returns (address)",
|
||||||
|
"function name() returns (string)",
|
||||||
|
"function nonces(address owner) returns (uint256)",
|
||||||
|
"function numCheckpoints(address account) returns (uint32)",
|
||||||
|
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
||||||
|
"function proposalCounter() returns (uint256)",
|
||||||
|
"function proposals(uint256 ) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, bytes, uint256, uint256)",
|
||||||
|
"function quorumPercentage() returns (uint256)",
|
||||||
|
"function supportedChainIds(uint256 ) returns (uint256)",
|
||||||
|
"function supportedChains(uint256 ) returns (bool)",
|
||||||
|
"function symbol() returns (string)",
|
||||||
|
"function tokenURI() returns (string)",
|
||||||
|
"function totalSupply() returns (uint256)",
|
||||||
|
"function transfer(address , uint256 ) returns (bool)",
|
||||||
|
"function transferFrom(address , address , uint256 ) returns (bool)",
|
||||||
|
"function vote(uint256 _proposalId, bool _support)",
|
||||||
|
"event Approval(address owner, address spender, uint256 value)",
|
||||||
|
"event ChainAdded(uint256 chainId)",
|
||||||
|
"event ChainRemoved(uint256 chainId)",
|
||||||
|
"event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)",
|
||||||
|
"event DLEInitialized(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, address tokenAddress, uint256[] supportedChainIds)",
|
||||||
|
"event DelegateChanged(address delegator, address fromDelegate, address toDelegate)",
|
||||||
|
"event DelegateVotesChanged(address delegate, uint256 previousVotes, uint256 newVotes)",
|
||||||
|
"event EIP712DomainChanged()",
|
||||||
|
"event InitialTokensDistributed(address[] partners, uint256[] amounts)",
|
||||||
|
"event LogoURIUpdated(string oldURI, string newURI)",
|
||||||
|
"event ModuleAdded(bytes32 moduleId, address moduleAddress)",
|
||||||
|
"event ModuleRemoved(bytes32 moduleId)",
|
||||||
|
"event ProposalCancelled(uint256 proposalId, string reason)",
|
||||||
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)",
|
||||||
|
"event ProposalExecuted(uint256 proposalId, bytes operation)",
|
||||||
|
"event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId)",
|
||||||
|
"event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId)",
|
||||||
|
"event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains)",
|
||||||
|
"event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower)",
|
||||||
|
"event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)",
|
||||||
|
"event TokensTransferredByGovernance(address recipient, uint256 amount)",
|
||||||
|
"event Transfer(address from, address to, uint256 value)",
|
||||||
|
"event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration)",
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ
|
||||||
|
export const DLE_DEACTIVATION_ABI = [
|
||||||
|
// Эти функции не существуют в контракте DLE
|
||||||
|
];
|
||||||
|
|
||||||
|
// ABI для токенов (базовые функции)
|
||||||
|
export const TOKEN_ABI = [
|
||||||
|
"function balanceOf(address owner) view returns (uint256)",
|
||||||
|
"function decimals() view returns (uint8)",
|
||||||
|
"function totalSupply() view returns (uint256)"
|
||||||
|
];
|
||||||
@@ -12,6 +12,91 @@
|
|||||||
|
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi';
|
||||||
|
|
||||||
|
// Функция для переключения сети кошелька
|
||||||
|
export async function switchToVotingNetwork(chainId) {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`);
|
||||||
|
|
||||||
|
// Конфигурации сетей
|
||||||
|
const networks = {
|
||||||
|
'11155111': { // Sepolia
|
||||||
|
chainId: '0xaa36a7',
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||||
|
rpcUrls: ['https://1rpc.io/sepolia'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||||
|
},
|
||||||
|
'17000': { // Holesky
|
||||||
|
chainId: '0x4268',
|
||||||
|
chainName: 'Holesky',
|
||||||
|
nativeCurrency: { name: 'Holesky Ether', symbol: 'ETH', decimals: 18 },
|
||||||
|
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||||
|
blockExplorerUrls: ['https://holesky.etherscan.io']
|
||||||
|
},
|
||||||
|
'421614': { // Arbitrum Sepolia
|
||||||
|
chainId: '0x66eee',
|
||||||
|
chainName: 'Arbitrum Sepolia',
|
||||||
|
nativeCurrency: { name: 'Arbitrum Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||||
|
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.arbiscan.io']
|
||||||
|
},
|
||||||
|
'84532': { // Base Sepolia
|
||||||
|
chainId: '0x14a34',
|
||||||
|
chainName: 'Base Sepolia',
|
||||||
|
nativeCurrency: { name: 'Base Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||||
|
rpcUrls: ['https://sepolia.base.org'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.basescan.org']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const networkConfig = networks[chainId];
|
||||||
|
if (!networkConfig) {
|
||||||
|
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, подключена ли уже нужная сеть
|
||||||
|
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
if (currentChainId === networkConfig.chainId) {
|
||||||
|
console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся переключиться на нужную сеть
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: networkConfig.chainId }]
|
||||||
|
});
|
||||||
|
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`);
|
||||||
|
return true;
|
||||||
|
} catch (switchError) {
|
||||||
|
// Если сеть не добавлена, добавляем её
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log(`➕ [NETWORK] Добавляем сеть ${chainId}...`);
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [networkConfig]
|
||||||
|
});
|
||||||
|
console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`);
|
||||||
|
return true;
|
||||||
|
} catch (addError) {
|
||||||
|
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить подключение к браузерному кошельку
|
* Проверить подключение к браузерному кошельку
|
||||||
@@ -60,6 +145,8 @@ export async function checkWalletConnection() {
|
|||||||
* Используется только система голосования (proposals)
|
* Используется только система голосования (proposals)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить информацию о DLE из блокчейна
|
* Получить информацию о DLE из блокчейна
|
||||||
* @param {string} dleAddress - Адрес DLE контракта
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
@@ -109,12 +196,9 @@ export async function createProposal(dleAddress, proposalData) {
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
// ABI для создания предложения
|
// Используем общий ABI
|
||||||
const dleAbi = [
|
|
||||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Создаем предложение
|
// Создаем предложение
|
||||||
const tx = await dle.createProposal(
|
const tx = await dle.createProposal(
|
||||||
@@ -162,14 +246,111 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
// ABI для голосования
|
// Используем общий ABI
|
||||||
const dleAbi = [
|
let dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
"function vote(uint256 _proposalId, bool _support) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
// Дополнительная диагностика перед голосованием
|
||||||
|
try {
|
||||||
|
console.log('🔍 [VOTE DEBUG] Проверяем состояние предложения...');
|
||||||
|
const proposalState = await dle.getProposalState(proposalId);
|
||||||
|
console.log('🔍 [VOTE DEBUG] Состояние предложения:', proposalState);
|
||||||
|
|
||||||
|
// Проверяем, можно ли голосовать (состояние должно быть 0 = Pending)
|
||||||
|
if (Number(proposalState) !== 0) {
|
||||||
|
throw new Error(`Предложение в состоянии ${proposalState}, голосование невозможно`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
|
||||||
|
|
||||||
|
// Проверяем сеть голосования
|
||||||
|
try {
|
||||||
|
const proposal = await dle.proposals(proposalId);
|
||||||
|
const currentChainId = await dle.getCurrentChainId();
|
||||||
|
const governanceChainId = proposal.governanceChainId;
|
||||||
|
|
||||||
|
console.log('🔍 [VOTE DEBUG] Текущая сеть контракта:', currentChainId.toString());
|
||||||
|
console.log('🔍 [VOTE DEBUG] Сеть голосования предложения:', governanceChainId.toString());
|
||||||
|
|
||||||
|
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||||
|
console.log('🔄 [VOTE DEBUG] Неправильная сеть! Пытаемся переключиться...');
|
||||||
|
|
||||||
|
// Пытаемся переключить сеть
|
||||||
|
const switched = await switchToVotingNetwork(governanceChainId.toString());
|
||||||
|
if (switched) {
|
||||||
|
console.log('✅ [VOTE DEBUG] Сеть успешно переключена, переподключаемся к контракту...');
|
||||||
|
|
||||||
|
// Определяем правильный адрес контракта для сети голосования
|
||||||
|
let correctContractAddress = dleAddress;
|
||||||
|
|
||||||
|
// Если контракт развернут в другой сети, нужно найти контракт в нужной сети
|
||||||
|
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||||
|
console.log('🔍 [VOTE DEBUG] Ищем контракт в сети голосования...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем информацию о мультичейн развертывании из БД
|
||||||
|
const response = await fetch('/api/dle-core/get-multichain-contracts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
originalContract: dleAddress,
|
||||||
|
targetChainId: governanceChainId.toString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.contractAddress) {
|
||||||
|
correctContractAddress = data.contractAddress;
|
||||||
|
console.log('🔍 [VOTE DEBUG] Найден контракт в сети голосования:', correctContractAddress);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ [VOTE DEBUG] Контракт в сети голосования не найден, используем исходный');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ [VOTE DEBUG] Ошибка получения контракта из БД, используем исходный');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ [VOTE DEBUG] Ошибка поиска контракта, используем исходный:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переподключаемся к контракту в новой сети
|
||||||
|
const newProvider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const newSigner = await newProvider.getSigner();
|
||||||
|
dle = new ethers.Contract(correctContractAddress, DLE_ABI, newSigner);
|
||||||
|
|
||||||
|
// Проверяем, что теперь все корректно
|
||||||
|
const newCurrentChainId = await dle.getCurrentChainId();
|
||||||
|
console.log('🔍 [VOTE DEBUG] Новая текущая сеть контракта:', newCurrentChainId.toString());
|
||||||
|
|
||||||
|
if (newCurrentChainId.toString() === governanceChainId.toString()) {
|
||||||
|
console.log('✅ [VOTE DEBUG] Сеть для голосования теперь корректна');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Не удалось переключиться на правильную сеть. Текущая: ${newCurrentChainId}, требуется: ${governanceChainId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Неправильная сеть! Контракт в сети ${currentChainId}, а голосование должно быть в сети ${governanceChainId}. Переключите кошелек вручную.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔍 [VOTE DEBUG] Сеть для голосования корректна');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем право голоса
|
||||||
|
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
|
||||||
|
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
|
||||||
|
if (votingPower === 0n) {
|
||||||
|
throw new Error('У пользователя нет права голоса (votingPower = 0)');
|
||||||
|
}
|
||||||
|
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
|
||||||
|
} catch (votingPowerError) {
|
||||||
|
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (debugError) {
|
||||||
|
console.warn('⚠️ [VOTE DEBUG] Ошибка диагностики (продолжаем):', debugError.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Голосуем за предложение
|
// Голосуем за предложение
|
||||||
|
console.log('🗳️ [VOTE] Отправляем транзакцию голосования...');
|
||||||
const tx = await dle.vote(proposalId, support);
|
const tx = await dle.vote(proposalId, support);
|
||||||
|
|
||||||
// Ждем подтверждения транзакции
|
// Ждем подтверждения транзакции
|
||||||
@@ -184,6 +365,36 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка голосования:', error);
|
console.error('Ошибка голосования:', error);
|
||||||
|
|
||||||
|
// Детальная диагностика ошибки
|
||||||
|
if (error.code === 'CALL_EXCEPTION' && error.data) {
|
||||||
|
console.error('🔍 [ERROR DEBUG] Детали ошибки:', {
|
||||||
|
code: error.code,
|
||||||
|
data: error.data,
|
||||||
|
reason: error.reason,
|
||||||
|
action: error.action
|
||||||
|
});
|
||||||
|
|
||||||
|
// Расшифровка кода ошибки
|
||||||
|
if (error.data === '0x2eaf0f6d') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrWrongChain - неправильная сеть для голосования');
|
||||||
|
} else if (error.data === '0xe7005635') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrAlreadyVoted - пользователь уже голосовал по этому предложению');
|
||||||
|
} else if (error.data === '0x21c19873') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrNoPower - у пользователя нет права голоса');
|
||||||
|
} else if (error.data === '0x834d7b85') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalMissing - предложение не найдено');
|
||||||
|
} else if (error.data === '0xd6792fad') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalEnded - время голосования истекло');
|
||||||
|
} else if (error.data === '0x2d686f73') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalExecuted - предложение уже исполнено');
|
||||||
|
} else if (error.data === '0xc7567e07') {
|
||||||
|
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalCanceled - предложение отменено');
|
||||||
|
} else {
|
||||||
|
console.error('❌ [ERROR DEBUG] Неизвестная ошибка:', error.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,12 +417,9 @@ export async function executeProposal(dleAddress, proposalId) {
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
// ABI для исполнения предложения
|
// Используем общий ABI
|
||||||
const dleAbi = [
|
|
||||||
"function executeProposal(uint256 _proposalId) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Исполняем предложение
|
// Исполняем предложение
|
||||||
const tx = await dle.executeProposal(proposalId);
|
const tx = await dle.executeProposal(proposalId);
|
||||||
@@ -233,30 +441,112 @@ export async function executeProposal(dleAddress, proposalId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать предложение о добавлении модуля
|
* Отменить предложение
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {string} reason - Причина отмены
|
||||||
|
* @returns {Promise<Object>} - Результат отмены
|
||||||
|
*/
|
||||||
|
export async function cancelProposal(dleAddress, proposalId, reason) {
|
||||||
|
try {
|
||||||
|
// Проверяем наличие браузерного кошелька
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрашиваем подключение к кошельку
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
|
// Используем общий ABI
|
||||||
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
|
// Отменяем предложение
|
||||||
|
const tx = await dle.cancelProposal(proposalId, reason);
|
||||||
|
|
||||||
|
// Ждем подтверждения транзакции
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('Предложение отменено, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка отмены предложения:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить баланс токенов пользователя
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Баланс токенов
|
||||||
|
*/
|
||||||
|
export async function checkTokenBalance(dleAddress, userAddress) {
|
||||||
|
try {
|
||||||
|
// Проверяем наличие браузерного кошелька
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем провайдер (только для чтения)
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, provider);
|
||||||
|
|
||||||
|
// Получаем баланс токенов
|
||||||
|
const balance = await dle.balanceOf(userAddress);
|
||||||
|
const balanceFormatted = ethers.formatEther(balance);
|
||||||
|
|
||||||
|
console.log(`💰 Баланс токенов для ${userAddress}: ${balanceFormatted}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
balance: balanceFormatted,
|
||||||
|
hasTokens: balance > 0,
|
||||||
|
rawBalance: balance.toString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки баланса токенов:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать предложение о добавлении модуля (с автоматической оплатой газа)
|
||||||
* @param {string} dleAddress - Адрес DLE контракта
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
* @param {string} description - Описание предложения
|
* @param {string} description - Описание предложения
|
||||||
* @param {number} duration - Длительность голосования в секундах
|
* @param {number} duration - Длительность голосования в секундах
|
||||||
* @param {string} moduleId - ID модуля
|
* @param {string} moduleId - ID модуля
|
||||||
* @param {string} moduleAddress - Адрес модуля
|
* @param {string} moduleAddress - Адрес модуля
|
||||||
* @param {number} chainId - ID цепочки для голосования
|
* @param {number} chainId - ID цепочки для голосования
|
||||||
|
* @param {string} deploymentId - ID деплоя для получения приватного ключа (опционально)
|
||||||
* @returns {Promise<Object>} - Результат создания предложения
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
*/
|
*/
|
||||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId = null) {
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/blockchain/create-add-module-proposal', {
|
const requestData = {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
description: description,
|
description: description,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
moduleId: moduleId,
|
moduleId: moduleId,
|
||||||
moduleAddress: moduleAddress,
|
moduleAddress: moduleAddress,
|
||||||
chainId: chainId
|
chainId: chainId
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Добавляем deploymentId если он передан
|
||||||
|
if (deploymentId) {
|
||||||
|
requestData.deploymentId = deploymentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post('/dle-modules/create-add-module-proposal', requestData);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля');
|
throw new Error(response.data.error || 'Не удалось создать предложение о добавлении модуля');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка создания предложения о добавлении модуля:', error);
|
console.error('Ошибка создания предложения о добавлении модуля:', error);
|
||||||
@@ -537,6 +827,7 @@ export async function getSupportedChains(dleAddress) {
|
|||||||
* @param {string} userAddress - Адрес пользователя
|
* @param {string} userAddress - Адрес пользователя
|
||||||
* @returns {Promise<Object>} - Результат деактивации
|
* @returns {Promise<Object>} - Результат деактивации
|
||||||
*/
|
*/
|
||||||
|
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||||
export async function deactivateDLE(dleAddress, userAddress) {
|
export async function deactivateDLE(dleAddress, userAddress) {
|
||||||
try {
|
try {
|
||||||
// Проверяем наличие браузерного кошелька
|
// Проверяем наличие браузерного кошелька
|
||||||
@@ -568,15 +859,9 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
|||||||
|
|
||||||
console.log('Проверка деактивации прошла успешно, выполняем деактивацию...');
|
console.log('Проверка деактивации прошла успешно, выполняем деактивацию...');
|
||||||
|
|
||||||
// ABI для деактивации DLE
|
// Используем общий ABI для деактивации
|
||||||
const dleAbi = [
|
|
||||||
"function deactivate() external",
|
|
||||||
"function balanceOf(address) external view returns (uint256)",
|
|
||||||
"function totalSupply() external view returns (uint256)",
|
|
||||||
"function isActive() external view returns (bool)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Дополнительные проверки перед деактивацией
|
// Дополнительные проверки перед деактивацией
|
||||||
const balance = await dle.balanceOf(userAddress);
|
const balance = await dle.balanceOf(userAddress);
|
||||||
@@ -640,6 +925,7 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
|||||||
* @param {number} chainId - ID цепочки для деактивации
|
* @param {number} chainId - ID цепочки для деактивации
|
||||||
* @returns {Promise<Object>} - Результат создания предложения
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
*/
|
*/
|
||||||
|
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||||
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
|
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
|
||||||
try {
|
try {
|
||||||
// Проверяем наличие браузерного кошелька
|
// Проверяем наличие браузерного кошелька
|
||||||
@@ -650,11 +936,9 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
const dleAbi = [
|
// Используем общий ABI для деактивации
|
||||||
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||||
|
|
||||||
const tx = await dle.createDeactivationProposal(description, duration, chainId);
|
const tx = await dle.createDeactivationProposal(description, duration, chainId);
|
||||||
const receipt = await tx.wait();
|
const receipt = await tx.wait();
|
||||||
@@ -681,6 +965,7 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
|||||||
* @param {boolean} support - Поддержка предложения
|
* @param {boolean} support - Поддержка предложения
|
||||||
* @returns {Promise<Object>} - Результат голосования
|
* @returns {Promise<Object>} - Результат голосования
|
||||||
*/
|
*/
|
||||||
|
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||||
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
|
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
|
||||||
try {
|
try {
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
@@ -690,11 +975,9 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
const dleAbi = [
|
// Используем общий ABI для деактивации
|
||||||
"function voteDeactivation(uint256 _proposalId, bool _support) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||||
|
|
||||||
const tx = await dle.voteDeactivation(proposalId, support);
|
const tx = await dle.voteDeactivation(proposalId, support);
|
||||||
const receipt = await tx.wait();
|
const receipt = await tx.wait();
|
||||||
@@ -744,6 +1027,7 @@ export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
|||||||
* @param {number} proposalId - ID предложения
|
* @param {number} proposalId - ID предложения
|
||||||
* @returns {Promise<Object>} - Результат исполнения
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
*/
|
*/
|
||||||
|
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||||
export async function executeDeactivationProposal(dleAddress, proposalId) {
|
export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||||
try {
|
try {
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
@@ -753,11 +1037,9 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
const dleAbi = [
|
// Используем общий ABI для деактивации
|
||||||
"function executeDeactivationProposal(uint256 _proposalId) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||||
|
|
||||||
const tx = await dle.executeDeactivationProposal(proposalId);
|
const tx = await dle.executeDeactivationProposal(proposalId);
|
||||||
const receipt = await tx.wait();
|
const receipt = await tx.wait();
|
||||||
@@ -823,12 +1105,9 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
|||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
// ABI для создания предложения
|
// Используем общий ABI
|
||||||
const dleAbi = [
|
|
||||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Кодируем операцию перевода токенов
|
// Кодируем операцию перевода токенов
|
||||||
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
|
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
|
||||||
@@ -873,3 +1152,74 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить мультиконтрактное предложение во всех целевых сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeMultichainProposal(dleAddress, proposalId, userAddress) {
|
||||||
|
try {
|
||||||
|
// Импортируем сервис мультиконтрактного исполнения
|
||||||
|
const {
|
||||||
|
executeInAllTargetChains,
|
||||||
|
getDeploymentId,
|
||||||
|
formatExecutionResult,
|
||||||
|
getExecutionErrors
|
||||||
|
} = await import('@/services/multichainExecutionService');
|
||||||
|
|
||||||
|
// Получаем ID деплоя
|
||||||
|
const deploymentId = await getDeploymentId(dleAddress);
|
||||||
|
|
||||||
|
// Исполняем во всех целевых сетях
|
||||||
|
const result = await executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result,
|
||||||
|
summary: formatExecutionResult(result),
|
||||||
|
errors: getExecutionErrors(result)
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка исполнения мультиконтрактного предложения:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить мультиконтрактное предложение в конкретной сети
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {number} targetChainId - ID целевой сети
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeMultichainProposalInChain(dleAddress, proposalId, targetChainId, userAddress) {
|
||||||
|
try {
|
||||||
|
// Импортируем сервис мультиконтрактного исполнения
|
||||||
|
const {
|
||||||
|
executeInTargetChain,
|
||||||
|
getDeploymentId,
|
||||||
|
getChainName
|
||||||
|
} = await import('@/services/multichainExecutionService');
|
||||||
|
|
||||||
|
// Получаем ID деплоя
|
||||||
|
const deploymentId = await getDeploymentId(dleAddress);
|
||||||
|
|
||||||
|
// Исполняем в конкретной сети
|
||||||
|
const result = await executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result,
|
||||||
|
chainName: getChainName(targetChainId)
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка исполнения мультиконтрактного предложения в сети:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
frontend/src/utils/networkConfig.js
Normal file
105
frontend/src/utils/networkConfig.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Конфигурации сетей блокчейна для DLE
|
||||||
|
*
|
||||||
|
* Author: HB3 Accelerator
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SUPPORTED_NETWORKS = {
|
||||||
|
1: {
|
||||||
|
chainId: '0x1',
|
||||||
|
chainName: 'Ethereum Mainnet',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://mainnet.infura.io/v3/'],
|
||||||
|
blockExplorerUrls: ['https://etherscan.io'],
|
||||||
|
},
|
||||||
|
11155111: {
|
||||||
|
chainId: '0xaa36a7',
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Sepolia Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://sepolia.infura.io/v3/', 'https://1rpc.io/sepolia'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io'],
|
||||||
|
},
|
||||||
|
17000: {
|
||||||
|
chainId: '0x4268',
|
||||||
|
chainName: 'Holesky',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Holesky Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||||
|
blockExplorerUrls: ['https://holesky.etherscan.io'],
|
||||||
|
},
|
||||||
|
421614: {
|
||||||
|
chainId: '0x66eee',
|
||||||
|
chainName: 'Arbitrum Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Arbitrum Sepolia Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.arbiscan.io'],
|
||||||
|
},
|
||||||
|
84532: {
|
||||||
|
chainId: '0x14a34',
|
||||||
|
chainName: 'Base Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Base Sepolia Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://sepolia.base.org'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.basescan.org'],
|
||||||
|
},
|
||||||
|
8453: {
|
||||||
|
chainId: '0x2105',
|
||||||
|
chainName: 'Base',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'Base Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://mainnet.base.org'],
|
||||||
|
blockExplorerUrls: ['https://basescan.org'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить конфигурацию сети по chainId
|
||||||
|
* @param {number|string} chainId - ID сети
|
||||||
|
* @returns {Object|null} - Конфигурация сети или null
|
||||||
|
*/
|
||||||
|
export function getNetworkConfig(chainId) {
|
||||||
|
const numericChainId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId;
|
||||||
|
return SUPPORTED_NETWORKS[numericChainId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить hex представление chainId
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {string} - Hex представление
|
||||||
|
*/
|
||||||
|
export function getHexChainId(chainId) {
|
||||||
|
return `0x${chainId.toString(16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, поддерживается ли сеть
|
||||||
|
* @param {number|string} chainId - ID сети
|
||||||
|
* @returns {boolean} - Поддерживается ли сеть
|
||||||
|
*/
|
||||||
|
export function isNetworkSupported(chainId) {
|
||||||
|
return getNetworkConfig(chainId) !== null;
|
||||||
|
}
|
||||||
157
frontend/src/utils/networkSwitcher.js
Normal file
157
frontend/src/utils/networkSwitcher.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Утилиты для переключения сетей блокчейна
|
||||||
|
*
|
||||||
|
* Author: HB3 Accelerator
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getNetworkConfig, getHexChainId, isNetworkSupported } from './networkConfig.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить сеть в MetaMask
|
||||||
|
* @param {number} targetChainId - ID целевой сети
|
||||||
|
* @returns {Promise<Object>} - Результат переключения
|
||||||
|
*/
|
||||||
|
export async function switchNetwork(targetChainId) {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 [Network Switch] Переключаемся на сеть ${targetChainId}...`);
|
||||||
|
|
||||||
|
// Проверяем, поддерживается ли сеть
|
||||||
|
if (!isNetworkSupported(targetChainId)) {
|
||||||
|
throw new Error(`Сеть ${targetChainId} не поддерживается`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие MetaMask
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем конфигурацию сети
|
||||||
|
const networkConfig = getNetworkConfig(targetChainId);
|
||||||
|
if (!networkConfig) {
|
||||||
|
throw new Error(`Конфигурация для сети ${targetChainId} не найдена`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем текущую сеть
|
||||||
|
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
console.log(`🔄 [Network Switch] Текущая сеть: ${currentChainId}, Целевая: ${getHexChainId(targetChainId)}`);
|
||||||
|
|
||||||
|
// Если уже в нужной сети, возвращаем успех
|
||||||
|
if (currentChainId === getHexChainId(targetChainId)) {
|
||||||
|
console.log(`✅ [Network Switch] Уже в сети ${targetChainId}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Уже в сети ${networkConfig.chainName}`,
|
||||||
|
chainId: targetChainId,
|
||||||
|
chainName: networkConfig.chainName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся переключиться на существующую сеть
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: getHexChainId(targetChainId) }],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ [Network Switch] Успешно переключились на ${networkConfig.chainName}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Переключились на ${networkConfig.chainName}`,
|
||||||
|
chainId: targetChainId,
|
||||||
|
chainName: networkConfig.chainName
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (switchError) {
|
||||||
|
// Если сеть не добавлена в MetaMask, добавляем её
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log(`➕ [Network Switch] Добавляем сеть ${networkConfig.chainName} в MetaMask...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: getHexChainId(targetChainId),
|
||||||
|
chainName: networkConfig.chainName,
|
||||||
|
nativeCurrency: networkConfig.nativeCurrency,
|
||||||
|
rpcUrls: networkConfig.rpcUrls,
|
||||||
|
blockExplorerUrls: networkConfig.blockExplorerUrls,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ [Network Switch] Сеть ${networkConfig.chainName} добавлена и активирована`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Сеть ${networkConfig.chainName} добавлена и активирована`,
|
||||||
|
chainId: targetChainId,
|
||||||
|
chainName: networkConfig.chainName
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (addError) {
|
||||||
|
console.error(`❌ [Network Switch] Ошибка добавления сети:`, addError);
|
||||||
|
throw new Error(`Не удалось добавить сеть ${networkConfig.chainName}: ${addError.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Другие ошибки переключения
|
||||||
|
console.error(`❌ [Network Switch] Ошибка переключения сети:`, switchError);
|
||||||
|
throw new Error(`Не удалось переключиться на ${networkConfig.chainName}: ${switchError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [Network Switch] Ошибка:`, error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
chainId: targetChainId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить текущую сеть
|
||||||
|
* @returns {Promise<Object>} - Информация о текущей сети
|
||||||
|
*/
|
||||||
|
export async function getCurrentNetwork() {
|
||||||
|
try {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не найден');
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
const numericChainId = parseInt(chainId, 16);
|
||||||
|
const networkConfig = getNetworkConfig(numericChainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
chainId: numericChainId,
|
||||||
|
hexChainId: chainId,
|
||||||
|
chainName: networkConfig?.chainName || 'Неизвестная сеть',
|
||||||
|
isSupported: isNetworkSupported(numericChainId)
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Network Check] Ошибка:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список поддерживаемых сетей
|
||||||
|
* @returns {Array} - Список поддерживаемых сетей
|
||||||
|
*/
|
||||||
|
export function getSupportedNetworks() {
|
||||||
|
return Object.entries(SUPPORTED_NETWORKS).map(([chainId, config]) => ({
|
||||||
|
chainId: parseInt(chainId),
|
||||||
|
hexChainId: getHexChainId(parseInt(chainId)),
|
||||||
|
chainName: config.chainName,
|
||||||
|
nativeCurrency: config.nativeCurrency,
|
||||||
|
rpcUrls: config.rpcUrls,
|
||||||
|
blockExplorerUrls: config.blockExplorerUrls
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ class WebSocketClient {
|
|||||||
console.log('[WebSocket] Подключение установлено');
|
console.log('[WebSocket] Подключение установлено');
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
|
|
||||||
|
// Уведомляем о подключении
|
||||||
|
this.emit('connected');
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.onmessage = (event) => {
|
||||||
@@ -120,6 +123,15 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Эмиссия события
|
||||||
|
emit(event, data) {
|
||||||
|
if (this.listeners.has(event)) {
|
||||||
|
this.listeners.get(event).forEach(callback => {
|
||||||
|
callback(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
||||||
subscribe(event, callback) {
|
subscribe(event, callback) {
|
||||||
this.on(event, callback);
|
this.on(event, callback);
|
||||||
|
|||||||
@@ -156,12 +156,8 @@ let unsubscribe = null;
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// console.log('[CrmView] Компонент загружен');
|
// console.log('[CrmView] Компонент загружен');
|
||||||
|
|
||||||
// Если пользователь авторизован, загружаем данные
|
// Загружаем DLE для всех пользователей (авторизованных и неавторизованных)
|
||||||
if (auth.isAuthenticated.value) {
|
|
||||||
loadDLEs();
|
loadDLEs();
|
||||||
} else {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подписка на события авторизации
|
// Подписка на события авторизации
|
||||||
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="isLoadingDles" class="loading-dles">
|
<div v-if="isLoadingDles" class="loading-dles">
|
||||||
<p>Загрузка деплоированных DLE...</p>
|
<p>Загрузка деплоированных DLE...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
class="dle-card"
|
class="dle-card"
|
||||||
@click="openDleManagement(dle.dleAddress)"
|
@click="openDleManagement(dle.dleAddress)"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="dle-header">
|
<div class="dle-header">
|
||||||
<div class="dle-title-section">
|
<div class="dle-title-section">
|
||||||
<img
|
<img
|
||||||
@@ -99,18 +101,18 @@
|
|||||||
<strong>Адреса контрактов:</strong>
|
<strong>Адреса контрактов:</strong>
|
||||||
<div class="addresses-list">
|
<div class="addresses-list">
|
||||||
<div
|
<div
|
||||||
v-for="network in dle.deployedNetworks || [{ chainId: 11155111, address: dle.dleAddress }]"
|
v-for="chainId in (dle.supportedChainIds || [11155111])"
|
||||||
:key="network.chainId"
|
:key="chainId"
|
||||||
class="address-item"
|
class="address-item"
|
||||||
>
|
>
|
||||||
<span class="chain-name">{{ getChainName(network.chainId) }}:</span>
|
<span class="chain-name">{{ getChainName(chainId) }}:</span>
|
||||||
<a
|
<a
|
||||||
:href="getExplorerUrl(network.chainId, network.address)"
|
:href="getExplorerUrl(chainId, dle.dleAddress)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="address-link"
|
class="address-link"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
{{ shortenAddress(network.address) }}
|
{{ shortenAddress(dle.dleAddress) }}
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,14 +255,22 @@ async function loadDeployedDles() {
|
|||||||
const dlesWithBlockchainData = await Promise.all(
|
const dlesWithBlockchainData = await Promise.all(
|
||||||
dlesFromApi.map(async (dle) => {
|
dlesFromApi.map(async (dle) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[ManagementView] Читаем данные из блокчейна для ${dle.dleAddress}`);
|
// Используем адрес из deployedNetworks если dleAddress null
|
||||||
|
const dleAddress = dle.dleAddress || (dle.deployedNetworks && dle.deployedNetworks.length > 0 ? dle.deployedNetworks[0].address : null);
|
||||||
|
|
||||||
|
if (!dleAddress) {
|
||||||
|
console.warn(`[ManagementView] Нет адреса для DLE ${dle.deployment_id || 'unknown'}`);
|
||||||
|
return dle;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ManagementView] Читаем данные из блокчейна для ${dleAddress}`);
|
||||||
|
|
||||||
// Читаем данные из блокчейна
|
// Читаем данные из блокчейна
|
||||||
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
||||||
dleAddress: dle.dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[ManagementView] Ответ от блокчейна для ${dle.dleAddress}:`, blockchainResponse.data);
|
console.log(`[ManagementView] Ответ от блокчейна для ${dleAddress}:`, blockchainResponse.data);
|
||||||
|
|
||||||
if (blockchainResponse.data.success) {
|
if (blockchainResponse.data.success) {
|
||||||
const blockchainData = blockchainResponse.data.data;
|
const blockchainData = blockchainResponse.data.data;
|
||||||
@@ -376,7 +386,15 @@ function formatTokenAmount(amount) {
|
|||||||
const num = parseFloat(amount);
|
const num = parseFloat(amount);
|
||||||
if (num === 0) return '0';
|
if (num === 0) return '0';
|
||||||
|
|
||||||
// Всегда показываем полное число с разделителями тысяч
|
// Для очень маленьких чисел показываем с большей точностью
|
||||||
|
if (num < 1) {
|
||||||
|
return num.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 18
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для больших чисел показываем с разделителями тысяч
|
||||||
return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
|
return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,6 +830,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Адаптивность */
|
/* Адаптивность */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dle-title-section {
|
.dle-title-section {
|
||||||
|
|||||||
@@ -84,8 +84,11 @@
|
|||||||
v-model.number="newToken.minBalance"
|
v-model.number="newToken.minBalance"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
>
|
>
|
||||||
|
<small class="form-text">Минимальный баланс токена для получения доступа</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Настройки прав доступа -->
|
<!-- Настройки прав доступа -->
|
||||||
@@ -158,6 +161,12 @@ async function addToken() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация порогов доступа
|
||||||
|
if (newToken.readonlyThreshold >= newToken.editorThreshold) {
|
||||||
|
alert('Минимум токенов для Read-Only доступа должен быть меньше минимума для Editor доступа');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
name: newToken.name,
|
name: newToken.name,
|
||||||
address: newToken.address,
|
address: newToken.address,
|
||||||
|
|||||||
@@ -66,34 +66,68 @@ const emailAuth = {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.webssh-settings-block {
|
.webssh-settings-block {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
padding: 2rem;
|
||||||
margin-top: 20px;
|
margin: 2rem auto;
|
||||||
margin-bottom: 20px;
|
max-width: 1000px;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 1.5rem;
|
||||||
right: 18px;
|
right: 1.5rem;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #bbb;
|
color: #666;
|
||||||
transition: color 0.2s;
|
padding: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn:hover {
|
.close-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 0.5rem;
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-right: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.webssh-settings-block {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
1413
frontend/src/views/smartcontracts/AddModuleFormView.vue
Normal file
1413
frontend/src/views/smartcontracts/AddModuleFormView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,13 +30,6 @@
|
|||||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||||
</div>
|
</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 v-if="!props.isAuthenticated" class="auth-notice">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
@@ -48,73 +41,18 @@
|
|||||||
|
|
||||||
<!-- Блоки операций -->
|
<!-- Блоки операций -->
|
||||||
<div class="operations-grid">
|
<div class="operations-grid">
|
||||||
<!-- Управление токенами -->
|
<!-- Основные операции DLE -->
|
||||||
<div class="operation-category">
|
<div class="operation-category">
|
||||||
<h5>💸 Управление токенами</h5>
|
<h5>Основные операции DLE</h5>
|
||||||
<div class="operation-blocks">
|
<div class="operation-blocks">
|
||||||
<div class="operation-block">
|
<div class="operation-block">
|
||||||
<div class="operation-icon">💸</div>
|
|
||||||
<h6>Передача токенов</h6>
|
<h6>Передача токенов</h6>
|
||||||
<p>Перевод токенов DLE другому адресу через governance</p>
|
<p>Перевод токенов DLE другому адресу через governance</p>
|
||||||
<button class="create-btn" @click="openTransferForm" :disabled="!props.isAuthenticated">
|
<button class="create-btn" @click="openTransferForm" :disabled="!props.isAuthenticated">
|
||||||
Создать
|
Создать
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Управление модулями -->
|
|
||||||
<div class="operation-category">
|
|
||||||
<h5>🔧 Управление модулями</h5>
|
|
||||||
<div class="operation-blocks">
|
|
||||||
<div class="operation-block">
|
<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>
|
<h6>Обновить данные DLE</h6>
|
||||||
<p>Изменение основной информации о DLE (название, символ, адрес и т.д.)</p>
|
<p>Изменение основной информации о DLE (название, символ, адрес и т.д.)</p>
|
||||||
<button class="create-btn" @click="openUpdateDLEInfoForm" :disabled="!props.isAuthenticated">
|
<button class="create-btn" @click="openUpdateDLEInfoForm" :disabled="!props.isAuthenticated">
|
||||||
@@ -122,7 +60,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="operation-block">
|
<div class="operation-block">
|
||||||
<div class="operation-icon">📊</div>
|
|
||||||
<h6>Изменить кворум</h6>
|
<h6>Изменить кворум</h6>
|
||||||
<p>Изменение процента голосов, необходимого для принятия решений</p>
|
<p>Изменение процента голосов, необходимого для принятия решений</p>
|
||||||
<button class="create-btn" @click="openUpdateQuorumForm" :disabled="!props.isAuthenticated">
|
<button class="create-btn" @click="openUpdateQuorumForm" :disabled="!props.isAuthenticated">
|
||||||
@@ -130,7 +67,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="operation-block">
|
<div class="operation-block">
|
||||||
<div class="operation-icon">⏰</div>
|
|
||||||
<h6>Изменить время голосования</h6>
|
<h6>Изменить время голосования</h6>
|
||||||
<p>Настройка минимального и максимального времени голосования</p>
|
<p>Настройка минимального и максимального времени голосования</p>
|
||||||
<button class="create-btn" @click="openUpdateVotingDurationsForm" :disabled="!props.isAuthenticated">
|
<button class="create-btn" @click="openUpdateVotingDurationsForm" :disabled="!props.isAuthenticated">
|
||||||
@@ -138,7 +74,41 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="operation-block">
|
<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 class="operation-block">
|
||||||
|
<h6>Добавить модуль</h6>
|
||||||
|
<p>Добавление нового модуля в DLE контракт</p>
|
||||||
|
<button class="create-btn" @click="openAddModuleForm" :disabled="!props.isAuthenticated">
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="operation-block">
|
||||||
|
<h6>Удалить модуль</h6>
|
||||||
|
<p>Удаление существующего модуля из DLE контракта</p>
|
||||||
|
<button class="create-btn" @click="openRemoveModuleForm" :disabled="!props.isAuthenticated">
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="operation-block">
|
||||||
|
<h6>Добавить сеть</h6>
|
||||||
|
<p>Добавление новой поддерживаемой блокчейн сети</p>
|
||||||
|
<button class="create-btn" @click="openAddChainForm" :disabled="!props.isAuthenticated">
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="operation-block">
|
||||||
|
<h6>Удалить сеть</h6>
|
||||||
|
<p>Удаление поддерживаемой блокчейн сети</p>
|
||||||
|
<button class="create-btn" @click="openRemoveChainForm" :disabled="!props.isAuthenticated">
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="operation-block">
|
||||||
<h6>Изменить логотип</h6>
|
<h6>Изменить логотип</h6>
|
||||||
<p>Обновление URI логотипа DLE для отображения в блокчейн-сканерах</p>
|
<p>Обновление URI логотипа DLE для отображения в блокчейн-сканерах</p>
|
||||||
<button class="create-btn" @click="openSetLogoURIForm" :disabled="!props.isAuthenticated">
|
<button class="create-btn" @click="openSetLogoURIForm" :disabled="!props.isAuthenticated">
|
||||||
@@ -166,10 +136,8 @@
|
|||||||
:key="operation.id"
|
:key="operation.id"
|
||||||
class="operation-block module-operation-block"
|
class="operation-block module-operation-block"
|
||||||
>
|
>
|
||||||
<div class="operation-icon">{{ operation.icon }}</div>
|
|
||||||
<h6>{{ operation.name }}</h6>
|
<h6>{{ operation.name }}</h6>
|
||||||
<p>{{ operation.description }}</p>
|
<p>{{ operation.description }}</p>
|
||||||
<div class="operation-category-tag">{{ operation.category }}</div>
|
|
||||||
<button
|
<button
|
||||||
class="create-btn"
|
class="create-btn"
|
||||||
@click="openModuleOperationForm(moduleOperation.moduleType, operation)"
|
@click="openModuleOperationForm(moduleOperation.moduleType, operation)"
|
||||||
@@ -182,21 +150,6 @@
|
|||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -259,7 +212,6 @@ const availableChains = ref([]);
|
|||||||
// Состояние модулей и их операций
|
// Состояние модулей и их операций
|
||||||
const moduleOperations = ref([]);
|
const moduleOperations = ref([]);
|
||||||
const isLoadingModuleOperations = ref(false);
|
const isLoadingModuleOperations = ref(false);
|
||||||
const modulesWebSocket = ref(null);
|
|
||||||
const isModulesWSConnected = ref(false);
|
const isModulesWSConnected = ref(false);
|
||||||
|
|
||||||
// Функции для открытия отдельных форм операций
|
// Функции для открытия отдельных форм операций
|
||||||
@@ -269,8 +221,11 @@ function openTransferForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openAddModuleForm() {
|
function openAddModuleForm() {
|
||||||
// TODO: Открыть форму для добавления модуля
|
if (dleAddress.value) {
|
||||||
alert('Форма добавления модуля будет реализована');
|
router.push(`/management/add-module?address=${dleAddress.value}`);
|
||||||
|
} else {
|
||||||
|
router.push('/management/add-module');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRemoveModuleForm() {
|
function openRemoveModuleForm() {
|
||||||
@@ -325,13 +280,7 @@ function openModuleOperationForm(moduleType, operation) {
|
|||||||
|
|
||||||
// Получить иконку для типа модуля
|
// Получить иконку для типа модуля
|
||||||
function getModuleIcon(moduleType) {
|
function getModuleIcon(moduleType) {
|
||||||
const icons = {
|
return '';
|
||||||
treasury: '💰',
|
|
||||||
timelock: '⏰',
|
|
||||||
reader: '📖',
|
|
||||||
hierarchicalVoting: '🗳️'
|
|
||||||
};
|
|
||||||
return icons[moduleType] || '🔧';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции
|
// Функции
|
||||||
@@ -364,6 +313,9 @@ async function loadDleData() {
|
|||||||
// Загружаем операции модулей
|
// Загружаем операции модулей
|
||||||
await loadModuleOperations();
|
await loadModuleOperations();
|
||||||
|
|
||||||
|
// Повторно подписываемся на обновления модулей для нового DLE
|
||||||
|
resubscribeToModules();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -401,49 +353,61 @@ async function loadModuleOperations() {
|
|||||||
|
|
||||||
// WebSocket функции для модулей
|
// WebSocket функции для модулей
|
||||||
function connectModulesWebSocket() {
|
function connectModulesWebSocket() {
|
||||||
if (modulesWebSocket.value && modulesWebSocket.value.readyState === WebSocket.OPEN) {
|
if (isModulesWSConnected.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsUrl = `ws://localhost:8000/ws/deployment`;
|
try {
|
||||||
modulesWebSocket.value = new WebSocket(wsUrl);
|
// Подключаемся через существующий WebSocket клиент
|
||||||
|
wsClient.connect();
|
||||||
|
|
||||||
modulesWebSocket.value.onopen = () => {
|
// Подписываемся на события deployment_update
|
||||||
console.log('[CreateProposalView] WebSocket модулей соединение установлено');
|
wsClient.on('deployment_update', (data) => {
|
||||||
isModulesWSConnected.value = true;
|
console.log('[CreateProposalView] Получено обновление деплоя:', data);
|
||||||
|
handleModulesWebSocketMessage(data);
|
||||||
|
});
|
||||||
|
|
||||||
// Подписываемся на обновления модулей для текущего DLE
|
// Подписываемся на подтверждение подписки
|
||||||
|
wsClient.on('subscribed', (data) => {
|
||||||
|
console.log('[CreateProposalView] Подписка подтверждена:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подписываемся на обновления модулей
|
||||||
|
wsClient.on('modules_updated', (data) => {
|
||||||
|
console.log('[CreateProposalView] Модули обновлены:', data);
|
||||||
|
// Перезагружаем операции модулей при обновлении
|
||||||
|
loadModuleOperations();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подписываемся на статус деплоя
|
||||||
|
wsClient.on('deployment_status', (data) => {
|
||||||
|
console.log('[CreateProposalView] Статус деплоя:', data);
|
||||||
|
handleModulesWebSocketMessage(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подписываемся на событие подключения
|
||||||
|
wsClient.on('connected', () => {
|
||||||
|
console.log('[CreateProposalView] WebSocket подключен, подписываемся на модули');
|
||||||
if (dleAddress.value) {
|
if (dleAddress.value) {
|
||||||
modulesWebSocket.value.send(JSON.stringify({
|
wsClient.ws.send(JSON.stringify({
|
||||||
type: 'subscribe',
|
type: 'subscribe',
|
||||||
dleAddress: dleAddress.value
|
dleAddress: dleAddress.value
|
||||||
}));
|
}));
|
||||||
|
console.log('[CreateProposalView] Подписка на модули отправлена для DLE:', dleAddress.value);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
modulesWebSocket.value.onmessage = (event) => {
|
isModulesWSConnected.value = true;
|
||||||
try {
|
console.log('[CreateProposalView] WebSocket модулей соединение установлено');
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
handleModulesWebSocketMessage(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[CreateProposalView] Ошибка парсинга WebSocket сообщения модулей:', error);
|
console.error('[CreateProposalView] Ошибка подключения WebSocket модулей:', error);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
modulesWebSocket.value.onclose = () => {
|
|
||||||
console.log('[CreateProposalView] WebSocket модулей соединение закрыто');
|
|
||||||
isModulesWSConnected.value = false;
|
isModulesWSConnected.value = false;
|
||||||
|
|
||||||
// Переподключаемся через 5 секунд
|
// Переподключаемся через 5 секунд
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connectModulesWebSocket();
|
connectModulesWebSocket();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
}
|
||||||
|
|
||||||
modulesWebSocket.value.onerror = (error) => {
|
|
||||||
console.error('[CreateProposalView] Ошибка WebSocket модулей:', error);
|
|
||||||
isModulesWSConnected.value = false;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModulesWebSocketMessage(data) {
|
function handleModulesWebSocketMessage(data) {
|
||||||
@@ -471,10 +435,30 @@ function handleModulesWebSocketMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disconnectModulesWebSocket() {
|
function disconnectModulesWebSocket() {
|
||||||
if (modulesWebSocket.value) {
|
if (isModulesWSConnected.value) {
|
||||||
modulesWebSocket.value.close();
|
// Отписываемся от всех событий
|
||||||
modulesWebSocket.value = null;
|
wsClient.off('deployment_update');
|
||||||
|
wsClient.off('subscribed');
|
||||||
|
wsClient.off('modules_updated');
|
||||||
|
wsClient.off('deployment_status');
|
||||||
|
wsClient.off('connected');
|
||||||
|
|
||||||
isModulesWSConnected.value = false;
|
isModulesWSConnected.value = false;
|
||||||
|
console.log('[CreateProposalView] WebSocket модулей отключен');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для повторной подписки при изменении DLE адреса
|
||||||
|
function resubscribeToModules() {
|
||||||
|
if (isModulesWSConnected.value && wsClient.ws && wsClient.ws.readyState === WebSocket.OPEN && dleAddress.value) {
|
||||||
|
wsClient.ws.send(JSON.stringify({
|
||||||
|
type: 'subscribe',
|
||||||
|
dleAddress: dleAddress.value
|
||||||
|
}));
|
||||||
|
console.log('[CreateProposalView] Повторная подписка на модули для DLE:', dleAddress.value);
|
||||||
|
} else if (wsClient.ws && wsClient.ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
// Если соединение еще устанавливается, ждем события подключения
|
||||||
|
console.log('[CreateProposalView] WebSocket еще подключается, ждем события connected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,32 +542,6 @@ onUnmounted(() => {
|
|||||||
color: #333;
|
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 {
|
.auth-notice {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@@ -626,110 +584,76 @@ onUnmounted(() => {
|
|||||||
.operation-category h5 {
|
.operation-category h5 {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
margin: 0 0 1.5rem 0;
|
margin: 0 0 1.5rem 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
border-bottom: 2px solid #f0f0f0;
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-blocks {
|
.operation-blocks {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-block {
|
.operation-block {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
background: white;
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.5rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
text-align: center;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
.operation-block::before {
|
min-height: 200px;
|
||||||
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 {
|
.operation-block:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
border-color: var(--color-primary);
|
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 {
|
.operation-block h6 {
|
||||||
color: #333;
|
margin: 0 0 1rem 0;
|
||||||
margin: 0 0 0.75rem 0;
|
color: var(--color-primary);
|
||||||
font-size: 1.1rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-block p {
|
.operation-block p {
|
||||||
color: #666;
|
|
||||||
margin: 0 0 1.5rem 0;
|
margin: 0 0 1.5rem 0;
|
||||||
font-size: 0.9rem;
|
color: #666;
|
||||||
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-btn {
|
.create-btn {
|
||||||
background: linear-gradient(135deg, var(--color-primary), #20c997);
|
background: var(--color-primary);
|
||||||
color: white;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
transition: all 0.2s;
|
||||||
transition: all 0.3s ease;
|
min-width: 120px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
margin-top: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
.create-btn:hover {
|
||||||
background: linear-gradient(135deg, #0056b3, #1ea085);
|
background: var(--color-primary-dark);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-btn:hover::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:disabled {
|
.create-btn:disabled {
|
||||||
background: #6c757d;
|
background: #6c757d;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -737,10 +661,6 @@ onUnmounted(() => {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-btn:disabled::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для модулей */
|
/* Стили для модулей */
|
||||||
.module-description {
|
.module-description {
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -751,54 +671,13 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.module-operation-block {
|
.module-operation-block {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
display: flex;
|
||||||
border: 2px solid #e9ecef;
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-operation-block::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
background: linear-gradient(90deg, #28a745, #20c997);
|
|
||||||
transform: scaleX(0);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-operation-block:hover::before {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-category-tag {
|
|
||||||
display: inline-block;
|
|
||||||
background: linear-gradient(135deg, #28a745, #20c997);
|
|
||||||
color: white;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Анимация появления модулей */
|
|
||||||
.operation-category {
|
|
||||||
animation: fadeInUp 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Индикатор загрузки модулей */
|
/* Индикатор загрузки модулей */
|
||||||
.loading-modules {
|
.loading-modules {
|
||||||
@@ -828,10 +707,6 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Адаптивность */
|
/* Адаптивность */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.operations-blocks {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-blocks {
|
.operation-blocks {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -840,14 +715,6 @@ onUnmounted(() => {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-icon {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocks-header h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-category h5 {
|
.operation-category h5 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,42 +32,30 @@
|
|||||||
|
|
||||||
<!-- Блоки управления -->
|
<!-- Блоки управления -->
|
||||||
<div class="management-blocks">
|
<div class="management-blocks">
|
||||||
<!-- Первый ряд -->
|
<!-- Столбец 1 -->
|
||||||
<div class="blocks-row">
|
<div class="blocks-column">
|
||||||
<div class="management-block create-proposal-block">
|
<div class="management-block">
|
||||||
<h3>Создать предложение</h3>
|
<h3>Создать предложение</h3>
|
||||||
<p>Универсальная форма для создания новых предложений</p>
|
<p>Универсальная форма для создания новых предложений</p>
|
||||||
<button class="details-btn create-btn" @click="openCreateProposal">
|
<button class="details-btn" @click="openCreateProposal">
|
||||||
Подробнее
|
Подробнее
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="management-block">
|
|
||||||
<h3>Предложения</h3>
|
|
||||||
<p>Создание, подписание, выполнение</p>
|
|
||||||
<button class="details-btn" @click="openProposals">Подробнее</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="management-block">
|
|
||||||
<h3>Токены DLE</h3>
|
|
||||||
<p>Балансы, трансферы, распределение</p>
|
|
||||||
<button class="details-btn" @click="openTokens">Подробнее</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Второй ряд -->
|
|
||||||
<div class="blocks-row">
|
|
||||||
<div class="management-block">
|
|
||||||
<h3>Кворум</h3>
|
|
||||||
<p>Настройки голосования</p>
|
|
||||||
<button class="details-btn" @click="openQuorum">Подробнее</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="management-block">
|
<div class="management-block">
|
||||||
<h3>Модули DLE</h3>
|
<h3>Модули DLE</h3>
|
||||||
<p>Установка, настройка, управление</p>
|
<p>Установка, настройка, управление</p>
|
||||||
<button class="details-btn" @click="openModules">Подробнее</button>
|
<button class="details-btn" @click="openModules">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Столбец 2 -->
|
||||||
|
<div class="blocks-column">
|
||||||
|
<div class="management-block">
|
||||||
|
<h3>Предложения</h3>
|
||||||
|
<p>Создание, подписание, выполнение</p>
|
||||||
|
<button class="details-btn" @click="openProposals">Подробнее</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="management-block">
|
<div class="management-block">
|
||||||
<h3>Аналитика</h3>
|
<h3>Аналитика</h3>
|
||||||
@@ -76,8 +64,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Третий ряд -->
|
<!-- Столбец 3 -->
|
||||||
<div class="blocks-row">
|
<div class="blocks-column">
|
||||||
<div class="management-block">
|
<div class="management-block">
|
||||||
<h3>История</h3>
|
<h3>История</h3>
|
||||||
<p>Лог операций, события, транзакции</p>
|
<p>Лог операций, события, транзакции</p>
|
||||||
@@ -125,21 +113,6 @@ const openProposals = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openTokens = () => {
|
|
||||||
if (dleAddress.value) {
|
|
||||||
router.push(`/management/tokens?address=${dleAddress.value}`);
|
|
||||||
} else {
|
|
||||||
router.push('/management/tokens');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openQuorum = () => {
|
|
||||||
if (dleAddress.value) {
|
|
||||||
router.push(`/management/quorum?address=${dleAddress.value}`);
|
|
||||||
} else {
|
|
||||||
router.push('/management/quorum');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModules = () => {
|
const openModules = () => {
|
||||||
if (dleAddress.value) {
|
if (dleAddress.value) {
|
||||||
@@ -236,15 +209,16 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.management-blocks {
|
.management-blocks {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocks-row {
|
.blocks-column {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.management-block {
|
.management-block {
|
||||||
@@ -255,6 +229,10 @@ onMounted(() => {
|
|||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.management-block:hover {
|
.management-block:hover {
|
||||||
@@ -268,6 +246,7 @@ onMounted(() => {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.management-block p {
|
.management-block p {
|
||||||
@@ -275,6 +254,7 @@ onMounted(() => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-btn {
|
.details-btn {
|
||||||
@@ -288,6 +268,8 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-btn:hover {
|
.details-btn:hover {
|
||||||
@@ -295,35 +277,16 @@ 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: 1024px) {
|
||||||
|
.management-blocks {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.blocks-row {
|
.management-blocks {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,598 +0,0 @@
|
|||||||
<!--
|
|
||||||
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="quorum-container">
|
|
||||||
<!-- Заголовок -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>Кворум</h1>
|
|
||||||
<p>Настройки голосования и кворума</p>
|
|
||||||
</div>
|
|
||||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Текущие настройки -->
|
|
||||||
<div class="current-settings-section">
|
|
||||||
<h2>Текущие настройки</h2>
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="setting-card">
|
|
||||||
<h3>Процент кворума</h3>
|
|
||||||
<p class="setting-value">{{ currentQuorum }}%</p>
|
|
||||||
<p class="setting-description">Минимальный процент токенов для принятия решения</p>
|
|
||||||
</div>
|
|
||||||
<div class="setting-card">
|
|
||||||
<h3>Задержка голосования</h3>
|
|
||||||
<p class="setting-value">{{ votingDelay }} блоков</p>
|
|
||||||
<p class="setting-description">Время между созданием и началом голосования</p>
|
|
||||||
</div>
|
|
||||||
<div class="setting-card">
|
|
||||||
<h3>Период голосования</h3>
|
|
||||||
<p class="setting-value">{{ votingPeriod }} блоков</p>
|
|
||||||
<p class="setting-description">Длительность периода голосования</p>
|
|
||||||
</div>
|
|
||||||
<div class="setting-card">
|
|
||||||
<h3>Порог предложений</h3>
|
|
||||||
<p class="setting-value">{{ proposalThreshold }} токенов</p>
|
|
||||||
<p class="setting-description">Минимальное количество токенов для создания предложения</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Изменение настроек -->
|
|
||||||
<div class="change-settings-section">
|
|
||||||
<h2>Изменить настройки</h2>
|
|
||||||
<form @submit.prevent="updateSettings" class="settings-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newQuorum">Новый процент кворума:</label>
|
|
||||||
<input
|
|
||||||
id="newQuorum"
|
|
||||||
v-model="newSettings.quorumPercentage"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
placeholder="51"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">% (от 1 до 100)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newVotingDelay">Новая задержка голосования:</label>
|
|
||||||
<input
|
|
||||||
id="newVotingDelay"
|
|
||||||
v-model="newSettings.votingDelay"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="1"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">блоков</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newVotingPeriod">Новый период голосования:</label>
|
|
||||||
<input
|
|
||||||
id="newVotingPeriod"
|
|
||||||
v-model="newSettings.votingPeriod"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="45818"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">блоков (~1 неделя)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newProposalThreshold">Новый порог предложений:</label>
|
|
||||||
<input
|
|
||||||
id="newProposalThreshold"
|
|
||||||
v-model="newSettings.proposalThreshold"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="100"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">токенов</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="changeReason">Причина изменения:</label>
|
|
||||||
<textarea
|
|
||||||
id="changeReason"
|
|
||||||
v-model="newSettings.reason"
|
|
||||||
placeholder="Опишите причину изменения настроек..."
|
|
||||||
rows="4"
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isUpdating">
|
|
||||||
{{ isUpdating ? 'Обновление...' : 'Обновить настройки' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- История изменений -->
|
|
||||||
<div class="history-section">
|
|
||||||
<h2>История изменений</h2>
|
|
||||||
<div v-if="settingsHistory.length === 0" class="empty-state">
|
|
||||||
<p>Нет истории изменений настроек</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="history-list">
|
|
||||||
<div
|
|
||||||
v-for="change in settingsHistory"
|
|
||||||
:key="change.id"
|
|
||||||
class="history-item"
|
|
||||||
>
|
|
||||||
<div class="history-header">
|
|
||||||
<h3>Изменение #{{ change.id }}</h3>
|
|
||||||
<span class="change-date">{{ formatDate(change.timestamp) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="change-details">
|
|
||||||
<p><strong>Причина:</strong> {{ change.reason }}</p>
|
|
||||||
<div class="changes-list">
|
|
||||||
<div v-if="change.quorumChange" class="change-item">
|
|
||||||
<span class="change-label">Кворум:</span>
|
|
||||||
<span class="change-value">{{ change.quorumChange.from }}% → {{ change.quorumChange.to }}%</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="change.votingDelayChange" class="change-item">
|
|
||||||
<span class="change-label">Задержка голосования:</span>
|
|
||||||
<span class="change-value">{{ change.votingDelayChange.from }} → {{ change.votingDelayChange.to }} блоков</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="change.votingPeriodChange" class="change-item">
|
|
||||||
<span class="change-label">Период голосования:</span>
|
|
||||||
<span class="change-value">{{ change.votingPeriodChange.from }} → {{ change.votingPeriodChange.to }} блоков</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="change.proposalThresholdChange" class="change-item">
|
|
||||||
<span class="change-label">Порог предложений:</span>
|
|
||||||
<span class="change-value">{{ change.proposalThresholdChange.from }} → {{ change.proposalThresholdChange.to }} токенов</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="change-author">
|
|
||||||
<span>Автор: {{ formatAddress(change.author) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, defineProps, defineEmits } from 'vue';
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
|
||||||
import { getGovernanceParams } from '../../services/dleV2Service.js';
|
|
||||||
import { getQuorumAt, getVotingPowerAt } from '../../services/proposalsService.js';
|
|
||||||
|
|
||||||
// Определяем props
|
|
||||||
const props = defineProps({
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем emits
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
// Получаем адрес DLE из URL
|
|
||||||
const dleAddress = computed(() => {
|
|
||||||
return route.query.address;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция возврата к блокам управления
|
|
||||||
const goBackToBlocks = () => {
|
|
||||||
if (dleAddress.value) {
|
|
||||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
|
||||||
} else {
|
|
||||||
router.push('/management');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Состояние
|
|
||||||
const isUpdating = ref(false);
|
|
||||||
|
|
||||||
// Текущие настройки
|
|
||||||
const currentQuorum = ref(51);
|
|
||||||
const votingDelay = ref(1);
|
|
||||||
const votingPeriod = ref(45818);
|
|
||||||
const proposalThreshold = ref(100);
|
|
||||||
|
|
||||||
// Новые настройки
|
|
||||||
const newSettings = ref({
|
|
||||||
quorumPercentage: '',
|
|
||||||
votingDelay: '',
|
|
||||||
votingPeriod: '',
|
|
||||||
proposalThreshold: '',
|
|
||||||
reason: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// История изменений (загружается из блокчейна)
|
|
||||||
const settingsHistory = ref([]);
|
|
||||||
|
|
||||||
// Методы
|
|
||||||
const updateSettings = async () => {
|
|
||||||
if (isUpdating.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isUpdating.value = true;
|
|
||||||
|
|
||||||
// Здесь будет логика обновления настроек в смарт-контракте
|
|
||||||
// console.log('Обновление настроек:', newSettings.value);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
const change = {
|
|
||||||
id: settingsHistory.value.length + 1,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
reason: newSettings.value.reason,
|
|
||||||
author: '0x' + Math.random().toString(16).substr(2, 40)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Добавляем изменения в историю
|
|
||||||
if (newSettings.value.quorumPercentage && newSettings.value.quorumPercentage !== currentQuorum.value) {
|
|
||||||
change.quorumChange = { from: currentQuorum.value, to: parseInt(newSettings.value.quorumPercentage) };
|
|
||||||
currentQuorum.value = parseInt(newSettings.value.quorumPercentage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSettings.value.votingDelay && newSettings.value.votingDelay !== votingDelay.value) {
|
|
||||||
change.votingDelayChange = { from: votingDelay.value, to: parseInt(newSettings.value.votingDelay) };
|
|
||||||
votingDelay.value = parseInt(newSettings.value.votingDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSettings.value.votingPeriod && newSettings.value.votingPeriod !== votingPeriod.value) {
|
|
||||||
change.votingPeriodChange = { from: votingPeriod.value, to: parseInt(newSettings.value.votingPeriod) };
|
|
||||||
votingPeriod.value = parseInt(newSettings.value.votingPeriod);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSettings.value.proposalThreshold && newSettings.value.proposalThreshold !== proposalThreshold.value) {
|
|
||||||
change.proposalThresholdChange = { from: proposalThreshold.value, to: parseInt(newSettings.value.proposalThreshold) };
|
|
||||||
proposalThreshold.value = parseInt(newSettings.value.proposalThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsHistory.value.unshift(change);
|
|
||||||
|
|
||||||
// Сброс формы
|
|
||||||
newSettings.value = {
|
|
||||||
quorumPercentage: '',
|
|
||||||
votingDelay: '',
|
|
||||||
votingPeriod: '',
|
|
||||||
proposalThreshold: '',
|
|
||||||
reason: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
alert('Настройки успешно обновлены!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка обновления настроек:', error);
|
|
||||||
alert('Ошибка при обновлении настроек');
|
|
||||||
} finally {
|
|
||||||
isUpdating.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (timestamp) => {
|
|
||||||
return new Date(timestamp).toLocaleString('ru-RU');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.quorum-container {
|
|
||||||
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: flex-start;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 2px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h1 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header p {
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-size: 1.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Секции */
|
|
||||||
.current-settings-section,
|
|
||||||
.change-settings-section,
|
|
||||||
.history-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-settings-section h2,
|
|
||||||
.change-settings-section h2,
|
|
||||||
.history-section h2 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Текущие настройки */
|
|
||||||
.settings-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-card {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border-left: 4px solid var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-card h3 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-value {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-description {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Форма настроек */
|
|
||||||
.settings-form {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group textarea {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-hint {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* История изменений */
|
|
||||||
.history-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-item {
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-item:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-date {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-details {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-details p {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.changes-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-value {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-author {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Состояния */
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 2px dashed #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.form-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,740 +0,0 @@
|
|||||||
<!--
|
|
||||||
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="tokens-container">
|
|
||||||
<!-- Заголовок -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>Управление токенами DLE</h1>
|
|
||||||
<p>Создание предложений для перевода токенов через систему голосования</p>
|
|
||||||
<div v-if="selectedDle" class="dle-info">
|
|
||||||
<span class="dle-name">{{ selectedDle.name }} ({{ selectedDle.symbol }})</span>
|
|
||||||
<span class="dle-address">{{ shortenAddress(selectedDle.dleAddress) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="isLoadingDle" class="loading-info">
|
|
||||||
<span>Загрузка данных DLE...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="no-dle-info">
|
|
||||||
<span>DLE не выбран</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Информация о токенах -->
|
|
||||||
<div class="token-info-section">
|
|
||||||
<h2>Информация о токенах</h2>
|
|
||||||
<div class="token-info-grid">
|
|
||||||
<div class="info-card">
|
|
||||||
<h3>Общий запас</h3>
|
|
||||||
<p class="token-amount">{{ totalSupply }} {{ tokenSymbol }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="info-card">
|
|
||||||
<h3>Ваш баланс</h3>
|
|
||||||
<p class="token-amount">{{ userBalance }} {{ tokenSymbol }}</p>
|
|
||||||
<p v-if="currentUserAddress" class="user-address">{{ shortenAddress(currentUserAddress) }}</p>
|
|
||||||
<p v-else class="no-wallet">Кошелек не подключен</p>
|
|
||||||
</div>
|
|
||||||
<div class="info-card">
|
|
||||||
<h3>Цена токена</h3>
|
|
||||||
<p class="token-amount">${{ tokenPrice }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Перевод токенов через governance -->
|
|
||||||
<div class="transfer-section">
|
|
||||||
<h2>Перевод токенов через Governance</h2>
|
|
||||||
<p class="section-description">
|
|
||||||
Создайте предложение для перевода токенов через систему голосования.
|
|
||||||
Токены будут переведены от имени DLE после одобрения кворумом.
|
|
||||||
<strong>Важно:</strong> Перевод через governance будет выполнен во всех поддерживаемых сетях DLE.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form @submit.prevent="createTransferProposal" class="transfer-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proposal-recipient">Адрес получателя:</label>
|
|
||||||
<input
|
|
||||||
id="proposal-recipient"
|
|
||||||
v-model="proposalData.recipient"
|
|
||||||
type="text"
|
|
||||||
placeholder="0x..."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proposal-amount">Количество токенов:</label>
|
|
||||||
<input
|
|
||||||
id="proposal-amount"
|
|
||||||
v-model="proposalData.amount"
|
|
||||||
type="number"
|
|
||||||
step="0.000001"
|
|
||||||
placeholder="0.0"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proposal-description">Описание предложения:</label>
|
|
||||||
<textarea
|
|
||||||
id="proposal-description"
|
|
||||||
v-model="proposalData.description"
|
|
||||||
placeholder="Опишите причину перевода токенов..."
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proposal-duration">Длительность голосования (часы):</label>
|
|
||||||
<input
|
|
||||||
id="proposal-duration"
|
|
||||||
v-model="proposalData.duration"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="168"
|
|
||||||
placeholder="24"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isCreatingProposal">
|
|
||||||
{{ isCreatingProposal ? 'Создание предложения...' : 'Создать предложение' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Статус предложения -->
|
|
||||||
<div v-if="proposalStatus" class="proposal-status">
|
|
||||||
<p class="status-message">{{ proposalStatus }}</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Держатели токенов -->
|
|
||||||
<div class="holders-section">
|
|
||||||
<h2>Держатели токенов</h2>
|
|
||||||
<div v-if="tokenHolders.length === 0" class="empty-state">
|
|
||||||
<p>Нет данных о держателях токенов</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="holders-list">
|
|
||||||
<div
|
|
||||||
v-for="holder in tokenHolders"
|
|
||||||
:key="holder.address"
|
|
||||||
class="holder-item"
|
|
||||||
>
|
|
||||||
<div class="holder-info">
|
|
||||||
<span class="holder-address">{{ formatAddress(holder.address) }}</span>
|
|
||||||
<span class="holder-balance">{{ holder.balance }} {{ tokenSymbol }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="holder-percentage">
|
|
||||||
{{ ((holder.balance / totalSupply) * 100).toFixed(2) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, watch, defineProps, defineEmits } from 'vue';
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
|
||||||
import { getTokenBalance, getTotalSupply, getTokenHolders } from '../../services/tokensService.js';
|
|
||||||
import api from '../../api/axios';
|
|
||||||
import { ethers } from 'ethers';
|
|
||||||
import { createTransferTokensProposal } from '../../utils/dle-contract.js';
|
|
||||||
|
|
||||||
// Определяем props
|
|
||||||
const props = defineProps({
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем emits
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
// Получаем адрес DLE из URL
|
|
||||||
const dleAddress = computed(() => {
|
|
||||||
const address = route.query.address;
|
|
||||||
console.log('DLE Address from URL (Tokens):', 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);
|
|
||||||
|
|
||||||
// Состояние для предложения о переводе токенов через governance
|
|
||||||
const isCreatingProposal = ref(false);
|
|
||||||
const proposalStatus = ref('');
|
|
||||||
|
|
||||||
// Данные токенов (загружаются из блокчейна)
|
|
||||||
const totalSupply = ref(0);
|
|
||||||
const userBalance = ref(0);
|
|
||||||
const deployerBalance = ref(0);
|
|
||||||
const quorumPercentage = ref(0);
|
|
||||||
const tokenPrice = ref(0);
|
|
||||||
|
|
||||||
// Данные для формы
|
|
||||||
const proposalData = ref({
|
|
||||||
recipient: '',
|
|
||||||
amount: '',
|
|
||||||
description: '',
|
|
||||||
duration: 86400, // 24 часа по умолчанию
|
|
||||||
governanceChainId: 11155111, // Sepolia по умолчанию
|
|
||||||
targetChains: [11155111] // Sepolia по умолчанию
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получаем адрес текущего пользователя
|
|
||||||
const currentUserAddress = computed(() => {
|
|
||||||
console.log('Проверяем identities:', props.identities);
|
|
||||||
|
|
||||||
// Получаем адрес из props или из window.ethereum
|
|
||||||
if (props.identities && props.identities.length > 0) {
|
|
||||||
const walletIdentity = props.identities.find(id => id.provider === 'wallet');
|
|
||||||
console.log('Найден wallet identity:', walletIdentity);
|
|
||||||
if (walletIdentity) {
|
|
||||||
return walletIdentity.provider_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: пытаемся получить из window.ethereum
|
|
||||||
if (window.ethereum && window.ethereum.selectedAddress) {
|
|
||||||
console.log('Получаем адрес из window.ethereum:', window.ethereum.selectedAddress);
|
|
||||||
return window.ethereum.selectedAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Адрес пользователя не найден');
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Держатели токенов (загружаются из блокчейна)
|
|
||||||
const tokenHolders = ref([]);
|
|
||||||
|
|
||||||
// Функции
|
|
||||||
async function loadDleData() {
|
|
||||||
if (!dleAddress.value) {
|
|
||||||
console.warn('Адрес DLE не указан');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingDle.value = true;
|
|
||||||
try {
|
|
||||||
// Читаем актуальные данные из блокчейна
|
|
||||||
const response = await api.post('/dle-core/read-dle-info', {
|
|
||||||
dleAddress: dleAddress.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
const blockchainData = response.data.data;
|
|
||||||
selectedDle.value = blockchainData;
|
|
||||||
console.log('Загружены данные DLE из блокчейна:', blockchainData);
|
|
||||||
|
|
||||||
// Загружаем баланс текущего пользователя
|
|
||||||
await loadUserBalance();
|
|
||||||
|
|
||||||
// Загружаем держателей токенов (если есть API)
|
|
||||||
await loadTokenHolders();
|
|
||||||
} else {
|
|
||||||
console.warn('Не удалось прочитать данные из блокчейна для', dleAddress.value);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
|
||||||
} finally {
|
|
||||||
isLoadingDle.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Новая функция для загрузки баланса текущего пользователя
|
|
||||||
async function loadUserBalance() {
|
|
||||||
if (!currentUserAddress.value || !dleAddress.value) {
|
|
||||||
userBalance.value = 0;
|
|
||||||
console.log('Не удается загрузить баланс: нет адреса пользователя или DLE');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Загружаем баланс для пользователя:', currentUserAddress.value);
|
|
||||||
|
|
||||||
const response = await api.post('/blockchain/get-token-balance', {
|
|
||||||
dleAddress: dleAddress.value,
|
|
||||||
account: currentUserAddress.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
userBalance.value = parseFloat(response.data.data.balance);
|
|
||||||
console.log('Баланс пользователя загружен:', userBalance.value);
|
|
||||||
} else {
|
|
||||||
console.warn('Не удалось загрузить баланс пользователя:', response.data.error);
|
|
||||||
userBalance.value = 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки баланса пользователя:', error);
|
|
||||||
userBalance.value = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTokenHolders() {
|
|
||||||
try {
|
|
||||||
// Здесь можно добавить загрузку держателей токенов из блокчейна
|
|
||||||
// Пока оставляем пустым
|
|
||||||
tokenHolders.value = [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки держателей токенов:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortenAddress(address) {
|
|
||||||
if (!address) return '';
|
|
||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Методы
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция создания предложения о переводе токенов через governance
|
|
||||||
const createTransferProposal = async () => {
|
|
||||||
if (isCreatingProposal.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем подключение к кошельку
|
|
||||||
if (!window.ethereum) {
|
|
||||||
alert('Пожалуйста, установите MetaMask или другой Web3 кошелек');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что пользователь подключен
|
|
||||||
if (!currentUserAddress.value) {
|
|
||||||
alert('Пожалуйста, подключите кошелек');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Валидация данных
|
|
||||||
const recipient = proposalData.value.recipient.trim();
|
|
||||||
const amount = parseFloat(proposalData.value.amount);
|
|
||||||
const description = proposalData.value.description.trim();
|
|
||||||
|
|
||||||
if (!recipient) {
|
|
||||||
alert('Пожалуйста, укажите адрес получателя');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что адрес получателя является корректным Ethereum адресом
|
|
||||||
if (!ethers.isAddress(recipient)) {
|
|
||||||
alert('Пожалуйста, укажите корректный Ethereum адрес получателя');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!amount || amount <= 0) {
|
|
||||||
alert('Пожалуйста, укажите корректное количество токенов');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!description) {
|
|
||||||
alert('Пожалуйста, укажите описание предложения');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что получатель не является отправителем
|
|
||||||
if (recipient.toLowerCase() === currentUserAddress.value.toLowerCase()) {
|
|
||||||
alert('Нельзя отправить токены самому себе');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreatingProposal.value = true;
|
|
||||||
proposalStatus.value = 'Создание предложения...';
|
|
||||||
|
|
||||||
// Создаем предложение
|
|
||||||
const result = await createTransferTokensProposal(dleAddress.value, {
|
|
||||||
recipient: recipient,
|
|
||||||
amount: amount,
|
|
||||||
description: description,
|
|
||||||
duration: proposalData.value.duration * 3600, // Конвертируем часы в секунды
|
|
||||||
governanceChainId: proposalData.value.governanceChainId,
|
|
||||||
targetChains: proposalData.value.targetChains
|
|
||||||
});
|
|
||||||
|
|
||||||
proposalStatus.value = 'Предложение создано!';
|
|
||||||
console.log('Предложение о переводе токенов создано:', result);
|
|
||||||
|
|
||||||
// Сброс формы
|
|
||||||
proposalData.value = {
|
|
||||||
recipient: '',
|
|
||||||
amount: '',
|
|
||||||
description: '',
|
|
||||||
duration: 86400,
|
|
||||||
governanceChainId: 11155111,
|
|
||||||
targetChains: [11155111]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Очищаем статус через 5 секунд
|
|
||||||
setTimeout(() => {
|
|
||||||
proposalStatus.value = '';
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
alert(`Предложение о переводе токенов создано!\nID предложения: ${result.proposalId}\nХеш транзакции: ${result.txHash}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка создания предложения о переводе токенов:', error);
|
|
||||||
|
|
||||||
// Очищаем статус предложения
|
|
||||||
proposalStatus.value = '';
|
|
||||||
|
|
||||||
let errorMessage = 'Ошибка создания предложения о переводе токенов';
|
|
||||||
|
|
||||||
if (error.code === 4001) {
|
|
||||||
errorMessage = 'Транзакция отменена пользователем';
|
|
||||||
} else if (error.message && error.message.includes('insufficient funds')) {
|
|
||||||
errorMessage = 'Недостаточно ETH для оплаты газа';
|
|
||||||
} else if (error.message && error.message.includes('execution reverted')) {
|
|
||||||
errorMessage = 'Ошибка выполнения транзакции. Проверьте данные и попробуйте снова';
|
|
||||||
} else if (error.message) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(errorMessage);
|
|
||||||
} finally {
|
|
||||||
isCreatingProposal.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Отслеживаем изменения в адресе DLE
|
|
||||||
watch(dleAddress, (newAddress) => {
|
|
||||||
if (newAddress) {
|
|
||||||
loadDleData();
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
// Отслеживаем изменения адреса пользователя
|
|
||||||
watch(currentUserAddress, (newAddress) => {
|
|
||||||
if (newAddress && dleAddress.value) {
|
|
||||||
loadUserBalance();
|
|
||||||
} else {
|
|
||||||
userBalance.value = 0;
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tokens-container {
|
|
||||||
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: flex-start;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 2px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h1 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header p {
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dle-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dle-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dle-address {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #666;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-info,
|
|
||||||
.no-dle-info {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Секции */
|
|
||||||
.token-info-section,
|
|
||||||
.transfer-section,
|
|
||||||
.holders-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-info-section h2,
|
|
||||||
.transfer-section h2,
|
|
||||||
.holders-section h2 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Информация о токенах */
|
|
||||||
.token-info-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border-left: 4px solid var(--color-primary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card h3 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-amount {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-address {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #666;
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-wallet {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #dc3545;
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Формы */
|
|
||||||
.transfer-form {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select,
|
|
||||||
.form-group textarea {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Статус предложения */
|
|
||||||
.proposal-status {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background: #e8f5e8;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border-left: 4px solid #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-status .status-message {
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Описание секции */
|
|
||||||
.section-description {
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--color-secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--color-secondary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Состояния */
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 2px dashed #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.form-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipient-item {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.holder-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-info-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -21,15 +21,25 @@ if [ ! -f "./ssl/keys/full_db_encryption.key" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
|
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
|
||||||
|
|
||||||
|
# Создаем роли Read-Only и Editor
|
||||||
|
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
||||||
|
INSERT INTO roles (id, name, description) VALUES
|
||||||
|
(1, 'readonly', 'Read-Only доступ - только просмотр данных'),
|
||||||
|
(2, 'editor', 'Editor доступ - просмотр и редактирование данных')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description;"
|
||||||
|
|
||||||
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
||||||
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
|
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
|
||||||
VALUES
|
VALUES
|
||||||
(encrypt_text('sepolia', '$ENCRYPTION_KEY'), encrypt_text('https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', '$ENCRYPTION_KEY'), 11155111),
|
(encrypt_text('sepolia', '$ENCRYPTION_KEY'), encrypt_text('https://1rpc.io/sepolia', '$ENCRYPTION_KEY'), 11155111),
|
||||||
(encrypt_text('holesky', '$ENCRYPTION_KEY'), encrypt_text('https://ethereum-holesky.publicnode.com', '$ENCRYPTION_KEY'), 17000)
|
(encrypt_text('holesky', '$ENCRYPTION_KEY'), encrypt_text('https://ethereum-holesky.publicnode.com', '$ENCRYPTION_KEY'), 17000)
|
||||||
ON CONFLICT DO NOTHING;"
|
ON CONFLICT DO NOTHING;"
|
||||||
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
||||||
INSERT INTO auth_tokens (name_encrypted, address_encrypted, network_encrypted, min_balance)
|
INSERT INTO auth_tokens (name_encrypted, address_encrypted, network_encrypted, min_balance, readonly_threshold, editor_threshold)
|
||||||
VALUES
|
VALUES
|
||||||
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('holesky', '$ENCRYPTION_KEY'), 1.0),
|
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('holesky', '$ENCRYPTION_KEY'), 1.000000000000000000, 1, 1),
|
||||||
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('sepolia', '$ENCRYPTION_KEY'), 1.0)
|
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('sepolia', '$ENCRYPTION_KEY'), 1.000000000000000000, 1, 1)
|
||||||
ON CONFLICT DO NOTHING;"
|
ON CONFLICT DO NOTHING;"
|
||||||
|
|||||||
Reference in New Issue
Block a user