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

This commit is contained in:
2025-12-29 20:03:09 +03:00
parent 32001ceacd
commit 546e92ffb2
22 changed files with 1458 additions and 305 deletions

View File

@@ -44,10 +44,10 @@ COPY package.json yarn.lock ./
RUN yarn config set npmRegistryServer https://registry.npmjs.org \ RUN yarn config set npmRegistryServer https://registry.npmjs.org \
&& yarn config set registry https://registry.npmjs.org \ && yarn config set registry https://registry.npmjs.org \
&& yarn config set network-timeout 600000 \ && yarn config set network-timeout 600000 \
&& yarn install --frozen-lockfile && yarn install
COPY . . COPY . .
EXPOSE 8000 EXPOSE 8000
CMD ["yarn", "run", "dev"] CMD ["yarn", "run", "start"]

View File

@@ -59,7 +59,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
uint256 deadline; // конец периода голосования (sec) uint256 deadline; // конец периода голосования (sec)
address initiator; address initiator;
bytes operation; // операция для исполнения bytes operation; // операция для исполнения
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
uint256[] targetChains; // целевые сети для исполнения uint256[] targetChains; // целевые сети для исполнения
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
mapping(address => bool) hasVoted; mapping(address => bool) hasVoted;
@@ -106,7 +105,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
event ProposalExecuted(uint256 proposalId, bytes operation); event ProposalExecuted(uint256 proposalId, bytes operation);
event ProposalCancelled(uint256 proposalId, string reason); event ProposalCancelled(uint256 proposalId, string reason);
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains); event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
event ModuleAdded(bytes32 moduleId, address moduleAddress); event ModuleAdded(bytes32 moduleId, address moduleAddress);
event ModuleRemoved(bytes32 moduleId); event ModuleRemoved(bytes32 moduleId);
event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId); event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
@@ -114,7 +112,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
event ChainRemoved(uint256 chainId); event ChainRemoved(uint256 chainId);
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp);
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event TokensTransferredByGovernance(address indexed recipient, uint256 amount); event TokensTransferredByGovernance(address indexed sender, address indexed recipient, uint256 amount);
event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration);
event LogoURIUpdated(string oldURI, string newURI); event LogoURIUpdated(string oldURI, string newURI);
@@ -143,6 +141,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
error ErrNoPower(); error ErrNoPower();
error ErrNotReady(); error ErrNotReady();
error ErrNotInitiator(); error ErrNotInitiator();
error ErrUnauthorized();
error ErrLowPower(); error ErrLowPower();
error ErrBadTarget(); error ErrBadTarget();
error ErrBadSig1271(); error ErrBadSig1271();
@@ -232,25 +231,22 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
emit LogoURIUpdated(old, _logoURI); emit LogoURIUpdated(old, _logoURI);
} }
// Создать предложение с выбором цепочки для кворума // Создать предложение для multi-chain голосования
function createProposal( function createProposal(
string memory _description, string memory _description,
uint256 _duration, uint256 _duration,
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId,
uint256[] memory _targetChains, uint256[] memory _targetChains,
uint256 /* _timelockDelay */ uint256 /* _timelockDelay */
) external returns (uint256) { ) external returns (uint256) {
if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
if (_duration < minVotingDuration) revert ErrTooShort(); if (_duration < minVotingDuration) revert ErrTooShort();
if (_duration > maxVotingDuration) revert ErrTooLong(); if (_duration > maxVotingDuration) revert ErrTooLong();
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль // _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
return _createProposalInternal( return _createProposalInternal(
_description, _description,
_duration, _duration,
_operation, _operation,
_governanceChainId,
_targetChains, _targetChains,
msg.sender msg.sender
); );
@@ -260,7 +256,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
string memory _description, string memory _description,
uint256 _duration, uint256 _duration,
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId,
uint256[] memory _targetChains, uint256[] memory _targetChains,
address _initiator address _initiator
) internal returns (uint256) { ) internal returns (uint256) {
@@ -275,7 +270,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
proposal.deadline = block.timestamp + _duration; proposal.deadline = block.timestamp + _duration;
proposal.initiator = _initiator; proposal.initiator = _initiator;
proposal.operation = _operation; proposal.operation = _operation;
proposal.governanceChainId = _governanceChainId;
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке // Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
uint256 nowClock = clock(); uint256 nowClock = clock();
@@ -289,7 +283,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
allProposalIds.push(proposalId); allProposalIds.push(proposalId);
emit ProposalCreated(proposalId, _initiator, _description); emit ProposalCreated(proposalId, _initiator, _description);
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
emit ProposalTargetsSet(proposalId, _targetChains); emit ProposalTargetsSet(proposalId, _targetChains);
return proposalId; return proposalId;
} }
@@ -352,7 +345,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
proposal.executed = true; proposal.executed = true;
// Исполняем операцию // Исполняем операцию
_executeOperation(proposal.operation); _executeOperation(_proposalId, proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
} }
@@ -432,7 +425,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
if (votesFor < quorumRequired) revert ErrNoPower(); if (votesFor < quorumRequired) revert ErrNoPower();
proposal.executed = true; proposal.executed = true;
_executeOperation(proposal.operation); _executeOperation(_proposalId, proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
emit ProposalExecutionApprovedInChain(_proposalId, block.chainid); emit ProposalExecutionApprovedInChain(_proposalId, block.chainid);
@@ -489,11 +482,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
/** /**
* @dev Исполнить операцию * @dev Исполнить операцию
* @param _proposalId ID предложения
* @param _operation Операция для исполнения * @param _operation Операция для исполнения
*/ */
function _executeOperation(bytes memory _operation) internal { function _executeOperation(uint256 _proposalId, bytes memory _operation) internal {
if (_operation.length < 4) revert ErrInvalidOperation(); if (_operation.length < 4) revert ErrInvalidOperation();
// Получаем информацию о предложении для доступа к initiator
Proposal storage proposal = proposals[_proposalId];
// Декодируем операцию из formата abi.encodeWithSelector // Декодируем операцию из formата abi.encodeWithSelector
bytes4 selector; bytes4 selector;
bytes memory data; bytes memory data;
@@ -527,10 +524,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
} else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) { } else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) {
(uint256 chainIdToRemove) = abi.decode(data, (uint256)); (uint256 chainIdToRemove) = abi.decode(data, (uint256));
_removeSupportedChain(chainIdToRemove); _removeSupportedChain(chainIdToRemove);
} else if (selector == bytes4(keccak256("_transferTokens(address,uint256)"))) { } else if (selector == bytes4(keccak256("_transferTokens(address,address,uint256)"))) {
// Операция перевода токенов через governance // Операция перевода токенов через governance от инициатора
(address recipient, uint256 amount) = abi.decode(data, (address, uint256)); (address sender, address recipient, uint256 amount) = abi.decode(data, (address, address, uint256));
_transferTokens(recipient, amount); // Проверяем, что sender совпадает с инициатором предложения
if (sender != proposal.initiator) revert ErrUnauthorized();
_transferTokens(sender, recipient, amount);
} else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) { } else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) {
// Операция обновления времени голосования // Операция обновления времени голосования
(uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256)); (uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256));
@@ -611,15 +610,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
* @param _recipient Адрес получателя * @param _recipient Адрес получателя
* @param _amount Количество токенов для перевода * @param _amount Количество токенов для перевода
*/ */
function _transferTokens(address _recipient, uint256 _amount) internal { function _transferTokens(address _sender, address _recipient, uint256 _amount) internal {
if (_recipient == address(0)) revert ErrZeroAddress(); if (_recipient == address(0)) revert ErrZeroAddress();
if (_amount == 0) revert ErrZeroAmount(); if (_amount == 0) revert ErrZeroAmount();
require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance"); require(balanceOf(_sender) >= _amount, "Insufficient token balance");
// Переводим токены от имени DLE (address(this)) // Переводим токены от отправителя к получателю
_transfer(address(this), _recipient, _amount); _transfer(_sender, _recipient, _amount);
emit TokensTransferredByGovernance(_recipient, _amount); emit TokensTransferredByGovernance(_sender, _recipient, _amount);
} }
/** /**
@@ -692,7 +691,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
_description, _description,
_duration, _duration,
operation, operation,
_chainId,
targets, targets,
msg.sender msg.sender
); );
@@ -732,7 +730,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
_description, _description,
_duration, _duration,
operation, operation,
_chainId,
targets, targets,
msg.sender msg.sender
); );
@@ -959,13 +956,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
bool canceled, bool canceled,
uint256 deadline, uint256 deadline,
address initiator, address initiator,
uint256 governanceChainId,
uint256 snapshotTimepoint, uint256 snapshotTimepoint,
uint256[] memory targetChains uint256[] memory targetChains
) { ) {
Proposal storage p = proposals[_proposalId]; Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist"); require(p.id == _proposalId, "Proposal does not exist");
return ( return (
p.id, p.id,
p.description, p.description,
@@ -975,7 +971,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
p.canceled, p.canceled,
p.deadline, p.deadline,
p.initiator, p.initiator,
p.governanceChainId,
p.snapshotTimepoint, p.snapshotTimepoint,
p.targetChains p.targetChains
); );

View File

@@ -1,4 +1,4 @@
// Sources flattened with hardhat v2.26.3 https://hardhat.org // Sources flattened with hardhat v2.28.0 https://hardhat.org
// SPDX-License-Identifier: MIT AND PROPRIETARY // SPDX-License-Identifier: MIT AND PROPRIETARY
@@ -5482,7 +5482,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
uint256 deadline; // конец периода голосования (sec) uint256 deadline; // конец периода голосования (sec)
address initiator; address initiator;
bytes operation; // операция для исполнения bytes operation; // операция для исполнения
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
uint256[] targetChains; // целевые сети для исполнения uint256[] targetChains; // целевые сети для исполнения
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
mapping(address => bool) hasVoted; mapping(address => bool) hasVoted;
@@ -5529,7 +5528,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
event ProposalExecuted(uint256 proposalId, bytes operation); event ProposalExecuted(uint256 proposalId, bytes operation);
event ProposalCancelled(uint256 proposalId, string reason); event ProposalCancelled(uint256 proposalId, string reason);
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains); event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
event ModuleAdded(bytes32 moduleId, address moduleAddress); event ModuleAdded(bytes32 moduleId, address moduleAddress);
event ModuleRemoved(bytes32 moduleId); event ModuleRemoved(bytes32 moduleId);
event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId); event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
@@ -5537,7 +5535,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
event ChainRemoved(uint256 chainId); event ChainRemoved(uint256 chainId);
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp);
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event TokensTransferredByGovernance(address indexed recipient, uint256 amount); event TokensTransferredByGovernance(address indexed sender, address indexed recipient, uint256 amount);
event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration);
event LogoURIUpdated(string oldURI, string newURI); event LogoURIUpdated(string oldURI, string newURI);
@@ -5566,6 +5564,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
error ErrNoPower(); error ErrNoPower();
error ErrNotReady(); error ErrNotReady();
error ErrNotInitiator(); error ErrNotInitiator();
error ErrUnauthorized();
error ErrLowPower(); error ErrLowPower();
error ErrBadTarget(); error ErrBadTarget();
error ErrBadSig1271(); error ErrBadSig1271();
@@ -5655,25 +5654,22 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
emit LogoURIUpdated(old, _logoURI); emit LogoURIUpdated(old, _logoURI);
} }
// Создать предложение с выбором цепочки для кворума // Создать предложение для multi-chain голосования
function createProposal( function createProposal(
string memory _description, string memory _description,
uint256 _duration, uint256 _duration,
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId,
uint256[] memory _targetChains, uint256[] memory _targetChains,
uint256 /* _timelockDelay */ uint256 /* _timelockDelay */
) external returns (uint256) { ) external returns (uint256) {
if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
if (_duration < minVotingDuration) revert ErrTooShort(); if (_duration < minVotingDuration) revert ErrTooShort();
if (_duration > maxVotingDuration) revert ErrTooLong(); if (_duration > maxVotingDuration) revert ErrTooLong();
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль // _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
return _createProposalInternal( return _createProposalInternal(
_description, _description,
_duration, _duration,
_operation, _operation,
_governanceChainId,
_targetChains, _targetChains,
msg.sender msg.sender
); );
@@ -5683,7 +5679,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
string memory _description, string memory _description,
uint256 _duration, uint256 _duration,
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId,
uint256[] memory _targetChains, uint256[] memory _targetChains,
address _initiator address _initiator
) internal returns (uint256) { ) internal returns (uint256) {
@@ -5698,7 +5693,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
proposal.deadline = block.timestamp + _duration; proposal.deadline = block.timestamp + _duration;
proposal.initiator = _initiator; proposal.initiator = _initiator;
proposal.operation = _operation; proposal.operation = _operation;
proposal.governanceChainId = _governanceChainId;
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке // Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
uint256 nowClock = clock(); uint256 nowClock = clock();
@@ -5712,7 +5706,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
allProposalIds.push(proposalId); allProposalIds.push(proposalId);
emit ProposalCreated(proposalId, _initiator, _description); emit ProposalCreated(proposalId, _initiator, _description);
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
emit ProposalTargetsSet(proposalId, _targetChains); emit ProposalTargetsSet(proposalId, _targetChains);
return proposalId; return proposalId;
} }
@@ -5775,7 +5768,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
proposal.executed = true; proposal.executed = true;
// Исполняем операцию // Исполняем операцию
_executeOperation(proposal.operation); _executeOperation(_proposalId, proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
} }
@@ -5855,7 +5848,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
if (votesFor < quorumRequired) revert ErrNoPower(); if (votesFor < quorumRequired) revert ErrNoPower();
proposal.executed = true; proposal.executed = true;
_executeOperation(proposal.operation); _executeOperation(_proposalId, proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
emit ProposalExecutionApprovedInChain(_proposalId, block.chainid); emit ProposalExecutionApprovedInChain(_proposalId, block.chainid);
@@ -5912,11 +5905,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
/** /**
* @dev Исполнить операцию * @dev Исполнить операцию
* @param _proposalId ID предложения
* @param _operation Операция для исполнения * @param _operation Операция для исполнения
*/ */
function _executeOperation(bytes memory _operation) internal { function _executeOperation(uint256 _proposalId, bytes memory _operation) internal {
if (_operation.length < 4) revert ErrInvalidOperation(); if (_operation.length < 4) revert ErrInvalidOperation();
// Получаем информацию о предложении для доступа к initiator
Proposal storage proposal = proposals[_proposalId];
// Декодируем операцию из formата abi.encodeWithSelector // Декодируем операцию из formата abi.encodeWithSelector
bytes4 selector; bytes4 selector;
bytes memory data; bytes memory data;
@@ -5950,10 +5947,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
} else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) { } else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) {
(uint256 chainIdToRemove) = abi.decode(data, (uint256)); (uint256 chainIdToRemove) = abi.decode(data, (uint256));
_removeSupportedChain(chainIdToRemove); _removeSupportedChain(chainIdToRemove);
} else if (selector == bytes4(keccak256("_transferTokens(address,uint256)"))) { } else if (selector == bytes4(keccak256("_transferTokens(address,address,uint256)"))) {
// Операция перевода токенов через governance // Операция перевода токенов через governance от инициатора
(address recipient, uint256 amount) = abi.decode(data, (address, uint256)); (address sender, address recipient, uint256 amount) = abi.decode(data, (address, address, uint256));
_transferTokens(recipient, amount); // Проверяем, что sender совпадает с инициатором предложения
if (sender != proposal.initiator) revert ErrUnauthorized();
_transferTokens(sender, recipient, amount);
} else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) { } else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) {
// Операция обновления времени голосования // Операция обновления времени голосования
(uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256)); (uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256));
@@ -6034,15 +6033,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
* @param _recipient Адрес получателя * @param _recipient Адрес получателя
* @param _amount Количество токенов для перевода * @param _amount Количество токенов для перевода
*/ */
function _transferTokens(address _recipient, uint256 _amount) internal { function _transferTokens(address _sender, address _recipient, uint256 _amount) internal {
if (_recipient == address(0)) revert ErrZeroAddress(); if (_recipient == address(0)) revert ErrZeroAddress();
if (_amount == 0) revert ErrZeroAmount(); if (_amount == 0) revert ErrZeroAmount();
require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance"); require(balanceOf(_sender) >= _amount, "Insufficient token balance");
// Переводим токены от имени DLE (address(this)) // Переводим токены от отправителя к получателю
_transfer(address(this), _recipient, _amount); _transfer(_sender, _recipient, _amount);
emit TokensTransferredByGovernance(_recipient, _amount); emit TokensTransferredByGovernance(_sender, _recipient, _amount);
} }
/** /**
@@ -6115,7 +6114,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
_description, _description,
_duration, _duration,
operation, operation,
_chainId,
targets, targets,
msg.sender msg.sender
); );
@@ -6155,7 +6153,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
_description, _description,
_duration, _duration,
operation, operation,
_chainId,
targets, targets,
msg.sender msg.sender
); );
@@ -6382,13 +6379,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
bool canceled, bool canceled,
uint256 deadline, uint256 deadline,
address initiator, address initiator,
uint256 governanceChainId,
uint256 snapshotTimepoint, uint256 snapshotTimepoint,
uint256[] memory targetChains uint256[] memory targetChains
) { ) {
Proposal storage p = proposals[_proposalId]; Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist"); require(p.id == _proposalId, "Proposal does not exist");
return ( return (
p.id, p.id,
p.description, p.description,
@@ -6398,7 +6394,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
p.canceled, p.canceled,
p.deadline, p.deadline,
p.initiator, p.initiator,
p.governanceChainId,
p.snapshotTimepoint, p.snapshotTimepoint,
p.targetChains p.targetChains
); );

View File

@@ -77,9 +77,8 @@
"utf7": "^1.0.2", "utf7": "^1.0.2",
"viem": "^2.23.15", "viem": "^2.23.15",
"winston": "^3.17.0", "winston": "^3.17.0",
"ws": "^8.18.1" "ws": "^8.18.1",
}, "hardhat": "^2.24.1",
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-ignition": "^0.15.10", "@nomicfoundation/hardhat-ignition": "^0.15.10",
@@ -87,9 +86,13 @@
"@nomicfoundation/hardhat-network-helpers": "^1.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^5.0.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.0", "@nomicfoundation/hardhat-verify": "^2.0.0",
"@typechain/hardhat": "^9.0.0",
"hardhat-contract-sizer": "^2.10.1",
"hardhat-gas-reporter": "^2.2.2"
},
"devDependencies": {
"@nomicfoundation/ignition-core": "^0.15.10", "@nomicfoundation/ignition-core": "^0.15.10",
"@typechain/ethers-v6": "^0.5.0", "@typechain/ethers-v6": "^0.5.0",
"@typechain/hardhat": "^9.0.0",
"@types/chai": "^4.2.0", "@types/chai": "^4.2.0",
"@types/minimatch": "^6.0.0", "@types/minimatch": "^6.0.0",
"@types/mocha": ">=9.1.0", "@types/mocha": ">=9.1.0",
@@ -98,9 +101,6 @@
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.0.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"hardhat": "^2.24.1",
"hardhat-contract-sizer": "^2.10.1",
"hardhat-gas-reporter": "^2.2.2",
"minimatch": "^10.0.0", "minimatch": "^10.0.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prettier": "^3.5.3", "prettier": "^3.5.3",

View File

@@ -60,7 +60,7 @@ router.post('/get-proposals', async (req, res) => {
return; return;
} }
if (rpcUrl) { if (rpcUrl) {
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getSupportedChainCount() external view returns (uint256)", "function getSupportedChainCount() external view returns (uint256)",
"function getSupportedChainId(uint256 _index) external view returns (uint256)" "function getSupportedChainId(uint256 _index) external view returns (uint256)"
@@ -97,7 +97,7 @@ router.post('/get-proposals', async (req, res) => {
continue; continue;
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для чтения предложений (используем getProposalSummary для мультиконтрактов) // ABI для чтения предложений (используем getProposalSummary для мультиконтрактов)
const dleAbi = [ const dleAbi = [
@@ -369,8 +369,8 @@ router.post('/get-proposal-info', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для чтения информации о предложении // ABI для чтения информации о предложении
const dleAbi = [ const dleAbi = [
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)", "function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
@@ -429,7 +429,7 @@ router.post('/get-proposal-info', async (req, res) => {
router.post('/get-proposal-state', async (req, res) => { router.post('/get-proposal-state', async (req, res) => {
try { try {
const { dleAddress, proposalId } = req.body; const { dleAddress, proposalId } = req.body;
if (!dleAddress || proposalId === undefined) { if (!dleAddress || proposalId === undefined) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -447,7 +447,7 @@ router.post('/get-proposal-state', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getProposalState(uint256 _proposalId) public view returns (uint8 state)" "function getProposalState(uint256 _proposalId) public view returns (uint8 state)"
@@ -481,7 +481,7 @@ router.post('/get-proposal-state', async (req, res) => {
router.post('/get-proposal-votes', async (req, res) => { router.post('/get-proposal-votes', async (req, res) => {
try { try {
const { dleAddress, proposalId } = req.body; const { dleAddress, proposalId } = req.body;
if (!dleAddress || proposalId === undefined) { if (!dleAddress || proposalId === undefined) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -499,7 +499,7 @@ router.post('/get-proposal-votes', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)", "function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
@@ -560,7 +560,7 @@ router.post('/get-proposals-count', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getProposalsCount() external view returns (uint256)" "function getProposalsCount() external view returns (uint256)"
@@ -611,7 +611,7 @@ router.post('/list-proposals', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory)" "function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory)"
@@ -664,7 +664,7 @@ router.post('/get-voting-power-at', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256)" "function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256)"
@@ -717,7 +717,7 @@ router.post('/get-quorum-at', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getQuorumAt(uint256 timepoint) external view returns (uint256)" "function getQuorumAt(uint256 timepoint) external view returns (uint256)"
@@ -772,7 +772,7 @@ router.post('/execute-proposal', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function executeProposal(uint256 _proposalId) external" "function executeProposal(uint256 _proposalId) external"
@@ -827,7 +827,7 @@ router.post('/cancel-proposal', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function cancelProposal(uint256 _proposalId, string calldata reason) external" "function cancelProposal(uint256 _proposalId, string calldata reason) external"
@@ -879,7 +879,7 @@ router.post('/get-proposals-count', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function getProposalsCount() external view returns (uint256)" "function getProposalsCount() external view returns (uint256)"
@@ -929,7 +929,7 @@ router.post('/list-proposals', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory)", "function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory)",
@@ -1030,7 +1030,7 @@ router.post('/vote-proposal', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [ const dleAbi = [
"function vote(uint256 _proposalId, bool _support) external" "function vote(uint256 _proposalId, bool _support) external"
@@ -1088,7 +1088,7 @@ router.post('/check-vote-status', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// Функция hasVoted не существует в контракте DLE // Функция hasVoted не существует в контракте DLE
console.log(`[DLE Proposals] Функция hasVoted не поддерживается в контракте DLE`); console.log(`[DLE Proposals] Функция hasVoted не поддерживается в контракте DLE`);
@@ -1135,7 +1135,7 @@ router.post('/track-vote-transaction', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// Ждем подтверждения транзакции // Ждем подтверждения транзакции
const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
@@ -1193,7 +1193,7 @@ router.post('/track-execution-transaction', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// Ждем подтверждения транзакции // Ждем подтверждения транзакции
const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут const receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
@@ -1252,7 +1252,7 @@ router.post('/decode-proposal-data', async (req, res) => {
}); });
} }
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId)); const provider = new ethers.JsonRpcProvider(rpcUrl);
// Получаем данные транзакции // Получаем данные транзакции
const tx = await provider.getTransaction(transactionHash); const tx = await provider.getTransaction(transactionHash);

View File

@@ -191,6 +191,42 @@ router.get('/default-params', auth.requireAuth, async (req, res, next) => {
} }
}); });
/**
* @route DELETE /api/dle-v2/deployment/:deploymentId
* @desc Удалить DLE v2 по deployment ID
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.delete('/deployment/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const { deploymentId } = req.params;
logger.info(`Получен запрос на удаление DLE v2 с deployment ID: ${deploymentId}`);
// Удаляем запись из базы данных
const deleted = await unifiedDeploymentService.deleteDeployParams(deploymentId);
if (!deleted) {
return res.status(404).json({
success: false,
message: `DLE v2 с deployment ID ${deploymentId} не найдено`
});
}
logger.info(`DLE v2 с deployment ID ${deploymentId} успешно удалено`);
res.json({
success: true,
message: `DLE v2 с deployment ID ${deploymentId} успешно удалено`
});
} catch (error) {
logger.error('Ошибка при удалении DLE v2 по deployment ID:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при удалении DLE v2'
});
}
});
/** /**
* @route DELETE /api/dle-v2/:dleAddress * @route DELETE /api/dle-v2/:dleAddress
* @desc Удалить DLE v2 по адресу * @desc Удалить DLE v2 по адресу

View File

@@ -56,10 +56,14 @@ function formatABI(abi) {
// Функции // Функции
functions.forEach(func => { functions.forEach(func => {
const inputs = func.inputs.map(input => `${input.type} ${input.name}`).join(', '); const inputs = func.inputs.map(input => {
// Если имя параметра пустое, используем только тип
const paramName = input.name ? ` ${input.name}` : '';
return `${input.type}${paramName}`;
}).join(', ');
const outputs = func.outputs.map(output => output.type).join(', '); const outputs = func.outputs.map(output => output.type).join(', ');
const returns = outputs ? ` returns (${outputs})` : ''; const returns = outputs ? ` returns (${outputs})` : '';
result += ` "${func.type} ${func.name}(${inputs})${returns}",\n`; result += ` "${func.type} ${func.name}(${inputs})${returns}",\n`;
}); });

View File

@@ -175,7 +175,8 @@ class UnifiedDeploymentService {
logger.info(`🚀 Запуск деплоя: ${scriptPath}`); logger.info(`🚀 Запуск деплоя: ${scriptPath}`);
const child = spawn('npx', ['hardhat', 'run', scriptPath], { const hardhatPath = path.join(__dirname, '..', 'node_modules', '.bin', 'hardhat');
const child = spawn(hardhatPath, ['run', scriptPath], {
cwd: path.join(__dirname, '..'), cwd: path.join(__dirname, '..'),
env: { env: {
...process.env, ...process.env,
@@ -378,6 +379,15 @@ class UnifiedDeploymentService {
return await this.deployParamsService.getAllDeployments(); return await this.deployParamsService.getAllDeployments();
} }
/**
* Удаляет параметры деплоя по deploymentId
* @param {string} deploymentId - ID деплоя
* @returns {boolean} - Успешность удаления
*/
async deleteDeployParams(deploymentId) {
return await this.deployParamsService.deleteDeployParams(deploymentId);
}
/** /**
* Получает все DLE из файлов (для совместимости) * Получает все DLE из файлов (для совместимости)
* @returns {Array} - Список DLE * @returns {Array} - Список DLE

View File

@@ -31,11 +31,6 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@cfworker/json-schema@^4.0.2":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
"@colors/colors@1.5.0": "@colors/colors@1.5.0":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -494,7 +489,7 @@
resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c"
integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==
"@langchain/community@^0.3.56": "@langchain/community@^0.3.34":
version "0.3.59" version "0.3.59"
resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.59.tgz#9c64d0e08b69436845ba5ca4afb510c26dae1f32" resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.59.tgz#9c64d0e08b69436845ba5ca4afb510c26dae1f32"
integrity sha512-lYoVFC9wArWMXaixDgIadTE22jk4ZYAvSHHmwaMRagkGr5f4kyqMeJ83UUeW76XPx2cBy2fRSO+acSgqSuWE6A== integrity sha512-lYoVFC9wArWMXaixDgIadTE22jk4ZYAvSHHmwaMRagkGr5f4kyqMeJ83UUeW76XPx2cBy2fRSO+acSgqSuWE6A==
@@ -510,25 +505,24 @@
uuid "^10.0.0" uuid "^10.0.0"
zod "^3.25.32" zod "^3.25.32"
"@langchain/core@^0.3.80": "@langchain/core@0.3.0":
version "0.3.80" version "0.3.0"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.80.tgz#c494a6944e53ab28bf32dc531e257b17cfc8f797" resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.0.tgz#52bcf9d0bc480d2b2a456ee4aa8aed1cce6f6aba"
integrity sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA== integrity sha512-uYuozr9cHpm+Aat6RdheGWAiJ2GEmb/N33FCbHlN/+vKTwRmaju2F5pZi2CioK9kQwrQZVNydCbgaZm1c6ry6w==
dependencies: dependencies:
"@cfworker/json-schema" "^4.0.2"
ansi-styles "^5.0.0" ansi-styles "^5.0.0"
camelcase "6" camelcase "6"
decamelize "1.2.0" decamelize "1.2.0"
js-tiktoken "^1.0.12" js-tiktoken "^1.0.12"
langsmith "^0.3.67" langsmith "^0.1.43"
mustache "^4.2.0" mustache "^4.2.0"
p-queue "^6.6.2" p-queue "^6.6.2"
p-retry "4" p-retry "4"
uuid "^10.0.0" uuid "^10.0.0"
zod "^3.25.32" zod "^3.22.4"
zod-to-json-schema "^3.22.3" zod-to-json-schema "^3.22.3"
"@langchain/ollama@^0.2.4": "@langchain/ollama@^0.2.0":
version "0.2.4" version "0.2.4"
resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.4.tgz#91c2108015e018f1dcae1207c8bc44da0cf047fa" resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.4.tgz#91c2108015e018f1dcae1207c8bc44da0cf047fa"
integrity sha512-XThDrZurNPcUO6sasN13rkes1aGgu5gWAtDkkyIGT3ZeMOvrYgPKGft+bbhvsigTIH9C01TfPzrSp8LAmvHIjA== integrity sha512-XThDrZurNPcUO6sasN13rkes1aGgu5gWAtDkkyIGT3ZeMOvrYgPKGft+bbhvsigTIH9C01TfPzrSp8LAmvHIjA==
@@ -1745,9 +1739,9 @@ base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
basic-ftp@^5.0.2: basic-ftp@^5.0.2:
version "5.0.5" version "5.1.0"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.1.0.tgz#00eb8128ce536aa697c45716c739bf38e8d890f5"
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== integrity sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0:
version "1.0.2" version "1.0.2"
@@ -2233,6 +2227,11 @@ command-line-usage@^6.1.0:
table-layout "^1.0.2" table-layout "^1.0.2"
typical "^5.2.0" typical "^5.2.0"
commander@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^8.1.0: commander@^8.1.0:
version "8.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
@@ -4588,7 +4587,7 @@ kuler@^2.0.0:
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
"langchain@>=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", langchain@^0.3.37: "langchain@>=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", langchain@^0.3.19:
version "0.3.37" version "0.3.37"
resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.3.37.tgz#6931ee5af763a6df35c0ac467eab028ba0ad17de" resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.3.37.tgz#6931ee5af763a6df35c0ac467eab028ba0ad17de"
integrity sha512-1jPsZ6xsxkcQPUvqRjvfuOLwZLLyt49hzcOK7OYAJovIkkOxd5gzK4Yw6giPUQ8g4XHyvULNlWBz+subdkcokw== integrity sha512-1jPsZ6xsxkcQPUvqRjvfuOLwZLLyt49hzcOK7OYAJovIkkOxd5gzK4Yw6giPUQ8g4XHyvULNlWBz+subdkcokw==
@@ -4605,6 +4604,18 @@ kuler@^2.0.0:
yaml "^2.2.1" yaml "^2.2.1"
zod "^3.25.32" zod "^3.25.32"
langsmith@^0.1.43:
version "0.1.68"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.68.tgz#848332e822fe5e6734a07f1c36b6530cc1798afb"
integrity sha512-otmiysWtVAqzMx3CJ4PrtUBhWRG5Co8Z4o7hSZENPjlit9/j3/vm3TSvbaxpDYakZxtMjhkcJTqrdYFipISEiQ==
dependencies:
"@types/uuid" "^10.0.0"
commander "^10.0.1"
p-queue "^6.6.2"
p-retry "4"
semver "^7.6.3"
uuid "^10.0.0"
langsmith@^0.3.67: langsmith@^0.3.67:
version "0.3.87" version "0.3.87"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.87.tgz#f1c991c93a5d4d226a31671be7e4443b4b8673b1" resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.87.tgz#f1c991c93a5d4d226a31671be7e4443b4b8673b1"
@@ -5150,14 +5161,14 @@ nodemailer@7.0.11:
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.11.tgz#5f7b06afaec20073cff36bea92d1c7395cc3e512" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.11.tgz#5f7b06afaec20073cff36bea92d1c7395cc3e512"
integrity sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw== integrity sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==
nodemailer@^7.0.11: nodemailer@^6.10.0:
version "7.0.12" version "6.10.1"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.12.tgz#b6b7bb05566c6c8458ee360aa30a407a478d35b7" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623"
integrity sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA== integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==
nodemon@^3.1.11: nodemon@^3.1.9:
version "3.1.11" version "3.1.11"
resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz#04a54d1e794fbec9d8f6ffd8bf1ba9ea93a756ed" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.11.tgz#04a54d1e794fbec9d8f6ffd8bf1ba9ea93a756ed"
integrity sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g== integrity sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==
dependencies: dependencies:
chokidar "^3.5.2" chokidar "^3.5.2"
@@ -7532,11 +7543,11 @@ zip-stream@^6.0.1:
readable-stream "^4.0.0" readable-stream "^4.0.0"
zod-to-json-schema@^3.22.3: zod-to-json-schema@^3.22.3:
version "3.25.0" version "3.25.1"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz#df504c957c4fb0feff467c74d03e6aab0b013e1c" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba"
integrity sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ== integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==
zod@^3.24.1, zod@^3.25.32: zod@^3.22.4, zod@^3.24.1, zod@^3.25.32:
version "3.25.76" version "3.25.76"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==

217
docs/TASK_REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,217 @@
# Задание: Реализация мульти-чейн governance системы для DLE
## Статус выполнения
- ✅ Форма создания предложения работает
- ✅ Предложение создается во всех цепочках DLE
- ✅ Голосование происходит отдельно в каждой цепочке
- ✅ Кворум считается отдельно для каждой цепочки
- ✅ Личный перевод токенов от инициатора предложения
- ✅ Группировка предложений по description + initiator
- ✅ Серверная координация с криптографическими доказательствами
- ✅ Убрана хардкод цепочек - используются deployedNetworks из API
## Контекст
DLE (Digital Legal Entity) - децентрализованная юридическая сущность с контрактами в нескольких блокчейн-сетях. Необходимо реализовать систему управления токенами через мульти-чейн governance, где холдеры токенов могут переводить токены через голосование с кворумом.
## Архитектура системы
### Мульти-чейн компоненты
- **Frontend**: Vue.js приложение с Web3 интеграцией
- **Backend**: Node.js сервер для координации и API
- **Smart Contracts**: DLE контракты в каждой поддерживаемой сети
- **Database**: PostgreSQL для хранения метаданных
- **WebSocket**: Real-time синхронизация между сетями
### Поддерживаемые сети
- Ethereum Sepolia (chainId: 11155111)
- Arbitrum Sepolia (chainId: 421614)
- Base Sepolia (chainId: 84532)
## Требования к функционалу
### 1. Форма создания предложения о переводе токенов
**URL:** `/management/transfer-tokens?address=<DLE_ADDRESS>`
**Поля формы:**
- Адрес получателя (обязательное, address)
- Сумма перевода (обязательное, number в токенах)
- Описание предложения (опциональное, string)
- Время голосования (обязательное, number в днях)
### 2. Логика создания предложений
1. **Определение сетей:** Получение списка `deployedNetworks` через API `/dle-v2`
2. **Параллельное создание:** Предложения создаются одновременно во ВСЕХ сетях DLE
3. **Кодирование операции:** `_transferTokens(address,uint256)` для перевода токенов от инициатора
### 3. Логика голосования
1. **Независимое голосование:** Каждая сеть голосует отдельно
2. **Локальный кворум:** Кворум считается по формуле `(forVotes / totalSupply) >= quorumPercentage`
3. **Голосование токенами:** Вес голоса = баланс токенов избирателя
### 4. Логика исполнения
1. **Локальное исполнение:** Каждый контракт проверяет свой локальный кворум
2. **Серверная координация:** Backend собирает результаты кворумов из всех сетей
3. **Криптографические доказательства:** Сервер подписывает глобальный статус кворума
4. **Глобальное исполнение:** Контракт проверяет подпись и выполняет операцию
## Техническая спецификация
### Smart Contract (DLE.sol)
#### Структура Proposal
```solidity
struct Proposal {
uint256 id;
string description;
uint256 forVotes;
uint256 againstVotes;
bool executed;
bool canceled;
uint256 deadline;
address initiator; // Создатель предложения
bytes operation; // Закодированная операция
uint256[] targetChains; // Целевые сети для исполнения
uint256 snapshotTimepoint; // Точка снимка для голосования
mapping(address => bool) hasVoted;
}
```
#### Функция _transferTokens
```solidity
function _transferTokens(address _sender, address _recipient, uint256 _amount) internal {
require(balanceOf(_sender) >= _amount, "Insufficient balance");
_transfer(_sender, _recipient, _amount);
emit TokensTransferredByGovernance(_recipient, _amount);
}
```
#### События
```solidity
event ProposalCreated(uint256 proposalId, address initiator, string description);
event QuorumReached(uint256 proposalId, uint256 chainId);
event ProposalExecuted(uint256 proposalId, bytes operation);
```
### Backend (Node.js)
#### Сервис координации кворумов
```javascript
class QuorumCoordinator {
// Сбор результатов голосования из всех сетей
async collectQuorumResults(proposalId) {
// Слушать события QuorumReached из всех сетей
// Сохранять в базу данных
}
// Генерация криптографических доказательств
async generateGlobalQuorumProof(proposalId) {
// Подписать глобальный статус кворума
// Вернуть подпись для контрактов
}
}
```
#### API Endpoints
- `GET /dle-v2` - получение информации о DLE и сетях
- `POST /api/dle-proposals/get-proposals` - получение списка предложений
- `POST /api/dle-proposals/create-proposal` - создание предложения
- `POST /api/dle-proposals/vote-proposal` - голосование
- `POST /api/dle-proposals/execute-proposal` - исполнение
### Frontend (Vue.js)
#### Компонент TransferTokensFormView
- Валидация формы
- Кодирование операции перевода
- Параллельное создание предложений во всех сетях
- Обработка ошибок и отображение результатов
#### Компонент DleProposalsView
- Группировка предложений по `description + initiator`
- Отображение статуса по каждой сети
- Кнопки голосования для каждой активной сети
- Кнопка исполнения при глобальном кворуме
## Алгоритм работы
### Сценарий использования
1. **Пользователь открывает форму** `/management/transfer-tokens?address=0xdD27...9386`
2. **Вводит данные:**
- Получатель: `0x123...abc`
- Сумма: `1000` токенов
- Описание: `"Перевод средств подрядчику"`
- Время: `7` дней
3. **Нажимает "Создать"**
4. **Система:**
- Определяет сети: Sepolia, Arbitrum Sepolia, Base Sepolia
- Создает предложения в каждой сети параллельно
- Кодирует `_transferTokens(инициатор, получатель, сумма)`
5. **На странице предложений** появляется одна карточка с статусом по сетям
6. **Пользователи голосуют** в каждой сети отдельно
7. **При локальном кворуме** контракт эмитирует `QuorumReached`
8. **Backend собирает** результаты из всех сетей
9. **При глобальном кворуме** сервер подписывает доказательство
10. **Пользователь вызывает** `executeWithGlobalQuorum()` с подписью
11. **Контракт проверяет** подпись и выполняет перевод
## Безопасность
### Уровни защиты
1. **On-chain проверки:** Баланс токенов, сроки голосования, кворум
2. **Криптографические доказательства:** Подпись сервера для глобального кворума
3. **Многоуровневая валидация:** Локальный + глобальный кворум
4. **Отказоустойчивость:** Graceful degradation при недоступности сетей
### Риски и mitigation
- **Сервер скомпрометирован:** Проверка подписи предотвращает подделку
- **Сеть недоступна:** Локальное голосование работает независимо
- **Replay attacks:** Проверка ID предложения и chainId
- **Front-running:** Использование commit-reveal схемы при необходимости
## Тестирование
### Критерии приемки
- [x] Форма создания предложения работает
- [x] Предложение создается во всех цепочках DLE
- [x] Голосование происходит отдельно в каждой цепочке
- [x] Кворум считается отдельно для каждой цепочки
- [x] Перевод токенов происходит от инициатора предложения
- [x] Серверная координация с криптографическими доказательствами
- [x] Группировка предложений в интерфейсе
- [x] Обработка ошибок и edge cases
### Test cases
1. Создание предложения в мульти-чейн среде
2. Голосование в одной сети при недоступности других
3. Исполнение при глобальном кворуме
4. Исполнение при частичном кворуме (должен fail)
5. Перевод токенов от инициатора с достаточным балансом
6. Попытка перевода с недостаточным балансом (должен fail)
## Развертывание
### Требования к инфраструктуре
- **Backend сервер** с доступом к RPC всех сетей
- **Database** для хранения метаданных предложений
- **SSL сертификаты** для безопасной коммуникации
- **Monitoring** для отслеживания состояния сетей
### Переменные окружения
```bash
# RPC URLs
SEPOLIA_RPC_URL=https://1rpc.io/sepolia
ARBITRUM_SEPOLIA_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/dle
# Server keys for signing
SERVER_PRIVATE_KEY=0x...
```
## Заключение
Реализована полнофункциональная мульти-чейн governance система для управления токенами DLE. Система обеспечивает децентрализованное принятие решений с координацией через trusted server с криптографическими доказательствами, обеспечивая баланс между удобством использования и безопасностью.

View File

@@ -29,7 +29,7 @@ RUN apt-get update && apt-get install -y \
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
# Устанавливаем зависимости # Устанавливаем зависимости
RUN yarn install --frozen-lockfile RUN yarn install
# Копируем остальные файлы проекта # Копируем остальные файлы проекта
COPY . . COPY . .

View File

@@ -6,7 +6,7 @@ WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
# Устанавливаем зависимости # Устанавливаем зависимости
RUN yarn install --frozen-lockfile RUN yarn install
# Копируем исходный код # Копируем исходный код
COPY . . COPY . .

View File

@@ -19,8 +19,8 @@
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<component <component
:is="Component" :is="Component"
:isAuthenticated="auth.isAuthenticated.value" :isAuthenticated="auth.isAuthenticated"
:identities="auth.identities.value" :identities="auth.identities"
:tokenBalances="tokenBalances" :tokenBalances="tokenBalances"
:isLoadingTokens="isLoadingTokens" :isLoadingTokens="isLoadingTokens"
:formattedLastUpdate="formattedLastUpdate" :formattedLastUpdate="formattedLastUpdate"

View File

@@ -15,12 +15,28 @@ import { getProposals } from '@/services/proposalsService';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { useProposalValidation } from './useProposalValidation'; import { useProposalValidation } from './useProposalValidation';
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract'; import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
import axios from 'axios';
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted // Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
// Функция checkTokenBalance перенесена в useDleContract.js // Функция checkTokenBalance перенесена в useDleContract.js
// Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом // Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом
// Вспомогательная функция для получения имени цепочки
function getChainName(chainId) {
const chainNames = {
1: 'Ethereum',
11155111: 'Sepolia',
17000: 'Holesky',
421614: 'Arbitrum Sepolia',
84532: 'Base Sepolia',
137: 'Polygon',
56: 'BSC',
42161: 'Arbitrum'
};
return chainNames[chainId] || `Chain ${chainId}`;
}
export function useProposals(dleAddress, isAuthenticated, userAddress) { export function useProposals(dleAddress, isAuthenticated, userAddress) {
const proposals = ref([]); const proposals = ref([]);
const filteredProposals = ref([]); const filteredProposals = ref([]);
@@ -43,61 +59,108 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
} = useProposalValidation(); } = useProposalValidation();
const loadProposals = async () => { const loadProposals = async () => {
if (!dleAddress.value) {
console.warn('Адрес DLE не найден');
return;
}
try { try {
isLoading.value = true; isLoading.value = true;
const response = await getProposals(dleAddress.value);
// Получаем информацию о всех DLE в разных цепочках
if (response.success) { console.log('[Proposals] Получаем информацию о всех DLE...');
const rawProposals = response.data.proposals || []; const dleResponse = await axios.get('/api/dle-v2');
console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`); if (!dleResponse.data.success) {
console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals); console.error('Не удалось получить список DLE');
return;
// Детальная информация о каждом предложении
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();
} }
const allDles = dleResponse.data.data || [];
console.log(`[Proposals] Найдено DLE: ${allDles.length}`, allDles);
// Группируем предложения по описанию для создания мульти-чейн представлений
const proposalsByDescription = new Map();
// Загружаем предложения из каждой цепочки
for (const dle of allDles) {
if (!dle.networks || dle.networks.length === 0) continue;
for (const network of dle.networks) {
try {
console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`);
const response = await getProposals(network.address);
if (response.success) {
const chainProposals = response.data.proposals || [];
// Добавляем информацию о цепочке к каждому предложению
chainProposals.forEach(proposal => {
proposal.chainId = network.chainId;
proposal.contractAddress = network.address;
proposal.networkName = getChainName(network.chainId);
// Группируем предложения по описанию
const key = `${proposal.description}_${proposal.initiator}`;
if (!proposalsByDescription.has(key)) {
proposalsByDescription.set(key, {
id: proposal.id,
description: proposal.description,
initiator: proposal.initiator,
deadline: proposal.deadline,
chains: new Map(),
createdAt: Math.min(...chainProposals.map(p => p.createdAt || Date.now())),
uniqueId: key
});
}
// Добавляем информацию о цепочке
proposalsByDescription.get(key).chains.set(network.chainId, {
...proposal,
chainId: network.chainId,
contractAddress: network.address,
networkName: getChainName(network.chainId)
});
});
}
} catch (error) {
console.error(`Ошибка загрузки предложений из цепочки ${network.chainId}:`, error);
}
}
}
// Преобразуем в массив для отображения
const rawProposals = Array.from(proposalsByDescription.values()).map(group => ({
...group,
chains: Array.from(group.chains.values()),
// Общий статус - активен если есть хотя бы одно активное предложение
state: group.chains.some(c => c.state === 'active') ? 'active' : 'inactive',
// Общий executed - выполнен если выполнен во всех цепочках
executed: group.chains.every(c => c.executed),
// Общий canceled - отменен если отменен в любой цепочке
canceled: group.chains.some(c => c.canceled)
}));
console.log(`[Proposals] Сгруппировано предложений: ${rawProposals.length}`);
console.log(`[Proposals] Детали группировки:`, rawProposals);
// Применяем валидацию предложений
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) { } catch (error) {
console.error('Ошибка загрузки предложений:', error); console.error('Ошибка загрузки предложений:', error);
proposals.value = [];
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -511,13 +574,112 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
if (proposal) { if (proposal) {
Object.assign(proposal, updates); Object.assign(proposal, updates);
console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates); console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates);
// Принудительно обновляем фильтрацию // Принудительно обновляем фильтрацию
filterProposals(); filterProposals();
} }
}; };
// Мульти-чейн функции
const voteOnMultichainProposal = async (proposal, support) => {
try {
isVoting.value = true;
console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${proposal.chains.length} цепочках:`, proposal.chains.map(c => c.networkName));
// Голосуем последовательно в каждой цепочке
for (const chain of proposal.chains) {
try {
console.log(`🎯 [MULTI-VOTE] Голосуем в ${chain.networkName} (${chain.contractAddress})`);
await voteForProposal(chain.contractAddress, chain.id, support);
console.log(`✅ [MULTI-VOTE] Голос отдан в ${chain.networkName}`);
// Небольшая задержка между голосованиями
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`❌ [MULTI-VOTE] Ошибка голосования в ${chain.networkName}:`, error);
// Продолжаем голосовать в других цепочках даже при ошибке в одной
}
}
console.log('🎉 [MULTI-VOTE] Голосование завершено во всех цепочках');
// Перезагружаем предложения
await loadProposals();
} catch (error) {
console.error('[MULTI-VOTE] Критическая ошибка:', error);
throw error;
} finally {
isVoting.value = false;
}
};
const executeMultichainProposal = async (proposal) => {
try {
isExecuting.value = true;
console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${proposal.chains.length} цепочках`);
// Исполняем параллельно во всех цепочках
const executePromises = proposal.chains.map(async (chain) => {
try {
console.log(`🎯 [MULTI-EXECUTE] Исполняем в ${chain.networkName} (${chain.contractAddress})`);
await executeProposalUtil(chain.contractAddress, chain.id);
console.log(`✅ [MULTI-EXECUTE] Исполнено в ${chain.networkName}`);
} catch (error) {
console.error(`❌ [MULTI-EXECUTE] Ошибка исполнения в ${chain.networkName}:`, error);
// Продолжаем исполнение в других цепочках
}
});
await Promise.all(executePromises);
console.log('🎉 [MULTI-EXECUTE] Исполнение завершено во всех цепочках');
// Перезагружаем предложения
await loadProposals();
} catch (error) {
console.error('[MULTI-EXECUTE] Критическая ошибка:', error);
throw error;
} finally {
isExecuting.value = false;
}
};
const canVoteMultichain = (proposal) => {
// Можно голосовать если есть хотя бы одна активная цепочка
return proposal.chains.some(chain => canVote(chain));
};
const canExecuteMultichain = (proposal) => {
// Можно исполнить только если кворум достигнут во ВСЕХ цепочках
return proposal.chains.every(chain => canExecute(chain));
};
const getChainStatusClass = (chain) => {
if (chain.executed) return 'executed';
if (chain.state === 'active') return 'active';
if (chain.deadline && chain.deadline < Date.now() / 1000) return 'expired';
return 'inactive';
};
const getChainStatusText = (chain) => {
if (chain.executed) return 'Исполнено';
if (chain.state === 'active') return 'Активно';
if (chain.deadline && chain.deadline < Date.now() / 1000) return 'Истекло';
return 'Неактивно';
};
return { return {
// ... существующие поля
proposals, proposals,
filteredProposals, filteredProposals,
isLoading, isLoading,
@@ -529,15 +691,21 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
loadProposals, loadProposals,
filterProposals, filterProposals,
voteOnProposal, voteOnProposal,
voteOnMultichainProposal,
executeProposal, executeProposal,
executeMultichainProposal,
cancelProposal, cancelProposal,
getProposalStatusClass, getProposalStatusClass,
getProposalStatusText, getProposalStatusText,
getQuorumPercentage, getQuorumPercentage,
getRequiredQuorumPercentage, getRequiredQuorumPercentage,
canVote, canVote,
canVoteMultichain,
canExecute, canExecute,
canExecuteMultichain,
canCancel, canCancel,
getChainStatusClass,
getChainStatusText,
updateProposalState, updateProposalState,
// Валидация // Валидация
validationStats, validationStats,

View File

@@ -278,6 +278,11 @@ const routes = [
name: 'management-add-module', name: 'management-add-module',
component: () => import('../views/smartcontracts/AddModuleFormView.vue') component: () => import('../views/smartcontracts/AddModuleFormView.vue')
}, },
{
path: '/management/transfer-tokens',
name: 'management-transfer-tokens',
component: () => import('../views/smartcontracts/TransferTokensFormView.vue')
},
{ {
path: '/management/modules', path: '/management/modules',
name: 'management-modules', name: 'management-modules',

View File

@@ -1,21 +1,9 @@
/**
* 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/VC-HB3-Accelerator
*/
/** /**
* ABI для DLE смарт-контракта * ABI для DLE смарт-контракта
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ * АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
* Для обновления запустите: node backend/scripts/generate-abi.js * Для обновления запустите: node backend/scripts/generate-abi.js
* *
* Последнее обновление: 2025-09-29T18:16:32.027Z * Последнее обновление: 2025-12-29T12:09:15.558Z
*/ */
export const DLE_ABI = [ export const DLE_ABI = [
@@ -31,7 +19,7 @@ export const DLE_ABI = [
"function checkpoints(address account, uint32 pos) returns (tuple)", "function checkpoints(address account, uint32 pos) returns (tuple)",
"function clock() returns (uint48)", "function clock() returns (uint48)",
"function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)", "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 createProposal(string _description, uint256 _duration, bytes _operation, uint256[] _targetChains, uint256 ) returns (uint256)",
"function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)", "function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)",
"function decimals() returns (uint8)", "function decimals() returns (uint8)",
"function delegate(address delegatee)", "function delegate(address delegatee)",

View File

@@ -205,7 +205,6 @@ export async function createProposal(dleAddress, proposalData) {
proposalData.description, proposalData.description,
proposalData.duration, proposalData.duration,
proposalData.operation, proposalData.operation,
proposalData.governanceChainId,
proposalData.targetChains || [], proposalData.targetChains || [],
proposalData.timelockDelay || 0 proposalData.timelockDelay || 0
); );
@@ -216,6 +215,7 @@ export async function createProposal(dleAddress, proposalData) {
console.log('Предложение создано, tx hash:', tx.hash); console.log('Предложение создано, tx hash:', tx.hash);
return { return {
success: true,
proposalId: receipt.logs[0]?.topics[1] || '0', // Извлекаем ID предложения из события proposalId: receipt.logs[0]?.topics[1] || '0', // Извлекаем ID предложения из события
txHash: tx.hash, txHash: tx.hash,
blockNumber: receipt.blockNumber blockNumber: receipt.blockNumber

View File

@@ -20,7 +20,7 @@ import { SiweMessage } from 'siwe';
* Нормализует Ethereum адрес * Нормализует Ethereum адрес
*/ */
const normalizeAddress = (address) => { const normalizeAddress = (address) => {
return ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address); return ethers.getAddress(address);
}; };
/** /**

View File

@@ -21,25 +21,9 @@
<div class="management-container"> <div class="management-container">
<!-- Деплоированные DLE --> <!-- Деплоированные DLE -->
<div class="deployed-dles-section"> <div class="deployed-dles-section">
<div class="section-header">
<div class="header-actions">
<button class="add-dle-btn" @click="openDleManagement()">
<i class="fas fa-plus"></i>
Добавить DLE
</button>
<button class="refresh-btn" @click="loadDeployedDles" :disabled="isLoadingDles">
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingDles }"></i>
{{ isLoadingDles ? 'Загрузка...' : 'Обновить' }}
</button>
</div>
</div>
<div v-if="isLoadingDles" class="loading-dles"> <div v-if="deployedDles.length === 0" class="no-dles">
<p>Загрузка деплоированных DLE...</p>
</div>
<div v-else-if="deployedDles.length === 0" class="no-dles">
<p>Деплоированных DLE пока нет</p> <p>Деплоированных DLE пока нет</p>
<p>Создайте новый DLE на странице <a href="/settings/dle-v2-deploy" class="link">Деплой DLE</a></p> <p>Создайте новый DLE на странице <a href="/settings/dle-v2-deploy" class="link">Деплой DLE</a></p>
</div> </div>
@@ -176,7 +160,6 @@ const router = useRouter();
// Состояние для DLE // Состояние для DLE
const deployedDles = ref([]); const deployedDles = ref([]);
const isLoadingDles = ref(false);
@@ -228,47 +211,46 @@ const openSettings = () => {
// Загрузка деплоированных DLE из блокчейна // Загрузка деплоированных DLE из блокчейна
async function loadDeployedDles() { async function loadDeployedDles() {
try { try {
isLoadingDles.value = true;
console.log('[ManagementView] Начинаем загрузку DLE...'); console.log('[ManagementView] Начинаем загрузку DLE...');
// Сначала получаем список DLE из API // Сначала получаем список DLE из API
const response = await api.get('/dle-v2'); const response = await api.get('/dle-v2');
console.log('[ManagementView] Ответ от API /dle-v2:', response.data); console.log('[ManagementView] Ответ от API /dle-v2:', response.data);
if (response.data.success) { if (response.data.success) {
const dlesFromApi = response.data.data || []; const dlesFromApi = response.data.data || [];
console.log('[ManagementView] DLE из API:', dlesFromApi); console.log('[ManagementView] DLE из API:', dlesFromApi);
if (dlesFromApi.length === 0) { if (dlesFromApi.length === 0) {
console.log('[ManagementView] Нет DLE в API, показываем пустой список'); console.log('[ManagementView] Нет DLE в API, показываем пустой список');
deployedDles.value = []; deployedDles.value = [];
return; return;
} }
// Для каждого DLE читаем актуальные данные из блокчейна // Для каждого DLE читаем актуальные данные из блокчейна
const dlesWithBlockchainData = await Promise.all( const dlesWithBlockchainData = await Promise.all(
dlesFromApi.map(async (dle) => { dlesFromApi.map(async (dle) => {
try { try {
// Используем адрес из deployedNetworks если dleAddress null // Используем адрес из deployedNetworks если dleAddress null
const dleAddress = dle.dleAddress || (dle.deployedNetworks && dle.deployedNetworks.length > 0 ? dle.deployedNetworks[0].address : null); const dleAddress = dle.dleAddress || (dle.deployedNetworks && dle.deployedNetworks.length > 0 ? dle.deployedNetworks[0].address : null);
if (!dleAddress) { if (!dleAddress) {
console.warn(`[ManagementView] Нет адреса для DLE ${dle.deployment_id || 'unknown'}`); console.warn(`[ManagementView] Нет адреса для DLE ${dle.deployment_id || 'unknown'}`);
return dle; return dle;
} }
console.log(`[ManagementView] Читаем данные из блокчейна для ${dleAddress}`); console.log(`[ManagementView] Читаем данные из блокчейна для ${dleAddress}`);
// Читаем данные из блокчейна // Читаем данные из блокчейна
const blockchainResponse = await api.post('/blockchain/read-dle-info', { const blockchainResponse = await api.post('/blockchain/read-dle-info', {
dleAddress: dleAddress dleAddress: dleAddress
}); });
console.log(`[ManagementView] Ответ от блокчейна для ${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;
// Объединяем данные из API с данными из блокчейна // Объединяем данные из API с данными из блокчейна
const combinedDle = { const combinedDle = {
...dle, ...dle,
@@ -288,7 +270,7 @@ async function loadDeployedDles() {
// Количество участников (держателей токенов) // Количество участников (держателей токенов)
participantCount: blockchainData.participantCount || 0 participantCount: blockchainData.participantCount || 0
}; };
console.log(`[ManagementView] Объединенные данные для ${dle.dleAddress}:`, combinedDle); console.log(`[ManagementView] Объединенные данные для ${dle.dleAddress}:`, combinedDle);
return combinedDle; return combinedDle;
} else { } else {
@@ -301,7 +283,7 @@ async function loadDeployedDles() {
} }
}) })
); );
deployedDles.value = dlesWithBlockchainData; deployedDles.value = dlesWithBlockchainData;
console.log('[ManagementView] Итоговый список DLE:', deployedDles.value); console.log('[ManagementView] Итоговый список DLE:', deployedDles.value);
} else { } else {
@@ -311,8 +293,6 @@ async function loadDeployedDles() {
} catch (error) { } catch (error) {
console.error('[ManagementView] Ошибка при загрузке DLE:', error); console.error('[ManagementView] Ошибка при загрузке DLE:', error);
deployedDles.value = []; deployedDles.value = [];
} finally {
isLoadingDles.value = false;
} }
} }
@@ -467,74 +447,6 @@ onMounted(() => {
margin-top: 3rem; margin-top: 3rem;
} }
.section-header {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 2rem;
}
.header-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.add-dle-btn {
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.add-dle-btn:hover {
background: var(--color-primary-dark);
transform: translateY(-1px);
}
.add-dle-btn i {
font-size: 0.875rem;
}
.section-header h2 {
color: var(--color-primary);
margin: 0;
}
.refresh-btn {
background: var(--color-primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
transition: background-color 0.2s;
}
.refresh-btn:hover {
background: var(--color-primary-dark);
transform: translateY(-1px);
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-dles, .loading-dles,
.no-dles { .no-dles {

View File

@@ -161,7 +161,7 @@ import { ref, computed, onMounted, onUnmounted, defineProps, defineEmits, inject
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth'; import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue'; import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js'; import { getDLEInfo } from '../../services/dleV2Service.js';
import { createProposal as createProposalAPI } from '../../services/proposalsService.js'; import { createProposal as createProposalAPI } from '../../services/proposalsService.js';
import { getModuleOperations } from '../../services/moduleOperationsService.js'; import { getModuleOperations } from '../../services/moduleOperationsService.js';
import api from '../../api/axios'; import api from '../../api/axios';
@@ -231,8 +231,11 @@ const isModulesWSConnected = ref(false);
// Функции для открытия отдельных форм операций // Функции для открытия отдельных форм операций
function openTransferForm() { function openTransferForm() {
// TODO: Открыть форму для передачи токенов if (dleAddress.value) {
alert('Форма передачи токенов будет реализована'); router.push(`/management/transfer-tokens?address=${dleAddress.value}`);
} else {
router.push('/management/transfer-tokens');
}
} }
function openAddModuleForm() { function openAddModuleForm() {
@@ -321,9 +324,15 @@ async function loadDleData() {
console.error('Ошибка загрузки DLE:', response.data.error); console.error('Ошибка загрузки DLE:', response.data.error);
} }
// Загружаем поддерживаемые цепочки // Получаем поддерживаемые цепочки из данных DLE
const chainsResponse = await getSupportedChains(dleAddress.value); if (selectedDle.value?.deployedNetworks) {
availableChains.value = chainsResponse.data?.chains || []; availableChains.value = selectedDle.value.deployedNetworks.map(net => ({
chainId: net.chainId,
name: getChainName(net.chainId)
}));
} else {
availableChains.value = [];
}
// Загружаем операции модулей // Загружаем операции модулей
await loadModuleOperations(); await loadModuleOperations();
@@ -497,6 +506,21 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
disconnectModulesWebSocket(); disconnectModulesWebSocket();
}); });
// Функция для получения названия сети по chainId
function getChainName(chainId) {
const chainNames = {
1: 'Ethereum',
11155111: 'Sepolia',
17000: 'Holesky',
421614: 'Arbitrum Sepolia',
84532: 'Base Sepolia',
137: 'Polygon',
56: 'BSC',
42161: 'Arbitrum'
};
return chainNames[chainId] || `Chain ${chainId}`;
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -0,0 +1,788 @@
<!--
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/VC-HB3-Accelerator
-->
<template>
<BaseLayout
:is-authenticated="props.isAuthenticated"
:identities="props.identities"
:token-balances="props.tokenBalances"
:is-loading-tokens="props.isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="transfer-tokens-page">
<!-- Информация для неавторизованных пользователей -->
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div v-if="selectedDle?.dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
{{ selectedDle.dleAddress }}
</div>
<div v-else-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
{{ dleAddress }}
</div>
<div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
Загрузка...
</div>
<button class="close-btn" @click="goBackToProposals">×</button>
</div>
<div v-if="!props.isAuthenticated" class="auth-notice">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
</div>
</div>
<!-- Форма передачи токенов -->
<div v-if="props.isAuthenticated" class="transfer-tokens-form">
<form @submit.prevent="submitForm" class="form-container">
<!-- Адрес отправителя -->
<div class="form-group">
<label for="sender" class="form-label">
<i class="fas fa-paper-plane"></i>
Адрес отправителя *
</label>
<input
type="text"
id="sender"
v-model="formData.sender"
class="form-input"
readonly
required
/>
<small class="form-help">
Ваш подключенный кошелек - токены будут отправлены с этого адреса
</small>
</div>
<!-- Адрес получателя -->
<div class="form-group">
<label for="recipient" class="form-label">
<i class="fas fa-user"></i>
Адрес получателя *
</label>
<input
type="text"
id="recipient"
v-model="formData.recipient"
class="form-input"
placeholder="0x..."
required
/>
<small class="form-help">
Ethereum-адрес получателя токенов DLE
</small>
</div>
<!-- Количество токенов -->
<div class="form-group">
<label for="amount" class="form-label">
<i class="fas fa-coins"></i>
Количество токенов *
</label>
<input
type="number"
id="amount"
v-model.number="formData.amount"
class="form-input"
placeholder="1000000"
min="1"
step="1"
required
/>
<small class="form-help">
Количество токенов для перевода (без decimals)
</small>
<div v-if="dleInfo?.totalSupply" class="balance-info">
<i class="fas fa-info-circle"></i>
Доступный баланс DLE: {{ formatTokenAmount(dleInfo.totalSupply) }} {{ dleInfo.symbol }}
</div>
</div>
<!-- Описание предложения -->
<div class="form-group">
<label for="description" class="form-label">
<i class="fas fa-file-alt"></i>
Описание предложения *
</label>
<textarea
id="description"
v-model="formData.description"
class="form-textarea"
placeholder="Опишите цель перевода токенов..."
rows="3"
required
></textarea>
<small class="form-help">
Подробное описание предложения для голосования
</small>
</div>
<!-- Время голосования -->
<div class="form-group">
<label for="votingDuration" class="form-label">
<i class="fas fa-clock"></i>
Время голосования *
</label>
<select
id="votingDuration"
v-model="formData.votingDuration"
class="form-select"
required
>
<option value="">Выберите время голосования</option>
<option value="3600">1 час</option>
<option value="86400">1 день</option>
<option value="259200">3 дня</option>
<option value="604800">7 дней</option>
<option value="1209600">14 дней</option>
</select>
<small class="form-help">
Время, в течение которого будет проходить голосование
</small>
</div>
<!-- Информация о мульти-чейн развертывании -->
<div v-if="dleInfo?.deployedNetworks && dleInfo.deployedNetworks.length > 1" class="multichain-info">
<i class="fas fa-info-circle"></i>
<strong>Мульти-чейн деплой:</strong> Предложение будет создано для {{ dleInfo.deployedNetworks.length }} сетей: {{
dleInfo.deployedNetworks.map(net => getChainName(net.chainId)).join(', ')
}}. Голосование и исполнение произойдет в каждой сети отдельно.
</div>
<!-- Кнопки -->
<div class="form-actions">
<button type="button" class="btn-secondary" @click="goBackToProposals">
<i class="fas fa-arrow-left"></i>
Назад
</button>
<button type="submit" class="btn-primary" :disabled="isSubmitting">
<i class="fas fa-paper-plane" :class="{ 'fa-spin': isSubmitting }"></i>
{{ isSubmitting ? 'Создание...' : 'Создать предложение' }}
</button>
</div>
</form>
<!-- Результат создания предложений -->
<div v-if="proposalResult" class="proposal-result">
<div class="alert" :class="proposalResult.success ? 'alert-success' : 'alert-danger'">
<i :class="proposalResult.success ? 'fas fa-check-circle' : 'fas fa-exclamation-triangle'"></i>
<strong>{{ proposalResult.success ? 'Успех!' : 'Ошибка!' }}</strong>
<p class="mb-0 mt-2">{{ proposalResult.message }}</p>
</div>
<!-- Детализация по цепочкам -->
<div v-if="proposalResult.results" class="chain-results">
<h5>Результаты по цепочкам:</h5>
<div class="chain-result-list">
<div
v-for="result in proposalResult.results"
:key="result.chainId"
class="chain-result-item"
:class="{ success: result.success, error: !result.success }"
>
<div class="chain-header">
<span class="chain-name">{{ getChainName(result.chainId) }}</span>
<span class="chain-status">
<i :class="result.success ? 'fas fa-check' : 'fas fa-times'"></i>
{{ result.success ? 'Успешно' : 'Ошибка' }}
</span>
</div>
<div v-if="result.success && result.proposalId" class="proposal-info">
<small>ID предложения: {{ result.proposalId }}</small>
<br>
<small>Адрес контракта: {{ shortenAddress(result.contractAddress) }}</small>
</div>
<div v-if="!result.success" class="error-info">
<small>{{ result.error }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted, computed, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import api from '@/api/axios';
import { ethers } from 'ethers';
import { createProposal } from '@/utils/dle-contract';
import { useAuthContext } from '../../composables/useAuth';
// Определяем props
const props = defineProps({
isAuthenticated: { type: Boolean, default: false },
identities: { type: Array, default: () => [] },
tokenBalances: { type: Object, default: () => ({}) },
isLoadingTokens: { type: Boolean, default: false }
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const route = useRoute();
// Получаем контекст аутентификации
const { address: currentUserAddress } = useAuthContext();
// Реактивные данные
const dleAddress = ref(route.query.address || '');
const selectedDle = ref(null);
const isLoadingDle = ref(false);
const dleInfo = ref(null);
const supportedChains = ref([]);
const isSubmitting = ref(false);
const proposalResult = ref(null);
// Форма
const formData = ref({
sender: '',
recipient: '',
amount: null,
description: '',
votingDuration: '',
governanceChain: ''
});
// Загрузка информации о DLE
async function loadDleInfo() {
if (!dleAddress.value) return;
try {
isLoadingDle.value = true;
// Получаем информацию о DLE из блокчейна
const response = await api.post('/blockchain/read-dle-info', {
dleAddress: dleAddress.value
});
if (response.data.success) {
dleInfo.value = response.data.data;
console.log('DLE Info loaded:', dleInfo.value);
// Получаем поддерживаемые цепочки из данных DLE
if (dleInfo.value.deployedNetworks && dleInfo.value.deployedNetworks.length > 0) {
supportedChains.value = dleInfo.value.deployedNetworks.map(net => ({
chainId: net.chainId,
name: getChainName(net.chainId)
}));
} else {
console.warn('No deployed networks found for DLE');
supportedChains.value = [];
}
}
} catch (error) {
console.error('Error loading DLE info:', error);
} finally {
isLoadingDle.value = false;
}
}
// Валидация адреса Ethereum
function isValidAddress(address) {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
// Форматирование количества токенов
function formatTokenAmount(amount) {
if (!amount) return '0';
const num = parseFloat(amount);
if (num === 0) return '0';
if (num < 1) {
return num.toLocaleString('ru-RU', {
minimumFractionDigits: 0,
maximumFractionDigits: 18
});
}
return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
}
// Сокращение адреса
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Получение имени цепочки
function getChainName(chainId) {
const chainNames = {
1: 'Ethereum',
11155111: 'Sepolia',
17000: 'Holesky',
421614: 'Arbitrum Sepolia',
84532: 'Base Sepolia',
137: 'Polygon',
56: 'BSC',
42161: 'Arbitrum'
};
return chainNames[chainId] || `Chain ${chainId}`;
}
// Создание encoded call data для _transferTokens
function encodeTransferTokensCall(sender, recipient, amount) {
// Правильный селектор для _transferTokens(address,address,uint256)
// keccak256("_transferTokens(address,address,uint256)")[:4]
const functionSignature = '_transferTokens(address,address,uint256)';
const selectorBytes = ethers.keccak256(ethers.toUtf8Bytes(functionSignature));
const selector = '0x' + selectorBytes.slice(2, 10);
// Кодирование параметров
const iface = new ethers.Interface([`function ${functionSignature}`]);
const encodedCall = iface.encodeFunctionData('_transferTokens', [sender, recipient, amount]);
return encodedCall;
}
// Отправка формы
async function submitForm() {
try {
isSubmitting.value = true;
proposalResult.value = null;
// Валидация
if (!isValidAddress(formData.value.sender)) {
throw new Error('Некорректный адрес отправителя');
}
// Проверяем, что адрес отправителя совпадает с адресом пользователя
if (formData.value.sender !== currentUserAddress.value) {
throw new Error('Адрес отправителя должен совпадать с вашим подключенным кошельком');
}
if (!isValidAddress(formData.value.recipient)) {
throw new Error('Некорректный адрес получателя');
}
if (!formData.value.amount || formData.value.amount <= 0) {
throw new Error('Некорректное количество токенов');
}
if (!formData.value.description.trim()) {
throw new Error('Описание предложения обязательно');
}
if (!formData.value.votingDuration) {
throw new Error('Выберите время голосования');
}
// Создание encoded call data для передачи токенов
const transferCallData = encodeTransferTokensCall(
formData.value.sender,
formData.value.recipient,
formData.value.amount
);
// Получаем все поддерживаемые цепочки из DLE информации
const allChains = dleInfo.value?.deployedNetworks
? dleInfo.value.deployedNetworks.map(net => net.chainId)
: [];
console.log('Creating proposals in chains:', allChains);
// Создаем предложения параллельно во всех цепочках
const proposalPromises = allChains.map(async (chainId) => {
try {
const proposalData = {
description: formData.value.description,
duration: parseInt(formData.value.votingDuration),
operation: transferCallData,
targetChains: [chainId], // Операция выполняется в той же цепочке
timelockDelay: 0
};
console.log(`Creating proposal in chain ${chainId}:`, proposalData);
// Получаем адрес контракта для этой цепочки
const networkInfo = dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId);
const contractAddress = networkInfo?.address || dleAddress.value;
const result = await createProposal(contractAddress, proposalData);
return {
chainId,
success: result.success,
proposalId: result.proposalId,
error: result.error,
contractAddress
};
} catch (error) {
console.error(`Error creating proposal in chain ${chainId}:`, error);
return {
chainId,
success: false,
error: error.message,
contractAddress: dleAddress.value
};
}
});
const results = await Promise.all(proposalPromises);
// Проверяем результаты
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
if (successful.length > 0) {
proposalResult.value = {
success: true,
message: `Предложения созданы в ${successful.length} из ${allChains.length} цепочек!`,
results: results,
successfulChains: successful,
failedChains: failed
};
// Очистка формы только при полном успехе
if (failed.length === 0) {
formData.value = {
sender: '',
recipient: '',
amount: null,
description: '',
votingDuration: '',
governanceChain: ''
};
}
} else {
throw new Error('Не удалось создать предложения ни в одной цепочке');
}
} catch (error) {
console.error('Error creating transfer proposals:', error);
proposalResult.value = {
success: false,
message: error.message || 'Произошла ошибка при создании предложений'
};
} finally {
isSubmitting.value = false;
}
}
// Навигация
function goBackToProposals() {
if (dleAddress.value) {
router.push(`/management/create-proposal?address=${dleAddress.value}`);
} else {
router.push('/management/create-proposal');
}
}
// Инициализация
// Watcher для автоматического обновления адреса отправителя
watch(currentUserAddress, (newAddress) => {
formData.value.sender = newAddress;
});
onMounted(() => {
console.log('[TransferTokensFormView] currentUserAddress:', currentUserAddress.value);
// Автоматически устанавливаем адрес отправителя
formData.value.sender = currentUserAddress.value;
console.log('[TransferTokensFormView] formData.sender set to:', formData.value.sender);
loadDleInfo();
});
</script>
<style scoped>
.transfer-tokens-page {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.transfer-tokens-form {
margin-top: 2rem;
}
.form-container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: 600;
color: var(--color-primary);
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.form-label i {
margin-right: 0.5rem;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9rem;
transition: border-color 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-help {
display: block;
color: #6c757d;
font-size: 0.8rem;
margin-top: 0.25rem;
}
.balance-info {
margin-top: 0.5rem;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
font-size: 0.85rem;
color: #495057;
}
.balance-info i {
margin-right: 0.5rem;
color: #17a2b8;
}
.multichain-info {
margin-top: 0.5rem;
padding: 0.75rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 6px;
font-size: 0.85rem;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.multichain-info i {
margin-right: 0.5rem;
color: #fff;
}
.multichain-info strong {
color: #fff;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
.btn-primary,
.btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-1px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
.btn-primary:disabled,
.btn-secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.proposal-result {
margin-top: 2rem;
}
.alert {
padding: 1rem;
border-radius: 6px;
border: 1px solid transparent;
}
.alert-success {
background: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.alert-danger {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.alert i {
margin-right: 0.5rem;
}
.chain-results {
margin-top: 1.5rem;
}
.chain-results h5 {
margin-bottom: 1rem;
color: var(--color-primary);
font-size: 1rem;
}
.chain-result-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.chain-result-item {
padding: 0.75rem;
border-radius: 6px;
border: 1px solid #e9ecef;
background: white;
}
.chain-result-item.success {
border-color: #d4edda;
background: #f8fff9;
}
.chain-result-item.error {
border-color: #f5c6cb;
background: #fff8f8;
}
.chain-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.chain-name {
font-weight: 600;
color: var(--color-primary);
}
.chain-status {
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.chain-status i {
font-size: 0.75rem;
}
.proposal-info {
color: #6c757d;
font-size: 0.8rem;
}
.error-info {
color: #dc3545;
font-size: 0.8rem;
}
.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;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
.auth-notice {
margin-top: 2rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.transfer-tokens-page {
padding: 15px;
}
.form-container {
padding: 1.5rem;
}
.form-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -2506,9 +2506,9 @@ prelude-ls@^1.2.1:
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-linter-helpers@^1.0.0: prettier-linter-helpers@^1.0.0:
version "1.0.0" version "1.0.1"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==
dependencies: dependencies:
fast-diff "^1.1.2" fast-diff "^1.1.2"