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

This commit is contained in:
2025-09-30 00:23:37 +03:00
parent ca718e3178
commit 4b03951b31
77 changed files with 17161 additions and 7255 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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: [],

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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: 'Смарт-контракты скомпилированы успешно',

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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]

View File

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

View 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;

View File

@@ -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 {

View File

@@ -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,

View File

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

View File

@@ -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) {
} }
// Запускаем деплой модулей через скрипт // Запускаем деплой модулей через скрипт

View 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('🏁 Инструкции выведены. Выполните очистку кэша в браузере.');

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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;
}
}
/** /**
* Закрывает соединение с базой данных * Закрывает соединение с базой данных
*/ */

View File

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

View File

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

View 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

View File

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

View 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;

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

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

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

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

View 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;

View File

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

View File

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

View File

@@ -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-совместимых сетях

View File

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

View 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>

View File

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

View File

@@ -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);
}
// Если пользователь только что аутентифицировался или сменил аккаунт, // Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом // связываем гостевые сообщения с его аккаунтом

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

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

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

View File

@@ -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',

View File

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

View File

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

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

View File

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

View File

@@ -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`],
// }); // });
// Запрашиваем подпись // Запрашиваем подпись

View 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)"
];

View File

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

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

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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,

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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